mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 08:42:31 -04:00
Compare commits
31 Commits
master
..
hurl-tests
| Author | SHA1 | Date | |
|---|---|---|---|
| 01828e38bb | |||
| 59aa8588c9 | |||
| a63a87e4ef | |||
| 501f525d35 | |||
| 47ef562bbd | |||
| d87760adab | |||
| 1904c37234 | |||
| cb72512d17 | |||
| 8d422f0d7f | |||
| 336d514797 | |||
| 6d89bc3942 | |||
| d186879da5 | |||
| 9f9f5ab4de | |||
| 535e40c342 | |||
| 218b3b192b | |||
| df59b09cad | |||
| c718744483 | |||
| e75fca007e | |||
| 25d94ffe2a | |||
| 7ea59f0d49 | |||
| ddc2ca3e10 | |||
| b34c13c5cf | |||
| 18a15d84ef | |||
| 9ba7ea76a9 | |||
| 01ae168f92 | |||
| 8d2ed344c1 | |||
| 3bdc6c035a | |||
| c1cdc25b77 | |||
| 0ecb1ba262 | |||
| eb6934f784 | |||
| 1b4bd3ee1b |
+7
-12
@@ -1,14 +1,15 @@
|
|||||||
# Security Policy
|
# Security Policy
|
||||||
|
|
||||||
The Caddy project would like to make sure that it stays on top of all relevant and practically-exploitable vulnerabilities.
|
The Caddy project would like to make sure that it stays on top of all practically-exploitable vulnerabilities.
|
||||||
|
|
||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ----------- | ----------|
|
| -------- | ----------|
|
||||||
| 2.latest | ✔️ |
|
| 2.latest | ✔️ |
|
||||||
| < 2.latest | :x: |
|
| 1.x | :x: |
|
||||||
|
| < 1.x | :x: |
|
||||||
|
|
||||||
|
|
||||||
## Acceptable Scope
|
## Acceptable Scope
|
||||||
@@ -17,7 +18,7 @@ A security report must demonstrate a security bug in the source code from this r
|
|||||||
|
|
||||||
Some security problems are the result of interplay between different components of the Web, rather than a vulnerability in the web server itself. Please only report vulnerabilities in the web server itself, as we cannot coerce the rest of the Web to be fixed (for example, we do not consider IP spoofing, BGP hijacks, or missing/misconfigured HTTP headers a vulnerability in the Caddy web server).
|
Some security problems are the result of interplay between different components of the Web, rather than a vulnerability in the web server itself. Please only report vulnerabilities in the web server itself, as we cannot coerce the rest of the Web to be fixed (for example, we do not consider IP spoofing, BGP hijacks, or missing/misconfigured HTTP headers a vulnerability in the Caddy web server).
|
||||||
|
|
||||||
Vulnerabilities caused by misconfigurations are out of scope. Yes, it is entirely possible to craft and use a configuration that is unsafe, just like with every other web server; we recommend against doing that. Similarly, external misconfigurations are out of scope. For example, an open or forwarded port from a public network to a Caddy instance intended to serve only internal clients is not a vulnerability in Caddy.
|
Vulnerabilities caused by misconfigurations are out of scope. Yes, it is entirely possible to craft and use a configuration that is unsafe, just like with every other web server; we recommend against doing that.
|
||||||
|
|
||||||
We do not accept reports if the steps imply or require a compromised system or third-party software, as we cannot control those. We expect that users secure their own systems and keep all their software patched. For example, if untrusted users are able to upload/write/host arbitrary files in the web root directory, it is NOT a security bug in Caddy if those files get served to clients; however, it _would_ be a valid report if a bug in Caddy's source code unintentionally gave unauthorized users the ability to upload unsafe files or delete files without relying on an unpatched system or piece of software.
|
We do not accept reports if the steps imply or require a compromised system or third-party software, as we cannot control those. We expect that users secure their own systems and keep all their software patched. For example, if untrusted users are able to upload/write/host arbitrary files in the web root directory, it is NOT a security bug in Caddy if those files get served to clients; however, it _would_ be a valid report if a bug in Caddy's source code unintentionally gave unauthorized users the ability to upload unsafe files or delete files without relying on an unpatched system or piece of software.
|
||||||
|
|
||||||
@@ -25,10 +26,6 @@ Client-side exploits are out of scope. In other words, it is not a bug in Caddy
|
|||||||
|
|
||||||
Security bugs in code dependencies (including Go's standard library) are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code.
|
Security bugs in code dependencies (including Go's standard library) are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code.
|
||||||
|
|
||||||
Many reports are not security bugs and can be addressed by updating the documentation.
|
|
||||||
|
|
||||||
We accept security reports and patches, but do not assign CVEs, for code that has not been released with a non-prerelease tag.
|
|
||||||
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
@@ -36,8 +33,6 @@ We get a lot of difficult reports that turn out to be invalid. Clear, obvious re
|
|||||||
|
|
||||||
First please ensure your report falls within the accepted scope of security bugs (above).
|
First please ensure your report falls within the accepted scope of security bugs (above).
|
||||||
|
|
||||||
:warning: **YOU MUST DISCLOSE WHETHER YOU USED LLMs ("AI") IN ANY WAY.** Whether you are using AI for discovery, as part of writing the report or its replies, and/or testing or validating proofs and changes, we require you to mention the extent of it. **FAILURE TO INCLUDE A DISCLOSURE EVEN IF YOU DO NOT USE AI MAY LEAD TO IMMEDIATE DISMISSAL OF YOUR REPORT AND POTENTIAL BLOCKLISTING.** We will not waste our time chatting with bots. But if you're a human, pull up a chair and we'll drink some chocolate milk.
|
|
||||||
|
|
||||||
We'll need enough information to verify the bug and make a patch. To speed things up, please include:
|
We'll need enough information to verify the bug and make a patch. To speed things up, please include:
|
||||||
|
|
||||||
- Most minimal possible config (without redactions!)
|
- Most minimal possible config (without redactions!)
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ jobs:
|
|||||||
models: read
|
models: read
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||||
- uses: github/ai-moderator@81159c370785e295c97461ade67d7c33576e9319
|
- uses: github/ai-moderator@6bcdb2a79c2e564db8d76d7d4439d91a044c4eb6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
spam-label: 'spam'
|
spam-label: 'spam'
|
||||||
|
|||||||
@@ -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');
|
|
||||||
|
|
||||||
+121
-15
@@ -31,13 +31,13 @@ jobs:
|
|||||||
- mac
|
- mac
|
||||||
- windows
|
- windows
|
||||||
go:
|
go:
|
||||||
- '1.26'
|
- '1.25'
|
||||||
|
|
||||||
include:
|
include:
|
||||||
# Set the minimum Go patch version for the given Go minor
|
# Set the minimum Go patch version for the given Go minor
|
||||||
# Usable via ${{ matrix.GO_SEMVER }}
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
- go: '1.26'
|
- go: '1.25'
|
||||||
GO_SEMVER: '~1.26.0'
|
GO_SEMVER: '~1.25.0'
|
||||||
|
|
||||||
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
||||||
# OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories)
|
# OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories)
|
||||||
@@ -63,17 +63,18 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
actions: write # to allow uploading artifacts and cache
|
actions: write # to allow uploading artifacts and cache
|
||||||
|
checks: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.GO_SEMVER }}
|
go-version: ${{ matrix.GO_SEMVER }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -120,7 +121,7 @@ jobs:
|
|||||||
./caddy stop
|
./caddy stop
|
||||||
|
|
||||||
- name: Publish Build Artifact
|
- name: Publish Build Artifact
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
|
name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
|
||||||
path: ${{ matrix.CADDY_BIN_PATH }}
|
path: ${{ matrix.CADDY_BIN_PATH }}
|
||||||
@@ -152,6 +153,111 @@ jobs:
|
|||||||
# echo "step_test ${{ steps.step_test.outputs.status }}\n"
|
# echo "step_test ${{ steps.step_test.outputs.status }}\n"
|
||||||
# exit 1
|
# exit 1
|
||||||
|
|
||||||
|
spec-test:
|
||||||
|
permissions:
|
||||||
|
checks: write
|
||||||
|
pull-requests: write
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- linux
|
||||||
|
go:
|
||||||
|
- '1.25'
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
|
||||||
|
# SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
|
||||||
|
- os: linux
|
||||||
|
OS_LABEL: ubuntu-latest
|
||||||
|
CADDY_BIN_PATH: ./cmd/caddy/caddy
|
||||||
|
SUCCESS: 0
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.OS_LABEL }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.GO_SEMVER }}
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Print Go version and environment
|
||||||
|
id: vars
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
printf "curl version: $(curl --version)\n"
|
||||||
|
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
|
||||||
|
printf "Git version: $(git version)\n\n"
|
||||||
|
# Calculate the short SHA1 hash of the git commit
|
||||||
|
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Get dependencies
|
||||||
|
run: |
|
||||||
|
go get -v -t ./...
|
||||||
|
# mkdir test-results
|
||||||
|
- name: Build Caddy
|
||||||
|
working-directory: ./cmd/caddy
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
run: |
|
||||||
|
go build -cover -tags nobadger,nopgx,nomysql -trimpath -ldflags="-w -s" -v
|
||||||
|
|
||||||
|
- name: Install Hurl
|
||||||
|
env:
|
||||||
|
HURL_VERSION: "7.0.0"
|
||||||
|
run: |
|
||||||
|
curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/${HURL_VERSION}/hurl_${HURL_VERSION}_amd64.deb
|
||||||
|
sudo dpkg -i hurl_${HURL_VERSION}_amd64.deb
|
||||||
|
hurl --version
|
||||||
|
|
||||||
|
- name: Run Caddy
|
||||||
|
run: |
|
||||||
|
./cmd/caddy/caddy environ
|
||||||
|
mkdir coverdir
|
||||||
|
export GOCOVERDIR=./coverdir
|
||||||
|
./cmd/caddy/caddy start
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
- name: Run tests with Hurl
|
||||||
|
run: |
|
||||||
|
mkdir hurl-report
|
||||||
|
find . -name *.hurl -exec hurl --jobs 1 --variables-file caddytest/spec/hurl_vars.properties --very-verbose --verbose --test --report-junit hurl-report/junit.xml --color {} \;
|
||||||
|
|
||||||
|
- name: Publish Test Results
|
||||||
|
uses: EnricoMi/publish-unit-test-result-action@3a74b2957438d0b6e2e61d67b05318aa25c9e6c6 # v2.20.0
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
hurl-report/junit.xml
|
||||||
|
|
||||||
|
- name: Generate Coverage Data
|
||||||
|
run: |
|
||||||
|
export GOCOVERDIR=./coverdir
|
||||||
|
./cmd/caddy/caddy stop
|
||||||
|
go tool covdata textfmt -i=coverdir -o hurl-report/caddy_cover_${{ steps.vars.outputs.short_sha }}.txt
|
||||||
|
go tool cover -html hurl-report/caddy_cover_${{ steps.vars.outputs.short_sha }}.txt -o hurl-report/caddy_cover_${{ steps.vars.outputs.short_sha }}.html
|
||||||
|
|
||||||
|
|
||||||
|
- name: Publish Coverage Profile
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
|
with:
|
||||||
|
path: hurl-report/caddy_cover_${{ steps.vars.outputs.short_sha }}.html
|
||||||
|
compression-level: 0
|
||||||
|
|
||||||
s390x-test:
|
s390x-test:
|
||||||
name: test (s390x on IBM Z)
|
name: test (s390x on IBM Z)
|
||||||
permissions:
|
permissions:
|
||||||
@@ -162,13 +268,13 @@ jobs:
|
|||||||
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
|
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
allowed-endpoints: ci-s390x.caddyserver.com:22
|
allowed-endpoints: ci-s390x.caddyserver.com:22
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
set +e
|
set +e
|
||||||
@@ -221,27 +327,27 @@ jobs:
|
|||||||
if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]'
|
if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]'
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
|
- uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: check
|
args: check
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: "~1.26"
|
go-version: "~1.25"
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Install xcaddy
|
- name: Install xcaddy
|
||||||
run: |
|
run: |
|
||||||
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
|
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
|
||||||
xcaddy version
|
xcaddy version
|
||||||
- uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
|
- uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: build --single-target --snapshot
|
args: build --single-target --snapshot
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ jobs:
|
|||||||
- 'darwin'
|
- 'darwin'
|
||||||
- 'netbsd'
|
- 'netbsd'
|
||||||
go:
|
go:
|
||||||
- '1.26'
|
- '1.25'
|
||||||
|
|
||||||
include:
|
include:
|
||||||
# Set the minimum Go patch version for the given Go minor
|
# Set the minimum Go patch version for the given Go minor
|
||||||
# Usable via ${{ matrix.GO_SEMVER }}
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
- go: '1.26'
|
- go: '1.25'
|
||||||
GO_SEMVER: '~1.26.0'
|
GO_SEMVER: '~1.25.0'
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
@@ -51,15 +51,15 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.GO_SEMVER }}
|
go-version: ${{ matrix.GO_SEMVER }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
+10
-10
@@ -45,18 +45,18 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: '~1.26'
|
go-version: '~1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
|
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
|
|
||||||
@@ -73,14 +73,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: govulncheck
|
- name: govulncheck
|
||||||
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
|
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
|
||||||
with:
|
with:
|
||||||
go-version-input: '~1.26.0'
|
go-version-input: '~1.25.0'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
dependency-review:
|
dependency-review:
|
||||||
@@ -90,14 +90,14 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: 'Checkout Repository'
|
- name: 'Checkout Repository'
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: 'Dependency Review'
|
- name: 'Dependency Review'
|
||||||
uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
|
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
|
||||||
with:
|
with:
|
||||||
comment-summary-in-pr: on-failure
|
comment-summary-in-pr: on-failure
|
||||||
# https://github.com/actions/dependency-review-action/issues/430#issuecomment-1468975566
|
# https://github.com/actions/dependency-review-action/issues/430#issuecomment-1468975566
|
||||||
|
|||||||
@@ -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@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
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
|
|
||||||
+19
-394
@@ -13,334 +13,20 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
verify-tag:
|
|
||||||
name: Verify Tag Signature and Approvals
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
outputs:
|
|
||||||
verification_passed: ${{ steps.verify.outputs.passed }}
|
|
||||||
tag_version: ${{ steps.info.outputs.version }}
|
|
||||||
proposal_issue_number: ${{ steps.find_proposal.outputs.result && fromJson(steps.find_proposal.outputs.result).number || '' }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
# Force fetch upstream tags -- because 65 minutes
|
|
||||||
# tl;dr: actions/checkout@v3 runs this line:
|
|
||||||
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
|
|
||||||
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
|
|
||||||
# git fetch --prune --unshallow
|
|
||||||
# which doesn't overwrite that tag because that would be destructive.
|
|
||||||
# Credit to @francislavoie for the investigation.
|
|
||||||
# https://github.com/actions/checkout/issues/290#issuecomment-680260080
|
|
||||||
- name: Force fetch upstream tags
|
|
||||||
run: git fetch --tags --force
|
|
||||||
|
|
||||||
- name: Get tag info
|
|
||||||
id: info
|
|
||||||
run: |
|
|
||||||
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
|
||||||
echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
|
|
||||||
- name: Print Go version and environment
|
|
||||||
id: vars
|
|
||||||
run: |
|
|
||||||
printf "Using go at: $(which go)\n"
|
|
||||||
printf "Go version: $(go version)\n"
|
|
||||||
printf "\n\nGo environment:\n\n"
|
|
||||||
go env
|
|
||||||
printf "\n\nSystem environment:\n\n"
|
|
||||||
env
|
|
||||||
echo "version_tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
|
|
||||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Add "pip install" CLI tools to PATH
|
|
||||||
echo ~/.local/bin >> $GITHUB_PATH
|
|
||||||
|
|
||||||
# Parse semver
|
|
||||||
TAG=${GITHUB_REF/refs\/tags\//}
|
|
||||||
SEMVER_RE='[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z\.-]*\)'
|
|
||||||
TAG_MAJOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\1#"`
|
|
||||||
TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"`
|
|
||||||
TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"`
|
|
||||||
TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"`
|
|
||||||
echo "tag_major=${TAG_MAJOR}" >> $GITHUB_OUTPUT
|
|
||||||
echo "tag_minor=${TAG_MINOR}" >> $GITHUB_OUTPUT
|
|
||||||
echo "tag_patch=${TAG_PATCH}" >> $GITHUB_OUTPUT
|
|
||||||
echo "tag_special=${TAG_SPECIAL}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Validate commits and tag signatures
|
|
||||||
id: verify
|
|
||||||
env:
|
|
||||||
signing_keys: ${{ secrets.SIGNING_KEYS }}
|
|
||||||
run: |
|
|
||||||
# Read the string into an array, splitting by IFS
|
|
||||||
IFS=";" read -ra keys_collection <<< "$signing_keys"
|
|
||||||
|
|
||||||
# ref: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#example-usage-of-the-runner-context
|
|
||||||
touch "${{ runner.temp }}/allowed_signers"
|
|
||||||
|
|
||||||
# Iterate and print the split elements
|
|
||||||
for item in "${keys_collection[@]}"; do
|
|
||||||
|
|
||||||
# trim leading whitespaces
|
|
||||||
item="${item##*( )}"
|
|
||||||
|
|
||||||
# trim trailing whitespaces
|
|
||||||
item="${item%%*( )}"
|
|
||||||
|
|
||||||
IFS=" " read -ra key_components <<< "$item"
|
|
||||||
# git wants it in format: email address, type, public key
|
|
||||||
# ssh has it in format: type, public key, email address
|
|
||||||
echo "${key_components[2]} namespaces=\"git\" ${key_components[0]} ${key_components[1]}" >> "${{ runner.temp }}/allowed_signers"
|
|
||||||
done
|
|
||||||
|
|
||||||
git config set --global gpg.ssh.allowedSignersFile "${{ runner.temp }}/allowed_signers"
|
|
||||||
|
|
||||||
echo "Verifying the tag: ${{ steps.vars.outputs.version_tag }}"
|
|
||||||
|
|
||||||
# Verify the tag is signed
|
|
||||||
if ! git verify-tag -v "${{ steps.vars.outputs.version_tag }}" 2>&1; then
|
|
||||||
echo "❌ Tag verification failed!"
|
|
||||||
echo "passed=false" >> $GITHUB_OUTPUT
|
|
||||||
git push --delete origin "${{ steps.vars.outputs.version_tag }}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# Run it again to capture the output
|
|
||||||
git verify-tag -v "${{ steps.vars.outputs.version_tag }}" 2>&1 | tee /tmp/verify-output.txt;
|
|
||||||
|
|
||||||
# SSH verification output typically includes the key fingerprint
|
|
||||||
# Use GNU grep with Perl regex for cleaner extraction (Linux environment)
|
|
||||||
KEY_SHA256=$(grep -oP "SHA256:[\"']?\K[A-Za-z0-9+/=]+(?=[\"']?)" /tmp/verify-output.txt | head -1 || echo "")
|
|
||||||
|
|
||||||
if [ -z "$KEY_SHA256" ]; then
|
|
||||||
# Try alternative pattern with "key" prefix
|
|
||||||
KEY_SHA256=$(grep -oP "key SHA256:[\"']?\K[A-Za-z0-9+/=]+(?=[\"']?)" /tmp/verify-output.txt | head -1 || echo "")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$KEY_SHA256" ]; then
|
|
||||||
# Fallback: extract any base64-like string (40+ chars)
|
|
||||||
KEY_SHA256=$(grep -oP '[A-Za-z0-9+/]{40,}=?' /tmp/verify-output.txt | head -1 || echo "")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$KEY_SHA256" ]; then
|
|
||||||
echo "Somehow could not extract SSH key fingerprint from git verify-tag output"
|
|
||||||
echo "Cancelling flow and deleting tag"
|
|
||||||
echo "passed=false" >> $GITHUB_OUTPUT
|
|
||||||
git push --delete origin "${{ steps.vars.outputs.version_tag }}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Tag verification succeeded!"
|
|
||||||
echo "passed=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "key_id=$KEY_SHA256" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Find related release proposal
|
|
||||||
id: find_proposal
|
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const version = '${{ steps.vars.outputs.version_tag }}';
|
|
||||||
|
|
||||||
// Search for PRs with release-proposal label that match this version
|
|
||||||
const prs = await github.rest.pulls.list({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
state: 'open', // Changed to 'all' to find both open and closed PRs
|
|
||||||
sort: 'updated',
|
|
||||||
direction: 'desc'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find the most recent PR for this version
|
|
||||||
const proposal = prs.data.find(pr =>
|
|
||||||
pr.title.includes(version) &&
|
|
||||||
pr.labels.some(label => label.name === 'release-proposal')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!proposal) {
|
|
||||||
console.log(`⚠️ No release proposal PR found for ${version}`);
|
|
||||||
console.log('This might be a hotfix or emergency release');
|
|
||||||
return { number: null, approved: true, approvals: 0, proposedCommit: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Found proposal PR #${proposal.number} for version ${version}`);
|
|
||||||
|
|
||||||
// Extract commit hash from PR body
|
|
||||||
const commitMatch = proposal.body.match(/\*\*Target Commit:\*\*\s*`([a-f0-9]+)`/);
|
|
||||||
const proposedCommit = commitMatch ? commitMatch[1] : null;
|
|
||||||
|
|
||||||
if (proposedCommit) {
|
|
||||||
console.log(`Proposal was for commit: ${proposedCommit}`);
|
|
||||||
} else {
|
|
||||||
console.log('⚠️ No target commit hash found in PR body');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get PR reviews to extract approvers
|
|
||||||
let approvers = 'Validated by automation';
|
|
||||||
let approvalCount = 2; // Minimum required
|
|
||||||
|
|
||||||
try {
|
|
||||||
const reviews = await github.rest.pulls.listReviews({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
pull_number: proposal.number
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get latest review per user and filter for approvals
|
|
||||||
const latestReviewsByUser = {};
|
|
||||||
reviews.data.forEach(review => {
|
|
||||||
const username = review.user.login;
|
|
||||||
if (!latestReviewsByUser[username] || new Date(review.submitted_at) > new Date(latestReviewsByUser[username].submitted_at)) {
|
|
||||||
latestReviewsByUser[username] = review;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const approvalReviews = Object.values(latestReviewsByUser).filter(review =>
|
|
||||||
review.state === 'APPROVED'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (approvalReviews.length > 0) {
|
|
||||||
approvers = approvalReviews.map(r => '@' + r.user.login).join(', ');
|
|
||||||
approvalCount = approvalReviews.length;
|
|
||||||
console.log(`Found ${approvalCount} approvals from: ${approvers}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Could not fetch reviews: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
number: proposal.number,
|
|
||||||
approved: true,
|
|
||||||
approvals: approvalCount,
|
|
||||||
approvers: approvers,
|
|
||||||
proposedCommit: proposedCommit
|
|
||||||
};
|
|
||||||
result-encoding: json
|
|
||||||
|
|
||||||
- name: Verify proposal commit
|
|
||||||
run: |
|
|
||||||
APPROVALS='${{ steps.find_proposal.outputs.result }}'
|
|
||||||
|
|
||||||
# Parse JSON
|
|
||||||
PROPOSED_COMMIT=$(echo "$APPROVALS" | jq -r '.proposedCommit')
|
|
||||||
CURRENT_COMMIT="${{ steps.info.outputs.sha }}"
|
|
||||||
|
|
||||||
echo "Proposed commit: $PROPOSED_COMMIT"
|
|
||||||
echo "Current commit: $CURRENT_COMMIT"
|
|
||||||
|
|
||||||
# Check if commits match (if proposal had a target commit)
|
|
||||||
if [ "$PROPOSED_COMMIT" != "null" ] && [ -n "$PROPOSED_COMMIT" ]; then
|
|
||||||
# Normalize both commits to full SHA for comparison
|
|
||||||
PROPOSED_FULL=$(git rev-parse "$PROPOSED_COMMIT" 2>/dev/null || echo "")
|
|
||||||
CURRENT_FULL=$(git rev-parse "$CURRENT_COMMIT" 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
if [ -z "$PROPOSED_FULL" ]; then
|
|
||||||
echo "⚠️ Could not resolve proposed commit: $PROPOSED_COMMIT"
|
|
||||||
elif [ "$PROPOSED_FULL" != "$CURRENT_FULL" ]; then
|
|
||||||
echo "❌ Commit mismatch!"
|
|
||||||
echo "The tag points to commit $CURRENT_FULL but the proposal was for $PROPOSED_FULL"
|
|
||||||
echo "This indicates an error in tag creation."
|
|
||||||
# Delete the tag remotely
|
|
||||||
git push --delete origin "${{ steps.vars.outputs.version_tag }}"
|
|
||||||
echo "Tag ${{steps.vars.outputs.version_tag}} has been deleted"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "✅ Commit hash matches proposal"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "⚠️ No target commit found in proposal (might be legacy release)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Tag verification completed"
|
|
||||||
|
|
||||||
- name: Update release proposal PR
|
|
||||||
if: fromJson(steps.find_proposal.outputs.result).number != null
|
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const result = ${{ steps.find_proposal.outputs.result }};
|
|
||||||
|
|
||||||
if (result.number) {
|
|
||||||
// Add in-progress label
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: result.number,
|
|
||||||
labels: ['release-in-progress']
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove approved label if present
|
|
||||||
try {
|
|
||||||
await github.rest.issues.removeLabel({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: result.number,
|
|
||||||
name: 'approved'
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Approved label not found:', e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const commentBody = [
|
|
||||||
'## 🚀 Release Workflow Started',
|
|
||||||
'',
|
|
||||||
'- **Tag:** ${{ steps.info.outputs.version }}',
|
|
||||||
'- **Signed by key:** ${{ steps.verify.outputs.key_id }}',
|
|
||||||
'- **Commit:** ${{ steps.info.outputs.sha }}',
|
|
||||||
'- **Approved by:** ' + result.approvers,
|
|
||||||
'',
|
|
||||||
'Release workflow is now running. This PR will be updated when the release is published.'
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: result.number,
|
|
||||||
body: commentBody
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
run: |
|
|
||||||
APPROVALS='${{ steps.find_proposal.outputs.result }}'
|
|
||||||
PROPOSED_COMMIT=$(echo "$APPROVALS" | jq -r '.proposedCommit // "N/A"')
|
|
||||||
APPROVERS=$(echo "$APPROVALS" | jq -r '.approvers // "N/A"')
|
|
||||||
|
|
||||||
echo "## Tag Verification Summary 🔐" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Tag:** ${{ steps.info.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Commit:** ${{ steps.info.outputs.sha }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Proposed Commit:** $PROPOSED_COMMIT" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Signature:** ✅ Verified" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Signed by:** ${{ steps.verify.outputs.key_id }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Approvals:** ✅ Sufficient" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Approved by:** $APPROVERS" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Proceeding with release build..." >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Release
|
name: Release
|
||||||
needs: verify-tag
|
|
||||||
if: ${{ needs.verify-tag.outputs.verification_passed == 'true' }}
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os:
|
os:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
go:
|
go:
|
||||||
- '1.26'
|
- '1.25'
|
||||||
|
|
||||||
include:
|
include:
|
||||||
# Set the minimum Go patch version for the given Go minor
|
# Set the minimum Go patch version for the given Go minor
|
||||||
# Usable via ${{ matrix.GO_SEMVER }}
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
- go: '1.26'
|
- go: '1.25'
|
||||||
GO_SEMVER: '~1.26.0'
|
GO_SEMVER: '~1.25.0'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
|
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
|
||||||
@@ -350,28 +36,26 @@ jobs:
|
|||||||
# https://docs.github.com/en/rest/overview/permissions-required-for-github-apps#permission-on-contents
|
# https://docs.github.com/en/rest/overview/permissions-required-for-github-apps#permission-on-contents
|
||||||
# "Releases" is part of `contents`, so it needs the `write`
|
# "Releases" is part of `contents`, so it needs the `write`
|
||||||
contents: write
|
contents: write
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.GO_SEMVER }}
|
go-version: ${{ matrix.GO_SEMVER }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
# Force fetch upstream tags -- because 65 minutes
|
# Force fetch upstream tags -- because 65 minutes
|
||||||
# tl;dr: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 runs this line:
|
# tl;dr: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2 runs this line:
|
||||||
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
|
# 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:
|
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
|
||||||
# git fetch --prune --unshallow
|
# git fetch --prune --unshallow
|
||||||
@@ -414,12 +98,22 @@ jobs:
|
|||||||
- name: Install Cloudsmith CLI
|
- name: Install Cloudsmith CLI
|
||||||
run: pip install --upgrade cloudsmith-cli
|
run: pip install --upgrade cloudsmith-cli
|
||||||
|
|
||||||
|
- name: Validate commits and tag signatures
|
||||||
|
run: |
|
||||||
|
|
||||||
|
# Import Matt Holt's key
|
||||||
|
curl 'https://github.com/mholt.gpg' | gpg --import
|
||||||
|
|
||||||
|
echo "Verifying the tag: ${{ steps.vars.outputs.version_tag }}"
|
||||||
|
# tags are only accepted if signed by Matt's key
|
||||||
|
git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # main
|
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # main
|
||||||
- name: Cosign version
|
- name: Cosign version
|
||||||
run: cosign version
|
run: cosign version
|
||||||
- name: Install Syft
|
- name: Install Syft
|
||||||
uses: anchore/sbom-action/download-syft@17ae1740179002c89186b61233e0f892c3118b11 # main
|
uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # main
|
||||||
- name: Syft version
|
- name: Syft version
|
||||||
run: syft version
|
run: syft version
|
||||||
- name: Install xcaddy
|
- name: Install xcaddy
|
||||||
@@ -428,7 +122,7 @@ jobs:
|
|||||||
xcaddy version
|
xcaddy version
|
||||||
# GoReleaser will take care of publishing those artifacts into the release
|
# GoReleaser will take care of publishing those artifacts into the release
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
|
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --clean --timeout 60m
|
args: release --clean --timeout 60m
|
||||||
@@ -494,72 +188,3 @@ jobs:
|
|||||||
echo "Pushing $filename to 'testing'"
|
echo "Pushing $filename to 'testing'"
|
||||||
cloudsmith push deb caddy/testing/any-distro/any-version $filename
|
cloudsmith push deb caddy/testing/any-distro/any-version $filename
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Update release proposal PR
|
|
||||||
if: needs.verify-tag.outputs.proposal_issue_number != ''
|
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const prNumber = parseInt('${{ needs.verify-tag.outputs.proposal_issue_number }}');
|
|
||||||
|
|
||||||
if (prNumber) {
|
|
||||||
// Get PR details to find the branch
|
|
||||||
const pr = await github.rest.pulls.get({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
pull_number: prNumber
|
|
||||||
});
|
|
||||||
|
|
||||||
const branchName = pr.data.head.ref;
|
|
||||||
|
|
||||||
// Remove in-progress label
|
|
||||||
try {
|
|
||||||
await github.rest.issues.removeLabel({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: prNumber,
|
|
||||||
name: 'release-in-progress'
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Label not found:', e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add released label
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: prNumber,
|
|
||||||
labels: ['released']
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add final comment
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: prNumber,
|
|
||||||
body: '## ✅ Release Published\n\nThe release has been successfully published and is now available.'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close the PR if it's still open
|
|
||||||
if (pr.data.state === 'open') {
|
|
||||||
await github.rest.pulls.update({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
pull_number: prNumber,
|
|
||||||
state: 'closed'
|
|
||||||
});
|
|
||||||
console.log(`Closed PR #${prNumber}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the branch
|
|
||||||
try {
|
|
||||||
await github.rest.git.deleteRef({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
ref: `heads/${branchName}`
|
|
||||||
});
|
|
||||||
console.log(`Deleted branch: ${branchName}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`Could not delete branch ${branchName}: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ jobs:
|
|||||||
|
|
||||||
# See https://github.com/peter-evans/repository-dispatch
|
# See https://github.com/peter-evans/repository-dispatch
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Trigger event on caddyserver/dist
|
- name: Trigger event on caddyserver/dist
|
||||||
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
|
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||||
repository: caddyserver/dist
|
repository: caddyserver/dist
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
|
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
|
||||||
|
|
||||||
- name: Trigger event on caddyserver/caddy-docker
|
- name: Trigger event on caddyserver/caddy-docker
|
||||||
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
|
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||||
repository: caddyserver/caddy-docker
|
repository: caddyserver/caddy-docker
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||||
# format to the repository Actions tab.
|
# format to the repository Actions tab.
|
||||||
- name: "Upload artifact"
|
- name: "Upload artifact"
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
name: SARIF file
|
name: SARIF file
|
||||||
path: results.sarif
|
path: results.sarif
|
||||||
@@ -81,6 +81,6 @@ jobs:
|
|||||||
# Upload the results to GitHub's code scanning dashboard (optional).
|
# Upload the results to GitHub's code scanning dashboard (optional).
|
||||||
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
|
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5
|
uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ linters:
|
|||||||
- importas
|
- importas
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- misspell
|
- misspell
|
||||||
- modernize
|
|
||||||
- prealloc
|
- prealloc
|
||||||
- promlinter
|
- promlinter
|
||||||
- sloglint
|
- sloglint
|
||||||
|
|||||||
+1
-3
@@ -13,7 +13,7 @@ before:
|
|||||||
- cp cmd/caddy/main.go caddy-build/main.go
|
- cp cmd/caddy/main.go caddy-build/main.go
|
||||||
- /bin/sh -c 'cd ./caddy-build && go mod init caddy'
|
- /bin/sh -c 'cd ./caddy-build && go mod init caddy'
|
||||||
# prepare syso files for windows embedding
|
# prepare syso files for windows embedding
|
||||||
- /bin/sh -c 'for a in amd64 arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a xcaddy build {{.Env.TAG}}; done'
|
- /bin/sh -c 'for a in amd64 arm arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a xcaddy build {{.Env.TAG}}; done'
|
||||||
- /bin/sh -c 'mv /tmp/buildenv_*/*.syso caddy-build'
|
- /bin/sh -c 'mv /tmp/buildenv_*/*.syso caddy-build'
|
||||||
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
|
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
|
||||||
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
|
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
|
||||||
@@ -67,8 +67,6 @@ builds:
|
|||||||
goarch: s390x
|
goarch: s390x
|
||||||
- goos: windows
|
- goos: windows
|
||||||
goarch: riscv64
|
goarch: riscv64
|
||||||
- goos: windows
|
|
||||||
goarch: arm
|
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: ppc64le
|
goarch: ppc64le
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
|
|||||||
@@ -1,217 +0,0 @@
|
|||||||
# Caddy Project Guidelines
|
|
||||||
|
|
||||||
## Mission
|
|
||||||
|
|
||||||
**Every site on HTTPS.** Caddy is a security-first, modular, extensible server platform.
|
|
||||||
|
|
||||||
## Code Style
|
|
||||||
|
|
||||||
### Go Idioms
|
|
||||||
|
|
||||||
Follow [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments):
|
|
||||||
|
|
||||||
- **Error flow**: Early return, indent error handling—not else blocks
|
|
||||||
```go
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// normal code
|
|
||||||
```
|
|
||||||
- **Naming**: initialisms (`URL`, `HTTP`, `ID`—not `Url`, `Http`, `Id`)
|
|
||||||
- **Receiver names**: 1–2 letters reflecting type (`c` for `Client`, `h` for `Handler`)
|
|
||||||
- **Error strings**: Lowercase, no trailing punctuation (`"something failed"` not `"Something failed."`)
|
|
||||||
- **Doc comments**: Full sentences starting with the name being documented
|
|
||||||
```go
|
|
||||||
// Handler serves HTTP requests for the file server.
|
|
||||||
type Handler struct { ... }
|
|
||||||
```
|
|
||||||
- **Empty slices**: `var t []string` (nil slice), not `t := []string{}` (non-nil zero-length)
|
|
||||||
- **Don't panic**: Use error returns for normal error handling
|
|
||||||
|
|
||||||
### Caddy Patterns
|
|
||||||
|
|
||||||
**Module registration**:
|
|
||||||
```go
|
|
||||||
func init() {
|
|
||||||
caddy.RegisterModule(MyModule{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (MyModule) CaddyModule() caddy.ModuleInfo {
|
|
||||||
return caddy.ModuleInfo{
|
|
||||||
ID: "namespace.category.name",
|
|
||||||
New: func() caddy.Module { return new(MyModule) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Module lifecycle**: `New()` → JSON unmarshal → `Provision()` → `Validate()` → use → `Cleanup()`
|
|
||||||
|
|
||||||
**Interface guards** — compile-time verification that modules implement required interfaces:
|
|
||||||
```go
|
|
||||||
var (
|
|
||||||
_ caddy.Provisioner = (*MyModule)(nil)
|
|
||||||
_ caddy.Validator = (*MyModule)(nil)
|
|
||||||
_ caddyfile.Unmarshaler = (*MyModule)(nil)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Structured logging** — use the module-scoped logger from context:
|
|
||||||
```go
|
|
||||||
func (m *MyModule) Provision(ctx caddy.Context) error {
|
|
||||||
m.logger = ctx.Logger()
|
|
||||||
m.logger.Debug("provisioning", zap.String("field", m.Field))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Caddyfile support** — implement `UnmarshalCaddyfile(*caddyfile.Dispenser)` using the `Dispenser` API:
|
|
||||||
```go
|
|
||||||
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
|
|
||||||
//
|
|
||||||
// directive [arg1] [arg2] {
|
|
||||||
// subdir value
|
|
||||||
// }
|
|
||||||
func (m *MyModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|
||||||
d.Next() // consume directive name
|
|
||||||
for d.NextArg() {
|
|
||||||
// handle inline arguments
|
|
||||||
}
|
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
|
||||||
switch d.Val() {
|
|
||||||
case "subdir":
|
|
||||||
if !d.NextArg() {
|
|
||||||
return d.ArgErr()
|
|
||||||
}
|
|
||||||
m.Field = d.Val()
|
|
||||||
default:
|
|
||||||
return d.Errf("unrecognized subdirective: %s", d.Val())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Admin API**: Implement `caddy.AdminRouter` for custom endpoints.
|
|
||||||
|
|
||||||
**Context**: Use `caddy.Context` for accessing other apps/modules and logging—don't store contexts in structs.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
Caddy is built around a **module system** where everything is a module registered via `caddy.RegisterModule()`:
|
|
||||||
|
|
||||||
- **Apps** (`caddy.App`): Top-level modules like `http`, `tls`, `pki` that Caddy loads and runs
|
|
||||||
- **Modules** (`caddy.Module`): Extensible components with namespaced IDs (e.g., `http.handlers.file_server`)
|
|
||||||
- **Configuration**: Native JSON with adapters (Caddyfile → JSON via `caddyconfig/httpcaddyfile`)
|
|
||||||
|
|
||||||
| Directory | Purpose |
|
|
||||||
|-----------|---------|
|
|
||||||
| `modules/` | All standard modules (HTTP, TLS, PKI, etc.) |
|
|
||||||
| `modules/standard/imports.go` | Standard module registry |
|
|
||||||
| `caddyconfig/httpcaddyfile/` | Caddyfile → JSON adapter for HTTP |
|
|
||||||
| `caddytest/` | Test utilities and integration tests |
|
|
||||||
| `cmd/caddy/` | CLI entry point with module imports |
|
|
||||||
|
|
||||||
### Critical Packages
|
|
||||||
|
|
||||||
`caddyhttp` and `caddytls` require **extra scrutiny** in code review—these are security-critical.
|
|
||||||
|
|
||||||
## Quality Gates
|
|
||||||
|
|
||||||
|
|
||||||
**All required before PR is merge-ready:**
|
|
||||||
|
|
||||||
| Gate | Command | Notes |
|
|
||||||
|------|---------|-------|
|
|
||||||
| Tests pass | `go test -race -short ./...` | Race detection enabled |
|
|
||||||
| Lint clean | `golangci-lint run --timeout 10m` | No warnings in changed files |
|
|
||||||
| Builds | `go build ./...` | Must compile |
|
|
||||||
| Benchmarks | `go test -bench=. -benchmem` | Required for optimizations |
|
|
||||||
|
|
||||||
CI runs tests on **Linux, macOS, and Windows**—ensure cross-platform compatibility.
|
|
||||||
|
|
||||||
### Build & Test
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build
|
|
||||||
cd cmd/caddy && go build
|
|
||||||
|
|
||||||
# Tests with race detection (matches CI)
|
|
||||||
go test -race -short ./...
|
|
||||||
|
|
||||||
# Integration tests
|
|
||||||
go test ./caddytest/integration/...
|
|
||||||
|
|
||||||
# Lint (matches CI)
|
|
||||||
golangci-lint run --timeout 10m
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Conventions
|
|
||||||
|
|
||||||
**Table-driven tests** (preferred pattern):
|
|
||||||
```go
|
|
||||||
func TestFeature(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
input string
|
|
||||||
expected string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{input: "valid", expected: "result", wantErr: false},
|
|
||||||
{input: "invalid", expected: "", wantErr: true},
|
|
||||||
} {
|
|
||||||
actual, err := Function(tc.input)
|
|
||||||
if tc.wantErr && err == nil {
|
|
||||||
t.Errorf("Test %d: expected error but got none", i)
|
|
||||||
}
|
|
||||||
if !tc.wantErr && err != nil {
|
|
||||||
t.Errorf("Test %d: unexpected error: %v", i, err)
|
|
||||||
}
|
|
||||||
if actual != tc.expected {
|
|
||||||
t.Errorf("Test %d: expected %q, got %q", i, tc.expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Integration tests** use `caddytest.Tester`:
|
|
||||||
```go
|
|
||||||
func TestHTTPFeature(t *testing.T) {
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.InitServer(`
|
|
||||||
{
|
|
||||||
admin localhost:2999
|
|
||||||
http_port 9080
|
|
||||||
}
|
|
||||||
localhost:9080 {
|
|
||||||
respond "hello"
|
|
||||||
}`, "caddyfile")
|
|
||||||
|
|
||||||
tester.AssertGetResponse("http://localhost:9080/", 200, "hello")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Use non-standard ports (9080, 9443, 2999) to avoid conflicts with running servers.
|
|
||||||
|
|
||||||
## AI Contribution Policy
|
|
||||||
|
|
||||||
Per [CONTRIBUTING.md](.github/CONTRIBUTING.md), AI-assisted code **MUST** be:
|
|
||||||
|
|
||||||
1. **Disclosed** — Tell reviewers when code was AI-generated or AI-assisted, mentioning which agent/model is used
|
|
||||||
2. **Fully comprehended** — You must be able to explain every line
|
|
||||||
3. **Tested** — Automated tests when feasible, thorough manual tests otherwise
|
|
||||||
4. **Licensed** — Verify AI output doesn't include plagiarized or incompatibly-licensed code
|
|
||||||
5. **Contributor License Agreement (CLA)** — The CLA must be signed by the human user
|
|
||||||
|
|
||||||
**Do NOT submit code you cannot fully explain.** Contributors are responsible for their submissions.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- **Avoid new dependencies** — Justify any additions; tiny deps can be inlined
|
|
||||||
- **No exported dependency types** — Caddy must not export types defined by external packages
|
|
||||||
- Use Go modules; check with `go mod tidy`
|
|
||||||
|
|
||||||
## Further Reading
|
|
||||||
|
|
||||||
- [CONTRIBUTING.md](.github/CONTRIBUTING.md) — Full PR process and expectations
|
|
||||||
- [Extending Caddy](https://caddyserver.com/docs/extending-caddy) — Module development guide
|
|
||||||
- [JSON Config](https://caddyserver.com/docs/json/) — Native configuration reference
|
|
||||||
- [Caddyfile](https://caddyserver.com/docs/caddyfile/concepts) — Caddyfile syntax guide
|
|
||||||
@@ -12,52 +12,24 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<h3 align="center">Every site on HTTPS</h3>
|
<h3 align="center">Every site on HTTPS</h3>
|
||||||
<p align="center">Caddy is an extensible server platform that uses TLS by default.</p>
|
<p align="center">Caddy is an extensible server platform that uses TLS by default.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/caddyserver/caddy/actions/workflows/ci.yml"><img src="https://github.com/caddyserver/caddy/actions/workflows/ci.yml/badge.svg"></a>
|
||||||
|
<a href="https://www.bestpractices.dev/projects/7141"><img src="https://www.bestpractices.dev/projects/7141/badge"></a>
|
||||||
|
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-%23007d9c.svg"></a>
|
||||||
|
<br>
|
||||||
|
<a href="https://x.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/twitter/follow/caddyserver" alt="@caddyserver on Twitter"></a>
|
||||||
|
<a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
|
||||||
|
<br>
|
||||||
|
<a href="https://sourcegraph.com/github.com/caddyserver/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/caddyserver/caddy/-/badge.svg" alt="Caddy on Sourcegraph"></a>
|
||||||
|
<a href="https://cloudsmith.io/~caddy/repos/"><img src="https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith" alt="Cloudsmith"></a>
|
||||||
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/caddyserver/caddy/releases">Releases</a> ·
|
<a href="https://github.com/caddyserver/caddy/releases">Releases</a> ·
|
||||||
<a href="https://caddyserver.com/docs/">Documentation</a> ·
|
<a href="https://caddyserver.com/docs/">Documentation</a> ·
|
||||||
<a href="https://caddy.community">Get Help</a>
|
<a href="https://caddy.community">Get Help</a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
|
||||||
<a href="https://github.com/caddyserver/caddy/actions/workflows/ci.yml"><img src="https://github.com/caddyserver/caddy/actions/workflows/ci.yml/badge.svg"></a>
|
|
||||||
|
|
||||||
<a href="https://www.bestpractices.dev/projects/7141"><img src="https://www.bestpractices.dev/projects/7141/badge"></a>
|
|
||||||
|
|
||||||
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-%23007d9c.svg"></a>
|
|
||||||
|
|
||||||
<a href="https://x.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/twitter/follow/caddyserver" alt="@caddyserver on Twitter"></a>
|
|
||||||
|
|
||||||
<a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
|
|
||||||
<br>
|
|
||||||
<a href="https://sourcegraph.com/github.com/caddyserver/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/caddyserver/caddy/-/badge.svg" alt="Caddy on Sourcegraph"></a>
|
|
||||||
|
|
||||||
<a href="https://cloudsmith.io/~caddy/repos/"><img src="https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith" alt="Cloudsmith"></a>
|
|
||||||
</p>
|
|
||||||
<p align="center">
|
|
||||||
<b>Powered by</b>
|
|
||||||
<br>
|
|
||||||
<a href="https://github.com/caddyserver/certmagic">
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/55066419/206946718-740b6371-3df3-4d72-a822-47e4c48af999.png">
|
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png">
|
|
||||||
<img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250">
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Warp sponsorship requests this section -->
|
|
||||||
<div align="center" markdown="1">
|
|
||||||
<hr>
|
|
||||||
<sup>Special thanks to:</sup>
|
|
||||||
<br>
|
|
||||||
<a href="https://go.warp.dev/caddy">
|
|
||||||
<img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/c8efffde-18c7-4af4-83ed-b1aba2dda394">
|
|
||||||
</a>
|
|
||||||
|
|
||||||
### [Warp, built for coding with multiple AI agents](https://go.warp.dev/caddy)
|
|
||||||
[Available for MacOS, Linux, & Windows](https://go.warp.dev/caddy)<br>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
### Menu
|
### Menu
|
||||||
|
|
||||||
@@ -72,6 +44,18 @@
|
|||||||
- [Getting help](#getting-help)
|
- [Getting help](#getting-help)
|
||||||
- [About](#about)
|
- [About](#about)
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<b>Powered by</b>
|
||||||
|
<br>
|
||||||
|
<a href="https://github.com/caddyserver/certmagic">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/55066419/206946718-740b6371-3df3-4d72-a822-47e4c48af999.png">
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png">
|
||||||
|
<img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
## [Features](https://caddyserver.com/features)
|
## [Features](https://caddyserver.com/features)
|
||||||
|
|
||||||
@@ -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.
|
replacing `username` with your actual username. Please be careful and only do this if you know what you are doing! We are only qualified to document how to use Caddy, not Go tooling or your computer, and we are providing these instructions for convenience only; please learn how to use your own computer at your own risk and make any needful adjustments.
|
||||||
|
|
||||||
Then you can run the tests in all modules or a specific one:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ go test ./...
|
|
||||||
$ go test ./modules/caddyhttp/tracing/
|
|
||||||
```
|
|
||||||
|
|
||||||
### With version information and/or plugins
|
### With version information and/or plugins
|
||||||
|
|
||||||
Using [our builder tool, `xcaddy`](https://github.com/caddyserver/xcaddy)...
|
Using [our builder tool, `xcaddy`](https://github.com/caddyserver/xcaddy)...
|
||||||
|
|
||||||
```bash
|
```
|
||||||
$ xcaddy build
|
$ xcaddy build
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -220,6 +197,6 @@ Matthew Holt began developing Caddy in 2014 while studying computer science at B
|
|||||||
- _Project on X: [@caddyserver](https://x.com/caddyserver)_
|
- _Project on X: [@caddyserver](https://x.com/caddyserver)_
|
||||||
- _Author on X: [@mholt6](https://x.com/mholt6)_
|
- _Author on X: [@mholt6](https://x.com/mholt6)_
|
||||||
|
|
||||||
Caddy is a project of [ZeroSSL](https://zerossl.com), an HID Global company.
|
Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company.
|
||||||
|
|
||||||
Debian package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that enables your organization to create, store and share packages in any format, to any place, with total confidence.
|
Debian package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that enables your organization to create, store and share packages in any format, to any place, with total confidence.
|
||||||
|
|||||||
@@ -45,16 +45,8 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/internal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// testCertMagicStorageOverride is a package-level test hook. Tests may set
|
|
||||||
// this variable to provide a temporary certmagic.Storage so that cert
|
|
||||||
// management in tests does not hit the real default storage on disk.
|
|
||||||
// This must NOT be set in production code.
|
|
||||||
var testCertMagicStorageOverride certmagic.Storage
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// The hard-coded default `DefaultAdminListen` can be overridden
|
// The hard-coded default `DefaultAdminListen` can be overridden
|
||||||
// by setting the `CADDY_ADMIN` environment variable.
|
// by setting the `CADDY_ADMIN` environment variable.
|
||||||
@@ -120,6 +112,10 @@ type AdminConfig struct {
|
|||||||
//
|
//
|
||||||
// EXPERIMENTAL: This feature is subject to change.
|
// EXPERIMENTAL: This feature is subject to change.
|
||||||
Remote *RemoteAdmin `json:"remote,omitempty"`
|
Remote *RemoteAdmin `json:"remote,omitempty"`
|
||||||
|
|
||||||
|
// Holds onto the routers so that we can later provision them
|
||||||
|
// if they require provisioning.
|
||||||
|
routers []AdminRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigSettings configures the management of configuration.
|
// ConfigSettings configures the management of configuration.
|
||||||
@@ -208,8 +204,8 @@ type AdminAccess struct {
|
|||||||
// AdminPermissions specifies what kinds of requests are allowed
|
// AdminPermissions specifies what kinds of requests are allowed
|
||||||
// to be made to the admin endpoint.
|
// to be made to the admin endpoint.
|
||||||
type AdminPermissions struct {
|
type AdminPermissions struct {
|
||||||
// The API paths allowed. A request path must either equal an
|
// The API paths allowed. Paths are simple prefix matches.
|
||||||
// allowed path or be a subpath with a path-segment boundary.
|
// Any subpath of the specified paths will be allowed.
|
||||||
Paths []string `json:"paths,omitempty"`
|
Paths []string `json:"paths,omitempty"`
|
||||||
|
|
||||||
// The HTTP methods allowed for the given paths.
|
// The HTTP methods allowed for the given paths.
|
||||||
@@ -218,7 +214,7 @@ type AdminPermissions struct {
|
|||||||
|
|
||||||
// newAdminHandler reads admin's config and returns an http.Handler suitable
|
// newAdminHandler reads admin's config and returns an http.Handler suitable
|
||||||
// for use in an admin endpoint server, which will be listening on listenAddr.
|
// for use in an admin endpoint server, which will be listening on listenAddr.
|
||||||
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, ctx Context) (adminHandler, error) {
|
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Context) adminHandler {
|
||||||
muxWrap := adminHandler{mux: http.NewServeMux()}
|
muxWrap := adminHandler{mux: http.NewServeMux()}
|
||||||
|
|
||||||
// secure the local or remote endpoint respectively
|
// secure the local or remote endpoint respectively
|
||||||
@@ -275,21 +271,34 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, ctx
|
|||||||
// register third-party module endpoints
|
// register third-party module endpoints
|
||||||
for _, m := range GetModules("admin.api") {
|
for _, m := range GetModules("admin.api") {
|
||||||
router := m.New().(AdminRouter)
|
router := m.New().(AdminRouter)
|
||||||
|
|
||||||
// provision the router before registering its routes, so
|
|
||||||
// handlers have access to all provisioned state
|
|
||||||
if provisioner, ok := router.(Provisioner); ok {
|
|
||||||
if err := provisioner.Provision(ctx); err != nil {
|
|
||||||
return adminHandler{}, fmt.Errorf("provisioning admin router module %s: %v", m.ID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, route := range router.Routes() {
|
for _, route := range router.Routes() {
|
||||||
addRoute(route.Pattern, handlerLabel, route.Handler)
|
addRoute(route.Pattern, handlerLabel, route.Handler)
|
||||||
}
|
}
|
||||||
|
admin.routers = append(admin.routers, router)
|
||||||
}
|
}
|
||||||
|
|
||||||
return muxWrap, nil
|
return muxWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
// provisionAdminRouters provisions all the router modules
|
||||||
|
// in the admin.api namespace that need provisioning.
|
||||||
|
func (admin *AdminConfig) provisionAdminRouters(ctx Context) error {
|
||||||
|
for _, router := range admin.routers {
|
||||||
|
provisioner, ok := router.(Provisioner)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := provisioner.Provision(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We no longer need the routers once provisioned, allow for GC
|
||||||
|
admin.routers = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// allowedOrigins returns a list of origins that are allowed.
|
// allowedOrigins returns a list of origins that are allowed.
|
||||||
@@ -413,7 +422,11 @@ func replaceLocalAdminServer(cfg *Config, ctx Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
handler, err := cfg.Admin.newAdminHandler(addr, false, ctx)
|
handler := cfg.Admin.newAdminHandler(addr, false, ctx)
|
||||||
|
|
||||||
|
// run the provisioners for loaded modules to make sure local
|
||||||
|
// state is properly re-initialized in the new admin server
|
||||||
|
err = cfg.Admin.provisionAdminRouters(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -537,7 +550,11 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
|
|||||||
|
|
||||||
// make the HTTP handler but disable Host/Origin enforcement
|
// make the HTTP handler but disable Host/Origin enforcement
|
||||||
// because we are using TLS authentication instead
|
// because we are using TLS authentication instead
|
||||||
handler, err := cfg.Admin.newAdminHandler(addr, true, ctx)
|
handler := cfg.Admin.newAdminHandler(addr, true, ctx)
|
||||||
|
|
||||||
|
// run the provisioners for loaded modules to make sure local
|
||||||
|
// state is properly re-initialized in the new admin server
|
||||||
|
err = cfg.Admin.provisionAdminRouters(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -616,19 +633,8 @@ func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool)
|
|||||||
// certmagic config, although it'll be mostly useless for remote management
|
// certmagic config, although it'll be mostly useless for remote management
|
||||||
ident = new(IdentityConfig)
|
ident = new(IdentityConfig)
|
||||||
}
|
}
|
||||||
// Choose storage: prefer the package-level test override when present,
|
|
||||||
// otherwise use the configured DefaultStorage. Tests may set an override
|
|
||||||
// to divert storage into a temporary location. Otherwise, in production
|
|
||||||
// we use the DefaultStorage since we don't want to act as part of a
|
|
||||||
// cluster; this storage is for the server's local identity only.
|
|
||||||
var storage certmagic.Storage
|
|
||||||
if testCertMagicStorageOverride != nil {
|
|
||||||
storage = testCertMagicStorageOverride
|
|
||||||
} else {
|
|
||||||
storage = DefaultStorage
|
|
||||||
}
|
|
||||||
template := certmagic.Config{
|
template := certmagic.Config{
|
||||||
Storage: storage,
|
Storage: DefaultStorage, // do not act as part of a cluster (this is for the server's local identity)
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
Issuers: ident.issuers,
|
Issuers: ident.issuers,
|
||||||
}
|
}
|
||||||
@@ -693,7 +699,7 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
|
|||||||
// verify path
|
// verify path
|
||||||
pathFound := accessPerm.Paths == nil
|
pathFound := accessPerm.Paths == nil
|
||||||
for _, allowedPath := range accessPerm.Paths {
|
for _, allowedPath := range accessPerm.Paths {
|
||||||
if adminPathAllowed(r.URL.Path, allowedPath) {
|
if strings.HasPrefix(r.URL.Path, allowedPath) {
|
||||||
pathFound = true
|
pathFound = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -722,31 +728,14 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func adminPathAllowed(reqPath, allowedPath string) bool {
|
|
||||||
if allowedPath == "" || allowedPath == "/" {
|
|
||||||
return strings.HasPrefix(reqPath, allowedPath)
|
|
||||||
}
|
|
||||||
if reqPath == allowedPath {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(allowedPath, "/") {
|
|
||||||
return strings.HasPrefix(reqPath, allowedPath)
|
|
||||||
}
|
|
||||||
return strings.HasPrefix(reqPath, allowedPath+"/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopAdminServer(srv *http.Server) error {
|
func stopAdminServer(srv *http.Server) error {
|
||||||
if srv == nil {
|
if srv == nil {
|
||||||
return fmt.Errorf("no admin server")
|
return fmt.Errorf("no admin server")
|
||||||
}
|
}
|
||||||
timeout := 10 * time.Second
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
ctx, cancel := context.WithTimeoutCause(context.Background(), timeout, fmt.Errorf("stopping admin server: %ds timeout", int(timeout.Seconds())))
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
err := srv.Shutdown(ctx)
|
err := srv.Shutdown(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) {
|
|
||||||
err = cause
|
|
||||||
}
|
|
||||||
return fmt.Errorf("shutting down admin server: %v", err)
|
return fmt.Errorf("shutting down admin server: %v", err)
|
||||||
}
|
}
|
||||||
Log().Named("admin").Info("stopped previous server", zap.String("address", srv.Addr))
|
Log().Named("admin").Info("stopped previous server", zap.String("address", srv.Addr))
|
||||||
@@ -790,7 +779,7 @@ func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
zap.String("uri", r.RequestURI),
|
zap.String("uri", r.RequestURI),
|
||||||
zap.String("remote_ip", ip),
|
zap.String("remote_ip", ip),
|
||||||
zap.String("remote_port", port),
|
zap.String("remote_port", port),
|
||||||
zap.Object("headers", internal.LoggableHTTPHeader{Header: r.Header}),
|
zap.Reflect("headers", r.Header),
|
||||||
)
|
)
|
||||||
if r.TLS != nil {
|
if r.TLS != nil {
|
||||||
log = log.With(
|
log = log.With(
|
||||||
@@ -818,37 +807,11 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// common mitigations in browser contexts
|
|
||||||
if strings.Contains(r.Header.Get("Upgrade"), "websocket") {
|
if strings.Contains(r.Header.Get("Upgrade"), "websocket") {
|
||||||
// I've never been able demonstrate a vulnerability myself, but apparently
|
// I've never been able demonstrate a vulnerability myself, but apparently
|
||||||
// WebSocket connections originating from browsers aren't subject to CORS
|
// WebSocket connections originating from browsers aren't subject to CORS
|
||||||
// restrictions, so we'll just be on the safe side
|
// restrictions, so we'll just be on the safe side
|
||||||
h.handleError(w, r, APIError{
|
h.handleError(w, r, fmt.Errorf("websocket connections aren't allowed"))
|
||||||
HTTPStatus: http.StatusBadRequest,
|
|
||||||
Err: errors.New("websocket connections aren't allowed"),
|
|
||||||
Message: "WebSocket connections aren't allowed.",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.Contains(r.Header.Get("Sec-Fetch-Mode"), "no-cors") {
|
|
||||||
// turns out web pages can just disable the same-origin policy (!???!?)
|
|
||||||
// but at least browsers let us know that's the case, holy heck
|
|
||||||
h.handleError(w, r, APIError{
|
|
||||||
HTTPStatus: http.StatusBadRequest,
|
|
||||||
Err: errors.New("client attempted to make request by disabling same-origin policy using no-cors mode"),
|
|
||||||
Message: "Disabling same-origin restrictions is not allowed.",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Header.Get("Origin") == "null" {
|
|
||||||
// bug in Firefox in certain cross-origin situations (yikes?)
|
|
||||||
// (not strictly a security vuln on its own, but it's red flaggy,
|
|
||||||
// since it seems to manifest in cross-origin contexts)
|
|
||||||
h.handleError(w, r, APIError{
|
|
||||||
HTTPStatus: http.StatusBadRequest,
|
|
||||||
Err: errors.New("invalid origin 'null'"),
|
|
||||||
Message: "Buggy browser is sending null Origin header.",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -861,9 +824,7 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, hasOriginHeader := r.Header["Origin"]
|
if h.enforceOrigin {
|
||||||
_, hasSecHeader := r.Header["Sec-Fetch-Mode"]
|
|
||||||
if h.enforceOrigin || hasOriginHeader || hasSecHeader {
|
|
||||||
// cross-site mitigation
|
// cross-site mitigation
|
||||||
origin, err := h.checkOrigin(r)
|
origin, err := h.checkOrigin(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1051,9 +1012,6 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
|||||||
buf.Reset()
|
buf.Reset()
|
||||||
defer bufPool.Put(buf)
|
defer bufPool.Put(buf)
|
||||||
|
|
||||||
const maxConfigSize = 100 * 1024 * 1024 // 100 MB
|
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxConfigSize)
|
|
||||||
|
|
||||||
_, err := io.Copy(buf, r.Body)
|
_, err := io.Copy(buf, r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return APIError{
|
return APIError{
|
||||||
@@ -1136,20 +1094,6 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseCanonicalArrayIndex(idx string) (int, error) {
|
|
||||||
if idx == "" {
|
|
||||||
return 0, fmt.Errorf("empty index")
|
|
||||||
}
|
|
||||||
i, err := strconv.Atoi(idx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if strconv.Itoa(i) != idx {
|
|
||||||
return 0, fmt.Errorf("non-canonical array index")
|
|
||||||
}
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// unsyncedConfigAccess traverses into the current config and performs
|
// unsyncedConfigAccess traverses into the current config and performs
|
||||||
// the operation at path according to method, using body and out as
|
// the operation at path according to method, using body and out as
|
||||||
// needed. This is a low-level, unsynchronized function; most callers
|
// needed. This is a low-level, unsynchronized function; most callers
|
||||||
@@ -1166,10 +1110,7 @@ func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error
|
|||||||
if len(body) > 0 {
|
if len(body) > 0 {
|
||||||
err = json.Unmarshal(body, &val)
|
err = json.Unmarshal(body, &val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if jsonErr, ok := err.(*json.SyntaxError); ok {
|
return fmt.Errorf("decoding request body: %v", err)
|
||||||
return fmt.Errorf("decoding request body: %w, at offset %d", jsonErr, jsonErr.Offset)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("decoding request body: %w", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1211,12 +1152,11 @@ traverseLoop:
|
|||||||
var idx int
|
var idx int
|
||||||
if method != http.MethodPost {
|
if method != http.MethodPost {
|
||||||
idxStr := parts[len(parts)-1]
|
idxStr := parts[len(parts)-1]
|
||||||
idx, err = parseCanonicalArrayIndex(idxStr)
|
idx, err = strconv.Atoi(idxStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[%s] invalid array index '%s': %v",
|
return fmt.Errorf("[%s] invalid array index '%s': %v",
|
||||||
path, idxStr, err)
|
path, idxStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) {
|
if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) {
|
||||||
return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
|
return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
|
||||||
}
|
}
|
||||||
@@ -1316,7 +1256,7 @@ traverseLoop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
case []any:
|
case []any:
|
||||||
partInt, err := parseCanonicalArrayIndex(part)
|
partInt, err := strconv.Atoi(part)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[/%s] invalid array index '%s': %v",
|
return fmt.Errorf("[/%s] invalid array index '%s': %v",
|
||||||
strings.Join(parts[:i+1], "/"), part, err)
|
strings.Join(parts[:i+1], "/"), part, err)
|
||||||
|
|||||||
+23
-243
@@ -15,28 +15,20 @@
|
|||||||
package caddy
|
package caddy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
dto "github.com/prometheus/client_model/go"
|
dto "github.com/prometheus/client_model/go"
|
||||||
"go.uber.org/zap"
|
|
||||||
"go.uber.org/zap/zaptest/observer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var testCfg = []byte(`{
|
var testCfg = []byte(`{
|
||||||
@@ -57,13 +49,6 @@ var testCfg = []byte(`{
|
|||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
type testAdminPublicKey string
|
|
||||||
|
|
||||||
func (k testAdminPublicKey) Equal(x crypto.PublicKey) bool {
|
|
||||||
other, ok := x.(testAdminPublicKey)
|
|
||||||
return ok && k == other
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnsyncedConfigAccess(t *testing.T) {
|
func TestUnsyncedConfigAccess(t *testing.T) {
|
||||||
// each test is performed in sequence, so
|
// each test is performed in sequence, so
|
||||||
// each change builds on the previous ones;
|
// each change builds on the previous ones;
|
||||||
@@ -255,51 +240,6 @@ func TestAdminHandlerErrorHandling(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAdminHandlerServeHTTPRedactsSensitiveHeadersInLogs(t *testing.T) {
|
|
||||||
core, logs := observer.New(zap.InfoLevel)
|
|
||||||
|
|
||||||
defaultLoggerMu.Lock()
|
|
||||||
origLogger := defaultLogger.logger
|
|
||||||
defaultLogger.logger = zap.New(core)
|
|
||||||
defaultLoggerMu.Unlock()
|
|
||||||
t.Cleanup(func() {
|
|
||||||
defaultLoggerMu.Lock()
|
|
||||||
defaultLogger.logger = origLogger
|
|
||||||
defaultLoggerMu.Unlock()
|
|
||||||
})
|
|
||||||
|
|
||||||
handler := adminHandler{
|
|
||||||
mux: http.NewServeMux(),
|
|
||||||
}
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer secret")
|
|
||||||
req.Header.Set("Cookie", "session=secret")
|
|
||||||
req.Header.Set("X-Test", "ok")
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
|
|
||||||
handler.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
if logs.Len() == 0 {
|
|
||||||
t.Fatal("expected request log entry")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := logs.All()[0].ContextMap()
|
|
||||||
headers, ok := ctx["headers"].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected headers field in log context, got %T", ctx["headers"])
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := headers["Authorization"]; !reflect.DeepEqual(got, []any{"REDACTED"}) {
|
|
||||||
t.Fatalf("expected redacted Authorization header, got %#v", got)
|
|
||||||
}
|
|
||||||
if got := headers["Cookie"]; !reflect.DeepEqual(got, []any{"REDACTED"}) {
|
|
||||||
t.Fatalf("expected redacted Cookie header, got %#v", got)
|
|
||||||
}
|
|
||||||
if got := headers["X-Test"]; !reflect.DeepEqual(got, []any{"ok"}) {
|
|
||||||
t.Fatalf("expected X-Test header to remain visible, got %#v", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initAdminMetrics() {
|
func initAdminMetrics() {
|
||||||
if adminMetrics.requestErrors != nil {
|
if adminMetrics.requestErrors != nil {
|
||||||
prometheus.Unregister(adminMetrics.requestErrors)
|
prometheus.Unregister(adminMetrics.requestErrors)
|
||||||
@@ -335,15 +275,13 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the admin handler directly (no listener active)
|
err := replaceLocalAdminServer(cfg, Context{})
|
||||||
addr, err := ParseNetworkAddress("localhost:2019")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to parse address: %v", err)
|
t.Fatalf("setting up admin server: %v", err)
|
||||||
}
|
|
||||||
handler, err := cfg.Admin.newAdminHandler(addr, false, Context{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create admin handler: %v", err)
|
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
stopAdminServer(localAdminServer)
|
||||||
|
}()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -376,7 +314,7 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
|
|||||||
req := httptest.NewRequest(test.method, fmt.Sprintf("http://localhost:2019%s", test.path), nil)
|
req := httptest.NewRequest(test.method, fmt.Sprintf("http://localhost:2019%s", test.path), nil)
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
handler.ServeHTTP(rr, req)
|
localAdminServer.Handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
if rr.Code != test.expectedStatus {
|
if rr.Code != test.expectedStatus {
|
||||||
t.Errorf("expected status %d but got %d", test.expectedStatus, rr.Code)
|
t.Errorf("expected status %d but got %d", test.expectedStatus, rr.Code)
|
||||||
@@ -464,10 +402,7 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) {
|
|||||||
admin := &AdminConfig{
|
admin := &AdminConfig{
|
||||||
EnforceOrigin: false,
|
EnforceOrigin: false,
|
||||||
}
|
}
|
||||||
handler, err := admin.newAdminHandler(addr, false, Context{})
|
handler := admin.newAdminHandler(addr, false, Context{})
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create admin handler: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/mock", nil)
|
req := httptest.NewRequest("GET", "/mock", nil)
|
||||||
req.Host = "localhost:2019"
|
req.Host = "localhost:2019"
|
||||||
@@ -479,6 +414,10 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) {
|
|||||||
t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
|
t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
|
||||||
t.Logf("Response body: %s", rr.Body.String())
|
t.Logf("Response body: %s", rr.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(admin.routers) != 1 {
|
||||||
|
t.Errorf("Expected 1 router to be stored, got %d", len(admin.routers))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockProvisionableRouter struct {
|
type mockProvisionableRouter struct {
|
||||||
@@ -516,16 +455,19 @@ func TestAdminRouterProvisioning(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
provisionErr error
|
provisionErr error
|
||||||
wantErr bool
|
wantErr bool
|
||||||
|
routersAfter int // expected number of routers after provisioning
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "successful provisioning",
|
name: "successful provisioning",
|
||||||
provisionErr: nil,
|
provisionErr: nil,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
|
routersAfter: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "provisioning error",
|
name: "provisioning error",
|
||||||
provisionErr: fmt.Errorf("provision failed"),
|
provisionErr: fmt.Errorf("provision failed"),
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
|
routersAfter: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,7 +503,8 @@ func TestAdminRouterProvisioning(t *testing.T) {
|
|||||||
t.Fatalf("Failed to parse address: %v", err)
|
t.Fatalf("Failed to parse address: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = admin.newAdminHandler(addr, false, Context{})
|
_ = admin.newAdminHandler(addr, false, Context{})
|
||||||
|
err = admin.provisionAdminRouters(Context{})
|
||||||
|
|
||||||
if test.wantErr {
|
if test.wantErr {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -572,6 +515,10 @@ func TestAdminRouterProvisioning(t *testing.T) {
|
|||||||
t.Errorf("Expected no error but got: %v", err)
|
t.Errorf("Expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(admin.routers) != test.routersAfter {
|
||||||
|
t.Errorf("Expected %d routers after provisioning, got %d", test.routersAfter, len(admin.routers))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -656,99 +603,6 @@ func TestAllowedOriginsUnixSocket(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRemoteAdminAccessControlPathSegmentMatching(t *testing.T) {
|
|
||||||
const authorizedKey testAdminPublicKey = "authorized"
|
|
||||||
peerCert := &x509.Certificate{PublicKey: authorizedKey}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
allowedPath string
|
|
||||||
requestPath string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "exact path",
|
|
||||||
allowedPath: "/pki/ca/prod",
|
|
||||||
requestPath: "/pki/ca/prod",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "subpath",
|
|
||||||
allowedPath: "/pki/ca/prod",
|
|
||||||
requestPath: "/pki/ca/prod/certificates",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "trailing slash subpath",
|
|
||||||
allowedPath: "/pki/ca/prod/",
|
|
||||||
requestPath: "/pki/ca/prod/certificates",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sibling with shared prefix",
|
|
||||||
allowedPath: "/pki/ca/prod",
|
|
||||||
requestPath: "/pki/ca/prod-backup",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "same segment plus digit",
|
|
||||||
allowedPath: "/pki/ca/prod",
|
|
||||||
requestPath: "/pki/ca/prod1",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "root path",
|
|
||||||
allowedPath: "/",
|
|
||||||
requestPath: "/pki/ca/prod",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
remote := RemoteAdmin{
|
|
||||||
AccessControl: []*AdminAccess{
|
|
||||||
{
|
|
||||||
Permissions: []AdminPermissions{
|
|
||||||
{
|
|
||||||
Methods: []string{http.MethodGet},
|
|
||||||
Paths: []string{test.allowedPath},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
publicKeys: []crypto.PublicKey{authorizedKey},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "https://localhost:2021"+test.requestPath, nil)
|
|
||||||
req.TLS = &tls.ConnectionState{
|
|
||||||
VerifiedChains: [][]*x509.Certificate{{peerCert}},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := remote.enforceAccessControls(req)
|
|
||||||
if test.wantErr {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("test %d (%s): allowed path %q, request path %q: expected forbidden error, got nil", i, test.name, test.allowedPath, test.requestPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var apiErr APIError
|
|
||||||
if !errors.As(err, &apiErr) {
|
|
||||||
t.Errorf("test %d (%s): allowed path %q, request path %q: expected APIError with HTTP status %d, got %T: %v", i, test.name, test.allowedPath, test.requestPath, http.StatusForbidden, err, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if apiErr.HTTPStatus != http.StatusForbidden {
|
|
||||||
t.Errorf("test %d (%s): allowed path %q, request path %q: expected HTTP status %d, got %d", i, test.name, test.allowedPath, test.requestPath, http.StatusForbidden, apiErr.HTTPStatus)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("test %d (%s): allowed path %q, request path %q: expected no error, got %v", i, test.name, test.allowedPath, test.requestPath, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReplaceRemoteAdminServer(t *testing.T) {
|
func TestReplaceRemoteAdminServer(t *testing.T) {
|
||||||
const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL
|
const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL
|
||||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw
|
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw
|
||||||
@@ -945,24 +799,8 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
|
|||||||
...
|
...
|
||||||
-----END PRIVATE KEY-----`)
|
-----END PRIVATE KEY-----`)
|
||||||
|
|
||||||
tmpDir, err := os.MkdirTemp("", "TestManageIdentity-")
|
testStorage := certmagic.FileStorage{Path: t.TempDir()}
|
||||||
if err != nil {
|
err := testStorage.Store(context.Background(), "localhost/localhost.crt", certPEM)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
testStorage := certmagic.FileStorage{Path: tmpDir}
|
|
||||||
// Clean up the temp dir after the test finishes. Ensure any background
|
|
||||||
// certificate maintenance is stopped first to avoid RemoveAll races.
|
|
||||||
t.Cleanup(func() {
|
|
||||||
if identityCertCache != nil {
|
|
||||||
identityCertCache.Stop()
|
|
||||||
identityCertCache = nil
|
|
||||||
}
|
|
||||||
// Give goroutines a moment to exit and release file handles.
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
_ = os.RemoveAll(tmpDir)
|
|
||||||
})
|
|
||||||
|
|
||||||
err = testStorage.Store(context.Background(), "localhost/localhost.crt", certPEM)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -1024,7 +862,7 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
storage: &testStorage,
|
storage: &certmagic.FileStorage{Path: "testdata"},
|
||||||
},
|
},
|
||||||
checkState: func(t *testing.T, cfg *Config) {
|
checkState: func(t *testing.T, cfg *Config) {
|
||||||
if len(cfg.Admin.Identity.issuers) != 1 {
|
if len(cfg.Admin.Identity.issuers) != 1 {
|
||||||
@@ -1062,13 +900,6 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
|
|||||||
identityCertCache.Stop()
|
identityCertCache.Stop()
|
||||||
identityCertCache = nil
|
identityCertCache = nil
|
||||||
}
|
}
|
||||||
// Ensure any cache started by manageIdentity is stopped at the end
|
|
||||||
defer func() {
|
|
||||||
if identityCertCache != nil {
|
|
||||||
identityCertCache.Stop()
|
|
||||||
identityCertCache = nil
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
ctx := Context{
|
ctx := Context{
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
@@ -1076,13 +907,6 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
|
|||||||
moduleInstances: make(map[string][]Module),
|
moduleInstances: make(map[string][]Module),
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this test provided a FileStorage, set the package-level
|
|
||||||
// testCertMagicStorageOverride so certmagicConfig will use it.
|
|
||||||
if test.cfg != nil && test.cfg.storage != nil {
|
|
||||||
testCertMagicStorageOverride = test.cfg.storage
|
|
||||||
defer func() { testCertMagicStorageOverride = nil }()
|
|
||||||
}
|
|
||||||
|
|
||||||
err := manageIdentity(ctx, test.cfg)
|
err := manageIdentity(ctx, test.cfg)
|
||||||
|
|
||||||
if test.wantErr {
|
if test.wantErr {
|
||||||
@@ -1101,47 +925,3 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnsyncedConfigAccessCanonicalArrayIndices(t *testing.T) {
|
|
||||||
rawCfg = map[string]any{
|
|
||||||
rawConfigKey: map[string]any{
|
|
||||||
"list": []any{"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
path string
|
|
||||||
wantOutput string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{name: "allow zero", path: "/" + rawConfigKey + "/list/0", wantOutput: "\"zero\"\n"},
|
|
||||||
{name: "allow one", path: "/" + rawConfigKey + "/list/1", wantOutput: "\"one\"\n"},
|
|
||||||
{name: "allow ten", path: "/" + rawConfigKey + "/list/10", wantOutput: "\"ten\"\n"},
|
|
||||||
{name: "reject leading zero", path: "/" + rawConfigKey + "/list/01", wantErr: true},
|
|
||||||
{name: "reject multiple leading zeros", path: "/" + rawConfigKey + "/list/002", wantErr: true},
|
|
||||||
{name: "reject plus sign", path: "/" + rawConfigKey + "/list/+1", wantErr: true},
|
|
||||||
{name: "reject negative zero", path: "/" + rawConfigKey + "/list/-0", wantErr: true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
var gotOutput bytes.Buffer
|
|
||||||
err := unsyncedConfigAccess(http.MethodGet, tc.path, nil, &gotOutput)
|
|
||||||
|
|
||||||
if tc.wantErr {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("test %d (%s): input path %q: expected error, got nil with output %q", i, tc.name, tc.path, gotOutput.String())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("test %d (%s): input path %q: expected no error with output %q, got error %v with output %q", i, tc.name, tc.path, tc.wantOutput, err, gotOutput.String())
|
|
||||||
}
|
|
||||||
if gotOutput.String() != tc.wantOutput {
|
|
||||||
t.Errorf("test %d (%s): input path %q: expected output %q, got %q", i, tc.name, tc.path, tc.wantOutput, gotOutput.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ type Config struct {
|
|||||||
storage certmagic.Storage
|
storage certmagic.Storage
|
||||||
eventEmitter eventEmitter
|
eventEmitter eventEmitter
|
||||||
|
|
||||||
cancelFunc context.CancelCauseFunc
|
cancelFunc context.CancelFunc
|
||||||
|
|
||||||
// fileSystems is a dict of fileSystems that will later be loaded from and added to.
|
// fileSystems is a dict of fileSystems that will later be loaded from and added to.
|
||||||
fileSystems FileSystems
|
fileSystems FileSystems
|
||||||
@@ -127,9 +127,10 @@ func Load(cfgJSON []byte, forceReload bool) error {
|
|||||||
zap.Error(notifyErr),
|
zap.Error(notifyErr),
|
||||||
zap.String("reload_err", err.Error()))
|
zap.String("reload_err", err.Error()))
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if notifyErr := notify.Ready(); notifyErr != nil {
|
if err := notify.Ready(); err != nil {
|
||||||
Log().Error("unable to notify to service manager of ready state", zap.Error(notifyErr))
|
Log().Error("unable to notify to service manager of ready state", zap.Error(err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -146,8 +147,8 @@ func Load(cfgJSON []byte, forceReload bool) error {
|
|||||||
// the new value (if applicable; i.e. "DELETE" doesn't have an input).
|
// the new value (if applicable; i.e. "DELETE" doesn't have an input).
|
||||||
// If the resulting config is the same as the previous, no reload will
|
// If the resulting config is the same as the previous, no reload will
|
||||||
// occur unless forceReload is true. If the config is unchanged and not
|
// occur unless forceReload is true. If the config is unchanged and not
|
||||||
// forcefully reloaded, then errConfigUnchanged is returned. This function
|
// forcefully reloaded, then errConfigUnchanged This function is safe for
|
||||||
// is safe for concurrent use.
|
// concurrent use.
|
||||||
// The ifMatchHeader can optionally be given a string of the format:
|
// The ifMatchHeader can optionally be given a string of the format:
|
||||||
//
|
//
|
||||||
// "<path> <hash>"
|
// "<path> <hash>"
|
||||||
@@ -226,18 +227,8 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
|
|||||||
idx := make(map[string]string)
|
idx := make(map[string]string)
|
||||||
err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
|
err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(rawCfgJSON) > 0 {
|
|
||||||
var oldCfg any
|
|
||||||
err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
|
|
||||||
if err2 != nil {
|
|
||||||
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
|
|
||||||
}
|
|
||||||
rawCfg[rawConfigKey] = oldCfg
|
|
||||||
} else {
|
|
||||||
rawCfg[rawConfigKey] = nil
|
|
||||||
}
|
|
||||||
return APIError{
|
return APIError{
|
||||||
HTTPStatus: http.StatusBadRequest,
|
HTTPStatus: http.StatusInternalServerError,
|
||||||
Err: fmt.Errorf("indexing config: %v", err),
|
Err: fmt.Errorf("indexing config: %v", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,8 +248,6 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
|
|||||||
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
|
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
|
||||||
}
|
}
|
||||||
rawCfg[rawConfigKey] = oldCfg
|
rawCfg[rawConfigKey] = oldCfg
|
||||||
} else {
|
|
||||||
rawCfg[rawConfigKey] = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("loading new config: %v", err)
|
return fmt.Errorf("loading new config: %v", err)
|
||||||
@@ -292,19 +281,14 @@ func indexConfigObjects(ptr any, configPath string, index map[string]string) err
|
|||||||
case map[string]any:
|
case map[string]any:
|
||||||
for k, v := range val {
|
for k, v := range val {
|
||||||
if k == idKey {
|
if k == idKey {
|
||||||
var idStr string
|
|
||||||
switch idVal := v.(type) {
|
switch idVal := v.(type) {
|
||||||
case string:
|
case string:
|
||||||
idStr = idVal
|
index[idVal] = configPath
|
||||||
case float64: // all JSON numbers decode as float64
|
case float64: // all JSON numbers decode as float64
|
||||||
idStr = fmt.Sprintf("%v", idVal)
|
index[fmt.Sprintf("%v", idVal)] = configPath
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("%s: %s field must be a string or number", configPath, idKey)
|
return fmt.Errorf("%s: %s field must be a string or number", configPath, idKey)
|
||||||
}
|
}
|
||||||
if existingPath, ok := index[idStr]; ok {
|
|
||||||
return fmt.Errorf("duplicate ID '%s' found at %s and %s", idStr, existingPath, configPath)
|
|
||||||
}
|
|
||||||
index[idStr] = configPath
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// traverse this object property recursively
|
// traverse this object property recursively
|
||||||
@@ -432,7 +416,7 @@ func run(newCfg *Config, start bool) (Context, error) {
|
|||||||
// partially copied from provisionContext
|
// partially copied from provisionContext
|
||||||
if err != nil {
|
if err != nil {
|
||||||
globalMetrics.configSuccess.Set(0)
|
globalMetrics.configSuccess.Set(0)
|
||||||
ctx.cfg.cancelFunc(fmt.Errorf("configuration start error: %w", err))
|
ctx.cfg.cancelFunc()
|
||||||
|
|
||||||
if currentCtx.cfg != nil {
|
if currentCtx.cfg != nil {
|
||||||
certmagic.Default.Storage = currentCtx.cfg.storage
|
certmagic.Default.Storage = currentCtx.cfg.storage
|
||||||
@@ -440,6 +424,13 @@ func run(newCfg *Config, start bool) (Context, error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Provision any admin routers which may need to access
|
||||||
|
// some of the other apps at runtime
|
||||||
|
err = ctx.cfg.Admin.provisionAdminRouters(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return ctx, err
|
||||||
|
}
|
||||||
|
|
||||||
// Start
|
// Start
|
||||||
err = func() error {
|
err = func() error {
|
||||||
started := make([]string, 0, len(ctx.cfg.apps))
|
started := make([]string, 0, len(ctx.cfg.apps))
|
||||||
@@ -501,7 +492,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
|
|||||||
// cleanup occurs when we return if there
|
// cleanup occurs when we return if there
|
||||||
// was an error; if no error, it will get
|
// was an error; if no error, it will get
|
||||||
// cleaned up on next config cycle
|
// cleaned up on next config cycle
|
||||||
ctx, cancelCause := NewContextWithCause(Context{Context: context.Background(), cfg: newCfg})
|
ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
globalMetrics.configSuccess.Set(0)
|
globalMetrics.configSuccess.Set(0)
|
||||||
@@ -510,7 +501,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
|
|||||||
// since the associated config won't be used;
|
// since the associated config won't be used;
|
||||||
// this will cause all modules that were newly
|
// this will cause all modules that were newly
|
||||||
// provisioned to clean themselves up
|
// provisioned to clean themselves up
|
||||||
cancelCause(fmt.Errorf("configuration error: %w", err))
|
cancel()
|
||||||
|
|
||||||
// also undo any other state changes we made
|
// also undo any other state changes we made
|
||||||
if currentCtx.cfg != nil {
|
if currentCtx.cfg != nil {
|
||||||
@@ -518,7 +509,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
newCfg.cancelFunc = cancelCause // clean up later
|
newCfg.cancelFunc = cancel // clean up later
|
||||||
|
|
||||||
// set up logging before anything bad happens
|
// set up logging before anything bad happens
|
||||||
if newCfg.Logging == nil {
|
if newCfg.Logging == nil {
|
||||||
@@ -738,7 +729,7 @@ func unsyncedStop(ctx Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// clean up all modules
|
// clean up all modules
|
||||||
ctx.cfg.cancelFunc(fmt.Errorf("stopping apps"))
|
ctx.cfg.cancelFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate loads, provisions, and validates
|
// Validate loads, provisions, and validates
|
||||||
@@ -746,7 +737,7 @@ func unsyncedStop(ctx Context) {
|
|||||||
func Validate(cfg *Config) error {
|
func Validate(cfg *Config) error {
|
||||||
_, err := run(cfg, false)
|
_, err := run(cfg, false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
cfg.cancelFunc(fmt.Errorf("validation complete")) // call Cleanup on all modules
|
cfg.cancelFunc() // call Cleanup on all modules
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -759,7 +750,7 @@ func Validate(cfg *Config) error {
|
|||||||
// code is emitted.
|
// code is emitted.
|
||||||
func exitProcess(ctx context.Context, logger *zap.Logger) {
|
func exitProcess(ctx context.Context, logger *zap.Logger) {
|
||||||
// let the rest of the program know we're quitting; only do it once
|
// let the rest of the program know we're quitting; only do it once
|
||||||
if !exiting.CompareAndSwap(false, true) {
|
if !atomic.CompareAndSwapInt32(exiting, 0, 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,11 +829,11 @@ func exitProcess(ctx context.Context, logger *zap.Logger) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
var exiting atomic.Bool
|
var exiting = new(int32) // accessed atomically
|
||||||
|
|
||||||
// Exiting returns true if the process is exiting.
|
// Exiting returns true if the process is exiting.
|
||||||
// EXPERIMENTAL API: subject to change or removal.
|
// EXPERIMENTAL API: subject to change or removal.
|
||||||
func Exiting() bool { return exiting.Load() }
|
func Exiting() bool { return atomic.LoadInt32(exiting) == 1 }
|
||||||
|
|
||||||
// OnExit registers a callback to invoke during process exit.
|
// OnExit registers a callback to invoke during process exit.
|
||||||
// This registration is PROCESS-GLOBAL, meaning that each
|
// This registration is PROCESS-GLOBAL, meaning that each
|
||||||
@@ -954,34 +945,6 @@ func InstanceID() (uuid.UUID, error) {
|
|||||||
// for example.
|
// for example.
|
||||||
var CustomVersion string
|
var CustomVersion string
|
||||||
|
|
||||||
// CustomBinaryName is an optional string that overrides the root
|
|
||||||
// command name from the default of "caddy". This is useful for
|
|
||||||
// downstream projects that embed Caddy but use a different binary
|
|
||||||
// name. Shell completions and help text will use this name instead
|
|
||||||
// of "caddy".
|
|
||||||
//
|
|
||||||
// Set this variable during `go build` with `-ldflags`:
|
|
||||||
//
|
|
||||||
// -ldflags '-X github.com/caddyserver/caddy/v2.CustomBinaryName=my_custom_caddy'
|
|
||||||
//
|
|
||||||
// for example.
|
|
||||||
var CustomBinaryName string
|
|
||||||
|
|
||||||
// CustomLongDescription is an optional string that overrides the
|
|
||||||
// long description of the root Cobra command. This is useful for
|
|
||||||
// downstream projects that embed Caddy but want different help
|
|
||||||
// output.
|
|
||||||
//
|
|
||||||
// Set this variable in an init() function of a package that is
|
|
||||||
// imported by your main:
|
|
||||||
//
|
|
||||||
// func init() {
|
|
||||||
// caddy.CustomLongDescription = "My custom server based on Caddy..."
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// for example.
|
|
||||||
var CustomLongDescription string
|
|
||||||
|
|
||||||
// Version returns the Caddy version in a simple/short form, and
|
// Version returns the Caddy version in a simple/short form, and
|
||||||
// a full version string. The short form will not have spaces and
|
// a full version string. The short form will not have spaces and
|
||||||
// is intended for User-Agent strings and similar, but may be
|
// is intended for User-Agent strings and similar, but may be
|
||||||
@@ -1129,7 +1092,7 @@ type Event struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewEvent creates a new event, but does not emit the event. To emit an
|
// NewEvent creates a new event, but does not emit the event. To emit an
|
||||||
// event, call Emit() on the current instance of the caddyevents app instead.
|
// event, call Emit() on the current instance of the caddyevents app insteaad.
|
||||||
//
|
//
|
||||||
// EXPERIMENTAL: Subject to change.
|
// EXPERIMENTAL: Subject to change.
|
||||||
func NewEvent(ctx Context, name string, data map[string]any) (Event, error) {
|
func NewEvent(ctx Context, name string, data map[string]any) (Event, error) {
|
||||||
@@ -1287,10 +1250,10 @@ func getLastConfig() (file, adapter string, fn reloadFromSourceFunc) {
|
|||||||
|
|
||||||
// lastConfigMatches returns true if the provided source file and/or adapter
|
// lastConfigMatches returns true if the provided source file and/or adapter
|
||||||
// matches the recorded last-config. Matching rules (in priority order):
|
// matches the recorded last-config. Matching rules (in priority order):
|
||||||
// 1. If srcAdapter is provided and differs from the recorded adapter, no match.
|
// 1. If srcAdapter is provided and differs from the recorded adapter, no match.
|
||||||
// 2. If srcFile exactly equals the recorded file, match.
|
// 2. If srcFile exactly equals the recorded file, match.
|
||||||
// 3. If both sides can be made absolute and equal, match.
|
// 3. If both sides can be made absolute and equal, match.
|
||||||
// 4. If basenames are equal, match.
|
// 4. If basenames are equal, match.
|
||||||
func lastConfigMatches(srcFile, srcAdapter string) bool {
|
func lastConfigMatches(srcFile, srcAdapter string) bool {
|
||||||
lf, la, _ := getLastConfig()
|
lf, la, _ := getLastConfig()
|
||||||
|
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ func (d *Dispenser) File() string {
|
|||||||
// targets are left unchanged. If all the targets are filled,
|
// targets are left unchanged. If all the targets are filled,
|
||||||
// then true is returned.
|
// then true is returned.
|
||||||
func (d *Dispenser) Args(targets ...*string) bool {
|
func (d *Dispenser) Args(targets ...*string) bool {
|
||||||
for i := range targets {
|
for i := 0; i < len(targets); i++ {
|
||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,33 +62,8 @@ func Format(input []byte) []byte {
|
|||||||
heredocClosingMarker []rune
|
heredocClosingMarker []rune
|
||||||
|
|
||||||
nesting int // indentation level
|
nesting int // indentation level
|
||||||
|
|
||||||
currentToken strings.Builder
|
|
||||||
currentLineFirstToken string
|
|
||||||
previousLineWasTopLevelImport bool
|
|
||||||
openBraceOwnLine bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
finishToken := func() {
|
|
||||||
if currentToken.Len() == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if currentLineFirstToken == "" {
|
|
||||||
currentLineFirstToken = currentToken.String()
|
|
||||||
}
|
|
||||||
currentToken.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
finishLine := func() {
|
|
||||||
finishToken()
|
|
||||||
if currentLineFirstToken != "" {
|
|
||||||
previousLineWasTopLevelImport = nesting == 0 && currentLineFirstToken == "import"
|
|
||||||
} else if !openBrace || !openBraceOwnLine || openBraceWritten {
|
|
||||||
previousLineWasTopLevelImport = false
|
|
||||||
}
|
|
||||||
currentLineFirstToken = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
write := func(ch rune) {
|
write := func(ch rune) {
|
||||||
out.WriteRune(ch)
|
out.WriteRune(ch)
|
||||||
last = ch
|
last = ch
|
||||||
@@ -235,21 +209,10 @@ func Format(input []byte) []byte {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(quotes, "`") {
|
|
||||||
if ch == '`' && space && !beginningOfLine {
|
|
||||||
write(' ')
|
|
||||||
}
|
|
||||||
write(ch)
|
|
||||||
space = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if unicode.IsSpace(ch) {
|
if unicode.IsSpace(ch) {
|
||||||
finishToken()
|
|
||||||
space = true
|
space = true
|
||||||
heredocEscaped = false
|
heredocEscaped = false
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
finishLine()
|
|
||||||
newLines++
|
newLines++
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -276,19 +239,13 @@ func Format(input []byte) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openBrace = false
|
openBrace = false
|
||||||
if openBraceOwnLine && previousLineWasTopLevelImport {
|
if beginningOfLine {
|
||||||
if last != '\n' {
|
|
||||||
nextLine()
|
|
||||||
}
|
|
||||||
indent()
|
|
||||||
} else if beginningOfLine {
|
|
||||||
indent()
|
indent()
|
||||||
} else if !openBraceSpace || !unicode.IsSpace(last) {
|
} else if !openBraceSpace || !unicode.IsSpace(last) {
|
||||||
write(' ')
|
write(' ')
|
||||||
}
|
}
|
||||||
write('{')
|
write('{')
|
||||||
openBraceWritten = true
|
openBraceWritten = true
|
||||||
openBraceOwnLine = false
|
|
||||||
nextLine()
|
nextLine()
|
||||||
newLines = 0
|
newLines = 0
|
||||||
// prevent infinite nesting from ridiculous inputs (issue #4169)
|
// prevent infinite nesting from ridiculous inputs (issue #4169)
|
||||||
@@ -299,10 +256,8 @@ func Format(input []byte) []byte {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case ch == '{':
|
case ch == '{':
|
||||||
finishToken()
|
|
||||||
openBrace = true
|
openBrace = true
|
||||||
openBraceSpace = spacePrior && !beginningOfLine
|
openBraceSpace = spacePrior && !beginningOfLine
|
||||||
openBraceOwnLine = newLines > 0
|
|
||||||
if openBraceSpace && newLines == 0 {
|
if openBraceSpace && newLines == 0 {
|
||||||
write(' ')
|
write(' ')
|
||||||
}
|
}
|
||||||
@@ -310,13 +265,11 @@ func Format(input []byte) []byte {
|
|||||||
if quotes == "`" {
|
if quotes == "`" {
|
||||||
write('{')
|
write('{')
|
||||||
openBraceWritten = true
|
openBraceWritten = true
|
||||||
openBraceOwnLine = false
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
case ch == '}' && (spacePrior || !openBrace):
|
case ch == '}' && (spacePrior || !openBrace):
|
||||||
finishToken()
|
|
||||||
if quotes == "`" {
|
if quotes == "`" {
|
||||||
write('}')
|
write('}')
|
||||||
continue
|
continue
|
||||||
@@ -361,7 +314,6 @@ func Format(input []byte) []byte {
|
|||||||
space = true
|
space = true
|
||||||
}
|
}
|
||||||
|
|
||||||
currentToken.WriteRune(ch)
|
|
||||||
write(ch)
|
write(ch)
|
||||||
|
|
||||||
beginningOfLine = false
|
beginningOfLine = false
|
||||||
|
|||||||
@@ -464,32 +464,6 @@ block2 {
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
description: "issue #7425: multiline backticked string indentation",
|
|
||||||
input: `https://localhost:8953 {
|
|
||||||
respond ` + "`" + `Here are some random numbers:
|
|
||||||
|
|
||||||
{{randNumeric 16}}
|
|
||||||
|
|
||||||
Hope this helps.` + "`" + `
|
|
||||||
}`,
|
|
||||||
expect: "https://localhost:8953 {\n\trespond `Here are some random numbers:\n\n{{randNumeric 16}}\n\nHope this helps.`\n}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "imports before global options block keep standalone brace",
|
|
||||||
input: `import ./conf.d/matcher_my_subnet.caddy
|
|
||||||
import ./conf.d/matcher_not_my_subnet.caddy
|
|
||||||
{
|
|
||||||
order crowdsec first
|
|
||||||
order appsec after crowdsec
|
|
||||||
}`,
|
|
||||||
expect: `import ./conf.d/matcher_my_subnet.caddy
|
|
||||||
import ./conf.d/matcher_not_my_subnet.caddy
|
|
||||||
{
|
|
||||||
order crowdsec first
|
|
||||||
order appsec after crowdsec
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
} {
|
} {
|
||||||
// the formatter should output a trailing newline,
|
// the formatter should output a trailing newline,
|
||||||
// even if the tests aren't written to expect that
|
// even if the tests aren't written to expect that
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ func (l *lexer) next() (bool, error) {
|
|||||||
// want to keep.
|
// want to keep.
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
if len(val) == 2 {
|
if len(val) == 2 {
|
||||||
return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alphanumeric characters, dashes and underscores; got empty string", l.line)
|
return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alpha-numeric characters, dashes and underscores; got empty string", l.line)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if there's too many <
|
// check if there's too many <
|
||||||
@@ -165,7 +165,7 @@ func (l *lexer) next() (bool, error) {
|
|||||||
|
|
||||||
heredocMarker = string(val[2:])
|
heredocMarker = string(val[2:])
|
||||||
if !heredocMarkerRegexp.Match([]byte(heredocMarker)) {
|
if !heredocMarkerRegexp.Match([]byte(heredocMarker)) {
|
||||||
return false, fmt.Errorf("heredoc marker on line #%d must contain only alphanumeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
|
return false, fmt.Errorf("heredoc marker on line #%d must contain only alpha-numeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
|
||||||
}
|
}
|
||||||
|
|
||||||
inHeredoc = true
|
inHeredoc = true
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ EOF
|
|||||||
{
|
{
|
||||||
input: []byte("not-a-heredoc <<\n"),
|
input: []byte("not-a-heredoc <<\n"),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
errorMessage: "missing opening heredoc marker on line #1; must contain only alphanumeric characters, dashes and underscores; got empty string",
|
errorMessage: "missing opening heredoc marker on line #1; must contain only alpha-numeric characters, dashes and underscores; got empty string",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: []byte(`heredoc <<<EOF
|
input: []byte(`heredoc <<<EOF
|
||||||
|
|||||||
@@ -507,7 +507,7 @@ func (p *parser) doImport(nesting int) error {
|
|||||||
// format, won't check for nesting correctness or any other error, that's what parser does.
|
// format, won't check for nesting correctness or any other error, that's what parser does.
|
||||||
if !maybeSnippet && nesting == 0 {
|
if !maybeSnippet && nesting == 0 {
|
||||||
// first of the line
|
// first of the line
|
||||||
if i == 0 || isNextOnNewLine(tokensCopy[len(tokensCopy)-1], token) {
|
if i == 0 || isNextOnNewLine(tokensCopy[i-1], token) {
|
||||||
index = 0
|
index = 0
|
||||||
} else {
|
} else {
|
||||||
index++
|
index++
|
||||||
@@ -550,11 +550,7 @@ func (p *parser) doImport(nesting int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if foundBlockDirective {
|
if foundBlockDirective {
|
||||||
if maybeSnippet {
|
tokensCopy = append(tokensCopy, tokensToAdd...)
|
||||||
tokensCopy = append(tokensCopy, token)
|
|
||||||
} else {
|
|
||||||
tokensCopy = append(tokensCopy, tokensToAdd...)
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,7 +616,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, p.Errf("Failed to get absolute path of file: %s: %v", importFile, err)
|
return nil, p.Errf("Failed to get absolute path of file: %s: %v", importFile, err)
|
||||||
}
|
}
|
||||||
for i := range importedTokens {
|
for i := 0; i < len(importedTokens); i++ {
|
||||||
importedTokens[i].File = filename
|
importedTokens[i].File = filename
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,31 +679,14 @@ func (p *parser) directive() error {
|
|||||||
// openCurlyBrace expects the current token to be an
|
// openCurlyBrace expects the current token to be an
|
||||||
// opening curly brace. This acts like an assertion
|
// opening curly brace. This acts like an assertion
|
||||||
// because it returns an error if the token is not
|
// because it returns an error if the token is not
|
||||||
// an opening curly brace. It does NOT advance the token.
|
// a opening curly brace. It does NOT advance the token.
|
||||||
func (p *parser) openCurlyBrace() error {
|
func (p *parser) openCurlyBrace() error {
|
||||||
if p.Val() != "{" {
|
if p.Val() != "{" {
|
||||||
if p.valLooksLikeGlobalOptionsAfterImportedSnippets() {
|
|
||||||
return p.Err("global options block must appear before import directives; move the global options block to the top of the Caddyfile")
|
|
||||||
}
|
|
||||||
return p.SyntaxErr("{")
|
return p.SyntaxErr("{")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) valLooksLikeGlobalOptionsAfterImportedSnippets() bool {
|
|
||||||
if p.Val() != "import" || len(p.block.Keys) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, key := range p.block.Keys {
|
|
||||||
if !strings.HasPrefix(key.Text, "(") || !strings.HasSuffix(key.Text, ")") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// closeCurlyBrace expects the current token to be
|
// closeCurlyBrace expects the current token to be
|
||||||
// a closing curly brace. This acts like an assertion
|
// a closing curly brace. This acts like an assertion
|
||||||
// because it returns an error if the token is not
|
// because it returns an error if the token is not
|
||||||
@@ -782,7 +761,7 @@ type ServerBlock struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sb ServerBlock) GetKeysText() []string {
|
func (sb ServerBlock) GetKeysText() []string {
|
||||||
res := make([]string, 0, len(sb.Keys))
|
res := []string{}
|
||||||
for _, k := range sb.Keys {
|
for _, k := range sb.Keys {
|
||||||
res = append(res, k.Text)
|
res = append(res, k.Text)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -930,107 +930,6 @@ func TestAcceptSiteImportWithBraces(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGlobalOptionsAfterImportedSnippetsGivesHelpfulError(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
importFile1 := filepath.Join(tempDir, "matcher_snippet_1.caddy")
|
|
||||||
importFile2 := filepath.Join(tempDir, "matcher_snippet_2.caddy")
|
|
||||||
|
|
||||||
err := os.WriteFile(importFile1, []byte(`(matcher1)`), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("writing first import file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(importFile2, []byte(`(matcher2)`), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("writing second import file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = Parse("Testfile", []byte(`import `+importFile1+`
|
|
||||||
import `+importFile2+`
|
|
||||||
{
|
|
||||||
debug
|
|
||||||
}`))
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Expected an error, but got nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := "global options block must appear before import directives; move the global options block to the top of the Caddyfile"
|
|
||||||
if !strings.HasPrefix(err.Error(), expected) {
|
|
||||||
t.Errorf("Expected error to start with '%s' but got '%v'", expected, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImportedSnippetDefinitionRetainsBlockPlaceholder(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
importFile := filepath.Join(tempDir, "snippets.caddy")
|
|
||||||
|
|
||||||
err := os.WriteFile(importFile, []byte(`
|
|
||||||
(site) {
|
|
||||||
http://{args[0]} {
|
|
||||||
respond "before"
|
|
||||||
{block}
|
|
||||||
respond "after"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("writing imported snippet file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
expectedDirectives []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "with nested block",
|
|
||||||
input: `
|
|
||||||
import ` + importFile + `
|
|
||||||
|
|
||||||
import site example.com {
|
|
||||||
redir https://example.net
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
expectedDirectives: []string{"respond", "redir", "respond"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "without nested block",
|
|
||||||
input: `
|
|
||||||
import ` + importFile + `
|
|
||||||
|
|
||||||
import site example.com
|
|
||||||
`,
|
|
||||||
expectedDirectives: []string{"respond", "respond"},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
p := testParser(tc.input)
|
|
||||||
blocks, err := p.parseAll()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("parseAll: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(blocks) != 1 {
|
|
||||||
t.Fatalf("expected exactly one server block, got %d", len(blocks))
|
|
||||||
}
|
|
||||||
|
|
||||||
if actual := blocks[0].GetKeysText(); len(actual) != 1 || actual[0] != "http://example.com" {
|
|
||||||
t.Fatalf("expected server block key http://example.com, got %v", actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(blocks[0].Segments) != len(tc.expectedDirectives) {
|
|
||||||
t.Fatalf("expected %d segments, got %d", len(tc.expectedDirectives), len(blocks[0].Segments))
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, directive := range tc.expectedDirectives {
|
|
||||||
if actual := blocks[0].Segments[i].Directive(); actual != directive {
|
|
||||||
t.Fatalf("segment %d: expected directive %q, got %q", i, directive, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testParser(input string) parser {
|
func testParser(input string) parser {
|
||||||
return parser{Dispenser: NewTestDispenser(input)}
|
return parser{Dispenser: NewTestDispenser(input)}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,11 +81,7 @@ func JSONModuleObject(val any, fieldName, fieldVal string, warnings *[]Warning)
|
|||||||
err = json.Unmarshal(enc, &tmp)
|
err = json.Unmarshal(enc, &tmp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if warnings != nil {
|
if warnings != nil {
|
||||||
message := err.Error()
|
*warnings = append(*warnings, Warning{Message: err.Error()})
|
||||||
if jsonErr, ok := err.(*json.SyntaxError); ok {
|
|
||||||
message = fmt.Sprintf("%v, at offset %d", jsonErr.Error(), jsonErr.Offset)
|
|
||||||
}
|
|
||||||
*warnings = append(*warnings, Warning{Message: message})
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ func parseBind(h Helper) ([]ConfigValue, error) {
|
|||||||
// issuer <module_name> [...]
|
// issuer <module_name> [...]
|
||||||
// get_certificate <module_name> [...]
|
// get_certificate <module_name> [...]
|
||||||
// insecure_secrets_log <log_file>
|
// insecure_secrets_log <log_file>
|
||||||
// renewal_window_ratio <ratio>
|
|
||||||
// }
|
// }
|
||||||
func parseTLS(h Helper) ([]ConfigValue, error) {
|
func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||||
h.Next() // consume directive name
|
h.Next() // consume directive name
|
||||||
@@ -130,7 +129,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
var onDemand bool
|
var onDemand bool
|
||||||
var reusePrivateKeys bool
|
var reusePrivateKeys bool
|
||||||
var forceAutomate bool
|
var forceAutomate bool
|
||||||
var renewalWindowRatio float64
|
|
||||||
|
|
||||||
// Track which DNS challenge options are set
|
// Track which DNS challenge options are set
|
||||||
var dnsOptionsSet []string
|
var dnsOptionsSet []string
|
||||||
@@ -475,20 +473,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
}
|
}
|
||||||
cp.InsecureSecretsLog = h.Val()
|
cp.InsecureSecretsLog = h.Val()
|
||||||
|
|
||||||
case "renewal_window_ratio":
|
|
||||||
arg := h.RemainingArgs()
|
|
||||||
if len(arg) != 1 {
|
|
||||||
return nil, h.ArgErr()
|
|
||||||
}
|
|
||||||
ratio, err := strconv.ParseFloat(arg[0], 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, h.Errf("parsing renewal_window_ratio: %v", err)
|
|
||||||
}
|
|
||||||
if ratio <= 0 || ratio >= 1 {
|
|
||||||
return nil, h.Errf("renewal_window_ratio must be between 0 and 1 (exclusive)")
|
|
||||||
}
|
|
||||||
renewalWindowRatio = ratio
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, h.Errf("unknown subdirective: %s", h.Val())
|
return nil, h.Errf("unknown subdirective: %s", h.Val())
|
||||||
}
|
}
|
||||||
@@ -550,11 +534,26 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case acmeIssuer != nil:
|
case acmeIssuer != nil:
|
||||||
// implicit ACME issuers (from various subdirectives) should inherit from
|
// implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one
|
||||||
// any globally-configured ACME issuer templates, then apply the local
|
defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email)
|
||||||
// shortcut settings as overrides.
|
|
||||||
defaultIssuers := implicitACMEIssuers(h, acmeIssuer)
|
// if an ACME CA endpoint was set, the user expects to use that specific one,
|
||||||
|
// not any others that may be defaults, so replace all defaults with that ACME CA
|
||||||
|
if acmeIssuer.CA != "" {
|
||||||
|
defaultIssuers = []certmagic.Issuer{acmeIssuer}
|
||||||
|
}
|
||||||
|
|
||||||
for _, issuer := range defaultIssuers {
|
for _, issuer := range defaultIssuers {
|
||||||
|
// apply settings from the implicitly-configured ACMEIssuer to any
|
||||||
|
// default ACMEIssuers, but preserve each default issuer's CA endpoint,
|
||||||
|
// because, for example, if you configure the DNS challenge, it should
|
||||||
|
// apply to any of the default ACMEIssuers, but you don't want to trample
|
||||||
|
// out their unique CA endpoints
|
||||||
|
if iss, ok := issuer.(*caddytls.ACMEIssuer); ok && iss != nil {
|
||||||
|
acmeCopy := *acmeIssuer
|
||||||
|
acmeCopy.CA = iss.CA
|
||||||
|
issuer = &acmeCopy
|
||||||
|
}
|
||||||
configVals = append(configVals, ConfigValue{
|
configVals = append(configVals, ConfigValue{
|
||||||
Class: "tls.cert_issuer",
|
Class: "tls.cert_issuer",
|
||||||
Value: issuer,
|
Value: issuer,
|
||||||
@@ -598,14 +597,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// renewal window ratio
|
|
||||||
if renewalWindowRatio > 0 {
|
|
||||||
configVals = append(configVals, ConfigValue{
|
|
||||||
Class: "tls.renewal_window_ratio",
|
|
||||||
Value: renewalWindowRatio,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// if enabled, the names in the site addresses will be
|
// if enabled, the names in the site addresses will be
|
||||||
// added to the automation policies
|
// added to the automation policies
|
||||||
if forceAutomate {
|
if forceAutomate {
|
||||||
@@ -653,8 +644,6 @@ func parseRoot(h Helper) ([]ConfigValue, error) {
|
|||||||
if !h.NextArg() {
|
if !h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
}
|
}
|
||||||
// store the unmatched root in block state so sibling directives can access it
|
|
||||||
h.BlockState["root"] = h.Val()
|
|
||||||
return h.NewRoute(nil, caddyhttp.VarsMiddleware{"root": h.Val()}), nil
|
return h.NewRoute(nil, caddyhttp.VarsMiddleware{"root": h.Val()}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,10 +658,6 @@ func parseRoot(h Helper) ([]ConfigValue, error) {
|
|||||||
if !h.NextArg() {
|
if !h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
}
|
}
|
||||||
// store the unmatched root in state so sibling/child directives can access it
|
|
||||||
if userMatcherSet == nil {
|
|
||||||
h.BlockState["root"] = h.Val()
|
|
||||||
}
|
|
||||||
// make the route with the matcher
|
// make the route with the matcher
|
||||||
return h.NewRoute(userMatcherSet, caddyhttp.VarsMiddleware{"root": h.Val()}), nil
|
return h.NewRoute(userMatcherSet, caddyhttp.VarsMiddleware{"root": h.Val()}), nil
|
||||||
}
|
}
|
||||||
@@ -945,7 +930,6 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
|||||||
// modifications to the parsing behavior.
|
// modifications to the parsing behavior.
|
||||||
parseAsGlobalOption := globalLogNames != nil
|
parseAsGlobalOption := globalLogNames != nil
|
||||||
|
|
||||||
// nolint:prealloc
|
|
||||||
var configValues []ConfigValue
|
var configValues []ConfigValue
|
||||||
|
|
||||||
// Logic below expects that a name is always present when a
|
// Logic below expects that a name is always present when a
|
||||||
@@ -1053,7 +1037,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
|||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
}
|
||||||
interval, err := caddy.ParseDuration(d.Val())
|
interval, err := time.ParseDuration(d.Val() + "ns")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, d.Errf("failed to parse interval: %v", err)
|
return nil, d.Errf("failed to parse interval: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,14 +66,14 @@ func TestLogDirectiveSyntax(t *testing.T) {
|
|||||||
input: `:8080 {
|
input: `:8080 {
|
||||||
log {
|
log {
|
||||||
sampling {
|
sampling {
|
||||||
interval 2s
|
interval 2
|
||||||
first 3
|
first 3
|
||||||
thereafter 4
|
thereafter 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2000000000,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
|
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
|
|||||||
@@ -202,10 +202,7 @@ func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) {
|
|||||||
type Helper struct {
|
type Helper struct {
|
||||||
*caddyfile.Dispenser
|
*caddyfile.Dispenser
|
||||||
// State stores intermediate variables during caddyfile adaptation.
|
// State stores intermediate variables during caddyfile adaptation.
|
||||||
State map[string]any
|
State map[string]any
|
||||||
// BlockState stores intermediate variables scoped to the current block.
|
|
||||||
// It propagates down, but unlike state not back up from child to parent.
|
|
||||||
BlockState map[string]any
|
|
||||||
options map[string]any
|
options map[string]any
|
||||||
warnings *[]caddyconfig.Warning
|
warnings *[]caddyconfig.Warning
|
||||||
matcherDefs map[string]caddy.ModuleMap
|
matcherDefs map[string]caddy.ModuleMap
|
||||||
@@ -388,11 +385,6 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clone BlockState once for the entire block so sibling directives
|
|
||||||
// can share state, but changes don't leak to the parent scope
|
|
||||||
subBlockState := make(map[string]any, len(h.BlockState))
|
|
||||||
maps.Copy(subBlockState, h.BlockState)
|
|
||||||
|
|
||||||
// with matchers ready to go, evaluate each directive's segment
|
// with matchers ready to go, evaluate each directive's segment
|
||||||
for _, seg := range segments {
|
for _, seg := range segments {
|
||||||
dir := seg.Directive()
|
dir := seg.Directive()
|
||||||
@@ -404,7 +396,6 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
|
|||||||
subHelper := h
|
subHelper := h
|
||||||
subHelper.Dispenser = caddyfile.NewDispenser(seg)
|
subHelper.Dispenser = caddyfile.NewDispenser(seg)
|
||||||
subHelper.matcherDefs = matcherDefs
|
subHelper.matcherDefs = matcherDefs
|
||||||
subHelper.BlockState = subBlockState
|
|
||||||
|
|
||||||
results, err := dirFunc(subHelper)
|
results, err := dirFunc(subHelper)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ func (st ServerType) Setup(
|
|||||||
parentBlock: sb.block,
|
parentBlock: sb.block,
|
||||||
groupCounter: gc,
|
groupCounter: gc,
|
||||||
State: state,
|
State: state,
|
||||||
BlockState: state,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
results, err := dirFunc(h)
|
results, err := dirFunc(h)
|
||||||
@@ -505,7 +504,6 @@ func (ServerType) extractNamedRoutes(
|
|||||||
parentBlock: sb.block,
|
parentBlock: sb.block,
|
||||||
groupCounter: gc,
|
groupCounter: gc,
|
||||||
State: state,
|
State: state,
|
||||||
BlockState: state,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handler, err := ParseSegmentAsSubroute(h)
|
handler, err := ParseSegmentAsSubroute(h)
|
||||||
@@ -824,7 +822,7 @@ func (st *ServerType) serversFromPairings(
|
|||||||
// https://caddy.community/t/making-sense-of-auto-https-and-why-disabling-it-still-serves-https-instead-of-http/9761
|
// https://caddy.community/t/making-sense-of-auto-https-and-why-disabling-it-still-serves-https-instead-of-http/9761
|
||||||
createdTLSConnPolicies, ok := sblock.pile["tls.connection_policy"]
|
createdTLSConnPolicies, ok := sblock.pile["tls.connection_policy"]
|
||||||
hasTLSEnabled := (ok && len(createdTLSConnPolicies) > 0) ||
|
hasTLSEnabled := (ok && len(createdTLSConnPolicies) > 0) ||
|
||||||
(addr.Host != "" && (srv.AutoHTTPS == nil || !slices.Contains(srv.AutoHTTPS.Skip, addr.Host)))
|
(addr.Host != "" && srv.AutoHTTPS != nil && !slices.Contains(srv.AutoHTTPS.Skip, addr.Host))
|
||||||
|
|
||||||
// we'll need to remember if the address qualifies for auto-HTTPS, so we
|
// we'll need to remember if the address qualifies for auto-HTTPS, so we
|
||||||
// can add a TLS conn policy if necessary
|
// can add a TLS conn policy if necessary
|
||||||
@@ -853,20 +851,6 @@ func (st *ServerType) serversFromPairings(
|
|||||||
srv.ListenerWrappersRaw = append(srv.ListenerWrappersRaw, jsonListenerWrapper)
|
srv.ListenerWrappersRaw = append(srv.ListenerWrappersRaw, jsonListenerWrapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for any config values that provide packet conn wrappers on the server block
|
|
||||||
for _, listenerConfig := range sblock.pile["packet_conn_wrapper"] {
|
|
||||||
packetConnWrapper, ok := listenerConfig.Value.(caddy.PacketConnWrapper)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("config for a packet conn wrapper did not provide a value that implements caddy.PacketConnWrapper")
|
|
||||||
}
|
|
||||||
jsonPacketConnWrapper := caddyconfig.JSONModuleObject(
|
|
||||||
packetConnWrapper,
|
|
||||||
"wrapper",
|
|
||||||
packetConnWrapper.(caddy.Module).CaddyModule().ID.Name(),
|
|
||||||
warnings)
|
|
||||||
srv.PacketConnWrappersRaw = append(srv.PacketConnWrappersRaw, jsonPacketConnWrapper)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up each handler directive, making sure to honor directive order
|
// set up each handler directive, making sure to honor directive order
|
||||||
dirRoutes := sblock.pile["route"]
|
dirRoutes := sblock.pile["route"]
|
||||||
siteSubroute, err := buildSubroute(dirRoutes, groupCounter, true)
|
siteSubroute, err := buildSubroute(dirRoutes, groupCounter, true)
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package httpcaddyfile
|
package httpcaddyfile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMatcherSyntax(t *testing.T) {
|
func TestMatcherSyntax(t *testing.T) {
|
||||||
@@ -211,53 +209,3 @@ func TestGlobalOptions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultSNIWithoutHTTPS(t *testing.T) {
|
|
||||||
caddyfileStr := `{
|
|
||||||
default_sni my-sni.com
|
|
||||||
}
|
|
||||||
example.com {
|
|
||||||
}`
|
|
||||||
|
|
||||||
adapter := caddyfile.Adapter{
|
|
||||||
ServerType: ServerType{},
|
|
||||||
}
|
|
||||||
|
|
||||||
result, _, err := adapter.Adapt([]byte(caddyfileStr), nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to adapt Caddyfile: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var config struct {
|
|
||||||
Apps struct {
|
|
||||||
HTTP struct {
|
|
||||||
Servers map[string]*caddyhttp.Server `json:"servers"`
|
|
||||||
} `json:"http"`
|
|
||||||
} `json:"apps"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(result, &config); err != nil {
|
|
||||||
t.Fatalf("Failed to unmarshal JSON config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
server, ok := config.Apps.HTTP.Servers["srv0"]
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected server 'srv0' to be created")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(server.TLSConnPolicies) == 0 {
|
|
||||||
t.Fatalf("Expected TLS connection policies to be generated, got none")
|
|
||||||
}
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, policy := range server.TLSConnPolicies {
|
|
||||||
if policy.DefaultSNI == "my-sni.com" {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
t.Errorf("Expected default_sni 'my-sni.com' in TLS connection policies, but it was missing. Generated JSON: %s", string(result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -64,9 +64,7 @@ func init() {
|
|||||||
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
|
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
|
||||||
RegisterGlobalOption("persist_config", parseOptPersistConfig)
|
RegisterGlobalOption("persist_config", parseOptPersistConfig)
|
||||||
RegisterGlobalOption("dns", parseOptDNS)
|
RegisterGlobalOption("dns", parseOptDNS)
|
||||||
RegisterGlobalOption("tls_resolvers", parseOptTLSResolvers)
|
|
||||||
RegisterGlobalOption("ech", parseOptECH)
|
RegisterGlobalOption("ech", parseOptECH)
|
||||||
RegisterGlobalOption("renewal_window_ratio", parseOptRenewalWindowRatio)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil }
|
func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil }
|
||||||
@@ -307,15 +305,6 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) {
|
|||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptTLSResolvers(d *caddyfile.Dispenser, _ any) (any, error) {
|
|
||||||
d.Next() // consume option name
|
|
||||||
resolvers := d.RemainingArgs()
|
|
||||||
if len(resolvers) == 0 {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
return resolvers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseOptDefaultBind(d *caddyfile.Dispenser, _ any) (any, error) {
|
func parseOptDefaultBind(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
d.Next() // consume option name
|
d.Next() // consume option name
|
||||||
|
|
||||||
@@ -468,8 +457,9 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
|
|||||||
case "disable_redirects":
|
case "disable_redirects":
|
||||||
case "disable_certs":
|
case "disable_certs":
|
||||||
case "ignore_loaded_certs":
|
case "ignore_loaded_certs":
|
||||||
|
case "prefer_wildcard":
|
||||||
default:
|
default:
|
||||||
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', or 'ignore_loaded_certs'")
|
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', 'ignore_loaded_certs', or 'prefer_wildcard'")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return val, nil
|
return val, nil
|
||||||
@@ -482,10 +472,6 @@ func unmarshalCaddyfileMetricsOptions(d *caddyfile.Dispenser) (any, error) {
|
|||||||
switch d.Val() {
|
switch d.Val() {
|
||||||
case "per_host":
|
case "per_host":
|
||||||
metrics.PerHost = true
|
metrics.PerHost = true
|
||||||
case "observe_catchall_hosts":
|
|
||||||
metrics.ObserveCatchallHosts = true
|
|
||||||
case "otlp":
|
|
||||||
metrics.OTLP = true
|
|
||||||
default:
|
default:
|
||||||
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
||||||
}
|
}
|
||||||
@@ -637,22 +623,3 @@ func parseOptECH(d *caddyfile.Dispenser, _ any) (any, error) {
|
|||||||
|
|
||||||
return ech, nil
|
return ech, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptRenewalWindowRatio(d *caddyfile.Dispenser, _ any) (any, error) {
|
|
||||||
d.Next() // consume option name
|
|
||||||
if !d.Next() {
|
|
||||||
return 0, d.ArgErr()
|
|
||||||
}
|
|
||||||
val := d.Val()
|
|
||||||
ratio, err := strconv.ParseFloat(val, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, d.Errf("parsing renewal_window_ratio: %v", err)
|
|
||||||
}
|
|
||||||
if ratio <= 0 || ratio >= 1 {
|
|
||||||
return 0, d.Errf("renewal_window_ratio must be between 0 and 1 (exclusive)")
|
|
||||||
}
|
|
||||||
if d.Next() {
|
|
||||||
return 0, d.ArgErr()
|
|
||||||
}
|
|
||||||
return ratio, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
package httpcaddyfile
|
package httpcaddyfile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
|
||||||
_ "github.com/caddyserver/caddy/v2/modules/logging"
|
_ "github.com/caddyserver/caddy/v2/modules/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,228 +62,3 @@ func TestGlobalLogOptionSyntax(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGlobalResolversOption(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
expectResolvers []string
|
|
||||||
expectError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "single resolver",
|
|
||||||
input: `{
|
|
||||||
tls_resolvers 1.1.1.1
|
|
||||||
}
|
|
||||||
example.com {
|
|
||||||
}`,
|
|
||||||
expectResolvers: []string{"1.1.1.1"},
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "two resolvers",
|
|
||||||
input: `{
|
|
||||||
tls_resolvers 1.1.1.1 8.8.8.8
|
|
||||||
}
|
|
||||||
example.com {
|
|
||||||
}`,
|
|
||||||
expectResolvers: []string{"1.1.1.1", "8.8.8.8"},
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple resolvers",
|
|
||||||
input: `{
|
|
||||||
tls_resolvers 1.1.1.1 8.8.8.8 9.9.9.9
|
|
||||||
}
|
|
||||||
example.com {
|
|
||||||
}`,
|
|
||||||
expectResolvers: []string{"1.1.1.1", "8.8.8.8", "9.9.9.9"},
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no resolvers specified",
|
|
||||||
input: `{
|
|
||||||
}
|
|
||||||
example.com {
|
|
||||||
}`,
|
|
||||||
expectResolvers: nil,
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
adapter := caddyfile.Adapter{
|
|
||||||
ServerType: ServerType{},
|
|
||||||
}
|
|
||||||
|
|
||||||
out, _, err := adapter.Adapt([]byte(tc.input), nil)
|
|
||||||
|
|
||||||
if (err != nil) != tc.expectError {
|
|
||||||
t.Errorf("error expectation failed. Expected error: %v, got: %v", tc.expectError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.expectError {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the output JSON to check resolvers
|
|
||||||
var config struct {
|
|
||||||
Apps struct {
|
|
||||||
TLS *caddytls.TLS `json:"tls"`
|
|
||||||
} `json:"apps"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(out, &config); err != nil {
|
|
||||||
t.Errorf("failed to unmarshal output: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if resolvers match expected
|
|
||||||
if config.Apps.TLS == nil {
|
|
||||||
if tc.expectResolvers != nil {
|
|
||||||
t.Errorf("Expected TLS config with resolvers %v, but TLS config is nil", tc.expectResolvers)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
actualResolvers := config.Apps.TLS.Resolvers
|
|
||||||
if len(tc.expectResolvers) == 0 && len(actualResolvers) == 0 {
|
|
||||||
return // Both empty, ok
|
|
||||||
}
|
|
||||||
if len(actualResolvers) != len(tc.expectResolvers) {
|
|
||||||
t.Errorf("Expected %d resolvers, got %d. Expected: %v, got: %v", len(tc.expectResolvers), len(actualResolvers), tc.expectResolvers, actualResolvers)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for j, expected := range tc.expectResolvers {
|
|
||||||
if actualResolvers[j] != expected {
|
|
||||||
t.Errorf("Resolver %d mismatch. Expected: %s, got: %s", j, expected, actualResolvers[j])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGlobalCertIssuerAppliesToImplicitACMEIssuer(t *testing.T) {
|
|
||||||
adapter := caddyfile.Adapter{
|
|
||||||
ServerType: ServerType{},
|
|
||||||
}
|
|
||||||
|
|
||||||
input := `{
|
|
||||||
cert_issuer acme {
|
|
||||||
disable_tlsalpn_challenge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
report.company.intern {
|
|
||||||
tls {
|
|
||||||
ca https://deglacme01.company.intern/acme/acme/directory
|
|
||||||
ca_root /etc/certs/company_root2.crt
|
|
||||||
}
|
|
||||||
respond "ok"
|
|
||||||
}`
|
|
||||||
|
|
||||||
out, _, err := adapter.Adapt([]byte(input), nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("adapting caddyfile: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var config struct {
|
|
||||||
Apps struct {
|
|
||||||
TLS *caddytls.TLS `json:"tls"`
|
|
||||||
} `json:"apps"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(out, &config); err != nil {
|
|
||||||
t.Fatalf("unmarshaling adapted config: %v", err)
|
|
||||||
}
|
|
||||||
if config.Apps.TLS == nil || config.Apps.TLS.Automation == nil {
|
|
||||||
t.Fatal("expected tls automation config")
|
|
||||||
}
|
|
||||||
|
|
||||||
var subjectPolicy *caddytls.AutomationPolicy
|
|
||||||
for _, ap := range config.Apps.TLS.Automation.Policies {
|
|
||||||
if len(ap.SubjectsRaw) == 1 && ap.SubjectsRaw[0] == "report.company.intern" {
|
|
||||||
subjectPolicy = ap
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if subjectPolicy == nil {
|
|
||||||
t.Fatal("expected subject-specific automation policy")
|
|
||||||
}
|
|
||||||
if len(subjectPolicy.IssuersRaw) != 1 {
|
|
||||||
t.Fatalf("expected one issuer for subject-specific policy, got %d", len(subjectPolicy.IssuersRaw))
|
|
||||||
}
|
|
||||||
|
|
||||||
var issuer caddytls.ACMEIssuer
|
|
||||||
if err := json.Unmarshal(subjectPolicy.IssuersRaw[0], &issuer); err != nil {
|
|
||||||
t.Fatalf("unmarshaling issuer: %v", err)
|
|
||||||
}
|
|
||||||
if issuer.CA != "https://deglacme01.company.intern/acme/acme/directory" {
|
|
||||||
t.Fatalf("expected custom ACME CA, got %q", issuer.CA)
|
|
||||||
}
|
|
||||||
if len(issuer.TrustedRootsPEMFiles) != 1 || issuer.TrustedRootsPEMFiles[0] != "/etc/certs/company_root2.crt" {
|
|
||||||
t.Fatalf("expected trusted roots to include site CA root, got %v", issuer.TrustedRootsPEMFiles)
|
|
||||||
}
|
|
||||||
if issuer.Challenges == nil || issuer.Challenges.TLSALPN == nil || !issuer.Challenges.TLSALPN.Disabled {
|
|
||||||
t.Fatalf("expected tls-alpn challenge to be disabled, got %#v", issuer.Challenges)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMergeACMEIssuers(t *testing.T) {
|
|
||||||
base := &caddytls.ACMEIssuer{
|
|
||||||
Email: "ops@example.com",
|
|
||||||
Challenges: &caddytls.ChallengesConfig{
|
|
||||||
HTTP: &caddytls.HTTPChallengeConfig{
|
|
||||||
AlternatePort: 8080,
|
|
||||||
},
|
|
||||||
TLSALPN: &caddytls.TLSALPNChallengeConfig{
|
|
||||||
Disabled: true,
|
|
||||||
AlternatePort: 8443,
|
|
||||||
},
|
|
||||||
DNS: &caddytls.DNSChallengeConfig{
|
|
||||||
Resolvers: []string{"1.1.1.1"},
|
|
||||||
OverrideDomain: "_acme-challenge.example.net",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
TrustedRootsPEMFiles: []string{"global.pem"},
|
|
||||||
}
|
|
||||||
overrides := &caddytls.ACMEIssuer{
|
|
||||||
CA: "https://deglacme01.company.intern/acme/acme/directory",
|
|
||||||
Challenges: &caddytls.ChallengesConfig{
|
|
||||||
HTTP: &caddytls.HTTPChallengeConfig{
|
|
||||||
Disabled: true,
|
|
||||||
},
|
|
||||||
DNS: &caddytls.DNSChallengeConfig{
|
|
||||||
PropagationTimeout: caddy.Duration(time.Minute),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
TrustedRootsPEMFiles: []string{"site.pem"},
|
|
||||||
}
|
|
||||||
|
|
||||||
merged := mergeACMEIssuers(base, overrides)
|
|
||||||
if merged.CA != overrides.CA {
|
|
||||||
t.Fatalf("expected merged CA %q, got %q", overrides.CA, merged.CA)
|
|
||||||
}
|
|
||||||
if merged.Email != base.Email {
|
|
||||||
t.Fatalf("expected merged email %q, got %q", base.Email, merged.Email)
|
|
||||||
}
|
|
||||||
if len(merged.TrustedRootsPEMFiles) != 2 || merged.TrustedRootsPEMFiles[0] != "global.pem" || merged.TrustedRootsPEMFiles[1] != "site.pem" {
|
|
||||||
t.Fatalf("expected merged roots [global.pem site.pem], got %v", merged.TrustedRootsPEMFiles)
|
|
||||||
}
|
|
||||||
if merged.Challenges == nil || merged.Challenges.HTTP == nil || !merged.Challenges.HTTP.Disabled || merged.Challenges.HTTP.AlternatePort != 8080 {
|
|
||||||
t.Fatalf("expected merged HTTP challenge config to preserve alternate port and apply disable flag, got %#v", merged.Challenges)
|
|
||||||
}
|
|
||||||
if merged.Challenges.TLSALPN == nil || !merged.Challenges.TLSALPN.Disabled || merged.Challenges.TLSALPN.AlternatePort != 8443 {
|
|
||||||
t.Fatalf("expected merged TLS-ALPN challenge config to preserve global settings, got %#v", merged.Challenges)
|
|
||||||
}
|
|
||||||
if merged.Challenges.DNS == nil || merged.Challenges.DNS.PropagationTimeout != caddy.Duration(time.Minute) || len(merged.Challenges.DNS.Resolvers) != 1 || merged.Challenges.DNS.Resolvers[0] != "1.1.1.1" || merged.Challenges.DNS.OverrideDomain != "_acme-challenge.example.net" {
|
|
||||||
t.Fatalf("expected merged DNS challenge config to preserve global values and apply overrides, got %#v", merged.Challenges)
|
|
||||||
}
|
|
||||||
|
|
||||||
if base.CA != "" {
|
|
||||||
t.Fatalf("expected base issuer to remain unchanged, got CA %q", base.CA)
|
|
||||||
}
|
|
||||||
if len(base.TrustedRootsPEMFiles) != 1 || base.TrustedRootsPEMFiles[0] != "global.pem" {
|
|
||||||
t.Fatalf("expected base roots to remain unchanged, got %v", base.TrustedRootsPEMFiles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ package httpcaddyfile
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
@@ -28,16 +27,14 @@ func init() {
|
|||||||
RegisterGlobalOption("pki", parsePKIApp)
|
RegisterGlobalOption("pki", parsePKIApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parsePKIApp parses the global pki option. Syntax:
|
// parsePKIApp parses the global log option. Syntax:
|
||||||
//
|
//
|
||||||
// pki {
|
// pki {
|
||||||
// ca [<id>] {
|
// ca [<id>] {
|
||||||
// name <name>
|
// name <name>
|
||||||
// root_cn <name>
|
// root_cn <name>
|
||||||
// intermediate_cn <name>
|
// intermediate_cn <name>
|
||||||
// intermediate_lifetime <duration>
|
// intermediate_lifetime <duration>
|
||||||
// maintenance_interval <duration>
|
|
||||||
// renewal_window_ratio <ratio>
|
|
||||||
// root {
|
// root {
|
||||||
// cert <path>
|
// cert <path>
|
||||||
// key <path>
|
// key <path>
|
||||||
@@ -102,26 +99,6 @@ func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) {
|
|||||||
}
|
}
|
||||||
pkiCa.IntermediateLifetime = caddy.Duration(dur)
|
pkiCa.IntermediateLifetime = caddy.Duration(dur)
|
||||||
|
|
||||||
case "maintenance_interval":
|
|
||||||
if !d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
dur, err := caddy.ParseDuration(d.Val())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pkiCa.MaintenanceInterval = caddy.Duration(dur)
|
|
||||||
|
|
||||||
case "renewal_window_ratio":
|
|
||||||
if !d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
ratio, err := strconv.ParseFloat(d.Val(), 64)
|
|
||||||
if err != nil || ratio <= 0 || ratio > 1 {
|
|
||||||
return nil, d.Errf("renewal_window_ratio must be a number in (0, 1], got %s", d.Val())
|
|
||||||
}
|
|
||||||
pkiCa.RenewalWindowRatio = ratio
|
|
||||||
|
|
||||||
case "root":
|
case "root":
|
||||||
if pkiCa.Root == nil {
|
if pkiCa.Root == nil {
|
||||||
pkiCa.Root = new(caddypki.KeyPair)
|
pkiCa.Root = new(caddypki.KeyPair)
|
||||||
|
|||||||
@@ -1,86 +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 httpcaddyfile
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParsePKIApp_maintenanceIntervalAndRenewalWindowRatio(t *testing.T) {
|
|
||||||
input := `{
|
|
||||||
pki {
|
|
||||||
ca local {
|
|
||||||
maintenance_interval 5m
|
|
||||||
renewal_window_ratio 0.15
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:8080 {
|
|
||||||
}
|
|
||||||
`
|
|
||||||
adapter := caddyfile.Adapter{ServerType: ServerType{}}
|
|
||||||
out, _, err := adapter.Adapt([]byte(input), nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Adapt failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var cfg struct {
|
|
||||||
Apps struct {
|
|
||||||
PKI struct {
|
|
||||||
CertificateAuthorities map[string]struct {
|
|
||||||
MaintenanceInterval int64 `json:"maintenance_interval,omitempty"`
|
|
||||||
RenewalWindowRatio float64 `json:"renewal_window_ratio,omitempty"`
|
|
||||||
} `json:"certificate_authorities,omitempty"`
|
|
||||||
} `json:"pki,omitempty"`
|
|
||||||
} `json:"apps"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(out, &cfg); err != nil {
|
|
||||||
t.Fatalf("unmarshal config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ca, ok := cfg.Apps.PKI.CertificateAuthorities["local"]
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("expected certificate_authorities.local to exist")
|
|
||||||
}
|
|
||||||
wantInterval := 5 * time.Minute.Nanoseconds()
|
|
||||||
if ca.MaintenanceInterval != wantInterval {
|
|
||||||
t.Errorf("maintenance_interval = %d, want %d (5m)", ca.MaintenanceInterval, wantInterval)
|
|
||||||
}
|
|
||||||
if ca.RenewalWindowRatio != 0.15 {
|
|
||||||
t.Errorf("renewal_window_ratio = %v, want 0.15", ca.RenewalWindowRatio)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParsePKIApp_renewalWindowRatioInvalid(t *testing.T) {
|
|
||||||
input := `{
|
|
||||||
pki {
|
|
||||||
ca local {
|
|
||||||
renewal_window_ratio 1.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:8080 {
|
|
||||||
}
|
|
||||||
`
|
|
||||||
adapter := caddyfile.Adapter{ServerType: ServerType{}}
|
|
||||||
_, _, err := adapter.Adapt([]byte(input), nil)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error for renewal_window_ratio > 1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -36,30 +36,26 @@ type serverOptions struct {
|
|||||||
ListenerAddress string
|
ListenerAddress string
|
||||||
|
|
||||||
// These will all map 1:1 to the caddyhttp.Server struct
|
// These will all map 1:1 to the caddyhttp.Server struct
|
||||||
Name string
|
Name string
|
||||||
ListenerWrappersRaw []json.RawMessage
|
ListenerWrappersRaw []json.RawMessage
|
||||||
PacketConnWrappersRaw []json.RawMessage
|
ReadTimeout caddy.Duration
|
||||||
ReadTimeout caddy.Duration
|
ReadHeaderTimeout caddy.Duration
|
||||||
ReadHeaderTimeout caddy.Duration
|
WriteTimeout caddy.Duration
|
||||||
WriteTimeout caddy.Duration
|
IdleTimeout caddy.Duration
|
||||||
IdleTimeout caddy.Duration
|
KeepAliveInterval caddy.Duration
|
||||||
KeepAliveInterval caddy.Duration
|
KeepAliveIdle caddy.Duration
|
||||||
KeepAliveIdle caddy.Duration
|
KeepAliveCount int
|
||||||
KeepAliveCount int
|
MaxHeaderBytes int
|
||||||
MaxHeaderBytes int
|
EnableFullDuplex bool
|
||||||
EnableFullDuplex bool
|
Protocols []string
|
||||||
Protocols []string
|
StrictSNIHost *bool
|
||||||
StrictSNIHost *bool
|
TrustedProxiesRaw json.RawMessage
|
||||||
TrustedProxiesRaw json.RawMessage
|
TrustedProxiesStrict int
|
||||||
TrustedProxiesStrict int
|
TrustedProxiesUnix bool
|
||||||
TrustedProxiesUnix bool
|
ClientIPHeaders []string
|
||||||
ClientIPHeaders []string
|
ShouldLogCredentials bool
|
||||||
ShouldLogCredentials bool
|
Metrics *caddyhttp.Metrics
|
||||||
Metrics *caddyhttp.Metrics
|
Trace bool // TODO: EXPERIMENTAL
|
||||||
Trace bool // TODO: EXPERIMENTAL
|
|
||||||
// If set, overrides whether QUIC listeners allow 0-RTT (early data).
|
|
||||||
// If nil, the default behavior is used (currently allowed).
|
|
||||||
Allow0RTT *bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||||
@@ -103,26 +99,6 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
|||||||
serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper)
|
serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "packet_conn_wrappers":
|
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
|
||||||
modID := "caddy.packetconns." + d.Val()
|
|
||||||
unm, err := caddyfile.UnmarshalModule(d, modID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
packetConnWrapper, ok := unm.(caddy.PacketConnWrapper)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("module %s (%T) is not a packet conn wrapper", modID, unm)
|
|
||||||
}
|
|
||||||
jsonPacketConnWrapper := caddyconfig.JSONModuleObject(
|
|
||||||
packetConnWrapper,
|
|
||||||
"wrapper",
|
|
||||||
packetConnWrapper.(caddy.Module).CaddyModule().ID.Name(),
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
serverOpts.PacketConnWrappersRaw = append(serverOpts.PacketConnWrappersRaw, jsonPacketConnWrapper)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "timeouts":
|
case "timeouts":
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
switch d.Val() {
|
switch d.Val() {
|
||||||
@@ -312,17 +288,6 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
|||||||
}
|
}
|
||||||
serverOpts.Trace = true
|
serverOpts.Trace = true
|
||||||
|
|
||||||
case "0rtt":
|
|
||||||
// only supports "off" for now
|
|
||||||
if !d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
if d.Val() != "off" {
|
|
||||||
return nil, d.Errf("unsupported 0rtt argument '%s' (only 'off' is supported)", d.Val())
|
|
||||||
}
|
|
||||||
boolVal := false
|
|
||||||
serverOpts.Allow0RTT = &boolVal
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
||||||
}
|
}
|
||||||
@@ -370,7 +335,6 @@ func applyServerOptions(
|
|||||||
|
|
||||||
// set all the options
|
// set all the options
|
||||||
server.ListenerWrappersRaw = opts.ListenerWrappersRaw
|
server.ListenerWrappersRaw = opts.ListenerWrappersRaw
|
||||||
server.PacketConnWrappersRaw = opts.PacketConnWrappersRaw
|
|
||||||
server.ReadTimeout = opts.ReadTimeout
|
server.ReadTimeout = opts.ReadTimeout
|
||||||
server.ReadHeaderTimeout = opts.ReadHeaderTimeout
|
server.ReadHeaderTimeout = opts.ReadHeaderTimeout
|
||||||
server.WriteTimeout = opts.WriteTimeout
|
server.WriteTimeout = opts.WriteTimeout
|
||||||
@@ -387,7 +351,6 @@ func applyServerOptions(
|
|||||||
server.TrustedProxiesStrict = opts.TrustedProxiesStrict
|
server.TrustedProxiesStrict = opts.TrustedProxiesStrict
|
||||||
server.TrustedProxiesUnix = opts.TrustedProxiesUnix
|
server.TrustedProxiesUnix = opts.TrustedProxiesUnix
|
||||||
server.Metrics = opts.Metrics
|
server.Metrics = opts.Metrics
|
||||||
server.Allow0RTT = opts.Allow0RTT
|
|
||||||
if opts.ShouldLogCredentials {
|
if opts.ShouldLogCredentials {
|
||||||
if server.Logs == nil {
|
if server.Logs == nil {
|
||||||
server.Logs = new(caddyhttp.ServerLogConfig)
|
server.Logs = new(caddyhttp.ServerLogConfig)
|
||||||
|
|||||||
@@ -92,8 +92,26 @@ func (st ServerType) buildTLSApp(
|
|||||||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
|
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var wildcardHosts []string // collect all hosts that have a wildcard in them, and aren't HTTP
|
||||||
forcedAutomatedNames := make(map[string]struct{}) // explicitly configured to be automated, even if covered by a wildcard
|
forcedAutomatedNames := make(map[string]struct{}) // explicitly configured to be automated, even if covered by a wildcard
|
||||||
|
|
||||||
|
for _, p := range pairings {
|
||||||
|
var addresses []string
|
||||||
|
for _, addressWithProtocols := range p.addressesWithProtocols {
|
||||||
|
addresses = append(addresses, addressWithProtocols.address)
|
||||||
|
}
|
||||||
|
if !listenersUseAnyPortOtherThan(addresses, httpPort) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, sblock := range p.serverBlocks {
|
||||||
|
for _, addr := range sblock.parsedKeys {
|
||||||
|
if strings.HasPrefix(addr.Host, "*.") {
|
||||||
|
wildcardHosts = append(wildcardHosts, addr.Host[2:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, p := range pairings {
|
for _, p := range pairings {
|
||||||
// avoid setting up TLS automation policies for a server that is HTTP-only
|
// avoid setting up TLS automation policies for a server that is HTTP-only
|
||||||
var addresses []string
|
var addresses []string
|
||||||
@@ -117,6 +135,12 @@ func (st ServerType) buildTLSApp(
|
|||||||
return nil, warnings, err
|
return nil, warnings, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make a plain copy so we can compare whether we made any changes
|
||||||
|
apCopy, err := newBaseAutomationPolicy(options, warnings, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, warnings, err
|
||||||
|
}
|
||||||
|
|
||||||
sblockHosts := sblock.hostsFromKeys(false)
|
sblockHosts := sblock.hostsFromKeys(false)
|
||||||
if len(sblockHosts) == 0 && catchAllAP != nil {
|
if len(sblockHosts) == 0 && catchAllAP != nil {
|
||||||
ap = catchAllAP
|
ap = catchAllAP
|
||||||
@@ -143,12 +167,6 @@ func (st ServerType) buildTLSApp(
|
|||||||
ap.KeyType = keyTypeVals[0].Value.(string)
|
ap.KeyType = keyTypeVals[0].Value.(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
if renewalWindowRatioVals, ok := sblock.pile["tls.renewal_window_ratio"]; ok {
|
|
||||||
ap.RenewalWindowRatio = renewalWindowRatioVals[0].Value.(float64)
|
|
||||||
} else if globalRenewalWindowRatio, ok := options["renewal_window_ratio"]; ok {
|
|
||||||
ap.RenewalWindowRatio = globalRenewalWindowRatio.(float64)
|
|
||||||
}
|
|
||||||
|
|
||||||
// certificate issuers
|
// certificate issuers
|
||||||
if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
|
if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
|
||||||
var issuers []certmagic.Issuer
|
var issuers []certmagic.Issuer
|
||||||
@@ -235,6 +253,16 @@ func (st ServerType) buildTLSApp(
|
|||||||
hostsNotHTTP := sblock.hostsFromKeysNotHTTP(httpPort)
|
hostsNotHTTP := sblock.hostsFromKeysNotHTTP(httpPort)
|
||||||
sort.Strings(hostsNotHTTP) // solely for deterministic test results
|
sort.Strings(hostsNotHTTP) // solely for deterministic test results
|
||||||
|
|
||||||
|
// if the we prefer wildcards and the AP is unchanged,
|
||||||
|
// then we can skip this AP because it should be covered
|
||||||
|
// by an AP with a wildcard
|
||||||
|
if slices.Contains(autoHTTPS, "prefer_wildcard") {
|
||||||
|
if hostsCoveredByWildcard(hostsNotHTTP, wildcardHosts) &&
|
||||||
|
reflect.DeepEqual(ap, apCopy) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// associate our new automation policy with this server block's hosts
|
// associate our new automation policy with this server block's hosts
|
||||||
ap.SubjectsRaw = hostsNotHTTP
|
ap.SubjectsRaw = hostsNotHTTP
|
||||||
|
|
||||||
@@ -334,11 +362,6 @@ func (st ServerType) buildTLSApp(
|
|||||||
tlsApp.DNSRaw = caddyconfig.JSONModuleObject(globalDNS, "name", globalDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
|
tlsApp.DNSRaw = caddyconfig.JSONModuleObject(globalDNS, "name", globalDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// set up "global" (to the TLS app) DNS resolvers config
|
|
||||||
if globalResolvers, ok := options["tls_resolvers"]; ok && globalResolvers != nil {
|
|
||||||
tlsApp.Resolvers = globalResolvers.([]string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up ECH from Caddyfile options
|
// set up ECH from Caddyfile options
|
||||||
if ech, ok := options["ech"].(*caddytls.ECH); ok {
|
if ech, ok := options["ech"].(*caddytls.ECH); ok {
|
||||||
tlsApp.EncryptedClientHello = ech
|
tlsApp.EncryptedClientHello = ech
|
||||||
@@ -553,8 +576,9 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
|||||||
if acmeIssuer.Challenges.DNS == nil {
|
if acmeIssuer.Challenges.DNS == nil {
|
||||||
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
||||||
}
|
}
|
||||||
if globalACMEDNS != nil && acmeIssuer.Challenges.DNS.ProviderRaw == nil {
|
// If global `dns` is set, do NOT set provider in issuer, just set empty dns config
|
||||||
// Set a global DNS provider if `acme_dns` is set
|
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.DNS.ProviderRaw = caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -600,301 +624,9 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
|||||||
if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
|
if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
|
||||||
acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
|
acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
|
||||||
}
|
}
|
||||||
// apply global resolvers if DNS challenge is configured and resolvers are not already set
|
|
||||||
globalResolvers := options["tls_resolvers"]
|
|
||||||
if globalResolvers != nil && acmeIssuer.Challenges != nil && acmeIssuer.Challenges.DNS != nil {
|
|
||||||
// Check if DNS challenge is actually configured
|
|
||||||
hasDNSChallenge := globalACMEDNSok || acmeIssuer.Challenges.DNS.ProviderRaw != nil
|
|
||||||
if hasDNSChallenge && len(acmeIssuer.Challenges.DNS.Resolvers) == 0 {
|
|
||||||
acmeIssuer.Challenges.DNS.Resolvers = globalResolvers.([]string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// implicitACMEIssuers returns the issuers to use for ACME-related tls
|
|
||||||
// shortcuts such as ca, ca_root, and dns. If any global cert_issuer options
|
|
||||||
// configure ACME issuers, those become the templates for the local shortcut
|
|
||||||
// configuration; otherwise, default ACME issuers are used.
|
|
||||||
func implicitACMEIssuers(h Helper, acmeIssuer *caddytls.ACMEIssuer) []certmagic.Issuer {
|
|
||||||
globalIssuers, _ := h.Option("cert_issuer").([]certmagic.Issuer)
|
|
||||||
|
|
||||||
var implicitIssuers []certmagic.Issuer
|
|
||||||
for _, issuer := range globalIssuers {
|
|
||||||
acmeWrapper, ok := issuer.(acmeCapable)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
baseIssuer := acmeWrapper.GetACMEIssuer()
|
|
||||||
if baseIssuer == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
implicitIssuers = append(implicitIssuers, mergeACMEIssuers(baseIssuer, acmeIssuer))
|
|
||||||
}
|
|
||||||
if len(implicitIssuers) > 0 {
|
|
||||||
return implicitIssuers
|
|
||||||
}
|
|
||||||
|
|
||||||
// If an ACME CA endpoint was set locally, the user expects to use only that
|
|
||||||
// CA rather than the usual default fallback issuers.
|
|
||||||
defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email)
|
|
||||||
if acmeIssuer.CA != "" {
|
|
||||||
defaultIssuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
|
|
||||||
}
|
|
||||||
|
|
||||||
implicitIssuers = make([]certmagic.Issuer, 0, len(defaultIssuers))
|
|
||||||
for _, issuer := range defaultIssuers {
|
|
||||||
acmeWrapper, ok := issuer.(acmeCapable)
|
|
||||||
if !ok {
|
|
||||||
implicitIssuers = append(implicitIssuers, issuer)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
baseIssuer := acmeWrapper.GetACMEIssuer()
|
|
||||||
if baseIssuer == nil {
|
|
||||||
implicitIssuers = append(implicitIssuers, issuer)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
implicitIssuers = append(implicitIssuers, mergeACMEIssuers(baseIssuer, acmeIssuer))
|
|
||||||
}
|
|
||||||
return implicitIssuers
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeACMEIssuers(base, overrides *caddytls.ACMEIssuer) *caddytls.ACMEIssuer {
|
|
||||||
if base == nil {
|
|
||||||
return cloneACMEIssuer(overrides)
|
|
||||||
}
|
|
||||||
|
|
||||||
merged := cloneACMEIssuer(base)
|
|
||||||
if overrides == nil {
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
if overrides.CA != "" {
|
|
||||||
merged.CA = overrides.CA
|
|
||||||
}
|
|
||||||
if overrides.TestCA != "" {
|
|
||||||
merged.TestCA = overrides.TestCA
|
|
||||||
}
|
|
||||||
if overrides.Email != "" {
|
|
||||||
merged.Email = overrides.Email
|
|
||||||
}
|
|
||||||
if overrides.Profile != "" {
|
|
||||||
merged.Profile = overrides.Profile
|
|
||||||
}
|
|
||||||
if overrides.AccountKey != "" {
|
|
||||||
merged.AccountKey = overrides.AccountKey
|
|
||||||
}
|
|
||||||
if overrides.ExternalAccount != nil {
|
|
||||||
merged.ExternalAccount = cloneACMEEAB(overrides.ExternalAccount)
|
|
||||||
}
|
|
||||||
if overrides.ACMETimeout != 0 {
|
|
||||||
merged.ACMETimeout = overrides.ACMETimeout
|
|
||||||
}
|
|
||||||
if len(overrides.TrustedRootsPEMFiles) > 0 {
|
|
||||||
merged.TrustedRootsPEMFiles = appendUniqueStrings(merged.TrustedRootsPEMFiles, overrides.TrustedRootsPEMFiles...)
|
|
||||||
}
|
|
||||||
if overrides.PreferredChains != nil {
|
|
||||||
merged.PreferredChains = cloneChainPreference(overrides.PreferredChains)
|
|
||||||
}
|
|
||||||
if overrides.CertificateLifetime != 0 {
|
|
||||||
merged.CertificateLifetime = overrides.CertificateLifetime
|
|
||||||
}
|
|
||||||
if len(overrides.NetworkProxyRaw) > 0 {
|
|
||||||
merged.NetworkProxyRaw = slices.Clone(overrides.NetworkProxyRaw)
|
|
||||||
}
|
|
||||||
merged.Challenges = mergeChallengesConfig(merged.Challenges, overrides.Challenges)
|
|
||||||
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeChallengesConfig(base, overrides *caddytls.ChallengesConfig) *caddytls.ChallengesConfig {
|
|
||||||
if base == nil {
|
|
||||||
return cloneChallengesConfig(overrides)
|
|
||||||
}
|
|
||||||
merged := cloneChallengesConfig(base)
|
|
||||||
if overrides == nil {
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
merged.HTTP = mergeHTTPChallengeConfig(merged.HTTP, overrides.HTTP)
|
|
||||||
merged.TLSALPN = mergeTLSALPNChallengeConfig(merged.TLSALPN, overrides.TLSALPN)
|
|
||||||
merged.DNS = mergeDNSChallengeConfig(merged.DNS, overrides.DNS)
|
|
||||||
if overrides.BindHost != "" {
|
|
||||||
merged.BindHost = overrides.BindHost
|
|
||||||
}
|
|
||||||
if overrides.Distributed != nil {
|
|
||||||
value := *overrides.Distributed
|
|
||||||
merged.Distributed = &value
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeHTTPChallengeConfig(base, overrides *caddytls.HTTPChallengeConfig) *caddytls.HTTPChallengeConfig {
|
|
||||||
if base == nil {
|
|
||||||
return cloneHTTPChallengeConfig(overrides)
|
|
||||||
}
|
|
||||||
merged := cloneHTTPChallengeConfig(base)
|
|
||||||
if overrides == nil {
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
if overrides.Disabled {
|
|
||||||
merged.Disabled = true
|
|
||||||
}
|
|
||||||
if overrides.AlternatePort != 0 {
|
|
||||||
merged.AlternatePort = overrides.AlternatePort
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeTLSALPNChallengeConfig(base, overrides *caddytls.TLSALPNChallengeConfig) *caddytls.TLSALPNChallengeConfig {
|
|
||||||
if base == nil {
|
|
||||||
return cloneTLSALPNChallengeConfig(overrides)
|
|
||||||
}
|
|
||||||
merged := cloneTLSALPNChallengeConfig(base)
|
|
||||||
if overrides == nil {
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
if overrides.Disabled {
|
|
||||||
merged.Disabled = true
|
|
||||||
}
|
|
||||||
if overrides.AlternatePort != 0 {
|
|
||||||
merged.AlternatePort = overrides.AlternatePort
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeDNSChallengeConfig(base, overrides *caddytls.DNSChallengeConfig) *caddytls.DNSChallengeConfig {
|
|
||||||
if base == nil {
|
|
||||||
return cloneDNSChallengeConfig(overrides)
|
|
||||||
}
|
|
||||||
merged := cloneDNSChallengeConfig(base)
|
|
||||||
if overrides == nil {
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(overrides.ProviderRaw) > 0 {
|
|
||||||
merged.ProviderRaw = slices.Clone(overrides.ProviderRaw)
|
|
||||||
}
|
|
||||||
if overrides.PropagationDelay != 0 {
|
|
||||||
merged.PropagationDelay = overrides.PropagationDelay
|
|
||||||
}
|
|
||||||
if overrides.PropagationTimeout != 0 {
|
|
||||||
merged.PropagationTimeout = overrides.PropagationTimeout
|
|
||||||
}
|
|
||||||
if overrides.Resolvers != nil {
|
|
||||||
merged.Resolvers = slices.Clone(overrides.Resolvers)
|
|
||||||
}
|
|
||||||
if overrides.OverrideDomain != "" {
|
|
||||||
merged.OverrideDomain = overrides.OverrideDomain
|
|
||||||
}
|
|
||||||
if overrides.TTL != 0 {
|
|
||||||
merged.TTL = overrides.TTL
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneACMEIssuer(iss *caddytls.ACMEIssuer) *caddytls.ACMEIssuer {
|
|
||||||
if iss == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cloned := *iss
|
|
||||||
cloned.Challenges = cloneChallengesConfig(iss.Challenges)
|
|
||||||
cloned.ExternalAccount = cloneACMEEAB(iss.ExternalAccount)
|
|
||||||
cloned.TrustedRootsPEMFiles = slices.Clone(iss.TrustedRootsPEMFiles)
|
|
||||||
cloned.PreferredChains = cloneChainPreference(iss.PreferredChains)
|
|
||||||
cloned.NetworkProxyRaw = slices.Clone(iss.NetworkProxyRaw)
|
|
||||||
|
|
||||||
return &cloned
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneChallengesConfig(cfg *caddytls.ChallengesConfig) *caddytls.ChallengesConfig {
|
|
||||||
if cfg == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cloned := *cfg
|
|
||||||
cloned.HTTP = cloneHTTPChallengeConfig(cfg.HTTP)
|
|
||||||
cloned.TLSALPN = cloneTLSALPNChallengeConfig(cfg.TLSALPN)
|
|
||||||
cloned.DNS = cloneDNSChallengeConfig(cfg.DNS)
|
|
||||||
if cfg.Distributed != nil {
|
|
||||||
value := *cfg.Distributed
|
|
||||||
cloned.Distributed = &value
|
|
||||||
}
|
|
||||||
|
|
||||||
return &cloned
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneHTTPChallengeConfig(cfg *caddytls.HTTPChallengeConfig) *caddytls.HTTPChallengeConfig {
|
|
||||||
if cfg == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cloned := *cfg
|
|
||||||
return &cloned
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneTLSALPNChallengeConfig(cfg *caddytls.TLSALPNChallengeConfig) *caddytls.TLSALPNChallengeConfig {
|
|
||||||
if cfg == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cloned := *cfg
|
|
||||||
return &cloned
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneDNSChallengeConfig(cfg *caddytls.DNSChallengeConfig) *caddytls.DNSChallengeConfig {
|
|
||||||
if cfg == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cloned := *cfg
|
|
||||||
cloned.ProviderRaw = slices.Clone(cfg.ProviderRaw)
|
|
||||||
cloned.Resolvers = slices.Clone(cfg.Resolvers)
|
|
||||||
|
|
||||||
return &cloned
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneACMEEAB(eab *acme.EAB) *acme.EAB {
|
|
||||||
if eab == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cloned := *eab
|
|
||||||
return &cloned
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneChainPreference(pref *caddytls.ChainPreference) *caddytls.ChainPreference {
|
|
||||||
if pref == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cloned := *pref
|
|
||||||
cloned.RootCommonName = slices.Clone(pref.RootCommonName)
|
|
||||||
cloned.AnyCommonName = slices.Clone(pref.AnyCommonName)
|
|
||||||
if pref.Smallest != nil {
|
|
||||||
value := *pref.Smallest
|
|
||||||
cloned.Smallest = &value
|
|
||||||
}
|
|
||||||
|
|
||||||
return &cloned
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendUniqueStrings(existing []string, additions ...string) []string {
|
|
||||||
for _, value := range additions {
|
|
||||||
if !slices.Contains(existing, value) {
|
|
||||||
existing = append(existing, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
|
|
||||||
// newBaseAutomationPolicy returns a new TLS automation policy that gets
|
// newBaseAutomationPolicy returns a new TLS automation policy that gets
|
||||||
// its values from the global options map. It should be used as the base
|
// its values from the global options map. It should be used as the base
|
||||||
// for any other automation policies. A nil policy (and no error) will be
|
// for any other automation policies. A nil policy (and no error) will be
|
||||||
@@ -909,8 +641,7 @@ func newBaseAutomationPolicy(
|
|||||||
_, hasLocalCerts := options["local_certs"]
|
_, hasLocalCerts := options["local_certs"]
|
||||||
keyType, hasKeyType := options["key_type"]
|
keyType, hasKeyType := options["key_type"]
|
||||||
ocspStapling, hasOCSPStapling := options["ocsp_stapling"]
|
ocspStapling, hasOCSPStapling := options["ocsp_stapling"]
|
||||||
renewalWindowRatio, hasRenewalWindowRatio := options["renewal_window_ratio"]
|
hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling
|
||||||
hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling || hasRenewalWindowRatio
|
|
||||||
|
|
||||||
globalACMECA := options["acme_ca"]
|
globalACMECA := options["acme_ca"]
|
||||||
globalACMECARoot := options["acme_ca_root"]
|
globalACMECARoot := options["acme_ca_root"]
|
||||||
@@ -957,10 +688,6 @@ func newBaseAutomationPolicy(
|
|||||||
ap.OCSPOverrides = ocspConfig.ResponderOverrides
|
ap.OCSPOverrides = ocspConfig.ResponderOverrides
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasRenewalWindowRatio {
|
|
||||||
ap.RenewalWindowRatio = renewalWindowRatio.(float64)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ap, nil
|
return ap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -981,31 +708,14 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
|
|||||||
emptyAPCount := 0
|
emptyAPCount := 0
|
||||||
origLenAPs := len(aps)
|
origLenAPs := len(aps)
|
||||||
// compute the number of empty policies (disregarding subjects) - see #4128
|
// compute the number of empty policies (disregarding subjects) - see #4128
|
||||||
// while we're at it,
|
|
||||||
emptyAP := new(caddytls.AutomationPolicy)
|
emptyAP := new(caddytls.AutomationPolicy)
|
||||||
for i := 0; i < len(aps); i++ {
|
for i := 0; i < len(aps); i++ {
|
||||||
emptyAP.SubjectsRaw = aps[i].SubjectsRaw
|
emptyAP.SubjectsRaw = aps[i].SubjectsRaw
|
||||||
emptyAP.ManagersRaw = nil
|
|
||||||
if reflect.DeepEqual(aps[i], emptyAP) {
|
if reflect.DeepEqual(aps[i], emptyAP) {
|
||||||
// AP is empty
|
|
||||||
emptyAPCount++
|
emptyAPCount++
|
||||||
|
if !automationPolicyHasAllPublicNames(aps[i]) {
|
||||||
// see if this AP shadows something later
|
// if this automation policy has internal names, we might as well remove it
|
||||||
shadowIdx := automationPolicyShadows(i, aps)
|
// so auto-https can implicitly use the internal issuer
|
||||||
emptyAP.SubjectsRaw = nil
|
|
||||||
if shadowIdx >= 0 {
|
|
||||||
emptyAP.SubjectsRaw = aps[shadowIdx].SubjectsRaw
|
|
||||||
// allow the later policy, which is likely for a wildcard, to have cert
|
|
||||||
// managers ("get_certificate"), since wildcards now cover specific
|
|
||||||
// subdomains by default, when configured (see discussion in #7559)
|
|
||||||
emptyAP.ManagersRaw = aps[shadowIdx].ManagersRaw
|
|
||||||
}
|
|
||||||
|
|
||||||
// if this is the last AP, we can delete it, since auto-https should
|
|
||||||
// pick it up; if it shadows something later that is also empty, we
|
|
||||||
// can similarly delete this; but if it shadows something that is NOT
|
|
||||||
// empty, we must not delete it since the shadowing has a purpose
|
|
||||||
if i == len(aps)-1 || (shadowIdx >= 0 && reflect.DeepEqual(aps[shadowIdx], emptyAP)) {
|
|
||||||
aps = slices.Delete(aps, i, i+1)
|
aps = slices.Delete(aps, i, i+1)
|
||||||
i--
|
i--
|
||||||
}
|
}
|
||||||
@@ -1036,7 +746,7 @@ outer:
|
|||||||
// otherwise the one without any subjects (a catch-all) would be
|
// otherwise the one without any subjects (a catch-all) would be
|
||||||
// eaten up by the one with subjects; and if both have subjects, we
|
// eaten up by the one with subjects; and if both have subjects, we
|
||||||
// need to combine their lists
|
// need to combine their lists
|
||||||
if automationPoliciesHaveSameIssuers(aps[i], aps[j]) &&
|
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
|
||||||
reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) &&
|
reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) &&
|
||||||
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
|
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
|
||||||
aps[i].MustStaple == aps[j].MustStaple &&
|
aps[i].MustStaple == aps[j].MustStaple &&
|
||||||
@@ -1128,58 +838,6 @@ func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) b
|
|||||||
(strings.Count(subj, "*.") < 2 || ap.OnDemand)
|
(strings.Count(subj, "*.") < 2 || ap.OnDemand)
|
||||||
}
|
}
|
||||||
|
|
||||||
func automationPoliciesHaveSameIssuers(a, b *caddytls.AutomationPolicy) bool {
|
|
||||||
if reflect.DeepEqual(a.IssuersRaw, b.IssuersRaw) {
|
|
||||||
return automationPoliciesHaveCompatibleImplicitIssuers(a, b)
|
|
||||||
}
|
|
||||||
return automationPolicyUsesDefaultInternalIssuer(a) && automationPolicyUsesDefaultInternalIssuer(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func automationPolicyUsesDefaultInternalIssuer(ap *caddytls.AutomationPolicy) bool {
|
|
||||||
if len(ap.IssuersRaw) == 0 && len(ap.Issuers) == 0 {
|
|
||||||
return automationPolicyImplicitIssuerClass(ap) == "internal"
|
|
||||||
}
|
|
||||||
return len(ap.IssuersRaw) == 1 &&
|
|
||||||
len(ap.Issuers) == 0 &&
|
|
||||||
string(bytes.TrimSpace(ap.IssuersRaw[0])) == `{"module":"internal"}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// automationPoliciesHaveCompatibleImplicitIssuers returns whether two policies
|
|
||||||
// without explicit issuers can be consolidated without changing default issuer
|
|
||||||
// selection for their subjects.
|
|
||||||
func automationPoliciesHaveCompatibleImplicitIssuers(a, b *caddytls.AutomationPolicy) bool {
|
|
||||||
if len(a.IssuersRaw) > 0 || len(a.Issuers) > 0 ||
|
|
||||||
len(b.IssuersRaw) > 0 || len(b.Issuers) > 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
aClass := automationPolicyImplicitIssuerClass(a)
|
|
||||||
bClass := automationPolicyImplicitIssuerClass(b)
|
|
||||||
return aClass == "catch-all" || bClass == "catch-all" || aClass == bClass
|
|
||||||
}
|
|
||||||
|
|
||||||
func automationPolicyImplicitIssuerClass(ap *caddytls.AutomationPolicy) string {
|
|
||||||
if len(ap.SubjectsRaw) == 0 {
|
|
||||||
return "catch-all"
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPublic := slices.ContainsFunc(ap.SubjectsRaw, func(subj string) bool {
|
|
||||||
return subjectQualifiesForPublicCert(ap, subj)
|
|
||||||
})
|
|
||||||
hasInternal := slices.ContainsFunc(ap.SubjectsRaw, func(subj string) bool {
|
|
||||||
return !subjectQualifiesForPublicCert(ap, subj)
|
|
||||||
})
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case hasPublic && hasInternal:
|
|
||||||
return "mixed"
|
|
||||||
case hasPublic:
|
|
||||||
return "public"
|
|
||||||
default:
|
|
||||||
return "internal"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// automationPolicyHasAllPublicNames returns true if all the names on the policy
|
// automationPolicyHasAllPublicNames returns true if all the names on the policy
|
||||||
// do NOT qualify for public certs OR are tailscale domains.
|
// do NOT qualify for public certs OR are tailscale domains.
|
||||||
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
|
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
|
||||||
@@ -1191,3 +849,20 @@ func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
|
|||||||
func isTailscaleDomain(name string) bool {
|
func isTailscaleDomain(name string) bool {
|
||||||
return strings.HasSuffix(strings.ToLower(name), ".ts.net")
|
return strings.HasSuffix(strings.ToLower(name), ".ts.net")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hostsCoveredByWildcard(hosts []string, wildcards []string) bool {
|
||||||
|
if len(hosts) == 0 || len(wildcards) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, host := range hosts {
|
||||||
|
for _, wildcard := range wildcards {
|
||||||
|
if strings.HasPrefix(host, "*.") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if certmagic.MatchWildcard(host, "*."+wildcard) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package httpcaddyfile
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,20 +54,3 @@ func TestAutomationPolicyIsSubset(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAutomationPoliciesAllowSameHostOnDifferentPorts(t *testing.T) {
|
|
||||||
input := `https://example.com:5000 localhost:5000 {
|
|
||||||
respond "one"
|
|
||||||
}
|
|
||||||
|
|
||||||
https://example.net localhost:8080 {
|
|
||||||
respond "two"
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
adapter := caddyfile.Adapter{ServerType: ServerType{}}
|
|
||||||
_, _, err := adapter.Adapt([]byte(input), nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("adapting Caddyfile: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response, error) {
|
func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response, error) {
|
||||||
resp, err := client.Do(request) //nolint:gosec // no SSRF; comes from trusted config
|
resp, err := client.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("problem calling http loader url: %v", err)
|
return nil, fmt.Errorf("problem calling http loader url: %v", err)
|
||||||
} else if resp.StatusCode < 200 || resp.StatusCode > 499 {
|
} else if resp.StatusCode < 200 || resp.StatusCode > 499 {
|
||||||
@@ -151,7 +151,7 @@ func doHttpCallWithRetries(ctx caddy.Context, client *http.Client, request *http
|
|||||||
var err error
|
var err error
|
||||||
const maxAttempts = 10
|
const maxAttempts = 10
|
||||||
|
|
||||||
for i := range maxAttempts {
|
for i := 0; i < maxAttempts; i++ {
|
||||||
resp, err = attemptHttpCall(client, request)
|
resp, err = attemptHttpCall(client, request)
|
||||||
if err != nil && i < maxAttempts-1 {
|
if err != nil && i < maxAttempts-1 {
|
||||||
select {
|
select {
|
||||||
|
|||||||
+1
-1
@@ -106,7 +106,7 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
caddy.Log().Named("admin.api.load").Error(err.Error())
|
caddy.Log().Named("admin.api.load").Error(err.Error())
|
||||||
}
|
}
|
||||||
_, _ = w.Write(respBody) //nolint:gosec // false positive: no XSS here
|
_, _ = w.Write(respBody)
|
||||||
}
|
}
|
||||||
body = result
|
body = result
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-31
@@ -187,7 +187,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
|
|||||||
req.Header.Add("Content-Type", "text/"+configType)
|
req.Header.Add("Content-Type", "text/"+configType)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := client.Do(req) //nolint:gosec // no SSRF because URL is hard-coded to localhost, and port comes from config
|
res, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("unable to contact caddy server. %s", err)
|
tc.t.Errorf("unable to contact caddy server. %s", err)
|
||||||
return err
|
return err
|
||||||
@@ -279,7 +279,7 @@ func validateTestPrerequisites(tc *Tester) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tc.t.Cleanup(func() {
|
tc.t.Cleanup(func() {
|
||||||
os.Remove(f.Name()) //nolint:gosec // false positive, filename comes from std lib, no path traversal
|
os.Remove(f.Name())
|
||||||
})
|
})
|
||||||
if _, err := fmt.Fprintf(f, initConfig, tc.config.AdminPort); err != nil {
|
if _, err := fmt.Fprintf(f, initConfig, tc.config.AdminPort); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -362,8 +362,6 @@ func CreateTestingTransport() *http.Transport {
|
|||||||
|
|
||||||
// AssertLoadError will load a config and expect an error
|
// AssertLoadError will load a config and expect an error
|
||||||
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
|
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
tc := NewTester(t)
|
tc := NewTester(t)
|
||||||
|
|
||||||
err := tc.initServer(rawConfig, configType)
|
err := tc.initServer(rawConfig, configType)
|
||||||
@@ -374,8 +372,6 @@ func AssertLoadError(t *testing.T, rawConfig string, configType string, expected
|
|||||||
|
|
||||||
// AssertRedirect makes a request and asserts the redirection happens
|
// AssertRedirect makes a request and asserts the redirection happens
|
||||||
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
|
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
|
||||||
tc.t.Helper()
|
|
||||||
|
|
||||||
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
|
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
|
||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
}
|
}
|
||||||
@@ -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
|
// CompareAdapt adapts a config and then compares it against an expected result
|
||||||
func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool {
|
func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool {
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
cfgAdapter := caddyconfig.GetAdapter(adapterName)
|
cfgAdapter := caddyconfig.GetAdapter(adapterName)
|
||||||
if cfgAdapter == nil {
|
if cfgAdapter == nil {
|
||||||
t.Logf("unrecognized config adapter '%s'", adapterName)
|
t.Logf("unrecognized config adapter '%s'", adapterName)
|
||||||
@@ -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
|
// AssertAdapt adapts a config and then tests it against an expected result
|
||||||
func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) {
|
func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) {
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
|
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
@@ -504,9 +496,7 @@ func applyHeaders(t testing.TB, req *http.Request, requestHeaders []string) {
|
|||||||
|
|
||||||
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
|
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
|
||||||
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
|
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
|
||||||
tc.t.Helper()
|
resp, err := tc.Client.Do(req)
|
||||||
|
|
||||||
resp, err := tc.Client.Do(req) //nolint:gosec // no SSRFs demonstrated
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Fatalf("failed to call server %s", err)
|
tc.t.Fatalf("failed to call server %s", err)
|
||||||
}
|
}
|
||||||
@@ -518,10 +508,8 @@ func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int)
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssertResponse requests a URI and asserts the status code and body.
|
// AssertResponse request a URI and assert the status code and the body contains a string
|
||||||
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
tc.t.Helper()
|
|
||||||
|
|
||||||
resp := tc.AssertResponseCode(req, expectedStatusCode)
|
resp := tc.AssertResponseCode(req, expectedStatusCode)
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@@ -541,10 +529,8 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
|
|||||||
|
|
||||||
// Verb specific test functions
|
// Verb specific test functions
|
||||||
|
|
||||||
// AssertGetResponse requests a URI with GET and expects a status code and body text.
|
// AssertGetResponse GET a URI and expect a statusCode and body text
|
||||||
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
tc.t.Helper()
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", requestURI, nil)
|
req, err := http.NewRequest("GET", requestURI, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Fatalf("unable to create request %s", err)
|
tc.t.Fatalf("unable to create request %s", err)
|
||||||
@@ -553,10 +539,8 @@ func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, e
|
|||||||
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssertDeleteResponse requests a URI with DELETE and expects a status code and body text.
|
// AssertDeleteResponse request a URI and expect a statusCode and body text
|
||||||
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
tc.t.Helper()
|
|
||||||
|
|
||||||
req, err := http.NewRequest("DELETE", requestURI, nil)
|
req, err := http.NewRequest("DELETE", requestURI, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Fatalf("unable to create request %s", err)
|
tc.t.Fatalf("unable to create request %s", err)
|
||||||
@@ -565,10 +549,8 @@ func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int
|
|||||||
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssertPostResponseBody requests a URI with POST and asserts the response code and body.
|
// AssertPostResponseBody POST to a URI and assert the response code and body
|
||||||
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
tc.t.Helper()
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", requestURI, requestBody)
|
req, err := http.NewRequest("POST", requestURI, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("failed to create request %s", err)
|
tc.t.Errorf("failed to create request %s", err)
|
||||||
@@ -580,10 +562,8 @@ func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []str
|
|||||||
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssertPutResponseBody requests a URI with PUT and asserts the response code and body.
|
// AssertPutResponseBody PUT to a URI and assert the response code and body
|
||||||
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
tc.t.Helper()
|
|
||||||
|
|
||||||
req, err := http.NewRequest("PUT", requestURI, requestBody)
|
req, err := http.NewRequest("PUT", requestURI, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("failed to create request %s", err)
|
tc.t.Errorf("failed to create request %s", err)
|
||||||
@@ -595,10 +575,8 @@ func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []stri
|
|||||||
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssertPatchResponseBody requests a URI with PATCH and asserts the response code and body.
|
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
|
||||||
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
tc.t.Helper()
|
|
||||||
|
|
||||||
req, err := http.NewRequest("PATCH", requestURI, requestBody)
|
req, err := http.NewRequest("PATCH", requestURI, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("failed to create request %s", err)
|
tc.t.Errorf("failed to create request %s", err)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package caddytest
|
package caddytest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -127,118 +126,3 @@ func TestLoadUnorderedJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
tester.AssertResponseCode(req, 200)
|
tester.AssertResponseCode(req, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckID(t *testing.T) {
|
|
||||||
tester := NewTester(t)
|
|
||||||
tester.InitServer(`{
|
|
||||||
"admin": {
|
|
||||||
"listen": "localhost:2999"
|
|
||||||
},
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"http_port": 9080,
|
|
||||||
"servers": {
|
|
||||||
"s_server": {
|
|
||||||
"@id": "s_server",
|
|
||||||
"listen": [
|
|
||||||
":9080"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "static_response",
|
|
||||||
"body": "Hello"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, "json")
|
|
||||||
headers := []string{"Content-Type:application/json"}
|
|
||||||
sServer1 := []byte(`{"@id":"s_server","listen":[":9080"],"routes":[{"@id":"route1","handle":[{"handler":"static_response","body":"Hello 2"}]}]}`)
|
|
||||||
|
|
||||||
// PUT to an existing ID should fail with a 409 conflict
|
|
||||||
tester.AssertPutResponseBody(
|
|
||||||
"http://localhost:2999/id/s_server",
|
|
||||||
headers,
|
|
||||||
bytes.NewBuffer(sServer1),
|
|
||||||
409,
|
|
||||||
`{"error":"[/config/apps/http/servers/s_server] key already exists: s_server"}`+"\n")
|
|
||||||
|
|
||||||
// POST replaces the object fully
|
|
||||||
tester.AssertPostResponseBody(
|
|
||||||
"http://localhost:2999/id/s_server",
|
|
||||||
headers,
|
|
||||||
bytes.NewBuffer(sServer1),
|
|
||||||
200,
|
|
||||||
"")
|
|
||||||
|
|
||||||
// Verify the server is running the new route
|
|
||||||
tester.AssertGetResponse(
|
|
||||||
"http://localhost:9080/",
|
|
||||||
200,
|
|
||||||
"Hello 2")
|
|
||||||
|
|
||||||
// Update the existing route to ensure IDs are handled correctly when replaced
|
|
||||||
tester.AssertPostResponseBody(
|
|
||||||
"http://localhost:2999/id/s_server",
|
|
||||||
headers,
|
|
||||||
bytes.NewBuffer([]byte(`{"@id":"s_server","listen":[":9080"],"routes":[{"@id":"route1","handle":[{"handler":"static_response","body":"Hello2"}],"match":[{"path":["/route_1/*"]}]}]}`)),
|
|
||||||
200,
|
|
||||||
"")
|
|
||||||
|
|
||||||
sServer2 := []byte(`{"@id":"s_server","listen":[":9080"],"routes":[{"@id":"route1","handle":[{"handler":"static_response","body":"Hello2"}],"match":[{"path":["/route_1/*"]}]}]}`)
|
|
||||||
|
|
||||||
// Identical patch should succeed and return 200 (config is unchanged branch)
|
|
||||||
tester.AssertPatchResponseBody(
|
|
||||||
"http://localhost:2999/id/s_server",
|
|
||||||
headers,
|
|
||||||
bytes.NewBuffer(sServer2),
|
|
||||||
200,
|
|
||||||
"")
|
|
||||||
|
|
||||||
route2 := []byte(`{"@id":"route2","handle": [{"handler": "static_response","body": "route2"}],"match":[{"path":["/route_2/*"]}]}`)
|
|
||||||
|
|
||||||
// Put a new route2 object before the route1 object due to the path of /id/route1
|
|
||||||
// Being translated to: /config/apps/http/servers/s_server/routes/0
|
|
||||||
tester.AssertPutResponseBody(
|
|
||||||
"http://localhost:2999/id/route1",
|
|
||||||
headers,
|
|
||||||
bytes.NewBuffer(route2),
|
|
||||||
200,
|
|
||||||
"")
|
|
||||||
|
|
||||||
// Verify that the whole config looks correct, now containing both route1 and route2
|
|
||||||
tester.AssertGetResponse(
|
|
||||||
"http://localhost:2999/config/",
|
|
||||||
200,
|
|
||||||
`{"admin":{"listen":"localhost:2999"},"apps":{"http":{"http_port":9080,"servers":{"s_server":{"@id":"s_server","listen":[":9080"],"routes":[{"@id":"route2","handle":[{"body":"route2","handler":"static_response"}],"match":[{"path":["/route_2/*"]}]},{"@id":"route1","handle":[{"body":"Hello2","handler":"static_response"}],"match":[{"path":["/route_1/*"]}]}]}}}}}`+"\n")
|
|
||||||
|
|
||||||
// Try to add another copy of route2 using POST to test duplicate ID handling
|
|
||||||
// Since the first route2 ended up at array index 0, and we are appending to the array, the index for the new element would be 2
|
|
||||||
tester.AssertPostResponseBody(
|
|
||||||
"http://localhost:2999/id/route2",
|
|
||||||
headers,
|
|
||||||
bytes.NewBuffer(route2),
|
|
||||||
400,
|
|
||||||
`{"error":"indexing config: duplicate ID 'route2' found at /config/apps/http/servers/s_server/routes/0 and /config/apps/http/servers/s_server/routes/2"}`+"\n")
|
|
||||||
|
|
||||||
// Use PATCH to modify an existing object successfully
|
|
||||||
tester.AssertPatchResponseBody(
|
|
||||||
"http://localhost:2999/id/route1",
|
|
||||||
headers,
|
|
||||||
bytes.NewBuffer([]byte(`{"@id":"route1","handle":[{"handler":"static_response","body":"route1"}],"match":[{"path":["/route_1/*"]}]}`)),
|
|
||||||
200,
|
|
||||||
"")
|
|
||||||
|
|
||||||
// Verify the PATCH updated the server state
|
|
||||||
tester.AssertGetResponse(
|
|
||||||
"http://localhost:9080/route_1/",
|
|
||||||
200,
|
|
||||||
"route1")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func TestACMEServerWithDefaults(t *testing.T) {
|
|||||||
Client: &acme.Client{
|
Client: &acme.Client{
|
||||||
Directory: "https://acme.localhost:9443/acme/local/directory",
|
Directory: "https://acme.localhost:9443/acme/local/directory",
|
||||||
HTTPClient: tester.Client,
|
HTTPClient: tester.Client,
|
||||||
Logger: slog.New(zapslog.NewHandler(logger.Core(), zapslog.WithName("acmez"))),
|
Logger: slog.New(zapslog.NewHandler(logger.Core())),
|
||||||
},
|
},
|
||||||
ChallengeSolvers: map[string]acmez.Solver{
|
ChallengeSolvers: map[string]acmez.Solver{
|
||||||
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
||||||
@@ -120,7 +120,7 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) {
|
|||||||
Client: &acme.Client{
|
Client: &acme.Client{
|
||||||
Directory: "https://acme.localhost:9443/acme/local/directory",
|
Directory: "https://acme.localhost:9443/acme/local/directory",
|
||||||
HTTPClient: tester.Client,
|
HTTPClient: tester.Client,
|
||||||
Logger: slog.New(zapslog.NewHandler(logger.Core(), zapslog.WithName("acmez"))),
|
Logger: slog.New(zapslog.NewHandler(logger.Core())),
|
||||||
},
|
},
|
||||||
ChallengeSolvers: map[string]acmez.Solver{
|
ChallengeSolvers: map[string]acmez.Solver{
|
||||||
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ func TestACMEServerAllowPolicy(t *testing.T) {
|
|||||||
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"not-matching.localhost"})
|
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"not-matching.localhost"})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("obtaining certificate for 'not-matching.localhost' domain")
|
t.Errorf("obtaining certificate for 'not-matching.localhost' domain")
|
||||||
} else if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
|
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
|
||||||
t.Logf("unexpected error: %v", err)
|
t.Logf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,7 +200,7 @@ func TestACMEServerDenyPolicy(t *testing.T) {
|
|||||||
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"})
|
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("obtaining certificate for 'deny.localhost' domain")
|
t.Errorf("obtaining certificate for 'deny.localhost' domain")
|
||||||
} else if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
|
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
|
||||||
t.Logf("unexpected error: %v", err)
|
t.Logf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,28 +55,6 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T
|
|||||||
tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect)
|
tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAutoHTTPtoHTTPSRedirectsPreferHTTPSPortOverAlternatePort(t *testing.T) {
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.InitServer(`
|
|
||||||
{
|
|
||||||
skip_install_trust
|
|
||||||
admin localhost:2999
|
|
||||||
http_port 9080
|
|
||||||
https_port 9443
|
|
||||||
local_certs
|
|
||||||
}
|
|
||||||
localhost {
|
|
||||||
respond "Canonical"
|
|
||||||
}
|
|
||||||
|
|
||||||
localhost:10443 {
|
|
||||||
respond "Alternate"
|
|
||||||
}
|
|
||||||
`, "caddyfile")
|
|
||||||
|
|
||||||
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
|
func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
|
||||||
tester := caddytest.NewTester(t)
|
tester := caddytest.NewTester(t)
|
||||||
tester.InitServer(`
|
tester.InitServer(`
|
||||||
@@ -165,26 +143,3 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSit
|
|||||||
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
|
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
|
||||||
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Foo")
|
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Foo")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAutoHTTPSRedirectSortingExactMatchOverWildcard(t *testing.T) {
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.InitServer(`
|
|
||||||
{
|
|
||||||
skip_install_trust
|
|
||||||
admin localhost:2999
|
|
||||||
http_port 9080
|
|
||||||
https_port 9443
|
|
||||||
local_certs
|
|
||||||
}
|
|
||||||
*.localhost:10443 {
|
|
||||||
respond "Wildcard"
|
|
||||||
}
|
|
||||||
dev.localhost {
|
|
||||||
respond "Exact"
|
|
||||||
}
|
|
||||||
`, "caddyfile")
|
|
||||||
|
|
||||||
tester.AssertRedirect("http://dev.localhost:9080/", "https://dev.localhost/", http.StatusPermanentRedirect)
|
|
||||||
|
|
||||||
tester.AssertRedirect("http://foo.localhost:9080/", "https://foo.localhost:10443/", http.StatusPermanentRedirect)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ encode gzip zstd {
|
|||||||
|
|
||||||
# Long way with a block for each encoding
|
# Long way with a block for each encoding
|
||||||
encode {
|
encode {
|
||||||
zstd {
|
zstd
|
||||||
disable_checksum
|
|
||||||
}
|
|
||||||
gzip 5
|
gzip 5
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,9 +71,7 @@ encode
|
|||||||
"gzip": {
|
"gzip": {
|
||||||
"level": 5
|
"level": 5
|
||||||
},
|
},
|
||||||
"zstd": {
|
"zstd": {}
|
||||||
"checksum": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"handler": "encode",
|
"handler": "encode",
|
||||||
"prefer": [
|
"prefer": [
|
||||||
|
|||||||
@@ -46,18 +46,6 @@ app.example.com {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"Remote-Email"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -85,18 +73,6 @@ app.example.com {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"Remote-Groups"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -124,18 +100,6 @@ app.example.com {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"Remote-Name"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -163,18 +127,6 @@ app.example.com {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"Remote-User"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -248,4 +200,4 @@ app.example.com {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
:8080
|
|
||||||
|
|
||||||
forward_auth 127.0.0.1:9091 {
|
|
||||||
uri /
|
|
||||||
copy_headers X-User-Id X-User-Role
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":8080"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handle_response": [
|
|
||||||
{
|
|
||||||
"match": {
|
|
||||||
"status_code": [
|
|
||||||
2
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "vars"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"X-User-Id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"set": {
|
|
||||||
"X-User-Id": [
|
|
||||||
"{http.reverse_proxy.header.X-User-Id}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"not": [
|
|
||||||
{
|
|
||||||
"vars": {
|
|
||||||
"{http.reverse_proxy.header.X-User-Id}": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"X-User-Role"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"set": {
|
|
||||||
"X-User-Role": [
|
|
||||||
"{http.reverse_proxy.header.X-User-Role}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"not": [
|
|
||||||
{
|
|
||||||
"vars": {
|
|
||||||
"{http.reverse_proxy.header.X-User-Role}": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"headers": {
|
|
||||||
"request": {
|
|
||||||
"set": {
|
|
||||||
"X-Forwarded-Method": [
|
|
||||||
"{http.request.method}"
|
|
||||||
],
|
|
||||||
"X-Forwarded-Uri": [
|
|
||||||
"{http.request.uri}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rewrite": {
|
|
||||||
"method": "GET",
|
|
||||||
"uri": "/"
|
|
||||||
},
|
|
||||||
"upstreams": [
|
|
||||||
{
|
|
||||||
"dial": "127.0.0.1:9091"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,18 +35,6 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"1"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -74,18 +62,6 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"B"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -113,18 +89,6 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"3"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -152,18 +116,6 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"D"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -191,18 +143,6 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"5"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -263,4 +203,4 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
log {
|
log {
|
||||||
sampling {
|
sampling {
|
||||||
interval 5m
|
interval 300
|
||||||
first 50
|
first 50
|
||||||
thereafter 40
|
thereafter 40
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"logs": {
|
"logs": {
|
||||||
"default": {
|
"default": {
|
||||||
"sampling": {
|
"sampling": {
|
||||||
"interval": 300000000000,
|
"interval": 300,
|
||||||
"first": 50,
|
"first": 50,
|
||||||
"thereafter": 40
|
"thereafter": 40
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
{
|
|
||||||
email test@example.com
|
|
||||||
dns mock
|
|
||||||
tls_resolvers 1.1.1.1 8.8.8.8
|
|
||||||
acme_dns
|
|
||||||
}
|
|
||||||
|
|
||||||
example.com {
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tls": {
|
|
||||||
"automation": {
|
|
||||||
"policies": [
|
|
||||||
{
|
|
||||||
"issuers": [
|
|
||||||
{
|
|
||||||
"challenges": {
|
|
||||||
"dns": {
|
|
||||||
"resolvers": [
|
|
||||||
"1.1.1.1",
|
|
||||||
"8.8.8.8"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"email": "test@example.com",
|
|
||||||
"module": "acme"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ca": "https://acme.zerossl.com/v2/DV90",
|
|
||||||
"challenges": {
|
|
||||||
"dns": {
|
|
||||||
"resolvers": [
|
|
||||||
"1.1.1.1",
|
|
||||||
"8.8.8.8"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"email": "test@example.com",
|
|
||||||
"module": "acme"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dns": {
|
|
||||||
"name": "mock"
|
|
||||||
},
|
|
||||||
"resolvers": [
|
|
||||||
"1.1.1.1",
|
|
||||||
"8.8.8.8"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-38
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
tls_resolvers 1.1.1.1 8.8.8.8
|
|
||||||
}
|
|
||||||
|
|
||||||
example.com {
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tls": {
|
|
||||||
"resolvers": [
|
|
||||||
"1.1.1.1",
|
|
||||||
"8.8.8.8"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-72
@@ -1,72 +0,0 @@
|
|||||||
{
|
|
||||||
email test@example.com
|
|
||||||
dns mock
|
|
||||||
tls_resolvers 1.1.1.1 8.8.8.8
|
|
||||||
}
|
|
||||||
|
|
||||||
example.com {
|
|
||||||
tls {
|
|
||||||
dns mock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tls": {
|
|
||||||
"automation": {
|
|
||||||
"policies": [
|
|
||||||
{
|
|
||||||
"subjects": [
|
|
||||||
"example.com"
|
|
||||||
],
|
|
||||||
"issuers": [
|
|
||||||
{
|
|
||||||
"challenges": {
|
|
||||||
"dns": {
|
|
||||||
"provider": {
|
|
||||||
"name": "mock"
|
|
||||||
},
|
|
||||||
"resolvers": [
|
|
||||||
"1.1.1.1",
|
|
||||||
"8.8.8.8"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"email": "test@example.com",
|
|
||||||
"module": "acme"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dns": {
|
|
||||||
"name": "mock"
|
|
||||||
},
|
|
||||||
"resolvers": [
|
|
||||||
"1.1.1.1",
|
|
||||||
"8.8.8.8"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-98
@@ -1,98 +0,0 @@
|
|||||||
{
|
|
||||||
email test@example.com
|
|
||||||
dns mock
|
|
||||||
tls_resolvers 1.1.1.1 8.8.8.8
|
|
||||||
acme_dns
|
|
||||||
}
|
|
||||||
|
|
||||||
example.com {
|
|
||||||
tls {
|
|
||||||
resolvers 9.9.9.9
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tls": {
|
|
||||||
"automation": {
|
|
||||||
"policies": [
|
|
||||||
{
|
|
||||||
"subjects": [
|
|
||||||
"example.com"
|
|
||||||
],
|
|
||||||
"issuers": [
|
|
||||||
{
|
|
||||||
"challenges": {
|
|
||||||
"dns": {
|
|
||||||
"resolvers": [
|
|
||||||
"9.9.9.9"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"email": "test@example.com",
|
|
||||||
"module": "acme"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"issuers": [
|
|
||||||
{
|
|
||||||
"challenges": {
|
|
||||||
"dns": {
|
|
||||||
"resolvers": [
|
|
||||||
"1.1.1.1",
|
|
||||||
"8.8.8.8"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"email": "test@example.com",
|
|
||||||
"module": "acme"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ca": "https://acme.zerossl.com/v2/DV90",
|
|
||||||
"challenges": {
|
|
||||||
"dns": {
|
|
||||||
"resolvers": [
|
|
||||||
"1.1.1.1",
|
|
||||||
"8.8.8.8"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"email": "test@example.com",
|
|
||||||
"module": "acme"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dns": {
|
|
||||||
"name": "mock"
|
|
||||||
},
|
|
||||||
"resolvers": [
|
|
||||||
"1.1.1.1",
|
|
||||||
"8.8.8.8"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
{
|
|
||||||
email test@example.com
|
|
||||||
dns mock
|
|
||||||
tls_resolvers 1.1.1.1 8.8.8.8
|
|
||||||
acme_dns
|
|
||||||
}
|
|
||||||
|
|
||||||
site1.example.com {
|
|
||||||
}
|
|
||||||
|
|
||||||
site2.example.com {
|
|
||||||
tls {
|
|
||||||
resolvers 9.9.9.9 8.8.4.4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"site1.example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"site2.example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tls": {
|
|
||||||
"automation": {
|
|
||||||
"policies": [
|
|
||||||
{
|
|
||||||
"subjects": [
|
|
||||||
"site2.example.com"
|
|
||||||
],
|
|
||||||
"issuers": [
|
|
||||||
{
|
|
||||||
"challenges": {
|
|
||||||
"dns": {
|
|
||||||
"resolvers": [
|
|
||||||
"9.9.9.9",
|
|
||||||
"8.8.4.4"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"email": "test@example.com",
|
|
||||||
"module": "acme"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"issuers": [
|
|
||||||
{
|
|
||||||
"challenges": {
|
|
||||||
"dns": {
|
|
||||||
"resolvers": [
|
|
||||||
"1.1.1.1",
|
|
||||||
"8.8.8.8"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"email": "test@example.com",
|
|
||||||
"module": "acme"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ca": "https://acme.zerossl.com/v2/DV90",
|
|
||||||
"challenges": {
|
|
||||||
"dns": {
|
|
||||||
"resolvers": [
|
|
||||||
"1.1.1.1",
|
|
||||||
"8.8.8.8"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"email": "test@example.com",
|
|
||||||
"module": "acme"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dns": {
|
|
||||||
"name": "mock"
|
|
||||||
},
|
|
||||||
"resolvers": [
|
|
||||||
"1.1.1.1",
|
|
||||||
"8.8.8.8"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,6 @@
|
|||||||
keepalive_interval 20s
|
keepalive_interval 20s
|
||||||
keepalive_idle 20s
|
keepalive_idle 20s
|
||||||
keepalive_count 10
|
keepalive_count 10
|
||||||
0rtt off
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,8 +90,7 @@ foo.com {
|
|||||||
"h2",
|
"h2",
|
||||||
"h2c",
|
"h2c",
|
||||||
"h3"
|
"h3"
|
||||||
],
|
]
|
||||||
"allow_0rtt": false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ handle {
|
|||||||
END!
|
END!
|
||||||
}
|
}
|
||||||
----------
|
----------
|
||||||
heredoc marker on line #4 must contain only alphanumeric characters, dashes and underscores; got 'END!'
|
heredoc marker on line #4 must contain only alpha-numeric characters, dashes and underscores; got 'END!'
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
admin off
|
|
||||||
auto_https off
|
|
||||||
}
|
|
||||||
|
|
||||||
import testdata/issue_7557_invalid_subdirective_snippet.conf
|
|
||||||
|
|
||||||
:8080 {
|
|
||||||
import test {
|
|
||||||
this_is_nonsense
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
----------
|
|
||||||
parsing caddyfile tokens for 'reverse_proxy': unrecognized subdirective this_is_nonsense
|
|
||||||
-52
@@ -1,52 +0,0 @@
|
|||||||
import testdata/issue_7518_unused_block_panic_snippets.conf
|
|
||||||
|
|
||||||
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,47 +0,0 @@
|
|||||||
{
|
|
||||||
log {
|
|
||||||
format journald {
|
|
||||||
wrap console
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:80 {
|
|
||||||
respond "Hello, World!"
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"logging": {
|
|
||||||
"logs": {
|
|
||||||
"default": {
|
|
||||||
"encoder": {
|
|
||||||
"format": "journald",
|
|
||||||
"wrap": {
|
|
||||||
"format": "console"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":80"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "Hello, World!",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +1,27 @@
|
|||||||
:80
|
:80
|
||||||
|
|
||||||
log one {
|
log {
|
||||||
output file /var/log/access.log {
|
output file /var/log/access.log {
|
||||||
mode 0644
|
|
||||||
dir_mode 0755
|
|
||||||
roll_size 1gb
|
roll_size 1gb
|
||||||
roll_uncompressed
|
roll_uncompressed
|
||||||
roll_compression none
|
|
||||||
roll_local_time
|
roll_local_time
|
||||||
roll_keep 5
|
roll_keep 5
|
||||||
roll_keep_for 90d
|
roll_keep_for 90d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log two {
|
|
||||||
output file /var/log/access-2.log {
|
|
||||||
mode 0777
|
|
||||||
dir_mode from_file
|
|
||||||
roll_size 1gib
|
|
||||||
roll_compression zstd
|
|
||||||
roll_interval 12h
|
|
||||||
roll_at 00:00 06:00 12:00,18:00
|
|
||||||
roll_minutes 10 40 45,46
|
|
||||||
roll_keep 10
|
|
||||||
roll_keep_for 90d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
"logging": {
|
"logging": {
|
||||||
"logs": {
|
"logs": {
|
||||||
"default": {
|
"default": {
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"http.log.access.one",
|
"http.log.access.log0"
|
||||||
"http.log.access.two"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"one": {
|
"log0": {
|
||||||
"writer": {
|
"writer": {
|
||||||
"dir_mode": "0755",
|
|
||||||
"filename": "/var/log/access.log",
|
"filename": "/var/log/access.log",
|
||||||
"mode": "0644",
|
|
||||||
"output": "file",
|
"output": "file",
|
||||||
"roll_compression": "none",
|
|
||||||
"roll_gzip": false,
|
"roll_gzip": false,
|
||||||
"roll_keep": 5,
|
"roll_keep": 5,
|
||||||
"roll_keep_days": 90,
|
"roll_keep_days": 90,
|
||||||
@@ -49,35 +29,7 @@ log two {
|
|||||||
"roll_size_mb": 954
|
"roll_size_mb": 954
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"http.log.access.one"
|
"http.log.access.log0"
|
||||||
]
|
|
||||||
},
|
|
||||||
"two": {
|
|
||||||
"writer": {
|
|
||||||
"dir_mode": "from_file",
|
|
||||||
"filename": "/var/log/access-2.log",
|
|
||||||
"mode": "0777",
|
|
||||||
"output": "file",
|
|
||||||
"roll_at": [
|
|
||||||
"00:00",
|
|
||||||
"06:00",
|
|
||||||
"12:00",
|
|
||||||
"18:00"
|
|
||||||
],
|
|
||||||
"roll_compression": "zstd",
|
|
||||||
"roll_interval": 43200000000000,
|
|
||||||
"roll_keep": 10,
|
|
||||||
"roll_keep_days": 90,
|
|
||||||
"roll_minutes": [
|
|
||||||
10,
|
|
||||||
40,
|
|
||||||
45,
|
|
||||||
46
|
|
||||||
],
|
|
||||||
"roll_size_mb": 1024
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"http.log.access.two"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +42,7 @@ log two {
|
|||||||
":80"
|
":80"
|
||||||
],
|
],
|
||||||
"logs": {
|
"logs": {
|
||||||
"default_logger_name": "two"
|
"default_logger_name": "log0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
:80 {
|
:80 {
|
||||||
log {
|
log {
|
||||||
sampling {
|
sampling {
|
||||||
interval 5m
|
interval 300
|
||||||
first 50
|
first 50
|
||||||
thereafter 40
|
thereafter 40
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"log0": {
|
"log0": {
|
||||||
"sampling": {
|
"sampling": {
|
||||||
"interval": 300000000000,
|
"interval": 300,
|
||||||
"first": 50,
|
"first": 50,
|
||||||
"thereafter": 40
|
"thereafter": 40
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
metrics {
|
|
||||||
otlp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:80 {
|
|
||||||
respond "Hello"
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":80"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "Hello",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"metrics": {
|
|
||||||
"otlp": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
renewal_window_ratio 0.1666
|
|
||||||
}
|
|
||||||
|
|
||||||
example.com {
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tls": {
|
|
||||||
"automation": {
|
|
||||||
"policies": [
|
|
||||||
{
|
|
||||||
"renewal_window_ratio": 0.1666
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-63
@@ -1,63 +0,0 @@
|
|||||||
{
|
|
||||||
renewal_window_ratio 0.1666
|
|
||||||
}
|
|
||||||
|
|
||||||
a.example.com {
|
|
||||||
tls {
|
|
||||||
renewal_window_ratio 0.25
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.example.com {
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"a.example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"b.example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tls": {
|
|
||||||
"automation": {
|
|
||||||
"policies": [
|
|
||||||
{
|
|
||||||
"subjects": [
|
|
||||||
"a.example.com"
|
|
||||||
],
|
|
||||||
"renewal_window_ratio": 0.25
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"renewal_window_ratio": 0.1666
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
:8884
|
|
||||||
|
|
||||||
reverse_proxy 127.0.0.1:65535 {
|
|
||||||
lb_retries 3
|
|
||||||
lb_retry_match expression `{rp.status_code} in [502, 503]`
|
|
||||||
lb_retry_match expression `{rp.is_transport_error} || {rp.status_code} == 502`
|
|
||||||
lb_retry_match expression `method('POST') && {rp.status_code} == 503`
|
|
||||||
lb_retry_match `{rp.status_code} == 504`
|
|
||||||
lb_retry_match `{rp.is_transport_error} && method('PUT')`
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":8884"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"load_balancing": {
|
|
||||||
"retries": 3,
|
|
||||||
"retry_match": [
|
|
||||||
{
|
|
||||||
"expression": "{http.reverse_proxy.status_code} in [502, 503]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "{http.reverse_proxy.is_transport_error} || {http.reverse_proxy.status_code} == 502"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "method('POST') \u0026\u0026 {http.reverse_proxy.status_code} == 503"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "{http.reverse_proxy.status_code} == 504"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "{http.reverse_proxy.is_transport_error} \u0026\u0026 method('PUT')"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"upstreams": [
|
|
||||||
{
|
|
||||||
"dial": "127.0.0.1:65535"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-147
@@ -1,147 +0,0 @@
|
|||||||
:8884
|
|
||||||
|
|
||||||
reverse_proxy 127.0.0.1:65535 {
|
|
||||||
lb_retries 5
|
|
||||||
|
|
||||||
# request matchers (backward-compatible, non-expression)
|
|
||||||
lb_retry_match {
|
|
||||||
method POST PUT
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
path /foo*
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
header X-Idempotency-Key *
|
|
||||||
}
|
|
||||||
|
|
||||||
# response status code via expression
|
|
||||||
lb_retry_match {
|
|
||||||
expression `{rp.status_code} in [502, 503, 504]`
|
|
||||||
}
|
|
||||||
|
|
||||||
# response header via expression
|
|
||||||
lb_retry_match {
|
|
||||||
expression `{rp.header.X-Retry} == "true"`
|
|
||||||
}
|
|
||||||
|
|
||||||
# CEL request functions combined with response placeholders
|
|
||||||
lb_retry_match {
|
|
||||||
expression `method('POST') && {rp.status_code} >= 500`
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
expression `path('/api*') && {rp.status_code} in [502, 503]`
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
expression `host('example.com') && {rp.status_code} == 503`
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
expression `query({'retry': 'true'}) && {rp.status_code} >= 500`
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
expression `header({'X-Idempotency-Key': '*'}) && {rp.status_code} in [502, 503]`
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
expression `protocol('https') && {rp.status_code} == 502`
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
expression `path_regexp('^/api/v[0-9]+/') && {rp.status_code} >= 500`
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
expression `header_regexp('Content-Type', '^application/json') && {rp.status_code} == 502`
|
|
||||||
}
|
|
||||||
|
|
||||||
# transport error handling via placeholder
|
|
||||||
lb_retry_match {
|
|
||||||
expression `{rp.is_transport_error} || {rp.status_code} in [502, 503]`
|
|
||||||
}
|
|
||||||
lb_retry_match {
|
|
||||||
expression `{rp.is_transport_error} && method('POST')`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":8884"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"load_balancing": {
|
|
||||||
"retries": 5,
|
|
||||||
"retry_match": [
|
|
||||||
{
|
|
||||||
"method": [
|
|
||||||
"POST",
|
|
||||||
"PUT"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": [
|
|
||||||
"/foo*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header": {
|
|
||||||
"X-Idempotency-Key": [
|
|
||||||
"*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "{http.reverse_proxy.status_code} in [502, 503, 504]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "{http.reverse_proxy.header.X-Retry} == \"true\""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "method('POST') \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "path('/api*') \u0026\u0026 {http.reverse_proxy.status_code} in [502, 503]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "host('example.com') \u0026\u0026 {http.reverse_proxy.status_code} == 503"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "query({'retry': 'true'}) \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "header({'X-Idempotency-Key': '*'}) \u0026\u0026 {http.reverse_proxy.status_code} in [502, 503]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "protocol('https') \u0026\u0026 {http.reverse_proxy.status_code} == 502"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "path_regexp('^/api/v[0-9]+/') \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "header_regexp('Content-Type', '^application/json') \u0026\u0026 {http.reverse_proxy.status_code} == 502"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "{http.reverse_proxy.is_transport_error} || {http.reverse_proxy.status_code} in [502, 503]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expression": "{http.reverse_proxy.is_transport_error} \u0026\u0026 method('POST')"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"upstreams": [
|
|
||||||
{
|
|
||||||
"dial": "127.0.0.1:65535"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
https://example.com {
|
|
||||||
reverse_proxy https://localhost:54321 {
|
|
||||||
stream_buffer_size 8KB
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"stream_buffer_size": 8000,
|
|
||||||
"transport": {
|
|
||||||
"protocol": "http",
|
|
||||||
"tls": {}
|
|
||||||
},
|
|
||||||
"upstreams": [
|
|
||||||
{
|
|
||||||
"dial": "localhost:54321"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
{
|
|
||||||
dns mock foo
|
|
||||||
acme_dns mock bar
|
|
||||||
}
|
|
||||||
|
|
||||||
localhost {
|
|
||||||
tls {
|
|
||||||
resolvers 8.8.8.8 8.8.4.4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tls": {
|
|
||||||
"automation": {
|
|
||||||
"policies": [
|
|
||||||
{
|
|
||||||
"subjects": [
|
|
||||||
"localhost"
|
|
||||||
],
|
|
||||||
"issuers": [
|
|
||||||
{
|
|
||||||
"challenges": {
|
|
||||||
"dns": {
|
|
||||||
"provider": {
|
|
||||||
"argument": "bar",
|
|
||||||
"name": "mock"
|
|
||||||
},
|
|
||||||
"resolvers": [
|
|
||||||
"8.8.8.8",
|
|
||||||
"8.8.4.4"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"module": "acme"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"issuers": [
|
|
||||||
{
|
|
||||||
"challenges": {
|
|
||||||
"dns": {
|
|
||||||
"provider": {
|
|
||||||
"argument": "bar",
|
|
||||||
"name": "mock"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"module": "acme"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dns": {
|
|
||||||
"argument": "foo",
|
|
||||||
"name": "mock"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -54,6 +54,11 @@ b.com {
|
|||||||
"via": "http"
|
"via": "http"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"b.com"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
# example from https://github.com/caddyserver/caddy/issues/7559
|
|
||||||
*.test.local {
|
|
||||||
tls {
|
|
||||||
get_certificate http http://cert-server:9000/certs
|
|
||||||
}
|
|
||||||
respond "wildcard"
|
|
||||||
}
|
|
||||||
|
|
||||||
# certificate for this subdomain is covered by wildcard above
|
|
||||||
subdomain.test.local {
|
|
||||||
respond "subdomain"
|
|
||||||
}
|
|
||||||
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"subdomain.test.local"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "subdomain",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"*.test.local"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "wildcard",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tls": {
|
|
||||||
"automation": {
|
|
||||||
"policies": [
|
|
||||||
{
|
|
||||||
"subjects": [
|
|
||||||
"*.test.local"
|
|
||||||
],
|
|
||||||
"get_certificate": [
|
|
||||||
{
|
|
||||||
"url": "http://cert-server:9000/certs",
|
|
||||||
"via": "http"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-87
@@ -1,87 +0,0 @@
|
|||||||
localhost
|
|
||||||
|
|
||||||
respond "hello from localhost"
|
|
||||||
tls {
|
|
||||||
client_auth {
|
|
||||||
mode request
|
|
||||||
trust_pool combined {
|
|
||||||
source inline {
|
|
||||||
trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
|
|
||||||
}
|
|
||||||
source file {
|
|
||||||
pem_file ../caddy.ca.cer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "hello from localhost",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tls_connection_policies": [
|
|
||||||
{
|
|
||||||
"match": {
|
|
||||||
"sni": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"client_authentication": {
|
|
||||||
"ca": {
|
|
||||||
"provider": "combined",
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"provider": "inline",
|
|
||||||
"trusted_ca_certs": [
|
|
||||||
"MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ=="
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pem_files": [
|
|
||||||
"../caddy.ca.cer"
|
|
||||||
],
|
|
||||||
"provider": "file"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"mode": "request"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-87
@@ -1,87 +0,0 @@
|
|||||||
localhost
|
|
||||||
|
|
||||||
respond "hello from localhost"
|
|
||||||
tls {
|
|
||||||
client_auth {
|
|
||||||
mode require_and_verify
|
|
||||||
trust_pool combined {
|
|
||||||
source inline {
|
|
||||||
trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
|
|
||||||
}
|
|
||||||
source pki_root {
|
|
||||||
authority local
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "hello from localhost",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tls_connection_policies": [
|
|
||||||
{
|
|
||||||
"match": {
|
|
||||||
"sni": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"client_authentication": {
|
|
||||||
"ca": {
|
|
||||||
"provider": "combined",
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"provider": "inline",
|
|
||||||
"trusted_ca_certs": [
|
|
||||||
"MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ=="
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"authority": [
|
|
||||||
"local"
|
|
||||||
],
|
|
||||||
"provider": "pki_root"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"mode": "require_and_verify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
localhost
|
|
||||||
|
|
||||||
respond "hello from localhost"
|
|
||||||
tls {
|
|
||||||
client_auth {
|
|
||||||
mode request
|
|
||||||
trust_pool system
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "hello from localhost",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tls_connection_policies": [
|
|
||||||
{
|
|
||||||
"match": {
|
|
||||||
"sni": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"client_authentication": {
|
|
||||||
"ca": {
|
|
||||||
"provider": "system"
|
|
||||||
},
|
|
||||||
"mode": "request"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,206 +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 integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddytest"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestForwardAuthCopyHeadersStripsClientHeaders is a regression test for the
|
|
||||||
// header injection vulnerability in forward_auth copy_headers.
|
|
||||||
//
|
|
||||||
// When the auth service returns 200 OK without one of the copy_headers headers,
|
|
||||||
// the MatchNot guard skips the Set operation. Before this fix, the original
|
|
||||||
// client-supplied header survived unchanged into the backend request, allowing
|
|
||||||
// privilege escalation with only a valid (non-privileged) bearer token. After
|
|
||||||
// the fix, an unconditional delete route runs first, so the backend always
|
|
||||||
// sees an absent header rather than the attacker-supplied value.
|
|
||||||
func TestForwardAuthCopyHeadersStripsClientHeaders(t *testing.T) {
|
|
||||||
// Mock auth service: accepts any Bearer token, returns 200 OK with NO
|
|
||||||
// identity headers. This is the stateless JWT validator pattern that
|
|
||||||
// triggers the vulnerability.
|
|
||||||
authSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
}))
|
|
||||||
defer authSrv.Close()
|
|
||||||
|
|
||||||
// Mock backend: records the identity headers it receives. A real application
|
|
||||||
// would use X-User-Id / X-User-Role to make authorization decisions.
|
|
||||||
type received struct{ userID, userRole string }
|
|
||||||
var (
|
|
||||||
mu sync.Mutex
|
|
||||||
last received
|
|
||||||
)
|
|
||||||
backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
mu.Lock()
|
|
||||||
last = received{
|
|
||||||
userID: r.Header.Get("X-User-Id"),
|
|
||||||
userRole: r.Header.Get("X-User-Role"),
|
|
||||||
}
|
|
||||||
mu.Unlock()
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
fmt.Fprint(w, "ok")
|
|
||||||
}))
|
|
||||||
defer backendSrv.Close()
|
|
||||||
|
|
||||||
authAddr := strings.TrimPrefix(authSrv.URL, "http://")
|
|
||||||
backendAddr := strings.TrimPrefix(backendSrv.URL, "http://")
|
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.InitServer(fmt.Sprintf(`
|
|
||||||
{
|
|
||||||
skip_install_trust
|
|
||||||
admin localhost:2999
|
|
||||||
http_port 9080
|
|
||||||
https_port 9443
|
|
||||||
grace_period 1ns
|
|
||||||
}
|
|
||||||
http://localhost:9080 {
|
|
||||||
forward_auth %s {
|
|
||||||
uri /
|
|
||||||
copy_headers X-User-Id X-User-Role
|
|
||||||
}
|
|
||||||
reverse_proxy %s
|
|
||||||
}
|
|
||||||
`, authAddr, backendAddr), "caddyfile")
|
|
||||||
|
|
||||||
// Case 1: no token. Auth must still reject the request even when the client
|
|
||||||
// includes identity headers. This confirms the auth check is not bypassed.
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
|
|
||||||
req.Header.Set("X-User-Id", "injected")
|
|
||||||
req.Header.Set("X-User-Role", "injected")
|
|
||||||
resp := tester.AssertResponseCode(req, http.StatusUnauthorized)
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
// Case 2: valid token, no injected headers. The backend should see absent
|
|
||||||
// identity headers (the auth service never returns them).
|
|
||||||
req, _ = http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer token123")
|
|
||||||
tester.AssertResponse(req, http.StatusOK, "ok")
|
|
||||||
mu.Lock()
|
|
||||||
gotID, gotRole := last.userID, last.userRole
|
|
||||||
mu.Unlock()
|
|
||||||
if gotID != "" {
|
|
||||||
t.Errorf("baseline: X-User-Id should be absent, got %q", gotID)
|
|
||||||
}
|
|
||||||
if gotRole != "" {
|
|
||||||
t.Errorf("baseline: X-User-Role should be absent, got %q", gotRole)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 3 (the security regression): valid token plus forged identity headers.
|
|
||||||
// The fix must strip those values so the backend never sees them.
|
|
||||||
req, _ = http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer token123")
|
|
||||||
req.Header.Set("X-User-Id", "admin") // forged
|
|
||||||
req.Header.Set("X-User-Role", "superadmin") // forged
|
|
||||||
tester.AssertResponse(req, http.StatusOK, "ok")
|
|
||||||
mu.Lock()
|
|
||||||
gotID, gotRole = last.userID, last.userRole
|
|
||||||
mu.Unlock()
|
|
||||||
if gotID != "" {
|
|
||||||
t.Errorf("injection: X-User-Id must be stripped, got %q", gotID)
|
|
||||||
}
|
|
||||||
if gotRole != "" {
|
|
||||||
t.Errorf("injection: X-User-Role must be stripped, got %q", gotRole)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestForwardAuthCopyHeadersAuthResponseWins verifies that when the auth
|
|
||||||
// service does include a copy_headers header in its response, that value
|
|
||||||
// is forwarded to the backend and takes precedence over any client-supplied
|
|
||||||
// value for the same header.
|
|
||||||
func TestForwardAuthCopyHeadersAuthResponseWins(t *testing.T) {
|
|
||||||
const wantUserID = "service-user-42"
|
|
||||||
const wantUserRole = "editor"
|
|
||||||
|
|
||||||
// Auth service: accepts bearer token and sets identity headers.
|
|
||||||
authSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
|
|
||||||
w.Header().Set("X-User-Id", wantUserID)
|
|
||||||
w.Header().Set("X-User-Role", wantUserRole)
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
}))
|
|
||||||
defer authSrv.Close()
|
|
||||||
|
|
||||||
type received struct{ userID, userRole string }
|
|
||||||
var (
|
|
||||||
mu sync.Mutex
|
|
||||||
last received
|
|
||||||
)
|
|
||||||
backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
mu.Lock()
|
|
||||||
last = received{
|
|
||||||
userID: r.Header.Get("X-User-Id"),
|
|
||||||
userRole: r.Header.Get("X-User-Role"),
|
|
||||||
}
|
|
||||||
mu.Unlock()
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
fmt.Fprint(w, "ok")
|
|
||||||
}))
|
|
||||||
defer backendSrv.Close()
|
|
||||||
|
|
||||||
authAddr := strings.TrimPrefix(authSrv.URL, "http://")
|
|
||||||
backendAddr := strings.TrimPrefix(backendSrv.URL, "http://")
|
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.InitServer(fmt.Sprintf(`
|
|
||||||
{
|
|
||||||
skip_install_trust
|
|
||||||
admin localhost:2999
|
|
||||||
http_port 9080
|
|
||||||
https_port 9443
|
|
||||||
grace_period 1ns
|
|
||||||
}
|
|
||||||
http://localhost:9080 {
|
|
||||||
forward_auth %s {
|
|
||||||
uri /
|
|
||||||
copy_headers X-User-Id X-User-Role
|
|
||||||
}
|
|
||||||
reverse_proxy %s
|
|
||||||
}
|
|
||||||
`, authAddr, backendAddr), "caddyfile")
|
|
||||||
|
|
||||||
// The client sends forged headers; the auth service overrides them with
|
|
||||||
// its own values. The backend must receive the auth service values.
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer token123")
|
|
||||||
req.Header.Set("X-User-Id", "forged-id") // must be overwritten
|
|
||||||
req.Header.Set("X-User-Role", "forged-role") // must be overwritten
|
|
||||||
tester.AssertResponse(req, http.StatusOK, "ok")
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
gotID, gotRole := last.userID, last.userRole
|
|
||||||
mu.Unlock()
|
|
||||||
if gotID != wantUserID {
|
|
||||||
t.Errorf("X-User-Id: want %q, got %q", wantUserID, gotID)
|
|
||||||
}
|
|
||||||
if gotRole != wantUserRole {
|
|
||||||
t.Errorf("X-User-Role: want %q, got %q", wantUserRole, gotRole)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ package integration
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand/v2"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -54,7 +54,7 @@ func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) {
|
|||||||
const uploadSize = (1024 * 1024) + 1 // 1 MB + 1 byte
|
const uploadSize = (1024 * 1024) + 1 // 1 MB + 1 byte
|
||||||
// 1 more than an MB
|
// 1 more than an MB
|
||||||
body := make([]byte, uploadSize)
|
body := make([]byte, uploadSize)
|
||||||
rand.NewChaCha8([32]byte{}).Read(body)
|
rand.New(rand.NewSource(0)).Read(body)
|
||||||
|
|
||||||
tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) {
|
tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func TestLeafCertLifetimeLessThanIntermediate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, "json", "should be less than intermediate certificate lifetime")
|
`, "json", "certificate lifetime (168h0m0s) should be less than intermediate certificate lifetime (168h0m0s)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIntermediateLifetimeLessThanRoot(t *testing.T) {
|
func TestIntermediateLifetimeLessThanRoot(t *testing.T) {
|
||||||
@@ -103,5 +103,5 @@ func TestIntermediateLifetimeLessThanRoot(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, "json", "intermediate certificate lifetime must be less than actual root certificate lifetime")
|
`, "json", "intermediate certificate lifetime must be less than root certificate lifetime (86400h0m0s)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,595 +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.
|
|
||||||
|
|
||||||
// Integration tests for Caddy's PROXY protocol support, covering two distinct
|
|
||||||
// roles that Caddy can play:
|
|
||||||
//
|
|
||||||
// 1. As a PROXY protocol *sender* (reverse proxy outbound transport):
|
|
||||||
// Caddy receives an inbound request from a test client and the
|
|
||||||
// reverse_proxy handler forwards it to an upstream with a PROXY protocol
|
|
||||||
// header (v1 or v2) prepended to the connection. A lightweight backend
|
|
||||||
// built with go-proxyproto validates that the header was received and
|
|
||||||
// carries the correct client address.
|
|
||||||
//
|
|
||||||
// Transport versions tested:
|
|
||||||
// - "1.1" -> plain HTTP/1.1 to the upstream
|
|
||||||
// - "h2c" -> HTTP/2 cleartext (h2c) to the upstream (regression for #7529)
|
|
||||||
// - "2" -> HTTP/2 over TLS (h2) to the upstream
|
|
||||||
//
|
|
||||||
// For each transport version both PROXY protocol v1 and v2 are exercised.
|
|
||||||
//
|
|
||||||
// HTTP/3 (h3) is not included because it uses QUIC/UDP and therefore
|
|
||||||
// bypasses the TCP-level dialContext that injects PROXY protocol headers;
|
|
||||||
// there is no meaningful h3 + proxy protocol sender combination to test.
|
|
||||||
//
|
|
||||||
// 2. As a PROXY protocol *receiver* (server-side listener wrapper):
|
|
||||||
// A raw TCP client dials Caddy directly, injects a PROXY v2 header
|
|
||||||
// spoofing a source address, and sends a normal HTTP/1.1 request. The
|
|
||||||
// Caddy server is configured with the proxy_protocol listener wrapper and
|
|
||||||
// is expected to surface the spoofed address via the
|
|
||||||
// {http.request.remote.host} placeholder.
|
|
||||||
|
|
||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
goproxy "github.com/pires/go-proxyproto"
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
"golang.org/x/net/http2/h2c"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddytest"
|
|
||||||
)
|
|
||||||
|
|
||||||
// proxyProtoBackend is a minimal HTTP server that sits behind a
|
|
||||||
// go-proxyproto listener and records the source address that was
|
|
||||||
// delivered in the PROXY header for each request.
|
|
||||||
type proxyProtoBackend struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
headerAddrs []string // host:port strings extracted from each PROXY header
|
|
||||||
|
|
||||||
ln net.Listener
|
|
||||||
srv *http.Server
|
|
||||||
}
|
|
||||||
|
|
||||||
// newProxyProtoBackend starts a TCP listener wrapped with go-proxyproto on a
|
|
||||||
// random local port and serves requests with a simple "OK" body. The PROXY
|
|
||||||
// header source addresses are accumulated in headerAddrs so tests can
|
|
||||||
// inspect them.
|
|
||||||
func newProxyProtoBackend(t *testing.T) *proxyProtoBackend {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
b := &proxyProtoBackend{}
|
|
||||||
|
|
||||||
rawLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("backend: listen: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap with go-proxyproto so the PROXY header is stripped and parsed
|
|
||||||
// before the HTTP server sees the connection. We use REQUIRE so that a
|
|
||||||
// missing header returns an error instead of silently passing through.
|
|
||||||
pLn := &goproxy.Listener{
|
|
||||||
Listener: rawLn,
|
|
||||||
Policy: func(_ net.Addr) (goproxy.Policy, error) {
|
|
||||||
return goproxy.REQUIRE, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
b.ln = pLn
|
|
||||||
|
|
||||||
// Wrap the handler with h2c support so the backend can speak HTTP/2
|
|
||||||
// cleartext (h2c) as well as plain HTTP/1.1. Without this, Caddy's
|
|
||||||
// reverse proxy would receive a 'frame too large' error when the
|
|
||||||
// upstream transport is configured to use h2c.
|
|
||||||
h2Server := &http2.Server{}
|
|
||||||
handlerFn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// go-proxyproto has already updated the net.Conn's remote
|
|
||||||
// address to the value from the PROXY header; the HTTP server
|
|
||||||
// surfaces it in r.RemoteAddr.
|
|
||||||
b.mu.Lock()
|
|
||||||
b.headerAddrs = append(b.headerAddrs, r.RemoteAddr)
|
|
||||||
b.mu.Unlock()
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
_, _ = fmt.Fprint(w, "OK")
|
|
||||||
})
|
|
||||||
|
|
||||||
b.srv = &http.Server{
|
|
||||||
Handler: h2c.NewHandler(handlerFn, h2Server),
|
|
||||||
}
|
|
||||||
|
|
||||||
go b.srv.Serve(pLn) //nolint:errcheck
|
|
||||||
t.Cleanup(func() {
|
|
||||||
_ = b.srv.Close()
|
|
||||||
_ = rawLn.Close()
|
|
||||||
})
|
|
||||||
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
// addr returns the listening address (host:port) of the backend.
|
|
||||||
func (b *proxyProtoBackend) addr() string {
|
|
||||||
return b.ln.Addr().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// recordedAddrs returns a snapshot of all PROXY-header source addresses seen
|
|
||||||
// so far.
|
|
||||||
func (b *proxyProtoBackend) recordedAddrs() []string {
|
|
||||||
b.mu.Lock()
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
cp := make([]string, len(b.headerAddrs))
|
|
||||||
copy(cp, b.headerAddrs)
|
|
||||||
return cp
|
|
||||||
}
|
|
||||||
|
|
||||||
// tlsProxyProtoBackend is a TLS-enabled backend that sits behind a
|
|
||||||
// go-proxyproto listener. The PROXY header is stripped before the TLS
|
|
||||||
// handshake so the layer order on a connection is:
|
|
||||||
//
|
|
||||||
// raw TCP → go-proxyproto (strips PROXY header) → TLS handshake → HTTP/2
|
|
||||||
type tlsProxyProtoBackend struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
headerAddrs []string
|
|
||||||
|
|
||||||
srv *httptest.Server
|
|
||||||
}
|
|
||||||
|
|
||||||
// newTLSProxyProtoBackend starts a TLS listener that first reads and strips
|
|
||||||
// PROXY protocol headers (go-proxyproto, REQUIRE policy) and then performs a
|
|
||||||
// TLS handshake. The backend speaks HTTP/2 over TLS (h2).
|
|
||||||
//
|
|
||||||
// The certificate is the standard self-signed certificate generated by
|
|
||||||
// httptest.Server; the Caddy transport must be configured with
|
|
||||||
// insecure_skip_verify: true to trust it.
|
|
||||||
func newTLSProxyProtoBackend(t *testing.T) *tlsProxyProtoBackend {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
b := &tlsProxyProtoBackend{}
|
|
||||||
|
|
||||||
handlerFn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
b.mu.Lock()
|
|
||||||
b.headerAddrs = append(b.headerAddrs, r.RemoteAddr)
|
|
||||||
b.mu.Unlock()
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
_, _ = fmt.Fprint(w, "OK")
|
|
||||||
})
|
|
||||||
|
|
||||||
rawLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("tlsBackend: listen: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap with go-proxyproto so the PROXY header is consumed before TLS.
|
|
||||||
pLn := &goproxy.Listener{
|
|
||||||
Listener: rawLn,
|
|
||||||
Policy: func(_ net.Addr) (goproxy.Policy, error) {
|
|
||||||
return goproxy.REQUIRE, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// httptest.NewUnstartedServer lets us replace the listener before
|
|
||||||
// calling StartTLS(), which wraps our proxyproto listener with
|
|
||||||
// tls.NewListener. This gives us the right layer order.
|
|
||||||
b.srv = httptest.NewUnstartedServer(handlerFn)
|
|
||||||
b.srv.Listener = pLn
|
|
||||||
|
|
||||||
// StartTLS enables HTTP/2 on the server automatically.
|
|
||||||
b.srv.StartTLS()
|
|
||||||
|
|
||||||
t.Cleanup(func() {
|
|
||||||
b.srv.Close()
|
|
||||||
})
|
|
||||||
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
// addr returns the listening address (host:port) of the TLS backend.
|
|
||||||
func (b *tlsProxyProtoBackend) addr() string {
|
|
||||||
return b.srv.Listener.Addr().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// tlsConfig returns the *tls.Config used by the backend server.
|
|
||||||
// Tests can use it to verify cert details if needed.
|
|
||||||
func (b *tlsProxyProtoBackend) tlsConfig() *tls.Config {
|
|
||||||
return b.srv.TLS
|
|
||||||
}
|
|
||||||
|
|
||||||
// recordedAddrs returns a snapshot of all PROXY-header source addresses.
|
|
||||||
func (b *tlsProxyProtoBackend) recordedAddrs() []string {
|
|
||||||
b.mu.Lock()
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
cp := make([]string, len(b.headerAddrs))
|
|
||||||
copy(cp, b.headerAddrs)
|
|
||||||
return cp
|
|
||||||
}
|
|
||||||
|
|
||||||
// proxyProtoTLSConfig builds a Caddy JSON configuration that proxies to a TLS
|
|
||||||
// upstream with PROXY protocol. The transport uses insecure_skip_verify so
|
|
||||||
// the self-signed certificate generated by httptest.Server is accepted.
|
|
||||||
func proxyProtoTLSConfig(listenPort int, backendAddr, ppVersion string, transportVersions []string) string {
|
|
||||||
versionsJSON, _ := json.Marshal(transportVersions)
|
|
||||||
return fmt.Sprintf(`{
|
|
||||||
"admin": {
|
|
||||||
"listen": "localhost:2999"
|
|
||||||
},
|
|
||||||
"apps": {
|
|
||||||
"pki": {
|
|
||||||
"certificate_authorities": {
|
|
||||||
"local": {
|
|
||||||
"install_trust": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"grace_period": 1,
|
|
||||||
"servers": {
|
|
||||||
"proxy": {
|
|
||||||
"listen": [":%d"],
|
|
||||||
"automatic_https": {
|
|
||||||
"disable": true
|
|
||||||
},
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"upstreams": [{"dial": "%s"}],
|
|
||||||
"transport": {
|
|
||||||
"protocol": "http",
|
|
||||||
"proxy_protocol": "%s",
|
|
||||||
"versions": %s,
|
|
||||||
"tls": {
|
|
||||||
"insecure_skip_verify": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`, listenPort, backendAddr, ppVersion, string(versionsJSON))
|
|
||||||
}
|
|
||||||
|
|
||||||
// testTLSProxyProtocolMatrix is the shared implementation for TLS-based proxy
|
|
||||||
// protocol tests. It mirrors testProxyProtocolMatrix but uses a TLS backend.
|
|
||||||
func testTLSProxyProtocolMatrix(t *testing.T, ppVersion string, transportVersions []string, numRequests int) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
backend := newTLSProxyProtoBackend(t)
|
|
||||||
listenPort := freePort(t)
|
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.WithDefaultOverrides(caddytest.Config{
|
|
||||||
AdminPort: 2999,
|
|
||||||
})
|
|
||||||
cfg := proxyProtoTLSConfig(listenPort, backend.addr(), ppVersion, transportVersions)
|
|
||||||
tester.InitServer(cfg, "json")
|
|
||||||
|
|
||||||
proxyURL := fmt.Sprintf("http://127.0.0.1:%d/", listenPort)
|
|
||||||
|
|
||||||
for i := 0; i < numRequests; i++ {
|
|
||||||
resp, err := tester.Client.Get(proxyURL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("request %d/%d: GET %s: %v", i+1, numRequests, proxyURL, err)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
t.Errorf("request %d/%d: expected status 200, got %d", i+1, numRequests, resp.StatusCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs := backend.recordedAddrs()
|
|
||||||
if len(addrs) == 0 {
|
|
||||||
t.Fatalf("backend recorded no PROXY protocol addresses (expected at least 1)")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, addr := range addrs {
|
|
||||||
host, _, err := net.SplitHostPort(addr)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("addr[%d] %q: SplitHostPort: %v", i, addr, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if host != "127.0.0.1" {
|
|
||||||
t.Errorf("addr[%d]: expected source 127.0.0.1, got %q", i, host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// proxyProtoConfig builds a Caddy JSON configuration that:
|
|
||||||
// - listens on listenPort for inbound HTTP requests
|
|
||||||
// - proxies them to backendAddr with PROXY protocol ppVersion ("v1"/"v2")
|
|
||||||
// - uses the given transport versions (e.g. ["1.1"] or ["h2c"])
|
|
||||||
func proxyProtoConfig(listenPort int, backendAddr, ppVersion string, transportVersions []string) string {
|
|
||||||
versionsJSON, _ := json.Marshal(transportVersions)
|
|
||||||
return fmt.Sprintf(`{
|
|
||||||
"admin": {
|
|
||||||
"listen": "localhost:2999"
|
|
||||||
},
|
|
||||||
"apps": {
|
|
||||||
"pki": {
|
|
||||||
"certificate_authorities": {
|
|
||||||
"local": {
|
|
||||||
"install_trust": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"grace_period": 1,
|
|
||||||
"servers": {
|
|
||||||
"proxy": {
|
|
||||||
"listen": [":%d"],
|
|
||||||
"automatic_https": {
|
|
||||||
"disable": true
|
|
||||||
},
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"upstreams": [{"dial": "%s"}],
|
|
||||||
"transport": {
|
|
||||||
"protocol": "http",
|
|
||||||
"proxy_protocol": "%s",
|
|
||||||
"versions": %s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`, listenPort, backendAddr, ppVersion, string(versionsJSON))
|
|
||||||
}
|
|
||||||
|
|
||||||
// freePort returns a free local TCP port by binding briefly and releasing it.
|
|
||||||
func freePort(t *testing.T) int {
|
|
||||||
t.Helper()
|
|
||||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("freePort: %v", err)
|
|
||||||
}
|
|
||||||
port := ln.Addr().(*net.TCPAddr).Port
|
|
||||||
_ = ln.Close()
|
|
||||||
return port
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestProxyProtocolV1WithH1 verifies that PROXY protocol v1 headers are sent
|
|
||||||
// correctly when the transport uses HTTP/1.1 to the upstream.
|
|
||||||
func TestProxyProtocolV1WithH1(t *testing.T) {
|
|
||||||
testProxyProtocolMatrix(t, "v1", []string{"1.1"}, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestProxyProtocolV2WithH1 verifies that PROXY protocol v2 headers are sent
|
|
||||||
// correctly when the transport uses HTTP/1.1 to the upstream.
|
|
||||||
func TestProxyProtocolV2WithH1(t *testing.T) {
|
|
||||||
testProxyProtocolMatrix(t, "v2", []string{"1.1"}, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestProxyProtocolV1WithH2C verifies that PROXY protocol v1 headers are sent
|
|
||||||
// correctly when the transport uses h2c (HTTP/2 cleartext) to the upstream.
|
|
||||||
func TestProxyProtocolV1WithH2C(t *testing.T) {
|
|
||||||
testProxyProtocolMatrix(t, "v1", []string{"h2c"}, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestProxyProtocolV2WithH2C verifies that PROXY protocol v2 headers are sent
|
|
||||||
// correctly when the transport uses h2c (HTTP/2 cleartext) to the upstream.
|
|
||||||
// This is the primary regression test for github.com/caddyserver/caddy/issues/7529:
|
|
||||||
// before the fix, the h2 transport opened a new TCP connection per request
|
|
||||||
// (because req.URL.Host was mangled differently for each request due to the
|
|
||||||
// varying client port), which caused file-descriptor exhaustion under load.
|
|
||||||
func TestProxyProtocolV2WithH2C(t *testing.T) {
|
|
||||||
testProxyProtocolMatrix(t, "v2", []string{"h2c"}, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestProxyProtocolV2WithH2CMultipleRequests sends several sequential requests
|
|
||||||
// through the h2c + PROXY-protocol path and confirms that:
|
|
||||||
// 1. Every request receives a 200 response (no connection exhaustion).
|
|
||||||
// 2. The backend received at least one PROXY header (connection was reused).
|
|
||||||
//
|
|
||||||
// This is the core regression guard for issue #7529: without the fix, a new
|
|
||||||
// TCP connection was opened per request, quickly exhausting file descriptors.
|
|
||||||
func TestProxyProtocolV2WithH2CMultipleRequests(t *testing.T) {
|
|
||||||
testProxyProtocolMatrix(t, "v2", []string{"h2c"}, 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestProxyProtocolV1WithH2 verifies that PROXY protocol v1 headers are sent
|
|
||||||
// correctly when the transport uses HTTP/2 over TLS (h2) to the upstream.
|
|
||||||
func TestProxyProtocolV1WithH2(t *testing.T) {
|
|
||||||
testTLSProxyProtocolMatrix(t, "v1", []string{"2"}, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestProxyProtocolV2WithH2 verifies that PROXY protocol v2 headers are sent
|
|
||||||
// correctly when the transport uses HTTP/2 over TLS (h2) to the upstream.
|
|
||||||
func TestProxyProtocolV2WithH2(t *testing.T) {
|
|
||||||
testTLSProxyProtocolMatrix(t, "v2", []string{"2"}, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestProxyProtocolServerAndProxy is an end-to-end matrix test that exercises
|
|
||||||
// all combinations of PROXY protocol version x transport version.
|
|
||||||
func TestProxyProtocolServerAndProxy(t *testing.T) {
|
|
||||||
plainTests := []struct {
|
|
||||||
name string
|
|
||||||
ppVersion string
|
|
||||||
transportVersions []string
|
|
||||||
numRequests int
|
|
||||||
}{
|
|
||||||
{"h1-v1", "v1", []string{"1.1"}, 3},
|
|
||||||
{"h1-v2", "v2", []string{"1.1"}, 3},
|
|
||||||
{"h2c-v1", "v1", []string{"h2c"}, 3},
|
|
||||||
{"h2c-v2", "v2", []string{"h2c"}, 3},
|
|
||||||
}
|
|
||||||
for _, tc := range plainTests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
testProxyProtocolMatrix(t, tc.ppVersion, tc.transportVersions, tc.numRequests)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsTests := []struct {
|
|
||||||
name string
|
|
||||||
ppVersion string
|
|
||||||
transportVersions []string
|
|
||||||
numRequests int
|
|
||||||
}{
|
|
||||||
{"h2-v1", "v1", []string{"2"}, 3},
|
|
||||||
{"h2-v2", "v2", []string{"2"}, 3},
|
|
||||||
}
|
|
||||||
for _, tc := range tlsTests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
testTLSProxyProtocolMatrix(t, tc.ppVersion, tc.transportVersions, tc.numRequests)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// testProxyProtocolMatrix is the shared implementation for the proxy protocol
|
|
||||||
// tests. It:
|
|
||||||
// 1. Starts a go-proxyproto-wrapped backend.
|
|
||||||
// 2. Configures Caddy as a reverse proxy with the given PROXY protocol
|
|
||||||
// version and transport versions.
|
|
||||||
// 3. Sends numRequests GET requests through Caddy and asserts 200 OK each time.
|
|
||||||
// 4. Asserts the backend recorded at least one PROXY header whose source host
|
|
||||||
// is 127.0.0.1 (the loopback address used by the test client).
|
|
||||||
func testProxyProtocolMatrix(t *testing.T, ppVersion string, transportVersions []string, numRequests int) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
backend := newProxyProtoBackend(t)
|
|
||||||
listenPort := freePort(t)
|
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.WithDefaultOverrides(caddytest.Config{
|
|
||||||
AdminPort: 2999,
|
|
||||||
})
|
|
||||||
cfg := proxyProtoConfig(listenPort, backend.addr(), ppVersion, transportVersions)
|
|
||||||
tester.InitServer(cfg, "json")
|
|
||||||
|
|
||||||
// If the test is h2c-only (no "1.1" in versions), reconfigure the test
|
|
||||||
// client transport to use unencrypted HTTP/2 so we actually exercise the
|
|
||||||
// h2c code path through Caddy.
|
|
||||||
if slices.Contains(transportVersions, "h2c") && !slices.Contains(transportVersions, "1.1") {
|
|
||||||
tr, ok := tester.Client.Transport.(*http.Transport)
|
|
||||||
if ok {
|
|
||||||
tr.Protocols = new(http.Protocols)
|
|
||||||
tr.Protocols.SetHTTP1(false)
|
|
||||||
tr.Protocols.SetUnencryptedHTTP2(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyURL := fmt.Sprintf("http://127.0.0.1:%d/", listenPort)
|
|
||||||
|
|
||||||
for i := 0; i < numRequests; i++ {
|
|
||||||
resp, err := tester.Client.Get(proxyURL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("request %d/%d: GET %s: %v", i+1, numRequests, proxyURL, err)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
t.Errorf("request %d/%d: expected status 200, got %d", i+1, numRequests, resp.StatusCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The backend must have seen at least one PROXY header. For h1, there is
|
|
||||||
// one per request; for h2c, requests share the same connection so only one
|
|
||||||
// header is written at connection establishment.
|
|
||||||
addrs := backend.recordedAddrs()
|
|
||||||
if len(addrs) == 0 {
|
|
||||||
t.Fatalf("backend recorded no PROXY protocol addresses (expected at least 1)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every PROXY-decoded source address must be the loopback address since
|
|
||||||
// the test client always connects from 127.0.0.1.
|
|
||||||
for i, addr := range addrs {
|
|
||||||
host, _, err := net.SplitHostPort(addr)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("addr[%d] %q: SplitHostPort: %v", i, addr, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if host != "127.0.0.1" {
|
|
||||||
t.Errorf("addr[%d]: expected source 127.0.0.1, got %q", i, host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestProxyProtocolListenerWrapper verifies that Caddy's
|
|
||||||
// caddy.listeners.proxy_protocol listener wrapper can successfully parse
|
|
||||||
// incoming PROXY protocol headers.
|
|
||||||
//
|
|
||||||
// The test dials Caddy's listening port directly, injects a raw PROXY v2
|
|
||||||
// header spoofing source address 10.0.0.1:1234, then sends a normal
|
|
||||||
// HTTP/1.1 GET request. The Caddy server is configured to echo back the
|
|
||||||
// remote address ({http.request.remote.host}). The test asserts that the
|
|
||||||
// echoed address is the spoofed 10.0.0.1.
|
|
||||||
func TestProxyProtocolListenerWrapper(t *testing.T) {
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.InitServer(`{
|
|
||||||
skip_install_trust
|
|
||||||
admin localhost:2999
|
|
||||||
http_port 9080
|
|
||||||
https_port 9443
|
|
||||||
grace_period 1ns
|
|
||||||
servers :9080 {
|
|
||||||
listener_wrappers {
|
|
||||||
proxy_protocol {
|
|
||||||
timeout 5s
|
|
||||||
allow 127.0.0.0/8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
http://localhost:9080 {
|
|
||||||
respond "{http.request.remote.host}"
|
|
||||||
}`, "caddyfile")
|
|
||||||
|
|
||||||
// Dial the Caddy listener directly and inject a PROXY v2 header that
|
|
||||||
// claims the connection originates from 10.0.0.1:1234.
|
|
||||||
conn, err := net.Dial("tcp", "127.0.0.1:9080")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("dial: %v", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
spoofedSrc := &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 1234}
|
|
||||||
spoofedDst := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 9080}
|
|
||||||
hdr := goproxy.HeaderProxyFromAddrs(2, spoofedSrc, spoofedDst)
|
|
||||||
if _, err := hdr.WriteTo(conn); err != nil {
|
|
||||||
t.Fatalf("write proxy header: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write a minimal HTTP/1.1 GET request.
|
|
||||||
_, err = fmt.Fprintf(conn,
|
|
||||||
"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("write HTTP request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the raw response and look for the spoofed address in the body.
|
|
||||||
buf := make([]byte, 4096)
|
|
||||||
n, _ := conn.Read(buf)
|
|
||||||
raw := string(buf[:n])
|
|
||||||
|
|
||||||
if !strings.Contains(raw, "10.0.0.1") {
|
|
||||||
t.Errorf("expected spoofed address 10.0.0.1 in response body; full response:\n%s", raw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddytest"
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
)
|
)
|
||||||
@@ -327,41 +327,6 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReverseProxyHealthCheck(t *testing.T) {
|
func TestReverseProxyHealthCheck(t *testing.T) {
|
||||||
// Start lightweight backend servers so they're ready before Caddy's
|
|
||||||
// active health checker runs; this avoids a startup race where the
|
|
||||||
// health checker probes backends that haven't yet begun accepting
|
|
||||||
// connections and marks them unhealthy.
|
|
||||||
//
|
|
||||||
// This mirrors how health checks are typically used in practice (to a separate
|
|
||||||
// backend service) and avoids probing the same Caddy instance while it's still
|
|
||||||
// provisioning and not ready to accept connections.
|
|
||||||
|
|
||||||
// backend server that responds to proxied requests
|
|
||||||
helloSrv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
_, _ = w.Write([]byte("Hello, World!"))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
ln0, err := net.Listen("tcp", "127.0.0.1:2020")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen on 127.0.0.1:2020: %v", err)
|
|
||||||
}
|
|
||||||
go helloSrv.Serve(ln0)
|
|
||||||
t.Cleanup(func() { helloSrv.Close(); ln0.Close() })
|
|
||||||
|
|
||||||
// backend server that serves health checks
|
|
||||||
healthSrv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
_, _ = w.Write([]byte("ok"))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
ln1, err := net.Listen("tcp", "127.0.0.1:2021")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen on 127.0.0.1:2021: %v", err)
|
|
||||||
}
|
|
||||||
go healthSrv.Serve(ln1)
|
|
||||||
t.Cleanup(func() { healthSrv.Close(); ln1.Close() })
|
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
tester := caddytest.NewTester(t)
|
||||||
tester.InitServer(`
|
tester.InitServer(`
|
||||||
{
|
{
|
||||||
@@ -371,6 +336,12 @@ func TestReverseProxyHealthCheck(t *testing.T) {
|
|||||||
https_port 9443
|
https_port 9443
|
||||||
grace_period 1ns
|
grace_period 1ns
|
||||||
}
|
}
|
||||||
|
http://localhost:2020 {
|
||||||
|
respond "Hello, World!"
|
||||||
|
}
|
||||||
|
http://localhost:2021 {
|
||||||
|
respond "ok"
|
||||||
|
}
|
||||||
http://localhost:9080 {
|
http://localhost:9080 {
|
||||||
reverse_proxy {
|
reverse_proxy {
|
||||||
to localhost:2020
|
to localhost:2020
|
||||||
@@ -384,68 +355,8 @@ func TestReverseProxyHealthCheck(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, "caddyfile")
|
`, "caddyfile")
|
||||||
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestReverseProxyHealthCheckPortUsed verifies that health_port is actually
|
time.Sleep(100 * time.Millisecond) // TODO: for some reason this test seems particularly flaky, getting 503 when it should be 200, unless we wait
|
||||||
// used for active health checks and not the upstream's main port. This is a
|
|
||||||
// regression test for https://github.com/caddyserver/caddy/issues/7524.
|
|
||||||
func TestReverseProxyHealthCheckPortUsed(t *testing.T) {
|
|
||||||
// upstream server: serves proxied requests normally, but returns 503 for
|
|
||||||
// /health so that if health checks mistakenly hit this port the upstream
|
|
||||||
// gets marked unhealthy and the proxy returns 503.
|
|
||||||
upstreamSrv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
if req.URL.Path == "/health" {
|
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, _ = w.Write([]byte("Hello, World!"))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
ln0, err := net.Listen("tcp", "127.0.0.1:2022")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen on 127.0.0.1:2022: %v", err)
|
|
||||||
}
|
|
||||||
go upstreamSrv.Serve(ln0)
|
|
||||||
t.Cleanup(func() { upstreamSrv.Close(); ln0.Close() })
|
|
||||||
|
|
||||||
// separate health check server on the configured health_port: returns 200
|
|
||||||
// so the upstream is marked healthy only if health checks go to this port.
|
|
||||||
healthSrv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
_, _ = w.Write([]byte("ok"))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
ln1, err := net.Listen("tcp", "127.0.0.1:2023")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen on 127.0.0.1:2023: %v", err)
|
|
||||||
}
|
|
||||||
go healthSrv.Serve(ln1)
|
|
||||||
t.Cleanup(func() { healthSrv.Close(); ln1.Close() })
|
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.InitServer(`
|
|
||||||
{
|
|
||||||
skip_install_trust
|
|
||||||
admin localhost:2999
|
|
||||||
http_port 9080
|
|
||||||
https_port 9443
|
|
||||||
grace_period 1ns
|
|
||||||
}
|
|
||||||
http://localhost:9080 {
|
|
||||||
reverse_proxy {
|
|
||||||
to localhost:2022
|
|
||||||
|
|
||||||
health_uri /health
|
|
||||||
health_port 2023
|
|
||||||
health_interval 10ms
|
|
||||||
health_timeout 100ms
|
|
||||||
health_passes 1
|
|
||||||
health_fails 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, "caddyfile")
|
|
||||||
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
|
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,233 +474,3 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
|
|||||||
|
|
||||||
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
|
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestReverseProxyRetryMatchStatusCode verifies that lb_retry_match with a
|
|
||||||
// CEL expression matching on {rp.status_code} causes the request to be
|
|
||||||
// retried on the next upstream when the first upstream returns a matching
|
|
||||||
// status code
|
|
||||||
func TestReverseProxyRetryMatchStatusCode(t *testing.T) {
|
|
||||||
// Bad upstream: returns 502
|
|
||||||
badSrv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusBadGateway)
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
badLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
go badSrv.Serve(badLn)
|
|
||||||
t.Cleanup(func() { badSrv.Close(); badLn.Close() })
|
|
||||||
|
|
||||||
// Good upstream: returns 200
|
|
||||||
goodSrv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Write([]byte("ok"))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
go goodSrv.Serve(goodLn)
|
|
||||||
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
|
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.InitServer(fmt.Sprintf(`
|
|
||||||
{
|
|
||||||
skip_install_trust
|
|
||||||
admin localhost:2999
|
|
||||||
http_port 9080
|
|
||||||
https_port 9443
|
|
||||||
grace_period 1ns
|
|
||||||
}
|
|
||||||
http://localhost:9080 {
|
|
||||||
reverse_proxy %s %s {
|
|
||||||
lb_policy round_robin
|
|
||||||
lb_retries 1
|
|
||||||
lb_retry_match {
|
|
||||||
expression `+"`{rp.status_code} in [502, 503]`"+`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, goodLn.Addr().String(), badLn.Addr().String()), "caddyfile")
|
|
||||||
|
|
||||||
tester.AssertGetResponse("http://localhost:9080/", 200, "ok")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestReverseProxyRetryMatchHeader verifies that lb_retry_match with a CEL
|
|
||||||
// expression matching on {rp.header.*} causes the request to be retried when
|
|
||||||
// the upstream sets a matching response header
|
|
||||||
func TestReverseProxyRetryMatchHeader(t *testing.T) {
|
|
||||||
var badHits atomic.Int32
|
|
||||||
|
|
||||||
// Bad upstream: returns 200 but signals retry via header
|
|
||||||
badSrv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
badHits.Add(1)
|
|
||||||
w.Header().Set("X-Upstream-Retry", "true")
|
|
||||||
w.Write([]byte("bad"))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
badLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
go badSrv.Serve(badLn)
|
|
||||||
t.Cleanup(func() { badSrv.Close(); badLn.Close() })
|
|
||||||
|
|
||||||
// Good upstream: returns 200 without retry header
|
|
||||||
goodSrv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Write([]byte("good"))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
go goodSrv.Serve(goodLn)
|
|
||||||
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
|
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.InitServer(fmt.Sprintf(`
|
|
||||||
{
|
|
||||||
skip_install_trust
|
|
||||||
admin localhost:2999
|
|
||||||
http_port 9080
|
|
||||||
https_port 9443
|
|
||||||
grace_period 1ns
|
|
||||||
}
|
|
||||||
http://localhost:9080 {
|
|
||||||
reverse_proxy %s %s {
|
|
||||||
lb_policy round_robin
|
|
||||||
lb_retries 1
|
|
||||||
lb_retry_match {
|
|
||||||
expression `+"`{rp.header.X-Upstream-Retry} == \"true\"`"+`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, goodLn.Addr().String(), badLn.Addr().String()), "caddyfile")
|
|
||||||
|
|
||||||
tester.AssertGetResponse("http://localhost:9080/", 200, "good")
|
|
||||||
|
|
||||||
if badHits.Load() != 1 {
|
|
||||||
t.Errorf("bad upstream hits: got %d, want 1", badHits.Load())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestReverseProxyRetryMatchCombined verifies that a CEL expression combining
|
|
||||||
// request path matching with response status code matching works correctly -
|
|
||||||
// only retrying when both conditions are met
|
|
||||||
func TestReverseProxyRetryMatchCombined(t *testing.T) {
|
|
||||||
// Upstream: returns 502 for all requests
|
|
||||||
srv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusBadGateway)
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
go srv.Serve(ln)
|
|
||||||
t.Cleanup(func() { srv.Close(); ln.Close() })
|
|
||||||
|
|
||||||
// Good upstream
|
|
||||||
goodSrv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Write([]byte("ok"))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
go goodSrv.Serve(goodLn)
|
|
||||||
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
|
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.InitServer(fmt.Sprintf(`
|
|
||||||
{
|
|
||||||
skip_install_trust
|
|
||||||
admin localhost:2999
|
|
||||||
http_port 9080
|
|
||||||
https_port 9443
|
|
||||||
grace_period 1ns
|
|
||||||
}
|
|
||||||
http://localhost:9080 {
|
|
||||||
reverse_proxy %s %s {
|
|
||||||
lb_policy round_robin
|
|
||||||
lb_retries 1
|
|
||||||
lb_retry_match {
|
|
||||||
expression `+"`path('/retry*') && {rp.status_code} in [502, 503]`"+`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, goodLn.Addr().String(), ln.Addr().String()), "caddyfile")
|
|
||||||
|
|
||||||
// /retry path matches the expression - should retry to good upstream
|
|
||||||
tester.AssertGetResponse("http://localhost:9080/retry", 200, "ok")
|
|
||||||
|
|
||||||
// /other path does NOT match - should return the 502
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/other", nil)
|
|
||||||
tester.AssertResponse(req, 502, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestReverseProxyRetryMatchIsTransportError verifies that the
|
|
||||||
// {rp.is_transport_error} == true CEL function correctly identifies transport errors
|
|
||||||
// and allows retrying them alongside response-based matching
|
|
||||||
func TestReverseProxyRetryMatchIsTransportError(t *testing.T) {
|
|
||||||
// Good upstream: returns 200
|
|
||||||
goodSrv := &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Write([]byte("ok"))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
go goodSrv.Serve(goodLn)
|
|
||||||
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
|
|
||||||
|
|
||||||
// Broken upstream: accepts connections but closes immediately
|
|
||||||
brokenLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() { brokenLn.Close() })
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
conn, err := brokenLn.Accept()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.InitServer(fmt.Sprintf(`
|
|
||||||
{
|
|
||||||
skip_install_trust
|
|
||||||
admin localhost:2999
|
|
||||||
http_port 9080
|
|
||||||
https_port 9443
|
|
||||||
grace_period 1ns
|
|
||||||
}
|
|
||||||
http://localhost:9080 {
|
|
||||||
reverse_proxy %s %s {
|
|
||||||
lb_policy round_robin
|
|
||||||
lb_retries 1
|
|
||||||
lb_retry_match {
|
|
||||||
expression `+"`{rp.is_transport_error} || {rp.status_code} in [502, 503]`"+`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, goodLn.Addr().String(), brokenLn.Addr().String()), "caddyfile")
|
|
||||||
|
|
||||||
// Transport error on broken upstream should be retried to good upstream
|
|
||||||
tester.AssertGetResponse("http://localhost:9080/", 200, "ok")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
|
|||||||
}
|
}
|
||||||
// We only accept HTTP/2!
|
// We only accept HTTP/2!
|
||||||
if r.ProtoMajor != 2 {
|
if r.ProtoMajor != 2 {
|
||||||
t.Error("Not an HTTP/2 request, rejected!")
|
t.Error("Not a HTTP/2 request, rejected!")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
# Used by import_block_snippet_non_replaced_block_from_separate_file.caddyfiletest
|
|
||||||
|
|
||||||
(snippet) {
|
|
||||||
header {
|
|
||||||
reverse_proxy localhost:3000
|
|
||||||
{block}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# This snippet being unused by the test Caddyfile is intentional.
|
|
||||||
# This is to test that a panic runtime error triggered by an out-of-range slice index access
|
|
||||||
# will not happen again, please see issue #7518 and pull request #7543 for more information
|
|
||||||
(unused_snippet) {
|
|
||||||
header SomeHeader SomeValue
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Used by import_block_snippet_invalid_subdirective.caddyfiletest
|
|
||||||
|
|
||||||
(test) {
|
|
||||||
reverse_proxy {
|
|
||||||
{block}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Configure Caddy
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
debug
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
log
|
||||||
|
basic_auth {
|
||||||
|
john $2a$14$x4HlYwA9Zeer4RkMEYbUzug9XxWmncneR.dcMs.UjalR95URnHg5.
|
||||||
|
}
|
||||||
|
respond "Hello, World!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# requests without `Authorization` header are rejected with 401
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 401
|
||||||
|
[Asserts]
|
||||||
|
header "WWW-Authenticate" == "Basic realm=\"restricted\""
|
||||||
|
|
||||||
|
|
||||||
|
# requests with `Authorization` header are accepted with 200
|
||||||
|
GET https://localhost:9443
|
||||||
|
[BasicAuth]
|
||||||
|
john:password
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
`Hello, World!`
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# Configure Caddy with error directive
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
error /forbidden* "Access denied" 403
|
||||||
|
respond "OK"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# error directive triggers 403 for matching paths
|
||||||
|
GET https://localhost:9443/forbidden/resource
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 403
|
||||||
|
|
||||||
|
|
||||||
|
# error directive does not trigger for non-matching paths
|
||||||
|
GET https://localhost:9443/allowed
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "OK"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with error and handle_errors
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
error /admin* "Forbidden" 403
|
||||||
|
handle_errors {
|
||||||
|
respond "Custom error: {err.status_code} - {err.status_text}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# error with handle_errors shows custom error page
|
||||||
|
GET https://localhost:9443/admin/panel
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 403
|
||||||
|
[Asserts]
|
||||||
|
body == "Custom error: 403 - Forbidden"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with conditional error
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
@admin path /admin*
|
||||||
|
error @admin 404
|
||||||
|
respond "Public content"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# error with named matcher triggers on match
|
||||||
|
GET https://localhost:9443/admin/users
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 404
|
||||||
|
|
||||||
|
|
||||||
|
# error with named matcher doesn't trigger on non-match
|
||||||
|
GET https://localhost:9443/public
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "Public content"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with error for specific methods
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
@post method POST
|
||||||
|
error @post "Method not allowed" 405
|
||||||
|
respond "GET OK"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# error blocks POST requests
|
||||||
|
POST https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 405
|
||||||
|
|
||||||
|
|
||||||
|
# error allows GET requests
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "GET OK"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with dynamic error message
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
error /error* "Path {path} not found" 404
|
||||||
|
handle_errors {
|
||||||
|
respond "{err.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# error message can use placeholders
|
||||||
|
GET https://localhost:9443/error/test
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 404
|
||||||
|
[Asserts]
|
||||||
|
body == "Path /error/test not found"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Index.html Title</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
Index.html
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
index.txt
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# Configure Caddy with default configuration
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
debug
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
root {{indexed_root}}
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# requests without specific file receive index file per
|
||||||
|
# the default index list: index.html, index.txt
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
```
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Index.html Title</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
Index.html
|
||||||
|
</body>
|
||||||
|
</html>```
|
||||||
|
|
||||||
|
|
||||||
|
# if index.txt is specifically requested, we expect index.txt
|
||||||
|
GET https://localhost:9443/index.txt
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "index.txt"
|
||||||
|
|
||||||
|
# requests for sub-folder followed by .. result in sanitized path
|
||||||
|
GET https://localhost:9443/non-existent/../index.txt
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "index.txt"
|
||||||
|
|
||||||
|
# results out of root folder are sanitized,
|
||||||
|
# and conform to default index list sequence.
|
||||||
|
GET https://localhost:9443/../
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
```
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Index.html Title</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
Index.html
|
||||||
|
</body>
|
||||||
|
</html>```
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with custsom index "index.txt"
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
debug
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
root {{indexed_root}}
|
||||||
|
file_server {
|
||||||
|
index index.txt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "index.txt"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure with a root not containing index files
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
debug
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
root {{unindexed_root}}
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 404
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# Configure Caddy with forward_auth directive
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
forward_auth localhost:9080 {
|
||||||
|
uri /auth
|
||||||
|
}
|
||||||
|
respond "Protected content"
|
||||||
|
}
|
||||||
|
http://localhost:9080 {
|
||||||
|
handle /auth {
|
||||||
|
respond 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# forward_auth allows request when auth endpoint returns 2xx
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
delay: 500ms
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "Protected content"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with forward_auth rejecting
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
forward_auth localhost:9080 {
|
||||||
|
uri /auth
|
||||||
|
}
|
||||||
|
respond "Protected content"
|
||||||
|
}
|
||||||
|
http://localhost:9080 {
|
||||||
|
handle /auth {
|
||||||
|
respond 401
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# forward_auth blocks request when auth endpoint returns 4xx
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
delay: 500ms
|
||||||
|
insecure: true
|
||||||
|
HTTP 401
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with forward_auth copying headers
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
forward_auth localhost:9080 {
|
||||||
|
uri /auth
|
||||||
|
copy_headers X-User-ID X-User-Email
|
||||||
|
}
|
||||||
|
respond "User: {header.X-User-ID}, Email: {header.X-User-Email}"
|
||||||
|
}
|
||||||
|
http://localhost:9080 {
|
||||||
|
handle /auth {
|
||||||
|
header X-User-ID "user123"
|
||||||
|
header X-User-Email "user@example.com"
|
||||||
|
respond 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# forward_auth copies specified headers from auth response
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
delay: 500ms
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "User: user123, Email: user@example.com"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with forward_auth and custom headers
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
forward_auth localhost:9080 {
|
||||||
|
uri /auth
|
||||||
|
header_up X-Original-URL {uri}
|
||||||
|
}
|
||||||
|
respond "OK"
|
||||||
|
}
|
||||||
|
http://localhost:9080 {
|
||||||
|
handle /auth {
|
||||||
|
respond "{header.X-Original-URL}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# forward_auth can send custom headers to auth endpoint
|
||||||
|
GET https://localhost:9443/test/path
|
||||||
|
[Options]
|
||||||
|
delay: 500ms
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "OK"
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Configure Caddy
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
debug
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
header "X-Custom-Header" "Custom-Value"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
header "X-Custom-Header" == "Custom-Value"
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
# Configure Caddy with request_header directive
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
request_header X-Custom-Header "CustomValue"
|
||||||
|
respond "{header.X-Custom-Header}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# request_header adds headers to request
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "CustomValue"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with request_header removing headers
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
request_header -User-Agent
|
||||||
|
respond "UA: {header.User-Agent}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# request_header can remove headers
|
||||||
|
GET https://localhost:9443
|
||||||
|
User-Agent: TestAgent/1.0
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "UA: "
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with request_header replacing headers
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
request_header Host "example.com"
|
||||||
|
respond "Host: {host}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# request_header can replace Host header
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "Host: example.com"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with request_header using placeholders
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
request_header X-Original-Path {path}
|
||||||
|
respond "Path: {header.X-Original-Path}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# request_header can use placeholders
|
||||||
|
GET https://localhost:9443/test/path
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "Path: /test/path"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with conditional request_header
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
@api path /api/*
|
||||||
|
request_header @api X-API "true"
|
||||||
|
respond "API: {header.X-API}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# request_header applies conditionally based on matcher
|
||||||
|
GET https://localhost:9443/api/test
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "API: true"
|
||||||
|
|
||||||
|
|
||||||
|
# request_header doesn't apply when matcher doesn't match
|
||||||
|
GET https://localhost:9443/other
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "API: "
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with multiple request_header operations
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
request_header X-First "1"
|
||||||
|
request_header X-Second "2"
|
||||||
|
request_header X-Third "3"
|
||||||
|
respond "{header.X-First},{header.X-Second},{header.X-Third}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# multiple request_header directives are applied
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "1,2,3"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with request_header and reverse_proxy
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
debug
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
request_header X-Custom-Header "Value"
|
||||||
|
reverse_proxy localhost:9450
|
||||||
|
}
|
||||||
|
http://localhost:9450 {
|
||||||
|
respond "{header.X-Custom-Header}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# request_header adds header before reverse_proxy
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "Value"
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Configure Caddy
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
log
|
||||||
|
request_body {
|
||||||
|
max_size 2B
|
||||||
|
}
|
||||||
|
reverse_proxy localhost:8000 # to fake body reading
|
||||||
|
handle_errors 4xx {
|
||||||
|
respond "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
http://localhost:8000 {
|
||||||
|
respond "Failed"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
delay: 1s
|
||||||
|
insecure: true
|
||||||
|
```
|
||||||
|
Hello
|
||||||
|
```
|
||||||
|
HTTP 413
|
||||||
|
`OK`
|
||||||
|
|
||||||
|
# TODO: how to test{read,write}_timeout?
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Configure Caddy
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
User-Agent: hurl/ci
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
rewrite /from /to
|
||||||
|
respond {uri}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# simple scenario: rewriting /from to /to produces expected result of seeing /to
|
||||||
|
GET https://localhost:9443/from
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "/to"
|
||||||
|
|
||||||
|
# unmatched path is passed through unchanged
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "/"
|
||||||
|
|
||||||
|
# having a query parameter does not trip the rewrite and retains the query
|
||||||
|
GET https://localhost:9443/from?query_param=value
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "/to?query_param=value"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
User-Agent: hurl/ci
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
rewrite /from /to?a=b
|
||||||
|
respond {uri}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# a rewrite with query parameters affects the parameters
|
||||||
|
GET https://localhost:9443/from?query_param=value
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "/to?a=b"
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
# Configure Caddy with route directive
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
route /api/* {
|
||||||
|
uri strip_prefix /api
|
||||||
|
respond "API: {uri}"
|
||||||
|
}
|
||||||
|
respond "Not API"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# route groups handlers and maintains order
|
||||||
|
GET https://localhost:9443/api/users
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "API: /users"
|
||||||
|
|
||||||
|
|
||||||
|
# route doesn't match non-matching paths
|
||||||
|
GET https://localhost:9443/other
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "Not API"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with nested routes
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
route /api/* {
|
||||||
|
uri strip_prefix /api
|
||||||
|
route /v1/* {
|
||||||
|
uri strip_prefix /v1
|
||||||
|
respond "API v1: {uri}"
|
||||||
|
}
|
||||||
|
respond "API: {uri}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# nested routes process sequentially
|
||||||
|
GET https://localhost:9443/api/v1/users
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "API v1: /users"
|
||||||
|
|
||||||
|
|
||||||
|
# outer route processes when inner doesn't match
|
||||||
|
GET https://localhost:9443/api/users
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "API: /users"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with route and terminal handlers
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
route {
|
||||||
|
header X-First "1"
|
||||||
|
respond "Response"
|
||||||
|
header X-Second "2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# route stops at terminal handler (respond)
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
header "X-First" == "1"
|
||||||
|
header "X-Second" not exists
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with route preserving handler order
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
route {
|
||||||
|
vars step1 "done"
|
||||||
|
vars step2 "done"
|
||||||
|
vars step3 "done"
|
||||||
|
respond "{vars.step1},{vars.step2},{vars.step3}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# route preserves exact handler order
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "done,done,done"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with route and matchers
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
route {
|
||||||
|
@api path /api/*
|
||||||
|
vars @api type "api"
|
||||||
|
vars type "other"
|
||||||
|
respond "{vars.type}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# route applies matchers in sequence
|
||||||
|
GET https://localhost:9443/api/test
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "other"
|
||||||
|
|
||||||
|
|
||||||
|
# route continues when matcher doesn't match
|
||||||
|
GET https://localhost:9443/test
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "other"
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# Configure Caddy
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
User-Agent: hurl/ci
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
log
|
||||||
|
respond "Hello, World!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
`Hello, World!`
|
||||||
|
|
||||||
|
|
||||||
|
GET https://localhost:9443/foo
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
`Hello, World!`
|
||||||
|
|
||||||
|
# Configure Caddy
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
User-Agent: hurl/ci
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
respond "New text!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP/2 200
|
||||||
|
[Asserts]
|
||||||
|
`New text!`
|
||||||
|
|
||||||
|
|
||||||
|
GET https://localhost:9443/foo
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP/2 200
|
||||||
|
[Asserts]
|
||||||
|
`New text!`
|
||||||
|
|
||||||
|
GET https://localhost:9443/foo
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP/2 200
|
||||||
|
[Asserts]
|
||||||
|
body != "Hello, World!"
|
||||||
|
|
||||||
|
# Configure Caddy
|
||||||
|
# The body is a placeholder
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
User-Agent: hurl/ci
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
log
|
||||||
|
respond {http.request.body}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# handler responds with the "application/json" if the response body is valid JSON
|
||||||
|
POST https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"greeting": "Hello, world!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
HTTP/2 200
|
||||||
|
[Asserts]
|
||||||
|
header "Content-Type" == "application/json"
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"greeting": "Hello, world!"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
# Configure Caddy with uri strip_prefix
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
uri strip_prefix /api
|
||||||
|
respond {uri}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# strip_prefix removes the prefix from the URI
|
||||||
|
GET https://localhost:9443/api/users
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "/users"
|
||||||
|
|
||||||
|
|
||||||
|
# URI without prefix is unchanged
|
||||||
|
GET https://localhost:9443/users
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "/users"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with uri strip_suffix
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
uri strip_suffix .php
|
||||||
|
respond {uri}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# strip_suffix removes the suffix from the URI
|
||||||
|
GET https://localhost:9443/index.php
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "/index"
|
||||||
|
|
||||||
|
|
||||||
|
# URI without suffix is unchanged
|
||||||
|
GET https://localhost:9443/index.html
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "/index.html"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with uri replace
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
uri replace old new
|
||||||
|
respond {uri}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# replace substitutes all occurrences
|
||||||
|
GET https://localhost:9443/old/path/old
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "/new/path/new"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with uri path_regexp
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
uri path_regexp /([0-9]+) /$1/id
|
||||||
|
respond {uri}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# path_regexp replaces using regular expressions
|
||||||
|
GET https://localhost:9443/123
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "/123/id"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with uri query operations
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
uri query +foo bar
|
||||||
|
respond {query}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# query operations add parameters
|
||||||
|
GET https://localhost:9443/?existing=value
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "existing=value&foo=bar"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with uri query delete
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
uri query -sensitive
|
||||||
|
respond {query}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# query operations delete parameters
|
||||||
|
GET https://localhost:9443/?keep=this&sensitive=secret
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "keep=this"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with uri query rename
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
uri query old>new
|
||||||
|
respond {query}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# query operations rename parameters
|
||||||
|
GET https://localhost:9443/?old=value
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "new=value"
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Configure Caddy with vars directive
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
vars my_var "custom_value"
|
||||||
|
vars another_var "another_value"
|
||||||
|
respond "{vars.my_var} {vars.another_var}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Variables are accessible in placeholders
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "custom_value another_value"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with vars using placeholders
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
vars request_path {path}
|
||||||
|
vars request_method {method}
|
||||||
|
respond "Path: {vars.request_path}, Method: {vars.request_method}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Variables can be set from request placeholders
|
||||||
|
GET https://localhost:9443/test/path
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "Path: /test/path, Method: GET"
|
||||||
|
|
||||||
|
|
||||||
|
# POST method is captured correctly
|
||||||
|
POST https://localhost:9443/another
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "Path: /another, Method: POST"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with vars in route
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
route /api/* {
|
||||||
|
vars api_version "v1"
|
||||||
|
respond "API {vars.api_version}"
|
||||||
|
}
|
||||||
|
respond "Not API"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Variables are scoped to their route
|
||||||
|
GET https://localhost:9443/api/users
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "API v1"
|
||||||
|
|
||||||
|
|
||||||
|
# Outside the route, variables are not set
|
||||||
|
GET https://localhost:9443/other
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "Not API"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure Caddy with vars overwriting
|
||||||
|
POST http://localhost:2019/load
|
||||||
|
Content-Type: text/caddyfile
|
||||||
|
```
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
# without `route`, middlewares are sorted an unstable sort
|
||||||
|
route {
|
||||||
|
vars my_var "2"
|
||||||
|
vars my_var "1"
|
||||||
|
}
|
||||||
|
respond "{vars.my_var}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Later vars directives overwrite earlier ones
|
||||||
|
GET https://localhost:9443
|
||||||
|
[Options]
|
||||||
|
insecure: true
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
body == "1"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
indexed_root=caddytest/spec/http/file_server/assets/indexed
|
||||||
|
unindexed_root=caddytest/spec/http/file_server/assets/unindexed
|
||||||
@@ -29,8 +29,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "time/tzdata"
|
|
||||||
|
|
||||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||||
|
|
||||||
// plug in Caddy modules here
|
// plug in Caddy modules here
|
||||||
|
|||||||
+4
-14
@@ -9,14 +9,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var defaultFactory = newRootCommandFactory(func() *cobra.Command {
|
var defaultFactory = newRootCommandFactory(func() *cobra.Command {
|
||||||
bin := caddy.CustomBinaryName
|
return &cobra.Command{
|
||||||
if bin == "" {
|
Use: "caddy",
|
||||||
bin = "caddy"
|
Long: `Caddy is an extensible server platform written in Go.
|
||||||
}
|
|
||||||
|
|
||||||
long := caddy.CustomLongDescription
|
|
||||||
if long == "" {
|
|
||||||
long = `Caddy is an extensible server platform written in Go.
|
|
||||||
|
|
||||||
At its core, Caddy merely manages configuration. Modules are plugged
|
At its core, Caddy merely manages configuration. Modules are plugged
|
||||||
in statically at compile-time to provide useful functionality. Caddy's
|
in statically at compile-time to provide useful functionality. Caddy's
|
||||||
@@ -96,12 +91,7 @@ package installers: https://caddyserver.com/docs/install
|
|||||||
|
|
||||||
Instructions for running Caddy in production are also available:
|
Instructions for running Caddy in production are also available:
|
||||||
https://caddyserver.com/docs/running
|
https://caddyserver.com/docs/running
|
||||||
`
|
`,
|
||||||
}
|
|
||||||
|
|
||||||
return &cobra.Command{
|
|
||||||
Use: bin,
|
|
||||||
Long: long,
|
|
||||||
Example: ` $ caddy run
|
Example: ` $ caddy run
|
||||||
$ caddy run --config caddy.json
|
$ caddy run --config caddy.json
|
||||||
$ caddy reload --config caddy.json
|
$ caddy reload --config caddy.json
|
||||||
|
|||||||
+17
-77
@@ -58,7 +58,7 @@ func cmdStart(fl Flags) (int, error) {
|
|||||||
|
|
||||||
// open a listener to which the child process will connect when
|
// open a listener to which the child process will connect when
|
||||||
// it is ready to confirm that it has successfully started
|
// it is ready to confirm that it has successfully started
|
||||||
ln, err := listenTCPForPingback(net.Listen)
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup,
|
return caddy.ExitCodeFailedStartup,
|
||||||
fmt.Errorf("opening listener for success confirmation: %v", err)
|
fmt.Errorf("opening listener for success confirmation: %v", err)
|
||||||
@@ -74,7 +74,7 @@ func cmdStart(fl Flags) (int, error) {
|
|||||||
// ensure it's the process we're expecting - we can be
|
// ensure it's the process we're expecting - we can be
|
||||||
// sure by giving it some random bytes and having it echo
|
// sure by giving it some random bytes and having it echo
|
||||||
// them back to us)
|
// them back to us)
|
||||||
cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String()) //nolint:gosec // no command injection that I can determine...
|
cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
|
||||||
// we should be able to run caddy in relative paths
|
// we should be able to run caddy in relative paths
|
||||||
if errors.Is(cmd.Err, exec.ErrDot) {
|
if errors.Is(cmd.Err, exec.ErrDot) {
|
||||||
cmd.Err = nil
|
cmd.Err = nil
|
||||||
@@ -169,22 +169,6 @@ func cmdStart(fl Flags) (int, error) {
|
|||||||
return caddy.ExitCodeSuccess, nil
|
return caddy.ExitCodeSuccess, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type tcpListenFunc func(network, address string) (net.Listener, error)
|
|
||||||
|
|
||||||
func listenTCPForPingback(listen tcpListenFunc) (net.Listener, error) {
|
|
||||||
ln, ipv4Err := listen("tcp4", "127.0.0.1:0")
|
|
||||||
if ipv4Err == nil {
|
|
||||||
return ln, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ln, ipv6Err := listen("tcp6", "[::1]:0")
|
|
||||||
if ipv6Err == nil {
|
|
||||||
return ln, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("listen on 127.0.0.1:0: %v; listen on [::1]:0: %v", ipv4Err, ipv6Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func cmdRun(fl Flags) (int, error) {
|
func cmdRun(fl Flags) (int, error) {
|
||||||
caddy.TrapSignals()
|
caddy.TrapSignals()
|
||||||
|
|
||||||
@@ -388,7 +372,7 @@ func cmdReload(fl Flags) (int, error) {
|
|||||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
|
||||||
}
|
}
|
||||||
|
|
||||||
adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFile, configAdapterFlag)
|
adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFlag, configAdapterFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
|
||||||
}
|
}
|
||||||
@@ -427,65 +411,11 @@ func cmdBuildInfo(_ Flags) (int, error) {
|
|||||||
return caddy.ExitCodeSuccess, nil
|
return caddy.ExitCodeSuccess, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// jsonModuleInfo holds metadata about a Caddy module for JSON output.
|
|
||||||
type jsonModuleInfo struct {
|
|
||||||
ModuleName string `json:"module_name"`
|
|
||||||
ModuleType string `json:"module_type"`
|
|
||||||
Version string `json:"version,omitempty"`
|
|
||||||
PackageURL string `json:"package_url,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func cmdListModules(fl Flags) (int, error) {
|
func cmdListModules(fl Flags) (int, error) {
|
||||||
packages := fl.Bool("packages")
|
packages := fl.Bool("packages")
|
||||||
versions := fl.Bool("versions")
|
versions := fl.Bool("versions")
|
||||||
skipStandard := fl.Bool("skip-standard")
|
skipStandard := fl.Bool("skip-standard")
|
||||||
jsonOutput := fl.Bool("json")
|
|
||||||
|
|
||||||
// Organize modules by whether they come with the standard distribution
|
|
||||||
standard, nonstandard, unknown, err := getModules()
|
|
||||||
if err != nil {
|
|
||||||
// If module info can't be fetched, just print the IDs and exit
|
|
||||||
for _, m := range caddy.Modules() {
|
|
||||||
fmt.Println(m)
|
|
||||||
}
|
|
||||||
return caddy.ExitCodeSuccess, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logic for JSON output
|
|
||||||
if jsonOutput {
|
|
||||||
output := []jsonModuleInfo{}
|
|
||||||
|
|
||||||
// addToOutput is a helper to convert internal module info to the JSON-serializable struct
|
|
||||||
addToOutput := func(list []moduleInfo, moduleType string) {
|
|
||||||
for _, mi := range list {
|
|
||||||
item := jsonModuleInfo{
|
|
||||||
ModuleName: mi.caddyModuleID,
|
|
||||||
ModuleType: moduleType, // Mapping the type here
|
|
||||||
}
|
|
||||||
if mi.goModule != nil {
|
|
||||||
item.Version = mi.goModule.Version
|
|
||||||
item.PackageURL = mi.goModule.Path
|
|
||||||
}
|
|
||||||
output = append(output, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass the respective type for each category
|
|
||||||
if !skipStandard {
|
|
||||||
addToOutput(standard, "standard")
|
|
||||||
}
|
|
||||||
addToOutput(nonstandard, "non-standard")
|
|
||||||
addToOutput(unknown, "unknown")
|
|
||||||
|
|
||||||
jsonBytes, err := json.MarshalIndent(output, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return caddy.ExitCodeFailedQuit, err
|
|
||||||
}
|
|
||||||
fmt.Println(string(jsonBytes))
|
|
||||||
return caddy.ExitCodeSuccess, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logic for Text output (Fallback)
|
|
||||||
printModuleInfo := func(mi moduleInfo) {
|
printModuleInfo := func(mi moduleInfo) {
|
||||||
fmt.Print(mi.caddyModuleID)
|
fmt.Print(mi.caddyModuleID)
|
||||||
if versions && mi.goModule != nil {
|
if versions && mi.goModule != nil {
|
||||||
@@ -503,6 +433,16 @@ func cmdListModules(fl Flags) (int, error) {
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// organize modules by whether they come with the standard distribution
|
||||||
|
standard, nonstandard, unknown, err := getModules()
|
||||||
|
if err != nil {
|
||||||
|
// oh well, just print the module IDs and exit
|
||||||
|
for _, m := range caddy.Modules() {
|
||||||
|
fmt.Println(m)
|
||||||
|
}
|
||||||
|
return caddy.ExitCodeSuccess, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Standard modules (always shipped with Caddy)
|
// Standard modules (always shipped with Caddy)
|
||||||
if !skipStandard {
|
if !skipStandard {
|
||||||
if len(standard) > 0 {
|
if len(standard) > 0 {
|
||||||
@@ -521,8 +461,8 @@ func cmdListModules(fl Flags) (int, error) {
|
|||||||
for _, mod := range nonstandard {
|
for _, mod := range nonstandard {
|
||||||
printModuleInfo(mod)
|
printModuleInfo(mod)
|
||||||
}
|
}
|
||||||
fmt.Printf("\n Non-standard modules: %d\n", len(nonstandard))
|
|
||||||
}
|
}
|
||||||
|
fmt.Printf("\n Non-standard modules: %d\n", len(nonstandard))
|
||||||
|
|
||||||
// Unknown modules (couldn't get Caddy module info)
|
// Unknown modules (couldn't get Caddy module info)
|
||||||
if len(unknown) > 0 {
|
if len(unknown) > 0 {
|
||||||
@@ -532,8 +472,8 @@ func cmdListModules(fl Flags) (int, error) {
|
|||||||
for _, mod := range unknown {
|
for _, mod := range unknown {
|
||||||
printModuleInfo(mod)
|
printModuleInfo(mod)
|
||||||
}
|
}
|
||||||
fmt.Printf("\n Unknown modules: %d\n", len(unknown))
|
|
||||||
}
|
}
|
||||||
|
fmt.Printf("\n Unknown modules: %d\n", len(unknown))
|
||||||
|
|
||||||
return caddy.ExitCodeSuccess, nil
|
return caddy.ExitCodeSuccess, nil
|
||||||
}
|
}
|
||||||
@@ -713,7 +653,7 @@ func cmdFmt(fl Flags) (int, error) {
|
|||||||
output := caddyfile.Format(input)
|
output := caddyfile.Format(input)
|
||||||
|
|
||||||
if fl.Bool("overwrite") {
|
if fl.Bool("overwrite") {
|
||||||
if err := os.WriteFile(configFile, output, 0o600); err != nil { //nolint:gosec // path traversal is not really a thing here, this is either "Caddyfile" or admin-controlled
|
if err := os.WriteFile(configFile, output, 0o600); err != nil {
|
||||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
|
||||||
}
|
}
|
||||||
return caddy.ExitCodeSuccess, nil
|
return caddy.ExitCodeSuccess, nil
|
||||||
@@ -836,7 +776,7 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Do(req) //nolint:gosec // the only SSRF here would be self-sabatoge I think
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("performing request: %v", err)
|
return nil, fmt.Errorf("performing request: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user