Merge branch 'master' into span-attributes

This commit is contained in:
Felix Hildén 2025-11-22 14:05:13 +02:00
commit 913e6ffd70
78 changed files with 3770 additions and 597 deletions

221
.github/workflows/auto-release-pr.yml vendored Normal file
View file

@ -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');

View file

@ -65,7 +65,7 @@ jobs:
actions: write # to allow uploading artifacts and cache actions: write # to allow uploading artifacts and cache
steps: steps:
- name: Harden the runner (Audit all outbound calls) - 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: with:
egress-policy: audit egress-policy: audit
@ -73,7 +73,7 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Go - name: Install Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: ${{ matrix.GO_SEMVER }} go-version: ${{ matrix.GO_SEMVER }}
check-latest: true check-latest: true
@ -162,7 +162,7 @@ jobs:
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
steps: steps:
- name: Harden the runner (Audit all outbound calls) - 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: with:
egress-policy: audit egress-policy: audit
allowed-endpoints: ci-s390x.caddyserver.com:22 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]' if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]'
steps: steps:
- name: Harden the runner (Audit all outbound calls) - 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: with:
egress-policy: audit egress-policy: audit
@ -233,7 +233,7 @@ jobs:
version: latest version: latest
args: check args: check
- name: Install Go - name: Install Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: "~1.25" go-version: "~1.25"
check-latest: true check-latest: true

View file

@ -51,7 +51,7 @@ jobs:
continue-on-error: true continue-on-error: true
steps: steps:
- name: Harden the runner (Audit all outbound calls) - 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: with:
egress-policy: audit egress-policy: audit
@ -59,7 +59,7 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Go - name: Install Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: ${{ matrix.GO_SEMVER }} go-version: ${{ matrix.GO_SEMVER }}
check-latest: true check-latest: true

View file

@ -45,12 +45,12 @@ jobs:
steps: steps:
- name: Harden the runner (Audit all outbound calls) - 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: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: '~1.25' go-version: '~1.25'
check-latest: true check-latest: true
@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden the runner (Audit all outbound calls) - 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: with:
egress-policy: audit egress-policy: audit
@ -90,14 +90,14 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Harden the runner (Audit all outbound calls) - 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: with:
egress-policy: audit egress-policy: audit
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@595b5aeba73380359d98a5e087f648dbb0edce1b # v4.7.3 uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
with: with:
comment-summary-in-pr: on-failure comment-summary-in-pr: on-failure
# https://github.com/actions/dependency-review-action/issues/430#issuecomment-1468975566 # https://github.com/actions/dependency-review-action/issues/430#issuecomment-1468975566

248
.github/workflows/release-proposal.yml vendored Normal file
View file

@ -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<<EOF" >> $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

View file

@ -13,8 +13,322 @@ permissions:
contents: read contents: read
jobs: 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: release:
name: Release name: Release
needs: verify-tag
if: ${{ needs.verify-tag.outputs.verification_passed == 'true' }}
strategy: strategy:
matrix: matrix:
os: os:
@ -36,10 +350,12 @@ jobs:
# https://docs.github.com/en/rest/overview/permissions-required-for-github-apps#permission-on-contents # 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` # "Releases" is part of `contents`, so it needs the `write`
contents: write contents: write
issues: write
pull-requests: write
steps: steps:
- name: Harden the runner (Audit all outbound calls) - 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: with:
egress-policy: audit egress-policy: audit
@ -49,7 +365,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Install Go - name: Install Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: ${{ matrix.GO_SEMVER }} go-version: ${{ matrix.GO_SEMVER }}
check-latest: true check-latest: true
@ -98,22 +414,12 @@ jobs:
- name: Install Cloudsmith CLI - name: Install Cloudsmith CLI
run: pip install --upgrade 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 - name: Install Cosign
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # main uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # main
- name: Cosign version - name: Cosign version
run: cosign version run: cosign version
- name: Install Syft - name: Install Syft
uses: anchore/sbom-action/download-syft@da167eac915b4e86f08b264dbdbc867b61be6f0c # main uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # main
- name: Syft version - name: Syft version
run: syft version run: syft version
- name: Install xcaddy - name: Install xcaddy
@ -188,3 +494,72 @@ jobs:
echo "Pushing $filename to 'testing'" echo "Pushing $filename to 'testing'"
cloudsmith push deb caddy/testing/any-distro/any-version $filename cloudsmith push deb caddy/testing/any-distro/any-version $filename
done 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}`);
}
}

View file

@ -24,12 +24,12 @@ jobs:
# See https://github.com/peter-evans/repository-dispatch # See https://github.com/peter-evans/repository-dispatch
- name: Harden the runner (Audit all outbound calls) - 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: with:
egress-policy: audit egress-policy: audit
- name: Trigger event on caddyserver/dist - 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: with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }} token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/dist repository: caddyserver/dist
@ -37,7 +37,7 @@ jobs:
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}' client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
- name: Trigger event on caddyserver/caddy-docker - 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: with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }} token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/caddy-docker repository: caddyserver/caddy-docker

View file

@ -37,7 +37,7 @@ jobs:
steps: steps:
- name: Harden the runner (Audit all outbound calls) - 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: with:
egress-policy: audit egress-policy: audit
@ -47,7 +47,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: "Run analysis" - name: "Run analysis"
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with: with:
results_file: results.sarif results_file: results.sarif
results_format: sarif results_format: sarif
@ -81,6 +81,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard (optional). # Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard # Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning" - 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: with:
sarif_file: results.sarif sarif_file: results.sarif

View file

@ -1029,6 +1029,13 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
return err 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: default:
return APIError{ return APIError{
HTTPStatus: http.StatusMethodNotAllowed, HTTPStatus: http.StatusMethodNotAllowed,

View file

@ -975,11 +975,11 @@ func Version() (simple, full string) {
if CustomVersion != "" { if CustomVersion != "" {
full = CustomVersion full = CustomVersion
simple = CustomVersion simple = CustomVersion
return return simple, full
} }
full = "unknown" full = "unknown"
simple = "unknown" simple = "unknown"
return return simple, full
} }
// find the Caddy module in the dependency list // find the Caddy module in the dependency list
for _, dep := range bi.Deps { 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. // Event represents something that has happened or is happening.
@ -1197,6 +1197,91 @@ var (
rawCfgMu sync.RWMutex 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 <file> --adapter <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 // errSameConfig is returned if the new config is the same
// as the old one. This isn't usually an actual, actionable // as the old one. This isn't usually an actual, actionable
// error; it's mostly a sentinel value. // error; it's mostly a sentinel value.

View file

@ -52,17 +52,16 @@ func Format(input []byte) []byte {
newLines int // count of newlines consumed newLines int // count of newlines consumed
comment bool // whether we're in a comment comment bool // whether we're in a comment
quoted bool // whether we're in a quoted segment quotes string // encountered quotes ('', '`', '"', '"`', '`"')
escaped bool // whether current char is escaped escaped bool // whether current char is escaped
heredoc heredocState // whether we're in a heredoc heredoc heredocState // whether we're in a heredoc
heredocEscaped bool // whether heredoc is escaped heredocEscaped bool // whether heredoc is escaped
heredocMarker []rune heredocMarker []rune
heredocClosingMarker []rune heredocClosingMarker []rune
nesting int // indentation level nesting int // indentation level
withinBackquote bool
) )
write := func(ch rune) { write := func(ch rune) {
@ -89,12 +88,8 @@ func Format(input []byte) []byte {
} }
panic(err) panic(err)
} }
if ch == '`' {
withinBackquote = !withinBackquote
}
// detect whether we have the start of a heredoc // detect whether we have the start of a heredoc
if !quoted && (heredoc == heredocClosed && !heredocEscaped) && if quotes == "" && (heredoc == heredocClosed && !heredocEscaped) &&
space && last == '<' && ch == '<' { space && last == '<' && ch == '<' {
write(ch) write(ch)
heredoc = heredocOpening heredoc = heredocOpening
@ -180,16 +175,38 @@ func Format(input []byte) []byte {
continue continue
} }
if quoted { if ch == '`' {
switch quotes {
case "\"`":
quotes = "\""
case "`":
quotes = ""
case "\"":
quotes = "\"`"
default:
quotes = "`"
}
}
if quotes == "\"" {
if ch == '"' { if ch == '"' {
quoted = false quotes = ""
} }
write(ch) write(ch)
continue continue
} }
if space && ch == '"' { if ch == '"' {
quoted = true switch quotes {
case "":
if space {
quotes = "\""
}
case "`\"":
quotes = "`"
case "\"`":
quotes = ""
}
} }
if unicode.IsSpace(ch) { if unicode.IsSpace(ch) {
@ -245,7 +262,7 @@ func Format(input []byte) []byte {
write(' ') write(' ')
} }
openBraceWritten = false openBraceWritten = false
if withinBackquote { if quotes == "`" {
write('{') write('{')
openBraceWritten = true openBraceWritten = true
continue continue
@ -253,7 +270,7 @@ func Format(input []byte) []byte {
continue continue
case ch == '}' && (spacePrior || !openBrace): case ch == '}' && (spacePrior || !openBrace):
if withinBackquote { if quotes == "`" {
write('}') write('}')
continue continue
} }

View file

@ -444,6 +444,11 @@ block2 {
input: "block {respond \"All braces should remain: {{now | date `2006`}}\"}", input: "block {respond \"All braces should remain: {{now | date `2006`}}\"}",
expect: "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", description: "No trailing space on line before env variable",
input: `{ input: `{

View file

@ -91,7 +91,7 @@ func parseBind(h Helper) ([]ConfigValue, error) {
// curves <curves...> // curves <curves...>
// client_auth { // client_auth {
// mode [request|require|verify_if_given|require_and_verify] // mode [request|require|verify_if_given|require_and_verify]
// trust_pool <module_name> [...] // trust_pool <module_name> [...]
// trusted_leaf_cert <base64_der> // trusted_leaf_cert <base64_der>
// trusted_leaf_cert_file <filename> // trusted_leaf_cert_file <filename>
// } // }
@ -481,7 +481,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
// Validate DNS challenge config: any DNS challenge option except "dns" requires a DNS provider // Validate DNS challenge config: any DNS challenge option except "dns" requires a DNS provider
if acmeIssuer != nil && acmeIssuer.Challenges != nil && acmeIssuer.Challenges.DNS != nil { if acmeIssuer != nil && acmeIssuer.Challenges != nil && acmeIssuer.Challenges.DNS != nil {
dnsCfg := acmeIssuer.Challenges.DNS 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 { if len(dnsOptionsSet) > 0 && !providerSet {
return nil, h.Errf( return nil, h.Errf(
"setting DNS challenge options [%s] requires a DNS provider (set with the 'dns' subdirective or 'acme_dns' global option)", "setting DNS challenge options [%s] requires a DNS provider (set with the 'dns' subdirective or 'acme_dns' global option)",

View file

@ -18,6 +18,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"slices" "slices"
"strconv"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
@ -42,12 +43,15 @@ type serverOptions struct {
WriteTimeout caddy.Duration WriteTimeout caddy.Duration
IdleTimeout caddy.Duration IdleTimeout caddy.Duration
KeepAliveInterval caddy.Duration KeepAliveInterval caddy.Duration
KeepAliveIdle caddy.Duration
KeepAliveCount int
MaxHeaderBytes int MaxHeaderBytes int
EnableFullDuplex bool EnableFullDuplex bool
Protocols []string Protocols []string
StrictSNIHost *bool StrictSNIHost *bool
TrustedProxiesRaw json.RawMessage TrustedProxiesRaw json.RawMessage
TrustedProxiesStrict int TrustedProxiesStrict int
TrustedProxiesUnix bool
ClientIPHeaders []string ClientIPHeaders []string
ShouldLogCredentials bool ShouldLogCredentials bool
Metrics *caddyhttp.Metrics Metrics *caddyhttp.Metrics
@ -142,6 +146,7 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val()) return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
} }
} }
case "keepalive_interval": case "keepalive_interval":
if !d.NextArg() { if !d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
@ -152,6 +157,26 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
} }
serverOpts.KeepAliveInterval = caddy.Duration(dur) 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": case "max_header_size":
var sizeStr string var sizeStr string
if !d.AllArgs(&sizeStr) { if !d.AllArgs(&sizeStr) {
@ -227,6 +252,12 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
} }
serverOpts.TrustedProxiesStrict = 1 serverOpts.TrustedProxiesStrict = 1
case "trusted_proxies_unix":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.TrustedProxiesUnix = true
case "client_ip_headers": case "client_ip_headers":
headers := d.RemainingArgs() headers := d.RemainingArgs()
for _, header := range headers { for _, header := range headers {
@ -309,6 +340,8 @@ func applyServerOptions(
server.WriteTimeout = opts.WriteTimeout server.WriteTimeout = opts.WriteTimeout
server.IdleTimeout = opts.IdleTimeout server.IdleTimeout = opts.IdleTimeout
server.KeepAliveInterval = opts.KeepAliveInterval server.KeepAliveInterval = opts.KeepAliveInterval
server.KeepAliveIdle = opts.KeepAliveIdle
server.KeepAliveCount = opts.KeepAliveCount
server.MaxHeaderBytes = opts.MaxHeaderBytes server.MaxHeaderBytes = opts.MaxHeaderBytes
server.EnableFullDuplex = opts.EnableFullDuplex server.EnableFullDuplex = opts.EnableFullDuplex
server.Protocols = opts.Protocols server.Protocols = opts.Protocols
@ -316,6 +349,7 @@ func applyServerOptions(
server.TrustedProxiesRaw = opts.TrustedProxiesRaw server.TrustedProxiesRaw = opts.TrustedProxiesRaw
server.ClientIPHeaders = opts.ClientIPHeaders server.ClientIPHeaders = opts.ClientIPHeaders
server.TrustedProxiesStrict = opts.TrustedProxiesStrict server.TrustedProxiesStrict = opts.TrustedProxiesStrict
server.TrustedProxiesUnix = opts.TrustedProxiesUnix
server.Metrics = opts.Metrics server.Metrics = opts.Metrics
if opts.ShouldLogCredentials { if opts.ShouldLogCredentials {
if server.Logs == nil { if server.Logs == nil {

View file

@ -554,6 +554,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
globalPreferredChains := options["preferred_chains"] globalPreferredChains := options["preferred_chains"]
globalCertLifetime := options["cert_lifetime"] globalCertLifetime := options["cert_lifetime"]
globalHTTPPort, globalHTTPSPort := options["http_port"], options["https_port"] globalHTTPPort, globalHTTPSPort := options["http_port"], options["https_port"]
globalDefaultBind := options["default_bind"]
if globalEmail != nil && acmeIssuer.Email == "" { if globalEmail != nil && acmeIssuer.Email == "" {
acmeIssuer.Email = globalEmail.(string) 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)) { if globalACMECARoot != nil && !slices.Contains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
acmeIssuer.TrustedRootsPEMFiles = append(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"] globalDNS := options["dns"]
if globalDNS != nil { if globalDNS == nil && globalACMEDNS == nil {
// If global `dns` is set, do NOT set provider in issuer, just set empty dns config
acmeIssuer.Challenges = &caddytls.ChallengesConfig{
DNS: &caddytls.DNSChallengeConfig{},
}
} else if globalACMEDNS != nil {
// Set a global DNS provider if `acme_dns` is set and `dns` is NOT set
acmeIssuer.Challenges = &caddytls.ChallengesConfig{
DNS: &caddytls.DNSChallengeConfig{
ProviderRaw: caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil),
},
}
} else {
return fmt.Errorf("acme_dns specified without DNS provider config, but no provider specified with 'dns' global option") 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 { if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB) 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) 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 { if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration) acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
} }

View file

@ -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") caddy.Log().Named("admin.api").Info("load complete")
return nil return nil

View file

@ -53,6 +53,7 @@ example.com {
"challenges": { "challenges": {
"dns": { "dns": {
"provider": { "provider": {
"argument": "foo",
"name": "mock" "name": "mock"
} }
} }

View file

@ -18,6 +18,9 @@
trusted_proxies static private_ranges trusted_proxies static private_ranges
client_ip_headers Custom-Real-Client-IP X-Forwarded-For client_ip_headers Custom-Real-Client-IP X-Forwarded-For
client_ip_headers A-Third-One 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, "read_header_timeout": 30000000000,
"write_timeout": 30000000000, "write_timeout": 30000000000,
"idle_timeout": 30000000000, "idle_timeout": 30000000000,
"keepalive_interval": 20000000000,
"keepalive_idle": 20000000000,
"keepalive_count": 10,
"max_header_bytes": 100000000, "max_header_bytes": 100000000,
"enable_full_duplex": true, "enable_full_duplex": true,
"routes": [ "routes": [

View file

@ -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"
}
}
}
}
}
}

View file

@ -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
}
}
}
}
}

View file

@ -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"
}
]
}
]
}
}
}
}

View file

@ -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"
}
}
}
}

