mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 08:42:31 -04:00
Compare commits
86 Commits
internal-tls
...
v2.11.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 03243e42fe | |||
| cb436f0a0e | |||
| a1081194bf | |||
| eec32a0bb5 | |||
| a2825c5dd9 | |||
| db256b53e5 | |||
| 6772ffb805 | |||
| 95941a71e8 | |||
| 3adcafd4c1 | |||
| 091add5ae3 | |||
| bdcdaf77ba | |||
| 9fe694c79c | |||
| b8b00d9160 | |||
| 68d50020ee | |||
| 8a18acc025 | |||
| 23d07ac89d | |||
| d64c7e67a4 | |||
| ff4f79aebe | |||
| f2213e943e | |||
| affbb99275 | |||
| d6a6b486db | |||
| 929d0e502a | |||
| 6718bd470f | |||
| 80bf81839d | |||
| d42d39b4bc | |||
| 0188ef2e62 | |||
| c0af7b665f | |||
| 72ac479f5d | |||
| 47f3e8f8dc | |||
| 03e6e439dd | |||
| 7c28c0c07a | |||
| 96f142c2a6 | |||
| 5ff50779cc | |||
| 1f43e8566b | |||
| bd374ca9d7 | |||
| 2ae0f7af69 | |||
| 58968b3fd3 | |||
| 42ca010e9d | |||
| 40927d2f75 | |||
| e0f8d9b204 | |||
| 3bb22672f9 | |||
| 935b09de83 | |||
| 7d24124430 | |||
| 565c1c3054 | |||
| d269405eab | |||
| e40bd019ff | |||
| cbebc1292b | |||
| e9d290de2f | |||
| 62134d65af | |||
| 5168acfb9c | |||
| 90972fbebc | |||
| 28103aafba | |||
| 6a57142896 | |||
| 80f2ae92cd | |||
| 7b031e1eb5 | |||
| b2d21f650a | |||
| 99d84be6dd | |||
| 1f1be3f4fe | |||
| 9eabd443cb | |||
| 5640611dfc | |||
| decc8a4d6f | |||
| 34fd2dfcff | |||
| 4037d05760 | |||
| 409a072135 | |||
| 6a4296b1a4 | |||
| 3c9c67e804 | |||
| 598b08f9ae | |||
| 374b7a637f | |||
| 6e0cbd0fa0 | |||
| bfdb04912d | |||
| 31960dc998 | |||
| be5f49fbeb | |||
| 7ebe72bbfe | |||
| 8a87bb3ffb | |||
| df9386fa12 | |||
| 786d537877 | |||
| 67a9e0657e | |||
| 2cb426776c | |||
| b9e6f3b227 | |||
| eead249382 | |||
| a6da1acdc8 | |||
| 56282c5737 | |||
| b3f2db233b | |||
| 07d2aaf22e | |||
| f2199d48b2 | |||
| 8285eba842 |
+3
-1
@@ -18,7 +18,7 @@ A security report must demonstrate a security bug in the source code from this r
|
|||||||
|
|
||||||
Some security problems are the result of interplay between different components of the Web, rather than a vulnerability in the web server itself. Please only report vulnerabilities in the web server itself, as we cannot coerce the rest of the Web to be fixed (for example, we do not consider IP spoofing, BGP hijacks, or missing/misconfigured HTTP headers a vulnerability in the Caddy web server).
|
Some security problems are the result of interplay between different components of the Web, rather than a vulnerability in the web server itself. Please only report vulnerabilities in the web server itself, as we cannot coerce the rest of the Web to be fixed (for example, we do not consider IP spoofing, BGP hijacks, or missing/misconfigured HTTP headers a vulnerability in the Caddy web server).
|
||||||
|
|
||||||
Vulnerabilities caused by misconfigurations are out of scope. Yes, it is entirely possible to craft and use a configuration that is unsafe, just like with every other web server; we recommend against doing that.
|
Vulnerabilities caused by misconfigurations are out of scope. Yes, it is entirely possible to craft and use a configuration that is unsafe, just like with every other web server; we recommend against doing that. Similarly, external misconfigurations are out of scope. For example, an open or forwarded port from a public network to a Caddy instance intended to serve only internal clients is not a vulnerability in Caddy.
|
||||||
|
|
||||||
We do not accept reports if the steps imply or require a compromised system or third-party software, as we cannot control those. We expect that users secure their own systems and keep all their software patched. For example, if untrusted users are able to upload/write/host arbitrary files in the web root directory, it is NOT a security bug in Caddy if those files get served to clients; however, it _would_ be a valid report if a bug in Caddy's source code unintentionally gave unauthorized users the ability to upload unsafe files or delete files without relying on an unpatched system or piece of software.
|
We do not accept reports if the steps imply or require a compromised system or third-party software, as we cannot control those. We expect that users secure their own systems and keep all their software patched. For example, if untrusted users are able to upload/write/host arbitrary files in the web root directory, it is NOT a security bug in Caddy if those files get served to clients; however, it _would_ be a valid report if a bug in Caddy's source code unintentionally gave unauthorized users the ability to upload unsafe files or delete files without relying on an unpatched system or piece of software.
|
||||||
|
|
||||||
@@ -33,6 +33,8 @@ We get a lot of difficult reports that turn out to be invalid. Clear, obvious re
|
|||||||
|
|
||||||
First please ensure your report falls within the accepted scope of security bugs (above).
|
First please ensure your report falls within the accepted scope of security bugs (above).
|
||||||
|
|
||||||
|
**YOU MUST DISCLOSE THE USE OF LLMs ("AI") INVOLVED IN ANY WAY.** Whether you are using AI for discovery, as part of writing the report or its replies, and/or testing or validating proofs and changes, we require you to mention the extent of it. **FAILURE TO INCLUDE A DISCLOSURE MAY LEAD TO IMMEDIATE DISMISSAL OF YOUR REPORT AND POTENTIAL BLOCKLISTING.**
|
||||||
|
|
||||||
We'll need enough information to verify the bug and make a patch. To speed things up, please include:
|
We'll need enough information to verify the bug and make a patch. To speed things up, please include:
|
||||||
|
|
||||||
- Most minimal possible config (without redactions!)
|
- Most minimal possible config (without redactions!)
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
@@ -31,13 +31,13 @@ jobs:
|
|||||||
- mac
|
- mac
|
||||||
- windows
|
- windows
|
||||||
go:
|
go:
|
||||||
- '1.25'
|
- '1.26'
|
||||||
|
|
||||||
include:
|
include:
|
||||||
# Set the minimum Go patch version for the given Go minor
|
# Set the minimum Go patch version for the given Go minor
|
||||||
# Usable via ${{ matrix.GO_SEMVER }}
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
- go: '1.25'
|
- go: '1.26'
|
||||||
GO_SEMVER: '~1.25.0'
|
GO_SEMVER: '~1.26.0'
|
||||||
|
|
||||||
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
||||||
# OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories)
|
# OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories)
|
||||||
@@ -235,7 +235,7 @@ jobs:
|
|||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: "~1.25"
|
go-version: "~1.26"
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Install xcaddy
|
- name: Install xcaddy
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ jobs:
|
|||||||
- 'darwin'
|
- 'darwin'
|
||||||
- 'netbsd'
|
- 'netbsd'
|
||||||
go:
|
go:
|
||||||
- '1.25'
|
- '1.26'
|
||||||
|
|
||||||
include:
|
include:
|
||||||
# Set the minimum Go patch version for the given Go minor
|
# Set the minimum Go patch version for the given Go minor
|
||||||
# Usable via ${{ matrix.GO_SEMVER }}
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
- go: '1.25'
|
- go: '1.26'
|
||||||
GO_SEMVER: '~1.25.0'
|
GO_SEMVER: '~1.26.0'
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: '~1.25'
|
go-version: '~1.26'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
@@ -80,7 +80,7 @@ jobs:
|
|||||||
- name: govulncheck
|
- name: govulncheck
|
||||||
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
|
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
|
||||||
with:
|
with:
|
||||||
go-version-input: '~1.25.0'
|
go-version-input: '~1.26.0'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
dependency-review:
|
dependency-review:
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
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
|
||||||
|
CLEANSED_COMMITS=$(echo "$COMMITS" | sed 's/`/\\`/g')
|
||||||
|
echo "changelog<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
echo "$CLEANSED_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
|
||||||
+388
-13
@@ -13,20 +13,334 @@ 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:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
go:
|
go:
|
||||||
- '1.25'
|
- '1.26'
|
||||||
|
|
||||||
include:
|
include:
|
||||||
# Set the minimum Go patch version for the given Go minor
|
# Set the minimum Go patch version for the given Go minor
|
||||||
# Usable via ${{ matrix.GO_SEMVER }}
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
- go: '1.25'
|
- go: '1.26'
|
||||||
GO_SEMVER: '~1.25.0'
|
GO_SEMVER: '~1.26.0'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
|
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
|
||||||
@@ -36,6 +350,8 @@ 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)
|
||||||
@@ -98,16 +414,6 @@ 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@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # main
|
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # main
|
||||||
- name: Cosign version
|
- name: Cosign version
|
||||||
@@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,24 +12,52 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<h3 align="center">Every site on HTTPS</h3>
|
<h3 align="center">Every site on HTTPS</h3>
|
||||||
<p align="center">Caddy is an extensible server platform that uses TLS by default.</p>
|
<p align="center">Caddy is an extensible server platform that uses TLS by default.</p>
|
||||||
<p align="center">
|
|
||||||
<a href="https://github.com/caddyserver/caddy/actions/workflows/ci.yml"><img src="https://github.com/caddyserver/caddy/actions/workflows/ci.yml/badge.svg"></a>
|
|
||||||
<a href="https://www.bestpractices.dev/projects/7141"><img src="https://www.bestpractices.dev/projects/7141/badge"></a>
|
|
||||||
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-%23007d9c.svg"></a>
|
|
||||||
<br>
|
|
||||||
<a href="https://x.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/twitter/follow/caddyserver" alt="@caddyserver on Twitter"></a>
|
|
||||||
<a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
|
|
||||||
<br>
|
|
||||||
<a href="https://sourcegraph.com/github.com/caddyserver/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/caddyserver/caddy/-/badge.svg" alt="Caddy on Sourcegraph"></a>
|
|
||||||
<a href="https://cloudsmith.io/~caddy/repos/"><img src="https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith" alt="Cloudsmith"></a>
|
|
||||||
</p>
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/caddyserver/caddy/releases">Releases</a> ·
|
<a href="https://github.com/caddyserver/caddy/releases">Releases</a> ·
|
||||||
<a href="https://caddyserver.com/docs/">Documentation</a> ·
|
<a href="https://caddyserver.com/docs/">Documentation</a> ·
|
||||||
<a href="https://caddy.community">Get Help</a>
|
<a href="https://caddy.community">Get Help</a>
|
||||||
</p>
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/caddyserver/caddy/actions/workflows/ci.yml"><img src="https://github.com/caddyserver/caddy/actions/workflows/ci.yml/badge.svg"></a>
|
||||||
|
|
||||||
|
<a href="https://www.bestpractices.dev/projects/7141"><img src="https://www.bestpractices.dev/projects/7141/badge"></a>
|
||||||
|
|
||||||
|
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-%23007d9c.svg"></a>
|
||||||
|
|
||||||
|
<a href="https://x.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/twitter/follow/caddyserver" alt="@caddyserver on Twitter"></a>
|
||||||
|
|
||||||
|
<a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
|
||||||
|
<br>
|
||||||
|
<a href="https://sourcegraph.com/github.com/caddyserver/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/caddyserver/caddy/-/badge.svg" alt="Caddy on Sourcegraph"></a>
|
||||||
|
|
||||||
|
<a href="https://cloudsmith.io/~caddy/repos/"><img src="https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith" alt="Cloudsmith"></a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<b>Powered by</b>
|
||||||
|
<br>
|
||||||
|
<a href="https://github.com/caddyserver/certmagic">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/55066419/206946718-740b6371-3df3-4d72-a822-47e4c48af999.png">
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png">
|
||||||
|
<img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Warp sponsorship requests this section -->
|
||||||
|
<div align="center" markdown="1">
|
||||||
|
<hr>
|
||||||
|
<sup>Special thanks to:</sup>
|
||||||
|
<br>
|
||||||
|
<a href="https://go.warp.dev/caddy">
|
||||||
|
<img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/c8efffde-18c7-4af4-83ed-b1aba2dda394">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### [Warp, built for coding with multiple AI agents](https://go.warp.dev/caddy)
|
||||||
|
[Available for MacOS, Linux, & Windows](https://go.warp.dev/caddy)<br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
### Menu
|
### Menu
|
||||||
|
|
||||||
@@ -44,18 +72,6 @@
|
|||||||
- [Getting help](#getting-help)
|
- [Getting help](#getting-help)
|
||||||
- [About](#about)
|
- [About](#about)
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<b>Powered by</b>
|
|
||||||
<br>
|
|
||||||
<a href="https://github.com/caddyserver/certmagic">
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/55066419/206946718-740b6371-3df3-4d72-a822-47e4c48af999.png">
|
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png">
|
|
||||||
<img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250">
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
## [Features](https://caddyserver.com/features)
|
## [Features](https://caddyserver.com/features)
|
||||||
|
|
||||||
@@ -117,11 +133,18 @@ username ALL=(ALL:ALL) NOPASSWD: /usr/sbin/setcap
|
|||||||
|
|
||||||
replacing `username` with your actual username. Please be careful and only do this if you know what you are doing! We are only qualified to document how to use Caddy, not Go tooling or your computer, and we are providing these instructions for convenience only; please learn how to use your own computer at your own risk and make any needful adjustments.
|
replacing `username` with your actual username. Please be careful and only do this if you know what you are doing! We are only qualified to document how to use Caddy, not Go tooling or your computer, and we are providing these instructions for convenience only; please learn how to use your own computer at your own risk and make any needful adjustments.
|
||||||
|
|
||||||
|
Then you can run the tests in all modules or a specific one:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ go test ./...
|
||||||
|
$ go test ./modules/caddyhttp/tracing/
|
||||||
|
```
|
||||||
|
|
||||||
### With version information and/or plugins
|
### With version information and/or plugins
|
||||||
|
|
||||||
Using [our builder tool, `xcaddy`](https://github.com/caddyserver/xcaddy)...
|
Using [our builder tool, `xcaddy`](https://github.com/caddyserver/xcaddy)...
|
||||||
|
|
||||||
```
|
```bash
|
||||||
$ xcaddy build
|
$ xcaddy build
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -197,6 +220,6 @@ Matthew Holt began developing Caddy in 2014 while studying computer science at B
|
|||||||
- _Project on X: [@caddyserver](https://x.com/caddyserver)_
|
- _Project on X: [@caddyserver](https://x.com/caddyserver)_
|
||||||
- _Author on X: [@mholt6](https://x.com/mholt6)_
|
- _Author on X: [@mholt6](https://x.com/mholt6)_
|
||||||
|
|
||||||
Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company.
|
Caddy is a project of [ZeroSSL](https://zerossl.com), an HID Global company.
|
||||||
|
|
||||||
Debian package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that enables your organization to create, store and share packages in any format, to any place, with total confidence.
|
Debian package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that enables your organization to create, store and share packages in any format, to any place, with total confidence.
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ import (
|
|||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// testCertMagicStorageOverride is a package-level test hook. Tests may set
|
||||||
|
// this variable to provide a temporary certmagic.Storage so that cert
|
||||||
|
// management in tests does not hit the real default storage on disk.
|
||||||
|
// This must NOT be set in production code.
|
||||||
|
var testCertMagicStorageOverride certmagic.Storage
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// The hard-coded default `DefaultAdminListen` can be overridden
|
// The hard-coded default `DefaultAdminListen` can be overridden
|
||||||
// by setting the `CADDY_ADMIN` environment variable.
|
// by setting the `CADDY_ADMIN` environment variable.
|
||||||
@@ -633,8 +639,19 @@ func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool)
|
|||||||
// certmagic config, although it'll be mostly useless for remote management
|
// certmagic config, although it'll be mostly useless for remote management
|
||||||
ident = new(IdentityConfig)
|
ident = new(IdentityConfig)
|
||||||
}
|
}
|
||||||
|
// Choose storage: prefer the package-level test override when present,
|
||||||
|
// otherwise use the configured DefaultStorage. Tests may set an override
|
||||||
|
// to divert storage into a temporary location. Otherwise, in production
|
||||||
|
// we use the DefaultStorage since we don't want to act as part of a
|
||||||
|
// cluster; this storage is for the server's local identity only.
|
||||||
|
var storage certmagic.Storage
|
||||||
|
if testCertMagicStorageOverride != nil {
|
||||||
|
storage = testCertMagicStorageOverride
|
||||||
|
} else {
|
||||||
|
storage = DefaultStorage
|
||||||
|
}
|
||||||
template := certmagic.Config{
|
template := certmagic.Config{
|
||||||
Storage: DefaultStorage, // do not act as part of a cluster (this is for the server's local identity)
|
Storage: storage,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
Issuers: ident.issuers,
|
Issuers: ident.issuers,
|
||||||
}
|
}
|
||||||
@@ -807,13 +824,38 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// common mitigations in browser contexts
|
||||||
if strings.Contains(r.Header.Get("Upgrade"), "websocket") {
|
if strings.Contains(r.Header.Get("Upgrade"), "websocket") {
|
||||||
// I've never been able demonstrate a vulnerability myself, but apparently
|
// I've never been able demonstrate a vulnerability myself, but apparently
|
||||||
// WebSocket connections originating from browsers aren't subject to CORS
|
// WebSocket connections originating from browsers aren't subject to CORS
|
||||||
// restrictions, so we'll just be on the safe side
|
// restrictions, so we'll just be on the safe side
|
||||||
h.handleError(w, r, fmt.Errorf("websocket connections aren't allowed"))
|
h.handleError(w, r, APIError{
|
||||||
|
HTTPStatus: http.StatusBadRequest,
|
||||||
|
Err: errors.New("websocket connections aren't allowed"),
|
||||||
|
Message: "WebSocket connections aren't allowed.",
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if strings.Contains(r.Header.Get("Sec-Fetch-Mode"), "no-cors") {
|
||||||
|
// turns out web pages can just disable the same-origin policy (!???!?)
|
||||||
|
// but at least browsers let us know that's the case, holy heck
|
||||||
|
h.handleError(w, r, APIError{
|
||||||
|
HTTPStatus: http.StatusBadRequest,
|
||||||
|
Err: errors.New("client attempted to make request by disabling same-origin policy using no-cors mode"),
|
||||||
|
Message: "Disabling same-origin restrictions is not allowed.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Header.Get("Origin") == "null" {
|
||||||
|
// bug in Firefox in certain cross-origin situations (yikes?)
|
||||||
|
// (not strictly a security vuln on its own, but it's red flaggy,
|
||||||
|
// since it seems to manifest in cross-origin contexts)
|
||||||
|
h.handleError(w, r, APIError{
|
||||||
|
HTTPStatus: http.StatusBadRequest,
|
||||||
|
Err: errors.New("invalid origin 'null'"),
|
||||||
|
Message: "Buggy browser is sending null Origin header.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if h.enforceHost {
|
if h.enforceHost {
|
||||||
// DNS rebinding mitigation
|
// DNS rebinding mitigation
|
||||||
@@ -824,7 +866,9 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.enforceOrigin {
|
_, hasOriginHeader := r.Header["Origin"]
|
||||||
|
_, hasSecHeader := r.Header["Sec-Fetch-Mode"]
|
||||||
|
if h.enforceOrigin || hasOriginHeader || hasSecHeader {
|
||||||
// cross-site mitigation
|
// cross-site mitigation
|
||||||
origin, err := h.checkOrigin(r)
|
origin, err := h.checkOrigin(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1110,7 +1154,10 @@ func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error
|
|||||||
if len(body) > 0 {
|
if len(body) > 0 {
|
||||||
err = json.Unmarshal(body, &val)
|
err = json.Unmarshal(body, &val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("decoding request body: %v", err)
|
if jsonErr, ok := err.(*json.SyntaxError); ok {
|
||||||
|
return fmt.Errorf("decoding request body: %w, at offset %d", jsonErr, jsonErr.Offset)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("decoding request body: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+40
-9
@@ -22,9 +22,11 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
@@ -275,13 +277,12 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := replaceLocalAdminServer(cfg, Context{})
|
// Build the admin handler directly (no listener active)
|
||||||
|
addr, err := ParseNetworkAddress("localhost:2019")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("setting up admin server: %v", err)
|
t.Fatalf("Failed to parse address: %v", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
handler := cfg.Admin.newAdminHandler(addr, false, Context{})
|
||||||
stopAdminServer(localAdminServer)
|
|
||||||
}()
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -314,7 +315,7 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
|
|||||||
req := httptest.NewRequest(test.method, fmt.Sprintf("http://localhost:2019%s", test.path), nil)
|
req := httptest.NewRequest(test.method, fmt.Sprintf("http://localhost:2019%s", test.path), nil)
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
localAdminServer.Handler.ServeHTTP(rr, req)
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
if rr.Code != test.expectedStatus {
|
if rr.Code != test.expectedStatus {
|
||||||
t.Errorf("expected status %d but got %d", test.expectedStatus, rr.Code)
|
t.Errorf("expected status %d but got %d", test.expectedStatus, rr.Code)
|
||||||
@@ -799,8 +800,24 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
|
|||||||
...
|
...
|
||||||
-----END PRIVATE KEY-----`)
|
-----END PRIVATE KEY-----`)
|
||||||
|
|
||||||
testStorage := certmagic.FileStorage{Path: t.TempDir()}
|
tmpDir, err := os.MkdirTemp("", "TestManageIdentity-")
|
||||||
err := testStorage.Store(context.Background(), "localhost/localhost.crt", certPEM)
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
testStorage := certmagic.FileStorage{Path: tmpDir}
|
||||||
|
// Clean up the temp dir after the test finishes. Ensure any background
|
||||||
|
// certificate maintenance is stopped first to avoid RemoveAll races.
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if identityCertCache != nil {
|
||||||
|
identityCertCache.Stop()
|
||||||
|
identityCertCache = nil
|
||||||
|
}
|
||||||
|
// Give goroutines a moment to exit and release file handles.
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
_ = os.RemoveAll(tmpDir)
|
||||||
|
})
|
||||||
|
|
||||||
|
err = testStorage.Store(context.Background(), "localhost/localhost.crt", certPEM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -862,7 +879,7 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
storage: &certmagic.FileStorage{Path: "testdata"},
|
storage: &testStorage,
|
||||||
},
|
},
|
||||||
checkState: func(t *testing.T, cfg *Config) {
|
checkState: func(t *testing.T, cfg *Config) {
|
||||||
if len(cfg.Admin.Identity.issuers) != 1 {
|
if len(cfg.Admin.Identity.issuers) != 1 {
|
||||||
@@ -900,6 +917,13 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
|
|||||||
identityCertCache.Stop()
|
identityCertCache.Stop()
|
||||||
identityCertCache = nil
|
identityCertCache = nil
|
||||||
}
|
}
|
||||||
|
// Ensure any cache started by manageIdentity is stopped at the end
|
||||||
|
defer func() {
|
||||||
|
if identityCertCache != nil {
|
||||||
|
identityCertCache.Stop()
|
||||||
|
identityCertCache = nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
ctx := Context{
|
ctx := Context{
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
@@ -907,6 +931,13 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
|
|||||||
moduleInstances: make(map[string][]Module),
|
moduleInstances: make(map[string][]Module),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this test provided a FileStorage, set the package-level
|
||||||
|
// testCertMagicStorageOverride so certmagicConfig will use it.
|
||||||
|
if test.cfg != nil && test.cfg.storage != nil {
|
||||||
|
testCertMagicStorageOverride = test.cfg.storage
|
||||||
|
defer func() { testCertMagicStorageOverride = nil }()
|
||||||
|
}
|
||||||
|
|
||||||
err := manageIdentity(ctx, test.cfg)
|
err := manageIdentity(ctx, test.cfg)
|
||||||
|
|
||||||
if test.wantErr {
|
if test.wantErr {
|
||||||
|
|||||||
@@ -147,8 +147,8 @@ func Load(cfgJSON []byte, forceReload bool) error {
|
|||||||
// the new value (if applicable; i.e. "DELETE" doesn't have an input).
|
// the new value (if applicable; i.e. "DELETE" doesn't have an input).
|
||||||
// If the resulting config is the same as the previous, no reload will
|
// If the resulting config is the same as the previous, no reload will
|
||||||
// occur unless forceReload is true. If the config is unchanged and not
|
// occur unless forceReload is true. If the config is unchanged and not
|
||||||
// forcefully reloaded, then errConfigUnchanged This function is safe for
|
// forcefully reloaded, then errConfigUnchanged is returned. This function
|
||||||
// concurrent use.
|
// is safe for concurrent use.
|
||||||
// The ifMatchHeader can optionally be given a string of the format:
|
// The ifMatchHeader can optionally be given a string of the format:
|
||||||
//
|
//
|
||||||
// "<path> <hash>"
|
// "<path> <hash>"
|
||||||
@@ -1092,7 +1092,7 @@ type Event struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewEvent creates a new event, but does not emit the event. To emit an
|
// NewEvent creates a new event, but does not emit the event. To emit an
|
||||||
// event, call Emit() on the current instance of the caddyevents app insteaad.
|
// event, call Emit() on the current instance of the caddyevents app instead.
|
||||||
//
|
//
|
||||||
// EXPERIMENTAL: Subject to change.
|
// EXPERIMENTAL: Subject to change.
|
||||||
func NewEvent(ctx Context, name string, data map[string]any) (Event, error) {
|
func NewEvent(ctx Context, name string, data map[string]any) (Event, error) {
|
||||||
@@ -1250,10 +1250,10 @@ func getLastConfig() (file, adapter string, fn reloadFromSourceFunc) {
|
|||||||
|
|
||||||
// lastConfigMatches returns true if the provided source file and/or adapter
|
// lastConfigMatches returns true if the provided source file and/or adapter
|
||||||
// matches the recorded last-config. Matching rules (in priority order):
|
// matches the recorded last-config. Matching rules (in priority order):
|
||||||
// 1. If srcAdapter is provided and differs from the recorded adapter, no match.
|
// 1. If srcAdapter is provided and differs from the recorded adapter, no match.
|
||||||
// 2. If srcFile exactly equals the recorded file, match.
|
// 2. If srcFile exactly equals the recorded file, match.
|
||||||
// 3. If both sides can be made absolute and equal, match.
|
// 3. If both sides can be made absolute and equal, match.
|
||||||
// 4. If basenames are equal, match.
|
// 4. If basenames are equal, match.
|
||||||
func lastConfigMatches(srcFile, srcAdapter string) bool {
|
func lastConfigMatches(srcFile, srcAdapter string) bool {
|
||||||
lf, la, _ := getLastConfig()
|
lf, la, _ := getLastConfig()
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -209,6 +210,15 @@ func Format(input []byte) []byte {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.Contains(quotes, "`") {
|
||||||
|
if ch == '`' && space && !beginningOfLine {
|
||||||
|
write(' ')
|
||||||
|
}
|
||||||
|
write(ch)
|
||||||
|
space = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if unicode.IsSpace(ch) {
|
if unicode.IsSpace(ch) {
|
||||||
space = true
|
space = true
|
||||||
heredocEscaped = false
|
heredocEscaped = false
|
||||||
|
|||||||
@@ -464,6 +464,17 @@ block2 {
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "issue #7425: multiline backticked string indentation",
|
||||||
|
input: `https://localhost:8953 {
|
||||||
|
respond ` + "`" + `Here are some random numbers:
|
||||||
|
|
||||||
|
{{randNumeric 16}}
|
||||||
|
|
||||||
|
Hope this helps.` + "`" + `
|
||||||
|
}`,
|
||||||
|
expect: "https://localhost:8953 {\n\trespond `Here are some random numbers:\n\n{{randNumeric 16}}\n\nHope this helps.`\n}",
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
// the formatter should output a trailing newline,
|
// the formatter should output a trailing newline,
|
||||||
// even if the tests aren't written to expect that
|
// even if the tests aren't written to expect that
|
||||||
|
|||||||
@@ -761,7 +761,7 @@ type ServerBlock struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sb ServerBlock) GetKeysText() []string {
|
func (sb ServerBlock) GetKeysText() []string {
|
||||||
res := []string{}
|
res := make([]string, 0, len(sb.Keys))
|
||||||
for _, k := range sb.Keys {
|
for _, k := range sb.Keys {
|
||||||
res = append(res, k.Text)
|
res = append(res, k.Text)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,11 @@ func JSONModuleObject(val any, fieldName, fieldVal string, warnings *[]Warning)
|
|||||||
err = json.Unmarshal(enc, &tmp)
|
err = json.Unmarshal(enc, &tmp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if warnings != nil {
|
if warnings != nil {
|
||||||
*warnings = append(*warnings, Warning{Message: err.Error()})
|
message := err.Error()
|
||||||
|
if jsonErr, ok := err.(*json.SyntaxError); ok {
|
||||||
|
message = fmt.Sprintf("%v, at offset %d", jsonErr.Error(), jsonErr.Offset)
|
||||||
|
}
|
||||||
|
*warnings = append(*warnings, Warning{Message: message})
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ func parseBind(h Helper) ([]ConfigValue, error) {
|
|||||||
// issuer <module_name> [...]
|
// issuer <module_name> [...]
|
||||||
// get_certificate <module_name> [...]
|
// get_certificate <module_name> [...]
|
||||||
// insecure_secrets_log <log_file>
|
// insecure_secrets_log <log_file>
|
||||||
|
// renewal_window_ratio <ratio>
|
||||||
// }
|
// }
|
||||||
func parseTLS(h Helper) ([]ConfigValue, error) {
|
func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||||
h.Next() // consume directive name
|
h.Next() // consume directive name
|
||||||
@@ -129,6 +130,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
var onDemand bool
|
var onDemand bool
|
||||||
var reusePrivateKeys bool
|
var reusePrivateKeys bool
|
||||||
var forceAutomate bool
|
var forceAutomate bool
|
||||||
|
var renewalWindowRatio float64
|
||||||
|
|
||||||
// Track which DNS challenge options are set
|
// Track which DNS challenge options are set
|
||||||
var dnsOptionsSet []string
|
var dnsOptionsSet []string
|
||||||
@@ -473,6 +475,20 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
}
|
}
|
||||||
cp.InsecureSecretsLog = h.Val()
|
cp.InsecureSecretsLog = h.Val()
|
||||||
|
|
||||||
|
case "renewal_window_ratio":
|
||||||
|
arg := h.RemainingArgs()
|
||||||
|
if len(arg) != 1 {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
ratio, err := strconv.ParseFloat(arg[0], 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, h.Errf("parsing renewal_window_ratio: %v", err)
|
||||||
|
}
|
||||||
|
if ratio <= 0 || ratio >= 1 {
|
||||||
|
return nil, h.Errf("renewal_window_ratio must be between 0 and 1 (exclusive)")
|
||||||
|
}
|
||||||
|
renewalWindowRatio = ratio
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, h.Errf("unknown subdirective: %s", h.Val())
|
return nil, h.Errf("unknown subdirective: %s", h.Val())
|
||||||
}
|
}
|
||||||
@@ -597,6 +613,14 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// renewal window ratio
|
||||||
|
if renewalWindowRatio > 0 {
|
||||||
|
configVals = append(configVals, ConfigValue{
|
||||||
|
Class: "tls.renewal_window_ratio",
|
||||||
|
Value: renewalWindowRatio,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// if enabled, the names in the site addresses will be
|
// if enabled, the names in the site addresses will be
|
||||||
// added to the automation policies
|
// added to the automation policies
|
||||||
if forceAutomate {
|
if forceAutomate {
|
||||||
@@ -930,6 +954,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
|||||||
// modifications to the parsing behavior.
|
// modifications to the parsing behavior.
|
||||||
parseAsGlobalOption := globalLogNames != nil
|
parseAsGlobalOption := globalLogNames != nil
|
||||||
|
|
||||||
|
// nolint:prealloc
|
||||||
var configValues []ConfigValue
|
var configValues []ConfigValue
|
||||||
|
|
||||||
// Logic below expects that a name is always present when a
|
// Logic below expects that a name is always present when a
|
||||||
|
|||||||
@@ -851,6 +851,20 @@ func (st *ServerType) serversFromPairings(
|
|||||||
srv.ListenerWrappersRaw = append(srv.ListenerWrappersRaw, jsonListenerWrapper)
|
srv.ListenerWrappersRaw = append(srv.ListenerWrappersRaw, jsonListenerWrapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Look for any config values that provide packet conn wrappers on the server block
|
||||||
|
for _, listenerConfig := range sblock.pile["packet_conn_wrapper"] {
|
||||||
|
packetConnWrapper, ok := listenerConfig.Value.(caddy.PacketConnWrapper)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("config for a packet conn wrapper did not provide a value that implements caddy.PacketConnWrapper")
|
||||||
|
}
|
||||||
|
jsonPacketConnWrapper := caddyconfig.JSONModuleObject(
|
||||||
|
packetConnWrapper,
|
||||||
|
"wrapper",
|
||||||
|
packetConnWrapper.(caddy.Module).CaddyModule().ID.Name(),
|
||||||
|
warnings)
|
||||||
|
srv.PacketConnWrappersRaw = append(srv.PacketConnWrappersRaw, jsonPacketConnWrapper)
|
||||||
|
}
|
||||||
|
|
||||||
// set up each handler directive, making sure to honor directive order
|
// set up each handler directive, making sure to honor directive order
|
||||||
dirRoutes := sblock.pile["route"]
|
dirRoutes := sblock.pile["route"]
|
||||||
siteSubroute, err := buildSubroute(dirRoutes, groupCounter, true)
|
siteSubroute, err := buildSubroute(dirRoutes, groupCounter, true)
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func init() {
|
|||||||
RegisterGlobalOption("persist_config", parseOptPersistConfig)
|
RegisterGlobalOption("persist_config", parseOptPersistConfig)
|
||||||
RegisterGlobalOption("dns", parseOptDNS)
|
RegisterGlobalOption("dns", parseOptDNS)
|
||||||
RegisterGlobalOption("ech", parseOptECH)
|
RegisterGlobalOption("ech", parseOptECH)
|
||||||
|
RegisterGlobalOption("renewal_window_ratio", parseOptRenewalWindowRatio)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil }
|
func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil }
|
||||||
@@ -457,9 +458,8 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
|
|||||||
case "disable_redirects":
|
case "disable_redirects":
|
||||||
case "disable_certs":
|
case "disable_certs":
|
||||||
case "ignore_loaded_certs":
|
case "ignore_loaded_certs":
|
||||||
case "prefer_wildcard":
|
|
||||||
default:
|
default:
|
||||||
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', 'ignore_loaded_certs', or 'prefer_wildcard'")
|
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', or 'ignore_loaded_certs'")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return val, nil
|
return val, nil
|
||||||
@@ -472,6 +472,8 @@ func unmarshalCaddyfileMetricsOptions(d *caddyfile.Dispenser) (any, error) {
|
|||||||
switch d.Val() {
|
switch d.Val() {
|
||||||
case "per_host":
|
case "per_host":
|
||||||
metrics.PerHost = true
|
metrics.PerHost = true
|
||||||
|
case "observe_catchall_hosts":
|
||||||
|
metrics.ObserveCatchallHosts = true
|
||||||
default:
|
default:
|
||||||
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
||||||
}
|
}
|
||||||
@@ -623,3 +625,22 @@ func parseOptECH(d *caddyfile.Dispenser, _ any) (any, error) {
|
|||||||
|
|
||||||
return ech, nil
|
return ech, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseOptRenewalWindowRatio(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
|
d.Next() // consume option name
|
||||||
|
if !d.Next() {
|
||||||
|
return 0, d.ArgErr()
|
||||||
|
}
|
||||||
|
val := d.Val()
|
||||||
|
ratio, err := strconv.ParseFloat(val, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, d.Errf("parsing renewal_window_ratio: %v", err)
|
||||||
|
}
|
||||||
|
if ratio <= 0 || ratio >= 1 {
|
||||||
|
return 0, d.Errf("renewal_window_ratio must be between 0 and 1 (exclusive)")
|
||||||
|
}
|
||||||
|
if d.Next() {
|
||||||
|
return 0, d.ArgErr()
|
||||||
|
}
|
||||||
|
return ratio, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package httpcaddyfile
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
@@ -27,14 +28,16 @@ func init() {
|
|||||||
RegisterGlobalOption("pki", parsePKIApp)
|
RegisterGlobalOption("pki", parsePKIApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parsePKIApp parses the global log option. Syntax:
|
// parsePKIApp parses the global pki option. Syntax:
|
||||||
//
|
//
|
||||||
// pki {
|
// pki {
|
||||||
// ca [<id>] {
|
// ca [<id>] {
|
||||||
// name <name>
|
// name <name>
|
||||||
// root_cn <name>
|
// root_cn <name>
|
||||||
// intermediate_cn <name>
|
// intermediate_cn <name>
|
||||||
// intermediate_lifetime <duration>
|
// intermediate_lifetime <duration>
|
||||||
|
// maintenance_interval <duration>
|
||||||
|
// renewal_window_ratio <ratio>
|
||||||
// root {
|
// root {
|
||||||
// cert <path>
|
// cert <path>
|
||||||
// key <path>
|
// key <path>
|
||||||
@@ -99,6 +102,26 @@ func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) {
|
|||||||
}
|
}
|
||||||
pkiCa.IntermediateLifetime = caddy.Duration(dur)
|
pkiCa.IntermediateLifetime = caddy.Duration(dur)
|
||||||
|
|
||||||
|
case "maintenance_interval":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
dur, err := caddy.ParseDuration(d.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pkiCa.MaintenanceInterval = caddy.Duration(dur)
|
||||||
|
|
||||||
|
case "renewal_window_ratio":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
ratio, err := strconv.ParseFloat(d.Val(), 64)
|
||||||
|
if err != nil || ratio <= 0 || ratio > 1 {
|
||||||
|
return nil, d.Errf("renewal_window_ratio must be a number in (0, 1], got %s", d.Val())
|
||||||
|
}
|
||||||
|
pkiCa.RenewalWindowRatio = ratio
|
||||||
|
|
||||||
case "root":
|
case "root":
|
||||||
if pkiCa.Root == nil {
|
if pkiCa.Root == nil {
|
||||||
pkiCa.Root = new(caddypki.KeyPair)
|
pkiCa.Root = new(caddypki.KeyPair)
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// 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 httpcaddyfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParsePKIApp_maintenanceIntervalAndRenewalWindowRatio(t *testing.T) {
|
||||||
|
input := `{
|
||||||
|
pki {
|
||||||
|
ca local {
|
||||||
|
maintenance_interval 5m
|
||||||
|
renewal_window_ratio 0.15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:8080 {
|
||||||
|
}
|
||||||
|
`
|
||||||
|
adapter := caddyfile.Adapter{ServerType: ServerType{}}
|
||||||
|
out, _, err := adapter.Adapt([]byte(input), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Adapt failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg struct {
|
||||||
|
Apps struct {
|
||||||
|
PKI struct {
|
||||||
|
CertificateAuthorities map[string]struct {
|
||||||
|
MaintenanceInterval int64 `json:"maintenance_interval,omitempty"`
|
||||||
|
RenewalWindowRatio float64 `json:"renewal_window_ratio,omitempty"`
|
||||||
|
} `json:"certificate_authorities,omitempty"`
|
||||||
|
} `json:"pki,omitempty"`
|
||||||
|
} `json:"apps"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(out, &cfg); err != nil {
|
||||||
|
t.Fatalf("unmarshal config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca, ok := cfg.Apps.PKI.CertificateAuthorities["local"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected certificate_authorities.local to exist")
|
||||||
|
}
|
||||||
|
wantInterval := 5 * time.Minute.Nanoseconds()
|
||||||
|
if ca.MaintenanceInterval != wantInterval {
|
||||||
|
t.Errorf("maintenance_interval = %d, want %d (5m)", ca.MaintenanceInterval, wantInterval)
|
||||||
|
}
|
||||||
|
if ca.RenewalWindowRatio != 0.15 {
|
||||||
|
t.Errorf("renewal_window_ratio = %v, want 0.15", ca.RenewalWindowRatio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePKIApp_renewalWindowRatioInvalid(t *testing.T) {
|
||||||
|
input := `{
|
||||||
|
pki {
|
||||||
|
ca local {
|
||||||
|
renewal_window_ratio 1.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:8080 {
|
||||||
|
}
|
||||||
|
`
|
||||||
|
adapter := caddyfile.Adapter{ServerType: ServerType{}}
|
||||||
|
_, _, err := adapter.Adapt([]byte(input), nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for renewal_window_ratio > 1")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,26 +36,30 @@ type serverOptions struct {
|
|||||||
ListenerAddress string
|
ListenerAddress string
|
||||||
|
|
||||||
// These will all map 1:1 to the caddyhttp.Server struct
|
// These will all map 1:1 to the caddyhttp.Server struct
|
||||||
Name string
|
Name string
|
||||||
ListenerWrappersRaw []json.RawMessage
|
ListenerWrappersRaw []json.RawMessage
|
||||||
ReadTimeout caddy.Duration
|
PacketConnWrappersRaw []json.RawMessage
|
||||||
ReadHeaderTimeout caddy.Duration
|
ReadTimeout caddy.Duration
|
||||||
WriteTimeout caddy.Duration
|
ReadHeaderTimeout caddy.Duration
|
||||||
IdleTimeout caddy.Duration
|
WriteTimeout caddy.Duration
|
||||||
KeepAliveInterval caddy.Duration
|
IdleTimeout caddy.Duration
|
||||||
KeepAliveIdle caddy.Duration
|
KeepAliveInterval caddy.Duration
|
||||||
KeepAliveCount int
|
KeepAliveIdle caddy.Duration
|
||||||
MaxHeaderBytes int
|
KeepAliveCount int
|
||||||
EnableFullDuplex bool
|
MaxHeaderBytes int
|
||||||
Protocols []string
|
EnableFullDuplex bool
|
||||||
StrictSNIHost *bool
|
Protocols []string
|
||||||
TrustedProxiesRaw json.RawMessage
|
StrictSNIHost *bool
|
||||||
TrustedProxiesStrict int
|
TrustedProxiesRaw json.RawMessage
|
||||||
TrustedProxiesUnix bool
|
TrustedProxiesStrict int
|
||||||
ClientIPHeaders []string
|
TrustedProxiesUnix bool
|
||||||
ShouldLogCredentials bool
|
ClientIPHeaders []string
|
||||||
Metrics *caddyhttp.Metrics
|
ShouldLogCredentials bool
|
||||||
Trace bool // TODO: EXPERIMENTAL
|
Metrics *caddyhttp.Metrics
|
||||||
|
Trace bool // TODO: EXPERIMENTAL
|
||||||
|
// If set, overrides whether QUIC listeners allow 0-RTT (early data).
|
||||||
|
// If nil, the default behavior is used (currently allowed).
|
||||||
|
Allow0RTT *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||||
@@ -99,6 +103,26 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
|||||||
serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper)
|
serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "packet_conn_wrappers":
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
modID := "caddy.packetconns." + d.Val()
|
||||||
|
unm, err := caddyfile.UnmarshalModule(d, modID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
packetConnWrapper, ok := unm.(caddy.PacketConnWrapper)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("module %s (%T) is not a packet conn wrapper", modID, unm)
|
||||||
|
}
|
||||||
|
jsonPacketConnWrapper := caddyconfig.JSONModuleObject(
|
||||||
|
packetConnWrapper,
|
||||||
|
"wrapper",
|
||||||
|
packetConnWrapper.(caddy.Module).CaddyModule().ID.Name(),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
serverOpts.PacketConnWrappersRaw = append(serverOpts.PacketConnWrappersRaw, jsonPacketConnWrapper)
|
||||||
|
}
|
||||||
|
|
||||||
case "timeouts":
|
case "timeouts":
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
switch d.Val() {
|
switch d.Val() {
|
||||||
@@ -288,6 +312,17 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
|||||||
}
|
}
|
||||||
serverOpts.Trace = true
|
serverOpts.Trace = true
|
||||||
|
|
||||||
|
case "0rtt":
|
||||||
|
// only supports "off" for now
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
if d.Val() != "off" {
|
||||||
|
return nil, d.Errf("unsupported 0rtt argument '%s' (only 'off' is supported)", d.Val())
|
||||||
|
}
|
||||||
|
boolVal := false
|
||||||
|
serverOpts.Allow0RTT = &boolVal
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
||||||
}
|
}
|
||||||
@@ -335,6 +370,7 @@ func applyServerOptions(
|
|||||||
|
|
||||||
// set all the options
|
// set all the options
|
||||||
server.ListenerWrappersRaw = opts.ListenerWrappersRaw
|
server.ListenerWrappersRaw = opts.ListenerWrappersRaw
|
||||||
|
server.PacketConnWrappersRaw = opts.PacketConnWrappersRaw
|
||||||
server.ReadTimeout = opts.ReadTimeout
|
server.ReadTimeout = opts.ReadTimeout
|
||||||
server.ReadHeaderTimeout = opts.ReadHeaderTimeout
|
server.ReadHeaderTimeout = opts.ReadHeaderTimeout
|
||||||
server.WriteTimeout = opts.WriteTimeout
|
server.WriteTimeout = opts.WriteTimeout
|
||||||
@@ -351,6 +387,7 @@ func applyServerOptions(
|
|||||||
server.TrustedProxiesStrict = opts.TrustedProxiesStrict
|
server.TrustedProxiesStrict = opts.TrustedProxiesStrict
|
||||||
server.TrustedProxiesUnix = opts.TrustedProxiesUnix
|
server.TrustedProxiesUnix = opts.TrustedProxiesUnix
|
||||||
server.Metrics = opts.Metrics
|
server.Metrics = opts.Metrics
|
||||||
|
server.Allow0RTT = opts.Allow0RTT
|
||||||
if opts.ShouldLogCredentials {
|
if opts.ShouldLogCredentials {
|
||||||
if server.Logs == nil {
|
if server.Logs == nil {
|
||||||
server.Logs = new(caddyhttp.ServerLogConfig)
|
server.Logs = new(caddyhttp.ServerLogConfig)
|
||||||
|
|||||||
@@ -92,26 +92,8 @@ func (st ServerType) buildTLSApp(
|
|||||||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
|
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
|
||||||
}
|
}
|
||||||
|
|
||||||
var wildcardHosts []string // collect all hosts that have a wildcard in them, and aren't HTTP
|
|
||||||
forcedAutomatedNames := make(map[string]struct{}) // explicitly configured to be automated, even if covered by a wildcard
|
forcedAutomatedNames := make(map[string]struct{}) // explicitly configured to be automated, even if covered by a wildcard
|
||||||
|
|
||||||
for _, p := range pairings {
|
|
||||||
var addresses []string
|
|
||||||
for _, addressWithProtocols := range p.addressesWithProtocols {
|
|
||||||
addresses = append(addresses, addressWithProtocols.address)
|
|
||||||
}
|
|
||||||
if !listenersUseAnyPortOtherThan(addresses, httpPort) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, sblock := range p.serverBlocks {
|
|
||||||
for _, addr := range sblock.parsedKeys {
|
|
||||||
if strings.HasPrefix(addr.Host, "*.") {
|
|
||||||
wildcardHosts = append(wildcardHosts, addr.Host[2:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range pairings {
|
for _, p := range pairings {
|
||||||
// avoid setting up TLS automation policies for a server that is HTTP-only
|
// avoid setting up TLS automation policies for a server that is HTTP-only
|
||||||
var addresses []string
|
var addresses []string
|
||||||
@@ -135,12 +117,6 @@ func (st ServerType) buildTLSApp(
|
|||||||
return nil, warnings, err
|
return nil, warnings, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// make a plain copy so we can compare whether we made any changes
|
|
||||||
apCopy, err := newBaseAutomationPolicy(options, warnings, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, warnings, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sblockHosts := sblock.hostsFromKeys(false)
|
sblockHosts := sblock.hostsFromKeys(false)
|
||||||
if len(sblockHosts) == 0 && catchAllAP != nil {
|
if len(sblockHosts) == 0 && catchAllAP != nil {
|
||||||
ap = catchAllAP
|
ap = catchAllAP
|
||||||
@@ -167,6 +143,12 @@ func (st ServerType) buildTLSApp(
|
|||||||
ap.KeyType = keyTypeVals[0].Value.(string)
|
ap.KeyType = keyTypeVals[0].Value.(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if renewalWindowRatioVals, ok := sblock.pile["tls.renewal_window_ratio"]; ok {
|
||||||
|
ap.RenewalWindowRatio = renewalWindowRatioVals[0].Value.(float64)
|
||||||
|
} else if globalRenewalWindowRatio, ok := options["renewal_window_ratio"]; ok {
|
||||||
|
ap.RenewalWindowRatio = globalRenewalWindowRatio.(float64)
|
||||||
|
}
|
||||||
|
|
||||||
// certificate issuers
|
// certificate issuers
|
||||||
if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
|
if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
|
||||||
var issuers []certmagic.Issuer
|
var issuers []certmagic.Issuer
|
||||||
@@ -253,16 +235,6 @@ func (st ServerType) buildTLSApp(
|
|||||||
hostsNotHTTP := sblock.hostsFromKeysNotHTTP(httpPort)
|
hostsNotHTTP := sblock.hostsFromKeysNotHTTP(httpPort)
|
||||||
sort.Strings(hostsNotHTTP) // solely for deterministic test results
|
sort.Strings(hostsNotHTTP) // solely for deterministic test results
|
||||||
|
|
||||||
// if the we prefer wildcards and the AP is unchanged,
|
|
||||||
// then we can skip this AP because it should be covered
|
|
||||||
// by an AP with a wildcard
|
|
||||||
if slices.Contains(autoHTTPS, "prefer_wildcard") {
|
|
||||||
if hostsCoveredByWildcard(hostsNotHTTP, wildcardHosts) &&
|
|
||||||
reflect.DeepEqual(ap, apCopy) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// associate our new automation policy with this server block's hosts
|
// associate our new automation policy with this server block's hosts
|
||||||
ap.SubjectsRaw = hostsNotHTTP
|
ap.SubjectsRaw = hostsNotHTTP
|
||||||
|
|
||||||
@@ -576,9 +548,8 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
|||||||
if acmeIssuer.Challenges.DNS == nil {
|
if acmeIssuer.Challenges.DNS == nil {
|
||||||
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
||||||
}
|
}
|
||||||
// If global `dns` is set, do NOT set provider in issuer, just set empty dns config
|
if globalACMEDNS != nil && acmeIssuer.Challenges.DNS.ProviderRaw == nil {
|
||||||
if globalDNS == nil && acmeIssuer.Challenges.DNS.ProviderRaw == nil {
|
// Set a global DNS provider if `acme_dns` is set
|
||||||
// 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)
|
acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -641,7 +612,8 @@ func newBaseAutomationPolicy(
|
|||||||
_, hasLocalCerts := options["local_certs"]
|
_, hasLocalCerts := options["local_certs"]
|
||||||
keyType, hasKeyType := options["key_type"]
|
keyType, hasKeyType := options["key_type"]
|
||||||
ocspStapling, hasOCSPStapling := options["ocsp_stapling"]
|
ocspStapling, hasOCSPStapling := options["ocsp_stapling"]
|
||||||
hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling
|
renewalWindowRatio, hasRenewalWindowRatio := options["renewal_window_ratio"]
|
||||||
|
hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling || hasRenewalWindowRatio
|
||||||
|
|
||||||
globalACMECA := options["acme_ca"]
|
globalACMECA := options["acme_ca"]
|
||||||
globalACMECARoot := options["acme_ca_root"]
|
globalACMECARoot := options["acme_ca_root"]
|
||||||
@@ -688,6 +660,10 @@ func newBaseAutomationPolicy(
|
|||||||
ap.OCSPOverrides = ocspConfig.ResponderOverrides
|
ap.OCSPOverrides = ocspConfig.ResponderOverrides
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasRenewalWindowRatio {
|
||||||
|
ap.RenewalWindowRatio = renewalWindowRatio.(float64)
|
||||||
|
}
|
||||||
|
|
||||||
return ap, nil
|
return ap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -849,20 +825,3 @@ func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
|
|||||||
func isTailscaleDomain(name string) bool {
|
func isTailscaleDomain(name string) bool {
|
||||||
return strings.HasSuffix(strings.ToLower(name), ".ts.net")
|
return strings.HasSuffix(strings.ToLower(name), ".ts.net")
|
||||||
}
|
}
|
||||||
|
|
||||||
func hostsCoveredByWildcard(hosts []string, wildcards []string) bool {
|
|
||||||
if len(hosts) == 0 || len(wildcards) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, host := range hosts {
|
|
||||||
for _, wildcard := range wildcards {
|
|
||||||
if strings.HasPrefix(host, "*.") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if certmagic.MatchWildcard(host, "*."+wildcard) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response, error) {
|
func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response, error) {
|
||||||
resp, err := client.Do(request)
|
resp, err := client.Do(request) //nolint:gosec // no SSRF; comes from trusted config
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("problem calling http loader url: %v", err)
|
return nil, fmt.Errorf("problem calling http loader url: %v", err)
|
||||||
} else if resp.StatusCode < 200 || resp.StatusCode > 499 {
|
} else if resp.StatusCode < 200 || resp.StatusCode > 499 {
|
||||||
|
|||||||
+1
-1
@@ -106,7 +106,7 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
caddy.Log().Named("admin.api.load").Error(err.Error())
|
caddy.Log().Named("admin.api.load").Error(err.Error())
|
||||||
}
|
}
|
||||||
_, _ = w.Write(respBody)
|
_, _ = w.Write(respBody) //nolint:gosec // false positive: no XSS here
|
||||||
}
|
}
|
||||||
body = result
|
body = result
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-3
@@ -187,7 +187,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
|
|||||||
req.Header.Add("Content-Type", "text/"+configType)
|
req.Header.Add("Content-Type", "text/"+configType)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := client.Do(req)
|
res, err := client.Do(req) //nolint:gosec // no SSRF because URL is hard-coded to localhost, and port comes from config
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("unable to contact caddy server. %s", err)
|
tc.t.Errorf("unable to contact caddy server. %s", err)
|
||||||
return err
|
return err
|
||||||
@@ -279,7 +279,7 @@ func validateTestPrerequisites(tc *Tester) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tc.t.Cleanup(func() {
|
tc.t.Cleanup(func() {
|
||||||
os.Remove(f.Name())
|
os.Remove(f.Name()) //nolint:gosec // false positive, filename comes from std lib, no path traversal
|
||||||
})
|
})
|
||||||
if _, err := fmt.Fprintf(f, initConfig, tc.config.AdminPort); err != nil {
|
if _, err := fmt.Fprintf(f, initConfig, tc.config.AdminPort); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -362,6 +362,8 @@ func CreateTestingTransport() *http.Transport {
|
|||||||
|
|
||||||
// AssertLoadError will load a config and expect an error
|
// AssertLoadError will load a config and expect an error
|
||||||
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
|
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
tc := NewTester(t)
|
tc := NewTester(t)
|
||||||
|
|
||||||
err := tc.initServer(rawConfig, configType)
|
err := tc.initServer(rawConfig, configType)
|
||||||
@@ -372,6 +374,8 @@ func AssertLoadError(t *testing.T, rawConfig string, configType string, expected
|
|||||||
|
|
||||||
// AssertRedirect makes a request and asserts the redirection happens
|
// AssertRedirect makes a request and asserts the redirection happens
|
||||||
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
|
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
|
||||||
|
tc.t.Helper()
|
||||||
|
|
||||||
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
|
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
|
||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
}
|
}
|
||||||
@@ -409,6 +413,8 @@ func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, e
|
|||||||
|
|
||||||
// CompareAdapt adapts a config and then compares it against an expected result
|
// CompareAdapt adapts a config and then compares it against an expected result
|
||||||
func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool {
|
func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
cfgAdapter := caddyconfig.GetAdapter(adapterName)
|
cfgAdapter := caddyconfig.GetAdapter(adapterName)
|
||||||
if cfgAdapter == nil {
|
if cfgAdapter == nil {
|
||||||
t.Logf("unrecognized config adapter '%s'", adapterName)
|
t.Logf("unrecognized config adapter '%s'", adapterName)
|
||||||
@@ -468,6 +474,8 @@ func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string,
|
|||||||
|
|
||||||
// AssertAdapt adapts a config and then tests it against an expected result
|
// AssertAdapt adapts a config and then tests it against an expected result
|
||||||
func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) {
|
func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
|
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
@@ -496,7 +504,9 @@ func applyHeaders(t testing.TB, req *http.Request, requestHeaders []string) {
|
|||||||
|
|
||||||
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
|
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
|
||||||
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
|
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
|
||||||
resp, err := tc.Client.Do(req)
|
tc.t.Helper()
|
||||||
|
|
||||||
|
resp, err := tc.Client.Do(req) //nolint:gosec // no SSRFs demonstrated
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Fatalf("failed to call server %s", err)
|
tc.t.Fatalf("failed to call server %s", err)
|
||||||
}
|
}
|
||||||
@@ -510,6 +520,8 @@ func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int)
|
|||||||
|
|
||||||
// AssertResponse request a URI and assert the status code and the body contains a string
|
// AssertResponse request a URI and assert the status code and the body contains a string
|
||||||
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
tc.t.Helper()
|
||||||
|
|
||||||
resp := tc.AssertResponseCode(req, expectedStatusCode)
|
resp := tc.AssertResponseCode(req, expectedStatusCode)
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@@ -531,6 +543,8 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
|
|||||||
|
|
||||||
// AssertGetResponse GET a URI and expect a statusCode and body text
|
// AssertGetResponse GET a URI and expect a statusCode and body text
|
||||||
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
tc.t.Helper()
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", requestURI, nil)
|
req, err := http.NewRequest("GET", requestURI, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Fatalf("unable to create request %s", err)
|
tc.t.Fatalf("unable to create request %s", err)
|
||||||
@@ -541,6 +555,8 @@ func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, e
|
|||||||
|
|
||||||
// AssertDeleteResponse request a URI and expect a statusCode and body text
|
// AssertDeleteResponse request a URI and expect a statusCode and body text
|
||||||
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
tc.t.Helper()
|
||||||
|
|
||||||
req, err := http.NewRequest("DELETE", requestURI, nil)
|
req, err := http.NewRequest("DELETE", requestURI, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Fatalf("unable to create request %s", err)
|
tc.t.Fatalf("unable to create request %s", err)
|
||||||
@@ -551,6 +567,8 @@ func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int
|
|||||||
|
|
||||||
// AssertPostResponseBody POST to a URI and assert the response code and body
|
// AssertPostResponseBody POST to a URI and assert the response code and body
|
||||||
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
tc.t.Helper()
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", requestURI, requestBody)
|
req, err := http.NewRequest("POST", requestURI, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("failed to create request %s", err)
|
tc.t.Errorf("failed to create request %s", err)
|
||||||
@@ -564,6 +582,8 @@ func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []str
|
|||||||
|
|
||||||
// AssertPutResponseBody PUT to a URI and assert the response code and body
|
// AssertPutResponseBody PUT to a URI and assert the response code and body
|
||||||
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
tc.t.Helper()
|
||||||
|
|
||||||
req, err := http.NewRequest("PUT", requestURI, requestBody)
|
req, err := http.NewRequest("PUT", requestURI, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("failed to create request %s", err)
|
tc.t.Errorf("failed to create request %s", err)
|
||||||
@@ -577,6 +597,8 @@ func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []stri
|
|||||||
|
|
||||||
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
|
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
|
||||||
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
tc.t.Helper()
|
||||||
|
|
||||||
req, err := http.NewRequest("PATCH", requestURI, requestBody)
|
req, err := http.NewRequest("PATCH", requestURI, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("failed to create request %s", err)
|
tc.t.Errorf("failed to create request %s", err)
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ func TestACMEServerAllowPolicy(t *testing.T) {
|
|||||||
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"not-matching.localhost"})
|
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"not-matching.localhost"})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("obtaining certificate for 'not-matching.localhost' domain")
|
t.Errorf("obtaining certificate for 'not-matching.localhost' domain")
|
||||||
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
|
} else if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
|
||||||
t.Logf("unexpected error: %v", err)
|
t.Logf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,7 +200,7 @@ func TestACMEServerDenyPolicy(t *testing.T) {
|
|||||||
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"})
|
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("obtaining certificate for 'deny.localhost' domain")
|
t.Errorf("obtaining certificate for 'deny.localhost' domain")
|
||||||
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
|
} else if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
|
||||||
t.Logf("unexpected error: %v", err)
|
t.Logf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
keepalive_interval 20s
|
keepalive_interval 20s
|
||||||
keepalive_idle 20s
|
keepalive_idle 20s
|
||||||
keepalive_count 10
|
keepalive_count 10
|
||||||
|
0rtt off
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +91,8 @@ foo.com {
|
|||||||
"h2",
|
"h2",
|
||||||
"h2c",
|
"h2c",
|
||||||
"h3"
|
"h3"
|
||||||
]
|
],
|
||||||
|
"allow_0rtt": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
renewal_window_ratio 0.1666
|
||||||
|
}
|
||||||
|
|
||||||
|
example.com {
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"renewal_window_ratio": 0.1666
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
renewal_window_ratio 0.1666
|
||||||
|
}
|
||||||
|
|
||||||
|
a.example.com {
|
||||||
|
tls {
|
||||||
|
renewal_window_ratio 0.25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.example.com {
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"a.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"b.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"a.example.com"
|
||||||
|
],
|
||||||
|
"renewal_window_ratio": 0.25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"renewal_window_ratio": 0.1666
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
dns mock foo
|
||||||
|
acme_dns mock bar
|
||||||
|
}
|
||||||
|
|
||||||
|
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": {
|
||||||
|
"argument": "bar",
|
||||||
|
"name": "mock"
|
||||||
|
},
|
||||||
|
"resolvers": [
|
||||||
|
"8.8.8.8",
|
||||||
|
"8.8.4.4"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"module": "acme"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"challenges": {
|
||||||
|
"dns": {
|
||||||
|
"provider": {
|
||||||
|
"argument": "bar",
|
||||||
|
"name": "mock"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"module": "acme"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"argument": "foo",
|
||||||
|
"name": "mock"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package integration
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand/v2"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -54,7 +54,7 @@ func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) {
|
|||||||
const uploadSize = (1024 * 1024) + 1 // 1 MB + 1 byte
|
const uploadSize = (1024 * 1024) + 1 // 1 MB + 1 byte
|
||||||
// 1 more than an MB
|
// 1 more than an MB
|
||||||
body := make([]byte, uploadSize)
|
body := make([]byte, uploadSize)
|
||||||
rand.New(rand.NewSource(0)).Read(body)
|
rand.NewChaCha8([32]byte{}).Read(body)
|
||||||
|
|
||||||
tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) {
|
tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func TestLeafCertLifetimeLessThanIntermediate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, "json", "certificate lifetime (168h0m0s) should be less than intermediate certificate lifetime (168h0m0s)")
|
`, "json", "should be less than intermediate certificate lifetime")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIntermediateLifetimeLessThanRoot(t *testing.T) {
|
func TestIntermediateLifetimeLessThanRoot(t *testing.T) {
|
||||||
@@ -103,5 +103,5 @@ func TestIntermediateLifetimeLessThanRoot(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, "json", "intermediate certificate lifetime must be less than root certificate lifetime (86400h0m0s)")
|
`, "json", "intermediate certificate lifetime must be less than actual root certificate lifetime")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddytest"
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
)
|
)
|
||||||
@@ -327,6 +326,41 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReverseProxyHealthCheck(t *testing.T) {
|
func TestReverseProxyHealthCheck(t *testing.T) {
|
||||||
|
// Start lightweight backend servers so they're ready before Caddy's
|
||||||
|
// active health checker runs; this avoids a startup race where the
|
||||||
|
// health checker probes backends that haven't yet begun accepting
|
||||||
|
// connections and marks them unhealthy.
|
||||||
|
//
|
||||||
|
// This mirrors how health checks are typically used in practice (to a separate
|
||||||
|
// backend service) and avoids probing the same Caddy instance while it's still
|
||||||
|
// provisioning and not ready to accept connections.
|
||||||
|
|
||||||
|
// backend server that responds to proxied requests
|
||||||
|
helloSrv := &http.Server{
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
_, _ = w.Write([]byte("Hello, World!"))
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
ln0, err := net.Listen("tcp", "127.0.0.1:2020")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to listen on 127.0.0.1:2020: %v", err)
|
||||||
|
}
|
||||||
|
go helloSrv.Serve(ln0)
|
||||||
|
t.Cleanup(func() { helloSrv.Close(); ln0.Close() })
|
||||||
|
|
||||||
|
// backend server that serves health checks
|
||||||
|
healthSrv := &http.Server{
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
_, _ = w.Write([]byte("ok"))
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
ln1, err := net.Listen("tcp", "127.0.0.1:2021")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to listen on 127.0.0.1:2021: %v", err)
|
||||||
|
}
|
||||||
|
go healthSrv.Serve(ln1)
|
||||||
|
t.Cleanup(func() { healthSrv.Close(); ln1.Close() })
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
tester := caddytest.NewTester(t)
|
||||||
tester.InitServer(`
|
tester.InitServer(`
|
||||||
{
|
{
|
||||||
@@ -336,12 +370,6 @@ func TestReverseProxyHealthCheck(t *testing.T) {
|
|||||||
https_port 9443
|
https_port 9443
|
||||||
grace_period 1ns
|
grace_period 1ns
|
||||||
}
|
}
|
||||||
http://localhost:2020 {
|
|
||||||
respond "Hello, World!"
|
|
||||||
}
|
|
||||||
http://localhost:2021 {
|
|
||||||
respond "ok"
|
|
||||||
}
|
|
||||||
http://localhost:9080 {
|
http://localhost:9080 {
|
||||||
reverse_proxy {
|
reverse_proxy {
|
||||||
to localhost:2020
|
to localhost:2020
|
||||||
@@ -355,8 +383,6 @@ func TestReverseProxyHealthCheck(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, "caddyfile")
|
`, "caddyfile")
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond) // TODO: for some reason this test seems particularly flaky, getting 503 when it should be 200, unless we wait
|
|
||||||
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
|
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
_ "time/tzdata"
|
||||||
|
|
||||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||||
|
|
||||||
// plug in Caddy modules here
|
// plug in Caddy modules here
|
||||||
|
|||||||
+58
-14
@@ -74,7 +74,7 @@ func cmdStart(fl Flags) (int, error) {
|
|||||||
// ensure it's the process we're expecting - we can be
|
// ensure it's the process we're expecting - we can be
|
||||||
// sure by giving it some random bytes and having it echo
|
// sure by giving it some random bytes and having it echo
|
||||||
// them back to us)
|
// them back to us)
|
||||||
cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
|
cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String()) //nolint:gosec // no command injection that I can determine...
|
||||||
// we should be able to run caddy in relative paths
|
// we should be able to run caddy in relative paths
|
||||||
if errors.Is(cmd.Err, exec.ErrDot) {
|
if errors.Is(cmd.Err, exec.ErrDot) {
|
||||||
cmd.Err = nil
|
cmd.Err = nil
|
||||||
@@ -411,11 +411,65 @@ func cmdBuildInfo(_ Flags) (int, error) {
|
|||||||
return caddy.ExitCodeSuccess, nil
|
return caddy.ExitCodeSuccess, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jsonModuleInfo holds metadata about a Caddy module for JSON output.
|
||||||
|
type jsonModuleInfo struct {
|
||||||
|
ModuleName string `json:"module_name"`
|
||||||
|
ModuleType string `json:"module_type"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
PackageURL string `json:"package_url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func cmdListModules(fl Flags) (int, error) {
|
func cmdListModules(fl Flags) (int, error) {
|
||||||
packages := fl.Bool("packages")
|
packages := fl.Bool("packages")
|
||||||
versions := fl.Bool("versions")
|
versions := fl.Bool("versions")
|
||||||
skipStandard := fl.Bool("skip-standard")
|
skipStandard := fl.Bool("skip-standard")
|
||||||
|
jsonOutput := fl.Bool("json")
|
||||||
|
|
||||||
|
// Organize modules by whether they come with the standard distribution
|
||||||
|
standard, nonstandard, unknown, err := getModules()
|
||||||
|
if err != nil {
|
||||||
|
// If module info can't be fetched, just print the IDs and exit
|
||||||
|
for _, m := range caddy.Modules() {
|
||||||
|
fmt.Println(m)
|
||||||
|
}
|
||||||
|
return caddy.ExitCodeSuccess, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic for JSON output
|
||||||
|
if jsonOutput {
|
||||||
|
output := []jsonModuleInfo{}
|
||||||
|
|
||||||
|
// addToOutput is a helper to convert internal module info to the JSON-serializable struct
|
||||||
|
addToOutput := func(list []moduleInfo, moduleType string) {
|
||||||
|
for _, mi := range list {
|
||||||
|
item := jsonModuleInfo{
|
||||||
|
ModuleName: mi.caddyModuleID,
|
||||||
|
ModuleType: moduleType, // Mapping the type here
|
||||||
|
}
|
||||||
|
if mi.goModule != nil {
|
||||||
|
item.Version = mi.goModule.Version
|
||||||
|
item.PackageURL = mi.goModule.Path
|
||||||
|
}
|
||||||
|
output = append(output, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass the respective type for each category
|
||||||
|
if !skipStandard {
|
||||||
|
addToOutput(standard, "standard")
|
||||||
|
}
|
||||||
|
addToOutput(nonstandard, "non-standard")
|
||||||
|
addToOutput(unknown, "unknown")
|
||||||
|
|
||||||
|
jsonBytes, err := json.MarshalIndent(output, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedQuit, err
|
||||||
|
}
|
||||||
|
fmt.Println(string(jsonBytes))
|
||||||
|
return caddy.ExitCodeSuccess, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic for Text output (Fallback)
|
||||||
printModuleInfo := func(mi moduleInfo) {
|
printModuleInfo := func(mi moduleInfo) {
|
||||||
fmt.Print(mi.caddyModuleID)
|
fmt.Print(mi.caddyModuleID)
|
||||||
if versions && mi.goModule != nil {
|
if versions && mi.goModule != nil {
|
||||||
@@ -433,16 +487,6 @@ func cmdListModules(fl Flags) (int, error) {
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
// organize modules by whether they come with the standard distribution
|
|
||||||
standard, nonstandard, unknown, err := getModules()
|
|
||||||
if err != nil {
|
|
||||||
// oh well, just print the module IDs and exit
|
|
||||||
for _, m := range caddy.Modules() {
|
|
||||||
fmt.Println(m)
|
|
||||||
}
|
|
||||||
return caddy.ExitCodeSuccess, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standard modules (always shipped with Caddy)
|
// Standard modules (always shipped with Caddy)
|
||||||
if !skipStandard {
|
if !skipStandard {
|
||||||
if len(standard) > 0 {
|
if len(standard) > 0 {
|
||||||
@@ -461,8 +505,8 @@ func cmdListModules(fl Flags) (int, error) {
|
|||||||
for _, mod := range nonstandard {
|
for _, mod := range nonstandard {
|
||||||
printModuleInfo(mod)
|
printModuleInfo(mod)
|
||||||
}
|
}
|
||||||
|
fmt.Printf("\n Non-standard modules: %d\n", len(nonstandard))
|
||||||
}
|
}
|
||||||
fmt.Printf("\n Non-standard modules: %d\n", len(nonstandard))
|
|
||||||
|
|
||||||
// Unknown modules (couldn't get Caddy module info)
|
// Unknown modules (couldn't get Caddy module info)
|
||||||
if len(unknown) > 0 {
|
if len(unknown) > 0 {
|
||||||
@@ -472,8 +516,8 @@ func cmdListModules(fl Flags) (int, error) {
|
|||||||
for _, mod := range unknown {
|
for _, mod := range unknown {
|
||||||
printModuleInfo(mod)
|
printModuleInfo(mod)
|
||||||
}
|
}
|
||||||
|
fmt.Printf("\n Unknown modules: %d\n", len(unknown))
|
||||||
}
|
}
|
||||||
fmt.Printf("\n Unknown modules: %d\n", len(unknown))
|
|
||||||
|
|
||||||
return caddy.ExitCodeSuccess, nil
|
return caddy.ExitCodeSuccess, nil
|
||||||
}
|
}
|
||||||
@@ -776,7 +820,7 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req) //nolint:gosec // the only SSRF here would be self-sabatoge I think
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("performing request: %v", err)
|
return nil, fmt.Errorf("performing request: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -229,12 +229,13 @@ documentation: https://go.dev/doc/modules/version-numbers
|
|||||||
|
|
||||||
RegisterCommand(Command{
|
RegisterCommand(Command{
|
||||||
Name: "list-modules",
|
Name: "list-modules",
|
||||||
Usage: "[--packages] [--versions] [--skip-standard]",
|
Usage: "[--packages] [--versions] [--skip-standard] [--json]",
|
||||||
Short: "Lists the installed Caddy modules",
|
Short: "Lists the installed Caddy modules",
|
||||||
CobraFunc: func(cmd *cobra.Command) {
|
CobraFunc: func(cmd *cobra.Command) {
|
||||||
cmd.Flags().BoolP("packages", "", false, "Print package paths")
|
cmd.Flags().BoolP("packages", "", false, "Print package paths")
|
||||||
cmd.Flags().BoolP("versions", "", false, "Print version information")
|
cmd.Flags().BoolP("versions", "", false, "Print version information")
|
||||||
cmd.Flags().BoolP("skip-standard", "s", false, "Skip printing standard modules")
|
cmd.Flags().BoolP("skip-standard", "s", false, "Skip printing standard modules")
|
||||||
|
cmd.Flags().BoolP("json", "", false, "Print modules in JSON format")
|
||||||
cmd.RunE = WrapCommandFuncForCobra(cmdListModules)
|
cmd.RunE = WrapCommandFuncForCobra(cmdListModules)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
+4
-1
@@ -231,7 +231,10 @@ 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)
|
if jsonErr, ok := err.(*json.SyntaxError); ok {
|
||||||
|
return nil, "", "", fmt.Errorf("config is not valid JSON: %w, at offset %d; did you mean to use a config adapter (the --adapter flag)?", err, jsonErr.Offset)
|
||||||
|
}
|
||||||
|
return nil, "", "", fmt.Errorf("config is not valid JSON: %w; did you mean to use a config adapter (the --adapter flag)?", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+42
-7
@@ -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,
|
||||||
|
|||||||
@@ -1,124 +1,125 @@
|
|||||||
module github.com/caddyserver/caddy/v2
|
module github.com/caddyserver/caddy/v2
|
||||||
|
|
||||||
go 1.25
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.5.0
|
github.com/BurntSushi/toml v1.6.0
|
||||||
github.com/DeRuina/timberjack v1.3.8
|
github.com/DeRuina/timberjack v1.3.9
|
||||||
github.com/KimMachineGun/automemlimit v0.7.4
|
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.23.1
|
||||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
|
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
|
||||||
github.com/caddyserver/certmagic v0.25.0
|
github.com/caddyserver/certmagic v0.25.2
|
||||||
github.com/caddyserver/zerossl v0.1.3
|
github.com/caddyserver/zerossl v0.1.5
|
||||||
github.com/cloudflare/circl v1.6.1
|
github.com/cloudflare/circl v1.6.3
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/go-chi/chi/v5 v5.2.3
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
github.com/google/cel-go v0.26.1
|
github.com/google/cel-go v0.27.0
|
||||||
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.4
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0
|
github.com/klauspost/cpuid/v2 v2.3.0
|
||||||
github.com/mholt/acmez/v3 v3.1.4
|
github.com/mholt/acmez/v3 v3.1.6
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/quic-go/quic-go v0.55.0
|
github.com/quic-go/quic-go v0.59.0
|
||||||
github.com/smallstep/certificates v0.28.4
|
github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c
|
||||||
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.2
|
||||||
github.com/spf13/pflag v1.0.10
|
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-20251216020129-aea342f6d747
|
||||||
github.com/yuin/goldmark v1.7.13
|
github.com/yuin/goldmark v1.7.16
|
||||||
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/exporters/autoexport v0.65.0
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
|
||||||
go.opentelemetry.io/contrib/propagators/autoprop v0.63.0
|
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0
|
||||||
go.opentelemetry.io/otel v1.38.0
|
go.opentelemetry.io/otel v1.40.0
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0
|
go.opentelemetry.io/otel/sdk v1.40.0
|
||||||
|
go.step.sm/crypto v0.76.2
|
||||||
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.1
|
||||||
go.uber.org/zap/exp v0.3.0
|
go.uber.org/zap/exp v0.3.0
|
||||||
golang.org/x/crypto v0.43.0
|
golang.org/x/crypto v0.48.0
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541
|
||||||
golang.org/x/net v0.46.0
|
golang.org/x/net v0.50.0
|
||||||
golang.org/x/sync v0.17.0
|
golang.org/x/sync v0.19.0
|
||||||
golang.org/x/term v0.36.0
|
golang.org/x/term v0.40.0
|
||||||
golang.org/x/time v0.14.0
|
golang.org/x/time v0.14.0
|
||||||
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.25.1 // indirect
|
||||||
cloud.google.com/go/auth v0.16.5 // indirect
|
cloud.google.com/go/auth v0.18.1 // 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.9.0 // indirect
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
dario.cat/mergo v1.0.1 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
filippo.io/bigmod v0.1.0 // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||||
github.com/ccoveille/go-safecast v1.6.1 // indirect
|
github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect
|
||||||
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.17.0 // 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.9.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.2 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.3 // 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.6 // indirect
|
github.com/google/go-tpm v0.9.8 // 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.11 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // 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
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
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/prometheus/otlptranslator v0.0.2 // indirect
|
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // 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.2 // 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.25.0 // indirect
|
||||||
github.com/smallstep/pkcs7 v0.2.1 // indirect
|
github.com/smallstep/pkcs7 v0.2.1 // indirect
|
||||||
github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 // indirect
|
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 // indirect
|
||||||
|
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||||
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.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 // indirect
|
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/aws v1.38.0 // indirect
|
go.opentelemetry.io/contrib/propagators/aws v1.40.0 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/b3 v1.38.0 // indirect
|
go.opentelemetry.io/contrib/propagators/b3 v1.40.0 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect
|
go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/ot v1.38.0 // indirect
|
go.opentelemetry.io/contrib/propagators/ot v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect
|
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/log v0.14.0 // indirect
|
go.opentelemetry.io/otel/log v0.16.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect
|
go.opentelemetry.io/otel/sdk/log v0.16.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/oauth2 v0.31.0 // indirect
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
google.golang.org/api v0.251.0 // indirect
|
golang.org/x/oauth2 v0.34.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
|
google.golang.org/api v0.265.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // 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
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.2.0 // indirect
|
||||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash v1.1.0 // indirect
|
github.com/cespare/xxhash v1.1.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0
|
github.com/cespare/xxhash/v2 v2.3.0
|
||||||
@@ -141,39 +142,37 @@ require (
|
|||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/libdns/libdns v1.1.1
|
github.com/libdns/libdns v1.1.1
|
||||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/miekg/dns v1.1.68 // indirect
|
github.com/miekg/dns v1.1.72 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/pires/go-proxyproto v0.8.1
|
github.com/pires/go-proxyproto v0.11.0
|
||||||
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.67.1 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.17.0 // indirect
|
github.com/prometheus/procfs v0.19.2 // 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.4 // indirect
|
||||||
github.com/slackhq/nebula v1.9.7 // indirect
|
github.com/slackhq/nebula v1.10.3 // 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/urfave/cli v1.22.17 // indirect
|
github.com/urfave/cli v1.22.17 // indirect
|
||||||
go.etcd.io/bbolt v1.3.10 // indirect
|
go.etcd.io/bbolt v1.3.10 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.38.0
|
go.opentelemetry.io/otel/trace v1.40.0
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||||
go.step.sm/crypto v0.72.0
|
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/mod v0.29.0 // indirect
|
golang.org/x/mod v0.33.0 // indirect
|
||||||
golang.org/x/sys v0.37.0
|
golang.org/x/sys v0.41.0
|
||||||
golang.org/x/text v0.30.0 // indirect
|
golang.org/x/text v0.34.0
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
google.golang.org/grpc v1.76.0 // indirect
|
google.golang.org/grpc v1.78.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
howett.net/plist v1.0.0 // indirect
|
howett.net/plist v1.0.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,104 +1,96 @@
|
|||||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
|
||||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
|
||||||
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
|
|
||||||
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
|
|
||||||
cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
|
|
||||||
cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
|
|
||||||
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.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
cloud.google.com/go/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.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||||
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
|
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||||
cloud.google.com/go/kms v1.23.1 h1:Mesyv84WoP3tPjUC0O5LRqPWICO0ufdpWf9jtBCEz64=
|
cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ=
|
||||||
cloud.google.com/go/kms v1.23.1/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g=
|
cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk=
|
||||||
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
|
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
|
||||||
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
|
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
||||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
||||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
||||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8=
|
||||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI=
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
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/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/DeRuina/timberjack v1.3.8 h1:lLxmRExvZygKSbb27Vp9hS0Tv8mL0WmFbwfRF29nY0Q=
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
github.com/DeRuina/timberjack v1.3.8/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
|
github.com/BurntSushi/toml v1.6.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.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||||
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
|
|
||||||
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
|
|
||||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||||
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||||
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
|
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||||
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.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||||
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.5.2/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.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||||
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.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I=
|
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.12 h1:pYM1Qgy0dKZLHX2cXslNacbcEFMkDMl+Bcj5ROuS6p8=
|
github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.12/go.mod h1:/MM0dyD7KSDPR+39p9ZNVKaHDLb9qnfDurvVS2KAhN8=
|
github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.16 h1:4JHirI4zp958zC026Sm+V4pSDwW4pwLefKrc0bF2lwI=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.16/go.mod h1:qQMtGx9OSw7ty1yLclzLxXCRbrkjWAM7JnObZjmCB7I=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 h1:Mv4Bc0mWmv6oDuSWTKnk+wgeqPL5DRFu5bQL9BGPQ8Y=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9/go.mod h1:IKlKfRppK2a1y0gy1yH6zD+yX5uplJ6UuPlgd48dJiQ=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 h1:se2vOWGD3dWQUtfn4wEjRQJb1HK1XsNIt825gskZ970=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9/go.mod h1:hijCGH2VfbZQxqCDN7bwz/4dzxV+hkyhjawAtdPWKZA=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 h1:6RBnKZLkJM4hQ+kN6E7yWFveOTg8NLPHAkqrs4ZPlTU=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9/go.mod h1:V9rQKRmK7AWuEsOMnHzKj8WyrIir1yUJbZxDuZLFvXI=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
|
||||||
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.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 h1:5r34CgVOD4WZudeEKZ9/iKpiT6cM1JyEROpXjOcdWv8=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9/go.mod h1:dB12CEbNWPbzO2uC6QSWHteqOg4JfBVJOojbAoAUb5I=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
|
||||||
github.com/aws/aws-sdk-go-v2/service/kms v1.45.6 h1:Br3kil4j7RPW+7LoLVkYt8SuhIWlg6ylmbmzXJ7PgXY=
|
github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE=
|
||||||
github.com/aws/aws-sdk-go-v2/service/kms v1.45.6/go.mod h1:FKXkHzw1fJZtg1P1qoAIiwen5thz/cDRTTDCIu8ljxc=
|
github.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.6 h1:A1oRkiSQOWstGh61y4Wc/yQ04sqrQZr1Si/oAXj20/s=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.6/go.mod h1:5PfYspyCU5Vw1wNPsxi15LZovOnULudOQuVxphSflQA=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 h1:5fm5RTONng73/QA73LhCNR7UT9RpFH3hR6HWL6bIgVY=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1/go.mod h1:xBEjWD13h+6nq+z4AkqSfSvqRKFgDIQeaMguAJndOWo=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 h1:p3jIvqYwUZgu/XYeI48bJxOhvm47hZb5HUQ0tn6Q9kA=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.6/go.mod h1:WtKK+ppze5yKPkZ0XwqIVWD4beCwv056ZbPQNoeHqM8=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
|
||||||
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
|
||||||
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||||
|
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||||
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/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc=
|
||||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg=
|
||||||
github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic=
|
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
|
||||||
github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
|
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||||
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
|
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
|
||||||
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=
|
||||||
github.com/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZkIff5Q=
|
|
||||||
github.com/ccoveille/go-safecast v1.6.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8=
|
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||||
@@ -114,15 +106,13 @@ 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.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
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=
|
||||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||||
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||||
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||||
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=
|
||||||
@@ -150,25 +140,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/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
|
||||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 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.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
|
||||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
|
||||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
|
||||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
github.com/go-jose/go-jose/v3 v3.0.4 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.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
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=
|
||||||
@@ -176,58 +160,39 @@ 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.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
|
||||||
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
|
||||||
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.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||||
github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
|
github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws=
|
||||||
github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ=
|
||||||
github.com/google/go-tpm-tools v0.4.6 h1:hwIwPG7w4z5eQEBq11gYw8YYr9xXLfBQ/0JsKyq5AJM=
|
|
||||||
github.com/google/go-tpm-tools v0.4.6/go.mod h1:MsVQbJnRhKDfWwf5zgr3cDGpj13P1uLAFF0wMEP/n5w=
|
|
||||||
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
|
github.com/google/go-tspi v0.3.0 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.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
||||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
|
||||||
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/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
|
||||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
@@ -244,14 +209,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.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
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=
|
||||||
@@ -259,32 +220,30 @@ 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=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU=
|
||||||
|
github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk=
|
||||||
|
github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U=
|
||||||
|
github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ=
|
||||||
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.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
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.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ=
|
github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=
|
||||||
github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||||
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=
|
||||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
@@ -293,20 +252,15 @@ 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=
|
||||||
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
|
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
|
||||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
@@ -314,26 +268,22 @@ 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.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
|
||||||
github.com/prometheus/client_model v0.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.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
|
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
|
||||||
github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ=
|
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
||||||
github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
|
||||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
@@ -341,59 +291,32 @@ 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/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/slackhq/nebula v1.10.3 h1:EstYj8ODEcv6T0R9X5BVq1zgWZnyU5gtPzk99QF1PMU=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/slackhq/nebula v1.10.3/go.mod h1:IL5TUQm4x9IFx2kCKPYm1gP47pwd5b8QGnnBH2RHnvs=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
|
||||||
github.com/slackhq/nebula v1.9.7 h1:v5u46efIyYHGdfjFnozQbRRhMdaB9Ma1SSTcUcE2lfE=
|
|
||||||
github.com/slackhq/nebula v1.9.7/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ=
|
|
||||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
|
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 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.30.0-rc2.0.20260211214201-20608299c29c h1:XQpX0IPYUAoJ661YlgfOJmY48ZOhIbglw4E2gw9mcyc=
|
||||||
github.com/smallstep/certificates v0.28.4/go.mod h1:LUqo+7mKZE7FZldlTb0zhU4A0bq4G4+akieFMcTaWvA=
|
github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c/go.mod h1:75NRLmYJq6ZcCb8ApJc+W1eL4oMYwjeufMJDHpv4rx4=
|
||||||
github.com/smallstep/cli-utils v0.12.1 h1:D9QvfbFqiKq3snGZ2xDcXEFrdFJ1mQfPHZMq/leerpE=
|
github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=
|
||||||
github.com/smallstep/cli-utils v0.12.1/go.mod h1:skV2Neg8qjiKPu2fphM89H9bIxNpKiiRTnX9Q6Lc+20=
|
github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y=
|
||||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
|
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
|
||||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
|
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
|
||||||
github.com/smallstep/linkedca v0.23.0 h1:5W/7EudlK1HcCIdZM68dJlZ7orqCCCyv6bm2l/0JmLU=
|
github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo=
|
||||||
github.com/smallstep/linkedca v0.23.0/go.mod h1:7cyRM9soAYySg9ag65QwytcgGOM+4gOlkJ/YA58A9E8=
|
github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc=
|
||||||
github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE=
|
github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE=
|
||||||
github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU=
|
github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU=
|
||||||
github.com/smallstep/pkcs7 v0.0.0-20240911091500-b1cae6277023/go.mod h1:CM5KrX7rxWgwDdMj9yef/pJB2OPgy/56z4IEx2UIbpc=
|
|
||||||
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
|
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
|
||||||
github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=
|
github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=
|
||||||
github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 h1:LyZqn24/ZiVg8v9Hq07K6mx6RqPtpDeK+De5vf4QEY4=
|
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA=
|
||||||
github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101/go.mod h1:EuKQjYGQwhUa1mgD21zxIgOgUYLsqikJmvxNscxpS/Y=
|
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492/go.mod h1:QQhwLqCS13nhv8L5ov7NgusowENUtXdEzdytjmJHdZQ=
|
||||||
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=
|
||||||
@@ -402,16 +325,14 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
|
|||||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
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/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 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
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/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
@@ -419,7 +340,6 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
|
|||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
@@ -427,21 +347,20 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
|||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
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/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
||||||
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
|
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
||||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 h1:RnBbFMmodYzhC6adOjTbtUQXyzV8dcvKYbolzs6Qch0=
|
||||||
|
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747/go.mod h1:ejPAJui3kVK4u5TgMtqtXlWf5HnKh9fLy5kvpaeuas0=
|
||||||
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=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||||
@@ -452,69 +371,68 @@ 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.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI=
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 h1:/Rij/t18Y7rUayNg7Id6rPrEnHgorxYabm2E6wUdPP4=
|
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac=
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0/go.mod h1:AdyDPn6pkbkt2w01n3BubRVk7xAsCRq1Yg1mpfyA/0E=
|
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g=
|
||||||
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 h1:NLnZybb9KkfMXPwZhd5diBYJoVxiO9Qa06dacEA7ySY=
|
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0=
|
||||||
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.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||||
go.opentelemetry.io/contrib/propagators/autoprop v0.63.0 h1:S3+4UwR3Y1tUKklruMwOacAFInNvtuOexz4ZTmJNAyw=
|
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 h1:kTaCycF9Xkm8VBBvH0rJ4wFeRjtIV55Erk3uuVsIs5s=
|
||||||
go.opentelemetry.io/contrib/propagators/autoprop v0.63.0/go.mod h1:qpIuOggbbw2T9nKRaO1je/oTRKd4zslAcJonN8LYbTg=
|
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0/go.mod h1:rooPzAbXfxMX9fsPJjmOBg2SN4RhFEV8D7cfGK+N3tE=
|
||||||
go.opentelemetry.io/contrib/propagators/aws v1.38.0 h1:eRZ7asSbLc5dH7+TBzL6hFKb1dabz0IV51uUUwYRZts=
|
go.opentelemetry.io/contrib/propagators/aws v1.40.0 h1:4VIrh75jW4RTimUNx1DSk+6H9/nDr1FvmKoOVDh3K04=
|
||||||
go.opentelemetry.io/contrib/propagators/aws v1.38.0/go.mod h1:wXqc9NTGcXapBExHBDVLEZlByu6quiQL8w7Tjgv8TCg=
|
go.opentelemetry.io/contrib/propagators/aws v1.40.0/go.mod h1:B0dCov9KNQGlut3T8wZZjDnLXEXdBroM7bFsHh/gRos=
|
||||||
go.opentelemetry.io/contrib/propagators/b3 v1.38.0 h1:uHsCCOSKl0kLrV2dLkFK+8Ywk9iKa/fptkytc6aFFEo=
|
go.opentelemetry.io/contrib/propagators/b3 v1.40.0 h1:xariChe8OOVF3rNlfzGFgQc61npQmXhzZj/i82mxMfg=
|
||||||
go.opentelemetry.io/contrib/propagators/b3 v1.38.0/go.mod h1:wMRSZJZcY8ya9mApLLhwIMjqmApy2o/Ml+62lhvxyHU=
|
go.opentelemetry.io/contrib/propagators/b3 v1.40.0/go.mod h1:72WvbdxbOfXaELEQfonFfOL6osvcVjI7uJEE8C2nkrs=
|
||||||
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 h1:nXGeLvT1QtCAhkASkP/ksjkTKZALIaQBIW+JSIw1KIc=
|
go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 h1:aXl9uobjJs5vquMLt9ZkI/3zIuz8XQ3TqOKSWx0/xdU=
|
||||||
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0/go.mod h1:oMvOXk78ZR3KEuPMBgp/ThAMDy9ku/eyUVztr+3G6Wo=
|
go.opentelemetry.io/contrib/propagators/jaeger v1.40.0/go.mod h1:ioMePqe6k6c/ovXSkmkMr1mbN5qRBGJxNTVop7/2XO0=
|
||||||
go.opentelemetry.io/contrib/propagators/ot v1.38.0 h1:k4gSyyohaDXI8F9BDXYC3uO2vr5sRNeQFMsN9Zn0EoI=
|
go.opentelemetry.io/contrib/propagators/ot v1.40.0 h1:Lon8J5SPmWaL1Ko2TIlCNHJ42/J1b5XbJlgJaE/9m7I=
|
||||||
go.opentelemetry.io/contrib/propagators/ot v1.38.0/go.mod h1:2hDsuiHRO39SRUMhYGqmj64z/IuMRoxE4bBSFR82Lo8=
|
go.opentelemetry.io/contrib/propagators/ot v1.40.0/go.mod h1:dKWtJTlp1Yj+8Cneye5idO46eRPIbi23qVuJYKjNnvY=
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
|
||||||
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.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
|
||||||
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.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo=
|
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk=
|
go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0/go.mod h1:mOJK8eMmgW6ocDJn6Bn11CcZ05gi3P8GylBXEkZtbgA=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8=
|
||||||
go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
|
go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4=
|
||||||
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno=
|
go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
|
go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
|
go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||||
go.step.sm/crypto v0.72.0 h1:cwkxbmnN8jj8YWmoXdoGhaac81d2SwXguwmHN9KJxHw=
|
go.step.sm/crypto v0.76.2 h1:JJ/yMcs/rmcCAwlo+afrHjq74XBFRTJw5B2y4Q4Z4c4=
|
||||||
go.step.sm/crypto v0.72.0/go.mod h1:EAy7MSOXxCvCaDAKJqz0bLdTSDdhpEM9xqye8XsfrM4=
|
go.step.sm/crypto v0.76.2/go.mod h1:m6KlB/HzIuGFep0UWI5e0SYi38UxpoKeCg6qUaHV6/Q=
|
||||||
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=
|
||||||
@@ -523,49 +441,34 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
|||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/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.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.1/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=
|
||||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
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.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99 h1:CH0o4/bZX6KIUCjjgjmtNtfM/kXSkTYlzTOB9vZF45g=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
|
||||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
|
||||||
golang.org/x/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.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
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=
|
||||||
@@ -574,55 +477,37 @@ 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.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.34.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.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
|
|
||||||
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
|
||||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
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=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.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.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.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.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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.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=
|
||||||
@@ -631,12 +516,10 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
|||||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
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.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
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=
|
||||||
@@ -644,76 +527,45 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.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.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
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.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU=
|
||||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY=
|
||||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||||
google.golang.org/api v0.251.0 h1:6lea5nHRT8RUmpy9kkC2PJYnhnDAB13LqrLSVQlMIE8=
|
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||||
google.golang.org/api v0.251.0/go.mod h1:Rwy0lPf/TD7+T2VhYcffCHhyyInyuxGjICxdfLqT7KI=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
||||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
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/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
|
|
||||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
|
||||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
|
||||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
|
||||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
|
||||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/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/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=
|
|
||||||
|
|||||||
+35
-91
@@ -31,17 +31,13 @@ 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().
|
||||||
@@ -309,64 +305,6 @@ 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
|
||||||
@@ -398,27 +336,9 @@ 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: fdAddr,
|
Host: host,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
var start, end uint64
|
var start, end uint64
|
||||||
@@ -511,7 +431,7 @@ func JoinNetworkAddress(network, host, port string) string {
|
|||||||
//
|
//
|
||||||
// NOTE: This API is EXPERIMENTAL and may be changed or removed.
|
// NOTE: This API is EXPERIMENTAL and may be changed or removed.
|
||||||
// NOTE: user should close the returned listener twice, once to stop accepting new connections, the second time to free up the packet conn.
|
// NOTE: user should close the returned listener twice, once to stop accepting new connections, the second time to free up the packet conn.
|
||||||
func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config) (http3.QUICListener, error) {
|
func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config, pcWrappers []PacketConnWrapper, allow0rttconf *bool) (http3.QUICListener, error) {
|
||||||
lnKey := listenerKey("quic"+na.Network, na.JoinHostPort(portOffset))
|
lnKey := listenerKey("quic"+na.Network, na.JoinHostPort(portOffset))
|
||||||
|
|
||||||
sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
||||||
@@ -523,12 +443,19 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config
|
|||||||
ln := lnAny.(net.PacketConn)
|
ln := lnAny.(net.PacketConn)
|
||||||
|
|
||||||
h3ln := ln
|
h3ln := ln
|
||||||
for {
|
if len(pcWrappers) == 0 {
|
||||||
// retrieve the underlying socket, so quic-go can optimize.
|
for {
|
||||||
if unwrapper, ok := h3ln.(interface{ Unwrap() net.PacketConn }); ok {
|
// retrieve the underlying socket, so quic-go can optimize.
|
||||||
h3ln = unwrapper.Unwrap()
|
if unwrapper, ok := h3ln.(interface{ Unwrap() net.PacketConn }); ok {
|
||||||
} else {
|
h3ln = unwrapper.Unwrap()
|
||||||
break
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// wrap packet conn before QUIC
|
||||||
|
for _, pcWrapper := range pcWrappers {
|
||||||
|
h3ln = pcWrapper.WrapPacketConn(h3ln)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,11 +470,15 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config
|
|||||||
Conn: h3ln,
|
Conn: h3ln,
|
||||||
VerifySourceAddress: func(addr net.Addr) bool { return !limiter.Allow() },
|
VerifySourceAddress: func(addr net.Addr) bool { return !limiter.Allow() },
|
||||||
}
|
}
|
||||||
|
allow0rtt := true
|
||||||
|
if allow0rttconf != nil {
|
||||||
|
allow0rtt = *allow0rttconf
|
||||||
|
}
|
||||||
earlyLn, err := tr.ListenEarly(
|
earlyLn, err := tr.ListenEarly(
|
||||||
http3.ConfigureTLSConfig(quicTlsConfig),
|
http3.ConfigureTLSConfig(quicTlsConfig),
|
||||||
&quic.Config{
|
&quic.Config{
|
||||||
Allow0RTT: true,
|
Allow0RTT: allow0rtt,
|
||||||
Tracer: qlog.DefaultConnectionTracer,
|
Tracer: h3qlog.DefaultConnectionTracer,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -775,6 +706,19 @@ type ListenerWrapper interface {
|
|||||||
WrapListener(net.Listener) net.Listener
|
WrapListener(net.Listener) net.Listener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PacketConnWrapper is a type that wraps a packet conn
|
||||||
|
// so it can modify the input packet conn methods.
|
||||||
|
// Modules that implement this interface are found
|
||||||
|
// in the caddy.packetconns namespace. Usually, to
|
||||||
|
// wrap a packet conn, you will define your own struct
|
||||||
|
// type that embeds the input packet conn, then
|
||||||
|
// implement your own methods that you want to wrap,
|
||||||
|
// calling the underlying packet conn methods where
|
||||||
|
// appropriate.
|
||||||
|
type PacketConnWrapper interface {
|
||||||
|
WrapPacketConn(net.PacketConn) net.PacketConn
|
||||||
|
}
|
||||||
|
|
||||||
// listenerPool stores and allows reuse of active listeners.
|
// listenerPool stores and allows reuse of active listeners.
|
||||||
var listenerPool = NewUsagePool()
|
var listenerPool = NewUsagePool()
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
package caddy
|
package caddy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -653,286 +652,3 @@ 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+5
-1
@@ -342,7 +342,11 @@ func ParseStructTag(tag string) (map[string]string, error) {
|
|||||||
func StrictUnmarshalJSON(data []byte, v any) error {
|
func StrictUnmarshalJSON(data []byte, v any) error {
|
||||||
dec := json.NewDecoder(bytes.NewReader(data))
|
dec := json.NewDecoder(bytes.NewReader(data))
|
||||||
dec.DisallowUnknownFields()
|
dec.DisallowUnknownFields()
|
||||||
return dec.Decode(v)
|
err := dec.Decode(v)
|
||||||
|
if jsonErr, ok := err.(*json.SyntaxError); ok {
|
||||||
|
return fmt.Errorf("%w, at offset %d", jsonErr, jsonErr.Offset)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var JSONRawMessageType = reflect.TypeFor[json.RawMessage]()
|
var JSONRawMessageType = reflect.TypeFor[json.RawMessage]()
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ func init() {
|
|||||||
// Placeholder | Description
|
// Placeholder | Description
|
||||||
// ------------|---------------
|
// ------------|---------------
|
||||||
// `{http.request.body}` | The request body (⚠️ inefficient; use only for debugging)
|
// `{http.request.body}` | The request body (⚠️ inefficient; use only for debugging)
|
||||||
|
// `{http.request.body_base64}` | The request body, base64-encoded (⚠️ for debugging)
|
||||||
// `{http.request.cookie.*}` | HTTP request cookie
|
// `{http.request.cookie.*}` | HTTP request cookie
|
||||||
// `{http.request.duration}` | Time up to now spent handling the request (after decoding headers from client)
|
// `{http.request.duration}` | Time up to now spent handling the request (after decoding headers from client)
|
||||||
// `{http.request.duration_ms}` | Same as 'duration', but in milliseconds.
|
// `{http.request.duration_ms}` | Same as 'duration', but in milliseconds.
|
||||||
@@ -82,6 +83,7 @@ func init() {
|
|||||||
// `{http.request.tls.proto}` | The negotiated next protocol
|
// `{http.request.tls.proto}` | The negotiated next protocol
|
||||||
// `{http.request.tls.proto_mutual}` | The negotiated next protocol was advertised by the server
|
// `{http.request.tls.proto_mutual}` | The negotiated next protocol was advertised by the server
|
||||||
// `{http.request.tls.server_name}` | The server name requested by the client, if any
|
// `{http.request.tls.server_name}` | The server name requested by the client, if any
|
||||||
|
// `{http.request.tls.ech}` | Whether ECH was offered by the client and accepted by the server
|
||||||
// `{http.request.tls.client.fingerprint}` | The SHA256 checksum of the client certificate
|
// `{http.request.tls.client.fingerprint}` | The SHA256 checksum of the client certificate
|
||||||
// `{http.request.tls.client.public_key}` | The public key of the client certificate.
|
// `{http.request.tls.client.public_key}` | The public key of the client certificate.
|
||||||
// `{http.request.tls.client.public_key_sha256}` | The SHA256 checksum of the client's public key.
|
// `{http.request.tls.client.public_key_sha256}` | The SHA256 checksum of the client's public key.
|
||||||
@@ -346,6 +348,20 @@ func (app *App) Provision(ctx caddy.Context) error {
|
|||||||
srv.listenerWrappers = append([]caddy.ListenerWrapper{new(tlsPlaceholderWrapper)}, srv.listenerWrappers...)
|
srv.listenerWrappers = append([]caddy.ListenerWrapper{new(tlsPlaceholderWrapper)}, srv.listenerWrappers...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set up each packet conn modifier
|
||||||
|
if srv.PacketConnWrappersRaw != nil {
|
||||||
|
vals, err := ctx.LoadModule(srv, "PacketConnWrappersRaw")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading packet conn wrapper modules: %v", err)
|
||||||
|
}
|
||||||
|
// if any wrappers were configured, they come before the QUIC handshake;
|
||||||
|
// unlike TLS above, there is no QUIC placeholder
|
||||||
|
for _, val := range vals.([]any) {
|
||||||
|
srv.packetConnWrappers = append(srv.packetConnWrappers, val.(caddy.PacketConnWrapper))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// pre-compile the primary handler chain, and be sure to wrap it in our
|
// pre-compile the primary handler chain, and be sure to wrap it in our
|
||||||
// route handler so that important security checks are done, etc.
|
// route handler so that important security checks are done, etc.
|
||||||
primaryRoute := emptyHandler
|
primaryRoute := emptyHandler
|
||||||
|
|||||||
@@ -90,7 +90,16 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
|||||||
// the log configuration for an HTTPS enabled server
|
// the log configuration for an HTTPS enabled server
|
||||||
var logCfg *ServerLogConfig
|
var logCfg *ServerLogConfig
|
||||||
|
|
||||||
for srvName, srv := range app.Servers {
|
// Sort server names to ensure deterministic iteration.
|
||||||
|
// This prevents race conditions where the order of server processing
|
||||||
|
// could affect which server gets assigned the HTTP->HTTPS redirect listener.
|
||||||
|
srvNames := make([]string, 0, len(app.Servers))
|
||||||
|
for name := range app.Servers {
|
||||||
|
srvNames = append(srvNames, name)
|
||||||
|
}
|
||||||
|
slices.Sort(srvNames)
|
||||||
|
for _, srvName := range srvNames {
|
||||||
|
srv := app.Servers[srvName]
|
||||||
// as a prerequisite, provision route matchers; this is
|
// as a prerequisite, provision route matchers; this is
|
||||||
// required for all routes on all servers, and must be
|
// required for all routes on all servers, and must be
|
||||||
// done before we attempt to do phase 1 of auto HTTPS,
|
// done before we attempt to do phase 1 of auto HTTPS,
|
||||||
@@ -398,15 +407,26 @@ uniqueDomainsLoop:
|
|||||||
return append(routes, app.makeRedirRoute(uint(app.httpsPort()), MatcherSet{MatchProtocol("http")}))
|
return append(routes, app.makeRedirRoute(uint(app.httpsPort()), MatcherSet{MatchProtocol("http")}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort redirect addresses to ensure deterministic process
|
||||||
|
redirServerAddrsSorted := make([]string, 0, len(redirServers))
|
||||||
|
for addr := range redirServers {
|
||||||
|
redirServerAddrsSorted = append(redirServerAddrsSorted, addr)
|
||||||
|
}
|
||||||
|
slices.Sort(redirServerAddrsSorted)
|
||||||
|
|
||||||
redirServersLoop:
|
redirServersLoop:
|
||||||
for redirServerAddr, routes := range redirServers {
|
for _, redirServerAddr := range redirServerAddrsSorted {
|
||||||
|
routes := redirServers[redirServerAddr]
|
||||||
// for each redirect listener, see if there's already a
|
// for each redirect listener, see if there's already a
|
||||||
// server configured to listen on that exact address; if so,
|
// server configured to listen on that exact address; if so,
|
||||||
// insert the redirect route to the end of its route list
|
// insert the redirect route to the end of its route list
|
||||||
// after any other routes with host matchers; otherwise,
|
// after any other routes with host matchers; otherwise,
|
||||||
// we'll create a new server for all the listener addresses
|
// we'll create a new server for all the listener addresses
|
||||||
// that are unused and serve the remaining redirects from it
|
// that are unused and serve the remaining redirects from it
|
||||||
for _, srv := range app.Servers {
|
|
||||||
|
// Use the sorted srvNames to consistently find the target server
|
||||||
|
for _, srvName := range srvNames {
|
||||||
|
srv := app.Servers[srvName]
|
||||||
// only look at servers which listen on an address which
|
// only look at servers which listen on an address which
|
||||||
// we want to add redirects to
|
// we want to add redirects to
|
||||||
if !srv.hasListenerAddress(redirServerAddr) {
|
if !srv.hasListenerAddress(redirServerAddr) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
weakrand "math/rand"
|
weakrand "math/rand/v2"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -244,7 +244,7 @@ func (c *Cache) makeRoom() {
|
|||||||
// strategy; generating random numbers is cheap and
|
// strategy; generating random numbers is cheap and
|
||||||
// ensures a much better distribution.
|
// ensures a much better distribution.
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
rnd := weakrand.Intn(len(c.cache))
|
rnd := weakrand.IntN(len(c.cache))
|
||||||
i := 0
|
i := 0
|
||||||
for key := range c.cache {
|
for key := range c.cache {
|
||||||
if i == rnd {
|
if i == rnd {
|
||||||
@@ -287,7 +287,7 @@ type Account struct {
|
|||||||
|
|
||||||
// The user's hashed password, in Modular Crypt Format (with `$` prefix)
|
// The user's hashed password, in Modular Crypt Format (with `$` prefix)
|
||||||
// or base64-encoded.
|
// or base64-encoded.
|
||||||
Password string `json:"password"`
|
Password string `json:"password"` //nolint:gosec // false positive, this is a hashed password
|
||||||
|
|
||||||
password []byte
|
password []byte
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -412,10 +412,12 @@ func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fa
|
|||||||
return nil, fmt.Errorf("unsupported matcher data type: %s, %s", matcherDataTypes[0], matcherDataTypes[1])
|
return nil, fmt.Errorf("unsupported matcher data type: %s, %s", matcherDataTypes[0], matcherDataTypes[1])
|
||||||
}
|
}
|
||||||
case 3:
|
case 3:
|
||||||
|
// nolint:gosec // false positive, impossible to be out of bounds; see: https://github.com/securego/gosec/issues/1525
|
||||||
if matcherDataTypes[0] == cel.StringType && matcherDataTypes[1] == cel.StringType && matcherDataTypes[2] == cel.StringType {
|
if matcherDataTypes[0] == cel.StringType && matcherDataTypes[1] == cel.StringType && matcherDataTypes[2] == cel.StringType {
|
||||||
macro = parser.NewGlobalMacro(macroName, 3, celMatcherStringListMacroExpander(funcName))
|
macro = parser.NewGlobalMacro(macroName, 3, celMatcherStringListMacroExpander(funcName))
|
||||||
matcherDataTypes = []*cel.Type{cel.ListType(cel.StringType)}
|
matcherDataTypes = []*cel.Type{cel.ListType(cel.StringType)}
|
||||||
} else {
|
} else {
|
||||||
|
// nolint:gosec // false positive, impossible to be out of bounds; see: https://github.com/securego/gosec/issues/1525
|
||||||
return nil, fmt.Errorf("unsupported matcher data type: %s, %s, %s", matcherDataTypes[0], matcherDataTypes[1], matcherDataTypes[2])
|
return nil, fmt.Errorf("unsupported matcher data type: %s, %s, %s", matcherDataTypes[0], matcherDataTypes[1], matcherDataTypes[2])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -665,12 +667,29 @@ 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) {
|
||||||
|
// Prefer map[string]any, but newer cel-go versions may return map[any]any
|
||||||
mapStrType := reflect.TypeFor[map[string]any]()
|
mapStrType := reflect.TypeFor[map[string]any]()
|
||||||
mapStrRaw, err := data.ConvertToNative(mapStrType)
|
mapStrRaw, err := data.ConvertToNative(mapStrType)
|
||||||
|
var mapStrIface map[string]any
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
// Try map[any]any and convert keys to strings
|
||||||
|
mapAnyType := reflect.TypeFor[map[any]any]()
|
||||||
|
mapAnyRaw, err2 := data.ConvertToNative(mapAnyType)
|
||||||
|
if err2 != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mapAnyIface := mapAnyRaw.(map[any]any)
|
||||||
|
mapStrIface = make(map[string]any, len(mapAnyIface))
|
||||||
|
for k, v := range mapAnyIface {
|
||||||
|
ks, ok := k.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unsupported map key type in header match: %T", k)
|
||||||
|
}
|
||||||
|
mapStrIface[ks] = v
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mapStrIface = mapStrRaw.(map[string]any)
|
||||||
}
|
}
|
||||||
mapStrIface := mapStrRaw.(map[string]any)
|
|
||||||
mapStrListStr := make(map[string][]string, len(mapStrIface))
|
mapStrListStr := make(map[string][]string, len(mapStrIface))
|
||||||
for k, v := range mapStrIface {
|
for k, v := range mapStrIface {
|
||||||
switch val := v.(type) {
|
switch val := v.(type) {
|
||||||
@@ -685,13 +704,26 @@ func CELValueToMapStrList(data ref.Val) (map[string][]string, error) {
|
|||||||
for i, elem := range val {
|
for i, elem := range val {
|
||||||
strVal, ok := elem.(types.String)
|
strVal, ok := elem.(types.String)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("unsupported value type in header match: %T", val)
|
return nil, fmt.Errorf("unsupported value type in matcher input: %T", val)
|
||||||
}
|
}
|
||||||
convVals[i] = string(strVal)
|
convVals[i] = string(strVal)
|
||||||
}
|
}
|
||||||
mapStrListStr[k] = convVals
|
mapStrListStr[k] = convVals
|
||||||
|
case []any:
|
||||||
|
convVals := make([]string, len(val))
|
||||||
|
for i, elem := range val {
|
||||||
|
switch e := elem.(type) {
|
||||||
|
case string:
|
||||||
|
convVals[i] = e
|
||||||
|
case types.String:
|
||||||
|
convVals[i] = string(e)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported element type in matcher input list: %T", elem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mapStrListStr[k] = convVals
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported value type in header match: %T", val)
|
return nil, fmt.Errorf("unsupported value type in matcher input: %T", val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return mapStrListStr, nil
|
return mapStrListStr, nil
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,6 +307,14 @@ func (rw *responseWriter) FlushError() error {
|
|||||||
return http.NewResponseController(rw.ResponseWriter).Flush()
|
return http.NewResponseController(rw.ResponseWriter).Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flush calls FlushError() and simply discards any error. It is only implemented for backwards
|
||||||
|
// compatibility with legacy code that does not use FlushError; we know at least one sponsor
|
||||||
|
// needs this. It should not be relied upon as a stable part of the exported API, as it may be
|
||||||
|
// removed in the future.
|
||||||
|
func (rw *responseWriter) Flush() {
|
||||||
|
_ = rw.FlushError()
|
||||||
|
}
|
||||||
|
|
||||||
// Write writes to the response. If the response qualifies,
|
// Write writes to the response. If the response qualifies,
|
||||||
// it is encoded using the encoder, which is initialized
|
// it is encoded using the encoder, which is initialized
|
||||||
// if not done so already.
|
// if not done so already.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ package caddyhttp
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
weakrand "math/rand"
|
weakrand "math/rand/v2"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -98,7 +98,7 @@ func randString(n int, sameCase bool) string {
|
|||||||
b := make([]byte, n)
|
b := make([]byte, n)
|
||||||
for i := range b {
|
for i := range b {
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
b[i] = dict[weakrand.Int63()%int64(len(dict))]
|
b[i] = dict[weakrand.IntN(len(dict))]
|
||||||
}
|
}
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w ht
|
|||||||
|
|
||||||
// Actual files
|
// Actual files
|
||||||
for _, item := range listing.Items {
|
for _, item := range listing.Items {
|
||||||
|
//nolint:gosec // not sure how this could be XSS unless you lose control of the file system (like aren't sanitizing) and client ignores Content-Type of text/plain
|
||||||
if _, err := fmt.Fprintf(writer, "%s\t%s\t%s\n",
|
if _, err := fmt.Fprintf(writer, "%s\t%s\t%s\n",
|
||||||
item.Name, item.HumanSize(), item.HumanModTime("January 2, 2006 at 15:04:05"),
|
item.Name, item.HumanSize(), item.HumanModTime("January 2, 2006 at 15:04:05"),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// for each glob result, combine all the forms of the path
|
// for each glob result, combine all the forms of the path
|
||||||
var candidates []matchCandidate
|
candidates := make([]matchCandidate, 0, len(globResults))
|
||||||
for _, result := range globResults {
|
for _, result := range globResults {
|
||||||
candidates = append(candidates, matchCandidate{
|
candidates = append(candidates, matchCandidate{
|
||||||
fullpath: result,
|
fullpath: result,
|
||||||
@@ -720,6 +720,7 @@ var globSafeRepl = strings.NewReplacer(
|
|||||||
"*", "\\*",
|
"*", "\\*",
|
||||||
"[", "\\[",
|
"[", "\\[",
|
||||||
"?", "\\?",
|
"?", "\\?",
|
||||||
|
"\\", "\\\\",
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
@@ -28,6 +30,13 @@ import (
|
|||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
path string
|
||||||
|
expectedPath string
|
||||||
|
expectedType string
|
||||||
|
matched bool
|
||||||
|
}
|
||||||
|
|
||||||
func TestFileMatcher(t *testing.T) {
|
func TestFileMatcher(t *testing.T) {
|
||||||
// Windows doesn't like colons in files names
|
// Windows doesn't like colons in files names
|
||||||
isWindows := runtime.GOOS == "windows"
|
isWindows := runtime.GOOS == "windows"
|
||||||
@@ -45,12 +54,7 @@ func TestFileMatcher(t *testing.T) {
|
|||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, tc := range []struct {
|
for i, tc := range []testCase{
|
||||||
path string
|
|
||||||
expectedPath string
|
|
||||||
expectedType string
|
|
||||||
matched bool
|
|
||||||
}{
|
|
||||||
{
|
{
|
||||||
path: "/foo.txt",
|
path: "/foo.txt",
|
||||||
expectedPath: "/foo.txt",
|
expectedPath: "/foo.txt",
|
||||||
@@ -116,44 +120,71 @@ func TestFileMatcher(t *testing.T) {
|
|||||||
matched: !isWindows,
|
matched: !isWindows,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
m := &MatchFile{
|
fileMatcherTest(t, i, tc)
|
||||||
fsmap: &filesystems.FileSystemMap{},
|
}
|
||||||
Root: "./testdata",
|
}
|
||||||
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"},
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(tc.path)
|
func TestFileMatcherNonWindows(t *testing.T) {
|
||||||
if err != nil {
|
if runtime.GOOS == "windows" {
|
||||||
t.Errorf("Test %d: parsing path: %v", i, err)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &http.Request{URL: u}
|
// this is impossible to test on Windows, but tests a security patch for other platforms
|
||||||
repl := caddyhttp.NewTestReplacer(req)
|
tc := testCase{
|
||||||
|
path: "/foodir/secr%5Cet.txt",
|
||||||
|
expectedPath: "/foodir/secr\\et.txt",
|
||||||
|
expectedType: "file",
|
||||||
|
matched: true,
|
||||||
|
}
|
||||||
|
|
||||||
result, err := m.MatchWithError(req)
|
f, err := os.Create(filepath.Join("testdata", strings.TrimPrefix(tc.expectedPath, "/")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Test %d: unexpected error: %v", i, err)
|
t.Fatalf("could not create test file: %v", err)
|
||||||
}
|
}
|
||||||
if result != tc.matched {
|
defer f.Close()
|
||||||
t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
|
defer os.Remove(f.Name())
|
||||||
}
|
|
||||||
|
|
||||||
rel, ok := repl.Get("http.matchers.file.relative")
|
fileMatcherTest(t, 0, tc)
|
||||||
if !ok && result {
|
}
|
||||||
t.Errorf("Test %d: expected replacer value", i)
|
|
||||||
}
|
|
||||||
if !result {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if rel != tc.expectedPath {
|
func fileMatcherTest(t *testing.T, i int, tc testCase) {
|
||||||
t.Errorf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath)
|
m := &MatchFile{
|
||||||
}
|
fsmap: &filesystems.FileSystemMap{},
|
||||||
|
Root: "./testdata",
|
||||||
|
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"},
|
||||||
|
}
|
||||||
|
|
||||||
fileType, _ := repl.Get("http.matchers.file.type")
|
u, err := url.Parse(tc.path)
|
||||||
if fileType != tc.expectedType {
|
if err != nil {
|
||||||
t.Errorf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType)
|
t.Errorf("Test %d: parsing path: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req := &http.Request{URL: u}
|
||||||
|
repl := caddyhttp.NewTestReplacer(req)
|
||||||
|
|
||||||
|
result, err := m.MatchWithError(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test %d: unexpected error: %v", i, err)
|
||||||
|
}
|
||||||
|
if result != tc.matched {
|
||||||
|
t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
rel, ok := repl.Get("http.matchers.file.relative")
|
||||||
|
if !ok && result {
|
||||||
|
t.Errorf("Test %d: expected replacer value", i)
|
||||||
|
}
|
||||||
|
if !result {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rel != tc.expectedPath {
|
||||||
|
t.Errorf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileType, _ := repl.Get("http.matchers.file.type")
|
||||||
|
if fileType != tc.expectedType {
|
||||||
|
t.Errorf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
weakrand "math/rand"
|
weakrand "math/rand/v2"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -601,7 +601,7 @@ func (fsrv *FileServer) openFile(fileSystem fs.FS, filename string, w http.Respo
|
|||||||
// maybe the server is under load and ran out of file descriptors?
|
// maybe the server is under load and ran out of file descriptors?
|
||||||
// have client wait arbitrary seconds to help prevent a stampede
|
// have client wait arbitrary seconds to help prevent a stampede
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
backoff := weakrand.Intn(maxBackoff-minBackoff) + minBackoff
|
backoff := weakrand.IntN(maxBackoff-minBackoff) + minBackoff
|
||||||
w.Header().Set("Retry-After", strconv.Itoa(backoff))
|
w.Header().Set("Retry-After", strconv.Itoa(backoff))
|
||||||
if c := fsrv.logger.Check(zapcore.DebugLevel, "retry after backoff"); c != nil {
|
if c := fsrv.logger.Check(zapcore.DebugLevel, "retry after backoff"); c != nil {
|
||||||
c.Write(zap.String("filename", filename), zap.Int("backoff", backoff), zap.Error(err))
|
c.Write(zap.String("filename", filename), zap.Int("backoff", backoff), zap.Error(err))
|
||||||
|
|||||||
@@ -168,8 +168,6 @@ func parseReqHdrCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue,
|
|||||||
}
|
}
|
||||||
h.Next() // consume the directive name again (matcher parsing resets)
|
h.Next() // consume the directive name again (matcher parsing resets)
|
||||||
|
|
||||||
configValues := []httpcaddyfile.ConfigValue{}
|
|
||||||
|
|
||||||
if !h.NextArg() {
|
if !h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
}
|
}
|
||||||
@@ -204,7 +202,7 @@ func parseReqHdrCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue,
|
|||||||
return nil, h.Err(err.Error())
|
return nil, h.Err(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
configValues = append(configValues, h.NewRoute(matcherSet, hdr)...)
|
configValues := h.NewRoute(matcherSet, hdr)
|
||||||
|
|
||||||
if h.NextArg() {
|
if h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
|
|||||||
@@ -217,7 +217,10 @@ type RespHeaderOps struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ApplyTo applies ops to hdr using repl.
|
// ApplyTo applies ops to hdr using repl.
|
||||||
func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
|
func (ops *HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
|
||||||
|
if ops == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
// before manipulating headers in other ways, check if there
|
// before manipulating headers in other ways, check if there
|
||||||
// is configuration to delete all headers, and do that first
|
// is configuration to delete all headers, and do that first
|
||||||
// because if a header is to be added, we don't want to delete
|
// because if a header is to be added, we don't want to delete
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -223,17 +233,21 @@ func errLogValues(err error) (status int, msg string, fields func() []zapcore.Fi
|
|||||||
|
|
||||||
// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
@@ -26,7 +28,7 @@ func init() {
|
|||||||
|
|
||||||
// parseCaddyfile sets up the log_append handler from Caddyfile tokens. Syntax:
|
// parseCaddyfile sets up the log_append handler from Caddyfile tokens. Syntax:
|
||||||
//
|
//
|
||||||
// log_append [<matcher>] <key> <value>
|
// log_append [<matcher>] [<]<key> <value>
|
||||||
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||||
handler := new(LogAppend)
|
handler := new(LogAppend)
|
||||||
err := handler.UnmarshalCaddyfile(h.Dispenser)
|
err := handler.UnmarshalCaddyfile(h.Dispenser)
|
||||||
@@ -43,6 +45,10 @@ func (h *LogAppend) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return d.ArgErr()
|
return d.ArgErr()
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(h.Key, "<") && len(h.Key) > 1 {
|
||||||
|
h.Early = true
|
||||||
|
h.Key = h.Key[1:]
|
||||||
|
}
|
||||||
h.Value = d.Val()
|
h.Value = d.Val()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -42,6 +44,12 @@ type LogAppend struct {
|
|||||||
// map, the value of that key will be used. Otherwise
|
// map, the value of that key will be used. Otherwise
|
||||||
// the value will be used as-is as a constant string.
|
// the value will be used as-is as a constant string.
|
||||||
Value string `json:"value,omitempty"`
|
Value string `json:"value,omitempty"`
|
||||||
|
|
||||||
|
// Early, if true, adds the log field before calling
|
||||||
|
// the next handler in the chain. By default, the log
|
||||||
|
// field is added on the way back up the middleware chain,
|
||||||
|
// after all subsequent handlers have completed.
|
||||||
|
Early bool `json:"early,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CaddyModule returns the Caddy module information.
|
// CaddyModule returns the Caddy module information.
|
||||||
@@ -53,13 +61,63 @@ func (LogAppend) CaddyModule() caddy.ModuleInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h LogAppend) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
func (h LogAppend) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||||
// Run the next handler in the chain first.
|
// Determine if we need to add the log field early.
|
||||||
|
// We do if the Early flag is set, or for convenience,
|
||||||
|
// if the value is a special placeholder for the request body.
|
||||||
|
needsEarly := h.Early || h.Value == placeholderRequestBody || h.Value == placeholderRequestBodyBase64
|
||||||
|
|
||||||
|
// Check if we need to buffer the response for special placeholders
|
||||||
|
needsResponseBody := h.Value == placeholderResponseBody || h.Value == placeholderResponseBodyBase64
|
||||||
|
|
||||||
|
if needsEarly && !needsResponseBody {
|
||||||
|
// Add the log field before calling the next handler
|
||||||
|
// (but not if we need the response body, which isn't available yet)
|
||||||
|
h.addLogField(r, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rec caddyhttp.ResponseRecorder
|
||||||
|
var buf *bytes.Buffer
|
||||||
|
|
||||||
|
if needsResponseBody {
|
||||||
|
// Wrap the response writer with a recorder to capture the response body
|
||||||
|
buf = new(bytes.Buffer)
|
||||||
|
rec = caddyhttp.NewResponseRecorder(w, buf, func(status int, header http.Header) bool {
|
||||||
|
// Always buffer the response when we need to log the body
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
w = rec
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the next handler in the chain.
|
||||||
// If an error occurs, we still want to add
|
// If an error occurs, we still want to add
|
||||||
// any extra log fields that we can, so we
|
// any extra log fields that we can, so we
|
||||||
// hold onto the error and return it later.
|
// hold onto the error and return it later.
|
||||||
handlerErr := next.ServeHTTP(w, r)
|
handlerErr := next.ServeHTTP(w, r)
|
||||||
|
|
||||||
// On the way back up the chain, add the extra log field
|
if needsResponseBody {
|
||||||
|
// Write the buffered response to the client
|
||||||
|
if rec.Buffered() {
|
||||||
|
h.addLogField(r, buf)
|
||||||
|
err := rec.WriteResponse()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handlerErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if !h.Early {
|
||||||
|
// Add the log field after the handler completes
|
||||||
|
h.addLogField(r, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
return handlerErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// addLogField adds the log field to the request's extra log fields.
|
||||||
|
// If buf is not nil, it contains the buffered response body for special
|
||||||
|
// response body placeholders.
|
||||||
|
func (h LogAppend) addLogField(r *http.Request, buf *bytes.Buffer) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
vars := ctx.Value(caddyhttp.VarsCtxKey).(map[string]any)
|
vars := ctx.Value(caddyhttp.VarsCtxKey).(map[string]any)
|
||||||
@@ -67,7 +125,21 @@ func (h LogAppend) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
|
|||||||
extra := ctx.Value(caddyhttp.ExtraLogFieldsCtxKey).(*caddyhttp.ExtraLogFields)
|
extra := ctx.Value(caddyhttp.ExtraLogFieldsCtxKey).(*caddyhttp.ExtraLogFields)
|
||||||
|
|
||||||
var varValue any
|
var varValue any
|
||||||
if strings.HasPrefix(h.Value, "{") &&
|
|
||||||
|
// Handle special case placeholders for response body
|
||||||
|
if h.Value == placeholderResponseBody {
|
||||||
|
if buf != nil {
|
||||||
|
varValue = buf.String()
|
||||||
|
} else {
|
||||||
|
varValue = ""
|
||||||
|
}
|
||||||
|
} else if h.Value == placeholderResponseBodyBase64 {
|
||||||
|
if buf != nil {
|
||||||
|
varValue = base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||||
|
} else {
|
||||||
|
varValue = ""
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(h.Value, "{") &&
|
||||||
strings.HasSuffix(h.Value, "}") &&
|
strings.HasSuffix(h.Value, "}") &&
|
||||||
strings.Count(h.Value, "{") == 1 {
|
strings.Count(h.Value, "{") == 1 {
|
||||||
// the value looks like a placeholder, so get its value
|
// the value looks like a placeholder, so get its value
|
||||||
@@ -84,10 +156,17 @@ func (h LogAppend) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
|
|||||||
// We use zap.Any because it will reflect
|
// We use zap.Any because it will reflect
|
||||||
// to the correct type for us.
|
// to the correct type for us.
|
||||||
extra.Add(zap.Any(h.Key, varValue))
|
extra.Add(zap.Any(h.Key, varValue))
|
||||||
|
|
||||||
return handlerErr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Special placeholder values that are handled by log_append
|
||||||
|
// rather than by the replacer.
|
||||||
|
placeholderRequestBody = "{http.request.body}"
|
||||||
|
placeholderRequestBodyBase64 = "{http.request.body_base64}"
|
||||||
|
placeholderResponseBody = "{http.response.body}"
|
||||||
|
placeholderResponseBodyBase64 = "{http.response.body_base64}"
|
||||||
|
)
|
||||||
|
|
||||||
// Interface guards
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ caddyhttp.MiddlewareHandler = (*LogAppend)(nil)
|
_ caddyhttp.MiddlewareHandler = (*LogAppend)(nil)
|
||||||
@@ -110,6 +110,7 @@ func (t LoggableTLSConnState) MarshalLogObject(enc zapcore.ObjectEncoder) error
|
|||||||
enc.AddUint16("cipher_suite", t.CipherSuite)
|
enc.AddUint16("cipher_suite", t.CipherSuite)
|
||||||
enc.AddString("proto", t.NegotiatedProtocol)
|
enc.AddString("proto", t.NegotiatedProtocol)
|
||||||
enc.AddString("server_name", t.ServerName)
|
enc.AddString("server_name", t.ServerName)
|
||||||
|
enc.AddBool("ech", t.ECHAccepted)
|
||||||
if len(t.PeerCertificates) > 0 {
|
if len(t.PeerCertificates) > 0 {
|
||||||
enc.AddString("client_common_name", t.PeerCertificates[0].Subject.CommonName)
|
enc.AddString("client_common_name", t.PeerCertificates[0].Subject.CommonName)
|
||||||
enc.AddString("client_serial", t.PeerCertificates[0].SerialNumber.String())
|
enc.AddString("client_serial", t.PeerCertificates[0].SerialNumber.String())
|
||||||
|
|||||||
@@ -262,13 +262,17 @@ func (m MatchHost) Provision(_ caddy.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("converting hostname '%s' to ASCII: %v", host, err)
|
return fmt.Errorf("converting hostname '%s' to ASCII: %v", host, err)
|
||||||
}
|
}
|
||||||
if asciiHost != host {
|
|
||||||
m[i] = asciiHost
|
|
||||||
}
|
|
||||||
normalizedHost := strings.ToLower(asciiHost)
|
normalizedHost := strings.ToLower(asciiHost)
|
||||||
if firstI, ok := seen[normalizedHost]; ok {
|
if firstI, ok := seen[normalizedHost]; ok {
|
||||||
return fmt.Errorf("host at index %d is repeated at index %d: %s", firstI, i, host)
|
return fmt.Errorf("host at index %d is repeated at index %d: %s", firstI, i, host)
|
||||||
}
|
}
|
||||||
|
// Normalize exact hosts for standardized comparison in large-list fastpath later on.
|
||||||
|
// Keep wildcards/placeholders untouched.
|
||||||
|
if m.fuzzy(asciiHost) {
|
||||||
|
m[i] = asciiHost
|
||||||
|
} else {
|
||||||
|
m[i] = normalizedHost
|
||||||
|
}
|
||||||
seen[normalizedHost] = i
|
seen[normalizedHost] = i
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,14 +316,15 @@ func (m MatchHost) MatchWithError(r *http.Request) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if m.large() {
|
if m.large() {
|
||||||
|
reqHostLower := strings.ToLower(reqHost)
|
||||||
// fast path: locate exact match using binary search (about 100-1000x faster for large lists)
|
// fast path: locate exact match using binary search (about 100-1000x faster for large lists)
|
||||||
pos := sort.Search(len(m), func(i int) bool {
|
pos := sort.Search(len(m), func(i int) bool {
|
||||||
if m.fuzzy(m[i]) {
|
if m.fuzzy(m[i]) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return m[i] >= reqHost
|
return m[i] >= reqHostLower
|
||||||
})
|
})
|
||||||
if pos < len(m) && m[pos] == reqHost {
|
if pos < len(m) && m[pos] == reqHostLower {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,6 +538,7 @@ func (m MatchPath) MatchWithError(r *http.Request) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) bool {
|
func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) bool {
|
||||||
|
escapedPath = strings.ToLower(escapedPath)
|
||||||
// We would just compare the pattern against r.URL.Path,
|
// We would just compare the pattern against r.URL.Path,
|
||||||
// but the pattern contains %, indicating that we should
|
// but the pattern contains %, indicating that we should
|
||||||
// compare at least some part of the path in raw/escaped
|
// compare at least some part of the path in raw/escaped
|
||||||
@@ -632,8 +638,8 @@ func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) b
|
|||||||
// we can now treat rawpath globs (%*) as regular globs (*)
|
// we can now treat rawpath globs (%*) as regular globs (*)
|
||||||
matchPath = strings.ReplaceAll(matchPath, "%*", "*")
|
matchPath = strings.ReplaceAll(matchPath, "%*", "*")
|
||||||
|
|
||||||
// ignore error here because we can't handle it anyway=
|
// ignore error here because we can't handle it anyway
|
||||||
matches, _ := path.Match(matchPath, sb.String())
|
matches, _ := path.Match(matchPath, strings.ToLower(sb.String()))
|
||||||
return matches
|
return matches
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -412,6 +412,16 @@ func TestPathMatcher(t *testing.T) {
|
|||||||
input: "/foo%2fbar/baz",
|
input: "/foo%2fbar/baz",
|
||||||
expect: true,
|
expect: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
match: MatchPath{"/admin%2fpanel"},
|
||||||
|
input: "/ADMIN%2fpanel",
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: MatchPath{"/admin%2fpa*el"},
|
||||||
|
input: "/ADMIN%2fPaAzZLm123NEL",
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
err := tc.match.Provision(caddy.Context{})
|
err := tc.match.Provision(caddy.Context{})
|
||||||
if err == nil && tc.provisionErr {
|
if err == nil && tc.provisionErr {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import (
|
|||||||
// "http": {
|
// "http": {
|
||||||
// "metrics": {
|
// "metrics": {
|
||||||
// "per_host": true,
|
// "per_host": true,
|
||||||
// "allow_catch_all_hosts": false
|
// "observe_catchall_hosts": false
|
||||||
// },
|
// },
|
||||||
// "servers": {
|
// "servers": {
|
||||||
// "srv0": {
|
// "srv0": {
|
||||||
@@ -65,7 +65,7 @@ type Metrics struct {
|
|||||||
//
|
//
|
||||||
// Set to true to allow all hosts to get individual metrics (NOT RECOMMENDED
|
// Set to true to allow all hosts to get individual metrics (NOT RECOMMENDED
|
||||||
// for production environments exposed to the internet).
|
// for production environments exposed to the internet).
|
||||||
AllowCatchAllHosts bool `json:"allow_catch_all_hosts,omitempty"`
|
ObserveCatchallHosts bool `json:"observe_catchall_hosts,omitempty"`
|
||||||
|
|
||||||
init sync.Once
|
init sync.Once
|
||||||
httpMetrics *httpMetrics
|
httpMetrics *httpMetrics
|
||||||
@@ -200,7 +200,7 @@ func (m *Metrics) shouldAllowHostMetrics(host string, isHTTPS bool) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For catch-all requests (not in allowed hosts)
|
// For catch-all requests (not in allowed hosts)
|
||||||
allowCatchAll := m.AllowCatchAllHosts || (isHTTPS && m.hasHTTPSServer)
|
allowCatchAll := m.ObserveCatchallHosts || (isHTTPS && m.hasHTTPSServer)
|
||||||
return allowCatchAll
|
return allowCatchAll
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -207,11 +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,
|
||||||
AllowCatchAllHosts: true, // Allow all hosts for testing
|
ObserveCatchallHosts: true, // Allow all hosts for testing
|
||||||
init: sync.Once{},
|
init: sync.Once{},
|
||||||
httpMetrics: &httpMetrics{},
|
httpMetrics: &httpMetrics{},
|
||||||
allowedHosts: make(map[string]struct{}),
|
allowedHosts: make(map[string]struct{}),
|
||||||
}
|
}
|
||||||
handlerErr := errors.New("oh noes")
|
handlerErr := errors.New("oh noes")
|
||||||
response := []byte("hello world!")
|
response := []byte("hello world!")
|
||||||
@@ -387,11 +387,11 @@ func TestMetricsCardinalityProtection(t *testing.T) {
|
|||||||
|
|
||||||
// Test 1: Without AllowCatchAllHosts, arbitrary hosts should be mapped to "_other"
|
// Test 1: Without AllowCatchAllHosts, arbitrary hosts should be mapped to "_other"
|
||||||
metrics := &Metrics{
|
metrics := &Metrics{
|
||||||
PerHost: true,
|
PerHost: true,
|
||||||
AllowCatchAllHosts: false, // Default - should map unknown hosts to "_other"
|
ObserveCatchallHosts: false, // Default - should map unknown hosts to "_other"
|
||||||
init: sync.Once{},
|
init: sync.Once{},
|
||||||
httpMetrics: &httpMetrics{},
|
httpMetrics: &httpMetrics{},
|
||||||
allowedHosts: make(map[string]struct{}),
|
allowedHosts: make(map[string]struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add one allowed host
|
// Add one allowed host
|
||||||
@@ -444,12 +444,12 @@ func TestMetricsHTTPSCatchAll(t *testing.T) {
|
|||||||
|
|
||||||
// Test that HTTPS requests allow catch-all even when AllowCatchAllHosts is false
|
// Test that HTTPS requests allow catch-all even when AllowCatchAllHosts is false
|
||||||
metrics := &Metrics{
|
metrics := &Metrics{
|
||||||
PerHost: true,
|
PerHost: true,
|
||||||
AllowCatchAllHosts: false,
|
ObserveCatchallHosts: false,
|
||||||
hasHTTPSServer: true, // Simulate having HTTPS servers
|
hasHTTPSServer: true, // Simulate having HTTPS servers
|
||||||
init: sync.Once{},
|
init: sync.Once{},
|
||||||
httpMetrics: &httpMetrics{},
|
httpMetrics: &httpMetrics{},
|
||||||
allowedHosts: make(map[string]struct{}), // Empty - no explicitly allowed hosts
|
allowedHosts: make(map[string]struct{}), // Empty - no explicitly allowed hosts
|
||||||
}
|
}
|
||||||
|
|
||||||
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
// include current token, which we treat as an argument here
|
// include current token, which we treat as an argument here
|
||||||
|
// nolint:prealloc
|
||||||
args := []string{h.Val()}
|
args := []string{h.Val()}
|
||||||
args = append(args, h.RemainingArgs()...)
|
args = append(args, h.RemainingArgs()...)
|
||||||
|
|
||||||
|
|||||||
@@ -229,6 +229,21 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
|||||||
req.Body = io.NopCloser(buf) // replace real body with buffered data
|
req.Body = io.NopCloser(buf) // replace real body with buffered data
|
||||||
return buf.String(), true
|
return buf.String(), true
|
||||||
|
|
||||||
|
case "http.request.body_base64":
|
||||||
|
if req.Body == nil {
|
||||||
|
return "", true
|
||||||
|
}
|
||||||
|
// normally net/http will close the body for us, but since we
|
||||||
|
// are replacing it with a fake one, we have to ensure we close
|
||||||
|
// the real body ourselves when we're done
|
||||||
|
defer req.Body.Close()
|
||||||
|
// read the request body into a buffer (can't pool because we
|
||||||
|
// don't know its lifetime and would have to make a copy anyway)
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
_, _ = io.Copy(buf, req.Body) // can't handle error, so just ignore it
|
||||||
|
req.Body = io.NopCloser(buf) // replace real body with buffered data
|
||||||
|
return base64.StdEncoding.EncodeToString(buf.Bytes()), true
|
||||||
|
|
||||||
// original request, before any internal changes
|
// original request, before any internal changes
|
||||||
case "http.request.orig_method":
|
case "http.request.orig_method":
|
||||||
or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
|
or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
|
||||||
@@ -511,6 +526,8 @@ func getReqTLSReplacement(req *http.Request, key string) (any, bool) {
|
|||||||
return true, true
|
return true, true
|
||||||
case "server_name":
|
case "server_name":
|
||||||
return req.TLS.ServerName, true
|
return req.TLS.ServerName, true
|
||||||
|
case "ech":
|
||||||
|
return req.TLS.ECHAccepted, true
|
||||||
}
|
}
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -1525,6 +1528,7 @@ func (u *SRVUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||||||
return d.Errf("bad delay value '%s': %v", d.Val(), err)
|
return d.Errf("bad delay value '%s': %v", d.Val(), err)
|
||||||
}
|
}
|
||||||
u.FallbackDelay = caddy.Duration(dur)
|
u.FallbackDelay = caddy.Duration(dur)
|
||||||
|
|
||||||
case "grace_period":
|
case "grace_period":
|
||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return d.ArgErr()
|
return d.ArgErr()
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand/v2"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/fcgi"
|
"net/http/fcgi"
|
||||||
@@ -197,7 +197,7 @@ func generateRandFile(size int) (p string, m string) {
|
|||||||
h := md5.New()
|
h := md5.New()
|
||||||
for i := 0; i < size/16; i++ {
|
for i := 0; i < size/16; i++ {
|
||||||
buf := make([]byte, 16)
|
buf := make([]byte, 16)
|
||||||
binary.PutVarint(buf, rand.Int63())
|
binary.PutVarint(buf, rand.Int64())
|
||||||
if _, err := fo.Write(buf); err != nil {
|
if _, err := fo.Write(buf); err != nil {
|
||||||
log.Printf("[ERROR] failed to write buffer: %v\n", err)
|
log.Printf("[ERROR] failed to write buffer: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package fastcgi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -23,9 +24,12 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
"golang.org/x/text/search"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
@@ -33,7 +37,11 @@ import (
|
|||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
)
|
)
|
||||||
|
|
||||||
var noopLogger = zap.NewNop()
|
var (
|
||||||
|
ErrInvalidSplitPath = errors.New("split path contains non-ASCII characters")
|
||||||
|
|
||||||
|
noopLogger = zap.NewNop()
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(Transport{})
|
caddy.RegisterModule(Transport{})
|
||||||
@@ -50,6 +58,9 @@ type Transport struct {
|
|||||||
// actual resource (CGI script) name, and the second piece will be set to
|
// actual resource (CGI script) name, and the second piece will be set to
|
||||||
// PATH_INFO for the CGI script to use.
|
// PATH_INFO for the CGI script to use.
|
||||||
//
|
//
|
||||||
|
// Split paths can only contain ASCII characters.
|
||||||
|
// Comparison is case-insensitive.
|
||||||
|
//
|
||||||
// Future enhancements should be careful to avoid CVE-2019-11043,
|
// Future enhancements should be careful to avoid CVE-2019-11043,
|
||||||
// which can be mitigated with use of a try_files-like behavior
|
// which can be mitigated with use of a try_files-like behavior
|
||||||
// that 404s if the fastcgi path info is not found.
|
// that 404s if the fastcgi path info is not found.
|
||||||
@@ -109,9 +120,45 @@ func (t *Transport) Provision(ctx caddy.Context) error {
|
|||||||
t.DialTimeout = caddy.Duration(3 * time.Second)
|
t.DialTimeout = caddy.Duration(3 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
for i, split := range t.SplitPath {
|
||||||
|
b.Grow(len(split))
|
||||||
|
|
||||||
|
for j := 0; j < len(split); j++ {
|
||||||
|
c := split[j]
|
||||||
|
if c >= utf8.RuneSelf {
|
||||||
|
return ErrInvalidSplitPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if 'A' <= c && c <= 'Z' {
|
||||||
|
b.WriteByte(c + 'a' - 'A')
|
||||||
|
} else {
|
||||||
|
b.WriteByte(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.SplitPath[i] = b.String()
|
||||||
|
b.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -371,8 +418,15 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) {
|
|||||||
return env, nil
|
return env, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var splitSearchNonASCII = search.New(language.Und, search.IgnoreCase)
|
||||||
|
|
||||||
// splitPos returns the index where path should
|
// splitPos returns the index where path should
|
||||||
// be split based on t.SplitPath.
|
// be split based on t.SplitPath.
|
||||||
|
//
|
||||||
|
// example: if splitPath is [".php"]
|
||||||
|
// "/path/to/script.php/some/path": ("/path/to/script.php", "/some/path")
|
||||||
|
//
|
||||||
|
// Adapted from FrankenPHP's code (copyright 2026 Kévin Dunglas, MIT license)
|
||||||
func (t Transport) splitPos(path string) int {
|
func (t Transport) splitPos(path string) int {
|
||||||
// TODO: from v1...
|
// TODO: from v1...
|
||||||
// if httpserver.CaseSensitivePath {
|
// if httpserver.CaseSensitivePath {
|
||||||
@@ -382,12 +436,54 @@ func (t Transport) splitPos(path string) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
lowerPath := strings.ToLower(path)
|
pathLen := len(path)
|
||||||
|
|
||||||
|
// We are sure that split strings are all ASCII-only and lower-case because of validation and normalization in Provision().
|
||||||
for _, split := range t.SplitPath {
|
for _, split := range t.SplitPath {
|
||||||
if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 {
|
splitLen := len(split)
|
||||||
return idx + len(split)
|
|
||||||
|
for i := 0; i < pathLen; i++ {
|
||||||
|
if path[i] >= utf8.RuneSelf {
|
||||||
|
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
|
||||||
|
return end
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if i+splitLen > pathLen {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
match := true
|
||||||
|
for j := 0; j < splitLen; j++ {
|
||||||
|
c := path[i+j]
|
||||||
|
|
||||||
|
if c >= utf8.RuneSelf {
|
||||||
|
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
|
||||||
|
return end
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if 'A' <= c && c <= 'Z' {
|
||||||
|
c += 'a' - 'A'
|
||||||
|
}
|
||||||
|
|
||||||
|
if c != split[j] {
|
||||||
|
match = false
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if match {
|
||||||
|
return i + splitLen
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,6 +523,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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
package fastcgi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProvisionSplitPath(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
splitPath []string
|
||||||
|
wantErr error
|
||||||
|
wantSplitPath []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid lowercase split path",
|
||||||
|
splitPath: []string{".php"},
|
||||||
|
wantErr: nil,
|
||||||
|
wantSplitPath: []string{".php"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid uppercase split path normalized",
|
||||||
|
splitPath: []string{".PHP"},
|
||||||
|
wantErr: nil,
|
||||||
|
wantSplitPath: []string{".php"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid mixed case split path normalized",
|
||||||
|
splitPath: []string{".PhP", ".PHTML"},
|
||||||
|
wantErr: nil,
|
||||||
|
wantSplitPath: []string{".php", ".phtml"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty split path",
|
||||||
|
splitPath: []string{},
|
||||||
|
wantErr: nil,
|
||||||
|
wantSplitPath: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-ASCII character in split path rejected",
|
||||||
|
splitPath: []string{".php", ".Ⱥphp"},
|
||||||
|
wantErr: ErrInvalidSplitPath,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode character in split path rejected",
|
||||||
|
splitPath: []string{".phpⱥ"},
|
||||||
|
wantErr: ErrInvalidSplitPath,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tr := Transport{SplitPath: tt.splitPath}
|
||||||
|
err := tr.Provision(caddy.Context{})
|
||||||
|
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.wantSplitPath, tr.SplitPath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitPos(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
splitPath []string
|
||||||
|
wantPos int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple php extension",
|
||||||
|
path: "/path/to/script.php",
|
||||||
|
splitPath: []string{".php"},
|
||||||
|
wantPos: 19,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "php extension with path info",
|
||||||
|
path: "/path/to/script.php/some/path",
|
||||||
|
splitPath: []string{".php"},
|
||||||
|
wantPos: 19,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "case insensitive match",
|
||||||
|
path: "/path/to/script.PHP",
|
||||||
|
splitPath: []string{".php"},
|
||||||
|
wantPos: 19,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed case match",
|
||||||
|
path: "/path/to/script.PhP/info",
|
||||||
|
splitPath: []string{".php"},
|
||||||
|
wantPos: 19,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no match",
|
||||||
|
path: "/path/to/script.txt",
|
||||||
|
splitPath: []string{".php"},
|
||||||
|
wantPos: -1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty split path",
|
||||||
|
path: "/path/to/script.php",
|
||||||
|
splitPath: []string{},
|
||||||
|
wantPos: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple split paths first match",
|
||||||
|
path: "/path/to/script.php",
|
||||||
|
splitPath: []string{".php", ".phtml"},
|
||||||
|
wantPos: 19,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple split paths second match",
|
||||||
|
path: "/path/to/script.phtml",
|
||||||
|
splitPath: []string{".php", ".phtml"},
|
||||||
|
wantPos: 21,
|
||||||
|
},
|
||||||
|
// Unicode case-folding tests (security fix for GHSA-g966-83w7-6w38)
|
||||||
|
// U+023A (Ⱥ) lowercases to U+2C65 (ⱥ), which has different UTF-8 byte length
|
||||||
|
// Ⱥ: 2 bytes (C8 BA), ⱥ: 3 bytes (E2 B1 A5)
|
||||||
|
{
|
||||||
|
name: "unicode path with case-folding length expansion",
|
||||||
|
path: "/ȺȺȺȺshell.php",
|
||||||
|
splitPath: []string{".php"},
|
||||||
|
wantPos: 18, // correct position in original string
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode path with extension after expansion chars",
|
||||||
|
path: "/ȺȺȺȺshell.php/path/info",
|
||||||
|
splitPath: []string{".php"},
|
||||||
|
wantPos: 18,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode in filename with multiple php occurrences",
|
||||||
|
path: "/ȺȺȺȺshell.php.txt.php",
|
||||||
|
splitPath: []string{".php"},
|
||||||
|
wantPos: 18, // should match first .php, not be confused by byte offset shift
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode case insensitive extension",
|
||||||
|
path: "/ȺȺȺȺshell.PHP",
|
||||||
|
splitPath: []string{".php"},
|
||||||
|
wantPos: 18,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode in middle of path",
|
||||||
|
path: "/path/Ⱥtest/script.php",
|
||||||
|
splitPath: []string{".php"},
|
||||||
|
wantPos: 23, // Ⱥ is 2 bytes, so path is 23 bytes total, .php ends at byte 23
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode only in directory not filename",
|
||||||
|
path: "/Ⱥ/script.php",
|
||||||
|
splitPath: []string{".php"},
|
||||||
|
wantPos: 14,
|
||||||
|
},
|
||||||
|
// Additional Unicode characters that expand when lowercased
|
||||||
|
// U+0130 (İ - Turkish capital I with dot) lowercases to U+0069 + U+0307
|
||||||
|
{
|
||||||
|
name: "turkish capital I with dot",
|
||||||
|
path: "/İtest.php",
|
||||||
|
splitPath: []string{".php"},
|
||||||
|
wantPos: 11,
|
||||||
|
},
|
||||||
|
// Ensure standard ASCII still works correctly
|
||||||
|
{
|
||||||
|
name: "ascii only path with case variation",
|
||||||
|
path: "/PATH/TO/SCRIPT.PHP/INFO",
|
||||||
|
splitPath: []string{".php"},
|
||||||
|
wantPos: 19,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path at root",
|
||||||
|
path: "/index.php",
|
||||||
|
splitPath: []string{".php"},
|
||||||
|
wantPos: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "extension in middle of filename",
|
||||||
|
path: "/test.php.bak",
|
||||||
|
splitPath: []string{".php"},
|
||||||
|
wantPos: 9,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotPos := Transport{SplitPath: tt.splitPath}.splitPos(tt.path)
|
||||||
|
assert.Equal(t, tt.wantPos, gotPos, "splitPos(%q, %v)", tt.path, tt.splitPath)
|
||||||
|
|
||||||
|
// Verify that the split produces valid substrings
|
||||||
|
if gotPos > 0 && gotPos <= len(tt.path) {
|
||||||
|
scriptName := tt.path[:gotPos]
|
||||||
|
pathInfo := tt.path[gotPos:]
|
||||||
|
|
||||||
|
// The script name should end with one of the split extensions (case-insensitive)
|
||||||
|
hasValidEnding := false
|
||||||
|
for _, split := range tt.splitPath {
|
||||||
|
if strings.HasSuffix(strings.ToLower(scriptName), split) {
|
||||||
|
hasValidEnding = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, hasValidEnding, "script name %q should end with one of %v", scriptName, tt.splitPath)
|
||||||
|
|
||||||
|
// Original path should be reconstructable
|
||||||
|
assert.Equal(t, tt.path, scriptName+pathInfo, "path should be reconstructable from split parts")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSplitPosUnicodeSecurityRegression specifically tests the vulnerability
|
||||||
|
// described in GHSA-g966-83w7-6w38 where Unicode case-folding caused
|
||||||
|
// incorrect SCRIPT_NAME/PATH_INFO splitting
|
||||||
|
func TestSplitPosUnicodeSecurityRegression(t *testing.T) {
|
||||||
|
// U+023A: Ⱥ (UTF-8: C8 BA). Lowercase is ⱥ (UTF-8: E2 B1 A5), longer in bytes.
|
||||||
|
path := "/ȺȺȺȺshell.php.txt.php"
|
||||||
|
split := []string{".php"}
|
||||||
|
|
||||||
|
pos := Transport{SplitPath: split}.splitPos(path)
|
||||||
|
|
||||||
|
// The vulnerable code would return 22 (computed on lowercased string)
|
||||||
|
// The correct code should return 18 (position in original string)
|
||||||
|
expectedPos := strings.Index(path, ".php") + len(".php")
|
||||||
|
assert.Equal(t, expectedPos, pos, "split position should match first .php in original string")
|
||||||
|
assert.Equal(t, 18, pos, "split position should be 18, not 22")
|
||||||
|
|
||||||
|
if pos > 0 && pos <= len(path) {
|
||||||
|
scriptName := path[:pos]
|
||||||
|
pathInfo := path[pos:]
|
||||||
|
|
||||||
|
assert.Equal(t, "/ȺȺȺȺshell.php", scriptName, "script name should be the path up to first .php")
|
||||||
|
assert.Equal(t, ".txt.php", pathInfo, "path info should be the remainder after first .php")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package reverseproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAddForwardedHeadersNonIP(t *testing.T) {
|
||||||
|
h := Handler{}
|
||||||
|
|
||||||
|
// Simulate a request with a non-IP remote address (e.g. SCION, abstract socket, or hostname)
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
req.RemoteAddr = "my-weird-network:12345"
|
||||||
|
|
||||||
|
// Mock the context variables required by Caddy.
|
||||||
|
// We need to inject the variable map manually since we aren't running the full server.
|
||||||
|
vars := map[string]interface{}{
|
||||||
|
caddyhttp.TrustedProxyVarKey: false,
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
// Execute the unexported function
|
||||||
|
err := h.addForwardedHeaders(req)
|
||||||
|
|
||||||
|
// Expectation: No error should be returned for non-IP addresses.
|
||||||
|
// The function should simply skip the trusted proxy check.
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error for non-IP address, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddForwardedHeaders_UnixSocketTrusted(t *testing.T) {
|
||||||
|
h := Handler{}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/", nil)
|
||||||
|
req.RemoteAddr = "@"
|
||||||
|
req.Header.Set("X-Forwarded-For", "1.2.3.4, 10.0.0.1")
|
||||||
|
req.Header.Set("X-Forwarded-Proto", "https")
|
||||||
|
req.Header.Set("X-Forwarded-Host", "original.example.com")
|
||||||
|
|
||||||
|
vars := map[string]interface{}{
|
||||||
|
caddyhttp.TrustedProxyVarKey: true,
|
||||||
|
caddyhttp.ClientIPVarKey: "1.2.3.4",
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
err := h.addForwardedHeaders(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := req.Header.Get("X-Forwarded-For"); got != "1.2.3.4, 10.0.0.1" {
|
||||||
|
t.Errorf("X-Forwarded-For = %q, want %q", got, "1.2.3.4, 10.0.0.1")
|
||||||
|
}
|
||||||
|
if got := req.Header.Get("X-Forwarded-Proto"); got != "https" {
|
||||||
|
t.Errorf("X-Forwarded-Proto = %q, want %q", got, "https")
|
||||||
|
}
|
||||||
|
if got := req.Header.Get("X-Forwarded-Host"); got != "original.example.com" {
|
||||||
|
t.Errorf("X-Forwarded-Host = %q, want %q", got, "original.example.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddForwardedHeaders_UnixSocketUntrusted(t *testing.T) {
|
||||||
|
h := Handler{}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/", nil)
|
||||||
|
req.RemoteAddr = "@"
|
||||||
|
req.Header.Set("X-Forwarded-For", "1.2.3.4")
|
||||||
|
req.Header.Set("X-Forwarded-Proto", "https")
|
||||||
|
req.Header.Set("X-Forwarded-Host", "spoofed.example.com")
|
||||||
|
|
||||||
|
vars := map[string]interface{}{
|
||||||
|
caddyhttp.TrustedProxyVarKey: false,
|
||||||
|
caddyhttp.ClientIPVarKey: "",
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
err := h.addForwardedHeaders(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := req.Header.Get("X-Forwarded-For"); got != "" {
|
||||||
|
t.Errorf("X-Forwarded-For should be deleted, got %q", got)
|
||||||
|
}
|
||||||
|
if got := req.Header.Get("X-Forwarded-Proto"); got != "" {
|
||||||
|
t.Errorf("X-Forwarded-Proto should be deleted, got %q", got)
|
||||||
|
}
|
||||||
|
if got := req.Header.Get("X-Forwarded-Host"); got != "" {
|
||||||
|
t.Errorf("X-Forwarded-Host should be deleted, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddForwardedHeaders_UnixSocketTrustedNoExistingHeaders(t *testing.T) {
|
||||||
|
h := Handler{}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/", nil)
|
||||||
|
req.RemoteAddr = "@"
|
||||||
|
|
||||||
|
vars := map[string]interface{}{
|
||||||
|
caddyhttp.TrustedProxyVarKey: true,
|
||||||
|
caddyhttp.ClientIPVarKey: "5.6.7.8",
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
err := h.addForwardedHeaders(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := req.Header.Get("X-Forwarded-For"); got != "" {
|
||||||
|
t.Errorf("X-Forwarded-For should be empty when no prior XFF exists, got %q", got)
|
||||||
|
}
|
||||||
|
if got := req.Header.Get("X-Forwarded-Proto"); got != "http" {
|
||||||
|
t.Errorf("X-Forwarded-Proto = %q, want %q", got, "http")
|
||||||
|
}
|
||||||
|
if got := req.Header.Get("X-Forwarded-Host"); got != "example.com" {
|
||||||
|
t.Errorf("X-Forwarded-Host = %q, want %q", got, "example.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -506,7 +500,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
|
|||||||
}
|
}
|
||||||
|
|
||||||
// do the request, being careful to tame the response body
|
// do the request, being careful to tame the response body
|
||||||
resp, err := h.HealthChecks.Active.httpClient.Do(req)
|
resp, err := h.HealthChecks.Active.httpClient.Do(req) //nolint:gosec // no SSRF
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if c := h.HealthChecks.Active.logger.Check(zapcore.InfoLevel, "HTTP request failed"); c != nil {
|
if c := h.HealthChecks.Active.logger.Check(zapcore.InfoLevel, "HTTP request failed"); c != nil {
|
||||||
c.Write(
|
c.Write(
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
weakrand "math/rand"
|
weakrand "math/rand/v2"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -40,6 +40,7 @@ import (
|
|||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
"github.com/caddyserver/caddy/v2/modules/internal/network"
|
"github.com/caddyserver/caddy/v2/modules/internal/network"
|
||||||
)
|
)
|
||||||
@@ -265,7 +266,7 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
|
|||||||
PreferGo: true,
|
PreferGo: true,
|
||||||
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
addr := h.Resolver.netAddrs[weakrand.Intn(len(h.Resolver.netAddrs))]
|
addr := h.Resolver.netAddrs[weakrand.IntN(len(h.Resolver.netAddrs))]
|
||||||
return d.DialContext(ctx, addr.Network, addr.JoinHostPort(0))
|
return d.DialContext(ctx, addr.Network, addr.JoinHostPort(0))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -514,6 +515,28 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
|
|||||||
return rt, nil
|
return rt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequestHeaderOps implements TransportHeaderOpsProvider. It returns header
|
||||||
|
// operations for requests when the transport's configuration indicates they
|
||||||
|
// should be applied. In particular, when TLS is enabled for this transport,
|
||||||
|
// return an operation to set the Host header to the upstream host:port
|
||||||
|
// placeholder so HTTPS upstreams get the proper Host by default.
|
||||||
|
//
|
||||||
|
// Note: this is a provision-time hook; the Handler will call this during
|
||||||
|
// its Provision and cache the resulting HeaderOps. The HeaderOps are
|
||||||
|
// applied per-request (so placeholders are expanded at request time).
|
||||||
|
func (h *HTTPTransport) RequestHeaderOps() *headers.HeaderOps {
|
||||||
|
// If TLS is not configured for this transport, don't inject Host
|
||||||
|
// defaults. TLS being non-nil indicates HTTPS to the upstream.
|
||||||
|
if h.TLS == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &headers.HeaderOps{
|
||||||
|
Set: http.Header{
|
||||||
|
"Host": []string{"{http.reverse_proxy.upstream.hostport}"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RoundTrip implements http.RoundTripper.
|
// RoundTrip implements http.RoundTripper.
|
||||||
func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
h.SetScheme(req)
|
h.SetScheme(req)
|
||||||
@@ -564,6 +587,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 {
|
||||||
@@ -820,8 +863,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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -94,3 +94,24 @@ func TestHTTPTransportUnmarshalCaddyFileWithCaPools(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHTTPTransport_RequestHeaderOps_TLS(t *testing.T) {
|
||||||
|
var ht HTTPTransport
|
||||||
|
// When TLS is nil, expect no header ops
|
||||||
|
if ops := ht.RequestHeaderOps(); ops != nil {
|
||||||
|
t.Fatalf("expected nil HeaderOps when TLS is nil, got: %#v", ops)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When TLS is configured, expect a HeaderOps that sets Host
|
||||||
|
ht.TLS = &TLSConfig{}
|
||||||
|
ops := ht.RequestHeaderOps()
|
||||||
|
if ops == nil {
|
||||||
|
t.Fatal("expected non-nil HeaderOps when TLS is set")
|
||||||
|
}
|
||||||
|
if ops.Set == nil {
|
||||||
|
t.Fatalf("expected ops.Set to be non-nil, got nil")
|
||||||
|
}
|
||||||
|
if got := ops.Set.Get("Host"); got != "{http.reverse_proxy.upstream.hostport}" {
|
||||||
|
t.Fatalf("unexpected Host value; want placeholder, got: %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -192,6 +192,13 @@ type Handler struct {
|
|||||||
CB CircuitBreaker `json:"-"`
|
CB CircuitBreaker `json:"-"`
|
||||||
DynamicUpstreams UpstreamSource `json:"-"`
|
DynamicUpstreams UpstreamSource `json:"-"`
|
||||||
|
|
||||||
|
// transportHeaderOps is a set of header operations provided
|
||||||
|
// by the transport at provision time, if the transport
|
||||||
|
// implements TransportHeaderOpsProvider. These ops are
|
||||||
|
// applied before any user-configured header ops so the
|
||||||
|
// user can override transport defaults.
|
||||||
|
transportHeaderOps *headers.HeaderOps
|
||||||
|
|
||||||
// Holds the parsed CIDR ranges from TrustedProxies
|
// Holds the parsed CIDR ranges from TrustedProxies
|
||||||
trustedProxies []netip.Prefix
|
trustedProxies []netip.Prefix
|
||||||
|
|
||||||
@@ -243,18 +250,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 {
|
||||||
@@ -324,6 +329,18 @@ func (h *Handler) Provision(ctx caddy.Context) error {
|
|||||||
h.Transport = t
|
h.Transport = t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the transport can provide header ops, cache them now so we don't
|
||||||
|
// have to compute them per-request. Provision the HeaderOps if present
|
||||||
|
// so any runtime artifacts (like precompiled regex) are prepared.
|
||||||
|
if tph, ok := h.Transport.(RequestHeaderOpsTransport); ok {
|
||||||
|
h.transportHeaderOps = tph.RequestHeaderOps()
|
||||||
|
if h.transportHeaderOps != nil {
|
||||||
|
if err := h.transportHeaderOps.Provision(ctx); err != nil {
|
||||||
|
return fmt.Errorf("provisioning transport header ops: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// set up load balancing
|
// set up load balancing
|
||||||
if h.LoadBalancing == nil {
|
if h.LoadBalancing == nil {
|
||||||
h.LoadBalancing = new(LoadBalancing)
|
h.LoadBalancing = new(LoadBalancing)
|
||||||
@@ -439,6 +456,20 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
|
|||||||
reqHost := clonedReq.Host
|
reqHost := clonedReq.Host
|
||||||
reqHeader := clonedReq.Header
|
reqHeader := clonedReq.Header
|
||||||
|
|
||||||
|
// If the cloned request body was fully buffered, keep a reference to its
|
||||||
|
// buffer so we can reuse it across retries and return it to the pool
|
||||||
|
// once we’re done.
|
||||||
|
var bufferedReqBody *bytes.Buffer
|
||||||
|
if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil {
|
||||||
|
bufferedReqBody = reqBodyBuf.buf
|
||||||
|
reqBodyBuf.buf = nil
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
bufferedReqBody.Reset()
|
||||||
|
bufPool.Put(bufferedReqBody)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
// total proxying duration, including time spent on LB and retries
|
// total proxying duration, including time spent on LB and retries
|
||||||
@@ -457,8 +488,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
|
|||||||
// and reusable, so if a backend partially or fully reads the body but then
|
// and reusable, so if a backend partially or fully reads the body but then
|
||||||
// produces an error, the request can be repeated to the next backend with
|
// produces an error, the request can be repeated to the next backend with
|
||||||
// the full body (retries should only happen for idempotent requests) (see #6259)
|
// the full body (retries should only happen for idempotent requests) (see #6259)
|
||||||
if reqBodyBuf, ok := r.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil {
|
if bufferedReqBody != nil {
|
||||||
r.Body = io.NopCloser(bytes.NewReader(reqBodyBuf.buf.Bytes()))
|
clonedReq.Body = io.NopCloser(bytes.NewReader(bufferedReqBody.Bytes()))
|
||||||
}
|
}
|
||||||
|
|
||||||
var done bool
|
var done bool
|
||||||
@@ -563,14 +594,26 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
|
|||||||
repl.Set("http.reverse_proxy.upstream.fails", upstream.Host.Fails())
|
repl.Set("http.reverse_proxy.upstream.fails", upstream.Host.Fails())
|
||||||
|
|
||||||
// mutate request headers according to this upstream;
|
// mutate request headers according to this upstream;
|
||||||
// because we're in a retry loop, we have to copy
|
// because we're in a retry loop, we have to copy headers
|
||||||
// headers (and the r.Host value) from the original
|
// (and the r.Host value) from the original so that each
|
||||||
// so that each retry is identical to the first
|
// retry is identical to the first. If either transport or
|
||||||
if h.Headers != nil && h.Headers.Request != nil {
|
// user ops exist, apply them in order (transport first,
|
||||||
|
// then user, so user's config wins).
|
||||||
|
var userOps *headers.HeaderOps
|
||||||
|
if h.Headers != nil {
|
||||||
|
userOps = h.Headers.Request
|
||||||
|
}
|
||||||
|
transportOps := h.transportHeaderOps
|
||||||
|
if transportOps != nil || userOps != nil {
|
||||||
r.Header = make(http.Header)
|
r.Header = make(http.Header)
|
||||||
copyHeader(r.Header, reqHeader)
|
copyHeader(r.Header, reqHeader)
|
||||||
r.Host = reqHost
|
r.Host = reqHost
|
||||||
h.Headers.Request.ApplyToRequest(r)
|
if transportOps != nil {
|
||||||
|
transportOps.ApplyToRequest(r)
|
||||||
|
}
|
||||||
|
if userOps != nil {
|
||||||
|
userOps.ApplyToRequest(r)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// proxy the request to that upstream
|
// proxy the request to that upstream
|
||||||
@@ -758,48 +801,71 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.
|
|||||||
// the headers at all, then they will be added with the values
|
// the headers at all, then they will be added with the values
|
||||||
// that we can glean from the request.
|
// that we can glean from the request.
|
||||||
func (h Handler) addForwardedHeaders(req *http.Request) error {
|
func (h Handler) addForwardedHeaders(req *http.Request) error {
|
||||||
// Parse the remote IP, ignore the error as non-fatal,
|
|
||||||
// but the remote IP is required to continue, so we
|
|
||||||
// just return early. This should probably never happen
|
|
||||||
// though, unless some other module manipulated the request's
|
|
||||||
// remote address and used an invalid value.
|
|
||||||
clientIP, _, err := net.SplitHostPort(req.RemoteAddr)
|
|
||||||
if err != nil {
|
|
||||||
// Remove the `X-Forwarded-*` headers to avoid upstreams
|
|
||||||
// potentially trusting a header that came from the client
|
|
||||||
req.Header.Del("X-Forwarded-For")
|
|
||||||
req.Header.Del("X-Forwarded-Proto")
|
|
||||||
req.Header.Del("X-Forwarded-Host")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client IP may contain a zone if IPv6, so we need
|
|
||||||
// to pull that out before parsing the IP
|
|
||||||
clientIP, _, _ = strings.Cut(clientIP, "%")
|
|
||||||
ipAddr, err := netip.ParseAddr(clientIP)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid IP address: '%s': %v", clientIP, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the client is a trusted proxy
|
// Check if the client is a trusted proxy
|
||||||
trusted := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool)
|
trusted := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool)
|
||||||
for _, ipRange := range h.trustedProxies {
|
|
||||||
if ipRange.Contains(ipAddr) {
|
var clientIP string
|
||||||
trusted = true
|
|
||||||
break
|
if req.RemoteAddr == "@" {
|
||||||
|
// For Unix socket connections, RemoteAddr is "@" which cannot
|
||||||
|
// be parsed as host:port. If untrusted, strip forwarded headers
|
||||||
|
// for security. If trusted, there is no peer IP to append to
|
||||||
|
// X-Forwarded-For, so clientIP stays empty.
|
||||||
|
if !trusted {
|
||||||
|
req.Header.Del("X-Forwarded-For")
|
||||||
|
req.Header.Del("X-Forwarded-Proto")
|
||||||
|
req.Header.Del("X-Forwarded-Host")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Parse the remote IP, ignore the error as non-fatal,
|
||||||
|
// but the remote IP is required to continue, so we
|
||||||
|
// just return early. This should probably never happen
|
||||||
|
// though, unless some other module manipulated the request's
|
||||||
|
// remote address and used an invalid value.
|
||||||
|
var err error
|
||||||
|
clientIP, _, err = net.SplitHostPort(req.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
// Remove the `X-Forwarded-*` headers to avoid upstreams
|
||||||
|
// potentially trusting a header that came from the client
|
||||||
|
req.Header.Del("X-Forwarded-For")
|
||||||
|
req.Header.Del("X-Forwarded-Proto")
|
||||||
|
req.Header.Del("X-Forwarded-Host")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client IP may contain a zone if IPv6, so we need
|
||||||
|
// to pull that out before parsing the IP
|
||||||
|
clientIP, _, _ = strings.Cut(clientIP, "%")
|
||||||
|
ipAddr, err := netip.ParseAddr(clientIP)
|
||||||
|
|
||||||
|
// If ParseAddr fails (e.g. non-IP network like SCION), we cannot check
|
||||||
|
// if it is a trusted proxy by IP range. In this case, we ignore the
|
||||||
|
// error and treat the connection as untrusted (or retain existing status).
|
||||||
|
if err == nil {
|
||||||
|
for _, ipRange := range h.trustedProxies {
|
||||||
|
if ipRange.Contains(ipAddr) {
|
||||||
|
trusted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we aren't the first proxy, and the proxy is trusted,
|
// If we aren't the first proxy, and the proxy is trusted,
|
||||||
// retain prior X-Forwarded-For information as a comma+space
|
// retain prior X-Forwarded-For information as a comma+space
|
||||||
// separated list and fold multiple headers into one.
|
// separated list and fold multiple headers into one.
|
||||||
clientXFF := clientIP
|
|
||||||
prior, ok, omit := allHeaderValues(req.Header, "X-Forwarded-For")
|
prior, ok, omit := allHeaderValues(req.Header, "X-Forwarded-For")
|
||||||
if trusted && ok && prior != "" {
|
|
||||||
clientXFF = prior + ", " + clientXFF
|
|
||||||
}
|
|
||||||
if !omit {
|
if !omit {
|
||||||
req.Header.Set("X-Forwarded-For", clientXFF)
|
if trusted && ok && prior != "" {
|
||||||
|
if clientIP != "" {
|
||||||
|
req.Header.Set("X-Forwarded-For", prior+", "+clientIP)
|
||||||
|
} else {
|
||||||
|
req.Header.Set("X-Forwarded-For", prior)
|
||||||
|
}
|
||||||
|
} else if clientIP != "" {
|
||||||
|
req.Header.Set("X-Forwarded-For", clientIP)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set X-Forwarded-Proto; many backend apps expect this,
|
// Set X-Forwarded-Proto; many backend apps expect this,
|
||||||
@@ -1210,7 +1276,7 @@ func (h *Handler) directRequest(req *http.Request, di DialInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// add client address to the host to let transport differentiate requests from different clients
|
// add client address to the host to let transport differentiate requests from different clients
|
||||||
if ht, ok := h.Transport.(*HTTPTransport); ok && ht.ProxyProtocol != "" {
|
if ppt, ok := h.Transport.(ProxyProtocolTransport); ok && ppt.ProxyProtocolEnabled() {
|
||||||
if proxyProtocolInfo, ok := caddyhttp.GetVar(req.Context(), proxyProtocolInfoVarKey).(ProxyProtocolInfo); ok {
|
if proxyProtocolInfo, ok := caddyhttp.GetVar(req.Context(), proxyProtocolInfoVarKey).(ProxyProtocolInfo); ok {
|
||||||
reqHost = proxyProtocolInfo.AddrPort.String() + "->" + reqHost
|
reqHost = proxyProtocolInfo.AddrPort.String() + "->" + reqHost
|
||||||
}
|
}
|
||||||
@@ -1501,6 +1567,43 @@ 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestHeaderOpsTransport may be implemented by a transport to provide
|
||||||
|
// header operations to apply to requests immediately before the RoundTrip.
|
||||||
|
// For example, overriding the default Host when TLS is enabled.
|
||||||
|
type RequestHeaderOpsTransport interface {
|
||||||
|
// RequestHeaderOps allows a transport to provide header operations
|
||||||
|
// to apply to the request. The transport is asked at provision time
|
||||||
|
// to return a HeaderOps (or nil) that will be applied before
|
||||||
|
// user-configured header ops.
|
||||||
|
RequestHeaderOps() *headers.HeaderOps
|
||||||
|
}
|
||||||
|
|
||||||
// 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 }
|
||||||
@@ -1514,7 +1617,12 @@ type bodyReadCloser struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (brc bodyReadCloser) Close() error {
|
func (brc bodyReadCloser) Close() error {
|
||||||
bufPool.Put(brc.buf)
|
// Inside this package this will be set to nil for fully-buffered
|
||||||
|
// requests due to the possibility of retrial.
|
||||||
|
if brc.buf != nil {
|
||||||
|
bufPool.Put(brc.buf)
|
||||||
|
}
|
||||||
|
// For fully-buffered bodies, body is nil, so Close is a no-op.
|
||||||
if brc.body != nil {
|
if brc.body != nil {
|
||||||
return brc.body.Close()
|
return brc.body.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
weakrand "math/rand"
|
weakrand "math/rand/v2"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -225,7 +225,7 @@ func (r RandomChoiceSelection) Select(pool UpstreamPool, _ *http.Request, _ http
|
|||||||
if !upstream.Available() {
|
if !upstream.Available() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
j := weakrand.Intn(i + 1) //nolint:gosec
|
j := weakrand.IntN(i + 1) //nolint:gosec
|
||||||
if j < k {
|
if j < k {
|
||||||
choices[j] = upstream
|
choices[j] = upstream
|
||||||
}
|
}
|
||||||
@@ -274,7 +274,7 @@ func (LeastConnSelection) Select(pool UpstreamPool, _ *http.Request, _ http.Resp
|
|||||||
// sample: https://en.wikipedia.org/wiki/Reservoir_sampling
|
// sample: https://en.wikipedia.org/wiki/Reservoir_sampling
|
||||||
if numReqs == leastReqs {
|
if numReqs == leastReqs {
|
||||||
count++
|
count++
|
||||||
if count == 1 || (weakrand.Int()%count) == 0 { //nolint:gosec
|
if count == 1 || weakrand.IntN(count) == 0 { //nolint:gosec
|
||||||
bestHost = host
|
bestHost = host
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -617,7 +617,7 @@ type CookieHashSelection struct {
|
|||||||
// The HTTP cookie name whose value is to be hashed and used for upstream selection.
|
// The HTTP cookie name whose value is to be hashed and used for upstream selection.
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
// Secret to hash (Hmac256) chosen upstream in cookie
|
// Secret to hash (Hmac256) chosen upstream in cookie
|
||||||
Secret string `json:"secret,omitempty"`
|
Secret string `json:"secret,omitempty"` //nolint:gosec // yes it's exported because it needs to encode to JSON
|
||||||
// The cookie's Max-Age before it expires. Default is no expiry.
|
// The cookie's Max-Age before it expires. Default is no expiry.
|
||||||
MaxAge caddy.Duration `json:"max_age,omitempty"`
|
MaxAge caddy.Duration `json:"max_age,omitempty"`
|
||||||
|
|
||||||
@@ -788,7 +788,7 @@ func selectRandomHost(pool []*Upstream) *Upstream {
|
|||||||
// upstream will always be chosen if there is at
|
// upstream will always be chosen if there is at
|
||||||
// least one available
|
// least one available
|
||||||
count++
|
count++
|
||||||
if (weakrand.Int() % count) == 0 { //nolint:gosec
|
if weakrand.IntN(count) == 0 { //nolint:gosec
|
||||||
randomHost = upstream
|
randomHost = upstream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -827,7 +827,7 @@ func leastRequests(upstreams []*Upstream) *Upstream {
|
|||||||
if len(best) == 1 {
|
if len(best) == 1 {
|
||||||
return best[0]
|
return best[0]
|
||||||
}
|
}
|
||||||
return best[weakrand.Intn(len(best))] //nolint:gosec
|
return best[weakrand.IntN(len(best))] //nolint:gosec
|
||||||
}
|
}
|
||||||
|
|
||||||
// hostByHashing returns an available host from pool based on a hashable string s.
|
// hostByHashing returns an available host from pool based on a hashable string s.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
weakrand "math/rand"
|
weakrand "math/rand/v2"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -214,7 +214,10 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
|
|||||||
timeoutc = timer.C
|
timeoutc = timer.C
|
||||||
}
|
}
|
||||||
|
|
||||||
errc := make(chan error, 1)
|
// when a stream timeout is encountered, no error will be read from errc
|
||||||
|
// a buffer size of 2 will allow both the read and write goroutines to send the error and exit
|
||||||
|
// see: https://github.com/caddyserver/caddy/issues/7418
|
||||||
|
errc := make(chan error, 2)
|
||||||
wg.Add(2)
|
wg.Add(2)
|
||||||
go spc.copyToBackend(errc)
|
go spc.copyToBackend(errc)
|
||||||
go spc.copyFromBackend(errc)
|
go spc.copyFromBackend(errc)
|
||||||
@@ -526,7 +529,7 @@ func maskBytes(key [4]byte, pos int, b []byte) int {
|
|||||||
// Create aligned word size key.
|
// Create aligned word size key.
|
||||||
var k [wordSize]byte
|
var k [wordSize]byte
|
||||||
for i := range k {
|
for i := range k {
|
||||||
k[i] = key[(pos+i)&3]
|
k[i] = key[(pos+i)&3] // nolint:gosec // false positive, impossible to be out of bounds; see: https://github.com/securego/gosec/issues/1525
|
||||||
}
|
}
|
||||||
kw := *(*uintptr)(unsafe.Pointer(&k))
|
kw := *(*uintptr)(unsafe.Pointer(&k))
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
weakrand "math/rand"
|
weakrand "math/rand/v2"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -70,6 +70,11 @@ type SRVUpstreams struct {
|
|||||||
// A negative value disables this.
|
// A negative value disables this.
|
||||||
FallbackDelay caddy.Duration `json:"dial_fallback_delay,omitempty"`
|
FallbackDelay caddy.Duration `json:"dial_fallback_delay,omitempty"`
|
||||||
|
|
||||||
|
// Specific network to dial when connecting to the upstream(s)
|
||||||
|
// provided by SRV records upstream. See Go's net package for
|
||||||
|
// accepted values. For example, to restrict to IPv4, use "tcp4".
|
||||||
|
DialNetwork string `json:"dial_network,omitempty"`
|
||||||
|
|
||||||
resolver *net.Resolver
|
resolver *net.Resolver
|
||||||
|
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
@@ -102,7 +107,7 @@ func (su *SRVUpstreams) Provision(ctx caddy.Context) error {
|
|||||||
PreferGo: true,
|
PreferGo: true,
|
||||||
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
addr := su.Resolver.netAddrs[weakrand.Intn(len(su.Resolver.netAddrs))]
|
addr := su.Resolver.netAddrs[weakrand.IntN(len(su.Resolver.netAddrs))]
|
||||||
return d.DialContext(ctx, addr.Network, addr.JoinHostPort(0))
|
return d.DialContext(ctx, addr.Network, addr.JoinHostPort(0))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -177,6 +182,9 @@ func (su SRVUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
addr := net.JoinHostPort(rec.Target, strconv.Itoa(int(rec.Port)))
|
addr := net.JoinHostPort(rec.Target, strconv.Itoa(int(rec.Port)))
|
||||||
|
if su.DialNetwork != "" {
|
||||||
|
addr = su.DialNetwork + "/" + addr
|
||||||
|
}
|
||||||
upstreams[i] = Upstream{Dial: addr}
|
upstreams[i] = Upstream{Dial: addr}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,7 +330,7 @@ func (au *AUpstreams) Provision(ctx caddy.Context) error {
|
|||||||
PreferGo: true,
|
PreferGo: true,
|
||||||
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
addr := au.Resolver.netAddrs[weakrand.Intn(len(au.Resolver.netAddrs))]
|
addr := au.Resolver.netAddrs[weakrand.IntN(len(au.Resolver.netAddrs))]
|
||||||
return d.DialContext(ctx, addr.Network, addr.JoinHostPort(0))
|
return d.DialContext(ctx, addr.Network, addr.JoinHostPort(0))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, err
|
|||||||
if hasArgs {
|
if hasArgs {
|
||||||
return nil, h.Err("Cannot specify uri query rewrites in both argument and block")
|
return nil, h.Err("Cannot specify uri query rewrites in both argument and block")
|
||||||
}
|
}
|
||||||
|
// nolint:prealloc
|
||||||
queryArgs := []string{h.Val()}
|
queryArgs := []string{h.Val()}
|
||||||
queryArgs = append(queryArgs, h.RemainingArgs()...)
|
queryArgs = append(queryArgs, h.RemainingArgs()...)
|
||||||
err := applyQueryOps(h, rewr.Query, queryArgs)
|
err := applyQueryOps(h, rewr.Query, queryArgs)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
@@ -110,14 +111,16 @@ func (r Route) Empty() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r Route) String() string {
|
func (r Route) String() string {
|
||||||
handlersRaw := "["
|
var handlersRaw strings.Builder
|
||||||
|
handlersRaw.WriteByte('[')
|
||||||
for _, hr := range r.HandlersRaw {
|
for _, hr := range r.HandlersRaw {
|
||||||
handlersRaw += " " + string(hr)
|
handlersRaw.WriteByte(' ')
|
||||||
|
handlersRaw.WriteString(string(hr))
|
||||||
}
|
}
|
||||||
handlersRaw += "]"
|
handlersRaw.WriteByte(']')
|
||||||
|
|
||||||
return fmt.Sprintf(`{Group:"%s" MatcherSetsRaw:%s HandlersRaw:%s Terminal:%t}`,
|
return fmt.Sprintf(`{Group:"%s" MatcherSetsRaw:%s HandlersRaw:%s Terminal:%t}`,
|
||||||
r.Group, r.MatcherSetsRaw, handlersRaw, r.Terminal)
|
r.Group, r.MatcherSetsRaw, handlersRaw.String(), r.Terminal)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision sets up both the matchers and handlers in the route.
|
// Provision sets up both the matchers and handlers in the route.
|
||||||
@@ -440,13 +443,15 @@ func (ms *MatcherSets) FromInterface(matcherSets any) error {
|
|||||||
|
|
||||||
// TODO: Is this used?
|
// TODO: Is this used?
|
||||||
func (ms MatcherSets) String() string {
|
func (ms MatcherSets) String() string {
|
||||||
result := "["
|
var result strings.Builder
|
||||||
|
result.WriteByte('[')
|
||||||
for _, matcherSet := range ms {
|
for _, matcherSet := range ms {
|
||||||
for _, matcher := range matcherSet {
|
for _, matcher := range matcherSet {
|
||||||
result += fmt.Sprintf(" %#v", matcher)
|
fmt.Fprintf(&result, " %#v", matcher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result + " ]"
|
result.WriteByte(']')
|
||||||
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
var routeGroupCtxKey = caddy.CtxKey("route_group")
|
var routeGroupCtxKey = caddy.CtxKey("route_group")
|
||||||
|
|||||||
+117
-77
@@ -18,6 +18,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@@ -33,7 +34,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"
|
||||||
|
|
||||||
@@ -55,6 +56,10 @@ type Server struct {
|
|||||||
// of the base listener. They are applied in the given order.
|
// of the base listener. They are applied in the given order.
|
||||||
ListenerWrappersRaw []json.RawMessage `json:"listener_wrappers,omitempty" caddy:"namespace=caddy.listeners inline_key=wrapper"`
|
ListenerWrappersRaw []json.RawMessage `json:"listener_wrappers,omitempty" caddy:"namespace=caddy.listeners inline_key=wrapper"`
|
||||||
|
|
||||||
|
// A list of packet conn wrapper modules, which can modify the behavior
|
||||||
|
// of the base packet conn. They are applied in the given order.
|
||||||
|
PacketConnWrappersRaw []json.RawMessage `json:"packet_conn_wrappers,omitempty" caddy:"namespace=caddy.packetconns inline_key=wrapper"`
|
||||||
|
|
||||||
// How long to allow a read from a client's upload. Setting this
|
// How long to allow a read from a client's upload. Setting this
|
||||||
// to a short, non-zero value can mitigate slowloris attacks, but
|
// to a short, non-zero value can mitigate slowloris attacks, but
|
||||||
// may also affect legitimately slow clients.
|
// may also affect legitimately slow clients.
|
||||||
@@ -248,6 +253,16 @@ type Server struct {
|
|||||||
// A nil value or element indicates that Protocols will be used instead.
|
// A nil value or element indicates that Protocols will be used instead.
|
||||||
ListenProtocols [][]string `json:"listen_protocols,omitempty"`
|
ListenProtocols [][]string `json:"listen_protocols,omitempty"`
|
||||||
|
|
||||||
|
// If set, overrides whether QUIC listeners allow 0-RTT (early data).
|
||||||
|
// If nil, the default behavior is used (currently allowed).
|
||||||
|
//
|
||||||
|
// One reason to disable 0-RTT is if a remote IP matcher is used,
|
||||||
|
// which introduces a dependency on the remote address being verified
|
||||||
|
// if routing happens before the TLS handshake completes. An HTTP 425
|
||||||
|
// response is written in that case, but some clients misbehave and
|
||||||
|
// don't perform a retry, so disabling 0-RTT can smooth it out.
|
||||||
|
Allow0RTT *bool `json:"allow_0rtt,omitempty"`
|
||||||
|
|
||||||
// If set, metrics observations will be enabled.
|
// If set, metrics observations will be enabled.
|
||||||
// This setting is EXPERIMENTAL and subject to change.
|
// This setting is EXPERIMENTAL and subject to change.
|
||||||
// DEPRECATED: Use the app-level `metrics` field.
|
// DEPRECATED: Use the app-level `metrics` field.
|
||||||
@@ -258,7 +273,8 @@ type Server struct {
|
|||||||
primaryHandlerChain Handler
|
primaryHandlerChain Handler
|
||||||
errorHandlerChain Handler
|
errorHandlerChain Handler
|
||||||
listenerWrappers []caddy.ListenerWrapper
|
listenerWrappers []caddy.ListenerWrapper
|
||||||
listeners []net.Listener // stdlib http.Server will close these
|
packetConnWrappers []caddy.PacketConnWrapper
|
||||||
|
listeners []net.Listener
|
||||||
quicListeners []http3.QUICListener // http3 now leave the quic.Listener management to us
|
quicListeners []http3.QUICListener // http3 now leave the quic.Listener management to us
|
||||||
|
|
||||||
tlsApp *caddytls.TLS
|
tlsApp *caddytls.TLS
|
||||||
@@ -285,8 +301,15 @@ 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) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
// 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.
|
||||||
if r.TLS == nil {
|
if r.TLS == nil {
|
||||||
if tlsConnStateFunc, ok := r.Context().Value(tlsConnectionStateFuncCtxKey).(func() *tls.ConnectionState); ok {
|
if tlsConnStateFunc, ok := r.Context().Value(tlsConnectionStateFuncCtxKey).(func() *tls.ConnectionState); ok {
|
||||||
@@ -294,55 +317,37 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Server", "Caddy")
|
|
||||||
|
|
||||||
// advertise HTTP/3, if enabled
|
|
||||||
if s.h3server != nil {
|
|
||||||
if r.ProtoMajor < 3 {
|
|
||||||
err := s.h3server.SetQUICHeaders(w.Header())
|
|
||||||
if err != nil {
|
|
||||||
if c := s.logger.Check(zapcore.ErrorLevel, "setting HTTP/3 Alt-Svc header"); c != nil {
|
|
||||||
c.Write(zap.Error(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// reject very long methods; probably a mistake or an attack
|
|
||||||
if len(r.Method) > 32 {
|
|
||||||
if s.shouldLogRequest(r) {
|
|
||||||
if c := s.accessLogger.Check(zapcore.DebugLevel, "rejecting request with long method"); c != nil {
|
|
||||||
c.Write(
|
|
||||||
zap.String("method_trunc", r.Method[:32]),
|
|
||||||
zap.String("remote_addr", r.RemoteAddr),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
repl := caddy.NewReplacer()
|
|
||||||
r = PrepareRequest(r, repl, w, s)
|
|
||||||
|
|
||||||
// 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clone the request for logging purposes before
|
// set the Server header
|
||||||
// it enters any handler chain; this is necessary
|
h := w.Header()
|
||||||
// to capture the original request in case it gets
|
h["Server"] = serverHeader
|
||||||
// modified during handling
|
|
||||||
// cloning the request and using .WithLazy is considerably faster
|
// advertise HTTP/3, if enabled
|
||||||
// than using .With, which will JSON encode the request immediately
|
if s.h3server != nil && r.ProtoMajor < 3 {
|
||||||
|
if err := s.h3server.SetQUICHeaders(h); err != nil {
|
||||||
|
if c := s.logger.Check(zapcore.ErrorLevel, "setting HTTP/3 Alt-Svc header"); c != nil {
|
||||||
|
c.Write(zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare internals of the request for the handler pipeline
|
||||||
|
repl := caddy.NewReplacer()
|
||||||
|
r = PrepareRequest(r, repl, w, s)
|
||||||
|
|
||||||
|
// clone the request for logging purposes before it enters any handler chain;
|
||||||
|
// this is necessary to capture the original request in case it gets modified
|
||||||
|
// during handling (cloning the request and using .WithLazy is considerably
|
||||||
|
// faster than using .With, which will JSON-encode the request immediately)
|
||||||
shouldLogCredentials := s.Logs != nil && s.Logs.ShouldLogCredentials
|
shouldLogCredentials := s.Logs != nil && s.Logs.ShouldLogCredentials
|
||||||
loggableReq := zap.Object("request", LoggableHTTPRequest{
|
loggableReq := zap.Object("request", LoggableHTTPRequest{
|
||||||
Request: r.Clone(r.Context()),
|
Request: r.Clone(r.Context()),
|
||||||
@@ -370,36 +375,33 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// capture the original version of the request
|
// capture the original version of the request
|
||||||
accLog := s.accessLogger.With(loggableReq)
|
accLog := s.accessLogger.WithLazy(loggableReq)
|
||||||
|
|
||||||
defer s.logRequest(accLog, r, wrec, &duration, repl, bodyReader, shouldLogCredentials)
|
defer s.logRequest(accLog, r, wrec, &duration, repl, bodyReader, shouldLogCredentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
start := time.Now()
|
// guarantee ACME HTTP challenges; handle them separately from any user-defined handlers
|
||||||
|
|
||||||
// guarantee ACME HTTP challenges; handle them
|
|
||||||
// separately from any user-defined handlers
|
|
||||||
if s.tlsApp.HandleHTTPChallenge(w, r) {
|
if s.tlsApp.HandleHTTPChallenge(w, r) {
|
||||||
duration = time.Since(start)
|
duration = time.Since(start)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// execute the primary handler chain
|
err := s.serveHTTP(w, r)
|
||||||
err := s.primaryHandlerChain.ServeHTTP(w, r)
|
|
||||||
duration = time.Since(start)
|
duration = time.Since(start)
|
||||||
|
|
||||||
// if no errors, we're done!
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// restore original request before invoking error handler chain (issue #3717)
|
// restore original request before invoking error handler chain (issue #3717)
|
||||||
// TODO: this does not restore original headers, if modified (for efficiency)
|
// NOTE: this does not restore original headers if modified (for efficiency)
|
||||||
origReq := r.Context().Value(OriginalRequestCtxKey).(http.Request)
|
origReq, ok := r.Context().Value(OriginalRequestCtxKey).(http.Request)
|
||||||
r.Method = origReq.Method
|
if ok {
|
||||||
r.RemoteAddr = origReq.RemoteAddr
|
r.Method = origReq.Method
|
||||||
r.RequestURI = origReq.RequestURI
|
r.RemoteAddr = origReq.RemoteAddr
|
||||||
cloneURL(origReq.URL, r.URL)
|
r.RequestURI = origReq.RequestURI
|
||||||
|
cloneURL(origReq.URL, r.URL)
|
||||||
|
}
|
||||||
|
|
||||||
// prepare the error log
|
// prepare the error log
|
||||||
errLog = errLog.With(zap.Duration("duration", duration))
|
errLog = errLog.With(zap.Duration("duration", duration))
|
||||||
@@ -417,10 +419,8 @@ 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 successfully, so now just log the error
|
||||||
// user's error route handled the error response
|
|
||||||
// successfully, so now just log the error
|
|
||||||
for _, logger := range errLoggers {
|
for _, logger := range errLoggers {
|
||||||
if c := logger.Check(zapcore.DebugLevel, errMsg); c != nil {
|
if c := logger.Check(zapcore.DebugLevel, errMsg); c != nil {
|
||||||
if fields == nil {
|
if fields == nil {
|
||||||
@@ -468,6 +468,35 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
// reject very long methods; probably a mistake or an attack
|
||||||
|
if len(r.Method) > 32 {
|
||||||
|
if s.shouldLogRequest(r) {
|
||||||
|
if c := s.accessLogger.Check(zapcore.DebugLevel, "rejecting request with long method"); c != nil {
|
||||||
|
c.Write(
|
||||||
|
zap.String("method_trunc", r.Method[:32]),
|
||||||
|
zap.String("remote_addr", r.RemoteAddr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return HandlerError{StatusCode: http.StatusMethodNotAllowed}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFC 9112 section 3.2: "A server MUST respond with a 400 (Bad Request) status
|
||||||
|
// code to any HTTP/1.1 request message that lacks a Host header field and to any
|
||||||
|
// request message that contains more than one Host header field line or a Host
|
||||||
|
// header field with an invalid field value."
|
||||||
|
if r.Host == "" {
|
||||||
|
return HandlerError{
|
||||||
|
Err: errors.New("rfc9112 forbids empty Host"),
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute the primary handler chain
|
||||||
|
return s.primaryHandlerChain.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
// wrapPrimaryRoute wraps stack (a compiled middleware handler chain)
|
// wrapPrimaryRoute wraps stack (a compiled middleware handler chain)
|
||||||
// in s.enforcementHandler which performs crucial security checks, etc.
|
// in s.enforcementHandler which performs crucial security checks, etc.
|
||||||
func (s *Server) wrapPrimaryRoute(stack Handler) Handler {
|
func (s *Server) wrapPrimaryRoute(stack Handler) Handler {
|
||||||
@@ -551,15 +580,21 @@ func (s *Server) hasListenerAddress(fullAddr string) bool {
|
|||||||
// The second issue seems very similar to a discussion here:
|
// The second issue seems very similar to a discussion here:
|
||||||
// https://github.com/nodejs/node/issues/9390
|
// https://github.com/nodejs/node/issues/9390
|
||||||
//
|
//
|
||||||
// This is very easy to reproduce by creating an HTTP server
|
// However, binding to *different specific* interfaces
|
||||||
// that listens to both addresses or just one with a host
|
// (e.g. 127.0.0.2:80 and 127.0.0.3:80) IS allowed on Linux.
|
||||||
// interface; or for a more confusing reproduction, try
|
// The conflict only happens when mixing specific IPs with
|
||||||
// listening on "127.0.0.1:80" and ":443" and you'll see
|
// wildcards (0.0.0.0 or ::).
|
||||||
// the error, if you take away the GOOS condition below.
|
|
||||||
//
|
// Hosts match exactly (e.g. 127.0.0.2 == 127.0.0.2) -> Conflict.
|
||||||
// So, an address is equivalent if the port is in the port
|
hostMatch := thisAddrs.Host == laddrs.Host
|
||||||
// range, and if not on Linux, the host is the same... sigh.
|
|
||||||
if (runtime.GOOS == "linux" || thisAddrs.Host == laddrs.Host) &&
|
// On Linux, specific IP vs Wildcard fails to bind.
|
||||||
|
// So if we are on Linux AND either host is empty (wildcard), we treat
|
||||||
|
// it as a match (conflict). But if both are specific and different
|
||||||
|
// (127.0.0.2 vs 127.0.0.3), this remains false (no conflict).
|
||||||
|
linuxWildcardConflict := runtime.GOOS == "linux" && (thisAddrs.Host == "" || laddrs.Host == "")
|
||||||
|
|
||||||
|
if (hostMatch || linuxWildcardConflict) &&
|
||||||
(laddrs.StartPort <= thisAddrs.EndPort) &&
|
(laddrs.StartPort <= thisAddrs.EndPort) &&
|
||||||
(laddrs.StartPort >= thisAddrs.StartPort) {
|
(laddrs.StartPort >= thisAddrs.StartPort) {
|
||||||
return true
|
return true
|
||||||
@@ -625,7 +660,7 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error
|
|||||||
return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err)
|
return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err)
|
||||||
}
|
}
|
||||||
addr.Network = h3net
|
addr.Network = h3net
|
||||||
h3ln, err := addr.ListenQUIC(s.ctx, 0, net.ListenConfig{}, tlsCfg)
|
h3ln, err := addr.ListenQUIC(s.ctx, 0, net.ListenConfig{}, tlsCfg, s.packetConnWrappers, s.Allow0RTT)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err)
|
return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err)
|
||||||
}
|
}
|
||||||
@@ -638,7 +673,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),
|
||||||
}
|
}
|
||||||
@@ -763,9 +798,11 @@ func (s *Server) shouldLogRequest(r *http.Request) bool {
|
|||||||
hostWithoutPort = r.Host
|
hostWithoutPort = r.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := s.Logs.LoggerNames[hostWithoutPort]; ok {
|
for loggerName := range s.Logs.LoggerNames {
|
||||||
// this host is mapped to a particular logger name
|
if certmagic.MatchWildcard(hostWithoutPort, loggerName) {
|
||||||
return true
|
// this host is mapped to a particular logger name
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _, dh := range s.Logs.SkipHosts {
|
for _, dh := range s.Logs.SkipHosts {
|
||||||
// logging for this particular host is disabled
|
// logging for this particular host is disabled
|
||||||
@@ -793,8 +830,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -812,7 +851,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"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -836,7 +875,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))
|
||||||
@@ -1001,6 +1040,7 @@ func isTrustedClientIP(ipAddr netip.Addr, trusted []netip.Prefix) bool {
|
|||||||
// then the first value from those headers is used.
|
// then the first value from those headers is used.
|
||||||
func trustedRealClientIP(r *http.Request, headers []string, clientIP string) string {
|
func trustedRealClientIP(r *http.Request, headers []string, clientIP string) string {
|
||||||
// Read all the values of the configured client IP headers, in order
|
// Read all the values of the configured client IP headers, in order
|
||||||
|
// nolint:prealloc
|
||||||
var values []string
|
var values []string
|
||||||
for _, field := range headers {
|
for _, field := range headers {
|
||||||
values = append(values, r.Header.Values(field)...)
|
values = append(values, r.Header.Values(field)...)
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, next H
|
|||||||
|
|
||||||
// write response body
|
// write response body
|
||||||
if statusCode != http.StatusEarlyHints && body != "" {
|
if statusCode != http.StatusEarlyHints && body != "" {
|
||||||
fmt.Fprint(w, body)
|
fmt.Fprint(w, body) //nolint:gosec // no XSS unless you sabatoge your own config
|
||||||
}
|
}
|
||||||
|
|
||||||
// continue handling after Early Hints as they are not the final response
|
// continue handling after Early Hints as they are not the final response
|
||||||
@@ -257,7 +257,16 @@ func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, next H
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildHTTPServer(i int, port uint, addr string, statusCode int, hdr http.Header, body string, accessLog bool) (*Server, error) {
|
func buildHTTPServer(
|
||||||
|
i int,
|
||||||
|
port uint,
|
||||||
|
addr string,
|
||||||
|
statusCode int,
|
||||||
|
hdr http.Header,
|
||||||
|
body string,
|
||||||
|
accessLog bool,
|
||||||
|
) (*Server, error) {
|
||||||
|
// nolint:prealloc
|
||||||
var handlers []json.RawMessage
|
var handlers []json.RawMessage
|
||||||
|
|
||||||
// response body supports a basic template; evaluate it
|
// response body supports a basic template; evaluate it
|
||||||
|
|||||||
@@ -306,6 +306,13 @@ func init() {
|
|||||||
// find the documentation on time layouts [in Go's docs](https://pkg.go.dev/time#pkg-constants).
|
// find the documentation on time layouts [in Go's docs](https://pkg.go.dev/time#pkg-constants).
|
||||||
// The default time layout is `RFC1123Z`, i.e. `Mon, 02 Jan 2006 15:04:05 -0700`.
|
// The default time layout is `RFC1123Z`, i.e. `Mon, 02 Jan 2006 15:04:05 -0700`.
|
||||||
//
|
//
|
||||||
|
// ```
|
||||||
|
// {{humanize "size" "2048000"}}
|
||||||
|
// {{placeholder "http.response.header.Content-Length" | humanize "size"}}
|
||||||
|
// {{humanize "time" "Fri, 05 May 2022 15:04:05 +0200"}}
|
||||||
|
// {{humanize "time:2006-Jan-02" "2022-May-05"}}
|
||||||
|
// ```
|
||||||
|
//
|
||||||
// ##### `pathEscape`
|
// ##### `pathEscape`
|
||||||
//
|
//
|
||||||
// Passes a string through `url.PathEscape`, replacing characters that have
|
// Passes a string through `url.PathEscape`, replacing characters that have
|
||||||
@@ -318,11 +325,22 @@ func init() {
|
|||||||
// {{pathEscape "50%_valid_filename?.jpg"}}
|
// {{pathEscape "50%_valid_filename?.jpg"}}
|
||||||
// ```
|
// ```
|
||||||
//
|
//
|
||||||
|
// ##### `maybe`
|
||||||
|
//
|
||||||
|
// Invokes a custom template function only if it is registered (plugged-in)
|
||||||
|
// in the `http.handlers.templates.functions.*` namespace.
|
||||||
|
//
|
||||||
|
// The first argument is the function name, and any subsequent arguments
|
||||||
|
// are forwarded to that function. If the named function is not available,
|
||||||
|
// the invocation is ignored and a log message is emitted.
|
||||||
|
//
|
||||||
|
// This is useful for templates that optionally use components which may
|
||||||
|
// not be present in every build or environment.
|
||||||
|
//
|
||||||
|
// NOTE: This function is EXPERIMENTAL and subject to change or removal.
|
||||||
|
//
|
||||||
// ```
|
// ```
|
||||||
// {{humanize "size" "2048000"}}
|
// {{ maybe "myOptionalFunc" "arg1" 2 }}
|
||||||
// {{placeholder "http.response.header.Content-Length" | humanize "size"}}
|
|
||||||
// {{humanize "time" "Fri, 05 May 2022 15:04:05 +0200"}}
|
|
||||||
// {{humanize "time:2006-Jan-02" "2022-May-05"}}
|
|
||||||
// ```
|
// ```
|
||||||
type Templates struct {
|
type Templates struct {
|
||||||
// The root path from which to load files. Required if template functions
|
// The root path from which to load files. Required if template functions
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ type Tracing struct {
|
|||||||
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#span
|
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#span
|
||||||
SpanName string `json:"span"`
|
SpanName string `json:"span"`
|
||||||
|
|
||||||
|
// SpanAttributes are custom key-value pairs to be added to spans
|
||||||
|
SpanAttributes map[string]string `json:"span_attributes,omitempty"`
|
||||||
|
|
||||||
// otel implements opentelemetry related logic.
|
// otel implements opentelemetry related logic.
|
||||||
otel openTelemetryWrapper
|
otel openTelemetryWrapper
|
||||||
|
|
||||||
@@ -46,7 +49,7 @@ func (ot *Tracing) Provision(ctx caddy.Context) error {
|
|||||||
ot.logger = ctx.Logger()
|
ot.logger = ctx.Logger()
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
ot.otel, err = newOpenTelemetryWrapper(ctx, ot.SpanName)
|
ot.otel, err = newOpenTelemetryWrapper(ctx, ot.SpanName, ot.SpanAttributes)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -69,6 +72,10 @@ func (ot *Tracing) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
|
|||||||
//
|
//
|
||||||
// tracing {
|
// tracing {
|
||||||
// [span <span_name>]
|
// [span <span_name>]
|
||||||
|
// [span_attributes {
|
||||||
|
// attr1 value1
|
||||||
|
// attr2 value2
|
||||||
|
// }]
|
||||||
// }
|
// }
|
||||||
func (ot *Tracing) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
func (ot *Tracing) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
setParameter := func(d *caddyfile.Dispenser, val *string) error {
|
setParameter := func(d *caddyfile.Dispenser, val *string) error {
|
||||||
@@ -94,12 +101,30 @@ func (ot *Tracing) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for d.NextBlock(0) {
|
for d.NextBlock(0) {
|
||||||
if dst, ok := paramsMap[d.Val()]; ok {
|
switch d.Val() {
|
||||||
if err := setParameter(d, dst); err != nil {
|
case "span_attributes":
|
||||||
return err
|
if ot.SpanAttributes == nil {
|
||||||
|
ot.SpanAttributes = make(map[string]string)
|
||||||
|
}
|
||||||
|
for d.NextBlock(1) {
|
||||||
|
key := d.Val()
|
||||||
|
if !d.NextArg() {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
value := d.Val()
|
||||||
|
if d.NextArg() {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
ot.SpanAttributes[key] = value
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if dst, ok := paramsMap[d.Val()]; ok {
|
||||||
|
if err := setParameter(d, dst); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return d.ArgErr()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return d.ArgErr()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ package tracing
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
@@ -15,17 +19,26 @@ import (
|
|||||||
|
|
||||||
func TestTracing_UnmarshalCaddyfile(t *testing.T) {
|
func TestTracing_UnmarshalCaddyfile(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
spanName string
|
spanName string
|
||||||
d *caddyfile.Dispenser
|
spanAttributes map[string]string
|
||||||
wantErr bool
|
d *caddyfile.Dispenser
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Full config",
|
name: "Full config",
|
||||||
spanName: "my-span",
|
spanName: "my-span",
|
||||||
|
spanAttributes: map[string]string{
|
||||||
|
"attr1": "value1",
|
||||||
|
"attr2": "value2",
|
||||||
|
},
|
||||||
d: caddyfile.NewTestDispenser(`
|
d: caddyfile.NewTestDispenser(`
|
||||||
tracing {
|
tracing {
|
||||||
span my-span
|
span my-span
|
||||||
|
span_attributes {
|
||||||
|
attr1 value1
|
||||||
|
attr2 value2
|
||||||
|
}
|
||||||
}`),
|
}`),
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
@@ -42,6 +55,21 @@ tracing {
|
|||||||
name: "Empty config",
|
name: "Empty config",
|
||||||
d: caddyfile.NewTestDispenser(`
|
d: caddyfile.NewTestDispenser(`
|
||||||
tracing {
|
tracing {
|
||||||
|
}`),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Only span attributes",
|
||||||
|
spanAttributes: map[string]string{
|
||||||
|
"service.name": "my-service",
|
||||||
|
"service.version": "1.0.0",
|
||||||
|
},
|
||||||
|
d: caddyfile.NewTestDispenser(`
|
||||||
|
tracing {
|
||||||
|
span_attributes {
|
||||||
|
service.name my-service
|
||||||
|
service.version 1.0.0
|
||||||
|
}
|
||||||
}`),
|
}`),
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
@@ -56,6 +84,20 @@ tracing {
|
|||||||
if ot.SpanName != tt.spanName {
|
if ot.SpanName != tt.spanName {
|
||||||
t.Errorf("UnmarshalCaddyfile() SpanName = %v, want SpanName %v", ot.SpanName, tt.spanName)
|
t.Errorf("UnmarshalCaddyfile() SpanName = %v, want SpanName %v", ot.SpanName, tt.spanName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(tt.spanAttributes) > 0 {
|
||||||
|
if ot.SpanAttributes == nil {
|
||||||
|
t.Errorf("UnmarshalCaddyfile() SpanAttributes is nil, expected %v", tt.spanAttributes)
|
||||||
|
} else {
|
||||||
|
for key, expectedValue := range tt.spanAttributes {
|
||||||
|
if actualValue, exists := ot.SpanAttributes[key]; !exists {
|
||||||
|
t.Errorf("UnmarshalCaddyfile() SpanAttributes missing key %v", key)
|
||||||
|
} else if actualValue != expectedValue {
|
||||||
|
t.Errorf("UnmarshalCaddyfile() SpanAttributes[%v] = %v, want %v", key, actualValue, expectedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,6 +121,26 @@ func TestTracing_UnmarshalCaddyfile_Error(t *testing.T) {
|
|||||||
d: caddyfile.NewTestDispenser(`
|
d: caddyfile.NewTestDispenser(`
|
||||||
tracing {
|
tracing {
|
||||||
span
|
span
|
||||||
|
}`),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Span attributes missing value",
|
||||||
|
d: caddyfile.NewTestDispenser(`
|
||||||
|
tracing {
|
||||||
|
span_attributes {
|
||||||
|
key
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Span attributes too many arguments",
|
||||||
|
d: caddyfile.NewTestDispenser(`
|
||||||
|
tracing {
|
||||||
|
span_attributes {
|
||||||
|
key value extra
|
||||||
|
}
|
||||||
}`),
|
}`),
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
@@ -181,6 +243,160 @@ func TestTracing_ServeHTTP_Next_Error(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTracing_JSON_Configuration(t *testing.T) {
|
||||||
|
// Test that our struct correctly marshals to and from JSON
|
||||||
|
original := &Tracing{
|
||||||
|
SpanName: "test-span",
|
||||||
|
SpanAttributes: map[string]string{
|
||||||
|
"service.name": "test-service",
|
||||||
|
"service.version": "1.0.0",
|
||||||
|
"env": "test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(original)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to marshal to JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var unmarshaled Tracing
|
||||||
|
if err := json.Unmarshal(jsonData, &unmarshaled); err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal from JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if unmarshaled.SpanName != original.SpanName {
|
||||||
|
t.Errorf("Expected SpanName %s, got %s", original.SpanName, unmarshaled.SpanName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(unmarshaled.SpanAttributes) != len(original.SpanAttributes) {
|
||||||
|
t.Errorf("Expected %d span attributes, got %d", len(original.SpanAttributes), len(unmarshaled.SpanAttributes))
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, expectedValue := range original.SpanAttributes {
|
||||||
|
if actualValue, exists := unmarshaled.SpanAttributes[key]; !exists {
|
||||||
|
t.Errorf("Expected span attribute %s to exist", key)
|
||||||
|
} else if actualValue != expectedValue {
|
||||||
|
t.Errorf("Expected span attribute %s = %s, got %s", key, expectedValue, actualValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("JSON representation: %s", string(jsonData))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTracing_OpenTelemetry_Span_Attributes(t *testing.T) {
|
||||||
|
// Create an in-memory span recorder to capture actual span data
|
||||||
|
spanRecorder := tracetest.NewSpanRecorder()
|
||||||
|
provider := trace.NewTracerProvider(
|
||||||
|
trace.WithSpanProcessor(spanRecorder),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create our tracing module with span attributes that include placeholders
|
||||||
|
ot := &Tracing{
|
||||||
|
SpanName: "test-span",
|
||||||
|
SpanAttributes: map[string]string{
|
||||||
|
"static": "test-service",
|
||||||
|
"request-placeholder": "{http.request.method}",
|
||||||
|
"response-placeholder": "{http.response.header.X-Some-Header}",
|
||||||
|
"mixed": "prefix-{http.request.method}-{http.response.header.X-Some-Header}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a specific request to test against
|
||||||
|
req, _ := http.NewRequest("POST", "https://api.example.com/v1/users?id=123", nil)
|
||||||
|
req.Host = "api.example.com"
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Set up the replacer
|
||||||
|
repl := caddy.NewReplacer()
|
||||||
|
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
|
||||||
|
ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, make(map[string]any))
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
// Set up request placeholders
|
||||||
|
repl.Set("http.request.method", req.Method)
|
||||||
|
repl.Set("http.request.uri", req.URL.RequestURI())
|
||||||
|
|
||||||
|
// Handler to generate the response
|
||||||
|
var handler caddyhttp.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) error {
|
||||||
|
writer.Header().Set("X-Some-Header", "some-value")
|
||||||
|
writer.WriteHeader(200)
|
||||||
|
|
||||||
|
// Make response headers available to replacer
|
||||||
|
repl.Set("http.response.header.X-Some-Header", writer.Header().Get("X-Some-Header"))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up Caddy context
|
||||||
|
caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Override the global tracer provider with our test provider
|
||||||
|
// This is a bit hacky but necessary to capture the actual spans
|
||||||
|
originalProvider := globalTracerProvider
|
||||||
|
globalTracerProvider = &tracerProvider{
|
||||||
|
tracerProvider: provider,
|
||||||
|
tracerProvidersCounter: 1, // Simulate one user
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
globalTracerProvider = originalProvider
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Provision the tracing module
|
||||||
|
if err := ot.Provision(caddyCtx); err != nil {
|
||||||
|
t.Errorf("Provision error: %v", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the request
|
||||||
|
if err := ot.ServeHTTP(w, req, handler); err != nil {
|
||||||
|
t.Errorf("ServeHTTP error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the recorded spans
|
||||||
|
spans := spanRecorder.Ended()
|
||||||
|
if len(spans) == 0 {
|
||||||
|
t.Fatal("Expected at least one span to be recorded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find our span (should be the one with our test span name)
|
||||||
|
var testSpan trace.ReadOnlySpan
|
||||||
|
for _, span := range spans {
|
||||||
|
if span.Name() == "test-span" {
|
||||||
|
testSpan = span
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if testSpan == nil {
|
||||||
|
t.Fatal("Could not find test span in recorded spans")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the span attributes were set correctly with placeholder replacement
|
||||||
|
expectedAttributes := map[string]string{
|
||||||
|
"static": "test-service",
|
||||||
|
"request-placeholder": "POST",
|
||||||
|
"response-placeholder": "some-value",
|
||||||
|
"mixed": "prefix-POST-some-value",
|
||||||
|
}
|
||||||
|
|
||||||
|
actualAttributes := make(map[string]string)
|
||||||
|
for _, attr := range testSpan.Attributes() {
|
||||||
|
actualAttributes[string(attr.Key)] = attr.Value.AsString()
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, expectedValue := range expectedAttributes {
|
||||||
|
if actualValue, exists := actualAttributes[key]; !exists {
|
||||||
|
t.Errorf("Expected span attribute %s to be set", key)
|
||||||
|
} else if actualValue != expectedValue {
|
||||||
|
t.Errorf("Expected span attribute %s = %s, got %s", key, expectedValue, actualValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Recorded span attributes: %+v", actualAttributes)
|
||||||
|
}
|
||||||
|
|
||||||
func createRequestWithContext(method string, url string) *http.Request {
|
func createRequestWithContext(method string, url string) *http.Request {
|
||||||
r, _ := http.NewRequest(method, url, nil)
|
r, _ := http.NewRequest(method, url, nil)
|
||||||
repl := caddy.NewReplacer()
|
repl := caddy.NewReplacer()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"go.opentelemetry.io/contrib/exporters/autoexport"
|
"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/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"
|
||||||
@@ -37,20 +38,23 @@ type openTelemetryWrapper struct {
|
|||||||
|
|
||||||
handler http.Handler
|
handler http.Handler
|
||||||
|
|
||||||
spanName string
|
spanName string
|
||||||
|
spanAttributes map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// newOpenTelemetryWrapper is responsible for the openTelemetryWrapper initialization using provided configuration.
|
// newOpenTelemetryWrapper is responsible for the openTelemetryWrapper initialization using provided configuration.
|
||||||
func newOpenTelemetryWrapper(
|
func newOpenTelemetryWrapper(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
spanName string,
|
spanName string,
|
||||||
|
spanAttributes map[string]string,
|
||||||
) (openTelemetryWrapper, error) {
|
) (openTelemetryWrapper, error) {
|
||||||
if spanName == "" {
|
if spanName == "" {
|
||||||
spanName = defaultSpanName
|
spanName = defaultSpanName
|
||||||
}
|
}
|
||||||
|
|
||||||
ot := openTelemetryWrapper{
|
ot := openTelemetryWrapper{
|
||||||
spanName: spanName,
|
spanName: spanName,
|
||||||
|
spanAttributes: spanAttributes,
|
||||||
}
|
}
|
||||||
|
|
||||||
version, _ := caddy.Version()
|
version, _ := caddy.Version()
|
||||||
@@ -99,8 +103,22 @@ func (ot *openTelemetryWrapper) serveHTTP(w http.ResponseWriter, r *http.Request
|
|||||||
extra.Add(zap.String("spanID", spanID))
|
extra.Add(zap.String("spanID", spanID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
next := ctx.Value(nextCallCtxKey).(*nextCall)
|
next := ctx.Value(nextCallCtxKey).(*nextCall)
|
||||||
next.err = next.next.ServeHTTP(w, r)
|
next.err = next.next.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
// Add custom span attributes to the current span
|
||||||
|
span := trace.SpanFromContext(ctx)
|
||||||
|
if span.IsRecording() && len(ot.spanAttributes) > 0 {
|
||||||
|
replacer := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||||
|
attributes := make([]attribute.KeyValue, 0, len(ot.spanAttributes))
|
||||||
|
for key, value := range ot.spanAttributes {
|
||||||
|
// Allow placeholder replacement in attribute values
|
||||||
|
replacedValue := replacer.ReplaceAll(value, "")
|
||||||
|
attributes = append(attributes, attribute.String(key, replacedValue))
|
||||||
|
}
|
||||||
|
span.SetAttributes(attributes...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP propagates call to the by wrapped by `otelhttp` next handler.
|
// ServeHTTP propagates call to the by wrapped by `otelhttp` next handler.
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ func TestOpenTelemetryWrapper_newOpenTelemetryWrapper(t *testing.T) {
|
|||||||
|
|
||||||
if otw, err = newOpenTelemetryWrapper(ctx,
|
if otw, err = newOpenTelemetryWrapper(ctx,
|
||||||
"",
|
"",
|
||||||
|
nil,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
t.Errorf("newOpenTelemetryWrapper() error = %v", err)
|
t.Errorf("newOpenTelemetryWrapper() error = %v", err)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ package acmeserver
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
weakrand "math/rand"
|
weakrand "math/rand/v2"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -140,6 +140,8 @@ func (ash *Handler) Provision(ctx caddy.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ash.warnIfPolicyAllowsAll()
|
||||||
|
|
||||||
// get a reference to the configured CA
|
// get a reference to the configured CA
|
||||||
appModule, err := ctx.App("pki")
|
appModule, err := ctx.App("pki")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -214,6 +216,21 @@ func (ash *Handler) Provision(ctx caddy.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ash *Handler) warnIfPolicyAllowsAll() {
|
||||||
|
allow := ash.Policy.normalizeAllowRules()
|
||||||
|
deny := ash.Policy.normalizeDenyRules()
|
||||||
|
if allow != nil || deny != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allowWildcardNames := ash.Policy != nil && ash.Policy.AllowWildcardNames
|
||||||
|
ash.logger.Warn(
|
||||||
|
"acme_server policy has no allow/deny rules; order identifiers are unrestricted (allow-all)",
|
||||||
|
zap.String("ca", ash.CA),
|
||||||
|
zap.Bool("allow_wildcard_names", allowWildcardNames),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func (ash Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
func (ash Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||||
if strings.HasPrefix(r.URL.Path, ash.PathPrefix) {
|
if strings.HasPrefix(r.URL.Path, ash.PathPrefix) {
|
||||||
acmeCtx := acme.NewContext(
|
acmeCtx := acme.NewContext(
|
||||||
@@ -307,7 +324,7 @@ func (ash Handler) makeClient() (acme.Client, error) {
|
|||||||
PreferGo: true,
|
PreferGo: true,
|
||||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
addr := ash.resolvers[weakrand.Intn(len(ash.resolvers))]
|
addr := ash.resolvers[weakrand.IntN(len(ash.resolvers))]
|
||||||
return dialer.DialContext(ctx, addr.Network, addr.JoinHostPort(0))
|
return dialer.DialContext(ctx, addr.Network, addr.JoinHostPort(0))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package acmeserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zaptest/observer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandler_warnIfPolicyAllowsAll(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
policy *Policy
|
||||||
|
wantWarns int
|
||||||
|
wantAllowWildcard bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "warns when policy is nil",
|
||||||
|
policy: nil,
|
||||||
|
wantWarns: 1,
|
||||||
|
wantAllowWildcard: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "warns when allow/deny rules are empty",
|
||||||
|
policy: &Policy{},
|
||||||
|
wantWarns: 1,
|
||||||
|
wantAllowWildcard: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "warns when only allow_wildcard_names is true",
|
||||||
|
policy: &Policy{
|
||||||
|
AllowWildcardNames: true,
|
||||||
|
},
|
||||||
|
wantWarns: 1,
|
||||||
|
wantAllowWildcard: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not warn when allow rules are configured",
|
||||||
|
policy: &Policy{
|
||||||
|
Allow: &RuleSet{
|
||||||
|
Domains: []string{"example.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantWarns: 0,
|
||||||
|
wantAllowWildcard: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not warn when deny rules are configured",
|
||||||
|
policy: &Policy{
|
||||||
|
Deny: &RuleSet{
|
||||||
|
Domains: []string{"bad.example.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantWarns: 0,
|
||||||
|
wantAllowWildcard: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
core, logs := observer.New(zap.WarnLevel)
|
||||||
|
ash := &Handler{
|
||||||
|
CA: "local",
|
||||||
|
Policy: tt.policy,
|
||||||
|
logger: zap.New(core),
|
||||||
|
}
|
||||||
|
|
||||||
|
ash.warnIfPolicyAllowsAll()
|
||||||
|
if logs.Len() != tt.wantWarns {
|
||||||
|
t.Fatalf("expected %d warning logs, got %d", tt.wantWarns, logs.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantWarns == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := logs.All()[0]
|
||||||
|
if entry.Level != zap.WarnLevel {
|
||||||
|
t.Fatalf("expected warn level, got %v", entry.Level)
|
||||||
|
}
|
||||||
|
if !strings.Contains(entry.Message, "policy has no allow/deny rules") {
|
||||||
|
t.Fatalf("unexpected log message: %q", entry.Message)
|
||||||
|
}
|
||||||
|
ctx := entry.ContextMap()
|
||||||
|
if ctx["ca"] != "local" {
|
||||||
|
t.Fatalf("expected ca=local, got %v", ctx["ca"])
|
||||||
|
}
|
||||||
|
if ctx["allow_wildcard_names"] != tt.wantAllowWildcard {
|
||||||
|
t.Fatalf("expected allow_wildcard_names=%v, got %v", tt.wantAllowWildcard, ctx["allow_wildcard_names"])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -163,9 +163,9 @@ func (a *adminAPI) handleCACerts(w http.ResponseWriter, r *http.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/pem-certificate-chain")
|
w.Header().Set("Content-Type", "application/pem-certificate-chain")
|
||||||
_, err = w.Write(interCert)
|
_, err = w.Write(interCert) //nolint:gosec // false positive... no XSS in a PEM for cryin' out loud
|
||||||
if err == nil {
|
if err == nil {
|
||||||
_, _ = w.Write(rootCert)
|
_, _ = w.Write(rootCert) //nolint:gosec // false positive... no XSS in a PEM for cryin' out loud
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -222,11 +222,16 @@ func rootAndIntermediatePEM(ca *CA) (root, inter []byte, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return root, inter, err
|
return root, inter, err
|
||||||
}
|
}
|
||||||
inter, err = pemEncodeCert(ca.IntermediateCertificate().Raw)
|
|
||||||
if err != nil {
|
for _, interCert := range ca.IntermediateCertificateChain() {
|
||||||
return root, inter, err
|
pemBytes, err := pemEncodeCert(interCert.Raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
inter = append(inter, pemBytes...)
|
||||||
}
|
}
|
||||||
return root, inter, err
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// caInfo is the response structure for the CA info API endpoint.
|
// caInfo is the response structure for the CA info API endpoint.
|
||||||
|
|||||||
+60
-30
@@ -63,6 +63,15 @@ type CA struct {
|
|||||||
// The intermediate (signing) certificate; if null, one will be generated.
|
// The intermediate (signing) certificate; if null, one will be generated.
|
||||||
Intermediate *KeyPair `json:"intermediate,omitempty"`
|
Intermediate *KeyPair `json:"intermediate,omitempty"`
|
||||||
|
|
||||||
|
// How often to check if intermediate (and root, when applicable) certificates need renewal.
|
||||||
|
// Default: 10m.
|
||||||
|
MaintenanceInterval caddy.Duration `json:"maintenance_interval,omitempty"`
|
||||||
|
|
||||||
|
// The fraction of certificate lifetime (0.0–1.0) after which renewal is attempted.
|
||||||
|
// For example, 0.2 means renew when 20% of the lifetime remains (e.g. ~73 days for a 1-year cert).
|
||||||
|
// Default: 0.2.
|
||||||
|
RenewalWindowRatio float64 `json:"renewal_window_ratio,omitempty"`
|
||||||
|
|
||||||
// Optionally configure a separate storage module associated with this
|
// Optionally configure a separate storage module associated with this
|
||||||
// issuer, instead of using Caddy's global/default-configured storage.
|
// issuer, instead of using Caddy's global/default-configured storage.
|
||||||
// This can be useful if you want to keep your signing keys in a
|
// This can be useful if you want to keep your signing keys in a
|
||||||
@@ -75,10 +84,11 @@ type CA struct {
|
|||||||
// and module provisioning.
|
// and module provisioning.
|
||||||
ID string `json:"-"`
|
ID string `json:"-"`
|
||||||
|
|
||||||
storage certmagic.Storage
|
storage certmagic.Storage
|
||||||
root, inter *x509.Certificate
|
root *x509.Certificate
|
||||||
interKey any // TODO: should we just store these as crypto.Signer?
|
interChain []*x509.Certificate
|
||||||
mu *sync.RWMutex
|
interKey crypto.Signer
|
||||||
|
mu *sync.RWMutex
|
||||||
|
|
||||||
rootCertPath string // mainly used for logging purposes if trusting
|
rootCertPath string // mainly used for logging purposes if trusting
|
||||||
log *zap.Logger
|
log *zap.Logger
|
||||||
@@ -125,16 +135,24 @@ 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)
|
||||||
}
|
}
|
||||||
|
if ca.MaintenanceInterval == 0 {
|
||||||
|
ca.MaintenanceInterval = caddy.Duration(defaultMaintenanceInterval)
|
||||||
|
}
|
||||||
|
if ca.RenewalWindowRatio <= 0 || ca.RenewalWindowRatio > 1 {
|
||||||
|
ca.RenewalWindowRatio = defaultRenewalWindowRatio
|
||||||
|
}
|
||||||
|
|
||||||
// load the certs and key that will be used for signing
|
// load the certs and key that will be used for signing
|
||||||
var rootCert, interCert *x509.Certificate
|
var rootCert *x509.Certificate
|
||||||
|
var rootCertChain, interCertChain []*x509.Certificate
|
||||||
var rootKey, interKey crypto.Signer
|
var rootKey, interKey crypto.Signer
|
||||||
var err error
|
var err error
|
||||||
if ca.Root != nil {
|
if ca.Root != nil {
|
||||||
if ca.Root.Format == "" || ca.Root.Format == "pem_file" {
|
if ca.Root.Format == "" || ca.Root.Format == "pem_file" {
|
||||||
ca.rootCertPath = ca.Root.Certificate
|
ca.rootCertPath = ca.Root.Certificate
|
||||||
}
|
}
|
||||||
rootCert, rootKey, err = ca.Root.Load()
|
rootCertChain, rootKey, err = ca.Root.Load()
|
||||||
|
rootCert = rootCertChain[0]
|
||||||
} else {
|
} else {
|
||||||
ca.rootCertPath = "storage:" + ca.storageKeyRootCert()
|
ca.rootCertPath = "storage:" + ca.storageKeyRootCert()
|
||||||
rootCert, rootKey, err = ca.loadOrGenRoot()
|
rootCert, rootKey, err = ca.loadOrGenRoot()
|
||||||
@@ -142,21 +160,23 @@ 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()
|
interCertChain, interKey, err = ca.Intermediate.Load()
|
||||||
} else {
|
} else {
|
||||||
interCert, interKey, err = ca.loadOrGenIntermediate(rootCert, rootKey)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
interCertChain, interKey, err = ca.loadOrGenIntermediate(rootCert, rootKey)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ca.mu.Lock()
|
ca.mu.Lock()
|
||||||
ca.root, ca.inter, ca.interKey = rootCert, interCert, interKey
|
ca.root, ca.interChain, ca.interKey = rootCert, interCertChain, interKey
|
||||||
ca.mu.Unlock()
|
ca.mu.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -172,21 +192,21 @@ func (ca CA) RootCertificate() *x509.Certificate {
|
|||||||
// RootKey returns the CA's root private key. Since the root key is
|
// RootKey returns the CA's root private key. Since the root key is
|
||||||
// not cached in memory long-term, it needs to be loaded from storage,
|
// not cached in memory long-term, it needs to be loaded from storage,
|
||||||
// which could yield an error.
|
// which could yield an error.
|
||||||
func (ca CA) RootKey() (any, error) {
|
func (ca CA) RootKey() (crypto.Signer, error) {
|
||||||
_, rootKey, err := ca.loadOrGenRoot()
|
_, rootKey, err := ca.loadOrGenRoot()
|
||||||
return rootKey, err
|
return rootKey, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// IntermediateCertificate returns the CA's intermediate
|
// IntermediateCertificateChain returns the CA's intermediate
|
||||||
// certificate (public key).
|
// certificate chain.
|
||||||
func (ca CA) IntermediateCertificate() *x509.Certificate {
|
func (ca CA) IntermediateCertificateChain() []*x509.Certificate {
|
||||||
ca.mu.RLock()
|
ca.mu.RLock()
|
||||||
defer ca.mu.RUnlock()
|
defer ca.mu.RUnlock()
|
||||||
return ca.inter
|
return ca.interChain
|
||||||
}
|
}
|
||||||
|
|
||||||
// IntermediateKey returns the CA's intermediate private key.
|
// IntermediateKey returns the CA's intermediate private key.
|
||||||
func (ca CA) IntermediateKey() any {
|
func (ca CA) IntermediateKey() crypto.Signer {
|
||||||
ca.mu.RLock()
|
ca.mu.RLock()
|
||||||
defer ca.mu.RUnlock()
|
defer ca.mu.RUnlock()
|
||||||
return ca.interKey
|
return ca.interKey
|
||||||
@@ -207,26 +227,27 @@ func (ca *CA) NewAuthority(authorityConfig AuthorityConfig) (*authority.Authorit
|
|||||||
// cert/key directly, since it's unlikely to expire
|
// cert/key directly, since it's unlikely to expire
|
||||||
// while Caddy is running (long lifetime)
|
// while Caddy is running (long lifetime)
|
||||||
var issuerCert *x509.Certificate
|
var issuerCert *x509.Certificate
|
||||||
var issuerKey any
|
var issuerKey crypto.Signer
|
||||||
issuerCert = rootCert
|
issuerCert = rootCert
|
||||||
var err error
|
var err error
|
||||||
issuerKey, err = ca.RootKey()
|
issuerKey, err = ca.RootKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("loading signing key: %v", err)
|
return nil, fmt.Errorf("loading signing key: %v", err)
|
||||||
}
|
}
|
||||||
signerOption = authority.WithX509Signer(issuerCert, issuerKey.(crypto.Signer))
|
signerOption = authority.WithX509Signer(issuerCert, issuerKey)
|
||||||
} else {
|
} else {
|
||||||
// if we're signing with intermediate, we need to make
|
// if we're signing with intermediate, we need to make
|
||||||
// sure it's always fresh, because the intermediate may
|
// sure it's always fresh, because the intermediate may
|
||||||
// renew while Caddy is running (medium lifetime)
|
// renew while Caddy is running (medium lifetime)
|
||||||
signerOption = authority.WithX509SignerFunc(func() ([]*x509.Certificate, crypto.Signer, error) {
|
signerOption = authority.WithX509SignerFunc(func() ([]*x509.Certificate, crypto.Signer, error) {
|
||||||
issuerCert := ca.IntermediateCertificate()
|
issuerChain := ca.IntermediateCertificateChain()
|
||||||
issuerKey := ca.IntermediateKey().(crypto.Signer)
|
issuerCert := issuerChain[0]
|
||||||
|
issuerKey := ca.IntermediateKey()
|
||||||
ca.log.Debug("using intermediate signer",
|
ca.log.Debug("using intermediate signer",
|
||||||
zap.String("serial", issuerCert.SerialNumber.String()),
|
zap.String("serial", issuerCert.SerialNumber.String()),
|
||||||
zap.String("not_before", issuerCert.NotBefore.String()),
|
zap.String("not_before", issuerCert.NotBefore.String()),
|
||||||
zap.String("not_after", issuerCert.NotAfter.String()))
|
zap.String("not_after", issuerCert.NotAfter.String()))
|
||||||
return []*x509.Certificate{issuerCert}, issuerKey, nil
|
return issuerChain, issuerKey, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +273,11 @@ func (ca *CA) NewAuthority(authorityConfig AuthorityConfig) (*authority.Authorit
|
|||||||
|
|
||||||
func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey crypto.Signer, err error) {
|
func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey crypto.Signer, err error) {
|
||||||
if ca.Root != nil {
|
if ca.Root != nil {
|
||||||
return ca.Root.Load()
|
rootChain, rootSigner, err := ca.Root.Load()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return rootChain[0], rootSigner, nil
|
||||||
}
|
}
|
||||||
rootCertPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyRootCert())
|
rootCertPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyRootCert())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -268,7 +293,7 @@ func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey crypto.Signer,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if rootCert == nil {
|
if rootCert == nil {
|
||||||
rootCert, err = pemDecodeSingleCert(rootCertPEM)
|
rootCert, err = pemDecodeCertificate(rootCertPEM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("parsing root certificate PEM: %v", err)
|
return nil, nil, fmt.Errorf("parsing root certificate PEM: %v", err)
|
||||||
}
|
}
|
||||||
@@ -314,7 +339,8 @@ func (ca CA) genRoot() (rootCert *x509.Certificate, rootKey crypto.Signer, err e
|
|||||||
return rootCert, rootKey, nil
|
return rootCert, rootKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCert *x509.Certificate, interKey crypto.Signer, err error) {
|
func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCertChain []*x509.Certificate, interKey crypto.Signer, err error) {
|
||||||
|
var interCert *x509.Certificate
|
||||||
interCertPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyIntermediateCert())
|
interCertPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyIntermediateCert())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
@@ -326,10 +352,12 @@ func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Si
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("generating new intermediate cert: %v", err)
|
return nil, nil, fmt.Errorf("generating new intermediate cert: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interCertChain = append(interCertChain, interCert)
|
||||||
}
|
}
|
||||||
|
|
||||||
if interCert == nil {
|
if len(interCertChain) == 0 {
|
||||||
interCert, err = pemDecodeSingleCert(interCertPEM)
|
interCertChain, err = pemDecodeCertificateChain(interCertPEM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("decoding intermediate certificate PEM: %v", err)
|
return nil, nil, fmt.Errorf("decoding intermediate certificate PEM: %v", err)
|
||||||
}
|
}
|
||||||
@@ -346,7 +374,7 @@ func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Si
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return interCert, interKey, nil
|
return interCertChain, interKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCert *x509.Certificate, interKey crypto.Signer, err error) {
|
func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCert *x509.Certificate, interKey crypto.Signer, err error) {
|
||||||
@@ -443,4 +471,6 @@ const (
|
|||||||
|
|
||||||
defaultRootLifetime = 24 * time.Hour * 30 * 12 * 10
|
defaultRootLifetime = 24 * time.Hour * 30 * 12 * 10
|
||||||
defaultIntermediateLifetime = 24 * time.Hour * 7
|
defaultIntermediateLifetime = 24 * time.Hour * 7
|
||||||
|
defaultMaintenanceInterval = 10 * time.Minute
|
||||||
|
defaultRenewalWindowRatio = 0.2
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,15 +17,20 @@ package caddypki
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto"
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
|
"go.step.sm/crypto/pemutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func pemDecodeSingleCert(pemDER []byte) (*x509.Certificate, error) {
|
func pemDecodeCertificate(pemDER []byte) (*x509.Certificate, error) {
|
||||||
pemBlock, remaining := pem.Decode(pemDER)
|
pemBlock, remaining := pem.Decode(pemDER)
|
||||||
if pemBlock == nil {
|
if pemBlock == nil {
|
||||||
return nil, fmt.Errorf("no PEM block found")
|
return nil, fmt.Errorf("no PEM block found")
|
||||||
@@ -39,6 +44,15 @@ func pemDecodeSingleCert(pemDER []byte) (*x509.Certificate, error) {
|
|||||||
return x509.ParseCertificate(pemBlock.Bytes)
|
return x509.ParseCertificate(pemBlock.Bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pemDecodeCertificateChain(pemDER []byte) ([]*x509.Certificate, error) {
|
||||||
|
chain, err := pemutil.ParseCertificateBundle(pemDER)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed parsing certificate chain: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain, nil
|
||||||
|
}
|
||||||
|
|
||||||
func pemEncodeCert(der []byte) ([]byte, error) {
|
func pemEncodeCert(der []byte) ([]byte, error) {
|
||||||
return pemEncode("CERTIFICATE", der)
|
return pemEncode("CERTIFICATE", der)
|
||||||
}
|
}
|
||||||
@@ -63,22 +77,25 @@ type KeyPair struct {
|
|||||||
|
|
||||||
// The private key. By default, this should be the path to
|
// The private key. By default, this should be the path to
|
||||||
// a PEM file unless format is something else.
|
// a PEM file unless format is something else.
|
||||||
PrivateKey string `json:"private_key,omitempty"`
|
PrivateKey string `json:"private_key,omitempty"` //nolint:gosec // false positive: yes it's exported, since it needs to encode/decode as JSON; and is often just a filepath
|
||||||
|
|
||||||
// The format in which the certificate and private
|
// The format in which the certificate and private
|
||||||
// key are provided. Default: pem_file
|
// key are provided. Default: pem_file
|
||||||
Format string `json:"format,omitempty"`
|
Format string `json:"format,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads the certificate and key.
|
// Load loads the certificate chain and (optional) private key from
|
||||||
func (kp KeyPair) Load() (*x509.Certificate, crypto.Signer, error) {
|
// the corresponding files, using the configured format. If a
|
||||||
|
// private key is read, it will be verified to belong to the first
|
||||||
|
// certificate in the chain.
|
||||||
|
func (kp KeyPair) Load() ([]*x509.Certificate, crypto.Signer, error) {
|
||||||
switch kp.Format {
|
switch kp.Format {
|
||||||
case "", "pem_file":
|
case "", "pem_file":
|
||||||
certData, err := os.ReadFile(kp.Certificate)
|
certData, err := os.ReadFile(kp.Certificate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
cert, err := pemDecodeSingleCert(certData)
|
chain, err := pemDecodeCertificateChain(certData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -93,11 +110,49 @@ func (kp KeyPair) Load() (*x509.Certificate, crypto.Signer, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
if err := verifyKeysMatch(chain[0], key); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cert, key, nil
|
return chain, key, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, nil, fmt.Errorf("unsupported format: %s", kp.Format)
|
return nil, nil, fmt.Errorf("unsupported format: %s", kp.Format)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// verifyKeysMatch verifies that the public key in the [x509.Certificate] matches
|
||||||
|
// the public key of the [crypto.Signer].
|
||||||
|
func verifyKeysMatch(crt *x509.Certificate, signer crypto.Signer) error {
|
||||||
|
switch pub := crt.PublicKey.(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
pk, ok := signer.Public().(*rsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("private key type %T does not match issuer public key type %T", signer.Public(), pub)
|
||||||
|
}
|
||||||
|
if !pub.Equal(pk) {
|
||||||
|
return errors.New("private key does not match issuer public key")
|
||||||
|
}
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
pk, ok := signer.Public().(*ecdsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("private key type %T does not match issuer public key type %T", signer.Public(), pub)
|
||||||
|
}
|
||||||
|
if !pub.Equal(pk) {
|
||||||
|
return errors.New("private key does not match issuer public key")
|
||||||
|
}
|
||||||
|
case ed25519.PublicKey:
|
||||||
|
pk, ok := signer.Public().(ed25519.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("private key type %T does not match issuer public key type %T", signer.Public(), pub)
|
||||||
|
}
|
||||||
|
if !pub.Equal(pk) {
|
||||||
|
return errors.New("private key does not match issuer public key")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported key type: %T", pub)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
// 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 caddypki
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.step.sm/crypto/keyutil"
|
||||||
|
"go.step.sm/crypto/pemutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKeyPair_Load(t *testing.T) {
|
||||||
|
rootSigner, err := keyutil.GenerateDefaultSigner()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed creating signer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := &x509.Certificate{
|
||||||
|
Subject: pkix.Name{CommonName: "test-root"},
|
||||||
|
IsCA: true,
|
||||||
|
MaxPathLen: 3,
|
||||||
|
}
|
||||||
|
rootBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, rootSigner.Public(), rootSigner)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Creating root certificate failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
root, err := x509.ParseCertificate(rootBytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parsing root certificate failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
intermediateSigner, err := keyutil.GenerateDefaultSigner()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Creating intermedaite signer failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
intermediateBytes, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{
|
||||||
|
Subject: pkix.Name{CommonName: "test-first-intermediate"},
|
||||||
|
IsCA: true,
|
||||||
|
MaxPathLen: 2,
|
||||||
|
NotAfter: time.Now().Add(time.Hour),
|
||||||
|
}, root, intermediateSigner.Public(), rootSigner)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Creating intermediate certificate failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
intermediate, err := x509.ParseCertificate(intermediateBytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parsing intermediate certificate failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var chainContents []byte
|
||||||
|
chain := []*x509.Certificate{intermediate, root}
|
||||||
|
for _, cert := range chain {
|
||||||
|
b, err := pemutil.Serialize(cert)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed serializing intermediate certificate: %v", err)
|
||||||
|
}
|
||||||
|
chainContents = append(chainContents, pem.EncodeToMemory(b)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
rootCertFile := filepath.Join(dir, "root.pem")
|
||||||
|
if _, err = pemutil.Serialize(root, pemutil.WithFilename(rootCertFile)); err != nil {
|
||||||
|
t.Fatalf("Failed serializing root certificate: %v", err)
|
||||||
|
}
|
||||||
|
rootKeyFile := filepath.Join(dir, "root.key")
|
||||||
|
if _, err = pemutil.Serialize(rootSigner, pemutil.WithFilename(rootKeyFile)); err != nil {
|
||||||
|
t.Fatalf("Failed serializing root key: %v", err)
|
||||||
|
}
|
||||||
|
intermediateCertFile := filepath.Join(dir, "intermediate.pem")
|
||||||
|
if _, err = pemutil.Serialize(intermediate, pemutil.WithFilename(intermediateCertFile)); err != nil {
|
||||||
|
t.Fatalf("Failed serializing intermediate certificate: %v", err)
|
||||||
|
}
|
||||||
|
intermediateKeyFile := filepath.Join(dir, "intermediate.key")
|
||||||
|
if _, err = pemutil.Serialize(intermediateSigner, pemutil.WithFilename(intermediateKeyFile)); err != nil {
|
||||||
|
t.Fatalf("Failed serializing intermediate key: %v", err)
|
||||||
|
}
|
||||||
|
chainFile := filepath.Join(dir, "chain.pem")
|
||||||
|
if err := os.WriteFile(chainFile, chainContents, 0644); err != nil {
|
||||||
|
t.Fatalf("Failed writing intermediate chain: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("ok/single-certificate-without-signer", func(t *testing.T) {
|
||||||
|
kp := KeyPair{
|
||||||
|
Certificate: rootCertFile,
|
||||||
|
}
|
||||||
|
chain, signer, err := kp.Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed loading KeyPair: %v", err)
|
||||||
|
}
|
||||||
|
if len(chain) != 1 {
|
||||||
|
t.Errorf("Expected 1 certificate in chain; got %d", len(chain))
|
||||||
|
}
|
||||||
|
if signer != nil {
|
||||||
|
t.Error("Expected no signer to be returned")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ok/single-certificate-with-signer", func(t *testing.T) {
|
||||||
|
kp := KeyPair{
|
||||||
|
Certificate: rootCertFile,
|
||||||
|
PrivateKey: rootKeyFile,
|
||||||
|
}
|
||||||
|
chain, signer, err := kp.Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed loading KeyPair: %v", err)
|
||||||
|
}
|
||||||
|
if len(chain) != 1 {
|
||||||
|
t.Errorf("Expected 1 certificate in chain; got %d", len(chain))
|
||||||
|
}
|
||||||
|
if signer == nil {
|
||||||
|
t.Error("Expected signer to be returned")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ok/multiple-certificates-with-signer", func(t *testing.T) {
|
||||||
|
kp := KeyPair{
|
||||||
|
Certificate: chainFile,
|
||||||
|
PrivateKey: intermediateKeyFile,
|
||||||
|
}
|
||||||
|
chain, signer, err := kp.Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed loading KeyPair: %v", err)
|
||||||
|
}
|
||||||
|
if len(chain) != 2 {
|
||||||
|
t.Errorf("Expected 2 certificates in chain; got %d", len(chain))
|
||||||
|
}
|
||||||
|
if signer == nil {
|
||||||
|
t.Error("Expected signer to be returned")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fail/non-matching-public-key", func(t *testing.T) {
|
||||||
|
kp := KeyPair{
|
||||||
|
Certificate: intermediateCertFile,
|
||||||
|
PrivateKey: rootKeyFile,
|
||||||
|
}
|
||||||
|
chain, signer, err := kp.Load()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected loading KeyPair to return an error")
|
||||||
|
}
|
||||||
|
if chain != nil {
|
||||||
|
t.Error("Expected no chain to be returned")
|
||||||
|
}
|
||||||
|
if signer != nil {
|
||||||
|
t.Error("Expected no signer to be returned")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_pemDecodeCertificate(t *testing.T) {
|
||||||
|
signer, err := keyutil.GenerateDefaultSigner()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed creating signer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := &x509.Certificate{
|
||||||
|
Subject: pkix.Name{CommonName: "test-cert"},
|
||||||
|
IsCA: true,
|
||||||
|
MaxPathLen: 3,
|
||||||
|
}
|
||||||
|
derBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, signer.Public(), signer)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Creating root certificate failed: %v", err)
|
||||||
|
}
|
||||||
|
cert, err := x509.ParseCertificate(derBytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parsing root certificate failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pemBlock, err := pemutil.Serialize(cert)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed serializing certificate: %v", err)
|
||||||
|
}
|
||||||
|
pemData := pem.EncodeToMemory(pemBlock)
|
||||||
|
|
||||||
|
t.Run("ok", func(t *testing.T) {
|
||||||
|
cert, err := pemDecodeCertificate(pemData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed decoding PEM data: %v", err)
|
||||||
|
}
|
||||||
|
if cert == nil {
|
||||||
|
t.Errorf("Expected a certificate in PEM data")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fail/no-pem-data", func(t *testing.T) {
|
||||||
|
cert, err := pemDecodeCertificate(nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected pemDecodeCertificate to return an error")
|
||||||
|
}
|
||||||
|
if cert != nil {
|
||||||
|
t.Errorf("Expected pemDecodeCertificate to return nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fail/multiple", func(t *testing.T) {
|
||||||
|
multiplePEMData := append(pemData, pemData...)
|
||||||
|
cert, err := pemDecodeCertificate(multiplePEMData)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected pemDecodeCertificate to return an error")
|
||||||
|
}
|
||||||
|
if cert != nil {
|
||||||
|
t.Errorf("Expected pemDecodeCertificate to return nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fail/no-pem-certificate", func(t *testing.T) {
|
||||||
|
pkData := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "PRIVATE KEY",
|
||||||
|
Bytes: []byte("some-bogus-private-key"),
|
||||||
|
})
|
||||||
|
cert, err := pemDecodeCertificate(pkData)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected pemDecodeCertificate to return an error")
|
||||||
|
}
|
||||||
|
if cert != nil {
|
||||||
|
t.Errorf("Expected pemDecodeCertificate to return nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_pemDecodeCertificateChain(t *testing.T) {
|
||||||
|
signer, err := keyutil.GenerateDefaultSigner()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed creating signer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := &x509.Certificate{
|
||||||
|
Subject: pkix.Name{CommonName: "test-cert"},
|
||||||
|
IsCA: true,
|
||||||
|
MaxPathLen: 3,
|
||||||
|
}
|
||||||
|
derBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, signer.Public(), signer)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Creating root certificate failed: %v", err)
|
||||||
|
}
|
||||||
|
cert, err := x509.ParseCertificate(derBytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parsing root certificate failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pemBlock, err := pemutil.Serialize(cert)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed serializing certificate: %v", err)
|
||||||
|
}
|
||||||
|
pemData := pem.EncodeToMemory(pemBlock)
|
||||||
|
|
||||||
|
t.Run("ok/single", func(t *testing.T) {
|
||||||
|
certs, err := pemDecodeCertificateChain(pemData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed decoding PEM data: %v", err)
|
||||||
|
}
|
||||||
|
if len(certs) != 1 {
|
||||||
|
t.Errorf("Expected 1 certificate in PEM data; got %d", len(certs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ok/multiple", func(t *testing.T) {
|
||||||
|
multiplePEMData := append(pemData, pemData...)
|
||||||
|
certs, err := pemDecodeCertificateChain(multiplePEMData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed decoding PEM data: %v", err)
|
||||||
|
}
|
||||||
|
if len(certs) != 2 {
|
||||||
|
t.Errorf("Expected 2 certificates in PEM data; got %d", len(certs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fail/no-pem-certificate", func(t *testing.T) {
|
||||||
|
pkData := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "PRIVATE KEY",
|
||||||
|
Bytes: []byte("some-bogus-private-key"),
|
||||||
|
})
|
||||||
|
certs, err := pemDecodeCertificateChain(pkData)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected pemDecodeCertificateChain to return an error")
|
||||||
|
}
|
||||||
|
if len(certs) != 0 {
|
||||||
|
t.Errorf("Expected 0 certificates in PEM data; got %d", len(certs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fail/no-der-certificate", func(t *testing.T) {
|
||||||
|
certs, err := pemDecodeCertificateChain([]byte("invalid-der-data"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected pemDecodeCertificateChain to return an error")
|
||||||
|
}
|
||||||
|
if len(certs) != 0 {
|
||||||
|
t.Errorf("Expected 0 certificates in PEM data; got %d", len(certs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -24,20 +24,24 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *PKI) maintenance() {
|
func (p *PKI) maintenanceForCA(ca *CA) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
log.Printf("[PANIC] PKI maintenance: %v\n%s", err, debug.Stack())
|
log.Printf("[PANIC] PKI maintenance for CA %s: %v\n%s", ca.ID, err, debug.Stack())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ticker := time.NewTicker(10 * time.Minute) // TODO: make configurable
|
interval := time.Duration(ca.MaintenanceInterval)
|
||||||
|
if interval <= 0 {
|
||||||
|
interval = defaultMaintenanceInterval
|
||||||
|
}
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
p.renewCerts()
|
_ = p.renewCertsForCA(ca)
|
||||||
case <-p.ctx.Done():
|
case <-p.ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -63,19 +67,19 @@ func (p *PKI) renewCertsForCA(ca *CA) error {
|
|||||||
|
|
||||||
// only maintain the root if it's not manually provided in the config
|
// only maintain the root if it's not manually provided in the config
|
||||||
if ca.Root == nil {
|
if ca.Root == nil {
|
||||||
if needsRenewal(ca.root) {
|
if ca.needsRenewal(ca.root) {
|
||||||
// TODO: implement root renewal (use same key)
|
// TODO: implement root renewal (use same key)
|
||||||
log.Warn("root certificate expiring soon (FIXME: ROOT RENEWAL NOT YET IMPLEMENTED)",
|
log.Warn("root certificate expiring soon (FIXME: ROOT RENEWAL NOT YET IMPLEMENTED)",
|
||||||
zap.Duration("time_remaining", time.Until(ca.inter.NotAfter)),
|
zap.Duration("time_remaining", time.Until(ca.interChain[0].NotAfter)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// only maintain the intermediate if it's not manually provided in the config
|
// only maintain the intermediate if it's not manually provided in the config
|
||||||
if ca.Intermediate == nil {
|
if ca.Intermediate == nil {
|
||||||
if needsRenewal(ca.inter) {
|
if ca.needsRenewal(ca.interChain[0]) {
|
||||||
log.Info("intermediate expires soon; renewing",
|
log.Info("intermediate expires soon; renewing",
|
||||||
zap.Duration("time_remaining", time.Until(ca.inter.NotAfter)),
|
zap.Duration("time_remaining", time.Until(ca.interChain[0].NotAfter)),
|
||||||
)
|
)
|
||||||
|
|
||||||
rootCert, rootKey, err := ca.loadOrGenRoot()
|
rootCert, rootKey, err := ca.loadOrGenRoot()
|
||||||
@@ -86,10 +90,10 @@ func (p *PKI) renewCertsForCA(ca *CA) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("generating new certificate: %v", err)
|
return fmt.Errorf("generating new certificate: %v", err)
|
||||||
}
|
}
|
||||||
ca.inter, ca.interKey = interCert, interKey
|
ca.interChain, ca.interKey = []*x509.Certificate{interCert}, interKey
|
||||||
|
|
||||||
log.Info("renewed intermediate",
|
log.Info("renewed intermediate",
|
||||||
zap.Time("new_expiration", ca.inter.NotAfter),
|
zap.Time("new_expiration", ca.interChain[0].NotAfter),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,11 +101,15 @@ func (p *PKI) renewCertsForCA(ca *CA) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func needsRenewal(cert *x509.Certificate) bool {
|
// needsRenewal reports whether the certificate is within its renewal window
|
||||||
|
// (i.e. the fraction of lifetime remaining is less than or equal to RenewalWindowRatio).
|
||||||
|
func (ca *CA) needsRenewal(cert *x509.Certificate) bool {
|
||||||
|
ratio := ca.RenewalWindowRatio
|
||||||
|
if ratio <= 0 {
|
||||||
|
ratio = defaultRenewalWindowRatio
|
||||||
|
}
|
||||||
lifetime := cert.NotAfter.Sub(cert.NotBefore)
|
lifetime := cert.NotAfter.Sub(cert.NotBefore)
|
||||||
renewalWindow := time.Duration(float64(lifetime) * renewalWindowRatio)
|
renewalWindow := time.Duration(float64(lifetime) * ratio)
|
||||||
renewalWindowStart := cert.NotAfter.Add(-renewalWindow)
|
renewalWindowStart := cert.NotAfter.Add(-renewalWindow)
|
||||||
return time.Now().After(renewalWindowStart)
|
return time.Now().After(renewalWindowStart)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renewalWindowRatio = 0.2 // TODO: make configurable
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// 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 caddypki
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCA_needsRenewal(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// cert with 100 days lifetime; last 20% = 20 days before expiry
|
||||||
|
// So renewal window starts at (NotAfter - 20 days)
|
||||||
|
makeCert := func(daysUntilExpiry int, lifetimeDays int) *x509.Certificate {
|
||||||
|
notAfter := now.AddDate(0, 0, daysUntilExpiry)
|
||||||
|
notBefore := notAfter.AddDate(0, 0, -lifetimeDays)
|
||||||
|
return &x509.Certificate{NotBefore: notBefore, NotAfter: notAfter}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ca *CA
|
||||||
|
cert *x509.Certificate
|
||||||
|
expect bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "inside renewal window with ratio 0.2",
|
||||||
|
ca: &CA{RenewalWindowRatio: 0.2},
|
||||||
|
cert: makeCert(10, 100),
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "outside renewal window with ratio 0.2",
|
||||||
|
ca: &CA{RenewalWindowRatio: 0.2},
|
||||||
|
cert: makeCert(50, 100),
|
||||||
|
expect: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "outside renewal window with 21 days left",
|
||||||
|
ca: &CA{RenewalWindowRatio: 0.2},
|
||||||
|
cert: makeCert(21, 100),
|
||||||
|
expect: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "just inside renewal window with ratio 0.5",
|
||||||
|
ca: &CA{RenewalWindowRatio: 0.5},
|
||||||
|
cert: makeCert(30, 100),
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero ratio uses default",
|
||||||
|
ca: &CA{RenewalWindowRatio: 0},
|
||||||
|
cert: makeCert(10, 100),
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid ratio uses default",
|
||||||
|
ca: &CA{RenewalWindowRatio: 1.5},
|
||||||
|
cert: makeCert(10, 100),
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := tt.ca.needsRenewal(tt.cert)
|
||||||
|
if got != tt.expect {
|
||||||
|
t.Errorf("needsRenewal() = %v, want %v", got, tt.expect)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,8 +109,10 @@ func (p *PKI) Start() error {
|
|||||||
// see if root/intermediates need renewal...
|
// see if root/intermediates need renewal...
|
||||||
p.renewCerts()
|
p.renewCerts()
|
||||||
|
|
||||||
// ...and keep them renewed
|
// ...and keep them renewed (one goroutine per CA with its own interval)
|
||||||
go p.maintenance()
|
for _, ca := range p.CAs {
|
||||||
|
go p.maintenanceForCA(ca)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error {
|
|||||||
PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout),
|
PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout),
|
||||||
Resolvers: iss.Challenges.DNS.Resolvers,
|
Resolvers: iss.Challenges.DNS.Resolvers,
|
||||||
OverrideDomain: iss.Challenges.DNS.OverrideDomain,
|
OverrideDomain: iss.Challenges.DNS.OverrideDomain,
|
||||||
|
Logger: iss.logger.Named("dns_manager"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,7 +337,7 @@ func (iss *ACMEIssuer) generateZeroSSLEABCredentials(ctx context.Context, acct a
|
|||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
req.Header.Set("User-Agent", certmagic.UserAgent)
|
req.Header.Set("User-Agent", certmagic.UserAgent)
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req) //nolint:gosec // no SSRF since URL is from trusted config
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, acct, fmt.Errorf("performing EAB credentials request: %v", err)
|
return nil, acct, fmt.Errorf("performing EAB credentials request: %v", err)
|
||||||
}
|
}
|
||||||
@@ -671,7 +672,7 @@ func ParseCaddyfilePreferredChainsOptions(d *caddyfile.Dispenser) (*ChainPrefere
|
|||||||
switch d.Val() {
|
switch d.Val() {
|
||||||
case "root_common_name":
|
case "root_common_name":
|
||||||
rootCommonNameOpt := d.RemainingArgs()
|
rootCommonNameOpt := d.RemainingArgs()
|
||||||
chainPref.RootCommonName = rootCommonNameOpt
|
chainPref.RootCommonName = append(chainPref.RootCommonName, rootCommonNameOpt...)
|
||||||
if rootCommonNameOpt == nil {
|
if rootCommonNameOpt == nil {
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
}
|
||||||
@@ -681,7 +682,7 @@ func ParseCaddyfilePreferredChainsOptions(d *caddyfile.Dispenser) (*ChainPrefere
|
|||||||
|
|
||||||
case "any_common_name":
|
case "any_common_name":
|
||||||
anyCommonNameOpt := d.RemainingArgs()
|
anyCommonNameOpt := d.RemainingArgs()
|
||||||
chainPref.AnyCommonName = anyCommonNameOpt
|
chainPref.AnyCommonName = append(chainPref.AnyCommonName, anyCommonNameOpt...)
|
||||||
if anyCommonNameOpt == nil {
|
if anyCommonNameOpt == nil {
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ func (PKIIntermediateCAPool) CaddyModule() caddy.ModuleInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loads the PKI app and load the intermediate certificates into the certificate pool
|
// Loads the PKI app and loads the intermediate certificates into the certificate pool
|
||||||
func (p *PKIIntermediateCAPool) Provision(ctx caddy.Context) error {
|
func (p *PKIIntermediateCAPool) Provision(ctx caddy.Context) error {
|
||||||
pkiApp, err := ctx.AppIfConfigured("pki")
|
pkiApp, err := ctx.AppIfConfigured("pki")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -274,7 +274,9 @@ func (p *PKIIntermediateCAPool) Provision(ctx caddy.Context) error {
|
|||||||
|
|
||||||
caPool := x509.NewCertPool()
|
caPool := x509.NewCertPool()
|
||||||
for _, ca := range p.ca {
|
for _, ca := range p.ca {
|
||||||
caPool.AddCert(ca.IntermediateCertificate())
|
for _, c := range ca.IntermediateCertificateChain() {
|
||||||
|
caPool.AddCert(c)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
p.pool = caPool
|
p.pool = caPool
|
||||||
return nil
|
return nil
|
||||||
@@ -500,8 +502,8 @@ func (t *TLSConfig) unmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||||||
// If there is no custom TLS configuration, a nil config may be returned.
|
// If there is no custom TLS configuration, a nil config may be returned.
|
||||||
// copied from with minor modifications: modules/caddyhttp/reverseproxy/httptransport.go
|
// copied from with minor modifications: modules/caddyhttp/reverseproxy/httptransport.go
|
||||||
func (t *TLSConfig) makeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) {
|
func (t *TLSConfig) makeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) {
|
||||||
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||||
if repl == nil {
|
if !ok || repl == nil {
|
||||||
repl = caddy.NewReplacer()
|
repl = caddy.NewReplacer()
|
||||||
}
|
}
|
||||||
cfg := new(tls.Config)
|
cfg := new(tls.Config)
|
||||||
@@ -586,7 +588,7 @@ func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
res, err := httpClient.Do(req)
|
res, err := httpClient.Do(req) //nolint:gosec // SSRF false positive... uri comes from config
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ func (hcg HTTPCertGetter) GetCertificate(ctx context.Context, hello *tls.ClientH
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req) //nolint:gosec // SSRF false positive... request URI comes from config
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,21 +168,11 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
|
|||||||
tlsApp.RegisterServerNames(echNames)
|
tlsApp.RegisterServerNames(echNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Ideally, ECH keys should be rotated. However, as of Go 1.24, the std lib implementation
|
tlsCfg.GetEncryptedClientHelloKeys = func(chi *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
|
||||||
// does not support safely modifying the tls.Config's EncryptedClientHelloKeys field.
|
tlsApp.EncryptedClientHello.configsMu.RLock()
|
||||||
// So, we implement static ECH keys temporarily. See https://github.com/golang/go/issues/71920.
|
defer tlsApp.EncryptedClientHello.configsMu.RUnlock()
|
||||||
// Revisit this after Go 1.25 is released and implement key rotation.
|
return tlsApp.EncryptedClientHello.stdlibReady, nil
|
||||||
var stdECHKeys []tls.EncryptedClientHelloKey
|
|
||||||
for _, echConfigs := range tlsApp.EncryptedClientHello.configs {
|
|
||||||
for _, c := range echConfigs {
|
|
||||||
stdECHKeys = append(stdECHKeys, tls.EncryptedClientHelloKey{
|
|
||||||
Config: c.configBin,
|
|
||||||
PrivateKey: c.privKeyBin,
|
|
||||||
SendAsRetry: c.sendAsRetry,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
tlsCfg.EncryptedClientHelloKeys = stdECHKeys
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,7 +784,7 @@ func (clientauth *ClientAuthentication) provision(ctx caddy.Context) error {
|
|||||||
for _, fpath := range clientauth.TrustedCACertPEMFiles {
|
for _, fpath := range clientauth.TrustedCACertPEMFiles {
|
||||||
ders, err := convertPEMFilesToDER(fpath)
|
ders, err := convertPEMFilesToDER(fpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
clientauth.TrustedCACerts = append(clientauth.TrustedCACerts, ders...)
|
clientauth.TrustedCACerts = append(clientauth.TrustedCACerts, ders...)
|
||||||
}
|
}
|
||||||
@@ -807,7 +797,7 @@ func (clientauth *ClientAuthentication) provision(ctx caddy.Context) error {
|
|||||||
}
|
}
|
||||||
err := caPool.Provision(ctx)
|
err := caPool.Provision(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
clientauth.ca = caPool
|
clientauth.ca = caPool
|
||||||
}
|
}
|
||||||
|
|||||||
+209
-64
@@ -2,6 +2,7 @@ package caddytls
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
@@ -48,12 +50,6 @@ func init() {
|
|||||||
// applied will automatically upgrade the minimum TLS version to 1.3, even if
|
// applied will automatically upgrade the minimum TLS version to 1.3, even if
|
||||||
// configured to a lower version.
|
// configured to a lower version.
|
||||||
//
|
//
|
||||||
// Note that, as of Caddy 2.10.0 (~March 2025), ECH keys are not automatically
|
|
||||||
// rotated due to a limitation in the Go standard library (see
|
|
||||||
// https://github.com/golang/go/issues/71920). This should be resolved when
|
|
||||||
// Go 1.25 is released (~Aug. 2025), and Caddy will be updated to automatically
|
|
||||||
// rotate ECH keys/configs at that point.
|
|
||||||
//
|
|
||||||
// EXPERIMENTAL: Subject to change.
|
// EXPERIMENTAL: Subject to change.
|
||||||
type ECH struct {
|
type ECH struct {
|
||||||
// The list of ECH configurations for which to automatically generate
|
// The list of ECH configurations for which to automatically generate
|
||||||
@@ -73,14 +69,17 @@ type ECH struct {
|
|||||||
// DNS RRs. (This also typically requires that they use DoH or DoT.)
|
// DNS RRs. (This also typically requires that they use DoH or DoT.)
|
||||||
Publication []*ECHPublication `json:"publication,omitempty"`
|
Publication []*ECHPublication `json:"publication,omitempty"`
|
||||||
|
|
||||||
// map of public_name to list of configs
|
configsMu *sync.RWMutex // protects both configs and the list of configs/keys the standard library uses
|
||||||
configs map[string][]echConfig
|
configs map[string][]echConfig // map of public_name to list of configs
|
||||||
|
stdlibReady []tls.EncryptedClientHelloKey // ECH configs+keys in a format the standard library can use
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision loads or creates ECH configs and returns outer names (for certificate
|
// Provision loads or creates ECH configs and returns outer names (for certificate
|
||||||
// management), but does not publish any ECH configs. The DNS module is used as
|
// management), but does not publish any ECH configs. The DNS module is used as
|
||||||
// a default for later publishing if needed.
|
// a default for later publishing if needed.
|
||||||
func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) {
|
func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) {
|
||||||
|
ech.configsMu = new(sync.RWMutex)
|
||||||
|
|
||||||
logger := ctx.Logger().Named("ech")
|
logger := ctx.Logger().Named("ech")
|
||||||
|
|
||||||
// set up publication modules before we need to obtain a lock in storage,
|
// set up publication modules before we need to obtain a lock in storage,
|
||||||
@@ -98,17 +97,57 @@ func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) {
|
|||||||
// the rest of provisioning needs an exclusive lock so that instances aren't
|
// the rest of provisioning needs an exclusive lock so that instances aren't
|
||||||
// stepping on each other when setting up ECH configs
|
// stepping on each other when setting up ECH configs
|
||||||
storage := ctx.Storage()
|
storage := ctx.Storage()
|
||||||
const echLockName = "ech_provision"
|
if err := storage.Lock(ctx, echStorageLockName); err != nil {
|
||||||
if err := storage.Lock(ctx, echLockName); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := storage.Unlock(ctx, echLockName); err != nil {
|
if err := storage.Unlock(ctx, echStorageLockName); err != nil {
|
||||||
logger.Error("unable to unlock ECH provisioning in storage", zap.Error(err))
|
logger.Error("unable to unlock ECH provisioning in storage", zap.Error(err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var outerNames []string //nolint:prealloc // (FALSE POSITIVE - see https://github.com/alexkohler/prealloc/issues/30)
|
ech.configsMu.Lock()
|
||||||
|
defer ech.configsMu.Unlock()
|
||||||
|
|
||||||
|
outerNames, err := ech.setConfigsFromStorage(ctx, logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("loading configs from storage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// see if we need to make any new ones based on the input configuration
|
||||||
|
for _, cfg := range ech.Configs {
|
||||||
|
publicName := strings.ToLower(strings.TrimSpace(cfg.PublicName))
|
||||||
|
|
||||||
|
if list, ok := ech.configs[publicName]; !ok || len(list) == 0 {
|
||||||
|
// no config with this public name was loaded, so create one
|
||||||
|
echCfg, err := generateAndStoreECHConfig(ctx, publicName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
logger.Debug("generated new ECH config",
|
||||||
|
zap.String("public_name", echCfg.RawPublicName),
|
||||||
|
zap.Uint8("id", echCfg.ConfigID))
|
||||||
|
ech.configs[publicName] = append(ech.configs[publicName], echCfg)
|
||||||
|
outerNames = append(outerNames, publicName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure old keys are rotated out
|
||||||
|
if err = ech.rotateECHKeys(ctx, logger, true); err != nil {
|
||||||
|
return nil, fmt.Errorf("rotating ECH configs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return outerNames, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setConfigsFromStorage sets the ECH configs in memory to those in storage.
|
||||||
|
// It must be called in a write lock on ech.configsMu.
|
||||||
|
func (ech *ECH) setConfigsFromStorage(ctx caddy.Context, logger *zap.Logger) ([]string, error) {
|
||||||
|
storage := ctx.Storage()
|
||||||
|
|
||||||
|
ech.configs = make(map[string][]echConfig)
|
||||||
|
|
||||||
|
var outerNames []string
|
||||||
|
|
||||||
// start by loading all the existing configs (even the older ones on the way out,
|
// start by loading all the existing configs (even the older ones on the way out,
|
||||||
// since some clients may still be using them if they haven't yet picked up on the
|
// since some clients may still be using them if they haven't yet picked up on the
|
||||||
@@ -131,48 +170,143 @@ func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) {
|
|||||||
logger.Debug("loaded ECH config",
|
logger.Debug("loaded ECH config",
|
||||||
zap.String("public_name", cfg.RawPublicName),
|
zap.String("public_name", cfg.RawPublicName),
|
||||||
zap.Uint8("id", cfg.ConfigID))
|
zap.Uint8("id", cfg.ConfigID))
|
||||||
ech.configs[cfg.RawPublicName] = append(ech.configs[cfg.RawPublicName], cfg)
|
if _, seen := ech.configs[cfg.RawPublicName]; !seen {
|
||||||
outerNames = append(outerNames, cfg.RawPublicName)
|
outerNames = append(outerNames, cfg.RawPublicName)
|
||||||
}
|
|
||||||
|
|
||||||
// all existing configs are now loaded; see if we need to make any new ones
|
|
||||||
// based on the input configuration, and also mark the most recent one(s) as
|
|
||||||
// current/active, so they can be used for ECH retries
|
|
||||||
for _, cfg := range ech.Configs {
|
|
||||||
publicName := strings.ToLower(strings.TrimSpace(cfg.PublicName))
|
|
||||||
|
|
||||||
if list, ok := ech.configs[publicName]; ok && len(list) > 0 {
|
|
||||||
// at least one config with this public name was loaded, so find the
|
|
||||||
// most recent one and mark it as active to be used with retries
|
|
||||||
var mostRecentDate time.Time
|
|
||||||
var mostRecentIdx int
|
|
||||||
for i, c := range list {
|
|
||||||
if mostRecentDate.IsZero() || c.meta.Created.After(mostRecentDate) {
|
|
||||||
mostRecentDate = c.meta.Created
|
|
||||||
mostRecentIdx = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
list[mostRecentIdx].sendAsRetry = true
|
|
||||||
} else {
|
|
||||||
// no config with this public name was loaded, so create one
|
|
||||||
echCfg, err := generateAndStoreECHConfig(ctx, publicName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
logger.Debug("generated new ECH config",
|
|
||||||
zap.String("public_name", echCfg.RawPublicName),
|
|
||||||
zap.Uint8("id", echCfg.ConfigID))
|
|
||||||
ech.configs[publicName] = append(ech.configs[publicName], echCfg)
|
|
||||||
outerNames = append(outerNames, publicName)
|
|
||||||
}
|
}
|
||||||
|
ech.configs[cfg.RawPublicName] = append(ech.configs[cfg.RawPublicName], cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
return outerNames, nil
|
return outerNames, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TLS) publishECHConfigs() error {
|
// rotateECHKeys updates the ECH keys/configs that are outdated. It should be called
|
||||||
logger := t.logger.Named("ech")
|
// in a write lock on ech.configsMu. If a lock is already obtained in storage, then
|
||||||
|
// pass true for storageSynced.
|
||||||
|
func (ech *ECH) rotateECHKeys(ctx caddy.Context, logger *zap.Logger, storageSynced bool) error {
|
||||||
|
storage := ctx.Storage()
|
||||||
|
|
||||||
|
// all existing configs are now loaded; rotate keys "regularly" as recommended by the spec
|
||||||
|
// (also: "Rotating too frequently limits the client anonymity set." - but the more server
|
||||||
|
// names, the more frequently rotation can be done safely)
|
||||||
|
const (
|
||||||
|
rotationInterval = 24 * time.Hour * 30
|
||||||
|
deleteAfter = 24 * time.Hour * 90
|
||||||
|
)
|
||||||
|
|
||||||
|
if !ech.rotationNeeded(rotationInterval, deleteAfter) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync this operation across cluster if not already
|
||||||
|
if !storageSynced {
|
||||||
|
if err := storage.Lock(ctx, echStorageLockName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := storage.Unlock(ctx, echStorageLockName); err != nil {
|
||||||
|
logger.Error("unable to unlock ECH rotation in storage", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// update what storage has, in case another instance already updated things
|
||||||
|
if _, err := ech.setConfigsFromStorage(ctx, logger); err != nil {
|
||||||
|
return fmt.Errorf("updating ECH keys from storage: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate the updated list and do any updates as needed
|
||||||
|
for publicName := range ech.configs {
|
||||||
|
for i := 0; i < len(ech.configs[publicName]); i++ {
|
||||||
|
cfg := ech.configs[publicName][i]
|
||||||
|
if time.Since(cfg.meta.Created) >= rotationInterval && cfg.meta.Replaced.IsZero() {
|
||||||
|
// key is due for rotation and it hasn't been replaced yet; do that now
|
||||||
|
logger.Debug("ECH config is due for rotation",
|
||||||
|
zap.String("public_name", cfg.RawPublicName),
|
||||||
|
zap.Uint8("id", cfg.ConfigID),
|
||||||
|
zap.Time("created", cfg.meta.Created),
|
||||||
|
zap.Duration("age", time.Since(cfg.meta.Created)),
|
||||||
|
zap.Duration("rotation_interval", rotationInterval))
|
||||||
|
|
||||||
|
// start by generating and storing the replacement ECH config
|
||||||
|
newCfg, err := generateAndStoreECHConfig(ctx, publicName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("generating and storing new replacement ECH config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark the key as replaced so we don't rotate it again, and instead delete it later
|
||||||
|
ech.configs[publicName][i].meta.Replaced = time.Now()
|
||||||
|
|
||||||
|
// persist the updated metadata
|
||||||
|
metaBytes, err := json.Marshal(ech.configs[publicName][i].meta)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshaling updated ECH config metadata: %v", err)
|
||||||
|
}
|
||||||
|
if err := storage.Store(ctx, echMetaKey(cfg.ConfigID), metaBytes); err != nil {
|
||||||
|
return fmt.Errorf("storing updated ECH config metadata: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ech.configs[publicName] = append(ech.configs[publicName], newCfg)
|
||||||
|
|
||||||
|
logger.Debug("rotated ECH key",
|
||||||
|
zap.String("public_name", cfg.RawPublicName),
|
||||||
|
zap.Uint8("old_id", cfg.ConfigID),
|
||||||
|
zap.Uint8("new_id", newCfg.ConfigID))
|
||||||
|
} else if time.Since(cfg.meta.Created) >= deleteAfter && !cfg.meta.Replaced.IsZero() {
|
||||||
|
// key has expired and is no longer supported; delete it from storage and memory
|
||||||
|
cfgIDKey := path.Join(echConfigsKey, strconv.Itoa(int(cfg.ConfigID)))
|
||||||
|
if err := storage.Delete(ctx, cfgIDKey); err != nil {
|
||||||
|
return fmt.Errorf("deleting expired ECH config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ech.configs[publicName] = append(ech.configs[publicName][:i], ech.configs[publicName][i+1:]...)
|
||||||
|
i--
|
||||||
|
|
||||||
|
logger.Debug("deleted expired ECH key",
|
||||||
|
zap.String("public_name", cfg.RawPublicName),
|
||||||
|
zap.Uint8("id", cfg.ConfigID),
|
||||||
|
zap.Duration("age", time.Since(cfg.meta.Created)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ech.updateKeyList()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotationNeeded returns true if any ECH key needs to be replaced, or deleted.
|
||||||
|
// It must be called inside a read or write lock of ech.configsMu (probably a
|
||||||
|
// write lock, so that the rotation can occur correctly in the same lock).)
|
||||||
|
func (ech *ECH) rotationNeeded(rotationInterval, deleteAfter time.Duration) bool {
|
||||||
|
for publicName := range ech.configs {
|
||||||
|
for i := 0; i < len(ech.configs[publicName]); i++ {
|
||||||
|
cfg := ech.configs[publicName][i]
|
||||||
|
if (time.Since(cfg.meta.Created) >= rotationInterval && cfg.meta.Replaced.IsZero()) ||
|
||||||
|
(time.Since(cfg.meta.Created) >= deleteAfter && !cfg.meta.Replaced.IsZero()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateKeyList updates the list of ECH keys the std lib uses to serve ECH.
|
||||||
|
// It must be called inside a write lock on ech.configsMu.
|
||||||
|
func (ech *ECH) updateKeyList() {
|
||||||
|
ech.stdlibReady = []tls.EncryptedClientHelloKey{}
|
||||||
|
for _, cfgs := range ech.configs {
|
||||||
|
for _, cfg := range cfgs {
|
||||||
|
ech.stdlibReady = append(ech.stdlibReady, tls.EncryptedClientHelloKey{
|
||||||
|
Config: cfg.configBin,
|
||||||
|
PrivateKey: cfg.privKeyBin,
|
||||||
|
SendAsRetry: cfg.meta.Replaced.IsZero(), // only send during retries if key has not been rotated out
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// publishECHConfigs publishes any configs that are configured for publication and which haven't been published already.
|
||||||
|
func (t *TLS) publishECHConfigs(logger *zap.Logger) error {
|
||||||
// make publication exclusive, since we don't need to repeat this unnecessarily
|
// make publication exclusive, since we don't need to repeat this unnecessarily
|
||||||
storage := t.ctx.Storage()
|
storage := t.ctx.Storage()
|
||||||
const echLockName = "ech_publish"
|
const echLockName = "ech_publish"
|
||||||
@@ -197,7 +331,7 @@ func (t *TLS) publishECHConfigs() error {
|
|||||||
publishers: []ECHPublisher{
|
publishers: []ECHPublisher{
|
||||||
&ECHDNSPublisher{
|
&ECHDNSPublisher{
|
||||||
provider: dnsProv,
|
provider: dnsProv,
|
||||||
logger: t.logger,
|
logger: logger,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -209,6 +343,7 @@ func (t *TLS) publishECHConfigs() error {
|
|||||||
// publish with it, and figure out which inner names to publish
|
// publish with it, and figure out which inner names to publish
|
||||||
// to/for, then publish
|
// to/for, then publish
|
||||||
for _, publication := range publicationList {
|
for _, publication := range publicationList {
|
||||||
|
t.EncryptedClientHello.configsMu.RLock()
|
||||||
// this publication is either configured for specific ECH configs,
|
// this publication is either configured for specific ECH configs,
|
||||||
// or we just use an implied default of all ECH configs
|
// or we just use an implied default of all ECH configs
|
||||||
var echCfgList echConfigList
|
var echCfgList echConfigList
|
||||||
@@ -231,6 +366,7 @@ func (t *TLS) publishECHConfigs() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
t.EncryptedClientHello.configsMu.RUnlock()
|
||||||
|
|
||||||
// marshal the ECH config list as binary for publication
|
// marshal the ECH config list as binary for publication
|
||||||
echCfgListBin, err := echCfgList.MarshalBinary()
|
echCfgListBin, err := echCfgList.MarshalBinary()
|
||||||
@@ -250,6 +386,10 @@ func (t *TLS) publishECHConfigs() error {
|
|||||||
if publication.Domains == nil {
|
if publication.Domains == nil {
|
||||||
serverNamesSet = make(map[string]struct{}, len(t.serverNames))
|
serverNamesSet = make(map[string]struct{}, len(t.serverNames))
|
||||||
for name := range t.serverNames {
|
for name := range t.serverNames {
|
||||||
|
// skip Tailscale names, a special case we also handle differently in our auto-HTTPS
|
||||||
|
if strings.HasSuffix(name, ".ts.net") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
serverNamesSet[name] = struct{}{}
|
serverNamesSet[name] = struct{}{}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -304,7 +444,7 @@ func (t *TLS) publishECHConfigs() error {
|
|||||||
// at least a partial failure, maybe a complete failure, but we can
|
// at least a partial failure, maybe a complete failure, but we can
|
||||||
// log each error by domain
|
// log each error by domain
|
||||||
for innerName, domainErr := range publishErrs {
|
for innerName, domainErr := range publishErrs {
|
||||||
t.logger.Error("failed to publish ECH configuration list",
|
logger.Error("failed to publish ECH configuration list",
|
||||||
zap.String("publisher", publisherKey),
|
zap.String("publisher", publisherKey),
|
||||||
zap.String("domain", innerName),
|
zap.String("domain", innerName),
|
||||||
zap.Uint8s("config_ids", configIDs),
|
zap.Uint8s("config_ids", configIDs),
|
||||||
@@ -312,7 +452,7 @@ func (t *TLS) publishECHConfigs() error {
|
|||||||
}
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
// generic error; assume the entire thing failed, I guess
|
// generic error; assume the entire thing failed, I guess
|
||||||
t.logger.Error("failed publishing ECH configuration list",
|
logger.Error("failed publishing ECH configuration list",
|
||||||
zap.String("publisher", publisherKey),
|
zap.String("publisher", publisherKey),
|
||||||
zap.Strings("domains", dnsNamesToPublish),
|
zap.Strings("domains", dnsNamesToPublish),
|
||||||
zap.Uint8s("config_ids", configIDs),
|
zap.Uint8s("config_ids", configIDs),
|
||||||
@@ -334,7 +474,7 @@ func (t *TLS) publishECHConfigs() error {
|
|||||||
successNames = append(successNames, name)
|
successNames = append(successNames, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.logger.Info("successfully published ECH configuration list for "+someAll+" domains",
|
logger.Info("successfully published ECH configuration list for "+someAll+" domains",
|
||||||
zap.String("publisher", publisherKey),
|
zap.String("publisher", publisherKey),
|
||||||
zap.Strings("domains", successNames),
|
zap.Strings("domains", successNames),
|
||||||
zap.Uint8s("config_ids", configIDs))
|
zap.Uint8s("config_ids", configIDs))
|
||||||
@@ -353,13 +493,12 @@ func (t *TLS) publishECHConfigs() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("marshaling ECH config metadata: %v", err)
|
return fmt.Errorf("marshaling ECH config metadata: %v", err)
|
||||||
}
|
}
|
||||||
metaKey := path.Join(echConfigsKey, strconv.Itoa(int(cfg.ConfigID)), "meta.json")
|
if err := t.ctx.Storage().Store(t.ctx, echMetaKey(cfg.ConfigID), metaBytes); err != nil {
|
||||||
if err := t.ctx.Storage().Store(t.ctx, metaKey, metaBytes); err != nil {
|
|
||||||
return fmt.Errorf("storing updated ECH config metadata: %v", err)
|
return fmt.Errorf("storing updated ECH config metadata: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t.logger.Error("all domains failed to publish ECH configuration list (see earlier errors)",
|
logger.Error("all domains failed to publish ECH configuration list (see earlier errors)",
|
||||||
zap.String("publisher", publisherKey),
|
zap.String("publisher", publisherKey),
|
||||||
zap.Strings("domains", dnsNamesToPublish),
|
zap.Strings("domains", dnsNamesToPublish),
|
||||||
zap.Uint8s("config_ids", configIDs))
|
zap.Uint8s("config_ids", configIDs))
|
||||||
@@ -489,7 +628,7 @@ func generateAndStoreECHConfig(ctx caddy.Context, publicName string) (echConfig,
|
|||||||
|
|
||||||
echCfg := echConfig{
|
echCfg := echConfig{
|
||||||
PublicKey: publicKey,
|
PublicKey: publicKey,
|
||||||
Version: draftTLSESNI22,
|
Version: draftTLSESNI25,
|
||||||
ConfigID: configID,
|
ConfigID: configID,
|
||||||
RawPublicName: publicName,
|
RawPublicName: publicName,
|
||||||
KEMID: kemChoice,
|
KEMID: kemChoice,
|
||||||
@@ -507,7 +646,6 @@ func generateAndStoreECHConfig(ctx caddy.Context, publicName string) (echConfig,
|
|||||||
AEADID: hpke.AEAD_ChaCha20Poly1305,
|
AEADID: hpke.AEAD_ChaCha20Poly1305,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sendAsRetry: true,
|
|
||||||
}
|
}
|
||||||
meta := echConfigMeta{
|
meta := echConfigMeta{
|
||||||
Created: time.Now(),
|
Created: time.Now(),
|
||||||
@@ -786,10 +924,9 @@ type echConfig struct {
|
|||||||
|
|
||||||
// these fields are not part of the spec, but are here for
|
// these fields are not part of the spec, but are here for
|
||||||
// our use when setting up TLS servers or maintenance
|
// our use when setting up TLS servers or maintenance
|
||||||
configBin []byte
|
configBin []byte
|
||||||
privKeyBin []byte
|
privKeyBin []byte
|
||||||
meta echConfigMeta
|
meta echConfigMeta
|
||||||
sendAsRetry bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (echCfg echConfig) MarshalBinary() ([]byte, error) {
|
func (echCfg echConfig) MarshalBinary() ([]byte, error) {
|
||||||
@@ -811,8 +948,8 @@ func (echCfg *echConfig) UnmarshalBinary(data []byte) error {
|
|||||||
if !b.ReadUint16(&echCfg.Version) {
|
if !b.ReadUint16(&echCfg.Version) {
|
||||||
return errInvalidLen
|
return errInvalidLen
|
||||||
}
|
}
|
||||||
if echCfg.Version != draftTLSESNI22 {
|
if echCfg.Version != draftTLSESNI25 {
|
||||||
return fmt.Errorf("supported version must be %d: got %d", draftTLSESNI22, echCfg.Version)
|
return fmt.Errorf("supported version must be %d: got %d", draftTLSESNI25, echCfg.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !b.ReadUint16LengthPrefixed(&content) || !b.Empty() {
|
if !b.ReadUint16LengthPrefixed(&content) || !b.Empty() {
|
||||||
@@ -1022,19 +1159,27 @@ func (p PublishECHConfigListErrors) Error() string {
|
|||||||
|
|
||||||
type echConfigMeta struct {
|
type echConfigMeta struct {
|
||||||
Created time.Time `json:"created"`
|
Created time.Time `json:"created"`
|
||||||
|
Replaced time.Time `json:"replaced,omitzero"`
|
||||||
Publications publicationHistory `json:"publications"`
|
Publications publicationHistory `json:"publications"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func echMetaKey(configID uint8) string {
|
||||||
|
return path.Join(echConfigsKey, strconv.Itoa(int(configID)), "meta.json")
|
||||||
|
}
|
||||||
|
|
||||||
// publicationHistory is a map of publisher key to
|
// publicationHistory is a map of publisher key to
|
||||||
// map of inner name to timestamp
|
// map of inner name to timestamp
|
||||||
type publicationHistory map[string]map[string]time.Time
|
type publicationHistory map[string]map[string]time.Time
|
||||||
|
|
||||||
|
// echStorageLockName is the name of the storage lock to sync ECH updates.
|
||||||
|
const echStorageLockName = "ech_rotation"
|
||||||
|
|
||||||
// The key prefix when putting ECH configs in storage. After this
|
// The key prefix when putting ECH configs in storage. After this
|
||||||
// comes the config ID.
|
// comes the config ID.
|
||||||
const echConfigsKey = "ech/configs"
|
const echConfigsKey = "ech/configs"
|
||||||
|
|
||||||
// https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html
|
// https://www.ietf.org/archive/id/draft-ietf-tls-esni-25.html
|
||||||
const draftTLSESNI22 = 0xfe0d
|
const draftTLSESNI25 = 0xfe0d
|
||||||
|
|
||||||
// Interface guard
|
// Interface guard
|
||||||
var _ ECHPublisher = (*ECHDNSPublisher)(nil)
|
var _ ECHPublisher = (*ECHDNSPublisher)(nil)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user