mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 00:32:31 -04:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b892bd2acf | |||
| 3bcfeee97a | |||
| ed9afb05d8 | |||
| 7668108b5d | |||
| e6d44851b1 | |||
| f7d16df78e | |||
| 7ac7ca3ff4 | |||
| 9f586657e8 | |||
| 07ad9534fb | |||
| 030ade0f98 | |||
| da8322bc6e | |||
| e2104d3235 | |||
| 6cc2f7b581 | |||
| 9d3e9e7826 | |||
| 50de66ce12 | |||
| 61d163217f |
@@ -1,31 +0,0 @@
|
||||
name: Issue
|
||||
description: An actionable development item, like a bug report or feature request
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for opening an issue! This is for actionable development items like bug reports and feature requests.
|
||||
If you have a question about using Caddy, please [post on our forums](https://caddy.community) instead.
|
||||
- type: textarea
|
||||
id: content
|
||||
attributes:
|
||||
label: Issue Details
|
||||
placeholder: Describe the issue here. Be specific by providing complete logs and minimal instructions to reproduce, or a thoughtful proposal, etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: assistance-disclosure
|
||||
attributes:
|
||||
label: Assistance Disclosure
|
||||
description: "Our project allows assistance by AI/LLM tools as long as it is disclosed and described so we can better respond. Please certify whether you have used any such tooling related to this issue:"
|
||||
options:
|
||||
-
|
||||
- AI used
|
||||
- AI not used
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: assistance-description
|
||||
attributes:
|
||||
label: If AI was used, describe the extent to which it was used.
|
||||
description: 'Examples: "ChatGPT translated from my native language" or "Claude proposed this change/feature"'
|
||||
@@ -1,5 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Caddy forum
|
||||
url: https://caddy.community
|
||||
about: If you have questions (or answers!) about using Caddy, please use our forum
|
||||
@@ -1,29 +0,0 @@
|
||||
|
||||
|
||||
|
||||
## Assistance Disclosure
|
||||
<!--
|
||||
Thank you for contributing! Please note:
|
||||
|
||||
The use of AI/LLM tools is allowed so long as it is disclosed, so
|
||||
that we can provide better code review and maintain project quality.
|
||||
|
||||
If you used AI/LLM tooling in any way related to this PR, please
|
||||
let us know to what extent it was utilized.
|
||||
|
||||
Examples:
|
||||
|
||||
"No AI was used."
|
||||
"I wrote the code, but Claude generated the tests."
|
||||
"I consulted ChatGPT for a solution, but I authored/coded it myself."
|
||||
"Cody generated the code, and I verified it is correct."
|
||||
"Copilot provided tab completion for code and comments."
|
||||
|
||||
We expect that you have vetted your contributions for correctness.
|
||||
Additionally, signing our CLA certifies that you have the rights to
|
||||
contribute this change.
|
||||
|
||||
Replace the text below with your disclosure:
|
||||
-->
|
||||
|
||||
_This PR is missing an assistance disclosure._
|
||||
@@ -1,30 +0,0 @@
|
||||
name: AI Moderator
|
||||
permissions: read-all
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
jobs:
|
||||
spam-detection:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
models: read
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
- uses: github/ai-moderator@6bcdb2a79c2e564db8d76d7d4439d91a044c4eb6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
spam-label: 'spam'
|
||||
ai-label: 'ai-generated'
|
||||
minimize-detected-comments: true
|
||||
# Built-in prompt configuration (all enabled by default)
|
||||
enable-spam-detection: true
|
||||
enable-link-spam-detection: true
|
||||
enable-ai-detection: true
|
||||
# custom-prompt-path: '.github/prompts/my-custom.prompt.yml' # Optional
|
||||
@@ -1,221 +0,0 @@
|
||||
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');
|
||||
|
||||
+17
-18
@@ -13,7 +13,6 @@ on:
|
||||
- 2.*
|
||||
|
||||
env:
|
||||
GOFLAGS: '-tags=nobadger,nomysql,nopgx'
|
||||
# https://github.com/actions/setup-go/issues/491
|
||||
GOTOOLCHAIN: local
|
||||
|
||||
@@ -31,13 +30,13 @@ jobs:
|
||||
- mac
|
||||
- windows
|
||||
go:
|
||||
- '1.25'
|
||||
- '1.24'
|
||||
|
||||
include:
|
||||
# Set the minimum Go patch version for the given Go minor
|
||||
# Usable via ${{ matrix.GO_SEMVER }}
|
||||
- go: '1.25'
|
||||
GO_SEMVER: '~1.25.0'
|
||||
- go: '1.24'
|
||||
GO_SEMVER: '~1.24.1'
|
||||
|
||||
# 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)
|
||||
@@ -65,15 +64,15 @@ jobs:
|
||||
actions: write # to allow uploading artifacts and cache
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: ${{ matrix.GO_SEMVER }}
|
||||
check-latest: true
|
||||
@@ -111,7 +110,7 @@ jobs:
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
run: |
|
||||
go build -trimpath -ldflags="-w -s" -v
|
||||
go build -tags nobadger,nomysql,nopgx -trimpath -ldflags="-w -s" -v
|
||||
|
||||
- name: Smoke test Caddy
|
||||
working-directory: ./cmd/caddy
|
||||
@@ -134,7 +133,7 @@ jobs:
|
||||
# continue-on-error: true
|
||||
run: |
|
||||
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
|
||||
go test -v -coverprofile="cover-profile.out" -short -race ./...
|
||||
go test -tags nobadger,nomysql,nopgx -v -coverprofile="cover-profile.out" -short -race ./...
|
||||
# echo "status=$?" >> $GITHUB_OUTPUT
|
||||
|
||||
# Relevant step if we reinvestigate publishing test/coverage reports
|
||||
@@ -162,13 +161,13 @@ jobs:
|
||||
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
allowed-endpoints: ci-s390x.caddyserver.com:22
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Run Tests
|
||||
run: |
|
||||
set +e
|
||||
@@ -191,7 +190,7 @@ jobs:
|
||||
retries=3
|
||||
exit_code=0
|
||||
while ((retries > 0)); do
|
||||
CGO_ENABLED=0 go test -p 1 -v ./...
|
||||
CGO_ENABLED=0 go test -p 1 -tags nobadger,nomysql,nopgx -v ./...
|
||||
exit_code=$?
|
||||
if ((exit_code == 0)); then
|
||||
break
|
||||
@@ -221,27 +220,27 @@ jobs:
|
||||
if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
||||
- uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
|
||||
with:
|
||||
version: latest
|
||||
args: check
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: "~1.25"
|
||||
go-version: "~1.24"
|
||||
check-latest: true
|
||||
- name: Install xcaddy
|
||||
run: |
|
||||
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
|
||||
xcaddy version
|
||||
- uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
||||
- uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
|
||||
with:
|
||||
version: latest
|
||||
args: build --single-target --snapshot
|
||||
|
||||
@@ -11,8 +11,6 @@ on:
|
||||
- 2.*
|
||||
|
||||
env:
|
||||
GOFLAGS: '-tags=nobadger,nomysql,nopgx'
|
||||
CGO_ENABLED: '0'
|
||||
# https://github.com/actions/setup-go/issues/491
|
||||
GOTOOLCHAIN: local
|
||||
|
||||
@@ -36,13 +34,13 @@ jobs:
|
||||
- 'darwin'
|
||||
- 'netbsd'
|
||||
go:
|
||||
- '1.25'
|
||||
- '1.24'
|
||||
|
||||
include:
|
||||
# Set the minimum Go patch version for the given Go minor
|
||||
# Usable via ${{ matrix.GO_SEMVER }}
|
||||
- go: '1.25'
|
||||
GO_SEMVER: '~1.25.0'
|
||||
- go: '1.24'
|
||||
GO_SEMVER: '~1.24.1'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -51,15 +49,15 @@ jobs:
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: ${{ matrix.GO_SEMVER }}
|
||||
check-latest: true
|
||||
@@ -76,9 +74,11 @@ jobs:
|
||||
|
||||
- name: Run Build
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goos == 'aix' && 'ppc64' || 'amd64' }}
|
||||
shell: bash
|
||||
continue-on-error: true
|
||||
working-directory: ./cmd/caddy
|
||||
run: go build -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null
|
||||
run: |
|
||||
GOOS=$GOOS GOARCH=$GOARCH go build -tags=nobadger,nomysql,nopgx -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null
|
||||
|
||||
@@ -45,14 +45,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: '~1.25'
|
||||
go-version: '~1.24'
|
||||
check-latest: true
|
||||
|
||||
- name: golangci-lint
|
||||
@@ -73,14 +73,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: govulncheck
|
||||
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
|
||||
with:
|
||||
go-version-input: '~1.25.0'
|
||||
go-version-input: '~1.24.1'
|
||||
check-latest: true
|
||||
|
||||
dependency-review:
|
||||
@@ -90,14 +90,14 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
|
||||
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
|
||||
with:
|
||||
comment-summary-in-pr: on-failure
|
||||
# https://github.com/actions/dependency-review-action/issues/430#issuecomment-1468975566
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
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
|
||||
+20
-395
@@ -13,334 +13,20 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
verify-tag:
|
||||
name: Verify Tag Signature and Approvals
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
outputs:
|
||||
verification_passed: ${{ steps.verify.outputs.passed }}
|
||||
tag_version: ${{ steps.info.outputs.version }}
|
||||
proposal_issue_number: ${{ steps.find_proposal.outputs.result && fromJson(steps.find_proposal.outputs.result).number || '' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# Force fetch upstream tags -- because 65 minutes
|
||||
# tl;dr: actions/checkout@v3 runs this line:
|
||||
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
|
||||
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
|
||||
# git fetch --prune --unshallow
|
||||
# which doesn't overwrite that tag because that would be destructive.
|
||||
# Credit to @francislavoie for the investigation.
|
||||
# https://github.com/actions/checkout/issues/290#issuecomment-680260080
|
||||
- name: Force fetch upstream tags
|
||||
run: git fetch --tags --force
|
||||
|
||||
- name: Get tag info
|
||||
id: info
|
||||
run: |
|
||||
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
# https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
|
||||
- name: Print Go version and environment
|
||||
id: vars
|
||||
run: |
|
||||
printf "Using go at: $(which go)\n"
|
||||
printf "Go version: $(go version)\n"
|
||||
printf "\n\nGo environment:\n\n"
|
||||
go env
|
||||
printf "\n\nSystem environment:\n\n"
|
||||
env
|
||||
echo "version_tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
|
||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
# Add "pip install" CLI tools to PATH
|
||||
echo ~/.local/bin >> $GITHUB_PATH
|
||||
|
||||
# Parse semver
|
||||
TAG=${GITHUB_REF/refs\/tags\//}
|
||||
SEMVER_RE='[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z\.-]*\)'
|
||||
TAG_MAJOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\1#"`
|
||||
TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"`
|
||||
TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"`
|
||||
TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"`
|
||||
echo "tag_major=${TAG_MAJOR}" >> $GITHUB_OUTPUT
|
||||
echo "tag_minor=${TAG_MINOR}" >> $GITHUB_OUTPUT
|
||||
echo "tag_patch=${TAG_PATCH}" >> $GITHUB_OUTPUT
|
||||
echo "tag_special=${TAG_SPECIAL}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Validate commits and tag signatures
|
||||
id: verify
|
||||
env:
|
||||
signing_keys: ${{ secrets.SIGNING_KEYS }}
|
||||
run: |
|
||||
# Read the string into an array, splitting by IFS
|
||||
IFS=";" read -ra keys_collection <<< "$signing_keys"
|
||||
|
||||
# ref: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#example-usage-of-the-runner-context
|
||||
touch "${{ runner.temp }}/allowed_signers"
|
||||
|
||||
# Iterate and print the split elements
|
||||
for item in "${keys_collection[@]}"; do
|
||||
|
||||
# trim leading whitespaces
|
||||
item="${item##*( )}"
|
||||
|
||||
# trim trailing whitespaces
|
||||
item="${item%%*( )}"
|
||||
|
||||
IFS=" " read -ra key_components <<< "$item"
|
||||
# git wants it in format: email address, type, public key
|
||||
# ssh has it in format: type, public key, email address
|
||||
echo "${key_components[2]} namespaces=\"git\" ${key_components[0]} ${key_components[1]}" >> "${{ runner.temp }}/allowed_signers"
|
||||
done
|
||||
|
||||
git config set --global gpg.ssh.allowedSignersFile "${{ runner.temp }}/allowed_signers"
|
||||
|
||||
echo "Verifying the tag: ${{ steps.vars.outputs.version_tag }}"
|
||||
|
||||
# Verify the tag is signed
|
||||
if ! git verify-tag -v "${{ steps.vars.outputs.version_tag }}" 2>&1; then
|
||||
echo "❌ Tag verification failed!"
|
||||
echo "passed=false" >> $GITHUB_OUTPUT
|
||||
git push --delete origin "${{ steps.vars.outputs.version_tag }}"
|
||||
exit 1
|
||||
fi
|
||||
# Run it again to capture the output
|
||||
git verify-tag -v "${{ steps.vars.outputs.version_tag }}" 2>&1 | tee /tmp/verify-output.txt;
|
||||
|
||||
# SSH verification output typically includes the key fingerprint
|
||||
# Use GNU grep with Perl regex for cleaner extraction (Linux environment)
|
||||
KEY_SHA256=$(grep -oP "SHA256:[\"']?\K[A-Za-z0-9+/=]+(?=[\"']?)" /tmp/verify-output.txt | head -1 || echo "")
|
||||
|
||||
if [ -z "$KEY_SHA256" ]; then
|
||||
# Try alternative pattern with "key" prefix
|
||||
KEY_SHA256=$(grep -oP "key SHA256:[\"']?\K[A-Za-z0-9+/=]+(?=[\"']?)" /tmp/verify-output.txt | head -1 || echo "")
|
||||
fi
|
||||
|
||||
if [ -z "$KEY_SHA256" ]; then
|
||||
# Fallback: extract any base64-like string (40+ chars)
|
||||
KEY_SHA256=$(grep -oP '[A-Za-z0-9+/]{40,}=?' /tmp/verify-output.txt | head -1 || echo "")
|
||||
fi
|
||||
|
||||
if [ -z "$KEY_SHA256" ]; then
|
||||
echo "Somehow could not extract SSH key fingerprint from git verify-tag output"
|
||||
echo "Cancelling flow and deleting tag"
|
||||
echo "passed=false" >> $GITHUB_OUTPUT
|
||||
git push --delete origin "${{ steps.vars.outputs.version_tag }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Tag verification succeeded!"
|
||||
echo "passed=true" >> $GITHUB_OUTPUT
|
||||
echo "key_id=$KEY_SHA256" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Find related release proposal
|
||||
id: find_proposal
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const version = '${{ steps.vars.outputs.version_tag }}';
|
||||
|
||||
// Search for PRs with release-proposal label that match this version
|
||||
const prs = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open', // Changed to 'all' to find both open and closed PRs
|
||||
sort: 'updated',
|
||||
direction: 'desc'
|
||||
});
|
||||
|
||||
// Find the most recent PR for this version
|
||||
const proposal = prs.data.find(pr =>
|
||||
pr.title.includes(version) &&
|
||||
pr.labels.some(label => label.name === 'release-proposal')
|
||||
);
|
||||
|
||||
if (!proposal) {
|
||||
console.log(`⚠️ No release proposal PR found for ${version}`);
|
||||
console.log('This might be a hotfix or emergency release');
|
||||
return { number: null, approved: true, approvals: 0, proposedCommit: null };
|
||||
}
|
||||
|
||||
console.log(`Found proposal PR #${proposal.number} for version ${version}`);
|
||||
|
||||
// Extract commit hash from PR body
|
||||
const commitMatch = proposal.body.match(/\*\*Target Commit:\*\*\s*`([a-f0-9]+)`/);
|
||||
const proposedCommit = commitMatch ? commitMatch[1] : null;
|
||||
|
||||
if (proposedCommit) {
|
||||
console.log(`Proposal was for commit: ${proposedCommit}`);
|
||||
} else {
|
||||
console.log('⚠️ No target commit hash found in PR body');
|
||||
}
|
||||
|
||||
// Get PR reviews to extract approvers
|
||||
let approvers = 'Validated by automation';
|
||||
let approvalCount = 2; // Minimum required
|
||||
|
||||
try {
|
||||
const reviews = await github.rest.pulls.listReviews({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: proposal.number
|
||||
});
|
||||
|
||||
// Get latest review per user and filter for approvals
|
||||
const latestReviewsByUser = {};
|
||||
reviews.data.forEach(review => {
|
||||
const username = review.user.login;
|
||||
if (!latestReviewsByUser[username] || new Date(review.submitted_at) > new Date(latestReviewsByUser[username].submitted_at)) {
|
||||
latestReviewsByUser[username] = review;
|
||||
}
|
||||
});
|
||||
|
||||
const approvalReviews = Object.values(latestReviewsByUser).filter(review =>
|
||||
review.state === 'APPROVED'
|
||||
);
|
||||
|
||||
if (approvalReviews.length > 0) {
|
||||
approvers = approvalReviews.map(r => '@' + r.user.login).join(', ');
|
||||
approvalCount = approvalReviews.length;
|
||||
console.log(`Found ${approvalCount} approvals from: ${approvers}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Could not fetch reviews: ${error.message}`);
|
||||
}
|
||||
|
||||
return {
|
||||
number: proposal.number,
|
||||
approved: true,
|
||||
approvals: approvalCount,
|
||||
approvers: approvers,
|
||||
proposedCommit: proposedCommit
|
||||
};
|
||||
result-encoding: json
|
||||
|
||||
- name: Verify proposal commit
|
||||
run: |
|
||||
APPROVALS='${{ steps.find_proposal.outputs.result }}'
|
||||
|
||||
# Parse JSON
|
||||
PROPOSED_COMMIT=$(echo "$APPROVALS" | jq -r '.proposedCommit')
|
||||
CURRENT_COMMIT="${{ steps.info.outputs.sha }}"
|
||||
|
||||
echo "Proposed commit: $PROPOSED_COMMIT"
|
||||
echo "Current commit: $CURRENT_COMMIT"
|
||||
|
||||
# Check if commits match (if proposal had a target commit)
|
||||
if [ "$PROPOSED_COMMIT" != "null" ] && [ -n "$PROPOSED_COMMIT" ]; then
|
||||
# Normalize both commits to full SHA for comparison
|
||||
PROPOSED_FULL=$(git rev-parse "$PROPOSED_COMMIT" 2>/dev/null || echo "")
|
||||
CURRENT_FULL=$(git rev-parse "$CURRENT_COMMIT" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$PROPOSED_FULL" ]; then
|
||||
echo "⚠️ Could not resolve proposed commit: $PROPOSED_COMMIT"
|
||||
elif [ "$PROPOSED_FULL" != "$CURRENT_FULL" ]; then
|
||||
echo "❌ Commit mismatch!"
|
||||
echo "The tag points to commit $CURRENT_FULL but the proposal was for $PROPOSED_FULL"
|
||||
echo "This indicates an error in tag creation."
|
||||
# Delete the tag remotely
|
||||
git push --delete origin "${{ steps.vars.outputs.version_tag }}"
|
||||
echo "Tag ${{steps.vars.outputs.version_tag}} has been deleted"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ Commit hash matches proposal"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ No target commit found in proposal (might be legacy release)"
|
||||
fi
|
||||
|
||||
echo "✅ Tag verification completed"
|
||||
|
||||
- name: Update release proposal PR
|
||||
if: fromJson(steps.find_proposal.outputs.result).number != null
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const result = ${{ steps.find_proposal.outputs.result }};
|
||||
|
||||
if (result.number) {
|
||||
// Add in-progress label
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: result.number,
|
||||
labels: ['release-in-progress']
|
||||
});
|
||||
|
||||
// Remove approved label if present
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: result.number,
|
||||
name: 'approved'
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('Approved label not found:', e.message);
|
||||
}
|
||||
|
||||
const commentBody = [
|
||||
'## 🚀 Release Workflow Started',
|
||||
'',
|
||||
'- **Tag:** ${{ steps.info.outputs.version }}',
|
||||
'- **Signed by key:** ${{ steps.verify.outputs.key_id }}',
|
||||
'- **Commit:** ${{ steps.info.outputs.sha }}',
|
||||
'- **Approved by:** ' + result.approvers,
|
||||
'',
|
||||
'Release workflow is now running. This PR will be updated when the release is published.'
|
||||
].join('\n');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: result.number,
|
||||
body: commentBody
|
||||
});
|
||||
}
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
APPROVALS='${{ steps.find_proposal.outputs.result }}'
|
||||
PROPOSED_COMMIT=$(echo "$APPROVALS" | jq -r '.proposedCommit // "N/A"')
|
||||
APPROVERS=$(echo "$APPROVALS" | jq -r '.approvers // "N/A"')
|
||||
|
||||
echo "## Tag Verification Summary 🔐" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Tag:** ${{ steps.info.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Commit:** ${{ steps.info.outputs.sha }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Proposed Commit:** $PROPOSED_COMMIT" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Signature:** ✅ Verified" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Signed by:** ${{ steps.verify.outputs.key_id }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Approvals:** ✅ Sufficient" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Approved by:** $APPROVERS" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Proceeding with release build..." >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
release:
|
||||
name: Release
|
||||
needs: verify-tag
|
||||
if: ${{ needs.verify-tag.outputs.verification_passed == 'true' }}
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
go:
|
||||
- '1.25'
|
||||
- '1.24'
|
||||
|
||||
include:
|
||||
# Set the minimum Go patch version for the given Go minor
|
||||
# Usable via ${{ matrix.GO_SEMVER }}
|
||||
- go: '1.25'
|
||||
GO_SEMVER: '~1.25.0'
|
||||
- go: '1.24'
|
||||
GO_SEMVER: '~1.24.1'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
|
||||
@@ -350,28 +36,26 @@ jobs:
|
||||
# https://docs.github.com/en/rest/overview/permissions-required-for-github-apps#permission-on-contents
|
||||
# "Releases" is part of `contents`, so it needs the `write`
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: ${{ matrix.GO_SEMVER }}
|
||||
check-latest: true
|
||||
|
||||
# Force fetch upstream tags -- because 65 minutes
|
||||
# tl;dr: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2 runs this line:
|
||||
# tl;dr: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 runs this line:
|
||||
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
|
||||
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
|
||||
# git fetch --prune --unshallow
|
||||
@@ -414,12 +98,22 @@ jobs:
|
||||
- name: Install Cloudsmith CLI
|
||||
run: pip install --upgrade cloudsmith-cli
|
||||
|
||||
- name: Validate commits and tag signatures
|
||||
run: |
|
||||
|
||||
# Import Matt Holt's key
|
||||
curl 'https://github.com/mholt.gpg' | gpg --import
|
||||
|
||||
echo "Verifying the tag: ${{ steps.vars.outputs.version_tag }}"
|
||||
# tags are only accepted if signed by Matt's key
|
||||
git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # main
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # main
|
||||
- name: Cosign version
|
||||
run: cosign version
|
||||
- name: Install Syft
|
||||
uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # main
|
||||
uses: anchore/sbom-action/download-syft@7b36ad622f042cab6f59a75c2ac24ccb256e9b45 # main
|
||||
- name: Syft version
|
||||
run: syft version
|
||||
- name: Install xcaddy
|
||||
@@ -428,7 +122,7 @@ jobs:
|
||||
xcaddy version
|
||||
# GoReleaser will take care of publishing those artifacts into the release
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
||||
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean --timeout 60m
|
||||
@@ -494,72 +188,3 @@ jobs:
|
||||
echo "Pushing $filename to 'testing'"
|
||||
cloudsmith push deb caddy/testing/any-distro/any-version $filename
|
||||
done
|
||||
|
||||
- name: Update release proposal PR
|
||||
if: needs.verify-tag.outputs.proposal_issue_number != ''
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const prNumber = parseInt('${{ needs.verify-tag.outputs.proposal_issue_number }}');
|
||||
|
||||
if (prNumber) {
|
||||
// Get PR details to find the branch
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
const branchName = pr.data.head.ref;
|
||||
|
||||
// Remove in-progress label
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
name: 'release-in-progress'
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('Label not found:', e.message);
|
||||
}
|
||||
|
||||
// Add released label
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
labels: ['released']
|
||||
});
|
||||
|
||||
// Add final comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body: '## ✅ Release Published\n\nThe release has been successfully published and is now available.'
|
||||
});
|
||||
|
||||
// Close the PR if it's still open
|
||||
if (pr.data.state === 'open') {
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber,
|
||||
state: 'closed'
|
||||
});
|
||||
console.log(`Closed PR #${prNumber}`);
|
||||
}
|
||||
|
||||
// Delete the branch
|
||||
try {
|
||||
await github.rest.git.deleteRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `heads/${branchName}`
|
||||
});
|
||||
console.log(`Deleted branch: ${branchName}`);
|
||||
} catch (e) {
|
||||
console.log(`Could not delete branch ${branchName}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,12 @@ jobs:
|
||||
|
||||
# See https://github.com/peter-evans/repository-dispatch
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Trigger event on caddyserver/dist
|
||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
|
||||
with:
|
||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||
repository: caddyserver/dist
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
|
||||
|
||||
- name: Trigger event on caddyserver/caddy-docker
|
||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
|
||||
with:
|
||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||
repository: caddyserver/caddy-docker
|
||||
|
||||
@@ -37,17 +37,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -81,6 +81,6 @@ jobs:
|
||||
# Upload the results to GitHub's code scanning dashboard (optional).
|
||||
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5
|
||||
uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -2,10 +2,6 @@ version: "2"
|
||||
run:
|
||||
issues-exit-code: 1
|
||||
tests: false
|
||||
build-tags:
|
||||
- nobadger
|
||||
- nomysql
|
||||
- nopgx
|
||||
output:
|
||||
formats:
|
||||
text:
|
||||
|
||||
@@ -12,52 +12,24 @@
|
||||
<hr>
|
||||
<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">
|
||||
<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">
|
||||
<a href="https://github.com/caddyserver/caddy/releases">Releases</a> ·
|
||||
<a href="https://caddyserver.com/docs/">Documentation</a> ·
|
||||
<a href="https://caddy.community">Get Help</a>
|
||||
</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
|
||||
|
||||
@@ -72,6 +44,18 @@
|
||||
- [Getting help](#getting-help)
|
||||
- [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)
|
||||
|
||||
@@ -105,7 +89,7 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i
|
||||
|
||||
Requirements:
|
||||
|
||||
- [Go 1.25.0 or newer](https://golang.org/dl/)
|
||||
- [Go 1.24.0 or newer](https://golang.org/dl/)
|
||||
|
||||
### For development
|
||||
|
||||
@@ -133,18 +117,11 @@ 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.
|
||||
|
||||
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
|
||||
|
||||
Using [our builder tool, `xcaddy`](https://github.com/caddyserver/xcaddy)...
|
||||
|
||||
```bash
|
||||
```
|
||||
$ xcaddy build
|
||||
```
|
||||
|
||||
|
||||
@@ -1029,13 +1029,6 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// If this request changed the config, clear the last
|
||||
// config info we have stored, if it is different from
|
||||
// the original source.
|
||||
ClearLastConfigIfDifferent(
|
||||
r.Header.Get("Caddy-Config-Source-File"),
|
||||
r.Header.Get("Caddy-Config-Source-Adapter"))
|
||||
|
||||
default:
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusMethodNotAllowed,
|
||||
@@ -1110,10 +1103,7 @@ func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error
|
||||
if len(body) > 0 {
|
||||
err = json.Unmarshal(body, &val)
|
||||
if err != nil {
|
||||
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)
|
||||
return fmt.Errorf("decoding request body: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -149,9 +149,11 @@ func TestLoadConcurrent(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Go(func() {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
_ = Load(testCfg, true)
|
||||
})
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -205,7 +207,7 @@ func TestETags(t *testing.T) {
|
||||
}
|
||||
|
||||
func BenchmarkLoad(b *testing.B) {
|
||||
for b.Loop() {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Load(testCfg, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -975,11 +975,11 @@ func Version() (simple, full string) {
|
||||
if CustomVersion != "" {
|
||||
full = CustomVersion
|
||||
simple = CustomVersion
|
||||
return simple, full
|
||||
return
|
||||
}
|
||||
full = "unknown"
|
||||
simple = "unknown"
|
||||
return simple, full
|
||||
return
|
||||
}
|
||||
// find the Caddy module in the dependency list
|
||||
for _, dep := range bi.Deps {
|
||||
@@ -1059,7 +1059,7 @@ func Version() (simple, full string) {
|
||||
}
|
||||
}
|
||||
|
||||
return simple, full
|
||||
return
|
||||
}
|
||||
|
||||
// Event represents something that has happened or is happening.
|
||||
@@ -1197,91 +1197,6 @@ var (
|
||||
rawCfgMu sync.RWMutex
|
||||
)
|
||||
|
||||
// lastConfigFile and lastConfigAdapter remember the source config
|
||||
// file and adapter used when Caddy was started via the CLI "run" command.
|
||||
// These are consulted by the SIGUSR1 handler to attempt reloading from
|
||||
// the same source. They are intentionally not set for other entrypoints
|
||||
// such as "caddy start" or subcommands like file-server.
|
||||
var (
|
||||
lastConfigMu sync.RWMutex
|
||||
lastConfigFile string
|
||||
lastConfigAdapter string
|
||||
)
|
||||
|
||||
// reloadFromSourceFunc is the type of stored callback
|
||||
// which is called when we receive a SIGUSR1 signal.
|
||||
type reloadFromSourceFunc func(file, adapter string) error
|
||||
|
||||
// reloadFromSourceCallback is the stored callback
|
||||
// which is called when we receive a SIGUSR1 signal.
|
||||
var reloadFromSourceCallback reloadFromSourceFunc
|
||||
|
||||
// errReloadFromSourceUnavailable is returned when no reload-from-source callback is set.
|
||||
var errReloadFromSourceUnavailable = errors.New("reload from source unavailable in this process") //nolint:unused
|
||||
|
||||
// SetLastConfig records the given source file and adapter as the
|
||||
// last-known external configuration source. Intended to be called
|
||||
// only when starting via "caddy run --config <file> --adapter <adapter>".
|
||||
func SetLastConfig(file, adapter string, fn reloadFromSourceFunc) {
|
||||
lastConfigMu.Lock()
|
||||
lastConfigFile = file
|
||||
lastConfigAdapter = adapter
|
||||
reloadFromSourceCallback = fn
|
||||
lastConfigMu.Unlock()
|
||||
}
|
||||
|
||||
// ClearLastConfigIfDifferent clears the recorded last-config if the provided
|
||||
// source file/adapter do not match the recorded last-config. If both srcFile
|
||||
// and srcAdapter are empty, the last-config is cleared.
|
||||
func ClearLastConfigIfDifferent(srcFile, srcAdapter string) {
|
||||
if (srcFile != "" || srcAdapter != "") && lastConfigMatches(srcFile, srcAdapter) {
|
||||
return
|
||||
}
|
||||
SetLastConfig("", "", nil)
|
||||
}
|
||||
|
||||
// getLastConfig returns the last-known config file and adapter.
|
||||
func getLastConfig() (file, adapter string, fn reloadFromSourceFunc) {
|
||||
lastConfigMu.RLock()
|
||||
f, a, cb := lastConfigFile, lastConfigAdapter, reloadFromSourceCallback
|
||||
lastConfigMu.RUnlock()
|
||||
return f, a, cb
|
||||
}
|
||||
|
||||
// lastConfigMatches returns true if the provided source file and/or adapter
|
||||
// matches the recorded last-config. Matching rules (in priority order):
|
||||
// 1. If srcAdapter is provided and differs from the recorded adapter, no match.
|
||||
// 2. If srcFile exactly equals the recorded file, match.
|
||||
// 3. If both sides can be made absolute and equal, match.
|
||||
// 4. If basenames are equal, match.
|
||||
func lastConfigMatches(srcFile, srcAdapter string) bool {
|
||||
lf, la, _ := getLastConfig()
|
||||
|
||||
// If adapter is provided, it must match.
|
||||
if srcAdapter != "" && srcAdapter != la {
|
||||
return false
|
||||
}
|
||||
|
||||
// Quick equality check.
|
||||
if srcFile == lf {
|
||||
return true
|
||||
}
|
||||
|
||||
// Try absolute path comparison.
|
||||
sAbs, sErr := filepath.Abs(srcFile)
|
||||
lAbs, lErr := filepath.Abs(lf)
|
||||
if sErr == nil && lErr == nil && sAbs == lAbs {
|
||||
return true
|
||||
}
|
||||
|
||||
// Final fallback: basename equality.
|
||||
if filepath.Base(srcFile) == filepath.Base(lf) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// errSameConfig is returned if the new config is the same
|
||||
// as the old one. This isn't usually an actual, actionable
|
||||
// error; it's mostly a sentinel value.
|
||||
|
||||
@@ -308,9 +308,9 @@ func (d *Dispenser) CountRemainingArgs() int {
|
||||
}
|
||||
|
||||
// RemainingArgs loads any more arguments (tokens on the same line)
|
||||
// into a slice of strings and returns them. Open curly brace tokens
|
||||
// also indicate the end of arguments, and the curly brace is not
|
||||
// included in the return value nor is it loaded.
|
||||
// into a slice and returns them. Open curly brace tokens also indicate
|
||||
// the end of arguments, and the curly brace is not included in
|
||||
// the return value nor is it loaded.
|
||||
func (d *Dispenser) RemainingArgs() []string {
|
||||
var args []string
|
||||
for d.NextArg() {
|
||||
@@ -320,9 +320,9 @@ func (d *Dispenser) RemainingArgs() []string {
|
||||
}
|
||||
|
||||
// RemainingArgsRaw loads any more arguments (tokens on the same line,
|
||||
// retaining quotes) into a slice of strings and returns them.
|
||||
// Open curly brace tokens also indicate the end of arguments,
|
||||
// and the curly brace is not included in the return value nor is it loaded.
|
||||
// retaining quotes) into a slice and returns them. Open curly brace
|
||||
// tokens also indicate the end of arguments, and the curly brace is
|
||||
// not included in the return value nor is it loaded.
|
||||
func (d *Dispenser) RemainingArgsRaw() []string {
|
||||
var args []string
|
||||
for d.NextArg() {
|
||||
@@ -331,18 +331,6 @@ func (d *Dispenser) RemainingArgsRaw() []string {
|
||||
return args
|
||||
}
|
||||
|
||||
// RemainingArgsAsTokens loads any more arguments (tokens on the same line)
|
||||
// into a slice of Token-structs and returns them. Open curly brace tokens
|
||||
// also indicate the end of arguments, and the curly brace is not included
|
||||
// in the return value nor is it loaded.
|
||||
func (d *Dispenser) RemainingArgsAsTokens() []Token {
|
||||
var args []Token
|
||||
for d.NextArg() {
|
||||
args = append(args, d.Token())
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// NewFromNextSegment returns a new dispenser with a copy of
|
||||
// the tokens from the current token until the end of the
|
||||
// "directive" whether that be to the end of the line or
|
||||
|
||||
@@ -274,66 +274,6 @@ func TestDispenser_RemainingArgs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispenser_RemainingArgsAsTokens(t *testing.T) {
|
||||
input := `dir1 arg1 arg2 arg3
|
||||
dir2 arg4 arg5
|
||||
dir3 arg6 { arg7
|
||||
dir4`
|
||||
d := NewTestDispenser(input)
|
||||
|
||||
d.Next() // dir1
|
||||
|
||||
args := d.RemainingArgsAsTokens()
|
||||
|
||||
tokenTexts := make([]string, 0, len(args))
|
||||
for _, arg := range args {
|
||||
tokenTexts = append(tokenTexts, arg.Text)
|
||||
}
|
||||
|
||||
if expected := []string{"arg1", "arg2", "arg3"}; !reflect.DeepEqual(tokenTexts, expected) {
|
||||
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
|
||||
}
|
||||
|
||||
d.Next() // dir2
|
||||
|
||||
args = d.RemainingArgsAsTokens()
|
||||
|
||||
tokenTexts = tokenTexts[:0]
|
||||
for _, arg := range args {
|
||||
tokenTexts = append(tokenTexts, arg.Text)
|
||||
}
|
||||
|
||||
if expected := []string{"arg4", "arg5"}; !reflect.DeepEqual(tokenTexts, expected) {
|
||||
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
|
||||
}
|
||||
|
||||
d.Next() // dir3
|
||||
|
||||
args = d.RemainingArgsAsTokens()
|
||||
tokenTexts = tokenTexts[:0]
|
||||
for _, arg := range args {
|
||||
tokenTexts = append(tokenTexts, arg.Text)
|
||||
}
|
||||
|
||||
if expected := []string{"arg6"}; !reflect.DeepEqual(tokenTexts, expected) {
|
||||
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
|
||||
}
|
||||
|
||||
d.Next() // {
|
||||
d.Next() // arg7
|
||||
d.Next() // dir4
|
||||
|
||||
args = d.RemainingArgsAsTokens()
|
||||
tokenTexts = tokenTexts[:0]
|
||||
for _, arg := range args {
|
||||
tokenTexts = append(tokenTexts, arg.Text)
|
||||
}
|
||||
|
||||
if len(args) != 0 {
|
||||
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", []string{}, tokenTexts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispenser_ArgErr_Err(t *testing.T) {
|
||||
input := `dir1 {
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"bytes"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
@@ -53,16 +52,17 @@ func Format(input []byte) []byte {
|
||||
|
||||
newLines int // count of newlines consumed
|
||||
|
||||
comment bool // whether we're in a comment
|
||||
quotes string // encountered quotes ('', '`', '"', '"`', '`"')
|
||||
escaped bool // whether current char is escaped
|
||||
comment bool // whether we're in a comment
|
||||
quoted bool // whether we're in a quoted segment
|
||||
escaped bool // whether current char is escaped
|
||||
|
||||
heredoc heredocState // whether we're in a heredoc
|
||||
heredocEscaped bool // whether heredoc is escaped
|
||||
heredocMarker []rune
|
||||
heredocClosingMarker []rune
|
||||
|
||||
nesting int // indentation level
|
||||
nesting int // indentation level
|
||||
withinBackquote bool
|
||||
)
|
||||
|
||||
write := func(ch rune) {
|
||||
@@ -89,8 +89,12 @@ func Format(input []byte) []byte {
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
if ch == '`' {
|
||||
withinBackquote = !withinBackquote
|
||||
}
|
||||
|
||||
// detect whether we have the start of a heredoc
|
||||
if quotes == "" && (heredoc == heredocClosed && !heredocEscaped) &&
|
||||
if !quoted && (heredoc == heredocClosed && !heredocEscaped) &&
|
||||
space && last == '<' && ch == '<' {
|
||||
write(ch)
|
||||
heredoc = heredocOpening
|
||||
@@ -176,47 +180,16 @@ func Format(input []byte) []byte {
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == '`' {
|
||||
switch quotes {
|
||||
case "\"`":
|
||||
quotes = "\""
|
||||
case "`":
|
||||
quotes = ""
|
||||
case "\"":
|
||||
quotes = "\"`"
|
||||
default:
|
||||
quotes = "`"
|
||||
}
|
||||
}
|
||||
|
||||
if quotes == "\"" {
|
||||
if quoted {
|
||||
if ch == '"' {
|
||||
quotes = ""
|
||||
quoted = false
|
||||
}
|
||||
write(ch)
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == '"' {
|
||||
switch quotes {
|
||||
case "":
|
||||
if space {
|
||||
quotes = "\""
|
||||
}
|
||||
case "`\"":
|
||||
quotes = "`"
|
||||
case "\"`":
|
||||
quotes = ""
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(quotes, "`") {
|
||||
if ch == '`' && space && !beginningOfLine {
|
||||
write(' ')
|
||||
}
|
||||
write(ch)
|
||||
space = false
|
||||
continue
|
||||
if space && ch == '"' {
|
||||
quoted = true
|
||||
}
|
||||
|
||||
if unicode.IsSpace(ch) {
|
||||
@@ -251,7 +224,7 @@ func Format(input []byte) []byte {
|
||||
openBrace = false
|
||||
if beginningOfLine {
|
||||
indent()
|
||||
} else if !openBraceSpace || !unicode.IsSpace(last) {
|
||||
} else if !openBraceSpace {
|
||||
write(' ')
|
||||
}
|
||||
write('{')
|
||||
@@ -268,11 +241,11 @@ func Format(input []byte) []byte {
|
||||
case ch == '{':
|
||||
openBrace = true
|
||||
openBraceSpace = spacePrior && !beginningOfLine
|
||||
if openBraceSpace && newLines == 0 {
|
||||
if openBraceSpace {
|
||||
write(' ')
|
||||
}
|
||||
openBraceWritten = false
|
||||
if quotes == "`" {
|
||||
if withinBackquote {
|
||||
write('{')
|
||||
openBraceWritten = true
|
||||
continue
|
||||
@@ -280,7 +253,7 @@ func Format(input []byte) []byte {
|
||||
continue
|
||||
|
||||
case ch == '}' && (spacePrior || !openBrace):
|
||||
if quotes == "`" {
|
||||
if withinBackquote {
|
||||
write('}')
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -444,37 +444,6 @@ block2 {
|
||||
input: "block {respond \"All braces should remain: {{now | date `2006`}}\"}",
|
||||
expect: "block {respond \"All braces should remain: {{now | date `2006`}}\"}",
|
||||
},
|
||||
{
|
||||
description: "Preserve quoted backticks and backticked quotes",
|
||||
input: "block { respond \"`\" } block { respond `\"`}",
|
||||
expect: "block {\n\trespond \"`\"\n}\n\nblock {\n\trespond `\"`\n}",
|
||||
},
|
||||
{
|
||||
description: "No trailing space on line before env variable",
|
||||
input: `{
|
||||
a
|
||||
|
||||
{$ENV_VAR}
|
||||
}
|
||||
`,
|
||||
expect: `{
|
||||
a
|
||||
|
||||
{$ENV_VAR}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
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,
|
||||
// even if the tests aren't written to expect that
|
||||
|
||||
@@ -379,23 +379,28 @@ func (p *parser) doImport(nesting int) error {
|
||||
if len(blockTokens) > 0 {
|
||||
// use such tokens to create a new dispenser, and then use it to parse each block
|
||||
bd := NewDispenser(blockTokens)
|
||||
|
||||
// one iteration processes one sub-block inside the import
|
||||
for bd.Next() {
|
||||
currentMappingKey := bd.Val()
|
||||
|
||||
if currentMappingKey == "{" {
|
||||
// see if we can grab a key
|
||||
var currentMappingKey string
|
||||
if bd.Val() == "{" {
|
||||
return p.Err("anonymous blocks are not supported")
|
||||
}
|
||||
|
||||
// load up all arguments (if there even are any)
|
||||
currentMappingTokens := bd.RemainingArgsAsTokens()
|
||||
|
||||
// load up the entire block
|
||||
for mappingNesting := bd.Nesting(); bd.NextBlock(mappingNesting); {
|
||||
currentMappingKey = bd.Val()
|
||||
currentMappingTokens := []Token{}
|
||||
// read all args until end of line / {
|
||||
if bd.NextArg() {
|
||||
currentMappingTokens = append(currentMappingTokens, bd.Token())
|
||||
for bd.NextArg() {
|
||||
currentMappingTokens = append(currentMappingTokens, bd.Token())
|
||||
}
|
||||
// TODO(elee1766): we don't enter another mapping here because it's annoying to extract the { and } properly.
|
||||
// maybe someone can do that in the future
|
||||
} else {
|
||||
// attempt to enter a block and add tokens to the currentMappingTokens
|
||||
for mappingNesting := bd.Nesting(); bd.NextBlock(mappingNesting); {
|
||||
currentMappingTokens = append(currentMappingTokens, bd.Token())
|
||||
}
|
||||
}
|
||||
|
||||
blockMapping[currentMappingKey] = currentMappingTokens
|
||||
}
|
||||
}
|
||||
@@ -533,24 +538,29 @@ func (p *parser) doImport(nesting int) error {
|
||||
}
|
||||
// if it is {block}, we substitute with all tokens in the block
|
||||
// if it is {blocks.*}, we substitute with the tokens in the mapping for the *
|
||||
var skip bool
|
||||
var tokensToAdd []Token
|
||||
foundBlockDirective := false
|
||||
switch {
|
||||
case token.Text == "{block}":
|
||||
foundBlockDirective = true
|
||||
tokensToAdd = blockTokens
|
||||
case strings.HasPrefix(token.Text, "{blocks.") && strings.HasSuffix(token.Text, "}"):
|
||||
foundBlockDirective = true
|
||||
// {blocks.foo.bar} will be extracted to key `foo.bar`
|
||||
blockKey := strings.TrimPrefix(strings.TrimSuffix(token.Text, "}"), "{blocks.")
|
||||
val, ok := blockMapping[blockKey]
|
||||
if ok {
|
||||
tokensToAdd = val
|
||||
}
|
||||
default:
|
||||
skip = true
|
||||
}
|
||||
|
||||
if foundBlockDirective {
|
||||
tokensCopy = append(tokensCopy, tokensToAdd...)
|
||||
if !skip {
|
||||
if len(tokensToAdd) == 0 {
|
||||
// if there is no content in the snippet block, don't do any replacement
|
||||
// this allows snippets which contained {block}/{block.*} before this change to continue functioning as normal
|
||||
tokensCopy = append(tokensCopy, token)
|
||||
} else {
|
||||
tokensCopy = append(tokensCopy, tokensToAdd...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -761,7 +771,7 @@ type ServerBlock struct {
|
||||
}
|
||||
|
||||
func (sb ServerBlock) GetKeysText() []string {
|
||||
res := make([]string, 0, len(sb.Keys))
|
||||
res := []string{}
|
||||
for _, k := range sb.Keys {
|
||||
res = append(res, k.Text)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -885,51 +884,6 @@ func TestRejectsGlobalMatcher(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectAnonymousImportBlock(t *testing.T) {
|
||||
p := testParser(`
|
||||
(site) {
|
||||
http://{args[0]} https://{args[0]} {
|
||||
{block}
|
||||
}
|
||||
}
|
||||
|
||||
import site test.domain {
|
||||
{
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
}
|
||||
`)
|
||||
_, err := p.parseAll()
|
||||
if err == nil {
|
||||
t.Fatal("Expected an error, but got nil")
|
||||
}
|
||||
expected := "anonymous blocks are not supported"
|
||||
if !strings.HasPrefix(err.Error(), "anonymous blocks are not supported") {
|
||||
t.Errorf("Expected error to start with '%s' but got '%v'", expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptSiteImportWithBraces(t *testing.T) {
|
||||
p := testParser(`
|
||||
(site) {
|
||||
http://{args[0]} https://{args[0]} {
|
||||
{block}
|
||||
}
|
||||
}
|
||||
|
||||
import site test.domain {
|
||||
reverse_proxy http://192.168.1.1:8080 {
|
||||
header_up Host {host}
|
||||
}
|
||||
}
|
||||
`)
|
||||
_, err := p.parseAll()
|
||||
if err != nil {
|
||||
t.Errorf("Expected error to be nil but got '%v'", err)
|
||||
}
|
||||
}
|
||||
|
||||
func testParser(input string) parser {
|
||||
return parser{Dispenser: NewTestDispenser(input)}
|
||||
}
|
||||
|
||||
@@ -81,11 +81,7 @@ func JSONModuleObject(val any, fieldName, fieldVal string, warnings *[]Warning)
|
||||
err = json.Unmarshal(enc, &tmp)
|
||||
if err != nil {
|
||||
if warnings != nil {
|
||||
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})
|
||||
*warnings = append(*warnings, Warning{Message: err.Error()})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func parseBind(h Helper) ([]ConfigValue, error) {
|
||||
// curves <curves...>
|
||||
// client_auth {
|
||||
// mode [request|require|verify_if_given|require_and_verify]
|
||||
// trust_pool <module_name> [...]
|
||||
// trust_pool <module_name> [...]
|
||||
// trusted_leaf_cert <base64_der>
|
||||
// trusted_leaf_cert_file <filename>
|
||||
// }
|
||||
@@ -481,7 +481,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
// Validate DNS challenge config: any DNS challenge option except "dns" requires a DNS provider
|
||||
if acmeIssuer != nil && acmeIssuer.Challenges != nil && acmeIssuer.Challenges.DNS != nil {
|
||||
dnsCfg := acmeIssuer.Challenges.DNS
|
||||
providerSet := dnsCfg.ProviderRaw != nil || h.Option("dns") != nil || h.Option("acme_dns") != nil
|
||||
providerSet := dnsCfg.ProviderRaw != nil || h.Option("dns") != nil
|
||||
if len(dnsOptionsSet) > 0 && !providerSet {
|
||||
return nil, h.Errf(
|
||||
"setting DNS challenge options [%s] requires a DNS provider (set with the 'dns' subdirective or 'acme_dns' global option)",
|
||||
@@ -930,7 +930,6 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
||||
// modifications to the parsing behavior.
|
||||
parseAsGlobalOption := globalLogNames != nil
|
||||
|
||||
// nolint:prealloc
|
||||
var configValues []ConfigValue
|
||||
|
||||
// Logic below expects that a name is always present when a
|
||||
|
||||
@@ -851,20 +851,6 @@ func (st *ServerType) serversFromPairings(
|
||||
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
|
||||
dirRoutes := sblock.pile["route"]
|
||||
siteSubroute, err := buildSubroute(dirRoutes, groupCounter, true)
|
||||
|
||||
@@ -458,6 +458,8 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||
case "disable_certs":
|
||||
case "ignore_loaded_certs":
|
||||
case "prefer_wildcard":
|
||||
break
|
||||
|
||||
default:
|
||||
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', 'ignore_loaded_certs', or 'prefer_wildcard'")
|
||||
}
|
||||
@@ -472,8 +474,6 @@ func unmarshalCaddyfileMetricsOptions(d *caddyfile.Dispenser) (any, error) {
|
||||
switch d.Val() {
|
||||
case "per_host":
|
||||
metrics.PerHost = true
|
||||
case "observe_catchall_hosts":
|
||||
metrics.ObserveCatchallHosts = true
|
||||
default:
|
||||
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
package httpcaddyfile
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
@@ -180,15 +178,6 @@ func (st ServerType) buildPKIApp(
|
||||
if _, ok := options["skip_install_trust"]; ok {
|
||||
skipInstallTrust = true
|
||||
}
|
||||
|
||||
// check if auto_https is off - in that case we should not create
|
||||
// any PKI infrastructure even with skip_install_trust directive
|
||||
autoHTTPS := []string{}
|
||||
if ah, ok := options["auto_https"].([]string); ok {
|
||||
autoHTTPS = ah
|
||||
}
|
||||
autoHTTPSOff := slices.Contains(autoHTTPS, "off")
|
||||
|
||||
falseBool := false
|
||||
|
||||
// Load the PKI app configured via global options
|
||||
@@ -229,8 +218,7 @@ func (st ServerType) buildPKIApp(
|
||||
// if there was no CAs defined in any of the servers,
|
||||
// and we were requested to not install trust, then
|
||||
// add one for the default/local CA to do so
|
||||
// only if auto_https is not completely disabled
|
||||
if len(pkiApp.CAs) == 0 && skipInstallTrust && !autoHTTPSOff {
|
||||
if len(pkiApp.CAs) == 0 && skipInstallTrust {
|
||||
ca := new(caddypki.CA)
|
||||
ca.ID = caddypki.DefaultCAID
|
||||
ca.InstallTrust = &falseBool
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
@@ -36,27 +35,23 @@ type serverOptions struct {
|
||||
ListenerAddress string
|
||||
|
||||
// These will all map 1:1 to the caddyhttp.Server struct
|
||||
Name string
|
||||
ListenerWrappersRaw []json.RawMessage
|
||||
PacketConnWrappersRaw []json.RawMessage
|
||||
ReadTimeout caddy.Duration
|
||||
ReadHeaderTimeout caddy.Duration
|
||||
WriteTimeout caddy.Duration
|
||||
IdleTimeout caddy.Duration
|
||||
KeepAliveInterval caddy.Duration
|
||||
KeepAliveIdle caddy.Duration
|
||||
KeepAliveCount int
|
||||
MaxHeaderBytes int
|
||||
EnableFullDuplex bool
|
||||
Protocols []string
|
||||
StrictSNIHost *bool
|
||||
TrustedProxiesRaw json.RawMessage
|
||||
TrustedProxiesStrict int
|
||||
TrustedProxiesUnix bool
|
||||
ClientIPHeaders []string
|
||||
ShouldLogCredentials bool
|
||||
Metrics *caddyhttp.Metrics
|
||||
Trace bool // TODO: EXPERIMENTAL
|
||||
Name string
|
||||
ListenerWrappersRaw []json.RawMessage
|
||||
ReadTimeout caddy.Duration
|
||||
ReadHeaderTimeout caddy.Duration
|
||||
WriteTimeout caddy.Duration
|
||||
IdleTimeout caddy.Duration
|
||||
KeepAliveInterval caddy.Duration
|
||||
MaxHeaderBytes int
|
||||
EnableFullDuplex bool
|
||||
Protocols []string
|
||||
StrictSNIHost *bool
|
||||
TrustedProxiesRaw json.RawMessage
|
||||
TrustedProxiesStrict int
|
||||
ClientIPHeaders []string
|
||||
ShouldLogCredentials bool
|
||||
Metrics *caddyhttp.Metrics
|
||||
Trace bool // TODO: EXPERIMENTAL
|
||||
}
|
||||
|
||||
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||
@@ -100,26 +95,6 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||
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":
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
switch d.Val() {
|
||||
@@ -167,7 +142,6 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
|
||||
}
|
||||
}
|
||||
|
||||
case "keepalive_interval":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
@@ -178,26 +152,6 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||
}
|
||||
serverOpts.KeepAliveInterval = caddy.Duration(dur)
|
||||
|
||||
case "keepalive_idle":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
dur, err := caddy.ParseDuration(d.Val())
|
||||
if err != nil {
|
||||
return nil, d.Errf("parsing keepalive idle duration: %v", err)
|
||||
}
|
||||
serverOpts.KeepAliveIdle = caddy.Duration(dur)
|
||||
|
||||
case "keepalive_count":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
cnt, err := strconv.ParseInt(d.Val(), 10, 32)
|
||||
if err != nil {
|
||||
return nil, d.Errf("parsing keepalive count int: %v", err)
|
||||
}
|
||||
serverOpts.KeepAliveCount = int(cnt)
|
||||
|
||||
case "max_header_size":
|
||||
var sizeStr string
|
||||
if !d.AllArgs(&sizeStr) {
|
||||
@@ -273,12 +227,6 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||
}
|
||||
serverOpts.TrustedProxiesStrict = 1
|
||||
|
||||
case "trusted_proxies_unix":
|
||||
if d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
serverOpts.TrustedProxiesUnix = true
|
||||
|
||||
case "client_ip_headers":
|
||||
headers := d.RemainingArgs()
|
||||
for _, header := range headers {
|
||||
@@ -356,14 +304,11 @@ func applyServerOptions(
|
||||
|
||||
// set all the options
|
||||
server.ListenerWrappersRaw = opts.ListenerWrappersRaw
|
||||
server.PacketConnWrappersRaw = opts.PacketConnWrappersRaw
|
||||
server.ReadTimeout = opts.ReadTimeout
|
||||
server.ReadHeaderTimeout = opts.ReadHeaderTimeout
|
||||
server.WriteTimeout = opts.WriteTimeout
|
||||
server.IdleTimeout = opts.IdleTimeout
|
||||
server.KeepAliveInterval = opts.KeepAliveInterval
|
||||
server.KeepAliveIdle = opts.KeepAliveIdle
|
||||
server.KeepAliveCount = opts.KeepAliveCount
|
||||
server.MaxHeaderBytes = opts.MaxHeaderBytes
|
||||
server.EnableFullDuplex = opts.EnableFullDuplex
|
||||
server.Protocols = opts.Protocols
|
||||
@@ -371,7 +316,6 @@ func applyServerOptions(
|
||||
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
|
||||
server.ClientIPHeaders = opts.ClientIPHeaders
|
||||
server.TrustedProxiesStrict = opts.TrustedProxiesStrict
|
||||
server.TrustedProxiesUnix = opts.TrustedProxiesUnix
|
||||
server.Metrics = opts.Metrics
|
||||
if opts.ShouldLogCredentials {
|
||||
if server.Logs == nil {
|
||||
|
||||
@@ -64,13 +64,10 @@ func placeholderShorthands() []string {
|
||||
"{orig_?query}", "{http.request.orig_uri.prefixed_query}",
|
||||
"{method}", "{http.request.method}",
|
||||
"{uri}", "{http.request.uri}",
|
||||
"{%uri}", "{http.request.uri_escaped}",
|
||||
"{path}", "{http.request.uri.path}",
|
||||
"{%path}", "{http.request.uri.path_escaped}",
|
||||
"{dir}", "{http.request.uri.path.dir}",
|
||||
"{file}", "{http.request.uri.path.file}",
|
||||
"{query}", "{http.request.uri.query}",
|
||||
"{%query}", "{http.request.uri.query_escaped}",
|
||||
"{?query}", "{http.request.uri.prefixed_query}",
|
||||
"{remote}", "{http.request.remote}",
|
||||
"{remote_host}", "{http.request.remote.host}",
|
||||
|
||||
@@ -554,7 +554,6 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
||||
globalPreferredChains := options["preferred_chains"]
|
||||
globalCertLifetime := options["cert_lifetime"]
|
||||
globalHTTPPort, globalHTTPSPort := options["http_port"], options["https_port"]
|
||||
globalDefaultBind := options["default_bind"]
|
||||
|
||||
if globalEmail != nil && acmeIssuer.Email == "" {
|
||||
acmeIssuer.Email = globalEmail.(string)
|
||||
@@ -565,21 +564,17 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
||||
if globalACMECARoot != nil && !slices.Contains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
|
||||
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
|
||||
}
|
||||
if globalACMEDNSok && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil || acmeIssuer.Challenges.DNS.ProviderRaw == nil) {
|
||||
globalDNS := options["dns"]
|
||||
if globalDNS == nil && globalACMEDNS == nil {
|
||||
return fmt.Errorf("acme_dns specified without DNS provider config, but no provider specified with 'dns' global option")
|
||||
if globalACMEDNSok && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) {
|
||||
if globalACMEDNS == nil {
|
||||
globalACMEDNS = options["dns"]
|
||||
if globalACMEDNS == nil {
|
||||
return fmt.Errorf("acme_dns specified without DNS provider config, but no provider specified with 'dns' global option")
|
||||
}
|
||||
}
|
||||
if acmeIssuer.Challenges == nil {
|
||||
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
||||
}
|
||||
if acmeIssuer.Challenges.DNS == nil {
|
||||
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
||||
}
|
||||
// If global `dns` is set, do NOT set provider in issuer, just set empty dns config
|
||||
if globalDNS == nil && acmeIssuer.Challenges.DNS.ProviderRaw == nil {
|
||||
// Set a global DNS provider if `acme_dns` is set and `dns` is NOT set
|
||||
acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
|
||||
acmeIssuer.Challenges = &caddytls.ChallengesConfig{
|
||||
DNS: &caddytls.DNSChallengeConfig{
|
||||
ProviderRaw: caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil),
|
||||
},
|
||||
}
|
||||
}
|
||||
if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
|
||||
@@ -607,20 +602,6 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
||||
}
|
||||
acmeIssuer.Challenges.TLSALPN.AlternatePort = globalHTTPSPort.(int)
|
||||
}
|
||||
// If BindHost is still unset, fall back to the first default_bind address if set
|
||||
// This avoids binding the automation policy to the wildcard socket, which is unexpected behavior when a more selective socket is specified via default_bind
|
||||
// In BSD it is valid to bind to the wildcard socket even though a more selective socket is already open (still unexpected behavior by the caller though)
|
||||
// In Linux the same call will error with EADDRINUSE whenever the listener for the automation policy is opened
|
||||
if acmeIssuer.Challenges == nil || (acmeIssuer.Challenges.DNS == nil && acmeIssuer.Challenges.BindHost == "") {
|
||||
if defBinds, ok := globalDefaultBind.([]ConfigValue); ok && len(defBinds) > 0 {
|
||||
if abp, ok := defBinds[0].Value.(addressesWithProtocols); ok && len(abp.addresses) > 0 {
|
||||
if acmeIssuer.Challenges == nil {
|
||||
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
||||
}
|
||||
acmeIssuer.Challenges.BindHost = abp.addresses[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
|
||||
acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
|
||||
}
|
||||
@@ -641,18 +622,12 @@ func newBaseAutomationPolicy(
|
||||
_, hasLocalCerts := options["local_certs"]
|
||||
keyType, hasKeyType := options["key_type"]
|
||||
ocspStapling, hasOCSPStapling := options["ocsp_stapling"]
|
||||
hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling
|
||||
|
||||
globalACMECA := options["acme_ca"]
|
||||
globalACMECARoot := options["acme_ca_root"]
|
||||
_, globalACMEDNS := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set
|
||||
globalACMEEAB := options["acme_eab"]
|
||||
globalPreferredChains := options["preferred_chains"]
|
||||
hasGlobalACMEDefaults := globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS || globalACMEEAB != nil || globalPreferredChains != nil
|
||||
hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling
|
||||
|
||||
// if there are no global options related to automation policies
|
||||
// set, then we can just return right away
|
||||
if !hasGlobalAutomationOpts && !hasGlobalACMEDefaults {
|
||||
if !hasGlobalAutomationOpts {
|
||||
if always {
|
||||
return new(caddytls.AutomationPolicy), nil
|
||||
}
|
||||
@@ -674,14 +649,6 @@ func newBaseAutomationPolicy(
|
||||
ap.Issuers = []certmagic.Issuer{new(caddytls.InternalIssuer)}
|
||||
}
|
||||
|
||||
if hasGlobalACMEDefaults {
|
||||
for i := range ap.Issuers {
|
||||
if err := fillInGlobalACMEDefaults(ap.Issuers[i], options); err != nil {
|
||||
return nil, fmt.Errorf("filling in global issuer defaults for issuer %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasOCSPStapling {
|
||||
ocspConfig := ocspStapling.(certmagic.OCSPConfig)
|
||||
ap.DisableOCSPStapling = ocspConfig.DisableStapling
|
||||
|
||||
@@ -121,13 +121,6 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
|
||||
}
|
||||
}
|
||||
|
||||
// If this request changed the config, clear the last
|
||||
// config info we have stored, if it is different from
|
||||
// the original source.
|
||||
caddy.ClearLastConfigIfDifferent(
|
||||
r.Header.Get("Caddy-Config-Source-File"),
|
||||
r.Header.Get("Caddy-Config-Source-Adapter"))
|
||||
|
||||
caddy.Log().Named("admin.api").Info("load complete")
|
||||
|
||||
return nil
|
||||
|
||||
@@ -362,8 +362,6 @@ func CreateTestingTransport() *http.Transport {
|
||||
|
||||
// AssertLoadError will load a config and expect an error
|
||||
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
|
||||
t.Helper()
|
||||
|
||||
tc := NewTester(t)
|
||||
|
||||
err := tc.initServer(rawConfig, configType)
|
||||
@@ -374,8 +372,6 @@ func AssertLoadError(t *testing.T, rawConfig string, configType string, expected
|
||||
|
||||
// AssertRedirect makes a request and asserts the redirection happens
|
||||
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
|
||||
tc.t.Helper()
|
||||
|
||||
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
@@ -413,8 +409,6 @@ func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, e
|
||||
|
||||
// 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 {
|
||||
t.Helper()
|
||||
|
||||
cfgAdapter := caddyconfig.GetAdapter(adapterName)
|
||||
if cfgAdapter == nil {
|
||||
t.Logf("unrecognized config adapter '%s'", adapterName)
|
||||
@@ -474,8 +468,6 @@ func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string,
|
||||
|
||||
// AssertAdapt adapts a config and then tests it against an expected result
|
||||
func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) {
|
||||
t.Helper()
|
||||
|
||||
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
|
||||
if !ok {
|
||||
t.Fail()
|
||||
@@ -504,8 +496,6 @@ 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
|
||||
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
|
||||
tc.t.Helper()
|
||||
|
||||
resp, err := tc.Client.Do(req)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("failed to call server %s", err)
|
||||
@@ -520,8 +510,6 @@ func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int)
|
||||
|
||||
// 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) {
|
||||
tc.t.Helper()
|
||||
|
||||
resp := tc.AssertResponseCode(req, expectedStatusCode)
|
||||
|
||||
defer resp.Body.Close()
|
||||
@@ -543,8 +531,6 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
|
||||
|
||||
// AssertGetResponse GET a URI and expect a statusCode and body text
|
||||
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
tc.t.Helper()
|
||||
|
||||
req, err := http.NewRequest("GET", requestURI, nil)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("unable to create request %s", err)
|
||||
@@ -555,8 +541,6 @@ func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, e
|
||||
|
||||
// AssertDeleteResponse request a URI and expect a statusCode and body text
|
||||
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
tc.t.Helper()
|
||||
|
||||
req, err := http.NewRequest("DELETE", requestURI, nil)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("unable to create request %s", err)
|
||||
@@ -567,8 +551,6 @@ func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int
|
||||
|
||||
// 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) {
|
||||
tc.t.Helper()
|
||||
|
||||
req, err := http.NewRequest("POST", requestURI, requestBody)
|
||||
if err != nil {
|
||||
tc.t.Errorf("failed to create request %s", err)
|
||||
@@ -582,8 +564,6 @@ func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []str
|
||||
|
||||
// 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) {
|
||||
tc.t.Helper()
|
||||
|
||||
req, err := http.NewRequest("PUT", requestURI, requestBody)
|
||||
if err != nil {
|
||||
tc.t.Errorf("failed to create request %s", err)
|
||||
@@ -597,8 +577,6 @@ func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []stri
|
||||
|
||||
// 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) {
|
||||
tc.t.Helper()
|
||||
|
||||
req, err := http.NewRequest("PATCH", requestURI, requestBody)
|
||||
if err != nil {
|
||||
tc.t.Errorf("failed to create request %s", err)
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
{
|
||||
acme_dns mock foo
|
||||
}
|
||||
|
||||
example.com {
|
||||
respond "Hello World"
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "Hello World",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"argument": "foo",
|
||||
"name": "mock"
|
||||
}
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,10 +34,17 @@ example.com {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"example.com"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {}
|
||||
"dns": {
|
||||
"provider": {
|
||||
"name": "mock"
|
||||
}
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
acme_dns
|
||||
}
|
||||
|
||||
example.com {
|
||||
respond "Hello World"
|
||||
}
|
||||
----------
|
||||
acme_dns specified without DNS provider config, but no provider specified with 'dns' global option
|
||||
@@ -31,6 +31,9 @@ example.com
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"example.com"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"module": "acme",
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
trusted_proxies static private_ranges
|
||||
client_ip_headers Custom-Real-Client-IP X-Forwarded-For
|
||||
client_ip_headers A-Third-One
|
||||
keepalive_interval 20s
|
||||
keepalive_idle 20s
|
||||
keepalive_count 10
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,9 +45,6 @@ foo.com {
|
||||
"read_header_timeout": 30000000000,
|
||||
"write_timeout": 30000000000,
|
||||
"idle_timeout": 30000000000,
|
||||
"keepalive_interval": 20000000000,
|
||||
"keepalive_idle": 20000000000,
|
||||
"keepalive_count": 10,
|
||||
"max_header_bytes": 100000000,
|
||||
"enable_full_duplex": true,
|
||||
"routes": [
|
||||
@@ -95,4 +89,4 @@ foo.com {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
(site) {
|
||||
http://{args[0]} https://{args[0]} {
|
||||
{block}
|
||||
}
|
||||
}
|
||||
import site test.domain {
|
||||
{
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
}
|
||||
----------
|
||||
anonymous blocks are not supported
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
(snippet) {
|
||||
header {
|
||||
reverse_proxy localhost:3000
|
||||
{block}
|
||||
}
|
||||
}
|
||||
|
||||
example.com {
|
||||
import snippet
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "headers",
|
||||
"response": {
|
||||
"set": {
|
||||
"Reverse_proxy": [
|
||||
"localhost:3000"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
(snippet) {
|
||||
header {
|
||||
reverse_proxy localhost:3000
|
||||
{blocks.content_type}
|
||||
}
|
||||
}
|
||||
|
||||
example.com {
|
||||
import snippet
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "headers",
|
||||
"response": {
|
||||
"set": {
|
||||
"Reverse_proxy": [
|
||||
"localhost:3000"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
(site) {
|
||||
https://{args[0]} {
|
||||
{block}
|
||||
}
|
||||
}
|
||||
|
||||
import site test.domain {
|
||||
reverse_proxy http://192.168.1.1:8080 {
|
||||
header_up Host {host}
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"test.domain"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"headers": {
|
||||
"request": {
|
||||
"set": {
|
||||
"Host": [
|
||||
"{http.request.host}"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "192.168.1.1:8080"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
:80
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format filter {
|
||||
wrap console
|
||||
|
||||
# Multiple regexp filters for the same field - this should work now!
|
||||
request>headers>Authorization regexp "Bearer\s+([A-Za-z0-9_-]+)" "Bearer [REDACTED]"
|
||||
request>headers>Authorization regexp "Basic\s+([A-Za-z0-9+/=]+)" "Basic [REDACTED]"
|
||||
request>headers>Authorization regexp "token=([^&\s]+)" "token=[REDACTED]"
|
||||
|
||||
# Single regexp filter - this should continue to work as before
|
||||
request>headers>Cookie regexp "sessionid=[^;]+" "sessionid=[REDACTED]"
|
||||
|
||||
# Mixed filters (non-regexp) - these should work normally
|
||||
request>headers>Server delete
|
||||
request>remote_ip ip_mask {
|
||||
ipv4 24
|
||||
ipv6 32
|
||||
}
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"logging": {
|
||||
"logs": {
|
||||
"default": {
|
||||
"exclude": [
|
||||
"http.log.access.log0"
|
||||
]
|
||||
},
|
||||
"log0": {
|
||||
"writer": {
|
||||
"output": "stdout"
|
||||
},
|
||||
"encoder": {
|
||||
"fields": {
|
||||
"request\u003eheaders\u003eAuthorization": {
|
||||
"filter": "multi_regexp",
|
||||
"operations": [
|
||||
{
|
||||
"regexp": "Bearer\\s+([A-Za-z0-9_-]+)",
|
||||
"value": "Bearer [REDACTED]"
|
||||
},
|
||||
{
|
||||
"regexp": "Basic\\s+([A-Za-z0-9+/=]+)",
|
||||
"value": "Basic [REDACTED]"
|
||||
},
|
||||
{
|
||||
"regexp": "token=([^\u0026\\s]+)",
|
||||
"value": "token=[REDACTED]"
|
||||
}
|
||||
]
|
||||
},
|
||||
"request\u003eheaders\u003eCookie": {
|
||||
"filter": "regexp",
|
||||
"regexp": "sessionid=[^;]+",
|
||||
"value": "sessionid=[REDACTED]"
|
||||
},
|
||||
"request\u003eheaders\u003eServer": {
|
||||
"filter": "delete"
|
||||
},
|
||||
"request\u003eremote_ip": {
|
||||
"filter": "ip_mask",
|
||||
"ipv4_cidr": 24,
|
||||
"ipv6_cidr": 32
|
||||
}
|
||||
},
|
||||
"format": "filter",
|
||||
"wrap": {
|
||||
"format": "console"
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"http.log.access.log0"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":80"
|
||||
],
|
||||
"logs": {
|
||||
"default_logger_name": "log0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
{
|
||||
servers {
|
||||
trusted_proxies_unix
|
||||
}
|
||||
}
|
||||
|
||||
example.com {
|
||||
reverse_proxy https://local:8080
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"transport": {
|
||||
"protocol": "http",
|
||||
"tls": {}
|
||||
},
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "local:8080"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
],
|
||||
"trusted_proxies_unix": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
{
|
||||
acme_dns mock foo
|
||||
}
|
||||
|
||||
localhost {
|
||||
tls {
|
||||
dns mock bar
|
||||
resolvers 8.8.8.8 8.8.4.4
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"localhost"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"argument": "bar",
|
||||
"name": "mock"
|
||||
},
|
||||
"resolvers": [
|
||||
"8.8.8.8",
|
||||
"8.8.4.4"
|
||||
]
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"argument": "foo",
|
||||
"name": "mock"
|
||||
}
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
{
|
||||
dns mock foo
|
||||
}
|
||||
|
||||
localhost {
|
||||
tls {
|
||||
dns mock bar
|
||||
resolvers 8.8.8.8 8.8.4.4
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"localhost"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"argument": "bar",
|
||||
"name": "mock"
|
||||
},
|
||||
"resolvers": [
|
||||
"8.8.8.8",
|
||||
"8.8.4.4"
|
||||
]
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"dns": {
|
||||
"argument": "foo",
|
||||
"name": "mock"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
{
|
||||
acme_dns mock
|
||||
}
|
||||
|
||||
localhost {
|
||||
tls {
|
||||
resolvers 8.8.8.8 8.8.4.4
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"localhost"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"name": "mock"
|
||||
},
|
||||
"resolvers": [
|
||||
"8.8.8.8",
|
||||
"8.8.4.4"
|
||||
]
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"name": "mock"
|
||||
}
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
)
|
||||
|
||||
func newH2ListenerWithVersionsWithTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.Tester {
|
||||
const baseConfig = `
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
servers :9443 {
|
||||
protocols %s
|
||||
}
|
||||
}
|
||||
localhost {
|
||||
respond "{http.request.tls.proto} {http.request.proto}"
|
||||
}
|
||||
`
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile")
|
||||
|
||||
tr := tester.Client.Transport.(*http.Transport)
|
||||
tr.TLSClientConfig.NextProtos = clientVersions
|
||||
tr.Protocols = new(http.Protocols)
|
||||
if slices.Contains(clientVersions, "h2") {
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
tr.Protocols.SetHTTP2(true)
|
||||
}
|
||||
if !slices.Contains(clientVersions, "http/1.1") {
|
||||
tr.Protocols.SetHTTP1(false)
|
||||
}
|
||||
|
||||
return tester
|
||||
}
|
||||
|
||||
func TestH2ListenerWithTLS(t *testing.T) {
|
||||
tests := []struct {
|
||||
serverVersions []string
|
||||
clientVersions []string
|
||||
expectedBody string
|
||||
failed bool
|
||||
}{
|
||||
{[]string{"h2"}, []string{"h2"}, "h2 HTTP/2.0", false},
|
||||
{[]string{"h2"}, []string{"http/1.1"}, "", true},
|
||||
{[]string{"h1"}, []string{"http/1.1"}, "http/1.1 HTTP/1.1", false},
|
||||
{[]string{"h1"}, []string{"h2"}, "", true},
|
||||
{[]string{"h2", "h1"}, []string{"h2"}, "h2 HTTP/2.0", false},
|
||||
{[]string{"h2", "h1"}, []string{"http/1.1"}, "http/1.1 HTTP/1.1", false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tester := newH2ListenerWithVersionsWithTLSTester(t, tc.serverVersions, tc.clientVersions)
|
||||
t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions)
|
||||
if tc.failed {
|
||||
resp, err := tester.Client.Get("https://localhost:9443")
|
||||
if err == nil {
|
||||
t.Errorf("unexpected response: %d", resp.StatusCode)
|
||||
}
|
||||
} else {
|
||||
tester.AssertGetResponse("https://localhost:9443", 200, tc.expectedBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newH2ListenerWithVersionsWithoutTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.Tester {
|
||||
const baseConfig = `
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
servers :9080 {
|
||||
protocols %s
|
||||
}
|
||||
}
|
||||
http://localhost {
|
||||
respond "{http.request.proto}"
|
||||
}
|
||||
`
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile")
|
||||
|
||||
tr := tester.Client.Transport.(*http.Transport)
|
||||
tr.Protocols = new(http.Protocols)
|
||||
if slices.Contains(clientVersions, "h2c") {
|
||||
tr.Protocols.SetHTTP1(false)
|
||||
tr.Protocols.SetUnencryptedHTTP2(true)
|
||||
} else if slices.Contains(clientVersions, "http/1.1") {
|
||||
tr.Protocols.SetHTTP1(true)
|
||||
tr.Protocols.SetUnencryptedHTTP2(false)
|
||||
}
|
||||
|
||||
return tester
|
||||
}
|
||||
|
||||
func TestH2ListenerWithoutTLS(t *testing.T) {
|
||||
tests := []struct {
|
||||
serverVersions []string
|
||||
clientVersions []string
|
||||
expectedBody string
|
||||
failed bool
|
||||
}{
|
||||
{[]string{"h2c"}, []string{"h2c"}, "HTTP/2.0", false},
|
||||
{[]string{"h2c"}, []string{"http/1.1"}, "", true},
|
||||
{[]string{"h1"}, []string{"http/1.1"}, "HTTP/1.1", false},
|
||||
{[]string{"h1"}, []string{"h2c"}, "", true},
|
||||
{[]string{"h2c", "h1"}, []string{"h2c"}, "HTTP/2.0", false},
|
||||
{[]string{"h2c", "h1"}, []string{"http/1.1"}, "HTTP/1.1", false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tester := newH2ListenerWithVersionsWithoutTLSTester(t, tc.serverVersions, tc.clientVersions)
|
||||
t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions)
|
||||
if tc.failed {
|
||||
resp, err := tester.Client.Get("http://localhost:9080")
|
||||
if err == nil {
|
||||
t.Errorf("unexpected response: %d", resp.StatusCode)
|
||||
}
|
||||
} else {
|
||||
tester.AssertGetResponse("http://localhost:9080", 200, tc.expectedBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,7 @@ func init() {
|
||||
}
|
||||
|
||||
// MockDNSProvider is a mock DNS provider, for testing config with DNS modules.
|
||||
type MockDNSProvider struct {
|
||||
Argument string `json:"argument,omitempty"` // optional argument useful for testing
|
||||
}
|
||||
type MockDNSProvider struct{}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (MockDNSProvider) CaddyModule() caddy.ModuleInfo {
|
||||
@@ -33,15 +31,7 @@ func (MockDNSProvider) Provision(ctx caddy.Context) error {
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
|
||||
func (p *MockDNSProvider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
d.Next() // consume directive name
|
||||
|
||||
if d.NextArg() {
|
||||
p.Argument = d.Val()
|
||||
}
|
||||
if d.NextArg() {
|
||||
return d.Errf("unexpected argument '%s'", d.Val())
|
||||
}
|
||||
func (MockDNSProvider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -29,8 +29,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "time/tzdata"
|
||||
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
|
||||
// plug in Caddy modules here
|
||||
|
||||
+23
-126
@@ -172,19 +172,9 @@ func cmdStart(fl Flags) (int, error) {
|
||||
func cmdRun(fl Flags) (int, error) {
|
||||
caddy.TrapSignals()
|
||||
|
||||
// set up buffered logging for early startup
|
||||
// so that we can hold onto logs until after
|
||||
// the config is loaded (or fails to load)
|
||||
// so that we can write the logs to the user's
|
||||
// configured output. we must be sure to flush
|
||||
// on any error before the config is loaded.
|
||||
logger, defaultLogger, logBuffer := caddy.BufferedLog()
|
||||
|
||||
logger := caddy.Log()
|
||||
undoMaxProcs := setResourceLimits(logger)
|
||||
defer undoMaxProcs()
|
||||
// release the local reference to the undo function so it can be GC'd;
|
||||
// the deferred call above has already captured the actual function value.
|
||||
undoMaxProcs = nil //nolint:ineffassign,wastedassign
|
||||
|
||||
configFlag := fl.String("config")
|
||||
configAdapterFlag := fl.String("adapter")
|
||||
@@ -197,7 +187,6 @@ func cmdRun(fl Flags) (int, error) {
|
||||
// load all additional envs as soon as possible
|
||||
err := handleEnvFileFlag(fl)
|
||||
if err != nil {
|
||||
logBuffer.FlushTo(defaultLogger)
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
@@ -215,7 +204,6 @@ func cmdRun(fl Flags) (int, error) {
|
||||
logger.Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
|
||||
resumeFlag = false
|
||||
} else if err != nil {
|
||||
logBuffer.FlushTo(defaultLogger)
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
} else {
|
||||
if configFlag == "" {
|
||||
@@ -231,11 +219,9 @@ func cmdRun(fl Flags) (int, error) {
|
||||
}
|
||||
// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
|
||||
var configFile string
|
||||
var adapterUsed string
|
||||
if !resumeFlag {
|
||||
config, configFile, adapterUsed, err = LoadConfig(configFlag, configAdapterFlag)
|
||||
config, configFile, err = LoadConfig(configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
logBuffer.FlushTo(defaultLogger)
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
}
|
||||
@@ -250,35 +236,11 @@ func cmdRun(fl Flags) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a source config file (we're running via 'caddy run --config ...'),
|
||||
// record it so SIGUSR1 can reload from the same file. Also provide a callback
|
||||
// that knows how to load/adapt that source when requested by the main process.
|
||||
if configFile != "" {
|
||||
caddy.SetLastConfig(configFile, adapterUsed, func(file, adapter string) error {
|
||||
cfg, _, _, err := LoadConfig(file, adapter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return caddy.Load(cfg, true)
|
||||
})
|
||||
}
|
||||
|
||||
// run the initial config
|
||||
err = caddy.Load(config, true)
|
||||
if err != nil {
|
||||
logBuffer.FlushTo(defaultLogger)
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err)
|
||||
}
|
||||
// release the reference to the config so it can be GC'd
|
||||
config = nil //nolint:ineffassign,wastedassign
|
||||
|
||||
// at this stage the config will have replaced the
|
||||
// default logger to the configured one, so we can
|
||||
// log normally, now that the config is running.
|
||||
// also clear our ref to the buffer so it can get GC'd
|
||||
logger = caddy.Log()
|
||||
defaultLogger = nil //nolint:ineffassign,wastedassign
|
||||
logBuffer = nil //nolint:wastedassign,ineffassign
|
||||
logger.Info("serving initial configuration")
|
||||
|
||||
// if we are to report to another process the successful start
|
||||
@@ -294,22 +256,18 @@ func cmdRun(fl Flags) (int, error) {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("dialing confirmation address: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
_, err = conn.Write(confirmationBytes)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("writing confirmation bytes to %s: %v", pingbackFlag, err)
|
||||
}
|
||||
// close (non-defer because we `select {}` below)
|
||||
// and release references so they can be GC'd
|
||||
conn.Close()
|
||||
confirmationBytes = nil //nolint:ineffassign,wastedassign
|
||||
conn = nil //nolint:wastedassign,ineffassign
|
||||
}
|
||||
|
||||
// if enabled, reload config file automatically on changes
|
||||
// (this better only be used in dev!)
|
||||
if watchFlag {
|
||||
go watchConfigFile(configFile, adapterUsed)
|
||||
go watchConfigFile(configFile, configAdapterFlag)
|
||||
}
|
||||
|
||||
// warn if the environment does not provide enough information about the disk
|
||||
@@ -331,9 +289,6 @@ func cmdRun(fl Flags) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// release the last local logger reference
|
||||
logger = nil //nolint:wastedassign,ineffassign
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
@@ -364,7 +319,7 @@ func cmdReload(fl Flags) (int, error) {
|
||||
forceFlag := fl.Bool("force")
|
||||
|
||||
// get the config in caddy's native format
|
||||
config, configFile, adapterUsed, err := LoadConfig(configFlag, configAdapterFlag)
|
||||
config, configFile, err := LoadConfig(configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
@@ -382,10 +337,6 @@ func cmdReload(fl Flags) (int, error) {
|
||||
if forceFlag {
|
||||
headers.Set("Cache-Control", "must-revalidate")
|
||||
}
|
||||
// Provide the source file/adapter to the running process so it can
|
||||
// preserve its last-config knowledge if this reload came from the same source.
|
||||
headers.Set("Caddy-Config-Source-File", configFile)
|
||||
headers.Set("Caddy-Config-Source-Adapter", adapterUsed)
|
||||
|
||||
resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
|
||||
if err != nil {
|
||||
@@ -411,65 +362,11 @@ func cmdBuildInfo(_ Flags) (int, error) {
|
||||
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) {
|
||||
packages := fl.Bool("packages")
|
||||
versions := fl.Bool("versions")
|
||||
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) {
|
||||
fmt.Print(mi.caddyModuleID)
|
||||
if versions && mi.goModule != nil {
|
||||
@@ -487,6 +384,16 @@ func cmdListModules(fl Flags) (int, error) {
|
||||
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)
|
||||
if !skipStandard {
|
||||
if len(standard) > 0 {
|
||||
@@ -505,8 +412,8 @@ func cmdListModules(fl Flags) (int, error) {
|
||||
for _, mod := range nonstandard {
|
||||
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)
|
||||
if len(unknown) > 0 {
|
||||
@@ -516,8 +423,8 @@ func cmdListModules(fl Flags) (int, error) {
|
||||
for _, mod := range unknown {
|
||||
printModuleInfo(mod)
|
||||
}
|
||||
fmt.Printf("\n Unknown modules: %d\n", len(unknown))
|
||||
}
|
||||
fmt.Printf("\n Unknown modules: %d\n", len(unknown))
|
||||
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
@@ -534,20 +441,16 @@ func cmdEnviron(fl Flags) (int, error) {
|
||||
}
|
||||
|
||||
func cmdAdaptConfig(fl Flags) (int, error) {
|
||||
configFlag := fl.String("config")
|
||||
inputFlag := fl.String("config")
|
||||
adapterFlag := fl.String("adapter")
|
||||
prettyFlag := fl.Bool("pretty")
|
||||
validateFlag := fl.Bool("validate")
|
||||
|
||||
var err error
|
||||
configFlag, err = configFileWithRespectToDefault(caddy.Log(), configFlag)
|
||||
inputFlag, err = configFileWithRespectToDefault(caddy.Log(), inputFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
if configFlag == "" {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
|
||||
}
|
||||
|
||||
// load all additional envs as soon as possible
|
||||
err = handleEnvFileFlag(fl)
|
||||
@@ -566,19 +469,13 @@ func cmdAdaptConfig(fl Flags) (int, error) {
|
||||
fmt.Errorf("unrecognized config adapter: %s", adapterFlag)
|
||||
}
|
||||
|
||||
var input []byte
|
||||
// read from stdin if the file name is "-"
|
||||
if configFlag == "-" {
|
||||
input, err = io.ReadAll(os.Stdin)
|
||||
} else {
|
||||
input, err = os.ReadFile(configFlag)
|
||||
}
|
||||
input, err := os.ReadFile(inputFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("reading input file: %v", err)
|
||||
}
|
||||
|
||||
opts := map[string]any{"filename": configFlag}
|
||||
opts := map[string]any{"filename": inputFlag}
|
||||
|
||||
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
|
||||
if err != nil {
|
||||
@@ -644,7 +541,7 @@ func cmdValidateConfig(fl Flags) (int, error) {
|
||||
fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
|
||||
}
|
||||
|
||||
input, _, _, err := LoadConfig(configFlag, adapterFlag)
|
||||
input, _, err := LoadConfig(configFlag, adapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
@@ -859,7 +756,7 @@ func DetermineAdminAPIAddress(address string, config []byte, configFile, configA
|
||||
loadedConfig := config
|
||||
if len(loadedConfig) == 0 {
|
||||
// get the config in caddy's native format
|
||||
loadedConfig, loadedConfigFile, _, err = LoadConfig(configFile, configAdapter)
|
||||
loadedConfig, loadedConfigFile, err = LoadConfig(configFile, configAdapter)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
+2
-5
@@ -229,13 +229,12 @@ documentation: https://go.dev/doc/modules/version-numbers
|
||||
|
||||
RegisterCommand(Command{
|
||||
Name: "list-modules",
|
||||
Usage: "[--packages] [--versions] [--skip-standard] [--json]",
|
||||
Usage: "[--packages] [--versions] [--skip-standard]",
|
||||
Short: "Lists the installed Caddy modules",
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().BoolP("packages", "", false, "Print package paths")
|
||||
cmd.Flags().BoolP("versions", "", false, "Print version information")
|
||||
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)
|
||||
},
|
||||
})
|
||||
@@ -294,8 +293,6 @@ zero exit status will be returned.
|
||||
|
||||
If --envfile is specified, an environment file with environment variables
|
||||
in the KEY=VALUE format will be loaded into the Caddy process.
|
||||
|
||||
If you wish to use stdin instead of a regular file, use - as the path.
|
||||
`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("config", "c", "", "Configuration file to adapt (required)")
|
||||
@@ -393,7 +390,7 @@ lines will be prefixed with '-' and '+' where they differ. Note that
|
||||
unchanged lines are prefixed with two spaces for alignment, and that this
|
||||
is not a valid patch format.
|
||||
|
||||
If you wish to use stdin instead of a regular file, use - as the path.
|
||||
If you wish you use stdin instead of a regular file, use - as the path.
|
||||
When reading from stdin, the --overwrite flag has no effect: the result
|
||||
is always printed to stdout.
|
||||
`,
|
||||
|
||||
+13
-21
@@ -100,12 +100,7 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
|
||||
// there is no config available. It prints any warnings to stderr,
|
||||
// and returns the resulting JSON config bytes along with
|
||||
// the name of the loaded config file (if any).
|
||||
// The return values are:
|
||||
// - config bytes (nil if no config)
|
||||
// - config file used ("" if none)
|
||||
// - adapter used ("" if none)
|
||||
// - error, if any
|
||||
func LoadConfig(configFile, adapterName string) ([]byte, string, string, error) {
|
||||
func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
|
||||
return loadConfigWithLogger(caddy.Log(), configFile, adapterName)
|
||||
}
|
||||
|
||||
@@ -143,7 +138,7 @@ func isCaddyfile(configFile, adapterName string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, string, error) {
|
||||
func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, error) {
|
||||
// if no logger is provided, use a nop logger
|
||||
// just so we don't have to check for nil
|
||||
if logger == nil {
|
||||
@@ -152,7 +147,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
|
||||
// specifying an adapter without a config file is ambiguous
|
||||
if adapterName != "" && configFile == "" {
|
||||
return nil, "", "", fmt.Errorf("cannot adapt config without config file (use --config)")
|
||||
return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)")
|
||||
}
|
||||
|
||||
// load initial config and adapter
|
||||
@@ -163,13 +158,13 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
if configFile == "-" {
|
||||
config, err = io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, "", "", fmt.Errorf("reading config from stdin: %v", err)
|
||||
return nil, "", fmt.Errorf("reading config from stdin: %v", err)
|
||||
}
|
||||
logger.Info("using config from stdin")
|
||||
} else {
|
||||
config, err = os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return nil, "", "", fmt.Errorf("reading config from file: %v", err)
|
||||
return nil, "", fmt.Errorf("reading config from file: %v", err)
|
||||
}
|
||||
logger.Info("using config from file", zap.String("file", configFile))
|
||||
}
|
||||
@@ -184,7 +179,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
cfgAdapter = nil
|
||||
} else if err != nil {
|
||||
// default Caddyfile exists, but error reading it
|
||||
return nil, "", "", fmt.Errorf("reading default Caddyfile: %v", err)
|
||||
return nil, "", fmt.Errorf("reading default Caddyfile: %v", err)
|
||||
} else {
|
||||
// success reading default Caddyfile
|
||||
configFile = "Caddyfile"
|
||||
@@ -196,14 +191,14 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
if yes, err := isCaddyfile(configFile, adapterName); yes {
|
||||
adapterName = "caddyfile"
|
||||
} else if err != nil {
|
||||
return nil, "", "", err
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// load config adapter
|
||||
if adapterName != "" {
|
||||
cfgAdapter = caddyconfig.GetAdapter(adapterName)
|
||||
if cfgAdapter == nil {
|
||||
return nil, "", "", fmt.Errorf("unrecognized config adapter: %s", adapterName)
|
||||
return nil, "", fmt.Errorf("unrecognized config adapter: %s", adapterName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +208,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
"filename": configFile,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, "", "", fmt.Errorf("adapting config using %s: %v", adapterName, err)
|
||||
return nil, "", fmt.Errorf("adapting config using %s: %v", adapterName, err)
|
||||
}
|
||||
logger.Info("adapted config to JSON", zap.String("adapter", adapterName))
|
||||
for _, warn := range warnings {
|
||||
@@ -231,14 +226,11 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
// validate that the config is at least valid JSON
|
||||
err = json.Unmarshal(config, new(any))
|
||||
if err != nil {
|
||||
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)
|
||||
return nil, "", fmt.Errorf("config is not valid JSON: %v; did you mean to use a config adapter (the --adapter flag)?", err)
|
||||
}
|
||||
}
|
||||
|
||||
return config, configFile, adapterName, nil
|
||||
return config, configFile, nil
|
||||
}
|
||||
|
||||
// watchConfigFile watches the config file at filename for changes
|
||||
@@ -264,7 +256,7 @@ func watchConfigFile(filename, adapterName string) {
|
||||
}
|
||||
|
||||
// get current config
|
||||
lastCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
lastCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
if err != nil {
|
||||
logger().Error("unable to load latest config", zap.Error(err))
|
||||
return
|
||||
@@ -276,7 +268,7 @@ func watchConfigFile(filename, adapterName string) {
|
||||
//nolint:staticcheck
|
||||
for range time.Tick(1 * time.Second) {
|
||||
// get current config
|
||||
newCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
newCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
if err != nil {
|
||||
logger().Error("unable to load latest config", zap.Error(err))
|
||||
return
|
||||
|
||||
@@ -62,7 +62,7 @@ func splitModule(arg string) (module, version string, err error) {
|
||||
err = fmt.Errorf("module name is required")
|
||||
}
|
||||
|
||||
return module, version, err
|
||||
return
|
||||
}
|
||||
|
||||
func cmdAddPackage(fl Flags) (int, error) {
|
||||
@@ -217,7 +217,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
err = fmt.Errorf("no build info")
|
||||
return standard, nonstandard, unknown, err
|
||||
return
|
||||
}
|
||||
|
||||
for _, modID := range caddy.Modules() {
|
||||
@@ -260,7 +260,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
|
||||
nonstandard = append(nonstandard, caddyModGoMod)
|
||||
}
|
||||
}
|
||||
return standard, nonstandard, unknown, err
|
||||
return
|
||||
}
|
||||
|
||||
func listModules(path string) error {
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ type storVal struct {
|
||||
// determineStorage returns the top-level storage module from the given config.
|
||||
// It may return nil even if no error.
|
||||
func determineStorage(configFile string, configAdapter string) (*storVal, error) {
|
||||
cfg, _, _, err := LoadConfig(configFile, configAdapter)
|
||||
cfg, _, err := LoadConfig(configFile, configAdapter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+7
-42
@@ -21,14 +21,12 @@ import (
|
||||
"log"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/exp/zapslog"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/internal/filesystems"
|
||||
)
|
||||
@@ -585,57 +583,24 @@ func (ctx Context) Logger(module ...Module) *zap.Logger {
|
||||
return ctx.cfg.Logging.Logger(mod)
|
||||
}
|
||||
|
||||
type slogHandlerFactory func(handler slog.Handler, core zapcore.Core, moduleID string) slog.Handler
|
||||
|
||||
var (
|
||||
slogHandlerFactories []slogHandlerFactory
|
||||
slogHandlerFactoriesMu sync.RWMutex
|
||||
)
|
||||
|
||||
// RegisterSlogHandlerFactory allows modules to register custom log/slog.Handler,
|
||||
// for instance, to add contextual data to the logs.
|
||||
func RegisterSlogHandlerFactory(factory slogHandlerFactory) {
|
||||
slogHandlerFactoriesMu.Lock()
|
||||
slogHandlerFactories = append(slogHandlerFactories, factory)
|
||||
slogHandlerFactoriesMu.Unlock()
|
||||
}
|
||||
|
||||
// Slogger returns a slog logger that is intended for use by
|
||||
// the most recent module associated with the context.
|
||||
func (ctx Context) Slogger() *slog.Logger {
|
||||
var (
|
||||
handler slog.Handler
|
||||
core zapcore.Core
|
||||
moduleID string
|
||||
)
|
||||
if ctx.cfg == nil {
|
||||
// often the case in tests; just use a dev logger
|
||||
l, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
panic("config missing, unable to create dev logger: " + err.Error())
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
return slog.New(zapslog.NewHandler(l.Core()))
|
||||
}
|
||||
|
||||
slogHandlerFactoriesMu.RLock()
|
||||
for _, f := range slogHandlerFactories {
|
||||
handler = f(handler, core, moduleID)
|
||||
mod := ctx.Module()
|
||||
if mod == nil {
|
||||
return slog.New(zapslog.NewHandler(Log().Core()))
|
||||
}
|
||||
slogHandlerFactoriesMu.RUnlock()
|
||||
|
||||
return slog.New(handler)
|
||||
return slog.New(zapslog.NewHandler(ctx.cfg.Logging.Logger(mod).Core(),
|
||||
zapslog.WithName(string(mod.CaddyModule().ID)),
|
||||
))
|
||||
}
|
||||
|
||||
// Modules returns the lineage of modules that this context provisioned,
|
||||
|
||||
@@ -1,178 +1,158 @@
|
||||
module github.com/caddyserver/caddy/v2
|
||||
|
||||
go 1.25
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.6.0
|
||||
github.com/DeRuina/timberjack v1.3.9
|
||||
github.com/KimMachineGun/automemlimit v0.7.5
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/KimMachineGun/automemlimit v0.7.1
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/alecthomas/chroma/v2 v2.21.1
|
||||
github.com/alecthomas/chroma/v2 v2.19.0
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
|
||||
github.com/caddyserver/certmagic v0.25.1
|
||||
github.com/caddyserver/zerossl v0.1.4
|
||||
github.com/cloudflare/circl v1.6.2
|
||||
github.com/caddyserver/certmagic v0.23.0
|
||||
github.com/caddyserver/zerossl v0.1.3
|
||||
github.com/cloudflare/circl v1.6.1
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/google/cel-go v0.26.1
|
||||
github.com/go-chi/chi/v5 v5.2.2
|
||||
github.com/google/cel-go v0.24.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/klauspost/compress v1.18.2
|
||||
github.com/klauspost/compress v1.18.0
|
||||
github.com/klauspost/cpuid/v2 v2.3.0
|
||||
github.com/mholt/acmez/v3 v3.1.4
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/quic-go/quic-go v0.59.0
|
||||
github.com/smallstep/certificates v0.29.0
|
||||
github.com/smallstep/nosql v0.7.0
|
||||
github.com/mholt/acmez/v3 v3.1.2
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/quic-go/quic-go v0.54.0
|
||||
github.com/smallstep/certificates v0.26.1
|
||||
github.com/smallstep/nosql v0.6.1
|
||||
github.com/smallstep/truststore v0.13.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747
|
||||
github.com/yuin/goldmark v1.7.15
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.7
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53
|
||||
github.com/yuin/goldmark v1.7.8
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.64.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.64.0
|
||||
go.opentelemetry.io/otel v1.39.0
|
||||
go.opentelemetry.io/otel/sdk v1.39.0
|
||||
go.step.sm/crypto v0.75.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.42.0
|
||||
go.opentelemetry.io/otel v1.31.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0
|
||||
go.opentelemetry.io/otel/sdk v1.31.0
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
go.uber.org/zap v1.27.1
|
||||
go.uber.org/zap v1.27.0
|
||||
go.uber.org/zap/exp v0.3.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99
|
||||
golang.org/x/net v0.48.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/term v0.38.0
|
||||
golang.org/x/time v0.14.0
|
||||
golang.org/x/crypto v0.40.0
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810
|
||||
golang.org/x/net v0.42.0
|
||||
golang.org/x/sync v0.16.0
|
||||
golang.org/x/term v0.33.0
|
||||
golang.org/x/time v0.12.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cel.dev/expr v0.19.1 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
|
||||
github.com/google/go-tpm v0.9.7 // indirect
|
||||
github.com/google/go-tpm v0.9.0 // indirect
|
||||
github.com/google/go-tspi v0.3.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.2 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // 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/linkedca v0.25.0 // indirect
|
||||
github.com/smallstep/pkcs7 v0.2.1 // indirect
|
||||
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 // indirect
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 // indirect
|
||||
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect
|
||||
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.64.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.15.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.15.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||
golang.org/x/oauth2 v0.33.0 // indirect
|
||||
google.golang.org/api v0.256.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.17.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||
github.com/dgraph-io/badger v1.6.2 // indirect
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||
github.com/dgraph-io/ristretto v0.2.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-kit/kit v0.13.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.14.3 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/libdns/libdns v1.1.1
|
||||
github.com/jackc/pgtype v1.14.0 // indirect
|
||||
github.com/jackc/pgx/v4 v4.18.3 // indirect
|
||||
github.com/libdns/libdns v1.0.0-beta.1
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/miekg/dns v1.1.69 // indirect
|
||||
github.com/miekg/dns v1.1.63 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1
|
||||
github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
github.com/prometheus/common v0.67.4 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/rosedblabs/wal v1.3.6
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/slackhq/nebula v1.9.7 // indirect
|
||||
github.com/slackhq/nebula v1.6.1 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||
github.com/urfave/cli v1.22.17 // indirect
|
||||
go.etcd.io/bbolt v1.3.10 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
github.com/urfave/cli v1.22.14 // indirect
|
||||
go.etcd.io/bbolt v1.3.9 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.31.0
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
go.step.sm/cli-utils v0.9.0 // indirect
|
||||
go.step.sm/crypto v0.45.0
|
||||
go.step.sm/linkedca v0.20.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/sys v0.39.0
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
google.golang.org/grpc v1.77.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/sys v0.34.0
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
google.golang.org/grpc v1.67.1 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// LogBufferCore is a zapcore.Core that buffers log entries in memory.
|
||||
type LogBufferCore struct {
|
||||
mu sync.Mutex
|
||||
entries []zapcore.Entry
|
||||
fields [][]zapcore.Field
|
||||
level zapcore.LevelEnabler
|
||||
}
|
||||
|
||||
type LogBufferCoreInterface interface {
|
||||
zapcore.Core
|
||||
FlushTo(*zap.Logger)
|
||||
}
|
||||
|
||||
func NewLogBufferCore(level zapcore.LevelEnabler) *LogBufferCore {
|
||||
return &LogBufferCore{
|
||||
level: level,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LogBufferCore) Enabled(lvl zapcore.Level) bool {
|
||||
return c.level.Enabled(lvl)
|
||||
}
|
||||
|
||||
func (c *LogBufferCore) With(fields []zapcore.Field) zapcore.Core {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *LogBufferCore) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
||||
if c.Enabled(entry.Level) {
|
||||
return ce.AddCore(entry, c)
|
||||
}
|
||||
return ce
|
||||
}
|
||||
|
||||
func (c *LogBufferCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.entries = append(c.entries, entry)
|
||||
c.fields = append(c.fields, fields)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *LogBufferCore) Sync() error { return nil }
|
||||
|
||||
// FlushTo flushes buffered logs to the given zap.Logger.
|
||||
func (c *LogBufferCore) FlushTo(logger *zap.Logger) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
for idx, entry := range c.entries {
|
||||
logger.WithOptions().Check(entry.Level, entry.Message).Write(c.fields[idx]...)
|
||||
}
|
||||
c.entries = nil
|
||||
c.fields = nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ zapcore.Core = (*LogBufferCore)(nil)
|
||||
_ LogBufferCoreInterface = (*LogBufferCore)(nil)
|
||||
)
|
||||
@@ -261,14 +261,14 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e
|
||||
if atomic.LoadInt32(&fcpc.closed) == 1 {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
if err = fcpc.SetReadDeadline(time.Time{}); err != nil {
|
||||
return n, addr, err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return n, addr, err
|
||||
return
|
||||
}
|
||||
|
||||
return n, addr, err
|
||||
return
|
||||
}
|
||||
|
||||
// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.
|
||||
|
||||
+12
-114
@@ -31,17 +31,13 @@ import (
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
h3qlog "github.com/quic-go/quic-go/http3/qlog"
|
||||
"github.com/quic-go/quic-go/qlog"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/internal"
|
||||
)
|
||||
|
||||
// listenFdsStart is the first file descriptor number for systemd socket activation.
|
||||
// File descriptors 0, 1, 2 are reserved for stdin, stdout, stderr.
|
||||
const listenFdsStart = 3
|
||||
|
||||
// NetworkAddress represents one or more network addresses.
|
||||
// It contains the individual components for a parsed network
|
||||
// address of the form accepted by ParseNetworkAddress().
|
||||
@@ -309,64 +305,6 @@ func IsFdNetwork(netw string) bool {
|
||||
return strings.HasPrefix(netw, "fd")
|
||||
}
|
||||
|
||||
// getFdByName returns the file descriptor number for the given
|
||||
// socket name from systemd's LISTEN_FDNAMES environment variable.
|
||||
// Socket names are provided by systemd via socket activation.
|
||||
//
|
||||
// The name can optionally include an index to handle multiple sockets
|
||||
// with the same name: "web:0" for first, "web:1" for second, etc.
|
||||
// If no index is specified, defaults to index 0 (first occurrence).
|
||||
func getFdByName(nameWithIndex string) (int, error) {
|
||||
if nameWithIndex == "" {
|
||||
return 0, fmt.Errorf("socket name cannot be empty")
|
||||
}
|
||||
|
||||
fdNamesStr := os.Getenv("LISTEN_FDNAMES")
|
||||
if fdNamesStr == "" {
|
||||
return 0, fmt.Errorf("LISTEN_FDNAMES environment variable not set")
|
||||
}
|
||||
|
||||
// Parse name and optional index
|
||||
parts := strings.Split(nameWithIndex, ":")
|
||||
if len(parts) > 2 {
|
||||
return 0, fmt.Errorf("invalid socket name format '%s': too many colons", nameWithIndex)
|
||||
}
|
||||
|
||||
name := parts[0]
|
||||
targetIndex := 0
|
||||
|
||||
if len(parts) > 1 {
|
||||
var err error
|
||||
targetIndex, err = strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid socket index '%s': %v", parts[1], err)
|
||||
}
|
||||
if targetIndex < 0 {
|
||||
return 0, fmt.Errorf("socket index cannot be negative: %d", targetIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the socket names
|
||||
names := strings.Split(fdNamesStr, ":")
|
||||
|
||||
// Find the Nth occurrence of the requested name
|
||||
matchCount := 0
|
||||
for i, fdName := range names {
|
||||
if fdName == name {
|
||||
if matchCount == targetIndex {
|
||||
return listenFdsStart + i, nil
|
||||
}
|
||||
matchCount++
|
||||
}
|
||||
}
|
||||
|
||||
if matchCount == 0 {
|
||||
return 0, fmt.Errorf("socket name '%s' not found in LISTEN_FDNAMES", name)
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("socket name '%s' found %d times, but index %d requested", name, matchCount, targetIndex)
|
||||
}
|
||||
|
||||
// ParseNetworkAddress parses addr into its individual
|
||||
// components. The input string is expected to be of
|
||||
// the form "network/host:port-range" where any part is
|
||||
@@ -398,27 +336,9 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
|
||||
}, err
|
||||
}
|
||||
if IsFdNetwork(network) {
|
||||
fdAddr := host
|
||||
|
||||
// Handle named socket activation (fdname/name, fdgramname/name)
|
||||
if strings.HasPrefix(network, "fdname") || strings.HasPrefix(network, "fdgramname") {
|
||||
fdNum, err := getFdByName(host)
|
||||
if err != nil {
|
||||
return NetworkAddress{}, fmt.Errorf("named socket activation: %v", err)
|
||||
}
|
||||
fdAddr = strconv.Itoa(fdNum)
|
||||
|
||||
// Normalize network to standard fd/fdgram
|
||||
if strings.HasPrefix(network, "fdname") {
|
||||
network = "fd"
|
||||
} else {
|
||||
network = "fdgram"
|
||||
}
|
||||
}
|
||||
|
||||
return NetworkAddress{
|
||||
Network: network,
|
||||
Host: fdAddr,
|
||||
Host: host,
|
||||
}, nil
|
||||
}
|
||||
var start, end uint64
|
||||
@@ -462,7 +382,7 @@ func SplitNetworkAddress(a string) (network, host, port string, err error) {
|
||||
a = afterSlash
|
||||
if IsUnixNetwork(network) || IsFdNetwork(network) {
|
||||
host = a
|
||||
return network, host, port, err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,7 +402,7 @@ func SplitNetworkAddress(a string) (network, host, port string, err error) {
|
||||
err = errors.Join(firstErr, err)
|
||||
}
|
||||
|
||||
return network, host, port, err
|
||||
return
|
||||
}
|
||||
|
||||
// JoinNetworkAddress combines network, host, and port into a single
|
||||
@@ -510,8 +430,7 @@ func JoinNetworkAddress(network, host, port string) string {
|
||||
// address instead.
|
||||
//
|
||||
// NOTE: This API is EXPERIMENTAL and may be changed or removed.
|
||||
// NOTE: user should close the returned listener twice, once to stop accepting new connections, the second time to free up the packet conn.
|
||||
func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config, pcWrappers []PacketConnWrapper) (http3.QUICListener, error) {
|
||||
func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config) (http3.QUICListener, error) {
|
||||
lnKey := listenerKey("quic"+na.Network, na.JoinHostPort(portOffset))
|
||||
|
||||
sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
||||
@@ -523,19 +442,12 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config
|
||||
ln := lnAny.(net.PacketConn)
|
||||
|
||||
h3ln := ln
|
||||
if len(pcWrappers) == 0 {
|
||||
for {
|
||||
// retrieve the underlying socket, so quic-go can optimize.
|
||||
if unwrapper, ok := h3ln.(interface{ Unwrap() net.PacketConn }); ok {
|
||||
h3ln = unwrapper.Unwrap()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// wrap packet conn before QUIC
|
||||
for _, pcWrapper := range pcWrappers {
|
||||
h3ln = pcWrapper.WrapPacketConn(h3ln)
|
||||
for {
|
||||
// retrieve the underlying socket, so quic-go can optimize.
|
||||
if unwrapper, ok := h3ln.(interface{ Unwrap() net.PacketConn }); ok {
|
||||
h3ln = unwrapper.Unwrap()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,7 +466,7 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config
|
||||
http3.ConfigureTLSConfig(quicTlsConfig),
|
||||
&quic.Config{
|
||||
Allow0RTT: true,
|
||||
Tracer: h3qlog.DefaultConnectionTracer,
|
||||
Tracer: qlog.DefaultConnectionTracer,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -714,7 +626,6 @@ func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error)
|
||||
func (fcql *fakeCloseQuicListener) Close() error {
|
||||
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
|
||||
fcql.contextCancel()
|
||||
} else if atomic.CompareAndSwapInt32(&fcql.closed, 1, 2) {
|
||||
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
|
||||
}
|
||||
return nil
|
||||
@@ -782,19 +693,6 @@ type ListenerWrapper interface {
|
||||
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.
|
||||
var listenerPool = NewUsagePool()
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-24
@@ -28,8 +28,6 @@ import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/internal"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -192,13 +190,6 @@ func (logging *Logging) setupNewDefault(ctx Context) error {
|
||||
)
|
||||
}
|
||||
|
||||
// if we had a buffered core, flush its contents ASAP
|
||||
// before we try to log anything else, so the order of
|
||||
// logs is preserved
|
||||
if oldBufferCore, ok := oldDefault.logger.Core().(*internal.LogBufferCore); ok {
|
||||
oldBufferCore.FlushTo(newDefault.logger)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -782,21 +773,6 @@ func Log() *zap.Logger {
|
||||
return defaultLogger.logger
|
||||
}
|
||||
|
||||
// BufferedLog sets the default logger to one that buffers
|
||||
// logs before a config is loaded.
|
||||
// Returns the buffered logger, the original default logger
|
||||
// (for flushing on errors), and the buffer core so that the
|
||||
// caller can flush the logs after the config is loaded or
|
||||
// fails to load.
|
||||
func BufferedLog() (*zap.Logger, *zap.Logger, *internal.LogBufferCore) {
|
||||
defaultLoggerMu.Lock()
|
||||
defer defaultLoggerMu.Unlock()
|
||||
origLogger := defaultLogger.logger
|
||||
bufferCore := internal.NewLogBufferCore(zap.InfoLevel)
|
||||
defaultLogger.logger = zap.New(bufferCore)
|
||||
return defaultLogger.logger, origLogger, bufferCore
|
||||
}
|
||||
|
||||
var (
|
||||
coloringEnabled = os.Getenv("NO_COLOR") == "" && os.Getenv("TERM") != "xterm-mono"
|
||||
defaultLogger, _ = newDefaultProductionLog()
|
||||
|
||||
+2
-8
@@ -342,18 +342,12 @@ func ParseStructTag(tag string) (map[string]string, error) {
|
||||
func StrictUnmarshalJSON(data []byte, v any) error {
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.DisallowUnknownFields()
|
||||
err := dec.Decode(v)
|
||||
if jsonErr, ok := err.(*json.SyntaxError); ok {
|
||||
return fmt.Errorf("%w, at offset %d", jsonErr, jsonErr.Offset)
|
||||
}
|
||||
return err
|
||||
return dec.Decode(v)
|
||||
}
|
||||
|
||||
var JSONRawMessageType = reflect.TypeFor[json.RawMessage]()
|
||||
|
||||
// isJSONRawMessage returns true if the type is encoding/json.RawMessage.
|
||||
func isJSONRawMessage(typ reflect.Type) bool {
|
||||
return typ == JSONRawMessageType
|
||||
return typ.PkgPath() == "encoding/json" && typ.Name() == "RawMessage"
|
||||
}
|
||||
|
||||
// isModuleMapType returns true if the type is map[string]json.RawMessage.
|
||||
|
||||
+76
-122
@@ -28,6 +28,7 @@ import (
|
||||
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyevents"
|
||||
@@ -51,7 +52,6 @@ func init() {
|
||||
// Placeholder | Description
|
||||
// ------------|---------------
|
||||
// `{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.duration}` | Time up to now spent handling the request (after decoding headers from client)
|
||||
// `{http.request.duration_ms}` | Same as 'duration', but in milliseconds.
|
||||
@@ -83,7 +83,6 @@ func init() {
|
||||
// `{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.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.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.
|
||||
@@ -152,11 +151,6 @@ type App struct {
|
||||
logger *zap.Logger
|
||||
tlsApp *caddytls.TLS
|
||||
|
||||
// stopped indicates whether the app has stopped
|
||||
// It can only happen if it has started successfully in the first place.
|
||||
// Otherwise, Cleanup will call Stop to clean up resources.
|
||||
stopped bool
|
||||
|
||||
// used temporarily between phases 1 and 2 of auto HTTPS
|
||||
allCertDomains map[string]struct{}
|
||||
}
|
||||
@@ -172,15 +166,13 @@ func (App) CaddyModule() caddy.ModuleInfo {
|
||||
// Provision sets up the app.
|
||||
func (app *App) Provision(ctx caddy.Context) error {
|
||||
// store some references
|
||||
app.logger = ctx.Logger()
|
||||
app.ctx = ctx
|
||||
|
||||
// provision TLS and events apps
|
||||
tlsAppIface, err := ctx.App("tls")
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting tls app: %v", err)
|
||||
}
|
||||
app.tlsApp = tlsAppIface.(*caddytls.TLS)
|
||||
app.ctx = ctx
|
||||
app.logger = ctx.Logger()
|
||||
|
||||
eventsAppIface, err := ctx.App("events")
|
||||
if err != nil {
|
||||
@@ -200,8 +192,6 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||
if app.Metrics != nil {
|
||||
app.Metrics.init = sync.Once{}
|
||||
app.Metrics.httpMetrics = &httpMetrics{}
|
||||
// Scan config for allowed hosts to prevent cardinality explosion
|
||||
app.Metrics.scanConfigForHosts(app)
|
||||
}
|
||||
// prepare each server
|
||||
oldContext := ctx.Context
|
||||
@@ -241,6 +231,15 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||
for _, srvProtocol := range srv.Protocols {
|
||||
srvProtocolsUnique[srvProtocol] = struct{}{}
|
||||
}
|
||||
_, h1ok := srvProtocolsUnique["h1"]
|
||||
_, h2ok := srvProtocolsUnique["h2"]
|
||||
_, h2cok := srvProtocolsUnique["h2c"]
|
||||
|
||||
// the Go standard library does not let us serve only HTTP/2 using
|
||||
// http.Server; we would probably need to write our own server
|
||||
if !h1ok && (h2ok || h2cok) {
|
||||
return fmt.Errorf("server %s: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName)
|
||||
}
|
||||
|
||||
if srv.ListenProtocols != nil {
|
||||
if len(srv.ListenProtocols) != len(srv.Listen) {
|
||||
@@ -274,6 +273,19 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
lnProtocolsIncludeUnique := map[string]struct{}{}
|
||||
for _, lnProtocol := range lnProtocolsInclude {
|
||||
lnProtocolsIncludeUnique[lnProtocol] = struct{}{}
|
||||
}
|
||||
_, h1ok := lnProtocolsIncludeUnique["h1"]
|
||||
_, h2ok := lnProtocolsIncludeUnique["h2"]
|
||||
_, h2cok := lnProtocolsIncludeUnique["h2c"]
|
||||
|
||||
// check if any listener protocols contain h2 or h2c without h1
|
||||
if !h1ok && (h2ok || h2cok) {
|
||||
return fmt.Errorf("server %s, listener %d: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName, i)
|
||||
}
|
||||
|
||||
srv.ListenProtocols[i] = lnProtocolsInclude
|
||||
}
|
||||
}
|
||||
@@ -348,20 +360,6 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||
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
|
||||
// route handler so that important security checks are done, etc.
|
||||
primaryRoute := emptyHandler
|
||||
@@ -445,25 +443,6 @@ func (app *App) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeTLSALPN(srv *Server, target string) {
|
||||
for _, cp := range srv.TLSConnPolicies {
|
||||
// the TLSConfig was already provisioned, so... manually remove it
|
||||
for i, np := range cp.TLSConfig.NextProtos {
|
||||
if np == target {
|
||||
cp.TLSConfig.NextProtos = append(cp.TLSConfig.NextProtos[:i], cp.TLSConfig.NextProtos[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
// remove it from the parent connection policy too, just to keep things tidy
|
||||
for i, alpn := range cp.ALPN {
|
||||
if alpn == target {
|
||||
cp.ALPN = append(cp.ALPN[:i], cp.ALPN[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start runs the app. It finishes automatic HTTPS if enabled,
|
||||
// including management of certificates.
|
||||
func (app *App) Start() error {
|
||||
@@ -482,44 +461,32 @@ func (app *App) Start() error {
|
||||
MaxHeaderBytes: srv.MaxHeaderBytes,
|
||||
Handler: srv,
|
||||
ErrorLog: serverLogger,
|
||||
Protocols: new(http.Protocols),
|
||||
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
|
||||
if nc, ok := c.(interface{ tlsNetConn() net.Conn }); ok {
|
||||
getTlsConStateFunc := sync.OnceValue(func() *tls.ConnectionState {
|
||||
tlsConnState := nc.tlsNetConn().(connectionStater).ConnectionState()
|
||||
return &tlsConnState
|
||||
})
|
||||
ctx = context.WithValue(ctx, tlsConnectionStateFuncCtxKey, getTlsConStateFunc)
|
||||
}
|
||||
return ctx
|
||||
return context.WithValue(ctx, ConnCtxKey, c)
|
||||
},
|
||||
}
|
||||
h2server := new(http2.Server)
|
||||
|
||||
// disable HTTP/2, which we enabled by default during provisioning
|
||||
if !srv.protocol("h2") {
|
||||
srv.server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
|
||||
removeTLSALPN(srv, "h2")
|
||||
}
|
||||
if !srv.protocol("h1") {
|
||||
removeTLSALPN(srv, "http/1.1")
|
||||
}
|
||||
|
||||
// configure the http versions the server will serve
|
||||
if srv.protocol("h1") {
|
||||
srv.server.Protocols.SetHTTP1(true)
|
||||
}
|
||||
|
||||
if srv.protocol("h2") || srv.protocol("h2c") {
|
||||
// skip setting h2 because if NextProtos is present, it's list of alpn versions will take precedence.
|
||||
// it will always be present because http2.ConfigureServer will populate that field
|
||||
// enabling h2c because some listener wrapper will wrap the connection that is no longer *tls.Conn
|
||||
// However, we need to handle the case that if the connection is h2c but h2c is not enabled. We identify
|
||||
// this type of connection by checking if it's behind a TLS listener wrapper or if it implements tls.ConnectionState.
|
||||
srv.server.Protocols.SetUnencryptedHTTP2(true)
|
||||
// when h2c is enabled but h2 disabled, we already removed h2 from NextProtos
|
||||
// the handshake will never succeed with h2
|
||||
// http2.ConfigureServer will enable the server to handle both h2 and h2c
|
||||
h2server := new(http2.Server)
|
||||
for _, cp := range srv.TLSConnPolicies {
|
||||
// the TLSConfig was already provisioned, so... manually remove it
|
||||
for i, np := range cp.TLSConfig.NextProtos {
|
||||
if np == "h2" {
|
||||
cp.TLSConfig.NextProtos = append(cp.TLSConfig.NextProtos[:i], cp.TLSConfig.NextProtos[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
// remove it from the parent connection policy too, just to keep things tidy
|
||||
for i, alpn := range cp.ALPN {
|
||||
if alpn == "h2" {
|
||||
cp.ALPN = append(cp.ALPN[:i], cp.ALPN[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//nolint:errcheck
|
||||
http2.ConfigureServer(srv.server, h2server)
|
||||
}
|
||||
@@ -529,6 +496,11 @@ func (app *App) Start() error {
|
||||
tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx)
|
||||
srv.configureServer(srv.server)
|
||||
|
||||
// enable H2C if configured
|
||||
if srv.protocol("h2c") {
|
||||
srv.server.Handler = h2c.NewHandler(srv, h2server)
|
||||
}
|
||||
|
||||
for lnIndex, lnAddr := range srv.Listen {
|
||||
listenAddr, err := caddy.ParseNetworkAddress(lnAddr)
|
||||
if err != nil {
|
||||
@@ -561,10 +533,8 @@ func (app *App) Start() error {
|
||||
// create the listener for this socket
|
||||
lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{
|
||||
KeepAliveConfig: net.KeepAliveConfig{
|
||||
Enable: srv.KeepAliveInterval >= 0,
|
||||
Enable: srv.KeepAliveInterval != 0,
|
||||
Interval: time.Duration(srv.KeepAliveInterval),
|
||||
Idle: time.Duration(srv.KeepAliveIdle),
|
||||
Count: srv.KeepAliveCount,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -595,13 +565,15 @@ func (app *App) Start() error {
|
||||
ln = srv.listenerWrappers[i].WrapListener(ln)
|
||||
}
|
||||
|
||||
// check if the connection is h2c
|
||||
ln = &http2Listener{
|
||||
useTLS: useTLS,
|
||||
useH1: h1ok,
|
||||
useH2: h2ok || h2cok,
|
||||
Listener: ln,
|
||||
logger: app.logger,
|
||||
// handle http2 if use tls listener wrapper
|
||||
if h2ok {
|
||||
http2lnWrapper := &http2Listener{
|
||||
Listener: ln,
|
||||
server: srv.server,
|
||||
h2server: h2server,
|
||||
}
|
||||
srv.h2listeners = append(srv.h2listeners, http2lnWrapper)
|
||||
ln = http2lnWrapper
|
||||
}
|
||||
|
||||
// if binding to port 0, the OS chooses a port for us;
|
||||
@@ -619,8 +591,11 @@ func (app *App) Start() error {
|
||||
|
||||
srv.listeners = append(srv.listeners, ln)
|
||||
|
||||
//nolint:errcheck
|
||||
go srv.server.Serve(ln)
|
||||
// enable HTTP/1 if configured
|
||||
if h1ok {
|
||||
//nolint:errcheck
|
||||
go srv.server.Serve(ln)
|
||||
}
|
||||
}
|
||||
|
||||
if h2ok && !useTLS {
|
||||
@@ -733,11 +708,6 @@ func (app *App) Stop() error {
|
||||
defer finishedShutdown.Done()
|
||||
startedShutdown.Done()
|
||||
|
||||
// possible if server failed to Start
|
||||
if server.server == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := server.server.Shutdown(ctx); err != nil {
|
||||
app.logger.Error("server shutdown",
|
||||
zap.Error(err),
|
||||
@@ -752,36 +722,31 @@ func (app *App) Stop() error {
|
||||
return
|
||||
}
|
||||
|
||||
// closing quic listeners won't affect accepted connections now
|
||||
// so like stdlib, close listeners first, but keep the net.PacketConns open
|
||||
for _, h3ln := range server.quicListeners {
|
||||
if err := h3ln.Close(); err != nil {
|
||||
app.logger.Error("http3 listener close",
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := server.h3server.Shutdown(ctx); err != nil {
|
||||
app.logger.Error("HTTP/3 server shutdown",
|
||||
zap.Error(err),
|
||||
zap.Strings("addresses", server.Listen))
|
||||
}
|
||||
}
|
||||
stopH2Listener := func(server *Server) {
|
||||
defer finishedShutdown.Done()
|
||||
startedShutdown.Done()
|
||||
|
||||
// close the underlying net.PacketConns now
|
||||
// see the comment for ListenQUIC
|
||||
for _, h3ln := range server.quicListeners {
|
||||
if err := h3ln.Close(); err != nil {
|
||||
app.logger.Error("http3 listener close socket",
|
||||
zap.Error(err))
|
||||
for i, s := range server.h2listeners {
|
||||
if err := s.Shutdown(ctx); err != nil {
|
||||
app.logger.Error("http2 listener shutdown",
|
||||
zap.Error(err),
|
||||
zap.Int("index", i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, server := range app.Servers {
|
||||
startedShutdown.Add(2)
|
||||
finishedShutdown.Add(2)
|
||||
startedShutdown.Add(3)
|
||||
finishedShutdown.Add(3)
|
||||
go stopServer(server)
|
||||
go stopH3Server(server)
|
||||
go stopH2Listener(server)
|
||||
}
|
||||
|
||||
// block until all the goroutines have been run by the scheduler;
|
||||
@@ -808,20 +773,9 @@ func (app *App) Stop() error {
|
||||
}
|
||||
}
|
||||
|
||||
app.stopped = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup will close remaining listeners if they still remain
|
||||
// because some of the servers fail to start.
|
||||
// It simply calls Stop because Stop won't be called when Start fails.
|
||||
func (app *App) Cleanup() error {
|
||||
if app.stopped {
|
||||
return nil
|
||||
}
|
||||
return app.Stop()
|
||||
}
|
||||
|
||||
func (app *App) httpPort() int {
|
||||
if app.HTTPPort == 0 {
|
||||
return DefaultHTTPPort
|
||||
|
||||
@@ -90,16 +90,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
||||
// the log configuration for an HTTPS enabled server
|
||||
var logCfg *ServerLogConfig
|
||||
|
||||
// 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]
|
||||
for srvName, srv := range app.Servers {
|
||||
// as a prerequisite, provision route matchers; this is
|
||||
// required for all routes on all servers, and must be
|
||||
// done before we attempt to do phase 1 of auto HTTPS,
|
||||
@@ -274,22 +265,6 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
||||
}
|
||||
}
|
||||
|
||||
// if all servers have auto_https disabled and no domains need certs,
|
||||
// skip the rest of the TLS automation setup to avoid creating
|
||||
// unnecessary PKI infrastructure and automation policies
|
||||
allServersDisabled := true
|
||||
for _, srv := range app.Servers {
|
||||
if srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled {
|
||||
allServersDisabled = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allServersDisabled && len(uniqueDomainsForCerts) == 0 {
|
||||
logger.Debug("all servers have automatic HTTPS disabled and no domains need certificates, skipping TLS automation setup")
|
||||
return nil
|
||||
}
|
||||
|
||||
// we now have a list of all the unique names for which we need certs
|
||||
var internal, tailscale []string
|
||||
uniqueDomainsLoop:
|
||||
@@ -407,26 +382,15 @@ uniqueDomainsLoop:
|
||||
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:
|
||||
for _, redirServerAddr := range redirServerAddrsSorted {
|
||||
routes := redirServers[redirServerAddr]
|
||||
for redirServerAddr, routes := range redirServers {
|
||||
// for each redirect listener, see if there's already a
|
||||
// server configured to listen on that exact address; if so,
|
||||
// insert the redirect route to the end of its route list
|
||||
// after any other routes with host matchers; otherwise,
|
||||
// we'll create a new server for all the listener addresses
|
||||
// that are unused and serve the remaining redirects from it
|
||||
|
||||
// Use the sorted srvNames to consistently find the target server
|
||||
for _, srvName := range srvNames {
|
||||
srv := app.Servers[srvName]
|
||||
for _, srv := range app.Servers {
|
||||
// only look at servers which listen on an address which
|
||||
// we want to add redirects to
|
||||
if !srv.hasListenerAddress(redirServerAddr) {
|
||||
@@ -497,8 +461,7 @@ func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
|
||||
if redirToPort != uint(app.httpPort()) &&
|
||||
redirToPort != uint(app.httpsPort()) &&
|
||||
redirToPort != DefaultHTTPPort &&
|
||||
redirToPort != DefaultHTTPSPort &&
|
||||
redirToPort > 0 {
|
||||
redirToPort != DefaultHTTPSPort {
|
||||
redirTo += ":" + strconv.Itoa(int(redirToPort))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyauth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(Argon2idHash{})
|
||||
}
|
||||
|
||||
const (
|
||||
argon2idName = "argon2id"
|
||||
defaultArgon2idTime = 1
|
||||
defaultArgon2idMemory = 46 * 1024
|
||||
defaultArgon2idThreads = 1
|
||||
defaultArgon2idKeylen = 32
|
||||
defaultSaltLength = 16
|
||||
)
|
||||
|
||||
// Argon2idHash implements the Argon2id password hashing.
|
||||
type Argon2idHash struct {
|
||||
salt []byte
|
||||
time uint32
|
||||
memory uint32
|
||||
threads uint8
|
||||
keyLen uint32
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (Argon2idHash) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "http.authentication.hashes.argon2id",
|
||||
New: func() caddy.Module { return new(Argon2idHash) },
|
||||
}
|
||||
}
|
||||
|
||||
// Compare checks if the plaintext password matches the given Argon2id hash.
|
||||
func (Argon2idHash) Compare(hashed, plaintext []byte) (bool, error) {
|
||||
argHash, storedKey, err := DecodeHash(hashed)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
computedKey := argon2.IDKey(
|
||||
plaintext,
|
||||
argHash.salt,
|
||||
argHash.time,
|
||||
argHash.memory,
|
||||
argHash.threads,
|
||||
argHash.keyLen,
|
||||
)
|
||||
|
||||
return subtle.ConstantTimeCompare(storedKey, computedKey) == 1, nil
|
||||
}
|
||||
|
||||
// Hash generates an Argon2id hash of the given plaintext using the configured parameters and salt.
|
||||
func (b Argon2idHash) Hash(plaintext []byte) ([]byte, error) {
|
||||
if b.salt == nil {
|
||||
s, err := generateSalt(defaultSaltLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.salt = s
|
||||
}
|
||||
|
||||
key := argon2.IDKey(
|
||||
plaintext,
|
||||
b.salt,
|
||||
b.time,
|
||||
b.memory,
|
||||
b.threads,
|
||||
b.keyLen,
|
||||
)
|
||||
|
||||
hash := fmt.Sprintf(
|
||||
"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||||
argon2.Version,
|
||||
b.memory,
|
||||
b.time,
|
||||
b.threads,
|
||||
base64.RawStdEncoding.EncodeToString(b.salt),
|
||||
base64.RawStdEncoding.EncodeToString(key),
|
||||
)
|
||||
|
||||
return []byte(hash), nil
|
||||
}
|
||||
|
||||
// DecodeHash parses an Argon2id PHC string into an Argon2idHash struct and returns the struct along with the derived key.
|
||||
func DecodeHash(hash []byte) (*Argon2idHash, []byte, error) {
|
||||
parts := strings.Split(string(hash), "$")
|
||||
if len(parts) != 6 {
|
||||
return nil, nil, fmt.Errorf("invalid hash format")
|
||||
}
|
||||
|
||||
if parts[1] != argon2idName {
|
||||
return nil, nil, fmt.Errorf("unsupported variant: %s", parts[1])
|
||||
}
|
||||
|
||||
version, err := strconv.Atoi(strings.TrimPrefix(parts[2], "v="))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid version: %w", err)
|
||||
}
|
||||
if version != argon2.Version {
|
||||
return nil, nil, fmt.Errorf("incompatible version: %d", version)
|
||||
}
|
||||
|
||||
params := strings.Split(parts[3], ",")
|
||||
if len(params) != 3 {
|
||||
return nil, nil, fmt.Errorf("invalid parameters")
|
||||
}
|
||||
|
||||
mem, err := strconv.ParseUint(strings.TrimPrefix(params[0], "m="), 10, 32)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid memory parameter: %w", err)
|
||||
}
|
||||
|
||||
iter, err := strconv.ParseUint(strings.TrimPrefix(params[1], "t="), 10, 32)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid iterations parameter: %w", err)
|
||||
}
|
||||
|
||||
threads, err := strconv.ParseUint(strings.TrimPrefix(params[2], "p="), 10, 8)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid parallelism parameter: %w", err)
|
||||
}
|
||||
|
||||
salt, err := base64.RawStdEncoding.Strict().DecodeString(parts[4])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("decode salt: %w", err)
|
||||
}
|
||||
|
||||
key, err := base64.RawStdEncoding.Strict().DecodeString(parts[5])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("decode key: %w", err)
|
||||
}
|
||||
|
||||
return &Argon2idHash{
|
||||
salt: salt,
|
||||
time: uint32(iter),
|
||||
memory: uint32(mem),
|
||||
threads: uint8(threads),
|
||||
keyLen: uint32(len(key)),
|
||||
}, key, nil
|
||||
}
|
||||
|
||||
// FakeHash returns a constant fake hash for timing attacks mitigation.
|
||||
func (Argon2idHash) FakeHash() []byte {
|
||||
// hashed with the following command:
|
||||
// caddy hash-password --plaintext "antitiming" --algorithm "argon2id"
|
||||
return []byte("$argon2id$v=19$m=47104,t=1,p=1$P2nzckEdTZ3bxCiBCkRTyA$xQL3Z32eo5jKl7u5tcIsnEKObYiyNZQQf5/4sAau6Pg")
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ Comparer = (*Argon2idHash)(nil)
|
||||
_ Hasher = (*Argon2idHash)(nil)
|
||||
)
|
||||
|
||||
func generateSalt(length int) ([]byte, error) {
|
||||
salt := make([]byte, length)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate salt: %w", err)
|
||||
}
|
||||
return salt, nil
|
||||
}
|
||||
@@ -60,8 +60,7 @@ func (Authentication) CaddyModule() caddy.ModuleInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets up an Authentication module by initializing its logger,
|
||||
// loading and registering all configured authentication providers.
|
||||
// Provision sets up a.
|
||||
func (a *Authentication) Provision(ctx caddy.Context) error {
|
||||
a.logger = ctx.Logger()
|
||||
a.Providers = make(map[string]Authenticator)
|
||||
|
||||
@@ -51,7 +51,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||
var hashName string
|
||||
switch len(args) {
|
||||
case 0:
|
||||
hashName = bcryptName
|
||||
hashName = "bcrypt"
|
||||
case 1:
|
||||
hashName = args[0]
|
||||
case 2:
|
||||
@@ -62,10 +62,8 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||
}
|
||||
|
||||
switch hashName {
|
||||
case bcryptName:
|
||||
case "bcrypt":
|
||||
cmp = BcryptHash{}
|
||||
case argon2idName:
|
||||
cmp = Argon2idHash{}
|
||||
default:
|
||||
return nil, h.Errf("unrecognized hash algorithm: %s", hashName)
|
||||
}
|
||||
|
||||
@@ -32,55 +32,21 @@ import (
|
||||
func init() {
|
||||
caddycmd.RegisterCommand(caddycmd.Command{
|
||||
Name: "hash-password",
|
||||
Usage: "[--plaintext <password>] [--algorithm <argon2id|bcrypt>] [--bcrypt-cost <difficulty>] [--argon2id-time <iterations>] [--argon2id-memory <KiB>] [--argon2id-threads <n>] [--argon2id-keylen <bytes>]",
|
||||
Usage: "[--plaintext <password>] [--algorithm <name>]",
|
||||
Short: "Hashes a password and writes base64",
|
||||
Long: `
|
||||
Convenient way to hash a plaintext password. The resulting
|
||||
hash is written to stdout as a base64 string.
|
||||
|
||||
--plaintext
|
||||
The password to hash. If omitted, it will be read from stdin.
|
||||
If Caddy is attached to a controlling TTY, the input will not be echoed.
|
||||
--plaintext, when omitted, will be read from stdin. If
|
||||
Caddy is attached to a controlling tty, the plaintext will
|
||||
not be echoed.
|
||||
|
||||
--algorithm
|
||||
Selects the hashing algorithm. Valid options are:
|
||||
* 'argon2id' (recommended for modern security)
|
||||
* 'bcrypt' (legacy, slower, configurable cost)
|
||||
|
||||
bcrypt-specific parameters:
|
||||
|
||||
--bcrypt-cost
|
||||
Sets the bcrypt hashing difficulty. Higher values increase security by
|
||||
making the hash computation slower and more CPU-intensive.
|
||||
Must be within the valid range [bcrypt.MinCost, bcrypt.MaxCost].
|
||||
If omitted or invalid, the default cost is used.
|
||||
|
||||
Argon2id-specific parameters:
|
||||
|
||||
--argon2id-time
|
||||
Number of iterations to perform. Increasing this makes
|
||||
hashing slower and more resistant to brute-force attacks.
|
||||
|
||||
--argon2id-memory
|
||||
Amount of memory to use during hashing.
|
||||
Larger values increase resistance to GPU/ASIC attacks.
|
||||
|
||||
--argon2id-threads
|
||||
Number of CPU threads to use. Increase for faster hashing
|
||||
on multi-core systems.
|
||||
|
||||
--argon2id-keylen
|
||||
Length of the resulting hash in bytes. Longer keys increase
|
||||
security but slightly increase storage size.
|
||||
--algorithm currently only supports 'bcrypt', and is the default.
|
||||
`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("plaintext", "p", "", "The plaintext password")
|
||||
cmd.Flags().StringP("algorithm", "a", bcryptName, "Name of the hash algorithm")
|
||||
cmd.Flags().Int("bcrypt-cost", defaultBcryptCost, "Bcrypt hashing cost (only used with 'bcrypt' algorithm)")
|
||||
cmd.Flags().Uint32("argon2id-time", defaultArgon2idTime, "Number of iterations for Argon2id hashing. Increasing this makes the hash slower and more resistant to brute-force attacks.")
|
||||
cmd.Flags().Uint32("argon2id-memory", defaultArgon2idMemory, "Memory to use in KiB for Argon2id hashing. Larger values increase resistance to GPU/ASIC attacks.")
|
||||
cmd.Flags().Uint8("argon2id-threads", defaultArgon2idThreads, "Number of CPU threads to use for Argon2id hashing. Increase for faster hashing on multi-core systems.")
|
||||
cmd.Flags().Uint32("argon2id-keylen", defaultArgon2idKeylen, "Length of the resulting Argon2id hash in bytes. Longer hashes increase security but slightly increase storage size.")
|
||||
cmd.Flags().StringP("algorithm", "a", "bcrypt", "Name of the hash algorithm")
|
||||
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdHashPassword)
|
||||
},
|
||||
})
|
||||
@@ -91,7 +57,6 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
|
||||
|
||||
algorithm := fs.String("algorithm")
|
||||
plaintext := []byte(fs.String("plaintext"))
|
||||
bcryptCost := fs.Int("bcrypt-cost")
|
||||
|
||||
if len(plaintext) == 0 {
|
||||
fd := int(os.Stdin.Fd())
|
||||
@@ -142,34 +107,8 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
|
||||
var hash []byte
|
||||
var hashString string
|
||||
switch algorithm {
|
||||
case bcryptName:
|
||||
hash, err = BcryptHash{cost: bcryptCost}.Hash(plaintext)
|
||||
hashString = string(hash)
|
||||
case argon2idName:
|
||||
time, err := fs.GetUint32("argon2id-time")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id time parameter: %w", err)
|
||||
}
|
||||
memory, err := fs.GetUint32("argon2id-memory")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id memory parameter: %w", err)
|
||||
}
|
||||
threads, err := fs.GetUint8("argon2id-threads")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id threads parameter: %w", err)
|
||||
}
|
||||
keyLen, err := fs.GetUint32("argon2id-keylen")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id keylen parameter: %w", err)
|
||||
}
|
||||
|
||||
hash, _ = Argon2idHash{
|
||||
time: time,
|
||||
memory: memory,
|
||||
threads: threads,
|
||||
keyLen: keyLen,
|
||||
}.Hash(plaintext)
|
||||
|
||||
case "bcrypt":
|
||||
hash, err = BcryptHash{}.Hash(plaintext)
|
||||
hashString = string(hash)
|
||||
default:
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm)
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
package caddyauth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
@@ -26,18 +24,8 @@ func init() {
|
||||
caddy.RegisterModule(BcryptHash{})
|
||||
}
|
||||
|
||||
// defaultBcryptCost cost 14 strikes a solid balance between security, usability, and hardware performance
|
||||
const (
|
||||
bcryptName = "bcrypt"
|
||||
defaultBcryptCost = 14
|
||||
)
|
||||
|
||||
// BcryptHash implements the bcrypt hash.
|
||||
type BcryptHash struct {
|
||||
// cost is the bcrypt hashing difficulty factor (work factor).
|
||||
// Higher values increase computation time and security.
|
||||
cost int
|
||||
}
|
||||
type BcryptHash struct{}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (BcryptHash) CaddyModule() caddy.ModuleInfo {
|
||||
@@ -50,7 +38,7 @@ func (BcryptHash) CaddyModule() caddy.ModuleInfo {
|
||||
// Compare compares passwords.
|
||||
func (BcryptHash) Compare(hashed, plaintext []byte) (bool, error) {
|
||||
err := bcrypt.CompareHashAndPassword(hashed, plaintext)
|
||||
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
|
||||
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
@@ -60,13 +48,8 @@ func (BcryptHash) Compare(hashed, plaintext []byte) (bool, error) {
|
||||
}
|
||||
|
||||
// Hash hashes plaintext using a random salt.
|
||||
func (b BcryptHash) Hash(plaintext []byte) ([]byte, error) {
|
||||
cost := b.cost
|
||||
if cost < bcrypt.MinCost || cost > bcrypt.MaxCost {
|
||||
cost = defaultBcryptCost
|
||||
}
|
||||
|
||||
return bcrypt.GenerateFromPassword(plaintext, cost)
|
||||
func (BcryptHash) Hash(plaintext []byte) ([]byte, error) {
|
||||
return bcrypt.GenerateFromPassword(plaintext, 14)
|
||||
}
|
||||
|
||||
// FakeHash returns a fake hash.
|
||||
@@ -665,7 +665,7 @@ func celMatcherJSONMacroExpander(funcName string) parser.MacroExpander {
|
||||
// map literals containing heterogeneous values, in this case string and list
|
||||
// of string.
|
||||
func CELValueToMapStrList(data ref.Val) (map[string][]string, error) {
|
||||
mapStrType := reflect.TypeFor[map[string]any]()
|
||||
mapStrType := reflect.TypeOf(map[string]any{})
|
||||
mapStrRaw, err := data.ConvertToNative(mapStrType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -535,7 +535,7 @@ func BenchmarkMatchExpressionMatch(b *testing.B) {
|
||||
}
|
||||
}
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
for i := 0; i < b.N; i++ {
|
||||
tc.expression.MatchWithError(req)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -50,7 +50,7 @@ type Encode struct {
|
||||
// Only encode responses that are at least this many bytes long.
|
||||
MinLength int `json:"minimum_length,omitempty"`
|
||||
|
||||
// Only encode responses that match against this ResponseMatcher.
|
||||
// Only encode responses that match against this ResponseMmatcher.
|
||||
// The default is a collection of text-based Content-Type headers.
|
||||
Matcher *caddyhttp.ResponseMatcher `json:"match,omitempty"`
|
||||
|
||||
@@ -92,7 +92,6 @@ func (enc *Encode) Provision(ctx caddy.Context) error {
|
||||
"application/font*",
|
||||
"application/geo+json*",
|
||||
"application/graphql+json*",
|
||||
"application/graphql-response+json*",
|
||||
"application/javascript*",
|
||||
"application/json*",
|
||||
"application/ld+json*",
|
||||
@@ -168,8 +167,8 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
|
||||
// caches without knowing about our changes...
|
||||
if etag := r.Header.Get("If-None-Match"); etag != "" && !strings.HasPrefix(etag, "W/") {
|
||||
ourSuffix := "-" + encName + `"`
|
||||
if before, ok := strings.CutSuffix(etag, ourSuffix); ok {
|
||||
etag = before + `"`
|
||||
if strings.HasSuffix(etag, ourSuffix) {
|
||||
etag = strings.TrimSuffix(etag, ourSuffix) + `"`
|
||||
r.Header.Set("If-None-Match", etag)
|
||||
}
|
||||
}
|
||||
@@ -177,17 +176,7 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err := next.ServeHTTP(w, r)
|
||||
// If there was an error, disable encoding completely
|
||||
// This prevents corruption when handle_errors processes the response
|
||||
if err != nil {
|
||||
if ew, ok := w.(*responseWriter); ok {
|
||||
ew.disabled = true
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
return next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (enc *Encode) addEncoding(e Encoding) error {
|
||||
@@ -243,7 +232,6 @@ type responseWriter struct {
|
||||
statusCode int
|
||||
wroteHeader bool
|
||||
isConnect bool
|
||||
disabled bool // disable encoding (for error responses)
|
||||
}
|
||||
|
||||
// WriteHeader stores the status to write when the time comes
|
||||
@@ -436,14 +424,7 @@ func (rw *responseWriter) Unwrap() http.ResponseWriter {
|
||||
|
||||
// init should be called before we write a response, if rw.buf has contents.
|
||||
func (rw *responseWriter) init() {
|
||||
// Don't initialize encoder for error responses
|
||||
// This prevents response corruption when handle_errors is used
|
||||
if rw.disabled {
|
||||
return
|
||||
}
|
||||
|
||||
hdr := rw.Header()
|
||||
|
||||
if hdr.Get("Content-Encoding") == "" && isEncodeAllowed(hdr) &&
|
||||
rw.config.Match(rw) {
|
||||
rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
|
||||
@@ -471,7 +452,8 @@ func (rw *responseWriter) init() {
|
||||
|
||||
func hasVaryValue(hdr http.Header, target string) bool {
|
||||
for _, vary := range hdr.Values("Vary") {
|
||||
for val := range strings.SplitSeq(vary, ",") {
|
||||
vals := strings.Split(vary, ",")
|
||||
for _, val := range vals {
|
||||
if strings.EqualFold(strings.TrimSpace(val), target) {
|
||||
return true
|
||||
}
|
||||
@@ -496,7 +478,7 @@ func AcceptedEncodings(r *http.Request, preferredOrder []string) []string {
|
||||
|
||||
prefs := []encodingPreference{}
|
||||
|
||||
for accepted := range strings.SplitSeq(acceptEncHeader, ",") {
|
||||
for _, accepted := range strings.Split(acceptEncHeader, ",") {
|
||||
parts := strings.Split(accepted, ";")
|
||||
encName := strings.ToLower(strings.TrimSpace(parts[0]))
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
func BenchmarkOpenResponseWriter(b *testing.B) {
|
||||
enc := new(Encode)
|
||||
for b.Loop() {
|
||||
for n := 0; n < b.N; n++ {
|
||||
enc.openResponseWriter("test", nil, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +404,7 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) {
|
||||
}
|
||||
|
||||
// for each glob result, combine all the forms of the path
|
||||
candidates := make([]matchCandidate, 0, len(globResults))
|
||||
var candidates []matchCandidate
|
||||
for _, result := range globResults {
|
||||
candidates = append(candidates, matchCandidate{
|
||||
fullpath: result,
|
||||
|
||||
@@ -167,8 +167,6 @@ type FileServer struct {
|
||||
// If set, file Etags will be read from sidecar files
|
||||
// with any of these suffixes, instead of generating
|
||||
// our own Etag.
|
||||
// Keep in mind that the Etag values in the files have to be quoted as per RFC7232.
|
||||
// See https://datatracker.ietf.org/doc/html/rfc7232#section-2.3 for a few examples.
|
||||
EtagFileExtensions []string `json:"etag_file_extensions,omitempty"`
|
||||
|
||||
fsmap caddy.FileSystems
|
||||
@@ -457,14 +455,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
|
||||
}
|
||||
defer file.Close()
|
||||
respHeader.Set("Content-Encoding", ae)
|
||||
|
||||
// stdlib won't set Content-Length for non-range requests if Content-Encoding is set.
|
||||
// see: https://github.com/caddyserver/caddy/issues/7040
|
||||
// Setting the Range header manually will result in 206 Partial Content.
|
||||
// see: https://github.com/caddyserver/caddy/issues/7250
|
||||
if r.Header.Get("Range") == "" {
|
||||
respHeader.Set("Content-Length", strconv.FormatInt(compressedInfo.Size(), 10))
|
||||
}
|
||||
respHeader.Del("Accept-Ranges")
|
||||
|
||||
// try to get the etag from pre computed files if an etag suffix list was provided
|
||||
if etag == "" && fsrv.EtagFileExtensions != nil {
|
||||
|
||||
@@ -168,6 +168,8 @@ func parseReqHdrCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue,
|
||||
}
|
||||
h.Next() // consume the directive name again (matcher parsing resets)
|
||||
|
||||
configValues := []httpcaddyfile.ConfigValue{}
|
||||
|
||||
if !h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
@@ -202,7 +204,7 @@ func parseReqHdrCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue,
|
||||
return nil, h.Err(err.Error())
|
||||
}
|
||||
|
||||
configValues := h.NewRoute(matcherSet, hdr)
|
||||
configValues = append(configValues, h.NewRoute(matcherSet, hdr)...)
|
||||
|
||||
if h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
|
||||
@@ -159,7 +159,7 @@ func (ops *HeaderOps) Provision(_ caddy.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// containsPlaceholders checks if the string contains Caddy placeholder syntax {key}
|
||||
// containsCaddyPlaceholders checks if the string contains Caddy placeholder syntax {key}
|
||||
func containsPlaceholders(s string) bool {
|
||||
openIdx := strings.Index(s, "{")
|
||||
if openIdx == -1 {
|
||||
@@ -217,10 +217,7 @@ type RespHeaderOps struct {
|
||||
}
|
||||
|
||||
// ApplyTo applies ops to hdr using repl.
|
||||
func (ops *HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
|
||||
if ops == nil {
|
||||
return
|
||||
}
|
||||
func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
|
||||
// before manipulating headers in other ways, check if there
|
||||
// is configuration to delete all headers, and do that first
|
||||
// because if a header is to be added, we don't want to delete
|
||||
|
||||
@@ -1,131 +1,102 @@
|
||||
package caddyhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
weakrand "math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
type connectionStater interface {
|
||||
// http2Listener wraps the listener to solve the following problems:
|
||||
// 1. server h2 natively without using h2c hack when listener handles tls connection but
|
||||
// don't return *tls.Conn
|
||||
// 2. graceful shutdown. the shutdown logic is copied from stdlib http.Server, it's an extra maintenance burden but
|
||||
// whatever, the shutdown logic maybe extracted to be used with h2c graceful shutdown. http2.Server supports graceful shutdown
|
||||
// sending GO_AWAY frame to connected clients, but doesn't track connection status. It requires explicit call of http2.ConfigureServer
|
||||
type http2Listener struct {
|
||||
cnt uint64
|
||||
net.Listener
|
||||
server *http.Server
|
||||
h2server *http2.Server
|
||||
}
|
||||
|
||||
type connectionStateConn interface {
|
||||
net.Conn
|
||||
ConnectionState() tls.ConnectionState
|
||||
}
|
||||
|
||||
// http2Listener wraps the listener to solve the following problems:
|
||||
// 1. prevent genuine h2c connections from succeeding if h2c is not enabled
|
||||
// and the connection doesn't implment connectionStater or the resulting NegotiatedProtocol
|
||||
// isn't http2.
|
||||
// This does allow a connection to pass as tls enabled even if it's not, listener wrappers
|
||||
// can do this.
|
||||
// 2. After wrapping the connection doesn't implement connectionStater, emit a warning so that listener
|
||||
// wrapper authors will hopefully implement it.
|
||||
// 3. check if the connection matches a specific http version. h2/h2c has a distinct preface.
|
||||
type http2Listener struct {
|
||||
useTLS bool
|
||||
useH1 bool
|
||||
useH2 bool
|
||||
net.Listener
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func (h *http2Listener) Accept() (net.Conn, error) {
|
||||
conn, err := h.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// *tls.Conn doesn't need to be wrapped because we already removed unwanted alpns
|
||||
// and handshake won't succeed without mutually supported alpns
|
||||
if tlsConn, ok := conn.(*tls.Conn); ok {
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
_, isConnectionStater := conn.(connectionStater)
|
||||
// emit a warning
|
||||
if h.useTLS && !isConnectionStater {
|
||||
h.logger.Warn("tls is enabled, but listener wrapper returns a connection that doesn't implement connectionStater")
|
||||
} else if !h.useTLS && isConnectionStater {
|
||||
h.logger.Warn("tls is disabled, but listener wrapper returns a connection that implements connectionStater")
|
||||
}
|
||||
|
||||
// if both h1 and h2 are enabled, we don't need to check the preface
|
||||
if h.useH1 && h.useH2 {
|
||||
if isConnectionStater {
|
||||
return tlsStateConn{conn}, nil
|
||||
for {
|
||||
conn, err := h.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if csc, ok := conn.(connectionStateConn); ok {
|
||||
// *tls.Conn will return empty string because it's only populated after handshake is complete
|
||||
if csc.ConnectionState().NegotiatedProtocol == http2.NextProtoTLS {
|
||||
go h.serveHttp2(csc)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// impossible both are false, either useH1 or useH2 must be true,
|
||||
// or else the listener wouldn't be created
|
||||
h2Conn := &http2Conn{
|
||||
h2Expected: h.useH2,
|
||||
logger: h.logger,
|
||||
Conn: conn,
|
||||
}
|
||||
if isConnectionStater {
|
||||
return tlsStateConn{http2StateConn{h2Conn}}, nil
|
||||
}
|
||||
return h2Conn, nil
|
||||
}
|
||||
|
||||
// tlsStateConn wraps a net.Conn that implements connectionStater to hide that method
|
||||
// we can call netConn to get the original net.Conn and get the tls connection state
|
||||
// golang 1.25 will call that method, and it breaks h2 with connections other than *tls.Conn
|
||||
type tlsStateConn struct {
|
||||
net.Conn
|
||||
func (h *http2Listener) serveHttp2(csc connectionStateConn) {
|
||||
atomic.AddUint64(&h.cnt, 1)
|
||||
h.runHook(csc, http.StateNew)
|
||||
defer func() {
|
||||
csc.Close()
|
||||
atomic.AddUint64(&h.cnt, ^uint64(0))
|
||||
h.runHook(csc, http.StateClosed)
|
||||
}()
|
||||
h.h2server.ServeConn(csc, &http2.ServeConnOpts{
|
||||
Context: h.server.ConnContext(context.Background(), csc),
|
||||
BaseConfig: h.server,
|
||||
Handler: h.server.Handler,
|
||||
})
|
||||
}
|
||||
|
||||
func (conn tlsStateConn) tlsNetConn() net.Conn {
|
||||
return conn.Conn
|
||||
}
|
||||
const shutdownPollIntervalMax = 500 * time.Millisecond
|
||||
|
||||
type http2StateConn struct {
|
||||
*http2Conn
|
||||
}
|
||||
|
||||
func (conn http2StateConn) ConnectionState() tls.ConnectionState {
|
||||
return conn.Conn.(connectionStater).ConnectionState()
|
||||
}
|
||||
|
||||
type http2Conn struct {
|
||||
// current index where the preface should match,
|
||||
// no matching is done if idx is >= len(http2.ClientPreface)
|
||||
idx int
|
||||
// whether the connection is expected to be h2/h2c
|
||||
h2Expected bool
|
||||
// log if one such connection is detected
|
||||
logger *zap.Logger
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (c *http2Conn) Read(p []byte) (int, error) {
|
||||
if c.idx >= len(http2.ClientPreface) {
|
||||
return c.Conn.Read(p)
|
||||
}
|
||||
n, err := c.Conn.Read(p)
|
||||
for i := range n {
|
||||
// first mismatch
|
||||
if p[i] != http2.ClientPreface[c.idx] {
|
||||
// close the connection if h2 is expected
|
||||
if c.h2Expected {
|
||||
c.logger.Debug("h1 connection detected, but h1 is not enabled")
|
||||
_ = c.Conn.Close()
|
||||
return 0, io.EOF
|
||||
}
|
||||
// no need to continue matching anymore
|
||||
c.idx = len(http2.ClientPreface)
|
||||
return n, err
|
||||
func (h *http2Listener) Shutdown(ctx context.Context) error {
|
||||
pollIntervalBase := time.Millisecond
|
||||
nextPollInterval := func() time.Duration {
|
||||
// Add 10% jitter.
|
||||
//nolint:gosec
|
||||
interval := pollIntervalBase + time.Duration(weakrand.Intn(int(pollIntervalBase/10)))
|
||||
// Double and clamp for next time.
|
||||
pollIntervalBase *= 2
|
||||
if pollIntervalBase > shutdownPollIntervalMax {
|
||||
pollIntervalBase = shutdownPollIntervalMax
|
||||
}
|
||||
c.idx++
|
||||
// matching complete
|
||||
if c.idx == len(http2.ClientPreface) && !c.h2Expected {
|
||||
c.logger.Debug("h2/h2c connection detected, but h2/h2c is not enabled")
|
||||
_ = c.Conn.Close()
|
||||
return 0, io.EOF
|
||||
return interval
|
||||
}
|
||||
|
||||
timer := time.NewTimer(nextPollInterval())
|
||||
defer timer.Stop()
|
||||
for {
|
||||
if atomic.LoadUint64(&h.cnt) == 0 {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
timer.Reset(nextPollInterval())
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (h *http2Listener) runHook(conn net.Conn, state http.ConnState) {
|
||||
if h.server.ConnState != nil {
|
||||
h.server.ConnState(conn, state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ package intercept
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -176,35 +175,10 @@ func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
|
||||
c.Write(zap.Int("handler", rec.handlerIndex))
|
||||
}
|
||||
|
||||
// response recorder doesn't create a new copy of the original headers, they're
|
||||
// present in the original response writer
|
||||
// create a new recorder to see if any response body from the new handler is present,
|
||||
// if not, use the already buffered response body
|
||||
recorder := caddyhttp.NewResponseRecorder(w, nil, nil)
|
||||
if err := rec.handler.Routes.Compile(emptyHandler).ServeHTTP(recorder, r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// no new response status and the status is not 0
|
||||
if recorder.Status() == 0 && rec.Status() != 0 {
|
||||
w.WriteHeader(rec.Status())
|
||||
}
|
||||
|
||||
// no new response body and there is some in the original response
|
||||
// TODO: what if the new response doesn't have a body by design?
|
||||
// see: https://github.com/caddyserver/caddy/pull/6232#issue-2235224400
|
||||
if recorder.Size() == 0 && buf.Len() > 0 {
|
||||
_, err := io.Copy(w, buf)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
// pass the request through the response handler routes
|
||||
return rec.handler.Routes.Compile(next).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// this handler does nothing because everything we need is already buffered
|
||||
var emptyHandler caddyhttp.Handler = caddyhttp.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
|
||||
//
|
||||
// intercept [<matcher>] {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
@@ -108,7 +109,7 @@ func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
[]*cel.Type{cel.ListType(cel.StringType)},
|
||||
// function to convert a constant list of strings to a MatchPath instance.
|
||||
func(data ref.Val) (RequestMatcherWithError, error) {
|
||||
refStringList := stringSliceType
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
strList, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -221,7 +222,7 @@ func (MatchClientIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
[]*cel.Type{cel.ListType(cel.StringType)},
|
||||
// function to convert a constant list of strings to a MatchPath instance.
|
||||
func(data ref.Val) (RequestMatcherWithError, error) {
|
||||
refStringList := stringSliceType
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
strList, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -15,28 +15,18 @@
|
||||
package caddyhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/exp/zapslog"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterSlogHandlerFactory(func(handler slog.Handler, core zapcore.Core, moduleID string) slog.Handler {
|
||||
return &extraFieldsSlogHandler{defaultHandler: handler, core: core, moduleID: moduleID}
|
||||
})
|
||||
}
|
||||
|
||||
// ServerLogConfig describes a server's logging configuration. If
|
||||
// enabled without customization, all requests to this server are
|
||||
// logged to the default logger; logger destinations may be
|
||||
@@ -219,7 +209,7 @@ func errLogValues(err error) (status int, msg string, fields func() []zapcore.Fi
|
||||
zap.String("err_trace", handlerErr.Trace),
|
||||
}
|
||||
}
|
||||
return status, msg, fields
|
||||
return
|
||||
}
|
||||
fields = func() []zapcore.Field {
|
||||
return []zapcore.Field{
|
||||
@@ -228,26 +218,22 @@ func errLogValues(err error) (status int, msg string, fields func() []zapcore.Fi
|
||||
}
|
||||
status = http.StatusInternalServerError
|
||||
msg = err.Error()
|
||||
return status, msg, fields
|
||||
return
|
||||
}
|
||||
|
||||
// ExtraLogFields is a list of extra fields to log with every request.
|
||||
type ExtraLogFields struct {
|
||||
fields []zapcore.Field
|
||||
handlers sync.Map
|
||||
fields []zapcore.Field
|
||||
}
|
||||
|
||||
// Add adds a field to the list of extra fields to log.
|
||||
func (e *ExtraLogFields) Add(field zap.Field) {
|
||||
e.handlers.Clear()
|
||||
e.fields = append(e.fields, field)
|
||||
}
|
||||
|
||||
// Set sets a field in the list of extra fields to log.
|
||||
// If the field already exists, it is replaced.
|
||||
func (e *ExtraLogFields) Set(field zap.Field) {
|
||||
e.handlers.Clear()
|
||||
|
||||
for i := range e.fields {
|
||||
if e.fields[i].Key == field.Key {
|
||||
e.fields[i] = field
|
||||
@@ -257,29 +243,6 @@ func (e *ExtraLogFields) Set(field zap.Field) {
|
||||
e.fields = append(e.fields, field)
|
||||
}
|
||||
|
||||
func (e *ExtraLogFields) getSloggerHandler(handler *extraFieldsSlogHandler) (h slog.Handler) {
|
||||
if existing, ok := e.handlers.Load(handler); ok {
|
||||
return existing.(slog.Handler)
|
||||
}
|
||||
|
||||
if handler.moduleID == "" {
|
||||
h = zapslog.NewHandler(handler.core.With(e.fields))
|
||||
} else {
|
||||
h = zapslog.NewHandler(handler.core.With(e.fields), zapslog.WithName(handler.moduleID))
|
||||
}
|
||||
|
||||
if handler.group != "" {
|
||||
h = h.WithGroup(handler.group)
|
||||
}
|
||||
if handler.attrs != nil {
|
||||
h = h.WithAttrs(handler.attrs)
|
||||
}
|
||||
|
||||
e.handlers.Store(handler, h)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
const (
|
||||
// Variable name used to indicate that this request
|
||||
// should be omitted from the access logs
|
||||
@@ -291,43 +254,3 @@ const (
|
||||
// Variable name used to indicate the logger to be used
|
||||
AccessLoggerNameVarKey string = "access_logger_names"
|
||||
)
|
||||
|
||||
type extraFieldsSlogHandler struct {
|
||||
defaultHandler slog.Handler
|
||||
core zapcore.Core
|
||||
moduleID string
|
||||
group string
|
||||
attrs []slog.Attr
|
||||
}
|
||||
|
||||
func (e *extraFieldsSlogHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||
return e.defaultHandler.Enabled(ctx, level)
|
||||
}
|
||||
|
||||
func (e *extraFieldsSlogHandler) Handle(ctx context.Context, record slog.Record) error {
|
||||
if elf, ok := ctx.Value(ExtraLogFieldsCtxKey).(*ExtraLogFields); ok {
|
||||
return elf.getSloggerHandler(e).Handle(ctx, record)
|
||||
}
|
||||
|
||||
return e.defaultHandler.Handle(ctx, record)
|
||||
}
|
||||
|
||||
func (e *extraFieldsSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return &extraFieldsSlogHandler{
|
||||
e.defaultHandler.WithAttrs(attrs),
|
||||
e.core,
|
||||
e.moduleID,
|
||||
e.group,
|
||||
append(e.attrs, attrs...),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *extraFieldsSlogHandler) WithGroup(name string) slog.Handler {
|
||||
return &extraFieldsSlogHandler{
|
||||
e.defaultHandler.WithGroup(name),
|
||||
e.core,
|
||||
e.moduleID,
|
||||
name,
|
||||
e.attrs,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
@@ -28,7 +26,7 @@ func init() {
|
||||
|
||||
// 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) {
|
||||
handler := new(LogAppend)
|
||||
err := handler.UnmarshalCaddyfile(h.Dispenser)
|
||||
@@ -45,10 +43,6 @@ func (h *LogAppend) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if strings.HasPrefix(h.Key, "<") && len(h.Key) > 1 {
|
||||
h.Early = true
|
||||
h.Key = h.Key[1:]
|
||||
}
|
||||
h.Value = d.Val()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -44,12 +42,6 @@ type LogAppend struct {
|
||||
// map, the value of that key will be used. Otherwise
|
||||
// the value will be used as-is as a constant string.
|
||||
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.
|
||||
@@ -61,63 +53,13 @@ func (LogAppend) CaddyModule() caddy.ModuleInfo {
|
||||
}
|
||||
|
||||
func (h LogAppend) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||
// 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.
|
||||
// Run the next handler in the chain first.
|
||||
// If an error occurs, we still want to add
|
||||
// any extra log fields that we can, so we
|
||||
// hold onto the error and return it later.
|
||||
handlerErr := next.ServeHTTP(w, r)
|
||||
|
||||
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) {
|
||||
// On the way back up the chain, add the extra log field
|
||||
ctx := r.Context()
|
||||
|
||||
vars := ctx.Value(caddyhttp.VarsCtxKey).(map[string]any)
|
||||
@@ -125,21 +67,7 @@ func (h LogAppend) addLogField(r *http.Request, buf *bytes.Buffer) {
|
||||
extra := ctx.Value(caddyhttp.ExtraLogFieldsCtxKey).(*caddyhttp.ExtraLogFields)
|
||||
|
||||
var varValue any
|
||||
|
||||
// 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, "{") &&
|
||||
if strings.HasPrefix(h.Value, "{") &&
|
||||
strings.HasSuffix(h.Value, "}") &&
|
||||
strings.Count(h.Value, "{") == 1 {
|
||||
// the value looks like a placeholder, so get its value
|
||||
@@ -156,16 +84,9 @@ func (h LogAppend) addLogField(r *http.Request, buf *bytes.Buffer) {
|
||||
// We use zap.Any because it will reflect
|
||||
// to the correct type for us.
|
||||
extra.Add(zap.Any(h.Key, varValue))
|
||||
}
|
||||
|
||||
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}"
|
||||
)
|
||||
return handlerErr
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
@@ -110,7 +110,6 @@ func (t LoggableTLSConnState) MarshalLogObject(enc zapcore.ObjectEncoder) error
|
||||
enc.AddUint16("cipher_suite", t.CipherSuite)
|
||||
enc.AddString("proto", t.NegotiatedProtocol)
|
||||
enc.AddString("server_name", t.ServerName)
|
||||
enc.AddBool("ech", t.ECHAccepted)
|
||||
if len(t.PeerCertificates) > 0 {
|
||||
enc.AddString("client_common_name", t.PeerCertificates[0].Subject.CommonName)
|
||||
enc.AddString("client_serial", t.PeerCertificates[0].SerialNumber.String())
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"path"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
@@ -372,7 +373,7 @@ func (MatchHost) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
"host_match_request_list",
|
||||
[]*cel.Type{cel.ListType(cel.StringType)},
|
||||
func(data ref.Val) (RequestMatcherWithError, error) {
|
||||
refStringList := stringSliceType
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
strList, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -653,7 +654,7 @@ func (MatchPath) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
[]*cel.Type{cel.ListType(cel.StringType)},
|
||||
// function to convert a constant list of strings to a MatchPath instance.
|
||||
func(data ref.Val) (RequestMatcherWithError, error) {
|
||||
refStringList := stringSliceType
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
strList, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -732,7 +733,7 @@ func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
"path_regexp_request_string_string",
|
||||
[]*cel.Type{cel.StringType, cel.StringType},
|
||||
func(data ref.Val) (RequestMatcherWithError, error) {
|
||||
refStringList := stringSliceType
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
params, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -801,7 +802,7 @@ func (MatchMethod) CELLibrary(_ caddy.Context) (cel.Library, error) {
|
||||
"method_request_list",
|
||||
[]*cel.Type{cel.ListType(cel.StringType)},
|
||||
func(data ref.Val) (RequestMatcherWithError, error) {
|
||||
refStringList := stringSliceType
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
strList, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1172,7 +1173,7 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
"header_regexp_request_string_string",
|
||||
[]*cel.Type{cel.StringType, cel.StringType},
|
||||
func(data ref.Val) (RequestMatcherWithError, error) {
|
||||
refStringList := stringSliceType
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
params, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1195,7 +1196,7 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
"header_regexp_request_string_string_string",
|
||||
[]*cel.Type{cel.StringType, cel.StringType, cel.StringType},
|
||||
func(data ref.Val) (RequestMatcherWithError, error) {
|
||||
refStringList := stringSliceType
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
params, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -947,7 +947,7 @@ func BenchmarkHeaderREMatcher(b *testing.B) {
|
||||
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
|
||||
req = req.WithContext(ctx)
|
||||
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
|
||||
for b.Loop() {
|
||||
for run := 0; run < b.N; run++ {
|
||||
match.MatchWithError(req)
|
||||
}
|
||||
}
|
||||
@@ -992,6 +992,8 @@ func TestVarREMatcher(t *testing.T) {
|
||||
expect: true,
|
||||
},
|
||||
} {
|
||||
i := i // capture range value
|
||||
tc := tc // capture range value
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// compile the regexp and validate its name
|
||||
@@ -1178,7 +1180,8 @@ func BenchmarkLargeHostMatcher(b *testing.B) {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
for b.Loop() {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
matcher.MatchWithError(req)
|
||||
}
|
||||
}
|
||||
@@ -1191,7 +1194,8 @@ func BenchmarkHostMatcherWithoutPlaceholder(b *testing.B) {
|
||||
|
||||
match := MatchHost{"localhost"}
|
||||
|
||||
for b.Loop() {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
match.MatchWithError(req)
|
||||
}
|
||||
}
|
||||
@@ -1208,7 +1212,8 @@ func BenchmarkHostMatcherWithPlaceholder(b *testing.B) {
|
||||
req = req.WithContext(ctx)
|
||||
match := MatchHost{"{env.GO_BENCHMARK_DOMAIN}"}
|
||||
|
||||
for b.Loop() {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
match.MatchWithError(req)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,60 +17,14 @@ import (
|
||||
|
||||
// Metrics configures metrics observations.
|
||||
// EXPERIMENTAL and subject to change or removal.
|
||||
//
|
||||
// Example configuration:
|
||||
//
|
||||
// {
|
||||
// "apps": {
|
||||
// "http": {
|
||||
// "metrics": {
|
||||
// "per_host": true,
|
||||
// "observe_catchall_hosts": false
|
||||
// },
|
||||
// "servers": {
|
||||
// "srv0": {
|
||||
// "routes": [{
|
||||
// "match": [{"host": ["example.com", "www.example.com"]}],
|
||||
// "handle": [{"handler": "static_response", "body": "Hello"}]
|
||||
// }]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// In this configuration:
|
||||
// - Requests to example.com and www.example.com get individual host labels
|
||||
// - All other hosts (e.g., attacker.com) are aggregated under "_other" label
|
||||
// - This prevents unlimited cardinality from arbitrary Host headers
|
||||
type Metrics struct {
|
||||
// Enable per-host metrics. Enabling this option may
|
||||
// incur high-memory consumption, depending on the number of hosts
|
||||
// managed by Caddy.
|
||||
//
|
||||
// CARDINALITY PROTECTION: To prevent unbounded cardinality attacks,
|
||||
// only explicitly configured hosts (via host matchers) are allowed
|
||||
// by default. Other hosts are aggregated under the "_other" label.
|
||||
// See AllowCatchAllHosts to change this behavior.
|
||||
PerHost bool `json:"per_host,omitempty"`
|
||||
|
||||
// Allow metrics for catch-all hosts (hosts without explicit configuration).
|
||||
// When false (default), only hosts explicitly configured via host matchers
|
||||
// will get individual metrics labels. All other hosts will be aggregated
|
||||
// under the "_other" label to prevent cardinality explosion.
|
||||
//
|
||||
// This is automatically enabled for HTTPS servers (since certificates provide
|
||||
// some protection against unbounded cardinality), but disabled for HTTP servers
|
||||
// by default to prevent cardinality attacks from arbitrary Host headers.
|
||||
//
|
||||
// Set to true to allow all hosts to get individual metrics (NOT RECOMMENDED
|
||||
// for production environments exposed to the internet).
|
||||
ObserveCatchallHosts bool `json:"observe_catchall_hosts,omitempty"`
|
||||
|
||||
init sync.Once
|
||||
httpMetrics *httpMetrics
|
||||
allowedHosts map[string]struct{}
|
||||
hasHTTPSServer bool
|
||||
init sync.Once
|
||||
httpMetrics *httpMetrics `json:"-"`
|
||||
}
|
||||
|
||||
type httpMetrics struct {
|
||||
@@ -147,63 +101,6 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) {
|
||||
}, httpLabels)
|
||||
}
|
||||
|
||||
// scanConfigForHosts scans the HTTP app configuration to build a set of allowed hosts
|
||||
// for metrics collection, similar to how auto-HTTPS scans for domain names.
|
||||
func (m *Metrics) scanConfigForHosts(app *App) {
|
||||
if !m.PerHost {
|
||||
return
|
||||
}
|
||||
|
||||
m.allowedHosts = make(map[string]struct{})
|
||||
m.hasHTTPSServer = false
|
||||
|
||||
for _, srv := range app.Servers {
|
||||
// Check if this server has TLS enabled
|
||||
serverHasTLS := len(srv.TLSConnPolicies) > 0
|
||||
if serverHasTLS {
|
||||
m.hasHTTPSServer = true
|
||||
}
|
||||
|
||||
// Collect hosts from route matchers
|
||||
for _, route := range srv.Routes {
|
||||
for _, matcherSet := range route.MatcherSets {
|
||||
for _, matcher := range matcherSet {
|
||||
if hm, ok := matcher.(*MatchHost); ok {
|
||||
for _, host := range *hm {
|
||||
// Only allow non-fuzzy hosts to prevent unbounded cardinality
|
||||
if !hm.fuzzy(host) {
|
||||
m.allowedHosts[strings.ToLower(host)] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shouldAllowHostMetrics determines if metrics should be collected for the given host.
|
||||
// This implements the cardinality protection by only allowing metrics for:
|
||||
// 1. Explicitly configured hosts
|
||||
// 2. Catch-all requests on HTTPS servers (if AllowCatchAllHosts is true or auto-enabled)
|
||||
// 3. Catch-all requests on HTTP servers only if explicitly allowed
|
||||
func (m *Metrics) shouldAllowHostMetrics(host string, isHTTPS bool) bool {
|
||||
if !m.PerHost {
|
||||
return true // host won't be used in labels anyway
|
||||
}
|
||||
|
||||
normalizedHost := strings.ToLower(host)
|
||||
|
||||
// Always allow explicitly configured hosts
|
||||
if _, exists := m.allowedHosts[normalizedHost]; exists {
|
||||
return true
|
||||
}
|
||||
|
||||
// For catch-all requests (not in allowed hosts)
|
||||
allowCatchAll := m.ObserveCatchallHosts || (isHTTPS && m.hasHTTPSServer)
|
||||
return allowCatchAll
|
||||
}
|
||||
|
||||
// serverNameFromContext extracts the current server name from the context.
|
||||
// Returns "UNKNOWN" if none is available (should probably never happen).
|
||||
func serverNameFromContext(ctx context.Context) string {
|
||||
@@ -236,19 +133,9 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
|
||||
// of a panic
|
||||
statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""}
|
||||
|
||||
// Determine if this is an HTTPS request
|
||||
isHTTPS := r.TLS != nil
|
||||
|
||||
if h.metrics.PerHost {
|
||||
// Apply cardinality protection for host metrics
|
||||
if h.metrics.shouldAllowHostMetrics(r.Host, isHTTPS) {
|
||||
labels["host"] = strings.ToLower(r.Host)
|
||||
statusLabels["host"] = strings.ToLower(r.Host)
|
||||
} else {
|
||||
// Use a catch-all label for unallowed hosts to prevent cardinality explosion
|
||||
labels["host"] = "_other"
|
||||
statusLabels["host"] = "_other"
|
||||
}
|
||||
labels["host"] = strings.ToLower(r.Host)
|
||||
statusLabels["host"] = strings.ToLower(r.Host)
|
||||
}
|
||||
|
||||
inFlight := h.metrics.httpMetrics.requestInFlight.With(labels)
|
||||
|
||||
@@ -2,7 +2,6 @@ package caddyhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -207,11 +206,9 @@ func TestMetricsInstrumentedHandler(t *testing.T) {
|
||||
func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
|
||||
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||
metrics := &Metrics{
|
||||
PerHost: true,
|
||||
ObserveCatchallHosts: true, // Allow all hosts for testing
|
||||
init: sync.Once{},
|
||||
httpMetrics: &httpMetrics{},
|
||||
allowedHosts: make(map[string]struct{}),
|
||||
PerHost: true,
|
||||
init: sync.Once{},
|
||||
httpMetrics: &httpMetrics{},
|
||||
}
|
||||
handlerErr := errors.New("oh noes")
|
||||
response := []byte("hello world!")
|
||||
@@ -382,112 +379,6 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsCardinalityProtection(t *testing.T) {
|
||||
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||
|
||||
// Test 1: Without AllowCatchAllHosts, arbitrary hosts should be mapped to "_other"
|
||||
metrics := &Metrics{
|
||||
PerHost: true,
|
||||
ObserveCatchallHosts: false, // Default - should map unknown hosts to "_other"
|
||||
init: sync.Once{},
|
||||
httpMetrics: &httpMetrics{},
|
||||
allowedHosts: make(map[string]struct{}),
|
||||
}
|
||||
|
||||
// Add one allowed host
|
||||
metrics.allowedHosts["allowed.com"] = struct{}{}
|
||||
|
||||
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
||||
w.Write([]byte("hello"))
|
||||
return nil
|
||||
})
|
||||
|
||||
ih := newMetricsInstrumentedHandler(ctx, "test", mh, metrics)
|
||||
|
||||
// Test request to allowed host
|
||||
r1 := httptest.NewRequest("GET", "http://allowed.com/", nil)
|
||||
r1.Host = "allowed.com"
|
||||
w1 := httptest.NewRecorder()
|
||||
ih.ServeHTTP(w1, r1, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
||||
|
||||
// Test request to unknown host (should be mapped to "_other")
|
||||
r2 := httptest.NewRequest("GET", "http://attacker.com/", nil)
|
||||
r2.Host = "attacker.com"
|
||||
w2 := httptest.NewRecorder()
|
||||
ih.ServeHTTP(w2, r2, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
||||
|
||||
// Test request to another unknown host (should also be mapped to "_other")
|
||||
r3 := httptest.NewRequest("GET", "http://evil.com/", nil)
|
||||
r3.Host = "evil.com"
|
||||
w3 := httptest.NewRecorder()
|
||||
ih.ServeHTTP(w3, r3, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
||||
|
||||
// Check that metrics contain:
|
||||
// - One entry for "allowed.com"
|
||||
// - One entry for "_other" (aggregating attacker.com and evil.com)
|
||||
expected := `
|
||||
# HELP caddy_http_requests_total Counter of HTTP(S) requests made.
|
||||
# TYPE caddy_http_requests_total counter
|
||||
caddy_http_requests_total{handler="test",host="_other",server="UNKNOWN"} 2
|
||||
caddy_http_requests_total{handler="test",host="allowed.com",server="UNKNOWN"} 1
|
||||
`
|
||||
|
||||
if err := testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expected),
|
||||
"caddy_http_requests_total",
|
||||
); err != nil {
|
||||
t.Errorf("Cardinality protection test failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsHTTPSCatchAll(t *testing.T) {
|
||||
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||
|
||||
// Test that HTTPS requests allow catch-all even when AllowCatchAllHosts is false
|
||||
metrics := &Metrics{
|
||||
PerHost: true,
|
||||
ObserveCatchallHosts: false,
|
||||
hasHTTPSServer: true, // Simulate having HTTPS servers
|
||||
init: sync.Once{},
|
||||
httpMetrics: &httpMetrics{},
|
||||
allowedHosts: make(map[string]struct{}), // Empty - no explicitly allowed hosts
|
||||
}
|
||||
|
||||
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
||||
w.Write([]byte("hello"))
|
||||
return nil
|
||||
})
|
||||
|
||||
ih := newMetricsInstrumentedHandler(ctx, "test", mh, metrics)
|
||||
|
||||
// Test HTTPS request (should be allowed even though not in allowedHosts)
|
||||
r1 := httptest.NewRequest("GET", "https://unknown.com/", nil)
|
||||
r1.Host = "unknown.com"
|
||||
r1.TLS = &tls.ConnectionState{} // Mark as TLS/HTTPS
|
||||
w1 := httptest.NewRecorder()
|
||||
ih.ServeHTTP(w1, r1, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
||||
|
||||
// Test HTTP request (should be mapped to "_other")
|
||||
r2 := httptest.NewRequest("GET", "http://unknown.com/", nil)
|
||||
r2.Host = "unknown.com"
|
||||
// No TLS field = HTTP request
|
||||
w2 := httptest.NewRecorder()
|
||||
ih.ServeHTTP(w2, r2, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
||||
|
||||
// Check that HTTPS request gets real host, HTTP gets "_other"
|
||||
expected := `
|
||||
# HELP caddy_http_requests_total Counter of HTTP(S) requests made.
|
||||
# TYPE caddy_http_requests_total counter
|
||||
caddy_http_requests_total{handler="test",host="_other",server="UNKNOWN"} 1
|
||||
caddy_http_requests_total{handler="test",host="unknown.com",server="UNKNOWN"} 1
|
||||
`
|
||||
|
||||
if err := testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expected),
|
||||
"caddy_http_requests_total",
|
||||
); err != nil {
|
||||
t.Errorf("HTTPS catch-all test failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
type middlewareHandlerFunc func(http.ResponseWriter, *http.Request, Handler) error
|
||||
|
||||
func (f middlewareHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, h Handler) error {
|
||||
|
||||
@@ -64,7 +64,6 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||
var err error
|
||||
|
||||
// include current token, which we treat as an argument here
|
||||
// nolint:prealloc
|
||||
args := []string{h.Val()}
|
||||
args = append(args, h.RemainingArgs()...)
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ func parseLinkHeader(header string) []linkResource {
|
||||
return resources
|
||||
}
|
||||
|
||||
for link := range strings.SplitSeq(header, comma) {
|
||||
for _, link := range strings.Split(header, comma) {
|
||||
l := linkResource{params: make(map[string]string)}
|
||||
|
||||
li, ri := strings.Index(link, "<"), strings.Index(link, ">")
|
||||
@@ -51,7 +51,7 @@ func parseLinkHeader(header string) []linkResource {
|
||||
|
||||
l.uri = strings.TrimSpace(link[li+1 : ri])
|
||||
|
||||
for param := range strings.SplitSeq(strings.TrimSpace(link[ri+1:]), semicolon) {
|
||||
for _, param := range strings.Split(strings.TrimSpace(link[ri+1:]), semicolon) {
|
||||
before, after, isCut := strings.Cut(strings.TrimSpace(param), equal)
|
||||
key := strings.TrimSpace(before)
|
||||
if key == "" {
|
||||
|
||||
@@ -172,12 +172,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||
// current URI, including any internal rewrites
|
||||
case "http.request.uri":
|
||||
return req.URL.RequestURI(), true
|
||||
case "http.request.uri_escaped":
|
||||
return url.QueryEscape(req.URL.RequestURI()), true
|
||||
case "http.request.uri.path":
|
||||
return req.URL.Path, true
|
||||
case "http.request.uri.path_escaped":
|
||||
return url.QueryEscape(req.URL.Path), true
|
||||
case "http.request.uri.path.file":
|
||||
_, file := path.Split(req.URL.Path)
|
||||
return file, true
|
||||
@@ -190,8 +186,6 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||
return path.Ext(req.URL.Path), true
|
||||
case "http.request.uri.query":
|
||||
return req.URL.RawQuery, true
|
||||
case "http.request.uri.query_escaped":
|
||||
return url.QueryEscape(req.URL.RawQuery), true
|
||||
case "http.request.uri.prefixed_query":
|
||||
if req.URL.RawQuery == "" {
|
||||
return "", true
|
||||
@@ -229,21 +223,6 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||
req.Body = io.NopCloser(buf) // replace real body with buffered data
|
||||
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
|
||||
case "http.request.orig_method":
|
||||
or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
|
||||
@@ -304,7 +283,7 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||
return prefix.String(), true
|
||||
}
|
||||
|
||||
// hostname labels (case insensitive, so normalize to lowercase)
|
||||
// hostname labels
|
||||
if strings.HasPrefix(key, reqHostLabelsReplPrefix) {
|
||||
idxStr := key[len(reqHostLabelsReplPrefix):]
|
||||
idx, err := strconv.Atoi(idxStr)
|
||||
@@ -319,7 +298,7 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||
if idx >= len(hostLabels) {
|
||||
return "", true
|
||||
}
|
||||
return strings.ToLower(hostLabels[len(hostLabels)-idx-1]), true
|
||||
return hostLabels[len(hostLabels)-idx-1], true
|
||||
}
|
||||
|
||||
// path parts
|
||||
@@ -526,8 +505,6 @@ func getReqTLSReplacement(req *http.Request, key string) (any, bool) {
|
||||
return true, true
|
||||
case "server_name":
|
||||
return req.TLS.ServerName, true
|
||||
case "ech":
|
||||
return req.TLS.ECHAccepted, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
)
|
||||
|
||||
func TestHTTPVarReplacement(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodGet, "/foo/bar.tar.gz?a=1&b=2", nil)
|
||||
req, _ := http.NewRequest(http.MethodGet, "/foo/bar.tar.gz", nil)
|
||||
repl := caddy.NewReplacer()
|
||||
localAddr, _ := net.ResolveTCPAddr("tcp", "192.168.159.1:80")
|
||||
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
|
||||
@@ -142,22 +142,6 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
|
||||
get: "http.request.host.labels.2",
|
||||
expect: "",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri",
|
||||
expect: "/foo/bar.tar.gz?a=1&b=2",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri_escaped",
|
||||
expect: "%2Ffoo%2Fbar.tar.gz%3Fa%3D1%26b%3D2",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri.path",
|
||||
expect: "/foo/bar.tar.gz",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri.path_escaped",
|
||||
expect: "%2Ffoo%2Fbar.tar.gz",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri.path.file",
|
||||
expect: "bar.tar.gz",
|
||||
@@ -171,26 +155,6 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
|
||||
get: "http.request.uri.path.file.ext",
|
||||
expect: ".gz",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri.query",
|
||||
expect: "a=1&b=2",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri.query_escaped",
|
||||
expect: "a%3D1%26b%3D2",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri.query.a",
|
||||
expect: "1",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri.query.b",
|
||||
expect: "2",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri.prefixed_query",
|
||||
expect: "?a=1&b=2",
|
||||
},
|
||||
{
|
||||
get: "http.request.tls.cipher_suite",
|
||||
expect: "TLS_AES_256_GCM_SHA384",
|
||||
|
||||
@@ -116,7 +116,7 @@ func (ew errorWrapper) Read(p []byte) (n int, err error) {
|
||||
if errors.As(err, &mbe) {
|
||||
err = caddyhttp.Error(http.StatusRequestEntityTooLarge, err)
|
||||
}
|
||||
return n, err
|
||||
return
|
||||
}
|
||||
|
||||
// Interface guard
|
||||
|
||||
@@ -888,11 +888,8 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
if commonScheme == "http" && te.TLSEnabled() {
|
||||
return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)")
|
||||
}
|
||||
if h2ct, ok := transport.(H2CTransport); ok && commonScheme == "h2c" {
|
||||
err := h2ct.EnableH2C()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if te, ok := transport.(*HTTPTransport); ok && commonScheme == "h2c" {
|
||||
te.Versions = []string{"h2c", "2"}
|
||||
}
|
||||
} else if commonScheme == "https" {
|
||||
return d.Errf("upstreams are configured for HTTPS but transport module does not support TLS: %T", transport)
|
||||
@@ -1528,7 +1525,6 @@ func (u *SRVUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
return d.Errf("bad delay value '%s': %v", d.Val(), err)
|
||||
}
|
||||
u.FallbackDelay = caddy.Duration(dur)
|
||||
|
||||
case "grace_period":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
|
||||
@@ -75,8 +75,8 @@ For proxying:
|
||||
cmd.Flags().BoolP("insecure", "", false, "Disable TLS verification (WARNING: DISABLES SECURITY BY NOT VERIFYING TLS CERTIFICATES!)")
|
||||
cmd.Flags().BoolP("disable-redirects", "r", false, "Disable HTTP->HTTPS redirects")
|
||||
cmd.Flags().BoolP("internal-certs", "i", false, "Use internal CA for issuing certs")
|
||||
cmd.Flags().StringArrayP("header-up", "H", []string{}, "Set a request header to send to the upstream (format: \"Field: value\")")
|
||||
cmd.Flags().StringArrayP("header-down", "d", []string{}, "Set a response header to send back to the client (format: \"Field: value\")")
|
||||
cmd.Flags().StringSliceP("header-up", "H", []string{}, "Set a request header to send to the upstream (format: \"Field: value\")")
|
||||
cmd.Flags().StringSliceP("header-down", "d", []string{}, "Set a response header to send back to the client (format: \"Field: value\")")
|
||||
cmd.Flags().BoolP("access-log", "", false, "Enable the access log")
|
||||
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
|
||||
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdReverseProxy)
|
||||
@@ -182,7 +182,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
|
||||
}
|
||||
|
||||
// set up header_up
|
||||
headerUp, err := fs.GetStringArray("header-up")
|
||||
headerUp, err := fs.GetStringSlice("header-up")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err)
|
||||
}
|
||||
@@ -204,7 +204,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
|
||||
}
|
||||
|
||||
// set up header_down
|
||||
headerDown, err := fs.GetStringArray("header-down")
|
||||
headerDown, err := fs.GetStringSlice("header-down")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err)
|
||||
}
|
||||
|
||||
@@ -154,13 +154,13 @@ func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error)
|
||||
|
||||
err = writer.writeBeginRequest(uint16(Responder), 0)
|
||||
if err != nil {
|
||||
return r, err
|
||||
return
|
||||
}
|
||||
|
||||
writer.recType = Params
|
||||
err = writer.writePairs(p)
|
||||
if err != nil {
|
||||
return r, err
|
||||
return
|
||||
}
|
||||
|
||||
writer.recType = Stdin
|
||||
@@ -176,7 +176,7 @@ func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error)
|
||||
}
|
||||
|
||||
r = &streamReader{c: c}
|
||||
return r, err
|
||||
return
|
||||
}
|
||||
|
||||
// clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer
|
||||
@@ -213,7 +213,7 @@ func (f clientCloser) Close() error {
|
||||
func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) {
|
||||
r, err := c.Do(p, req)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
return
|
||||
}
|
||||
|
||||
rb := bufio.NewReader(r)
|
||||
@@ -223,7 +223,7 @@ func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Respons
|
||||
// Parse the response headers.
|
||||
mimeHeader, err := tp.ReadMIMEHeader()
|
||||
if err != nil && err != io.EOF {
|
||||
return resp, err
|
||||
return
|
||||
}
|
||||
resp.Header = http.Header(mimeHeader)
|
||||
|
||||
@@ -231,7 +231,7 @@ func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Respons
|
||||
statusNumber, statusInfo, statusIsCut := strings.Cut(resp.Header.Get("Status"), " ")
|
||||
resp.StatusCode, err = strconv.Atoi(statusNumber)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
return
|
||||
}
|
||||
if statusIsCut {
|
||||
resp.Status = statusInfo
|
||||
@@ -260,7 +260,7 @@ func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Respons
|
||||
}
|
||||
resp.Body = closer
|
||||
|
||||
return resp, err
|
||||
return
|
||||
}
|
||||
|
||||
// Get issues a GET request to the fcgi responder.
|
||||
@@ -329,7 +329,7 @@ func (c *client) PostFile(p map[string]string, data url.Values, file map[string]
|
||||
for _, v0 := range val {
|
||||
err = writer.WriteField(key, v0)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,13 +347,13 @@ func (c *client) PostFile(p map[string]string, data url.Values, file map[string]
|
||||
}
|
||||
_, err = io.Copy(part, fd)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
return resp, err
|
||||
return
|
||||
}
|
||||
|
||||
return c.Post(p, "POST", bodyType, buf, int64(buf.Len()))
|
||||
|
||||
@@ -120,7 +120,7 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
|
||||
conn, err := net.Dial("tcp", ipPort)
|
||||
if err != nil {
|
||||
log.Println("err:", err)
|
||||
return content
|
||||
return
|
||||
}
|
||||
|
||||
fcgi := client{rwc: conn, reqID: 1}
|
||||
@@ -162,7 +162,7 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
|
||||
|
||||
if err != nil {
|
||||
log.Println("err:", err)
|
||||
return content
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
@@ -176,7 +176,7 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
|
||||
globalt.Error("Server return failed message")
|
||||
}
|
||||
|
||||
return content
|
||||
return
|
||||
}
|
||||
|
||||
func generateRandFile(size int) (p string, m string) {
|
||||
@@ -206,7 +206,7 @@ func generateRandFile(size int) (p string, m string) {
|
||||
}
|
||||
}
|
||||
m = fmt.Sprintf("%x", h.Sum(nil))
|
||||
return p, m
|
||||
return
|
||||
}
|
||||
|
||||
func DisabledTest(t *testing.T) {
|
||||
|
||||
@@ -112,20 +112,6 @@ func (t *Transport) Provision(ctx caddy.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultBufferSizes enables request buffering for fastcgi if not configured.
|
||||
// This is because most fastcgi servers are php-fpm that require the content length to be set to read the body, golang
|
||||
// std has fastcgi implementation that doesn't need this value to process the body, but we can safely assume that's
|
||||
// not used.
|
||||
// http3 requests have a negative content length for GET and HEAD requests, if that header is not sent.
|
||||
// see: https://github.com/caddyserver/caddy/issues/6678#issuecomment-2472224182
|
||||
// Though it appears even if CONTENT_LENGTH is invalid, php-fpm can handle just fine if the body is empty (no Stdin records sent).
|
||||
// php-fpm will hang if there is any data in the body though, https://github.com/caddyserver/caddy/issues/5420#issuecomment-2415943516
|
||||
|
||||
// TODO: better default buffering for fastcgi requests without content length, in theory a value of 1 should be enough, make it bigger anyway
|
||||
func (t Transport) DefaultBufferSizes() (int64, int64) {
|
||||
return 4096, 0
|
||||
}
|
||||
|
||||
// RoundTrip implements http.RoundTripper.
|
||||
func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server)
|
||||
@@ -441,7 +427,6 @@ var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
|
||||
var (
|
||||
_ zapcore.ObjectMarshaler = (*loggableEnv)(nil)
|
||||
|
||||
_ caddy.Provisioner = (*Transport)(nil)
|
||||
_ http.RoundTripper = (*Transport)(nil)
|
||||
_ reverseproxy.BufferedTransport = (*Transport)(nil)
|
||||
_ caddy.Provisioner = (*Transport)(nil)
|
||||
_ http.RoundTripper = (*Transport)(nil)
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user