View file

@ -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"
}
]
}
]
}
}
}
}

View file

@ -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)
}
}
}

View file

@ -15,7 +15,9 @@ func init() {
} }
// MockDNSProvider is a mock DNS provider, for testing config with DNS modules. // 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. // CaddyModule returns the Caddy module information.
func (MockDNSProvider) CaddyModule() caddy.ModuleInfo { func (MockDNSProvider) CaddyModule() caddy.ModuleInfo {
@ -31,7 +33,15 @@ func (MockDNSProvider) Provision(ctx caddy.Context) error {
} }
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. // 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 return nil
} }

View file

@ -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 // 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 configFile string
var adapterUsed string
if !resumeFlag { if !resumeFlag {
config, configFile, err = LoadConfig(configFlag, configAdapterFlag) config, configFile, adapterUsed, err = LoadConfig(configFlag, configAdapterFlag)
if err != nil { if err != nil {
logBuffer.FlushTo(defaultLogger) logBuffer.FlushTo(defaultLogger)
return caddy.ExitCodeFailedStartup, err 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 // run the initial config
err = caddy.Load(config, true) err = caddy.Load(config, true)
if err != nil { if err != nil {
@ -295,7 +309,7 @@ func cmdRun(fl Flags) (int, error) {
// if enabled, reload config file automatically on changes // if enabled, reload config file automatically on changes
// (this better only be used in dev!) // (this better only be used in dev!)
if watchFlag { if watchFlag {
go watchConfigFile(configFile, configAdapterFlag) go watchConfigFile(configFile, adapterUsed)
} }
// warn if the environment does not provide enough information about the disk // 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") forceFlag := fl.Bool("force")
// get the config in caddy's native format // get the config in caddy's native format
config, configFile, err := LoadConfig(configFlag, configAdapterFlag) config, configFile, adapterUsed, err := LoadConfig(configFlag, configAdapterFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} }
@ -368,6 +382,10 @@ func cmdReload(fl Flags) (int, error) {
if forceFlag { if forceFlag {
headers.Set("Cache-Control", "must-revalidate") 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)) resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
if err != nil { 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)") 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 { if err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} }
@ -797,7 +815,7 @@ func DetermineAdminAPIAddress(address string, config []byte, configFile, configA
loadedConfig := config loadedConfig := config
if len(loadedConfig) == 0 { if len(loadedConfig) == 0 {
// get the config in caddy's native format // get the config in caddy's native format
loadedConfig, loadedConfigFile, err = LoadConfig(configFile, configAdapter) loadedConfig, loadedConfigFile, _, err = LoadConfig(configFile, configAdapter)
if err != nil { if err != nil {
return "", err return "", err
} }

View file

@ -100,7 +100,12 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
// there is no config available. It prints any warnings to stderr, // there is no config available. It prints any warnings to stderr,
// and returns the resulting JSON config bytes along with // and returns the resulting JSON config bytes along with
// the name of the loaded config file (if any). // 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) return loadConfigWithLogger(caddy.Log(), configFile, adapterName)
} }
@ -138,7 +143,7 @@ func isCaddyfile(configFile, adapterName string) (bool, error) {
return false, nil 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 // if no logger is provided, use a nop logger
// just so we don't have to check for nil // just so we don't have to check for nil
if logger == 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 // specifying an adapter without a config file is ambiguous
if adapterName != "" && configFile == "" { 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 // load initial config and adapter
@ -158,13 +163,13 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
if configFile == "-" { if configFile == "-" {
config, err = io.ReadAll(os.Stdin) config, err = io.ReadAll(os.Stdin)
if err != nil { 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") logger.Info("using config from stdin")
} else { } else {
config, err = os.ReadFile(configFile) config, err = os.ReadFile(configFile)
if err != nil { 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)) logger.Info("using config from file", zap.String("file", configFile))
} }
@ -179,7 +184,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
cfgAdapter = nil cfgAdapter = nil
} else if err != nil { } else if err != nil {
// default Caddyfile exists, but error reading it // 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 { } else {
// success reading default Caddyfile // success reading default Caddyfile
configFile = "Caddyfile" configFile = "Caddyfile"
@ -191,14 +196,14 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
if yes, err := isCaddyfile(configFile, adapterName); yes { if yes, err := isCaddyfile(configFile, adapterName); yes {
adapterName = "caddyfile" adapterName = "caddyfile"
} else if err != nil { } else if err != nil {
return nil, "", err return nil, "", "", err
} }
// load config adapter // load config adapter
if adapterName != "" { if adapterName != "" {
cfgAdapter = caddyconfig.GetAdapter(adapterName) cfgAdapter = caddyconfig.GetAdapter(adapterName)
if cfgAdapter == nil { 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, "filename": configFile,
}) })
if err != nil { 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)) logger.Info("adapted config to JSON", zap.String("adapter", adapterName))
for _, warn := range warnings { 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 // validate that the config is at least valid JSON
err = json.Unmarshal(config, new(any)) err = json.Unmarshal(config, new(any))
if err != nil { 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 // watchConfigFile watches the config file at filename for changes
@ -256,7 +261,7 @@ func watchConfigFile(filename, adapterName string) {
} }
// get current config // get current config
lastCfg, _, err := loadConfigWithLogger(nil, filename, adapterName) lastCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName)
if err != nil { if err != nil {
logger().Error("unable to load latest config", zap.Error(err)) logger().Error("unable to load latest config", zap.Error(err))
return return
@ -268,7 +273,7 @@ func watchConfigFile(filename, adapterName string) {
//nolint:staticcheck //nolint:staticcheck
for range time.Tick(1 * time.Second) { for range time.Tick(1 * time.Second) {
// get current config // get current config
newCfg, _, err := loadConfigWithLogger(nil, filename, adapterName) newCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName)
if err != nil { if err != nil {
logger().Error("unable to load latest config", zap.Error(err)) logger().Error("unable to load latest config", zap.Error(err))
return return

View file

@ -62,7 +62,7 @@ func splitModule(arg string) (module, version string, err error) {
err = fmt.Errorf("module name is required") err = fmt.Errorf("module name is required")
} }
return return module, version, err
} }
func cmdAddPackage(fl Flags) (int, error) { func cmdAddPackage(fl Flags) (int, error) {
@ -217,7 +217,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
bi, ok := debug.ReadBuildInfo() bi, ok := debug.ReadBuildInfo()
if !ok { if !ok {
err = fmt.Errorf("no build info") err = fmt.Errorf("no build info")
return return standard, nonstandard, unknown, err
} }
for _, modID := range caddy.Modules() { for _, modID := range caddy.Modules() {
@ -260,7 +260,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
nonstandard = append(nonstandard, caddyModGoMod) nonstandard = append(nonstandard, caddyModGoMod)
} }
} }
return return standard, nonstandard, unknown, err
} }
func listModules(path string) error { func listModules(path string) error {

View file

@ -36,7 +36,7 @@ type storVal struct {
// determineStorage returns the top-level storage module from the given config. // determineStorage returns the top-level storage module from the given config.
// It may return nil even if no error. // It may return nil even if no error.
func determineStorage(configFile string, configAdapter string) (*storVal, error) { func determineStorage(configFile string, configAdapter string) (*storVal, error) {
cfg, _, err := LoadConfig(configFile, configAdapter) cfg, _, _, err := LoadConfig(configFile, configAdapter)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -21,12 +21,14 @@ import (
"log" "log"
"log/slog" "log/slog"
"reflect" "reflect"
"sync"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/collectors"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/exp/zapslog" "go.uber.org/zap/exp/zapslog"
"go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2/internal/filesystems" "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) 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 // Slogger returns a slog logger that is intended for use by
// the most recent module associated with the context. // the most recent module associated with the context.
func (ctx Context) Slogger() *slog.Logger { func (ctx Context) Slogger() *slog.Logger {
var (
handler slog.Handler
core zapcore.Core
moduleID string
)
if ctx.cfg == nil { if ctx.cfg == nil {
// often the case in tests; just use a dev logger // often the case in tests; just use a dev logger
l, err := zap.NewDevelopment() l, err := zap.NewDevelopment()
if err != nil { if err != nil {
panic("config missing, unable to create dev logger: " + err.Error()) 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 { slogHandlerFactoriesMu.RLock()
return slog.New(zapslog.NewHandler(Log().Core())) for _, f := range slogHandlerFactories {
handler = f(handler, core, moduleID)
} }
return slog.New(zapslog.NewHandler(ctx.cfg.Logging.Logger(mod).Core(), slogHandlerFactoriesMu.RUnlock()
zapslog.WithName(string(mod.CaddyModule().ID)),
)) return slog.New(handler)
} }
// Modules returns the lineage of modules that this context provisioned, // Modules returns the lineage of modules that this context provisioned,

85
go.mod
View file

@ -4,7 +4,8 @@ go 1.25
require ( require (
github.com/BurntSushi/toml v1.5.0 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/Masterminds/sprig/v3 v3.3.0
github.com/alecthomas/chroma/v2 v2.20.0 github.com/alecthomas/chroma/v2 v2.20.0
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b 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/go-chi/chi/v5 v5.2.3
github.com/google/cel-go v0.26.1 github.com/google/cel-go v0.26.1
github.com/google/uuid v1.6.0 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/klauspost/cpuid/v2 v2.3.0
github.com/mholt/acmez/v3 v3.1.3 github.com/mholt/acmez/v3 v3.1.4
github.com/prometheus/client_golang v1.23.0 github.com/prometheus/client_golang v1.23.2
github.com/quic-go/quic-go v0.54.0 github.com/quic-go/quic-go v0.57.0
github.com/smallstep/certificates v0.28.4 github.com/smallstep/certificates v0.28.4
github.com/smallstep/nosql v0.7.0 github.com/smallstep/nosql v0.7.0
github.com/smallstep/truststore v0.13.0 github.com/smallstep/truststore v0.13.0
github.com/spf13/cobra v1.10.1 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/stretchr/testify v1.11.1
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53
github.com/yuin/goldmark v1.7.13 github.com/yuin/goldmark v1.7.13
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc 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/instrumentation/net/http/otelhttp v0.63.0
go.opentelemetry.io/contrib/propagators/autoprop v0.63.0 go.opentelemetry.io/contrib/propagators/autoprop v0.63.0
go.opentelemetry.io/otel v1.38.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.opentelemetry.io/otel/sdk v1.38.0
go.uber.org/automaxprocs v1.6.0 go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
go.uber.org/zap/exp v0.3.0 go.uber.org/zap/exp v0.3.0
golang.org/x/crypto v0.41.0 golang.org/x/crypto v0.45.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99
golang.org/x/net v0.43.0 golang.org/x/net v0.47.0
golang.org/x/sync v0.16.0 golang.org/x/sync v0.18.0
golang.org/x/term v0.34.0 golang.org/x/term v0.37.0
golang.org/x/time v0.12.0 golang.org/x/time v0.14.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
cel.dev/expr v0.24.0 // indirect 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/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 dario.cat/mergo v1.0.1 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.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/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/coreos/go-oidc/v3 v3.14.1 // indirect github.com/coreos/go-oidc/v3 v3.14.1 // indirect
github.com/davecgh/go-spew v1.1.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/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // 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/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/go-tspi v0.3.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // 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/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pmezard/go-difflib v1.0.0 // 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/cli-utils v0.12.1 // indirect
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca // indirect github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca // indirect
github.com/smallstep/linkedca v0.23.0 // indirect github.com/smallstep/linkedca v0.23.0 // indirect
@ -85,16 +86,30 @@ require (
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // 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/aws v1.38.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 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/jaeger v1.38.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.38.0 // indirect go.opentelemetry.io/contrib/propagators/ot v1.38.0 // indirect
go.uber.org/mock v0.5.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
google.golang.org/api v0.247.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/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 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/pires/go-proxyproto v0.8.1
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.6.2 github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/common v0.67.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect github.com/prometheus/procfs v0.17.0 // indirect
github.com/rs/xid v1.6.0 // indirect github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.3 // 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/spf13/cast v1.7.0 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/urfave/cli v1.22.17 // 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/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 go.opentelemetry.io/otel/trace v1.38.0
go.opentelemetry.io/proto/otlp v1.7.1 // indirect 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 go.uber.org/multierr v1.11.0 // indirect
golang.org/x/mod v0.26.0 // indirect golang.org/x/mod v0.29.0 // indirect
golang.org/x/sys v0.35.0 golang.org/x/sys v0.38.0
golang.org/x/text v0.28.0 // indirect golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.35.0 // indirect golang.org/x/tools v0.38.0 // indirect
google.golang.org/grpc v1.75.0 // indirect google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect google.golang.org/protobuf v1.36.10 // indirect
howett.net/plist v1.0.0 // indirect howett.net/plist v1.0.0 // indirect
) )

376
go.sum
View file

@ -1,39 +1,32 @@
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= 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 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= 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.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M= 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 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= 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.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= 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 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= 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.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k=
cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8= 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 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= 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 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 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 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 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 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= 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 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 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/KimMachineGun/automemlimit v0.7.4 h1:UY7QYOIfrr3wjjOAqahFmC3IaQCLWvur9nmfIn6LnWk= github.com/DeRuina/timberjack v1.3.9 h1:6UXZ1I7ExPGTX/1UNYawR58LlOJUHKBPiYC7WQ91eBo=
github.com/KimMachineGun/automemlimit v0.7.4/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= 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 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= 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.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 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 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 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= 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/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 h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= 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.39.5 h1:e/SXuia3rkFtapghJROrydtQpfQaaUgd1cUvyO1mp2w=
github.com/aws/aws-sdk-go-v2 v1.38.0/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= 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.0 h1:9yH0xiY5fUnVNLRWO0AtayqwU1ndriZdN78LlhruJR4= github.com/aws/aws-sdk-go-v2/config v1.31.16 h1:E4Tz+tJiPc7kGnXwIfCyUj6xHJNpENlY11oKpRTgsjc=
github.com/aws/aws-sdk-go-v2/config v1.31.0/go.mod h1:VeV3K72nXnhbe4EuxxhzsDc/ByrCSlZwUnWH52Nde/I= 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.4 h1:IPd0Algf1b+Qy9BcDp0sCUcIWdCQPSzDoMK3a8pcbUM= github.com/aws/aws-sdk-go-v2/credentials v1.18.20 h1:KFndAnHd9NUuzikHjQ8D5CfFVO+bgELkmcGY8yAw98Q=
github.com/aws/aws-sdk-go-v2/credentials v1.18.4/go.mod h1:nwg78FjH2qvsRM1EVZlX9WuGUJOL5od+0qvm0adEzHk= 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.3 h1:GicIdnekoJsjq9wqnvyi2elW6CGMSYKhdozE7/Svh78= 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.3/go.mod h1:R7BIi6WNC5mc1kfRM7XM/VHC3uRWkjc396sfabq4iOo= 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.3 h1:o9RnO+YZ4X+kt5Z7Nvcishlz0nksIt2PIzDglLMP0vA= 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.3/go.mod h1:+6aLJzOG1fvMOyzIySYjOFjcguGvVRL68R+uoRencN4= 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.3 h1:joyyUFhiTQQmVK6ImzNU9TQSNRNeD9kOklqTzyk5v6s= 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.3/go.mod h1:+vNIyZQP3b3B1tSLI0lxvrU9cfM7gpdRXMFfm67ZcPc= 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.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= 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.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= 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.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= 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.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= 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.3 h1:ieRzyHXypu5ByllM7Sp4hC5f/1Fy5wqxqY0yB85hC7s= 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.3/go.mod h1:O5ROz8jHiOAKAwx179v+7sHMhfobFVi6nZt8DEyiYoM= 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.44.0 h1:Z95XCqqSnwXr0AY7PgsiOUBhUG2GoDM5getw6RfD1Lg= 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.44.0/go.mod h1:DqcSngL7jJeU1fOzh5Ll5rSvX/MlMV6OZlE4mVdFAQc= 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.28.0 h1:Mc/MKBf2m4VynyJkABoVEN+QzkfLqGj0aiJuEe7cMeM= github.com/aws/aws-sdk-go-v2/service/sso v1.30.0 h1:xHXvxst78wBpJFgDW07xllOx0IAzbryrSdM4nMVQ4Dw=
github.com/aws/aws-sdk-go-v2/service/sso v1.28.0/go.mod h1:iS5OmxEcN4QIPXARGhavH7S8kETNL11kym6jhoS7IUQ= 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.33.0 h1:6csaS/aJmqZQbKhi1EyEMM7yBW653Wy/B9hnBofW+sw= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4 h1:tBw2Qhf0kj4ZwtsVpDiVRU3zKLvjvjgIjHMKirxXg8M=
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/ssooidc v1.35.4/go.mod h1:Deq4B7sRM6Awq/xyOBlxBdgW8/Z926KYNNaGMW2lrkA=
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.39.0 h1:C+BRMnasSYFcgDw8o9H5hzehKzXyAb9GY5v/8bP9DUY=
github.com/aws/aws-sdk-go-v2/service/sts v1.37.0/go.mod h1:JdeBDPgpJfuS6rU/hNglmOigKhyEZtBmbraLE4GK1J8= 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.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 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 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic=
github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA= github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= github.com/caddyserver/zerossl v0.1.3 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 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 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 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 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/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 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= 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-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 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.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7 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/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 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 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/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 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 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 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 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 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/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.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= 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.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 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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-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 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 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.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 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 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.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 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= 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.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 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= 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.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.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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= github.com/google/go-tpm-tools v0.4.6 h1:hwIwPG7w4z5eQEBq11gYw8YYr9xXLfBQ/0JsKyq5AJM=
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm-tools v0.4.6/go.mod h1:MsVQbJnRhKDfWwf5zgr3cDGpj13P1uLAFF0wMEP/n5w=
github.com/google/go-tpm-tools v0.4.5 h1:3fhthtyMDbIZFR5/0y1hvUoZ1Kf4i1eZ7C73R4Pvd+k=
github.com/google/go-tpm-tools v0.4.5/go.mod h1:ktjTNq8yZFD6TzdBFefUfen96rF3NpYwpSb2d8bc+Y8=
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= 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 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= 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 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= 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/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= 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/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/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/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 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 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/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.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.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 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 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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.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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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/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 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= 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/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 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 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.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ=
github.com/mholt/acmez/v3 v3.1.3/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0 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/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 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 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/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/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 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 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.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 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/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.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= 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 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0 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/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 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=
github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg= 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 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 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 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 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.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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.7 h1:v5u46efIyYHGdfjFnozQbRRhMdaB9Ma1SSTcUcE2lfE=
github.com/slackhq/nebula v1.9.5/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ= 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 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/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
github.com/smallstep/certificates v0.28.4 h1:JTU6/A5Xes6m+OsR6fw1RACSA362vJc9SOFVG7poBEw= 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/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 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4=
github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A= 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 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 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 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/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/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.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.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/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 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 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/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 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ=
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU= 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/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 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= 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 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 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= 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= 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 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= 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 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 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 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/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 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/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 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= 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 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= 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 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/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 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= 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 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 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 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= 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 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= 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 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= 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.74.0 h1:/APBEv45yYR4qQFg47HA8w1nesIGcxh44pGyQNw6JRA=
go.step.sm/crypto v0.70.0/go.mod h1:pzfUhS5/ue7ev64PLlEgXvhx1opwbhFCjkvlhsxVds0= 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 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 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.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= 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 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 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 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 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 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-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-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.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.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.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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= 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.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 h1:V5+zy0jmgNYmK1uW/sPpBw8ioFvalrhaUrYWmu1Fpe4= golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99 h1:CH0o4/bZX6KIUCjjgjmtNtfM/kXSkTYlzTOB9vZF45g=
golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU= golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.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.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.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.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
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/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.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.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.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.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-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-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-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-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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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.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.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.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.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.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= 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.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= 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.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.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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.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.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-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.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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 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.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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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.254.0 h1:jl3XrGj7lRjnlUvZAbAdhINTLbsg5dbjmR90+pTQvt4=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.254.0/go.mod h1:5BkSURm3D9kAqjGvBNgf0EcbX6Rnrf6UArKkwBzAyqQ=
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/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= 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 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 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= 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-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
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/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= 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/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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 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 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-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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.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.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= 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=

View file

@ -261,14 +261,14 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e
if atomic.LoadInt32(&fcpc.closed) == 1 { if atomic.LoadInt32(&fcpc.closed) == 1 {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() { if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
if err = fcpc.SetReadDeadline(time.Time{}); err != nil { 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. // Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.

View file

@ -31,13 +31,17 @@ import (
"github.com/quic-go/quic-go" "github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3" "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"
"golang.org/x/time/rate" "golang.org/x/time/rate"
"github.com/caddyserver/caddy/v2/internal" "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. // NetworkAddress represents one or more network addresses.
// It contains the individual components for a parsed network // It contains the individual components for a parsed network
// address of the form accepted by ParseNetworkAddress(). // address of the form accepted by ParseNetworkAddress().
@ -305,6 +309,64 @@ func IsFdNetwork(netw string) bool {
return strings.HasPrefix(netw, "fd") 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 // ParseNetworkAddress parses addr into its individual
// components. The input string is expected to be of // components. The input string is expected to be of
// the form "network/host:port-range" where any part is // the form "network/host:port-range" where any part is
@ -336,9 +398,27 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
}, err }, err
} }
if IsFdNetwork(network) { 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{ return NetworkAddress{
Network: network, Network: network,
Host: host, Host: fdAddr,
}, nil }, nil
} }
var start, end uint64 var start, end uint64
@ -382,7 +462,7 @@ func SplitNetworkAddress(a string) (network, host, port string, err error) {
a = afterSlash a = afterSlash
if IsUnixNetwork(network) || IsFdNetwork(network) { if IsUnixNetwork(network) || IsFdNetwork(network) {
host = a 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) err = errors.Join(firstErr, err)
} }
return return network, host, port, err
} }
// JoinNetworkAddress combines network, host, and port into a single // 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), http3.ConfigureTLSConfig(quicTlsConfig),
&quic.Config{ &quic.Config{
Allow0RTT: true, Allow0RTT: true,
Tracer: qlog.DefaultConnectionTracer, Tracer: h3qlog.DefaultConnectionTracer,
}, },
) )
if err != nil { if err != nil {

View file

@ -15,6 +15,7 @@
package caddy package caddy
import ( import (
"os"
"reflect" "reflect"
"testing" "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)
}
}
}

View file

@ -345,9 +345,11 @@ func StrictUnmarshalJSON(data []byte, v any) error {
return dec.Decode(v) return dec.Decode(v)
} }
var JSONRawMessageType = reflect.TypeFor[json.RawMessage]()
// isJSONRawMessage returns true if the type is encoding/json.RawMessage. // isJSONRawMessage returns true if the type is encoding/json.RawMessage.
func isJSONRawMessage(typ reflect.Type) bool { 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. // isModuleMapType returns true if the type is map[string]json.RawMessage.

View file

@ -198,6 +198,8 @@ func (app *App) Provision(ctx caddy.Context) error {
if app.Metrics != nil { if app.Metrics != nil {
app.Metrics.init = sync.Once{} app.Metrics.init = sync.Once{}
app.Metrics.httpMetrics = &httpMetrics{} app.Metrics.httpMetrics = &httpMetrics{}
// Scan config for allowed hosts to prevent cardinality explosion
app.Metrics.scanConfigForHosts(app)
} }
// prepare each server // prepare each server
oldContext := ctx.Context oldContext := ctx.Context
@ -466,7 +468,14 @@ func (app *App) Start() error {
ErrorLog: serverLogger, ErrorLog: serverLogger,
Protocols: new(http.Protocols), Protocols: new(http.Protocols),
ConnContext: func(ctx context.Context, c net.Conn) context.Context { 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{ KeepAliveConfig: net.KeepAliveConfig{
Enable: srv.KeepAliveInterval >= 0, Enable: srv.KeepAliveInterval >= 0,
Interval: time.Duration(srv.KeepAliveInterval), Interval: time.Duration(srv.KeepAliveInterval),
Idle: time.Duration(srv.KeepAliveIdle),
Count: srv.KeepAliveCount,
}, },
}) })
if err != nil { if err != nil {

View file

@ -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
}

View file

@ -27,7 +27,10 @@ func init() {
} }
// defaultBcryptCost cost 14 strikes a solid balance between security, usability, and hardware performance // 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. // BcryptHash implements the bcrypt hash.
type BcryptHash struct { type BcryptHash struct {

View file

@ -51,7 +51,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
var hashName string var hashName string
switch len(args) { switch len(args) {
case 0: case 0:
hashName = "bcrypt" hashName = bcryptName
case 1: case 1:
hashName = args[0] hashName = args[0]
case 2: case 2:
@ -62,8 +62,10 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
} }
switch hashName { switch hashName {
case "bcrypt": case bcryptName:
cmp = BcryptHash{} cmp = BcryptHash{}
case argon2idName:
cmp = Argon2idHash{}
default: default:
return nil, h.Errf("unrecognized hash algorithm: %s", hashName) return nil, h.Errf("unrecognized hash algorithm: %s", hashName)
} }

View file

@ -32,28 +32,55 @@ import (
func init() { func init() {
caddycmd.RegisterCommand(caddycmd.Command{ caddycmd.RegisterCommand(caddycmd.Command{
Name: "hash-password", Name: "hash-password",
Usage: "[--plaintext <password>] [--algorithm <name>] [--bcrypt-cost <difficulty>]", Usage: "[--plaintext <password>] [--algorithm <argon2id|bcrypt>] [--bcrypt-cost <difficulty>] [--argon2id-time <iterations>] [--argon2id-memory <KiB>] [--argon2id-threads <n>] [--argon2id-keylen <bytes>]",
Short: "Hashes a password and writes base64", Short: "Hashes a password and writes base64",
Long: ` Long: `
Convenient way to hash a plaintext password. The resulting Convenient way to hash a plaintext password. The resulting
hash is written to stdout as a base64 string. hash is written to stdout as a base64 string.
--plaintext, when omitted, will be read from stdin. If --plaintext
Caddy is attached to a controlling tty, the plaintext will The password to hash. If omitted, it will be read from stdin.
not be echoed. 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. bcrypt-specific parameters:
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], --bcrypt-cost
the default value (defaultBcryptCost) will be used instead. Sets the bcrypt hashing difficulty. Higher values increase security by
Note: Higher cost values can significantly degrade performance on slower systems. 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) { CobraFunc: func(cmd *cobra.Command) {
cmd.Flags().StringP("plaintext", "p", "", "The plaintext password") 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().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) cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdHashPassword)
}, },
}) })
@ -115,8 +142,34 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
var hash []byte var hash []byte
var hashString string var hashString string
switch algorithm { switch algorithm {
case "bcrypt": case bcryptName:
hash, err = BcryptHash{cost: bcryptCost}.Hash(plaintext) 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) hashString = string(hash)
default: default:
return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm) return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm)

View file

@ -665,7 +665,7 @@ func celMatcherJSONMacroExpander(funcName string) parser.MacroExpander {
// map literals containing heterogeneous values, in this case string and list // map literals containing heterogeneous values, in this case string and list
// of string. // of string.
func CELValueToMapStrList(data ref.Val) (map[string][]string, error) { 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) mapStrRaw, err := data.ConvertToNative(mapStrType)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -50,7 +50,7 @@ type Encode struct {
// Only encode responses that are at least this many bytes long. // Only encode responses that are at least this many bytes long.
MinLength int `json:"minimum_length,omitempty"` 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. // The default is a collection of text-based Content-Type headers.
Matcher *caddyhttp.ResponseMatcher `json:"match,omitempty"` 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... // caches without knowing about our changes...
if etag := r.Header.Get("If-None-Match"); etag != "" && !strings.HasPrefix(etag, "W/") { if etag := r.Header.Get("If-None-Match"); etag != "" && !strings.HasPrefix(etag, "W/") {
ourSuffix := "-" + encName + `"` ourSuffix := "-" + encName + `"`
if strings.HasSuffix(etag, ourSuffix) { if before, ok := strings.CutSuffix(etag, ourSuffix); ok {
etag = strings.TrimSuffix(etag, ourSuffix) + `"` etag = before + `"`
r.Header.Set("If-None-Match", etag) r.Header.Set("If-None-Match", etag)
} }
} }

View file

@ -36,6 +36,12 @@ func (h *http2Listener) Accept() (net.Conn, error) {
return nil, err 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) _, isConnectionStater := conn.(connectionStater)
// emit a warning // emit a warning
if h.useTLS && !isConnectionStater { 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 both h1 and h2 are enabled, we don't need to check the preface
if h.useH1 && h.useH2 { if h.useH1 && h.useH2 {
if isConnectionStater {
return tlsStateConn{conn}, nil
}
return conn, nil return conn, nil
} }
@ -53,14 +62,26 @@ func (h *http2Listener) Accept() (net.Conn, error) {
// or else the listener wouldn't be created // or else the listener wouldn't be created
h2Conn := &http2Conn{ h2Conn := &http2Conn{
h2Expected: h.useH2, h2Expected: h.useH2,
logger: h.logger,
Conn: conn, Conn: conn,
} }
if isConnectionStater { if isConnectionStater {
return http2StateConn{h2Conn}, nil return tlsStateConn{http2StateConn{h2Conn}}, nil
} }
return 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 { type http2StateConn struct {
*http2Conn *http2Conn
} }

View file

@ -17,6 +17,7 @@ package intercept
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@ -175,10 +176,35 @@ func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
c.Write(zap.Int("handler", rec.handlerIndex)) c.Write(zap.Int("handler", rec.handlerIndex))
} }
// pass the request through the response handler routes // response recorder doesn't create a new copy of the original headers, they're
return rec.handler.Routes.Compile(next).ServeHTTP(w, r) // 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: // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
// //
// intercept [<matcher>] { // intercept [<matcher>] {

View file

@ -15,18 +15,28 @@
package caddyhttp package caddyhttp
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"log/slog"
"net" "net"
"net/http" "net/http"
"strings" "strings"
"sync"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/exp/zapslog"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2" "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 // ServerLogConfig describes a server's logging configuration. If
// enabled without customization, all requests to this server are // enabled without customization, all requests to this server are
// logged to the default logger; logger destinations may be // 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), zap.String("err_trace", handlerErr.Trace),
} }
} }
return return status, msg, fields
} }
fields = func() []zapcore.Field { fields = func() []zapcore.Field {
return []zapcore.Field{ return []zapcore.Field{
@ -218,22 +228,26 @@ func errLogValues(err error) (status int, msg string, fields func() []zapcore.Fi
} }
status = http.StatusInternalServerError status = http.StatusInternalServerError
msg = err.Error() msg = err.Error()
return return status, msg, fields
} }
// ExtraLogFields is a list of extra fields to log with every request. // ExtraLogFields is a list of extra fields to log with every request.
type ExtraLogFields struct { type ExtraLogFields struct {
fields []zapcore.Field fields []zapcore.Field
handlers sync.Map
} }
// Add adds a field to the list of extra fields to log. // Add adds a field to the list of extra fields to log.
func (e *ExtraLogFields) Add(field zap.Field) { func (e *ExtraLogFields) Add(field zap.Field) {
e.handlers.Clear()
e.fields = append(e.fields, field) e.fields = append(e.fields, field)
} }
// Set sets a field in the list of extra fields to log. // Set sets a field in the list of extra fields to log.
// If the field already exists, it is replaced. // If the field already exists, it is replaced.
func (e *ExtraLogFields) Set(field zap.Field) { func (e *ExtraLogFields) Set(field zap.Field) {
e.handlers.Clear()
for i := range e.fields { for i := range e.fields {
if e.fields[i].Key == field.Key { if e.fields[i].Key == field.Key {
e.fields[i] = field e.fields[i] = field
@ -243,6 +257,29 @@ func (e *ExtraLogFields) Set(field zap.Field) {
e.fields = append(e.fields, 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 ( const (
// Variable name used to indicate that this request // Variable name used to indicate that this request
// should be omitted from the access logs // should be omitted from the access logs
@ -254,3 +291,43 @@ const (
// Variable name used to indicate the logger to be used // Variable name used to indicate the logger to be used
AccessLoggerNameVarKey string = "access_logger_names" 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,
}
}

View file

@ -992,7 +992,6 @@ func TestVarREMatcher(t *testing.T) {
expect: true, expect: true,
}, },
} { } {
tc := tc // capture range value
t.Run(tc.desc, func(t *testing.T) { t.Run(tc.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
// compile the regexp and validate its name // compile the regexp and validate its name

View file

@ -17,14 +17,60 @@ import (
// Metrics configures metrics observations. // Metrics configures metrics observations.
// EXPERIMENTAL and subject to change or removal. // 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 { type Metrics struct {
// Enable per-host metrics. Enabling this option may // Enable per-host metrics. Enabling this option may
// incur high-memory consumption, depending on the number of hosts // incur high-memory consumption, depending on the number of hosts
// managed by Caddy. // 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"` PerHost bool `json:"per_host,omitempty"`
init sync.Once // Allow metrics for catch-all hosts (hosts without explicit configuration).
httpMetrics *httpMetrics `json:"-"` // 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 { type httpMetrics struct {
@ -101,6 +147,63 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) {
}, httpLabels) }, 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. // serverNameFromContext extracts the current server name from the context.
// Returns "UNKNOWN" if none is available (should probably never happen). // Returns "UNKNOWN" if none is available (should probably never happen).
func serverNameFromContext(ctx context.Context) string { func serverNameFromContext(ctx context.Context) string {
@ -133,9 +236,19 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
// of a panic // of a panic
statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""} 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 { if h.metrics.PerHost {
labels["host"] = strings.ToLower(r.Host) // Apply cardinality protection for host metrics
statusLabels["host"] = strings.ToLower(r.Host) 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) inFlight := h.metrics.httpMetrics.requestInFlight.With(labels)

View file

@ -2,6 +2,7 @@ package caddyhttp
import ( import (
"context" "context"
"crypto/tls"
"errors" "errors"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -206,9 +207,11 @@ func TestMetricsInstrumentedHandler(t *testing.T) {
func TestMetricsInstrumentedHandlerPerHost(t *testing.T) { func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
metrics := &Metrics{ metrics := &Metrics{
PerHost: true, PerHost: true,
init: sync.Once{}, AllowCatchAllHosts: true, // Allow all hosts for testing
httpMetrics: &httpMetrics{}, init: sync.Once{},
httpMetrics: &httpMetrics{},
allowedHosts: make(map[string]struct{}),
} }
handlerErr := errors.New("oh noes") handlerErr := errors.New("oh noes")
response := []byte("hello world!") 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 type middlewareHandlerFunc func(http.ResponseWriter, *http.Request, Handler) error
func (f middlewareHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, h Handler) error { func (f middlewareHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, h Handler) error {

View file

@ -116,7 +116,7 @@ func (ew errorWrapper) Read(p []byte) (n int, err error) {
if errors.As(err, &mbe) { if errors.As(err, &mbe) {
err = caddyhttp.Error(http.StatusRequestEntityTooLarge, err) err = caddyhttp.Error(http.StatusRequestEntityTooLarge, err)
} }
return return n, err
} }
// Interface guard // Interface guard

View file

@ -888,8 +888,11 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if commonScheme == "http" && te.TLSEnabled() { if commonScheme == "http" && te.TLSEnabled() {
return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)") return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)")
} }
if te, ok := transport.(*HTTPTransport); ok && commonScheme == "h2c" { if h2ct, ok := transport.(H2CTransport); ok && commonScheme == "h2c" {
te.Versions = []string{"h2c", "2"} err := h2ct.EnableH2C()
if err != nil {
return err
}
} }
} else if commonScheme == "https" { } else if commonScheme == "https" {
return d.Errf("upstreams are configured for HTTPS but transport module does not support TLS: %T", transport) return d.Errf("upstreams are configured for HTTPS but transport module does not support TLS: %T", transport)

View file

@ -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("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("disable-redirects", "r", false, "Disable HTTP->HTTPS redirects")
cmd.Flags().BoolP("internal-certs", "i", false, "Use internal CA for issuing certs") 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().StringArrayP("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-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("access-log", "", false, "Enable the access log")
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs") cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdReverseProxy) cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdReverseProxy)
@ -182,7 +182,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
} }
// set up header_up // set up header_up
headerUp, err := fs.GetStringSlice("header-up") headerUp, err := fs.GetStringArray("header-up")
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err) 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 // set up header_down
headerDown, err := fs.GetStringSlice("header-down") headerDown, err := fs.GetStringArray("header-down")
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err)
} }

View file

@ -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) err = writer.writeBeginRequest(uint16(Responder), 0)
if err != nil { if err != nil {
return return r, err
} }
writer.recType = Params writer.recType = Params
err = writer.writePairs(p) err = writer.writePairs(p)
if err != nil { if err != nil {
return return r, err
} }
writer.recType = Stdin 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} r = &streamReader{c: c}
return return r, err
} }
// clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer // 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) { func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) {
r, err := c.Do(p, req) r, err := c.Do(p, req)
if err != nil { if err != nil {
return return resp, err
} }
rb := bufio.NewReader(r) 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. // Parse the response headers.
mimeHeader, err := tp.ReadMIMEHeader() mimeHeader, err := tp.ReadMIMEHeader()
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return return resp, err
} }
resp.Header = http.Header(mimeHeader) 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"), " ") statusNumber, statusInfo, statusIsCut := strings.Cut(resp.Header.Get("Status"), " ")
resp.StatusCode, err = strconv.Atoi(statusNumber) resp.StatusCode, err = strconv.Atoi(statusNumber)
if err != nil { if err != nil {
return return resp, err
} }
if statusIsCut { if statusIsCut {
resp.Status = statusInfo resp.Status = statusInfo
@ -260,7 +260,7 @@ func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Respons
} }
resp.Body = closer resp.Body = closer
return return resp, err
} }
// Get issues a GET request to the fcgi responder. // 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 { for _, v0 := range val {
err = writer.WriteField(key, v0) err = writer.WriteField(key, v0)
if err != nil { 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) _, err = io.Copy(part, fd)
if err != nil { if err != nil {
return return resp, err
} }
} }
err = writer.Close() err = writer.Close()
if err != nil { if err != nil {
return return resp, err
} }
return c.Post(p, "POST", bodyType, buf, int64(buf.Len())) return c.Post(p, "POST", bodyType, buf, int64(buf.Len()))

View file

@ -120,7 +120,7 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
conn, err := net.Dial("tcp", ipPort) conn, err := net.Dial("tcp", ipPort)
if err != nil { if err != nil {
log.Println("err:", err) log.Println("err:", err)
return return content
} }
fcgi := client{rwc: conn, reqID: 1} 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 { if err != nil {
log.Println("err:", err) log.Println("err:", err)
return return content
} }
defer resp.Body.Close() 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") globalt.Error("Server return failed message")
} }
return return content
} }
func generateRandFile(size int) (p string, m string) { 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)) m = fmt.Sprintf("%x", h.Sum(nil))
return return p, m
} }
func DisabledTest(t *testing.T) { func DisabledTest(t *testing.T) {

View file

@ -112,6 +112,20 @@ func (t *Transport) Provision(ctx caddy.Context) error {
return nil 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. // RoundTrip implements http.RoundTripper.
func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) { func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) {
server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server) server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server)
@ -427,6 +441,7 @@ var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
var ( var (
_ zapcore.ObjectMarshaler = (*loggableEnv)(nil) _ zapcore.ObjectMarshaler = (*loggableEnv)(nil)
_ caddy.Provisioner = (*Transport)(nil) _ caddy.Provisioner = (*Transport)(nil)
_ http.RoundTripper = (*Transport)(nil) _ http.RoundTripper = (*Transport)(nil)
_ reverseproxy.BufferedTransport = (*Transport)(nil)
) )

View file

@ -30,23 +30,23 @@ func (rec *record) fill(r io.Reader) (err error) {
rec.lr.N = rec.padding rec.lr.N = rec.padding
rec.lr.R = r rec.lr.R = r
if _, err = io.Copy(io.Discard, rec); err != nil { if _, err = io.Copy(io.Discard, rec); err != nil {
return return err
} }
if err = binary.Read(r, binary.BigEndian, &rec.h); err != nil { if err = binary.Read(r, binary.BigEndian, &rec.h); err != nil {
return return err
} }
if rec.h.Version != 1 { if rec.h.Version != 1 {
err = errors.New("fcgi: invalid header version") err = errors.New("fcgi: invalid header version")
return return err
} }
if rec.h.Type == EndRequest { if rec.h.Type == EndRequest {
err = io.EOF err = io.EOF
return return err
} }
rec.lr.N = int64(rec.h.ContentLength) rec.lr.N = int64(rec.h.ContentLength)
rec.padding = int64(rec.h.PaddingLength) rec.padding = int64(rec.h.PaddingLength)
return return err
} }
func (rec *record) Read(p []byte) (n int, err error) { func (rec *record) Read(p []byte) (n int, err error) {

View file

@ -112,7 +112,7 @@ func encodeSize(b []byte, size uint32) int {
binary.BigEndian.PutUint32(b, size) binary.BigEndian.PutUint32(b, size)
return 4 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 return 1
} }

View file

@ -84,7 +84,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
// create the reverse proxy handler // create the reverse proxy handler
rpHandler := &reverseproxy.Handler{ rpHandler := &reverseproxy.Handler{
// set up defaults for header_up; reverse_proxy already deals with // 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 // we want to also send along the incoming method and URI since this
// request will have a rewritten URI and method. // request will have a rewritten URI and method.
Headers: &headers.Handler{ Headers: &headers.Handler{

View file

@ -23,7 +23,6 @@ import (
"net/url" "net/url"
"regexp" "regexp"
"runtime/debug" "runtime/debug"
"slices"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -405,14 +404,9 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
u.Host = net.JoinHostPort(host, port) u.Host = net.JoinHostPort(host, port)
} }
// this is kind of a hacky way to know if we should use HTTPS, but whatever // override health check schemes if applicable
if tt, ok := h.Transport.(TLSTransport); ok && tt.TLSEnabled() { if hcsot, ok := h.Transport.(HealthCheckSchemeOverriderTransport); ok {
u.Scheme = "https" hcsot.OverrideHealthCheckScheme(u, port)
// 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"
}
} }
// if we have a provisioned uri, use that, otherwise use // if we have a provisioned uri, use that, otherwise use

View file

@ -281,3 +281,10 @@ const proxyProtocolInfoVarKey = "reverse_proxy.proxy_protocol_info"
type ProxyProtocolInfo struct { type ProxyProtocolInfo struct {
AddrPort netip.AddrPort 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"

View file

@ -24,6 +24,7 @@ import (
weakrand "math/rand" weakrand "math/rand"
"net" "net"
"net/http" "net/http"
"net/url"
"os" "os"
"reflect" "reflect"
"slices" "slices"
@ -159,8 +160,7 @@ type HTTPTransport struct {
// `HTTPS_PROXY`, and `NO_PROXY` environment variables. // `HTTPS_PROXY`, and `NO_PROXY` environment variables.
NetworkProxyRaw json.RawMessage `json:"network_proxy,omitempty" caddy:"namespace=caddy.network_proxy inline_key=from"` 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. // 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) { func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, error) {
// Set keep-alive defaults if it wasn't otherwise configured // Set keep-alive defaults if it wasn't otherwise configured
if h.KeepAlive == nil { if h.KeepAlive == nil {
h.KeepAlive = &KeepAlive{ h.KeepAlive = new(KeepAlive)
ProbeInterval: caddy.Duration(30 * time.Second), }
IdleConnTimeout: caddy.Duration(2 * time.Minute), if h.KeepAlive.ProbeInterval == 0 {
MaxIdleConnsPerHost: 32, // seems about optimal, see #2805 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. // 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) { dialContext := func(ctx context.Context, network, address string) (net.Conn, error) {
// For unix socket upstreams, we need to recover the dial info from // The network is usually tcp, and the address is the host in http.Request.URL.Host
// the request's context, because the Host on the request's URL // and that's been overwritten in directRequest
// will have been modified by directing the request, overwriting // However, if proxy is used according to http.ProxyFromEnvironment or proxy providers,
// the unix socket filename. // address will be the address of the proxy server.
// Also, we need to avoid overwriting the address at this point
// when not necessary, because http.ProxyFromEnvironment may have // This means we can safely use the address in dialInfo if proxy is not used (the address and network will be same any way)
// modified the address according to the user's env proxy config. // 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 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 network = dialInfo.Network
address = dialInfo.Address 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))``") 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{ rt := &http.Transport{
Proxy: proxy, Proxy: proxyWrapper,
DialContext: dialContext, DialContext: dialContext,
MaxConnsPerHost: h.MaxConnsPerHost, MaxConnsPerHost: h.MaxConnsPerHost,
ResponseHeaderTimeout: time.Duration(h.ResponseHeaderTimeout), 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) repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
tlsConfig := rt.TLSClientConfig.Clone() tlsConfig := rt.TLSClientConfig.Clone()
tlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, "") 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) tlsConn := tls.Client(conn, tlsConfig)
// complete the handshake before returning the connection // 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) 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 { if h.Compression != nil {
rt.DisableCompression = !*h.Compression 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 // configure HTTP/3 transport if enabled; however, this does not
// automatically fall back to lower versions like most web browsers // automatically fall back to lower versions like most web browsers
// do (that'd add latency and complexity, besides, we expect that // 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") 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 // if h2/c is enabled, configure it explicitly
// does not "HTTP/2 over cleartext TCP") if slices.Contains(h.Versions, "2") || slices.Contains(h.Versions, "h2c") {
if slices.Contains(h.Versions, "h2c") { if err := http2.ConfigureTransport(rt); err != nil {
// crafting our own http2.Transport doesn't allow us to utilize return nil, err
// 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 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 return rt, nil
@ -517,15 +523,6 @@ func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return h.h3Transport.RoundTrip(req) 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) return h.Transport.RoundTrip(req)
} }
@ -567,6 +564,26 @@ func (h *HTTPTransport) EnableTLS(base *TLSConfig) error {
return nil 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. // Cleanup implements caddy.CleanerUpper and closes any idle connections.
func (h HTTPTransport) Cleanup() error { func (h HTTPTransport) Cleanup() error {
if h.Transport == nil { if h.Transport == nil {
@ -823,8 +840,11 @@ func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
// Interface guards // Interface guards
var ( var (
_ caddy.Provisioner = (*HTTPTransport)(nil) _ caddy.Provisioner = (*HTTPTransport)(nil)
_ http.RoundTripper = (*HTTPTransport)(nil) _ http.RoundTripper = (*HTTPTransport)(nil)
_ caddy.CleanerUpper = (*HTTPTransport)(nil) _ caddy.CleanerUpper = (*HTTPTransport)(nil)
_ TLSTransport = (*HTTPTransport)(nil) _ TLSTransport = (*HTTPTransport)(nil)
_ H2CTransport = (*HTTPTransport)(nil)
_ HealthCheckSchemeOverriderTransport = (*HTTPTransport)(nil)
_ ProxyProtocolTransport = (*HTTPTransport)(nil)
) )

View file

@ -243,18 +243,16 @@ func (h *Handler) Provision(ctx caddy.Context) error {
return fmt.Errorf("loading transport: %v", err) return fmt.Errorf("loading transport: %v", err)
} }
h.Transport = mod.(http.RoundTripper) 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 // set default buffer sizes if applicable
if module, ok := h.Transport.(caddy.Module); ok && module.CaddyModule().ID.Name() == "fastcgi" && h.RequestBuffers == 0 { if bt, ok := h.Transport.(BufferedTransport); ok {
h.RequestBuffers = 4096 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 { 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, return caddyhttp.Error(http.StatusInternalServerError,
fmt.Errorf("preparing request for upstream round-trip: %v", err)) 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 // 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") clonedReq.Header.Del(":protocol")
// keep the body for later use. http1.1 upgrade uses http.NoBody // 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.Body = http.NoBody
clonedReq.Method = http.MethodGet clonedReq.Method = http.MethodGet
clonedReq.Header.Set("Upgrade", "websocket") clonedReq.Header.Set("Upgrade", "websocket")
@ -726,6 +728,12 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.
proxyProtocolInfo := ProxyProtocolInfo{AddrPort: addrPort} proxyProtocolInfo := ProxyProtocolInfo{AddrPort: addrPort}
caddyhttp.SetVar(req.Context(), proxyProtocolInfoVarKey, proxyProtocolInfo) 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 // Add the supported X-Forwarded-* headers
err = h.addForwardedHeaders(req) err = h.addForwardedHeaders(req)
if err != nil { 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 // directRequest modifies only req.URL so that it points to the upstream
// in the given DialInfo. It must modify ONLY the request URL. // 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 // we need a host, so set the upstream's host address
reqHost := di.Address reqHost := di.Address
@ -1199,6 +1207,13 @@ func (Handler) directRequest(req *http.Request, di DialInfo) {
reqHost = di.Host 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 req.URL.Host = reqHost
} }
@ -1484,6 +1499,32 @@ type TLSTransport interface {
EnableTLS(base *TLSConfig) error 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 // roundtripSucceededError is an error type that is returned if the
// roundtrip succeeded, but an error occurred after-the-fact. // roundtrip succeeded, but an error occurred after-the-fact.
type roundtripSucceededError struct{ error } type roundtripSucceededError struct{ error }

View file

@ -94,9 +94,9 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
conn io.ReadWriteCloser conn io.ReadWriteCloser
brw *bufio.ReadWriter 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 // 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 req.Body = body
rw.Header().Del("Upgrade") rw.Header().Del("Upgrade")
rw.Header().Del("Connection") rw.Header().Del("Connection")
@ -588,11 +588,11 @@ func (m *maxLatencyWriter) Write(p []byte) (n int, err error) {
m.logger.Debug("flushing immediately") m.logger.Debug("flushing immediately")
//nolint:errcheck //nolint:errcheck
m.flush() m.flush()
return return n, err
} }
if m.flushPending { if m.flushPending {
m.logger.Debug("delayed flush already pending") m.logger.Debug("delayed flush already pending")
return return n, err
} }
if m.t == nil { if m.t == nil {
m.t = time.AfterFunc(m.latency, m.delayedFlush) 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)) c.Write(zap.Duration("duration", m.latency))
} }
m.flushPending = true m.flushPending = true
return return n, err
} }
func (m *maxLatencyWriter) delayedFlush() { func (m *maxLatencyWriter) delayedFlush() {

View file

@ -213,12 +213,12 @@ func (su SRVUpstreams) expandedAddr(r *http.Request) (addr, service, proto, name
name = repl.ReplaceAll(su.Name, "") name = repl.ReplaceAll(su.Name, "")
if su.Service == "" && su.Proto == "" { if su.Service == "" && su.Proto == "" {
addr = name addr = name
return return addr, service, proto, name
} }
service = repl.ReplaceAll(su.Service, "") service = repl.ReplaceAll(su.Service, "")
proto = repl.ReplaceAll(su.Proto, "") proto = repl.ReplaceAll(su.Proto, "")
addr = su.formattedAddr(service, proto, name) addr = su.formattedAddr(service, proto, name)
return return addr, service, proto, name
} }
// formattedAddr the RFC 2782 representation of the SRV domain, in // formattedAddr the RFC 2782 representation of the SRV domain, in

View file

@ -33,7 +33,7 @@ import (
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/quic-go/quic-go" "github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3" "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"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
@ -76,9 +76,25 @@ type Server struct {
// KeepAliveInterval is the interval at which TCP keepalive packets // KeepAliveInterval is the interval at which TCP keepalive packets
// are sent to keep the connection alive at the TCP layer when no other // 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"` 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 // MaxHeaderBytes is the maximum size to parse from a client's
// HTTP request headers. // HTTP request headers.
MaxHeaderBytes int `json:"max_header_bytes,omitempty"` MaxHeaderBytes int `json:"max_header_bytes,omitempty"`
@ -186,6 +202,13 @@ type Server struct {
// This option is disabled by default. // This option is disabled by default.
TrustedProxiesStrict int `json:"trusted_proxies_strict,omitempty"` 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 // Enables access logging and configures how access logs are handled
// in this server. To minimally enable access logs, simply set this // in this server. To minimally enable access logs, simply set this
// to a non-null, empty struct. // to a non-null, empty struct.
@ -262,30 +285,28 @@ type Server struct {
onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023) onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023)
} }
var (
ServerHeader = "Caddy"
serverHeader = []string{ServerHeader}
)
// ServeHTTP is the entry point for all HTTP requests. // ServeHTTP is the entry point for all HTTP requests.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 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. // 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 { if r.TLS == nil {
// not all requests have a conn (like virtual requests) - see #5698 if tlsConnStateFunc, ok := r.Context().Value(tlsConnectionStateFuncCtxKey).(func() *tls.ConnectionState); ok {
if conn, ok := r.Context().Value(ConnCtxKey).(net.Conn); ok { r.TLS = tlsConnStateFunc()
if csc, ok := conn.(connectionStater); ok {
r.TLS = new(tls.ConnectionState)
*r.TLS = csc.ConnectionState()
}
} }
} }
w.Header().Set("Server", "Caddy") h := w.Header()
h["Server"] = serverHeader
// advertise HTTP/3, if enabled // advertise HTTP/3, if enabled
if s.h3server != nil { if s.h3server != nil && r.ProtoMajor < 3 {
if r.ProtoMajor < 3 { if err := s.h3server.SetQUICHeaders(h); err != nil {
err := s.h3server.SetQUICHeaders(w.Header()) if c := s.logger.Check(zapcore.ErrorLevel, "setting HTTP/3 Alt-Svc header"); c != nil {
if err != nil { c.Write(zap.Error(err))
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 // enable full-duplex for HTTP/1, ensuring the entire
// request body gets consumed before writing the response // request body gets consumed before writing the response
if s.EnableFullDuplex && r.ProtoMajor == 1 { if s.EnableFullDuplex && r.ProtoMajor == 1 {
//nolint:bodyclose if err := http.NewResponseController(w).EnableFullDuplex(); err != nil { //nolint:bodyclose
err := http.NewResponseController(w).EnableFullDuplex()
if err != nil {
if c := s.logger.Check(zapcore.WarnLevel, "failed to enable full duplex"); c != nil { if c := s.logger.Check(zapcore.WarnLevel, "failed to enable full duplex"); c != nil {
c.Write(zap.Error(err)) c.Write(zap.Error(err))
} }
@ -399,8 +418,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var fields []zapcore.Field var fields []zapcore.Field
if s.Errors != nil && len(s.Errors.Routes) > 0 { if s.Errors != nil && len(s.Errors.Routes) > 0 {
// execute user-defined error handling route // execute user-defined error handling route
err2 := s.errorHandlerChain.ServeHTTP(w, r) if err2 := s.errorHandlerChain.ServeHTTP(w, r); err2 == nil {
if err2 == nil {
// user's error route handled the error response // user's error route handled the error response
// successfully, so now just log the error // successfully, so now just log the error
for _, logger := range errLoggers { for _, logger := range errLoggers {
@ -620,7 +638,7 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error
MaxHeaderBytes: s.MaxHeaderBytes, MaxHeaderBytes: s.MaxHeaderBytes,
QUICConfig: &quic.Config{ QUICConfig: &quic.Config{
Versions: []quic.Version{quic.Version1, quic.Version2}, Versions: []quic.Version{quic.Version1, quic.Version2},
Tracer: qlog.DefaultConnectionTracer, Tracer: h3qlog.DefaultConnectionTracer,
}, },
IdleTimeout: time.Duration(s.IdleTimeout), IdleTimeout: time.Duration(s.IdleTimeout),
} }
@ -775,8 +793,10 @@ func (s *Server) logRequest(
accLog *zap.Logger, r *http.Request, wrec ResponseRecorder, duration *time.Duration, accLog *zap.Logger, r *http.Request, wrec ResponseRecorder, duration *time.Duration,
repl *caddy.Replacer, bodyReader *lengthReader, shouldLogCredentials bool, repl *caddy.Replacer, bodyReader *lengthReader, shouldLogCredentials bool,
) { ) {
ctx := r.Context()
// this request may be flagged as omitted from the logs // 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 return
} }
@ -794,7 +814,7 @@ func (s *Server) logRequest(
} }
message := "handled request" message := "handled request"
if nop, ok := GetVar(r.Context(), "unhandled").(bool); ok && nop { if nop, ok := GetVar(ctx, "unhandled").(bool); ok && nop {
message = "NOP" message = "NOP"
} }
@ -818,7 +838,7 @@ func (s *Server) logRequest(
reqBodyLength = bodyReader.Length reqBodyLength = bodyReader.Length
} }
extra := r.Context().Value(ExtraLogFieldsCtxKey).(*ExtraLogFields) extra := ctx.Value(ExtraLogFieldsCtxKey).(*ExtraLogFields)
fieldCount := 6 fieldCount := 6
fields = make([]zapcore.Field, 0, fieldCount+len(extra.fields)) fields = make([]zapcore.Field, 0, fieldCount+len(extra.fields))
@ -925,6 +945,17 @@ func determineTrustedProxy(r *http.Request, s *Server) (bool, string) {
return false, "" 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, // Parse the remote IP, ignore the error as non-fatal,
// but the remote IP is required to continue, so we // but the remote IP is required to continue, so we
// just return early. This should probably never happen // just return early. This should probably never happen
@ -1081,11 +1112,14 @@ const (
// originally came into the server's entry handler // originally came into the server's entry handler
OriginalRequestCtxKey caddy.CtxKey = "original_request" OriginalRequestCtxKey caddy.CtxKey = "original_request"
// For referencing underlying net.Conn // DEPRECATED: not used anymore.
// This will eventually be deprecated and not used. To refer to the underlying connection, implement a middleware plugin // To refer to the underlying connection, implement a middleware plugin
// that RegisterConnContext during provisioning. // that RegisterConnContext during provisioning.
ConnCtxKey caddy.CtxKey = "conn" 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 // For tracking whether the client is a trusted proxy
TrustedProxyVarKey string = "trusted_proxy" TrustedProxyVarKey string = "trusted_proxy"

View file

@ -297,6 +297,39 @@ func TestServer_DetermineTrustedProxy_TrustedLoopback(t *testing.T) {
assert.Equal(t, clientIP, "31.40.0.10") 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) { func TestServer_DetermineTrustedProxy_UntrustedPrefix(t *testing.T) {
loopbackPrefix, _ := netip.ParsePrefix("127.0.0.1/8") loopbackPrefix, _ := netip.ParsePrefix("127.0.0.1/8")

View file

@ -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().StringP("body", "b", "", "The body of the HTTP response")
cmd.Flags().BoolP("access-log", "", false, "Enable the access log") cmd.Flags().BoolP("access-log", "", false, "Enable the access log")
cmd.Flags().BoolP("debug", "v", false, "Enable more verbose debug-level logging") 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) cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdRespond)
}, },
}) })
@ -359,7 +359,7 @@ func cmdRespond(fl caddycmd.Flags) (int, error) {
} }
// build headers map // build headers map
headers, err := fl.GetStringSlice("header") headers, err := fl.GetStringArray("header")
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err)
} }

View file

@ -5,10 +5,10 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"go.opentelemetry.io/contrib/exporters/autoexport"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/contrib/propagators/autoprop" "go.opentelemetry.io/contrib/propagators/autoprop"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace" sdktrace "go.opentelemetry.io/otel/sdk/trace"
@ -63,7 +63,7 @@ func newOpenTelemetryWrapper(
return ot, fmt.Errorf("creating resource error: %w", err) return ot, fmt.Errorf("creating resource error: %w", err)
} }
traceExporter, err := otlptracegrpc.New(ctx) traceExporter, err := autoexport.NewSpanExporter(ctx)
if err != nil { if err != nil {
return ot, fmt.Errorf("creating trace exporter error: %w", err) return ot, fmt.Errorf("creating trace exporter error: %w", err)
} }

View file

@ -220,13 +220,13 @@ func (a *adminAPI) getCAFromAPIRequestPath(r *http.Request) (*CA, error) {
func rootAndIntermediatePEM(ca *CA) (root, inter []byte, err error) { func rootAndIntermediatePEM(ca *CA) (root, inter []byte, err error) {
root, err = pemEncodeCert(ca.RootCertificate().Raw) root, err = pemEncodeCert(ca.RootCertificate().Raw)
if err != nil { if err != nil {
return return root, inter, err
} }
inter, err = pemEncodeCert(ca.IntermediateCertificate().Raw) inter, err = pemEncodeCert(ca.IntermediateCertificate().Raw)
if err != nil { if err != nil {
return return root, inter, err
} }
return return root, inter, err
} }
// caInfo is the response structure for the CA info API endpoint. // caInfo is the response structure for the CA info API endpoint.

View file

@ -124,8 +124,6 @@ func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error {
} }
if ca.IntermediateLifetime == 0 { if ca.IntermediateLifetime == 0 {
ca.IntermediateLifetime = caddy.Duration(defaultIntermediateLifetime) 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 // 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 { if err != nil {
return err 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 { if ca.Intermediate != nil {
interCert, interKey, err = ca.Intermediate.Load() interCert, interKey, err = ca.Intermediate.Load()
} else { } else {

View file

@ -29,9 +29,9 @@ func init() {
caddy.RegisterModule(LeafFolderLoader{}) 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 // 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 { type LeafFolderLoader struct {
Folders []string `json:"folders,omitempty"` Folders []string `json:"folders,omitempty"`
} }

View file

@ -22,9 +22,11 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"time"
"github.com/DeRuina/timberjack"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"gopkg.in/natefinch/lumberjack.v2"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@ -96,6 +98,21 @@ type FileWriter struct {
// it will be rotated. // it will be rotated.
RollSizeMB int `json:"roll_size_mb,omitempty"` 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 // Whether to compress rolled files. Default: true
RollCompress *bool `json:"roll_gzip,omitempty"` RollCompress *bool `json:"roll_gzip,omitempty"`
@ -109,6 +126,11 @@ type FileWriter struct {
// How many days to keep rolled log files. Default: 90 // How many days to keep rolled log files. Default: 90
RollKeepDays int `json:"roll_keep_days,omitempty"` RollKeepDays int `json:"roll_keep_days,omitempty"`
// Rotated file will have format <logfilename>-<format>-<criterion>.log
// Optional. If unset or invalid, defaults to 2006-01-02T15-04-05.000 (with fallback warning)
// <format> 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. // CaddyModule returns the Caddy module information.
@ -156,7 +178,7 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
roll := fw.Roll == nil || *fw.Roll roll := fw.Roll == nil || *fw.Roll
// create the file if it does not exist; create with the configured mode, or default // 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 { if err := os.MkdirAll(filepath.Dir(fw.Filename), 0o700); err != nil {
return nil, err return nil, err
} }
@ -166,7 +188,7 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
} }
info, err := file.Stat() info, err := file.Stat()
if roll { 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. // 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 { if fw.RollKeepDays == 0 {
fw.RollKeepDays = 90 fw.RollKeepDays = 90
} }
return &lumberjack.Logger{ return &timberjack.Logger{
Filename: fw.Filename, Filename: fw.Filename,
MaxSize: fw.RollSizeMB, MaxSize: fw.RollSizeMB,
MaxAge: fw.RollKeepDays, MaxAge: fw.RollKeepDays,
MaxBackups: fw.RollKeep, MaxBackups: fw.RollKeep,
LocalTime: fw.RollLocalTime, LocalTime: fw.RollLocalTime,
Compress: *fw.RollCompress, Compress: *fw.RollCompress,
RotationInterval: fw.RollInterval,
RotateAtMinutes: fw.RollAtMinutes,
RotateAt: fw.RollAt,
BackupTimeFormat: fw.BackupTimeFormat,
}, nil }, nil
} }
@ -314,6 +340,53 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
} }
fw.RollKeepDays = int(math.Ceil(keepFor.Hours() / 24)) 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: default:
return d.Errf("unrecognized subdirective '%s'", d.Val()) return d.Errf("unrecognized subdirective '%s'", d.Val())
} }

View file

@ -152,6 +152,9 @@ func (fe *FilterEncoder) ConfigureDefaultFormat(wo caddy.WriterOpener) error {
func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume encoder name d.Next() // consume encoder name
// Track regexp filters for automatic merging
regexpFilters := make(map[string][]*RegexpFilter)
// parse a field // parse a field
parseField := func() error { parseField := func() error {
if fe.FieldsRaw == nil { if fe.FieldsRaw == nil {
@ -171,6 +174,23 @@ func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if !ok { if !ok {
return d.Errf("module %s (%T) is not a logging.LogFieldFilter", moduleID, unm) 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) fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(filter, "filter", filterName, nil)
return 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 return nil
} }

View file

@ -41,6 +41,7 @@ func init() {
caddy.RegisterModule(CookieFilter{}) caddy.RegisterModule(CookieFilter{})
caddy.RegisterModule(RegexpFilter{}) caddy.RegisterModule(RegexpFilter{})
caddy.RegisterModule(RenameFilter{}) caddy.RegisterModule(RenameFilter{})
caddy.RegisterModule(MultiRegexpFilter{})
} }
// LogFieldFilter can filter (or manipulate) // LogFieldFilter can filter (or manipulate)
@ -625,6 +626,222 @@ func (f *RegexpFilter) Filter(in zapcore.Field) zapcore.Field {
return in 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 <pattern> <replacement>
// regexp <pattern> <replacement>
// ...
// }
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 // RenameFilter is a Caddy log field filter that
// renames the field's key with the indicated name. // renames the field's key with the indicated name.
type RenameFilter struct { type RenameFilter struct {
@ -664,6 +881,7 @@ var (
_ LogFieldFilter = (*CookieFilter)(nil) _ LogFieldFilter = (*CookieFilter)(nil)
_ LogFieldFilter = (*RegexpFilter)(nil) _ LogFieldFilter = (*RegexpFilter)(nil)
_ LogFieldFilter = (*RenameFilter)(nil) _ LogFieldFilter = (*RenameFilter)(nil)
_ LogFieldFilter = (*MultiRegexpFilter)(nil)
_ caddyfile.Unmarshaler = (*DeleteFilter)(nil) _ caddyfile.Unmarshaler = (*DeleteFilter)(nil)
_ caddyfile.Unmarshaler = (*HashFilter)(nil) _ caddyfile.Unmarshaler = (*HashFilter)(nil)
@ -673,9 +891,12 @@ var (
_ caddyfile.Unmarshaler = (*CookieFilter)(nil) _ caddyfile.Unmarshaler = (*CookieFilter)(nil)
_ caddyfile.Unmarshaler = (*RegexpFilter)(nil) _ caddyfile.Unmarshaler = (*RegexpFilter)(nil)
_ caddyfile.Unmarshaler = (*RenameFilter)(nil) _ caddyfile.Unmarshaler = (*RenameFilter)(nil)
_ caddyfile.Unmarshaler = (*MultiRegexpFilter)(nil)
_ caddy.Provisioner = (*IPMaskFilter)(nil) _ caddy.Provisioner = (*IPMaskFilter)(nil)
_ caddy.Provisioner = (*RegexpFilter)(nil) _ caddy.Provisioner = (*RegexpFilter)(nil)
_ caddy.Provisioner = (*MultiRegexpFilter)(nil)
_ caddy.Validator = (*QueryFilter)(nil) _ caddy.Validator = (*QueryFilter)(nil)
_ caddy.Validator = (*MultiRegexpFilter)(nil)
) )

View file

@ -1,6 +1,8 @@
package logging package logging
import ( import (
"fmt"
"strings"
"testing" "testing"
"go.uber.org/zap/zapcore" "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]) 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)
}
}

View file

@ -172,7 +172,7 @@ func (reconn *redialerConn) Write(b []byte) (n int, err error) {
reconn.connMu.RUnlock() reconn.connMu.RUnlock()
if conn != nil { if conn != nil {
if n, err = conn.Write(b); err == 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 // one of them might have already re-dialed by now; try writing again
if reconn.Conn != nil { if reconn.Conn != nil {
if n, err = reconn.Conn.Write(b); err == 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 { if err2 != nil {
// logger socket still offline; instead of discarding the log, dump it to stderr // logger socket still offline; instead of discarding the log, dump it to stderr
os.Stderr.Write(b) os.Stderr.Write(b)
return return n, err
} }
if n, err = conn2.Write(b); err == nil { if n, err = conn2.Write(b); err == nil {
if reconn.Conn != nil { if reconn.Conn != nil {
@ -211,7 +211,7 @@ func (reconn *redialerConn) Write(b []byte) (n int, err error) {
os.Stderr.Write(b) os.Stderr.Write(b)
} }
return return n, err
} }
func (reconn *redialerConn) dial() (net.Conn, error) { func (reconn *redialerConn) dial() (net.Conn, error) {

View file

@ -18,6 +18,7 @@ package caddy
import ( import (
"context" "context"
"errors"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
@ -48,7 +49,31 @@ func trapSignalsPosix() {
exitProcessFromSignal("SIGTERM") exitProcessFromSignal("SIGTERM")
case syscall.SIGUSR1: 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 <file> --adapter <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: case syscall.SIGUSR2:
Log().Info("not implemented", zap.String("signal", "SIGUSR2")) Log().Info("not implemented", zap.String("signal", "SIGUSR2"))

View file

@ -106,7 +106,7 @@ func (up *UsagePool) LoadOrNew(key any, construct Constructor) (value any, loade
} }
upv.Unlock() upv.Unlock()
} }
return return value, loaded, err
} }
// LoadOrStore loads the value associated with key from the pool if it // 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() up.Unlock()
value = val value = val
} }
return return value, loaded
} }
// Range iterates the pool similarly to how sync.Map.Range() does: // 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)) upv.value, upv.refs))
} }
} }
return return deleted, err
} }
// References returns the number of references (count of usages) to a // References returns the number of references (count of usages) to a