mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 08:42:31 -04:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f1ff118f8 | |||
| 4d40619aa4 | |||
| 3c591ecac9 | |||
| 73854014d9 | |||
| c0d9a2383e | |||
| 7bc7e1680e | |||
| edf4168c8e | |||
| 926fb82f6b | |||
| 841fe2544d | |||
| b19feec6dc | |||
| 41a4320fd3 | |||
| b491fc5d6c | |||
| 01cb878087 | |||
| b98c89fbb6 | |||
| 2619271a5c | |||
| 93a1853022 |
@@ -1,31 +0,0 @@
|
|||||||
name: Issue
|
|
||||||
description: An actionable development item, like a bug report or feature request
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Thank you for opening an issue! This is for actionable development items like bug reports and feature requests.
|
|
||||||
If you have a question about using Caddy, please [post on our forums](https://caddy.community) instead.
|
|
||||||
- type: textarea
|
|
||||||
id: content
|
|
||||||
attributes:
|
|
||||||
label: Issue Details
|
|
||||||
placeholder: Describe the issue here. Be specific by providing complete logs and minimal instructions to reproduce, or a thoughtful proposal, etc.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: assistance-disclosure
|
|
||||||
attributes:
|
|
||||||
label: Assistance Disclosure
|
|
||||||
description: "Our project allows assistance by AI/LLM tools as long as it is disclosed and described so we can better respond. Please certify whether you have used any such tooling related to this issue:"
|
|
||||||
options:
|
|
||||||
-
|
|
||||||
- AI used
|
|
||||||
- AI not used
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: assistance-description
|
|
||||||
attributes:
|
|
||||||
label: If AI was used, describe the extent to which it was used.
|
|
||||||
description: 'Examples: "ChatGPT translated from my native language" or "Claude proposed this change/feature"'
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: Caddy forum
|
|
||||||
url: https://caddy.community
|
|
||||||
about: If you have questions (or answers!) about using Caddy, please use our forum
|
|
||||||
+7
-7
@@ -5,11 +5,11 @@ The Caddy project would like to make sure that it stays on top of all practicall
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| -------- | ----------|
|
| ------- | ------------------ |
|
||||||
| 2.latest | ✔️ |
|
| 2.x | ✔️ |
|
||||||
| 1.x | :x: |
|
| 1.x | :x: |
|
||||||
| < 1.x | :x: |
|
| < 1.x | :x: |
|
||||||
|
|
||||||
|
|
||||||
## Acceptable Scope
|
## Acceptable Scope
|
||||||
@@ -48,9 +48,9 @@ We consider publicly-registered domain names to be public information. This nece
|
|||||||
|
|
||||||
It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, depending on available resources. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. (We get a lot of invalid reports.) Thank you for understanding.
|
It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, depending on available resources. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. (We get a lot of invalid reports.) Thank you for understanding.
|
||||||
|
|
||||||
When you are ready, please submit a [new private vulnerability report](https://github.com/caddyserver/caddy/security/advisories/new).
|
When you are ready, please email Matt Holt (the author) directly: matt at dyanim dot com.
|
||||||
|
|
||||||
Please don't encrypt the message. It only makes the process more complicated.
|
Please don't encrypt the email body. It only makes the process more complicated.
|
||||||
|
|
||||||
Please also understand that due to our nature as an open source project, we do not have a budget to award security bounties. We can only thank you.
|
Please also understand that due to our nature as an open source project, we do not have a budget to award security bounties. We can only thank you.
|
||||||
|
|
||||||
|
|||||||
@@ -3,20 +3,5 @@ version: 2
|
|||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
open-pull-requests-limit: 1
|
|
||||||
groups:
|
|
||||||
actions-deps:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
schedule:
|
|
||||||
interval: "monthly"
|
|
||||||
|
|
||||||
- package-ecosystem: "gomod"
|
|
||||||
directory: "/"
|
|
||||||
open-pull-requests-limit: 1
|
|
||||||
groups:
|
|
||||||
all-updates:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
schedule:
|
schedule:
|
||||||
interval: "monthly"
|
interval: "monthly"
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Assistance Disclosure
|
|
||||||
<!--
|
|
||||||
Thank you for contributing! Please note:
|
|
||||||
|
|
||||||
The use of AI/LLM tools is allowed so long as it is disclosed, so
|
|
||||||
that we can provide better code review and maintain project quality.
|
|
||||||
|
|
||||||
If you used AI/LLM tooling in any way related to this PR, please
|
|
||||||
let us know to what extent it was utilized.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
"No AI was used."
|
|
||||||
"I wrote the code, but Claude generated the tests."
|
|
||||||
"I consulted ChatGPT for a solution, but I authored/coded it myself."
|
|
||||||
"Cody generated the code, and I verified it is correct."
|
|
||||||
"Copilot provided tab completion for code and comments."
|
|
||||||
|
|
||||||
We expect that you have vetted your contributions for correctness.
|
|
||||||
Additionally, signing our CLA certifies that you have the rights to
|
|
||||||
contribute this change.
|
|
||||||
|
|
||||||
Replace the text below with your disclosure:
|
|
||||||
-->
|
|
||||||
|
|
||||||
_This PR is missing an assistance disclosure._
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
name: AI Moderator
|
|
||||||
permissions: read-all
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
pull_request_review_comment:
|
|
||||||
types: [created]
|
|
||||||
jobs:
|
|
||||||
spam-detection:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
models: read
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
|
||||||
- uses: github/ai-moderator@6bcdb2a79c2e564db8d76d7d4439d91a044c4eb6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
spam-label: 'spam'
|
|
||||||
ai-label: 'ai-generated'
|
|
||||||
minimize-detected-comments: true
|
|
||||||
# Built-in prompt configuration (all enabled by default)
|
|
||||||
enable-spam-detection: true
|
|
||||||
enable-link-spam-detection: true
|
|
||||||
enable-ai-detection: true
|
|
||||||
# custom-prompt-path: '.github/prompts/my-custom.prompt.yml' # Optional
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
name: Release Proposal Approval Tracker
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_review:
|
|
||||||
types: [submitted, dismissed]
|
|
||||||
pull_request:
|
|
||||||
types: [labeled, unlabeled, synchronize, closed]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-approvals:
|
|
||||||
name: Track Maintainer Approvals
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Only run on PRs with release-proposal label
|
|
||||||
if: contains(github.event.pull_request.labels.*.name, 'release-proposal') && github.event.pull_request.state == 'open'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check approvals and update PR
|
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
||||||
env:
|
|
||||||
MAINTAINER_LOGINS: ${{ secrets.MAINTAINER_LOGINS }}
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const pr = context.payload.pull_request;
|
|
||||||
|
|
||||||
// Extract version from PR title (e.g., "Release Proposal: v1.2.3")
|
|
||||||
const versionMatch = pr.title.match(/Release Proposal:\s*(v[\d.]+(?:-[\w.]+)?)/);
|
|
||||||
const commitMatch = pr.body.match(/\*\*Target Commit:\*\*\s*`([a-f0-9]+)`/);
|
|
||||||
|
|
||||||
if (!versionMatch || !commitMatch) {
|
|
||||||
console.log('Could not extract version from title or commit from body');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const version = versionMatch[1];
|
|
||||||
const targetCommit = commitMatch[1];
|
|
||||||
|
|
||||||
console.log(`Version: ${version}, Target Commit: ${targetCommit}`);
|
|
||||||
|
|
||||||
// Get all reviews
|
|
||||||
const reviews = await github.rest.pulls.listReviews({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
pull_number: pr.number
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get list of maintainers
|
|
||||||
const maintainerLoginsRaw = process.env.MAINTAINER_LOGINS || '';
|
|
||||||
const maintainerLogins = maintainerLoginsRaw
|
|
||||||
.split(/[,;]/)
|
|
||||||
.map(login => login.trim())
|
|
||||||
.filter(login => login.length > 0);
|
|
||||||
|
|
||||||
console.log(`Maintainer logins: ${maintainerLogins.join(', ')}`);
|
|
||||||
|
|
||||||
// Get the latest review from each user
|
|
||||||
const latestReviewsByUser = {};
|
|
||||||
reviews.data.forEach(review => {
|
|
||||||
const username = review.user.login;
|
|
||||||
if (!latestReviewsByUser[username] || new Date(review.submitted_at) > new Date(latestReviewsByUser[username].submitted_at)) {
|
|
||||||
latestReviewsByUser[username] = review;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Count approvals from maintainers
|
|
||||||
const maintainerApprovals = Object.entries(latestReviewsByUser)
|
|
||||||
.filter(([username, review]) =>
|
|
||||||
maintainerLogins.includes(username) &&
|
|
||||||
review.state === 'APPROVED'
|
|
||||||
)
|
|
||||||
.map(([username, review]) => username);
|
|
||||||
|
|
||||||
const approvalCount = maintainerApprovals.length;
|
|
||||||
console.log(`Found ${approvalCount} maintainer approvals from: ${maintainerApprovals.join(', ')}`);
|
|
||||||
|
|
||||||
// Get current labels
|
|
||||||
const currentLabels = pr.labels.map(label => label.name);
|
|
||||||
const hasApprovedLabel = currentLabels.includes('approved');
|
|
||||||
const hasAwaitingApprovalLabel = currentLabels.includes('awaiting-approval');
|
|
||||||
|
|
||||||
if (approvalCount >= 2 && !hasApprovedLabel) {
|
|
||||||
console.log('✅ Quorum reached! Updating PR...');
|
|
||||||
|
|
||||||
// Remove awaiting-approval label if present
|
|
||||||
if (hasAwaitingApprovalLabel) {
|
|
||||||
await github.rest.issues.removeLabel({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: pr.number,
|
|
||||||
name: 'awaiting-approval'
|
|
||||||
}).catch(e => console.log('Label not found:', e.message));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add approved label
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: pr.number,
|
|
||||||
labels: ['approved']
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add comment with tagging instructions
|
|
||||||
const approversList = maintainerApprovals.map(u => `@${u}`).join(', ');
|
|
||||||
const commentBody = [
|
|
||||||
'## ✅ Approval Quorum Reached',
|
|
||||||
'',
|
|
||||||
`This release proposal has been approved by ${approvalCount} maintainers: ${approversList}`,
|
|
||||||
'',
|
|
||||||
'### Tagging Instructions',
|
|
||||||
'',
|
|
||||||
'A maintainer should now create and push the signed tag:',
|
|
||||||
'',
|
|
||||||
'```bash',
|
|
||||||
`git checkout ${targetCommit}`,
|
|
||||||
`git tag -s ${version} -m "Release ${version}"`,
|
|
||||||
`git push origin ${version}`,
|
|
||||||
`git checkout -`,
|
|
||||||
'```',
|
|
||||||
'',
|
|
||||||
'The release workflow will automatically start when the tag is pushed.'
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: pr.number,
|
|
||||||
body: commentBody
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Posted tagging instructions');
|
|
||||||
} else if (approvalCount < 2 && hasApprovedLabel) {
|
|
||||||
console.log('⚠️ Approval count dropped below quorum, removing approved label');
|
|
||||||
|
|
||||||
// Remove approved label
|
|
||||||
await github.rest.issues.removeLabel({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: pr.number,
|
|
||||||
name: 'approved'
|
|
||||||
}).catch(e => console.log('Label not found:', e.message));
|
|
||||||
|
|
||||||
// Add awaiting-approval label
|
|
||||||
if (!hasAwaitingApprovalLabel) {
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: pr.number,
|
|
||||||
labels: ['awaiting-approval']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`⏳ Waiting for more approvals (${approvalCount}/2 required)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
handle-pr-closed:
|
|
||||||
name: Handle PR Closed Without Tag
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: |
|
|
||||||
contains(github.event.pull_request.labels.*.name, 'release-proposal') &&
|
|
||||||
github.event.action == 'closed' && !contains(github.event.pull_request.labels.*.name, 'released')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Add cancelled label and comment
|
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const pr = context.payload.pull_request;
|
|
||||||
|
|
||||||
// Check if the release-in-progress label is present
|
|
||||||
const hasReleaseInProgress = pr.labels.some(label => label.name === 'release-in-progress');
|
|
||||||
|
|
||||||
if (hasReleaseInProgress) {
|
|
||||||
// PR was closed while release was in progress - this is unusual
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: pr.number,
|
|
||||||
body: '⚠️ **Warning:** This PR was closed while a release was in progress. This may indicate an error. Please verify the release status.'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// PR was closed before tag was created - this is normal cancellation
|
|
||||||
const versionMatch = pr.title.match(/Release Proposal:\s*(v[\d.]+(?:-[\w.]+)?)/);
|
|
||||||
const version = versionMatch ? versionMatch[1] : 'unknown';
|
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: pr.number,
|
|
||||||
body: `## 🚫 Release Proposal Cancelled\n\nThis release proposal for ${version} was closed without creating the tag.\n\nIf you want to proceed with this release later, you can create a new release proposal.`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add cancelled label
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: pr.number,
|
|
||||||
labels: ['cancelled']
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove other workflow labels if present
|
|
||||||
const labelsToRemove = ['awaiting-approval', 'approved', 'release-in-progress'];
|
|
||||||
for (const label of labelsToRemove) {
|
|
||||||
try {
|
|
||||||
await github.rest.issues.removeLabel({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: pr.number,
|
|
||||||
name: label
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`Label ${label} not found or already removed`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Added cancelled label and cleaned up workflow labels');
|
|
||||||
|
|
||||||
+22
-90
@@ -12,32 +12,28 @@ on:
|
|||||||
- master
|
- master
|
||||||
- 2.*
|
- 2.*
|
||||||
|
|
||||||
env:
|
|
||||||
GOFLAGS: '-tags=nobadger,nomysql,nopgx'
|
|
||||||
# https://github.com/actions/setup-go/issues/491
|
|
||||||
GOTOOLCHAIN: local
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
strategy:
|
strategy:
|
||||||
# Default is true, cancels jobs for other platforms in the matrix if one fails
|
# Default is true, cancels jobs for other platforms in the matrix if one fails
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os:
|
os:
|
||||||
- linux
|
- linux
|
||||||
- mac
|
- mac
|
||||||
- windows
|
- windows
|
||||||
go:
|
go:
|
||||||
- '1.25'
|
- '1.21'
|
||||||
|
- '1.22'
|
||||||
|
|
||||||
include:
|
include:
|
||||||
# Set the minimum Go patch version for the given Go minor
|
# Set the minimum Go patch version for the given Go minor
|
||||||
# Usable via ${{ matrix.GO_SEMVER }}
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
- go: '1.25'
|
- go: '1.21'
|
||||||
GO_SEMVER: '~1.25.0'
|
GO_SEMVER: '~1.21.0'
|
||||||
|
|
||||||
|
- go: '1.22'
|
||||||
|
GO_SEMVER: '~1.22.3'
|
||||||
|
|
||||||
# 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)
|
||||||
@@ -59,21 +55,13 @@ jobs:
|
|||||||
SUCCESS: 'True'
|
SUCCESS: 'True'
|
||||||
|
|
||||||
runs-on: ${{ matrix.OS_LABEL }}
|
runs-on: ${{ matrix.OS_LABEL }}
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
actions: write # to allow uploading artifacts and cache
|
|
||||||
steps:
|
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.GO_SEMVER }}
|
go-version: ${{ matrix.GO_SEMVER }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -111,7 +99,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
run: |
|
run: |
|
||||||
go build -trimpath -ldflags="-w -s" -v
|
go build -tags nobdger -trimpath -ldflags="-w -s" -v
|
||||||
|
|
||||||
- name: Smoke test Caddy
|
- name: Smoke test Caddy
|
||||||
working-directory: ./cmd/caddy
|
working-directory: ./cmd/caddy
|
||||||
@@ -120,7 +108,7 @@ jobs:
|
|||||||
./caddy stop
|
./caddy stop
|
||||||
|
|
||||||
- name: Publish Build Artifact
|
- name: Publish Build Artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@v4
|
||||||
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 }}
|
||||||
@@ -134,7 +122,7 @@ jobs:
|
|||||||
# continue-on-error: true
|
# continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
|
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
|
||||||
go test -v -coverprofile="cover-profile.out" -short -race ./...
|
go test -tags nobadger -v -coverprofile="cover-profile.out" -short -race ./...
|
||||||
# echo "status=$?" >> $GITHUB_OUTPUT
|
# echo "status=$?" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# Relevant step if we reinvestigate publishing test/coverage reports
|
# Relevant step if we reinvestigate publishing test/coverage reports
|
||||||
@@ -154,58 +142,26 @@ jobs:
|
|||||||
|
|
||||||
s390x-test:
|
s390x-test:
|
||||||
name: test (s390x on IBM Z)
|
name: test (s390x on IBM Z)
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]'
|
if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]'
|
||||||
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)
|
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
allowed-endpoints: ci-s390x.caddyserver.com:22
|
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@v4
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
set +e
|
|
||||||
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
|
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
|
||||||
|
|
||||||
# short sha is enough?
|
# short sha is enough?
|
||||||
short_sha=$(git rev-parse --short HEAD)
|
short_sha=$(git rev-parse --short HEAD)
|
||||||
|
|
||||||
# To shorten the following lines
|
|
||||||
ssh_opts="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
|
||||||
ssh_host="$CI_USER@ci-s390x.caddyserver.com"
|
|
||||||
|
|
||||||
# The environment is fresh, so there's no point in keeping accepting and adding the key.
|
# The environment is fresh, so there's no point in keeping accepting and adding the key.
|
||||||
rsync -arz -e "ssh $ssh_opts" --progress --delete --exclude '.git' . "$ssh_host":/var/tmp/"$short_sha"
|
rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . "$CI_USER"@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
|
||||||
ssh $ssh_opts -t "$ssh_host" bash <<EOF
|
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$CI_USER"@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -tags nobadger -v ./..."
|
||||||
cd /var/tmp/$short_sha
|
|
||||||
go version
|
|
||||||
go env
|
|
||||||
printf "\n\n"
|
|
||||||
retries=3
|
|
||||||
exit_code=0
|
|
||||||
while ((retries > 0)); do
|
|
||||||
CGO_ENABLED=0 go test -p 1 -v ./...
|
|
||||||
exit_code=$?
|
|
||||||
if ((exit_code == 0)); then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
echo "\n\nTest failed: \$exit_code, retrying..."
|
|
||||||
((retries--))
|
|
||||||
done
|
|
||||||
echo "Remote exit code: \$exit_code"
|
|
||||||
exit \$exit_code
|
|
||||||
EOF
|
|
||||||
test_result=$?
|
test_result=$?
|
||||||
|
|
||||||
# There's no need leaving the files around
|
# There's no need leaving the files around
|
||||||
ssh $ssh_opts "$ssh_host" "rm -rf /var/tmp/'$short_sha'"
|
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$CI_USER"@ci-s390x.caddyserver.com "rm -rf /var/tmp/'$short_sha'"
|
||||||
|
|
||||||
echo "Test exit code: $test_result"
|
echo "Test exit code: $test_result"
|
||||||
exit $test_result
|
exit $test_result
|
||||||
@@ -215,35 +171,11 @@ jobs:
|
|||||||
|
|
||||||
goreleaser-check:
|
goreleaser-check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
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)
|
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
- uses: goreleaser/goreleaser-action@v6
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: check
|
args: check
|
||||||
- name: Install Go
|
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
|
||||||
with:
|
|
||||||
go-version: "~1.25"
|
|
||||||
check-latest: true
|
|
||||||
- name: Install xcaddy
|
|
||||||
run: |
|
|
||||||
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
|
|
||||||
xcaddy version
|
|
||||||
- uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
|
||||||
with:
|
|
||||||
version: latest
|
|
||||||
args: build --single-target --snapshot
|
|
||||||
env:
|
|
||||||
TAG: ${{ github.head_ref || github.ref_name }}
|
|
||||||
|
|||||||
@@ -10,21 +10,12 @@ on:
|
|||||||
- master
|
- master
|
||||||
- 2.*
|
- 2.*
|
||||||
|
|
||||||
env:
|
|
||||||
GOFLAGS: '-tags=nobadger,nomysql,nopgx'
|
|
||||||
CGO_ENABLED: '0'
|
|
||||||
# https://github.com/actions/setup-go/issues/491
|
|
||||||
GOTOOLCHAIN: local
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
goos:
|
goos:
|
||||||
- 'aix'
|
- 'aix'
|
||||||
- 'linux'
|
- 'linux'
|
||||||
- 'solaris'
|
- 'solaris'
|
||||||
@@ -35,31 +26,23 @@ jobs:
|
|||||||
- 'windows'
|
- 'windows'
|
||||||
- 'darwin'
|
- 'darwin'
|
||||||
- 'netbsd'
|
- 'netbsd'
|
||||||
go:
|
go:
|
||||||
- '1.25'
|
- '1.22'
|
||||||
|
|
||||||
include:
|
include:
|
||||||
# Set the minimum Go patch version for the given Go minor
|
# Set the minimum Go patch version for the given Go minor
|
||||||
# Usable via ${{ matrix.GO_SEMVER }}
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
- go: '1.25'
|
- go: '1.22'
|
||||||
GO_SEMVER: '~1.25.0'
|
GO_SEMVER: '~1.22.3'
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.GO_SEMVER }}
|
go-version: ${{ matrix.GO_SEMVER }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -76,9 +59,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Build
|
- name: Run Build
|
||||||
env:
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
GOOS: ${{ matrix.goos }}
|
GOOS: ${{ matrix.goos }}
|
||||||
GOARCH: ${{ matrix.goos == 'aix' && 'ppc64' || 'amd64' }}
|
GOARCH: ${{ matrix.goos == 'aix' && 'ppc64' || 'amd64' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
working-directory: ./cmd/caddy
|
working-directory: ./cmd/caddy
|
||||||
run: go build -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null
|
run: |
|
||||||
|
GOOS=$GOOS GOARCH=$GOARCH go build -tags nobadger -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
|
||||||
# https://github.com/actions/setup-go/issues/491
|
|
||||||
GOTOOLCHAIN: local
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# From https://github.com/golangci/golangci-lint-action
|
# From https://github.com/golangci/golangci-lint-action
|
||||||
golangci:
|
golangci:
|
||||||
@@ -44,21 +40,16 @@ jobs:
|
|||||||
runs-on: ${{ matrix.OS_LABEL }}
|
runs-on: ${{ matrix.OS_LABEL }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- uses: actions/checkout@v4
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
go-version: '~1.22.3'
|
||||||
|
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
|
||||||
with:
|
|
||||||
go-version: '~1.25'
|
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
uses: golangci/golangci-lint-action@v6
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: v1.55
|
||||||
|
|
||||||
# Windows times out frequently after about 5m50s if we don't set a longer timeout.
|
# Windows times out frequently after about 5m50s if we don't set a longer timeout.
|
||||||
args: --timeout 10m
|
args: --timeout 10m
|
||||||
@@ -67,39 +58,10 @@ jobs:
|
|||||||
# only-new-issues: true
|
# only-new-issues: true
|
||||||
|
|
||||||
govulncheck:
|
govulncheck:
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
- name: govulncheck
|
- name: govulncheck
|
||||||
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
|
uses: golang/govulncheck-action@v1
|
||||||
with:
|
with:
|
||||||
go-version-input: '~1.25.0'
|
go-version-input: '~1.22.3'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
dependency-review:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
- name: 'Checkout Repository'
|
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
- name: 'Dependency Review'
|
|
||||||
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
|
|
||||||
with:
|
|
||||||
comment-summary-in-pr: on-failure
|
|
||||||
# https://github.com/actions/dependency-review-action/issues/430#issuecomment-1468975566
|
|
||||||
base-ref: ${{ github.event.pull_request.base.sha || 'master' }}
|
|
||||||
head-ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
|
||||||
|
|||||||
@@ -1,249 +0,0 @@
|
|||||||
name: Release Proposal
|
|
||||||
|
|
||||||
# This workflow creates a release proposal as a PR that requires approval from maintainers
|
|
||||||
# Triggered manually by maintainers when ready to prepare a release
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: 'Version to release (e.g., v2.8.0)'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
commit_hash:
|
|
||||||
description: 'Commit hash to release from'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
create-proposal:
|
|
||||||
name: Create Release Proposal
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Trim and validate inputs
|
|
||||||
id: inputs
|
|
||||||
run: |
|
|
||||||
# Trim whitespace from inputs
|
|
||||||
VERSION=$(echo "${{ inputs.version }}" | xargs)
|
|
||||||
COMMIT_HASH=$(echo "${{ inputs.commit_hash }}" | xargs)
|
|
||||||
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "commit_hash=$COMMIT_HASH" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Validate version format
|
|
||||||
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
|
|
||||||
echo "Error: Version must follow semver format (e.g., v2.8.0 or v2.8.0-beta.1)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Validate commit hash format
|
|
||||||
if [[ ! "$COMMIT_HASH" =~ ^[a-f0-9]{7,40}$ ]]; then
|
|
||||||
echo "Error: Commit hash must be a valid SHA (7-40 characters)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if commit exists
|
|
||||||
if ! git cat-file -e "$COMMIT_HASH"; then
|
|
||||||
echo "Error: Commit $COMMIT_HASH does not exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Check if tag already exists
|
|
||||||
run: |
|
|
||||||
if git rev-parse "${{ steps.inputs.outputs.version }}" >/dev/null 2>&1; then
|
|
||||||
echo "Error: Tag ${{ steps.inputs.outputs.version }} already exists"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Check for existing proposal PR
|
|
||||||
id: check_existing
|
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const version = '${{ steps.inputs.outputs.version }}';
|
|
||||||
|
|
||||||
// Search for existing open PRs with release-proposal label that match this version
|
|
||||||
const openPRs = await github.rest.pulls.list({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
state: 'open',
|
|
||||||
sort: 'updated',
|
|
||||||
direction: 'desc'
|
|
||||||
});
|
|
||||||
|
|
||||||
const existingOpenPR = openPRs.data.find(pr =>
|
|
||||||
pr.title.includes(version) &&
|
|
||||||
pr.labels.some(label => label.name === 'release-proposal')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingOpenPR) {
|
|
||||||
const hasReleased = existingOpenPR.labels.some(label => label.name === 'released');
|
|
||||||
const hasReleaseInProgress = existingOpenPR.labels.some(label => label.name === 'release-in-progress');
|
|
||||||
|
|
||||||
if (hasReleased || hasReleaseInProgress) {
|
|
||||||
core.setFailed(`A release for ${version} is already in progress or completed: ${existingOpenPR.html_url}`);
|
|
||||||
} else {
|
|
||||||
core.setFailed(`An open release proposal already exists for ${version}: ${existingOpenPR.html_url}\n\nPlease use the existing PR or close it first.`);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for closed PRs with this version that were cancelled
|
|
||||||
const closedPRs = await github.rest.pulls.list({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
state: 'closed',
|
|
||||||
sort: 'updated',
|
|
||||||
direction: 'desc'
|
|
||||||
});
|
|
||||||
|
|
||||||
const cancelledPR = closedPRs.data.find(pr =>
|
|
||||||
pr.title.includes(version) &&
|
|
||||||
pr.labels.some(label => label.name === 'release-proposal') &&
|
|
||||||
pr.labels.some(label => label.name === 'cancelled')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (cancelledPR) {
|
|
||||||
console.log(`Found previously cancelled proposal for ${version}: ${cancelledPR.html_url}`);
|
|
||||||
console.log('Creating new proposal to replace cancelled one...');
|
|
||||||
} else {
|
|
||||||
console.log(`No existing proposal found for ${version}, proceeding...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Generate changelog and create branch
|
|
||||||
id: setup
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.inputs.outputs.version }}"
|
|
||||||
COMMIT_HASH="${{ steps.inputs.outputs.commit_hash }}"
|
|
||||||
|
|
||||||
# Create a new branch for the release proposal
|
|
||||||
BRANCH_NAME="release_proposal-$VERSION"
|
|
||||||
git checkout -b "$BRANCH_NAME"
|
|
||||||
|
|
||||||
# Calculate how many commits behind HEAD
|
|
||||||
COMMITS_BEHIND=$(git rev-list --count ${COMMIT_HASH}..HEAD)
|
|
||||||
|
|
||||||
if [ "$COMMITS_BEHIND" -eq 0 ]; then
|
|
||||||
BEHIND_INFO="This is the latest commit (HEAD)"
|
|
||||||
else
|
|
||||||
BEHIND_INFO="This commit is **${COMMITS_BEHIND} commits behind HEAD**"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "commits_behind=$COMMITS_BEHIND" >> $GITHUB_OUTPUT
|
|
||||||
echo "behind_info=$BEHIND_INFO" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Get the last tag
|
|
||||||
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
if [ -z "$LAST_TAG" ]; then
|
|
||||||
echo "No previous tag found, generating full changelog"
|
|
||||||
COMMITS=$(git log --pretty=format:"- %s (%h)" --reverse "$COMMIT_HASH")
|
|
||||||
else
|
|
||||||
echo "Generating changelog since $LAST_TAG"
|
|
||||||
COMMITS=$(git log --pretty=format:"- %s (%h)" --reverse "${LAST_TAG}..$COMMIT_HASH")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Store changelog for PR body
|
|
||||||
CLEANSED_COMMITS=$(echo "$COMMITS" | sed 's/`/\\`/g')
|
|
||||||
echo "changelog<<EOF" >> $GITHUB_OUTPUT
|
|
||||||
echo "$CLEANSED_COMMITS" >> $GITHUB_OUTPUT
|
|
||||||
echo "EOF" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Create empty commit for the PR
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git commit --allow-empty -m "Release proposal for $VERSION"
|
|
||||||
|
|
||||||
# Push the branch
|
|
||||||
git push origin "$BRANCH_NAME"
|
|
||||||
|
|
||||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Create release proposal PR
|
|
||||||
id: create_pr
|
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const changelog = `${{ steps.setup.outputs.changelog }}`;
|
|
||||||
|
|
||||||
const pr = await github.rest.pulls.create({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
title: `Release Proposal: ${{ steps.inputs.outputs.version }}`,
|
|
||||||
head: '${{ steps.setup.outputs.branch_name }}',
|
|
||||||
base: 'master',
|
|
||||||
body: `## Release Proposal: ${{ steps.inputs.outputs.version }}
|
|
||||||
|
|
||||||
**Target Commit:** \`${{ steps.inputs.outputs.commit_hash }}\`
|
|
||||||
**Requested by:** @${{ github.actor }}
|
|
||||||
**Commit Status:** ${{ steps.setup.outputs.behind_info }}
|
|
||||||
|
|
||||||
This PR proposes creating release tag \`${{ steps.inputs.outputs.version }}\` at commit \`${{ steps.inputs.outputs.commit_hash }}\`.
|
|
||||||
|
|
||||||
### Approval Process
|
|
||||||
|
|
||||||
This PR requires **approval from 2+ maintainers** before the tag can be created.
|
|
||||||
|
|
||||||
### What happens next?
|
|
||||||
|
|
||||||
1. Maintainers review this proposal
|
|
||||||
2. When 2+ maintainer approvals are received, an automated workflow will post tagging instructions
|
|
||||||
3. A maintainer manually creates and pushes the signed tag
|
|
||||||
4. The release workflow is triggered automatically by the tag push
|
|
||||||
5. Upon release completion, this PR is closed and the branch is deleted
|
|
||||||
|
|
||||||
### Changes Since Last Release
|
|
||||||
|
|
||||||
${changelog}
|
|
||||||
|
|
||||||
### Release Checklist
|
|
||||||
|
|
||||||
- [ ] All tests pass
|
|
||||||
- [ ] Security review completed
|
|
||||||
- [ ] Documentation updated
|
|
||||||
- [ ] Breaking changes documented
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Note:** Tag creation is manual and requires a signed tag from a maintainer.`,
|
|
||||||
draft: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add labels
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: pr.data.number,
|
|
||||||
labels: ['release-proposal', 'awaiting-approval']
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Created PR: ${pr.data.html_url}`);
|
|
||||||
|
|
||||||
return { number: pr.data.number, url: pr.data.html_url };
|
|
||||||
result-encoding: json
|
|
||||||
|
|
||||||
- name: Post summary
|
|
||||||
run: |
|
|
||||||
echo "## Release Proposal PR Created! 🚀" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Version: **${{ steps.inputs.outputs.version }}**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Commit: **${{ steps.inputs.outputs.commit_hash }}**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Status: ${{ steps.setup.outputs.behind_info }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "PR: ${{ fromJson(steps.create_pr.outputs.result).url }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
+19
-410
@@ -5,342 +5,21 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- 'v*.*.*'
|
- 'v*.*.*'
|
||||||
|
|
||||||
env:
|
|
||||||
# https://github.com/actions/setup-go/issues/491
|
|
||||||
GOTOOLCHAIN: local
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
verify-tag:
|
|
||||||
name: Verify Tag Signature and Approvals
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
outputs:
|
|
||||||
verification_passed: ${{ steps.verify.outputs.passed }}
|
|
||||||
tag_version: ${{ steps.info.outputs.version }}
|
|
||||||
proposal_issue_number: ${{ steps.find_proposal.outputs.result && fromJson(steps.find_proposal.outputs.result).number || '' }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
# Force fetch upstream tags -- because 65 minutes
|
|
||||||
# tl;dr: actions/checkout@v3 runs this line:
|
|
||||||
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
|
|
||||||
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
|
|
||||||
# git fetch --prune --unshallow
|
|
||||||
# which doesn't overwrite that tag because that would be destructive.
|
|
||||||
# Credit to @francislavoie for the investigation.
|
|
||||||
# https://github.com/actions/checkout/issues/290#issuecomment-680260080
|
|
||||||
- name: Force fetch upstream tags
|
|
||||||
run: git fetch --tags --force
|
|
||||||
|
|
||||||
- name: Get tag info
|
|
||||||
id: info
|
|
||||||
run: |
|
|
||||||
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
|
||||||
echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
|
|
||||||
- name: Print Go version and environment
|
|
||||||
id: vars
|
|
||||||
run: |
|
|
||||||
printf "Using go at: $(which go)\n"
|
|
||||||
printf "Go version: $(go version)\n"
|
|
||||||
printf "\n\nGo environment:\n\n"
|
|
||||||
go env
|
|
||||||
printf "\n\nSystem environment:\n\n"
|
|
||||||
env
|
|
||||||
echo "version_tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
|
|
||||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Add "pip install" CLI tools to PATH
|
|
||||||
echo ~/.local/bin >> $GITHUB_PATH
|
|
||||||
|
|
||||||
# Parse semver
|
|
||||||
TAG=${GITHUB_REF/refs\/tags\//}
|
|
||||||
SEMVER_RE='[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z\.-]*\)'
|
|
||||||
TAG_MAJOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\1#"`
|
|
||||||
TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"`
|
|
||||||
TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"`
|
|
||||||
TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"`
|
|
||||||
echo "tag_major=${TAG_MAJOR}" >> $GITHUB_OUTPUT
|
|
||||||
echo "tag_minor=${TAG_MINOR}" >> $GITHUB_OUTPUT
|
|
||||||
echo "tag_patch=${TAG_PATCH}" >> $GITHUB_OUTPUT
|
|
||||||
echo "tag_special=${TAG_SPECIAL}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Validate commits and tag signatures
|
|
||||||
id: verify
|
|
||||||
env:
|
|
||||||
signing_keys: ${{ secrets.SIGNING_KEYS }}
|
|
||||||
run: |
|
|
||||||
# Read the string into an array, splitting by IFS
|
|
||||||
IFS=";" read -ra keys_collection <<< "$signing_keys"
|
|
||||||
|
|
||||||
# ref: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#example-usage-of-the-runner-context
|
|
||||||
touch "${{ runner.temp }}/allowed_signers"
|
|
||||||
|
|
||||||
# Iterate and print the split elements
|
|
||||||
for item in "${keys_collection[@]}"; do
|
|
||||||
|
|
||||||
# trim leading whitespaces
|
|
||||||
item="${item##*( )}"
|
|
||||||
|
|
||||||
# trim trailing whitespaces
|
|
||||||
item="${item%%*( )}"
|
|
||||||
|
|
||||||
IFS=" " read -ra key_components <<< "$item"
|
|
||||||
# git wants it in format: email address, type, public key
|
|
||||||
# ssh has it in format: type, public key, email address
|
|
||||||
echo "${key_components[2]} namespaces=\"git\" ${key_components[0]} ${key_components[1]}" >> "${{ runner.temp }}/allowed_signers"
|
|
||||||
done
|
|
||||||
|
|
||||||
git config set --global gpg.ssh.allowedSignersFile "${{ runner.temp }}/allowed_signers"
|
|
||||||
|
|
||||||
echo "Verifying the tag: ${{ steps.vars.outputs.version_tag }}"
|
|
||||||
|
|
||||||
# Verify the tag is signed
|
|
||||||
if ! git verify-tag -v "${{ steps.vars.outputs.version_tag }}" 2>&1; then
|
|
||||||
echo "❌ Tag verification failed!"
|
|
||||||
echo "passed=false" >> $GITHUB_OUTPUT
|
|
||||||
git push --delete origin "${{ steps.vars.outputs.version_tag }}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# Run it again to capture the output
|
|
||||||
git verify-tag -v "${{ steps.vars.outputs.version_tag }}" 2>&1 | tee /tmp/verify-output.txt;
|
|
||||||
|
|
||||||
# SSH verification output typically includes the key fingerprint
|
|
||||||
# Use GNU grep with Perl regex for cleaner extraction (Linux environment)
|
|
||||||
KEY_SHA256=$(grep -oP "SHA256:[\"']?\K[A-Za-z0-9+/=]+(?=[\"']?)" /tmp/verify-output.txt | head -1 || echo "")
|
|
||||||
|
|
||||||
if [ -z "$KEY_SHA256" ]; then
|
|
||||||
# Try alternative pattern with "key" prefix
|
|
||||||
KEY_SHA256=$(grep -oP "key SHA256:[\"']?\K[A-Za-z0-9+/=]+(?=[\"']?)" /tmp/verify-output.txt | head -1 || echo "")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$KEY_SHA256" ]; then
|
|
||||||
# Fallback: extract any base64-like string (40+ chars)
|
|
||||||
KEY_SHA256=$(grep -oP '[A-Za-z0-9+/]{40,}=?' /tmp/verify-output.txt | head -1 || echo "")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$KEY_SHA256" ]; then
|
|
||||||
echo "Somehow could not extract SSH key fingerprint from git verify-tag output"
|
|
||||||
echo "Cancelling flow and deleting tag"
|
|
||||||
echo "passed=false" >> $GITHUB_OUTPUT
|
|
||||||
git push --delete origin "${{ steps.vars.outputs.version_tag }}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Tag verification succeeded!"
|
|
||||||
echo "passed=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "key_id=$KEY_SHA256" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Find related release proposal
|
|
||||||
id: find_proposal
|
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const version = '${{ steps.vars.outputs.version_tag }}';
|
|
||||||
|
|
||||||
// Search for PRs with release-proposal label that match this version
|
|
||||||
const prs = await github.rest.pulls.list({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
state: 'open', // Changed to 'all' to find both open and closed PRs
|
|
||||||
sort: 'updated',
|
|
||||||
direction: 'desc'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find the most recent PR for this version
|
|
||||||
const proposal = prs.data.find(pr =>
|
|
||||||
pr.title.includes(version) &&
|
|
||||||
pr.labels.some(label => label.name === 'release-proposal')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!proposal) {
|
|
||||||
console.log(`⚠️ No release proposal PR found for ${version}`);
|
|
||||||
console.log('This might be a hotfix or emergency release');
|
|
||||||
return { number: null, approved: true, approvals: 0, proposedCommit: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Found proposal PR #${proposal.number} for version ${version}`);
|
|
||||||
|
|
||||||
// Extract commit hash from PR body
|
|
||||||
const commitMatch = proposal.body.match(/\*\*Target Commit:\*\*\s*`([a-f0-9]+)`/);
|
|
||||||
const proposedCommit = commitMatch ? commitMatch[1] : null;
|
|
||||||
|
|
||||||
if (proposedCommit) {
|
|
||||||
console.log(`Proposal was for commit: ${proposedCommit}`);
|
|
||||||
} else {
|
|
||||||
console.log('⚠️ No target commit hash found in PR body');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get PR reviews to extract approvers
|
|
||||||
let approvers = 'Validated by automation';
|
|
||||||
let approvalCount = 2; // Minimum required
|
|
||||||
|
|
||||||
try {
|
|
||||||
const reviews = await github.rest.pulls.listReviews({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
pull_number: proposal.number
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get latest review per user and filter for approvals
|
|
||||||
const latestReviewsByUser = {};
|
|
||||||
reviews.data.forEach(review => {
|
|
||||||
const username = review.user.login;
|
|
||||||
if (!latestReviewsByUser[username] || new Date(review.submitted_at) > new Date(latestReviewsByUser[username].submitted_at)) {
|
|
||||||
latestReviewsByUser[username] = review;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const approvalReviews = Object.values(latestReviewsByUser).filter(review =>
|
|
||||||
review.state === 'APPROVED'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (approvalReviews.length > 0) {
|
|
||||||
approvers = approvalReviews.map(r => '@' + r.user.login).join(', ');
|
|
||||||
approvalCount = approvalReviews.length;
|
|
||||||
console.log(`Found ${approvalCount} approvals from: ${approvers}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Could not fetch reviews: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
number: proposal.number,
|
|
||||||
approved: true,
|
|
||||||
approvals: approvalCount,
|
|
||||||
approvers: approvers,
|
|
||||||
proposedCommit: proposedCommit
|
|
||||||
};
|
|
||||||
result-encoding: json
|
|
||||||
|
|
||||||
- name: Verify proposal commit
|
|
||||||
run: |
|
|
||||||
APPROVALS='${{ steps.find_proposal.outputs.result }}'
|
|
||||||
|
|
||||||
# Parse JSON
|
|
||||||
PROPOSED_COMMIT=$(echo "$APPROVALS" | jq -r '.proposedCommit')
|
|
||||||
CURRENT_COMMIT="${{ steps.info.outputs.sha }}"
|
|
||||||
|
|
||||||
echo "Proposed commit: $PROPOSED_COMMIT"
|
|
||||||
echo "Current commit: $CURRENT_COMMIT"
|
|
||||||
|
|
||||||
# Check if commits match (if proposal had a target commit)
|
|
||||||
if [ "$PROPOSED_COMMIT" != "null" ] && [ -n "$PROPOSED_COMMIT" ]; then
|
|
||||||
# Normalize both commits to full SHA for comparison
|
|
||||||
PROPOSED_FULL=$(git rev-parse "$PROPOSED_COMMIT" 2>/dev/null || echo "")
|
|
||||||
CURRENT_FULL=$(git rev-parse "$CURRENT_COMMIT" 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
if [ -z "$PROPOSED_FULL" ]; then
|
|
||||||
echo "⚠️ Could not resolve proposed commit: $PROPOSED_COMMIT"
|
|
||||||
elif [ "$PROPOSED_FULL" != "$CURRENT_FULL" ]; then
|
|
||||||
echo "❌ Commit mismatch!"
|
|
||||||
echo "The tag points to commit $CURRENT_FULL but the proposal was for $PROPOSED_FULL"
|
|
||||||
echo "This indicates an error in tag creation."
|
|
||||||
# Delete the tag remotely
|
|
||||||
git push --delete origin "${{ steps.vars.outputs.version_tag }}"
|
|
||||||
echo "Tag ${{steps.vars.outputs.version_tag}} has been deleted"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "✅ Commit hash matches proposal"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "⚠️ No target commit found in proposal (might be legacy release)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Tag verification completed"
|
|
||||||
|
|
||||||
- name: Update release proposal PR
|
|
||||||
if: fromJson(steps.find_proposal.outputs.result).number != null
|
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const result = ${{ steps.find_proposal.outputs.result }};
|
|
||||||
|
|
||||||
if (result.number) {
|
|
||||||
// Add in-progress label
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: result.number,
|
|
||||||
labels: ['release-in-progress']
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove approved label if present
|
|
||||||
try {
|
|
||||||
await github.rest.issues.removeLabel({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: result.number,
|
|
||||||
name: 'approved'
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Approved label not found:', e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const commentBody = [
|
|
||||||
'## 🚀 Release Workflow Started',
|
|
||||||
'',
|
|
||||||
'- **Tag:** ${{ steps.info.outputs.version }}',
|
|
||||||
'- **Signed by key:** ${{ steps.verify.outputs.key_id }}',
|
|
||||||
'- **Commit:** ${{ steps.info.outputs.sha }}',
|
|
||||||
'- **Approved by:** ' + result.approvers,
|
|
||||||
'',
|
|
||||||
'Release workflow is now running. This PR will be updated when the release is published.'
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: result.number,
|
|
||||||
body: commentBody
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
run: |
|
|
||||||
APPROVALS='${{ steps.find_proposal.outputs.result }}'
|
|
||||||
PROPOSED_COMMIT=$(echo "$APPROVALS" | jq -r '.proposedCommit // "N/A"')
|
|
||||||
APPROVERS=$(echo "$APPROVALS" | jq -r '.approvers // "N/A"')
|
|
||||||
|
|
||||||
echo "## Tag Verification Summary 🔐" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Tag:** ${{ steps.info.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Commit:** ${{ steps.info.outputs.sha }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Proposed Commit:** $PROPOSED_COMMIT" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Signature:** ✅ Verified" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Signed by:** ${{ steps.verify.outputs.key_id }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Approvals:** ✅ Sufficient" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Approved by:** $APPROVERS" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Proceeding with release build..." >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Release
|
name: Release
|
||||||
needs: verify-tag
|
|
||||||
if: ${{ needs.verify-tag.outputs.verification_passed == 'true' }}
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os:
|
os:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
go:
|
go:
|
||||||
- '1.25'
|
- '1.22'
|
||||||
|
|
||||||
include:
|
include:
|
||||||
# Set the minimum Go patch version for the given Go minor
|
# Set the minimum Go patch version for the given Go minor
|
||||||
# Usable via ${{ matrix.GO_SEMVER }}
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
- go: '1.25'
|
- go: '1.22'
|
||||||
GO_SEMVER: '~1.25.0'
|
GO_SEMVER: '~1.22.3'
|
||||||
|
|
||||||
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 +29,21 @@ 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)
|
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@v5
|
||||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2 runs this line:
|
# tl;dr: actions/checkout@v4 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,21 +86,27 @@ 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@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@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # main
|
uses: anchore/sbom-action/download-syft@main
|
||||||
- name: Syft version
|
- name: Syft version
|
||||||
run: syft version
|
run: syft version
|
||||||
- name: Install xcaddy
|
|
||||||
run: |
|
|
||||||
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
|
|
||||||
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@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
uses: goreleaser/goreleaser-action@v6
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --clean --timeout 60m
|
args: release --clean --timeout 60m
|
||||||
@@ -494,72 +172,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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ on:
|
|||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Release Published
|
name: Release Published
|
||||||
@@ -16,20 +13,12 @@ jobs:
|
|||||||
os:
|
os:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
actions: write
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
# See https://github.com/peter-evans/repository-dispatch
|
# See https://github.com/peter-evans/repository-dispatch
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
- name: Trigger event on caddyserver/dist
|
- name: Trigger event on caddyserver/dist
|
||||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
uses: peter-evans/repository-dispatch@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||||
repository: caddyserver/dist
|
repository: caddyserver/dist
|
||||||
@@ -37,7 +26,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@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
uses: peter-evans/repository-dispatch@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||||
repository: caddyserver/caddy-docker
|
repository: caddyserver/caddy-docker
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
# This workflow uses actions that are not certified by GitHub. They are provided
|
|
||||||
# by a third-party and are governed by separate terms of service, privacy
|
|
||||||
# policy, and support documentation.
|
|
||||||
|
|
||||||
name: OpenSSF Scorecard supply-chain security
|
|
||||||
on:
|
|
||||||
# For Branch-Protection check. Only the default branch is supported. See
|
|
||||||
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
|
|
||||||
branch_protection_rule:
|
|
||||||
# To guarantee Maintained check is occasionally updated. See
|
|
||||||
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
|
|
||||||
schedule:
|
|
||||||
- cron: '20 2 * * 5'
|
|
||||||
push:
|
|
||||||
branches: [ "master", "2.*" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "master", "2.*" ]
|
|
||||||
|
|
||||||
|
|
||||||
# Declare default permissions as read only.
|
|
||||||
permissions: read-all
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analysis:
|
|
||||||
name: Scorecard analysis
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# `publish_results: true` only works when run from the default branch. conditional can be removed if disabled.
|
|
||||||
if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request'
|
|
||||||
permissions:
|
|
||||||
# Needed to upload the results to code-scanning dashboard.
|
|
||||||
security-events: write
|
|
||||||
# Needed to publish results and get a badge (see publish_results below).
|
|
||||||
id-token: write
|
|
||||||
# Uncomment the permissions below if installing in a private repository.
|
|
||||||
# contents: read
|
|
||||||
# actions: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
- name: "Checkout code"
|
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: "Run analysis"
|
|
||||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
|
||||||
with:
|
|
||||||
results_file: results.sarif
|
|
||||||
results_format: sarif
|
|
||||||
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
|
|
||||||
# - you want to enable the Branch-Protection check on a *public* repository, or
|
|
||||||
# - you are installing Scorecard on a *private* repository
|
|
||||||
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional.
|
|
||||||
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
|
|
||||||
|
|
||||||
# Public repositories:
|
|
||||||
# - Publish results to OpenSSF REST API for easy access by consumers
|
|
||||||
# - Allows the repository to include the Scorecard badge.
|
|
||||||
# - See https://github.com/ossf/scorecard-action#publishing-results.
|
|
||||||
# For private repositories:
|
|
||||||
# - `publish_results` will always be set to `false`, regardless
|
|
||||||
# of the value entered here.
|
|
||||||
publish_results: true
|
|
||||||
|
|
||||||
# (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore
|
|
||||||
# file_mode: git
|
|
||||||
|
|
||||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
|
||||||
# format to the repository Actions tab.
|
|
||||||
- name: "Upload artifact"
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
||||||
with:
|
|
||||||
name: SARIF file
|
|
||||||
path: results.sarif
|
|
||||||
retention-days: 5
|
|
||||||
|
|
||||||
# Upload the results to GitHub's code scanning dashboard (optional).
|
|
||||||
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
|
|
||||||
- name: "Upload to code-scanning"
|
|
||||||
uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5
|
|
||||||
with:
|
|
||||||
sarif_file: results.sarif
|
|
||||||
+137
-91
@@ -1,19 +1,25 @@
|
|||||||
version: "2"
|
linters-settings:
|
||||||
run:
|
errcheck:
|
||||||
issues-exit-code: 1
|
ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.*
|
||||||
tests: false
|
ignoretests: true
|
||||||
build-tags:
|
gci:
|
||||||
- nobadger
|
sections:
|
||||||
- nomysql
|
- standard # Standard section: captures all standard packages.
|
||||||
- nopgx
|
- default # Default section: contains all imports that could not be matched to another section type.
|
||||||
output:
|
- prefix(github.com/caddyserver/caddy/v2/cmd) # ensure that this is always at the top and always has a line break.
|
||||||
formats:
|
- prefix(github.com/caddyserver/caddy) # Custom section: groups all imports with the specified Prefix.
|
||||||
text:
|
# Skip generated files.
|
||||||
path: stdout
|
# Default: true
|
||||||
print-linter-name: true
|
skip-generated: true
|
||||||
print-issued-lines: true
|
# Enable custom order of sections.
|
||||||
|
# If `true`, make the section order the same as the order of `sections`.
|
||||||
|
# Default: false
|
||||||
|
custom-order: true
|
||||||
|
exhaustive:
|
||||||
|
ignore-enum-types: reflect.Kind|svc.Cmd
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
default: none
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
- asasalint
|
- asasalint
|
||||||
- asciicheck
|
- asciicheck
|
||||||
@@ -27,96 +33,136 @@ linters:
|
|||||||
- errcheck
|
- errcheck
|
||||||
- errname
|
- errname
|
||||||
- exhaustive
|
- exhaustive
|
||||||
|
- exportloopref
|
||||||
|
- gci
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- gofumpt
|
||||||
- gosec
|
- gosec
|
||||||
|
- gosimple
|
||||||
- govet
|
- govet
|
||||||
- importas
|
|
||||||
- ineffassign
|
- ineffassign
|
||||||
|
- importas
|
||||||
- misspell
|
- misspell
|
||||||
- prealloc
|
- prealloc
|
||||||
- promlinter
|
- promlinter
|
||||||
- sloglint
|
- sloglint
|
||||||
- sqlclosecheck
|
- sqlclosecheck
|
||||||
- staticcheck
|
- staticcheck
|
||||||
|
- tenv
|
||||||
- testableexamples
|
- testableexamples
|
||||||
- testifylint
|
- testifylint
|
||||||
- tparallel
|
- tparallel
|
||||||
|
- typecheck
|
||||||
- unconvert
|
- unconvert
|
||||||
- unused
|
- unused
|
||||||
- wastedassign
|
- wastedassign
|
||||||
- whitespace
|
- whitespace
|
||||||
- zerologlint
|
- zerologlint
|
||||||
settings:
|
# these are implicitly disabled:
|
||||||
staticcheck:
|
# - containedctx
|
||||||
checks: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-QF1006", "-QF1008"] # default, and exclude 1 more undesired check
|
# - contextcheck
|
||||||
errcheck:
|
# - cyclop
|
||||||
exclude-functions:
|
# - depguard
|
||||||
- fmt.*
|
# - errchkjson
|
||||||
- (go.uber.org/zap/zapcore.ObjectEncoder).AddObject
|
# - errorlint
|
||||||
- (go.uber.org/zap/zapcore.ObjectEncoder).AddArray
|
# - exhaustruct
|
||||||
exhaustive:
|
# - execinquery
|
||||||
ignore-enum-types: reflect.Kind|svc.Cmd
|
# - exhaustruct
|
||||||
exclusions:
|
# - forbidigo
|
||||||
generated: lax
|
# - forcetypeassert
|
||||||
presets:
|
# - funlen
|
||||||
- comments
|
# - ginkgolinter
|
||||||
- common-false-positives
|
# - gocheckcompilerdirectives
|
||||||
- legacy
|
# - gochecknoglobals
|
||||||
- std-error-handling
|
# - gochecknoinits
|
||||||
rules:
|
# - gochecksumtype
|
||||||
- linters:
|
# - gocognit
|
||||||
- gosec
|
# - goconst
|
||||||
text: G115 # TODO: Either we should fix the issues or nuke the linter if it's bad
|
# - gocritic
|
||||||
- linters:
|
# - gocyclo
|
||||||
- gosec
|
# - godot
|
||||||
text: G107 # we aren't calling unknown URL
|
# - godox
|
||||||
- linters:
|
# - goerr113
|
||||||
- gosec
|
# - goheader
|
||||||
text: G203 # as a web server that's expected to handle any template, this is totally in the hands of the user.
|
# - gomnd
|
||||||
- linters:
|
# - gomoddirectives
|
||||||
- gosec
|
# - gomodguard
|
||||||
text: G204 # we're shelling out to known commands, not relying on user-defined input.
|
# - goprintffuncname
|
||||||
- linters:
|
# - gosmopolitan
|
||||||
- gosec
|
# - grouper
|
||||||
# the choice of weakrand is deliberate, hence the named import "weakrand"
|
# - inamedparam
|
||||||
path: modules/caddyhttp/reverseproxy/selectionpolicies.go
|
# - interfacebloat
|
||||||
text: G404
|
# - ireturn
|
||||||
- linters:
|
# - lll
|
||||||
- gosec
|
# - loggercheck
|
||||||
path: modules/caddyhttp/reverseproxy/streaming.go
|
# - maintidx
|
||||||
text: G404
|
# - makezero
|
||||||
- linters:
|
# - mirror
|
||||||
- dupl
|
# - musttag
|
||||||
path: modules/logging/filters.go
|
# - nakedret
|
||||||
- linters:
|
# - nestif
|
||||||
- dupl
|
# - nilerr
|
||||||
path: modules/caddyhttp/matchers.go
|
# - nilnil
|
||||||
- linters:
|
# - nlreturn
|
||||||
- dupl
|
# - noctx
|
||||||
path: modules/caddyhttp/vars.go
|
# - nolintlint
|
||||||
- linters:
|
# - nonamedreturns
|
||||||
- errcheck
|
# - nosprintfhostport
|
||||||
path: _test\.go
|
# - paralleltest
|
||||||
paths:
|
# - perfsprint
|
||||||
- third_party$
|
# - predeclared
|
||||||
- builtin$
|
# - protogetter
|
||||||
- examples$
|
# - reassign
|
||||||
formatters:
|
# - revive
|
||||||
enable:
|
# - rowserrcheck
|
||||||
- gci
|
# - stylecheck
|
||||||
- gofmt
|
# - tagalign
|
||||||
- gofumpt
|
# - tagliatelle
|
||||||
- goimports
|
# - testpackage
|
||||||
settings:
|
# - thelper
|
||||||
gci:
|
# - unparam
|
||||||
sections:
|
# - usestdlibvars
|
||||||
- standard # Standard section: captures all standard packages.
|
# - varnamelen
|
||||||
- default # Default section: contains all imports that could not be matched to another section type.
|
# - wrapcheck
|
||||||
- prefix(github.com/caddyserver/caddy/v2/cmd) # ensure that this is always at the top and always has a line break.
|
# - wsl
|
||||||
- prefix(github.com/caddyserver/caddy) # Custom section: groups all imports with the specified Prefix.
|
|
||||||
custom-order: true
|
run:
|
||||||
exclusions:
|
# default concurrency is a available CPU number.
|
||||||
generated: lax
|
# concurrency: 4 # explicitly omit this value to fully utilize available resources.
|
||||||
paths:
|
deadline: 5m
|
||||||
- third_party$
|
issues-exit-code: 1
|
||||||
- builtin$
|
tests: false
|
||||||
- examples$
|
|
||||||
|
# output configuration options
|
||||||
|
output:
|
||||||
|
format: 'colored-line-number'
|
||||||
|
print-issued-lines: true
|
||||||
|
print-linter-name: true
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-rules:
|
||||||
|
# we aren't calling unknown URL
|
||||||
|
- text: 'G107' # G107: Url provided to HTTP request as taint input
|
||||||
|
linters:
|
||||||
|
- gosec
|
||||||
|
# as a web server that's expected to handle any template, this is totally in the hands of the user.
|
||||||
|
- text: 'G203' # G203: Use of unescaped data in HTML templates
|
||||||
|
linters:
|
||||||
|
- gosec
|
||||||
|
# we're shelling out to known commands, not relying on user-defined input.
|
||||||
|
- text: 'G204' # G204: Audit use of command execution
|
||||||
|
linters:
|
||||||
|
- gosec
|
||||||
|
# the choice of weakrand is deliberate, hence the named import "weakrand"
|
||||||
|
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go
|
||||||
|
text: 'G404' # G404: Insecure random number source (rand)
|
||||||
|
linters:
|
||||||
|
- gosec
|
||||||
|
- path: modules/caddyhttp/reverseproxy/streaming.go
|
||||||
|
text: 'G404' # G404: Insecure random number source (rand)
|
||||||
|
linters:
|
||||||
|
- gosec
|
||||||
|
- path: modules/logging/filters.go
|
||||||
|
linters:
|
||||||
|
- dupl
|
||||||
|
|||||||
+2
-9
@@ -12,9 +12,6 @@ before:
|
|||||||
- mkdir -p caddy-build
|
- mkdir -p caddy-build
|
||||||
- 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
|
|
||||||
- /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'
|
|
||||||
# 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
|
||||||
- go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod
|
- go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod
|
||||||
@@ -34,6 +31,7 @@ builds:
|
|||||||
- env:
|
- env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
- GO111MODULE=on
|
- GO111MODULE=on
|
||||||
|
main: main.go
|
||||||
dir: ./caddy-build
|
dir: ./caddy-build
|
||||||
binary: caddy
|
binary: caddy
|
||||||
goos:
|
goos:
|
||||||
@@ -83,8 +81,6 @@ builds:
|
|||||||
- -s -w
|
- -s -w
|
||||||
tags:
|
tags:
|
||||||
- nobadger
|
- nobadger
|
||||||
- nomysql
|
|
||||||
- nopgx
|
|
||||||
|
|
||||||
signs:
|
signs:
|
||||||
- cmd: cosign
|
- cmd: cosign
|
||||||
@@ -111,7 +107,7 @@ archives:
|
|||||||
- id: default
|
- id: default
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
formats: zip
|
format: zip
|
||||||
name_template: >-
|
name_template: >-
|
||||||
{{ .ProjectName }}_
|
{{ .ProjectName }}_
|
||||||
{{- .Version }}_
|
{{- .Version }}_
|
||||||
@@ -192,9 +188,6 @@ nfpms:
|
|||||||
preremove: ./caddy-dist/scripts/preremove.sh
|
preremove: ./caddy-dist/scripts/preremove.sh
|
||||||
postremove: ./caddy-dist/scripts/postremove.sh
|
postremove: ./caddy-dist/scripts/postremove.sh
|
||||||
|
|
||||||
provides:
|
|
||||||
- httpd
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
github:
|
github:
|
||||||
owner: caddyserver
|
owner: caddyserver
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
repos:
|
|
||||||
- repo: https://github.com/gitleaks/gitleaks
|
|
||||||
rev: v8.16.3
|
|
||||||
hooks:
|
|
||||||
- id: gitleaks
|
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
|
||||||
rev: v1.52.2
|
|
||||||
hooks:
|
|
||||||
- id: golangci-lint-config-verify
|
|
||||||
- id: golangci-lint
|
|
||||||
- id: golangci-lint-fmt
|
|
||||||
- repo: https://github.com/jumanjihouse/pre-commit-hooks
|
|
||||||
rev: 3.0.0
|
|
||||||
hooks:
|
|
||||||
- id: shellcheck
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v4.4.0
|
|
||||||
hooks:
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- id: trailing-whitespace
|
|
||||||
@@ -14,10 +14,9 @@
|
|||||||
<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">
|
<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://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://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-%23007d9c.svg"></a>
|
||||||
<br>
|
<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://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" 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>
|
<a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
|
||||||
<br>
|
<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://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>
|
||||||
@@ -68,7 +67,6 @@
|
|||||||
- Fully-managed local CA for internal names & IPs
|
- Fully-managed local CA for internal names & IPs
|
||||||
- Can coordinate with other Caddy instances in a cluster
|
- Can coordinate with other Caddy instances in a cluster
|
||||||
- Multi-issuer fallback
|
- Multi-issuer fallback
|
||||||
- Encrypted ClientHello (ECH) support
|
|
||||||
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
|
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
|
||||||
- **Production-ready** after serving trillions of requests and managing millions of TLS certificates
|
- **Production-ready** after serving trillions of requests and managing millions of TLS certificates
|
||||||
- **Scales to hundreds of thousands of sites** as proven in production
|
- **Scales to hundreds of thousands of sites** as proven in production
|
||||||
@@ -89,7 +87,7 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i
|
|||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
|
|
||||||
- [Go 1.25.0 or newer](https://golang.org/dl/)
|
- [Go 1.21 or newer](https://golang.org/dl/)
|
||||||
|
|
||||||
### For development
|
### For development
|
||||||
|
|
||||||
@@ -117,18 +115,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
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -140,7 +131,7 @@ $ xcaddy build
|
|||||||
4. Initialize a Go module: `go mod init caddy`
|
4. Initialize a Go module: `go mod init caddy`
|
||||||
5. (Optional) Pin Caddy version: `go get github.com/caddyserver/caddy/v2@version` replacing `version` with a git tag, commit, or branch name.
|
5. (Optional) Pin Caddy version: `go get github.com/caddyserver/caddy/v2@version` replacing `version` with a git tag, commit, or branch name.
|
||||||
6. (Optional) Add plugins by adding their import: `_ "import/path/here"`
|
6. (Optional) Add plugins by adding their import: `_ "import/path/here"`
|
||||||
7. Compile: `go build -tags=nobadger,nomysql,nopgx`
|
7. Compile: `go build`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -185,7 +176,7 @@ The docs are also open source. You can contribute to them here: https://github.c
|
|||||||
|
|
||||||
## Getting help
|
## Getting help
|
||||||
|
|
||||||
- We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com) before help is needed.
|
- We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed.
|
||||||
|
|
||||||
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way! We can offer private help to sponsors. If Caddy is benefitting your company, please consider a sponsorship. This not only helps fund full-time work to ensure the longevity of the project, it provides your company the resources, support, and discounts you need; along with being a great look for your company to your customers and potential customers!
|
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way! We can offer private help to sponsors. If Caddy is benefitting your company, please consider a sponsorship. This not only helps fund full-time work to ensure the longevity of the project, it provides your company the resources, support, and discounts you need; along with being a great look for your company to your customers and potential customers!
|
||||||
|
|
||||||
@@ -201,8 +192,8 @@ Matthew Holt began developing Caddy in 2014 while studying computer science at B
|
|||||||
|
|
||||||
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH.
|
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH.
|
||||||
|
|
||||||
- _Project on X: [@caddyserver](https://x.com/caddyserver)_
|
- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_
|
||||||
- _Author on X: [@mholt6](https://x.com/mholt6)_
|
- _Author on Twitter: [@mholt6](https://twitter.com/mholt6)_
|
||||||
|
|
||||||
Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company.
|
Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company.
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -214,15 +213,14 @@ 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, _ Context) adminHandler {
|
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) 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
|
||||||
if remote {
|
if remote {
|
||||||
muxWrap.remoteControl = admin.Remote
|
muxWrap.remoteControl = admin.Remote
|
||||||
} else {
|
} else {
|
||||||
// see comment in allowedOrigins() as to why we disable the host check for unix/fd networks
|
muxWrap.enforceHost = !addr.isWildcardInterface()
|
||||||
muxWrap.enforceHost = !addr.isWildcardInterface() && !addr.IsUnixNetwork() && !addr.IsFdNetwork()
|
|
||||||
muxWrap.allowedOrigins = admin.allowedOrigins(addr)
|
muxWrap.allowedOrigins = admin.allowedOrigins(addr)
|
||||||
muxWrap.enforceOrigin = admin.EnforceOrigin
|
muxWrap.enforceOrigin = admin.EnforceOrigin
|
||||||
}
|
}
|
||||||
@@ -271,6 +269,7 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Co
|
|||||||
// 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)
|
||||||
|
handlerLabel := m.ID.Name()
|
||||||
for _, route := range router.Routes() {
|
for _, route := range router.Routes() {
|
||||||
addRoute(route.Pattern, handlerLabel, route.Handler)
|
addRoute(route.Pattern, handlerLabel, route.Handler)
|
||||||
}
|
}
|
||||||
@@ -311,43 +310,47 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
|
|||||||
for _, o := range admin.Origins {
|
for _, o := range admin.Origins {
|
||||||
uniqueOrigins[o] = struct{}{}
|
uniqueOrigins[o] = struct{}{}
|
||||||
}
|
}
|
||||||
// RFC 2616, Section 14.26:
|
if admin.Origins == nil {
|
||||||
// "A client MUST include a Host header field in all HTTP/1.1 request
|
|
||||||
// messages. If the requested URI does not include an Internet host
|
|
||||||
// name for the service being requested, then the Host header field MUST
|
|
||||||
// be given with an empty value."
|
|
||||||
//
|
|
||||||
// UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6.
|
|
||||||
// Understandable, but frustrating. See:
|
|
||||||
// https://github.com/golang/go/issues/60374
|
|
||||||
// See also the discussion here:
|
|
||||||
// https://github.com/golang/go/issues/61431
|
|
||||||
//
|
|
||||||
// We can no longer conform to RFC 2616 Section 14.26 from either Go or curl
|
|
||||||
// in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a
|
|
||||||
// bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin
|
|
||||||
// security checks, the infosec community assures me that it is secure to do
|
|
||||||
// so, because:
|
|
||||||
//
|
|
||||||
// 1) Browsers do not allow access to unix sockets
|
|
||||||
// 2) DNS is irrelevant to unix sockets
|
|
||||||
//
|
|
||||||
// If either of those two statements ever fail to hold true, it is not the
|
|
||||||
// fault of Caddy.
|
|
||||||
//
|
|
||||||
// Thus, we do not fill out allowed origins and do not enforce Host
|
|
||||||
// requirements for unix sockets. Enforcing it leads to confusion and
|
|
||||||
// frustration, when UDS have their own permissions from the OS.
|
|
||||||
// Enforcing host requirements here is effectively security theater,
|
|
||||||
// and a false sense of security.
|
|
||||||
//
|
|
||||||
// See also the discussion in #6832.
|
|
||||||
if admin.Origins == nil && !addr.IsUnixNetwork() && !addr.IsFdNetwork() {
|
|
||||||
if addr.isLoopback() {
|
if addr.isLoopback() {
|
||||||
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
|
if addr.IsUnixNetwork() {
|
||||||
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
|
// RFC 2616, Section 14.26:
|
||||||
uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{}
|
// "A client MUST include a Host header field in all HTTP/1.1 request
|
||||||
} else {
|
// messages. If the requested URI does not include an Internet host
|
||||||
|
// name for the service being requested, then the Host header field MUST
|
||||||
|
// be given with an empty value."
|
||||||
|
//
|
||||||
|
// UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6.
|
||||||
|
// Understandable, but frustrating. See:
|
||||||
|
// https://github.com/golang/go/issues/60374
|
||||||
|
// See also the discussion here:
|
||||||
|
// https://github.com/golang/go/issues/61431
|
||||||
|
//
|
||||||
|
// We can no longer conform to RFC 2616 Section 14.26 from either Go or curl
|
||||||
|
// in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a
|
||||||
|
// bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin
|
||||||
|
// security checks, the infosec community assures me that it is secure to do
|
||||||
|
// so, because:
|
||||||
|
// 1) Browsers do not allow access to unix sockets
|
||||||
|
// 2) DNS is irrelevant to unix sockets
|
||||||
|
//
|
||||||
|
// I am not quite ready to trust either of those external factors, so instead
|
||||||
|
// of disabling Host/Origin checks, we now allow specific Host values when
|
||||||
|
// accessing the admin endpoint over unix sockets. I definitely don't trust
|
||||||
|
// DNS (e.g. I don't trust 'localhost' to always resolve to the local host),
|
||||||
|
// and IP shouldn't even be used, but if it is for some reason, I think we can
|
||||||
|
// at least be reasonably assured that 127.0.0.1 and ::1 route to the local
|
||||||
|
// machine, meaning that a hypothetical browser origin would have to be on the
|
||||||
|
// local machine as well.
|
||||||
|
uniqueOrigins[""] = struct{}{}
|
||||||
|
uniqueOrigins["127.0.0.1"] = struct{}{}
|
||||||
|
uniqueOrigins["::1"] = struct{}{}
|
||||||
|
} else {
|
||||||
|
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
|
||||||
|
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
|
||||||
|
uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !addr.IsUnixNetwork() {
|
||||||
uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
|
uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,9 +381,7 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
|
|||||||
// for the admin endpoint exists in cfg, a default one is used, so
|
// for the admin endpoint exists in cfg, a default one is used, so
|
||||||
// that there is always an admin server (unless it is explicitly
|
// that there is always an admin server (unless it is explicitly
|
||||||
// configured to be disabled).
|
// configured to be disabled).
|
||||||
// Critically note that some elements and functionality of the context
|
func replaceLocalAdminServer(cfg *Config) error {
|
||||||
// may not be ready, e.g. storage. Tread carefully.
|
|
||||||
func replaceLocalAdminServer(cfg *Config, ctx Context) error {
|
|
||||||
// always* be sure to close down the old admin endpoint
|
// always* be sure to close down the old admin endpoint
|
||||||
// as gracefully as possible, even if the new one is
|
// as gracefully as possible, even if the new one is
|
||||||
// disabled -- careful to use reference to the current
|
// disabled -- careful to use reference to the current
|
||||||
@@ -422,14 +423,7 @@ func replaceLocalAdminServer(cfg *Config, ctx Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := cfg.Admin.newAdminHandler(addr, false, ctx)
|
handler := cfg.Admin.newAdminHandler(addr, false)
|
||||||
|
|
||||||
// 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 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{})
|
ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -550,14 +544,7 @@ 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 := cfg.Admin.newAdminHandler(addr, true, ctx)
|
handler := cfg.Admin.newAdminHandler(addr, true)
|
||||||
|
|
||||||
// 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 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// create client certificate pool for TLS mutual auth, and extract public keys
|
// create client certificate pool for TLS mutual auth, and extract public keys
|
||||||
// so that we can enforce access controls at the application layer
|
// so that we can enforce access controls at the application layer
|
||||||
@@ -688,7 +675,13 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
|
|||||||
// key recognized; make sure its HTTP request is permitted
|
// key recognized; make sure its HTTP request is permitted
|
||||||
for _, accessPerm := range adminAccess.Permissions {
|
for _, accessPerm := range adminAccess.Permissions {
|
||||||
// verify method
|
// verify method
|
||||||
methodFound := accessPerm.Methods == nil || slices.Contains(accessPerm.Methods, r.Method)
|
methodFound := accessPerm.Methods == nil
|
||||||
|
for _, method := range accessPerm.Methods {
|
||||||
|
if method == r.Method {
|
||||||
|
methodFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
if !methodFound {
|
if !methodFound {
|
||||||
return APIError{
|
return APIError{
|
||||||
HTTPStatus: http.StatusForbidden,
|
HTTPStatus: http.StatusForbidden,
|
||||||
@@ -884,9 +877,13 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
|
|||||||
// a trustworthy/expected value. This helps to mitigate DNS
|
// a trustworthy/expected value. This helps to mitigate DNS
|
||||||
// rebinding attacks.
|
// rebinding attacks.
|
||||||
func (h adminHandler) checkHost(r *http.Request) error {
|
func (h adminHandler) checkHost(r *http.Request) error {
|
||||||
allowed := slices.ContainsFunc(h.allowedOrigins, func(u *url.URL) bool {
|
var allowed bool
|
||||||
return r.Host == u.Host
|
for _, allowedOrigin := range h.allowedOrigins {
|
||||||
})
|
if r.Host == allowedOrigin.Host {
|
||||||
|
allowed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
if !allowed {
|
if !allowed {
|
||||||
return APIError{
|
return APIError{
|
||||||
HTTPStatus: http.StatusForbidden,
|
HTTPStatus: http.StatusForbidden,
|
||||||
@@ -946,7 +943,7 @@ func (h adminHandler) originAllowed(origin *url.URL) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// etagHasher returns the hasher we used on the config to both
|
// etagHasher returns a the hasher we used on the config to both
|
||||||
// produce and verify ETags.
|
// produce and verify ETags.
|
||||||
func etagHasher() hash.Hash { return xxhash.New() }
|
func etagHasher() hash.Hash { return xxhash.New() }
|
||||||
|
|
||||||
@@ -1029,13 +1026,6 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this request changed the config, clear the last
|
|
||||||
// config info we have stored, if it is different from
|
|
||||||
// the original source.
|
|
||||||
ClearLastConfigIfDifferent(
|
|
||||||
r.Header.Get("Caddy-Config-Source-File"),
|
|
||||||
r.Header.Get("Caddy-Config-Source-Adapter"))
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return APIError{
|
return APIError{
|
||||||
HTTPStatus: http.StatusMethodNotAllowed,
|
HTTPStatus: http.StatusMethodNotAllowed,
|
||||||
@@ -1157,7 +1147,7 @@ traverseLoop:
|
|||||||
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 || 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-727
@@ -15,20 +15,12 @@
|
|||||||
package caddy
|
package caddy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
dto "github.com/prometheus/client_model/go"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var testCfg = []byte(`{
|
var testCfg = []byte(`{
|
||||||
@@ -149,9 +141,11 @@ func TestLoadConcurrent(t *testing.T) {
|
|||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
wg.Go(func() {
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
_ = Load(testCfg, true)
|
_ = Load(testCfg, true)
|
||||||
})
|
wg.Done()
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
@@ -205,723 +199,7 @@ func TestETags(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkLoad(b *testing.B) {
|
func BenchmarkLoad(b *testing.B) {
|
||||||
for b.Loop() {
|
for i := 0; i < b.N; i++ {
|
||||||
Load(testCfg, true)
|
Load(testCfg, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAdminHandlerErrorHandling(t *testing.T) {
|
|
||||||
initAdminMetrics()
|
|
||||||
|
|
||||||
handler := adminHandler{
|
|
||||||
mux: http.NewServeMux(),
|
|
||||||
}
|
|
||||||
|
|
||||||
handler.mux.Handle("/error", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
err := fmt.Errorf("test error")
|
|
||||||
handler.handleError(w, r, err)
|
|
||||||
}))
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/error", nil)
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
|
|
||||||
handler.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
if rr.Code == http.StatusOK {
|
|
||||||
t.Error("expected error response, got success")
|
|
||||||
}
|
|
||||||
|
|
||||||
var apiErr APIError
|
|
||||||
if err := json.NewDecoder(rr.Body).Decode(&apiErr); err != nil {
|
|
||||||
t.Fatalf("decoding response: %v", err)
|
|
||||||
}
|
|
||||||
if apiErr.Message != "test error" {
|
|
||||||
t.Errorf("expected error message 'test error', got '%s'", apiErr.Message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initAdminMetrics() {
|
|
||||||
if adminMetrics.requestErrors != nil {
|
|
||||||
prometheus.Unregister(adminMetrics.requestErrors)
|
|
||||||
}
|
|
||||||
if adminMetrics.requestCount != nil {
|
|
||||||
prometheus.Unregister(adminMetrics.requestCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
adminMetrics.requestErrors = prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
||||||
Namespace: "caddy",
|
|
||||||
Subsystem: "admin_http",
|
|
||||||
Name: "request_errors_total",
|
|
||||||
Help: "Number of errors that occurred handling admin endpoint requests",
|
|
||||||
}, []string{"handler", "path", "method"})
|
|
||||||
|
|
||||||
adminMetrics.requestCount = prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
||||||
Namespace: "caddy",
|
|
||||||
Subsystem: "admin_http",
|
|
||||||
Name: "requests_total",
|
|
||||||
Help: "Count of requests to the admin endpoint",
|
|
||||||
}, []string{"handler", "path", "code", "method"}) // Added code and method labels
|
|
||||||
|
|
||||||
prometheus.MustRegister(adminMetrics.requestErrors)
|
|
||||||
prometheus.MustRegister(adminMetrics.requestCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
|
|
||||||
initAdminMetrics()
|
|
||||||
|
|
||||||
cfg := &Config{
|
|
||||||
Admin: &AdminConfig{
|
|
||||||
Listen: "localhost:2019",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := replaceLocalAdminServer(cfg, Context{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("setting up admin server: %v", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
stopAdminServer(localAdminServer)
|
|
||||||
}()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
path string
|
|
||||||
method string
|
|
||||||
expectedStatus int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "stop endpoint wrong method",
|
|
||||||
path: "/stop",
|
|
||||||
method: http.MethodGet,
|
|
||||||
expectedStatus: http.StatusMethodNotAllowed,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "config endpoint wrong content-type",
|
|
||||||
path: "/config/",
|
|
||||||
method: http.MethodPost,
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "config ID missing ID",
|
|
||||||
path: "/id/",
|
|
||||||
method: http.MethodGet,
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
req := httptest.NewRequest(test.method, fmt.Sprintf("http://localhost:2019%s", test.path), nil)
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
|
|
||||||
localAdminServer.Handler.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
if rr.Code != test.expectedStatus {
|
|
||||||
t.Errorf("expected status %d but got %d", test.expectedStatus, rr.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
metricValue := testGetMetricValue(map[string]string{
|
|
||||||
"path": test.path,
|
|
||||||
"handler": "admin",
|
|
||||||
"method": test.method,
|
|
||||||
})
|
|
||||||
if metricValue != 1 {
|
|
||||||
t.Errorf("expected error metric to be incremented once, got %v", metricValue)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGetMetricValue(labels map[string]string) float64 {
|
|
||||||
promLabels := prometheus.Labels{}
|
|
||||||
maps.Copy(promLabels, labels)
|
|
||||||
|
|
||||||
metric, err := adminMetrics.requestErrors.GetMetricWith(promLabels)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
pb := &dto.Metric{}
|
|
||||||
metric.Write(pb)
|
|
||||||
return pb.GetCounter().GetValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockRouter struct {
|
|
||||||
routes []AdminRoute
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m mockRouter) Routes() []AdminRoute {
|
|
||||||
return m.routes
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockModule struct {
|
|
||||||
mockRouter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockModule) CaddyModule() ModuleInfo {
|
|
||||||
return ModuleInfo{
|
|
||||||
ID: "admin.api.mock",
|
|
||||||
New: func() Module {
|
|
||||||
mm := &mockModule{
|
|
||||||
mockRouter: mockRouter{
|
|
||||||
routes: m.routes,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return mm
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewAdminHandlerRouterRegistration(t *testing.T) {
|
|
||||||
originalModules := make(map[string]ModuleInfo)
|
|
||||||
maps.Copy(originalModules, modules)
|
|
||||||
defer func() {
|
|
||||||
modules = originalModules
|
|
||||||
}()
|
|
||||||
|
|
||||||
mockRoute := AdminRoute{
|
|
||||||
Pattern: "/mock",
|
|
||||||
Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return nil
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
mock := &mockModule{
|
|
||||||
mockRouter: mockRouter{
|
|
||||||
routes: []AdminRoute{mockRoute},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
RegisterModule(mock)
|
|
||||||
|
|
||||||
addr, err := ParseNetworkAddress("localhost:2019")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse address: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
admin := &AdminConfig{
|
|
||||||
EnforceOrigin: false,
|
|
||||||
}
|
|
||||||
handler := admin.newAdminHandler(addr, false, Context{})
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/mock", nil)
|
|
||||||
req.Host = "localhost:2019"
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
|
|
||||||
handler.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
if rr.Code != http.StatusOK {
|
|
||||||
t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
|
|
||||||
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 {
|
|
||||||
mockRouter
|
|
||||||
provisionErr error
|
|
||||||
provisioned bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockProvisionableRouter) Provision(Context) error {
|
|
||||||
m.provisioned = true
|
|
||||||
return m.provisionErr
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockProvisionableModule struct {
|
|
||||||
*mockProvisionableRouter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockProvisionableModule) CaddyModule() ModuleInfo {
|
|
||||||
return ModuleInfo{
|
|
||||||
ID: "admin.api.mock_provision",
|
|
||||||
New: func() Module {
|
|
||||||
mm := &mockProvisionableModule{
|
|
||||||
mockProvisionableRouter: &mockProvisionableRouter{
|
|
||||||
mockRouter: m.mockRouter,
|
|
||||||
provisionErr: m.provisionErr,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return mm
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdminRouterProvisioning(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
provisionErr error
|
|
||||||
wantErr bool
|
|
||||||
routersAfter int // expected number of routers after provisioning
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "successful provisioning",
|
|
||||||
provisionErr: nil,
|
|
||||||
wantErr: false,
|
|
||||||
routersAfter: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "provisioning error",
|
|
||||||
provisionErr: fmt.Errorf("provision failed"),
|
|
||||||
wantErr: true,
|
|
||||||
routersAfter: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
originalModules := make(map[string]ModuleInfo)
|
|
||||||
maps.Copy(originalModules, modules)
|
|
||||||
defer func() {
|
|
||||||
modules = originalModules
|
|
||||||
}()
|
|
||||||
|
|
||||||
mockRoute := AdminRoute{
|
|
||||||
Pattern: "/mock",
|
|
||||||
Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return nil
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create provisionable module
|
|
||||||
mock := &mockProvisionableModule{
|
|
||||||
mockProvisionableRouter: &mockProvisionableRouter{
|
|
||||||
mockRouter: mockRouter{
|
|
||||||
routes: []AdminRoute{mockRoute},
|
|
||||||
},
|
|
||||||
provisionErr: test.provisionErr,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
RegisterModule(mock)
|
|
||||||
|
|
||||||
admin := &AdminConfig{}
|
|
||||||
addr, err := ParseNetworkAddress("localhost:2019")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse address: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = admin.newAdminHandler(addr, false, Context{})
|
|
||||||
err = admin.provisionAdminRouters(Context{})
|
|
||||||
|
|
||||||
if test.wantErr {
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error but got nil")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAllowedOriginsUnixSocket(t *testing.T) {
|
|
||||||
// see comment in allowedOrigins() as to why we do not fill out allowed origins for UDS
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
addr NetworkAddress
|
|
||||||
origins []string
|
|
||||||
expectOrigins []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "unix socket with default origins",
|
|
||||||
addr: NetworkAddress{
|
|
||||||
Network: "unix",
|
|
||||||
Host: "/tmp/caddy.sock",
|
|
||||||
},
|
|
||||||
origins: nil, // default origins
|
|
||||||
expectOrigins: []string{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unix socket with custom origins",
|
|
||||||
addr: NetworkAddress{
|
|
||||||
Network: "unix",
|
|
||||||
Host: "/tmp/caddy.sock",
|
|
||||||
},
|
|
||||||
origins: []string{"example.com"},
|
|
||||||
expectOrigins: []string{
|
|
||||||
"example.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tcp socket on localhost gets all loopback addresses",
|
|
||||||
addr: NetworkAddress{
|
|
||||||
Network: "tcp",
|
|
||||||
Host: "localhost",
|
|
||||||
StartPort: 2019,
|
|
||||||
EndPort: 2019,
|
|
||||||
},
|
|
||||||
origins: nil,
|
|
||||||
expectOrigins: []string{
|
|
||||||
"localhost:2019",
|
|
||||||
"[::1]:2019",
|
|
||||||
"127.0.0.1:2019",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
admin := AdminConfig{
|
|
||||||
Origins: test.origins,
|
|
||||||
}
|
|
||||||
|
|
||||||
got := admin.allowedOrigins(test.addr)
|
|
||||||
|
|
||||||
var gotOrigins []string
|
|
||||||
for _, u := range got {
|
|
||||||
gotOrigins = append(gotOrigins, u.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(gotOrigins) != len(test.expectOrigins) {
|
|
||||||
t.Errorf("%d: Expected %d origins but got %d", i, len(test.expectOrigins), len(gotOrigins))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
expectMap := make(map[string]struct{})
|
|
||||||
for _, origin := range test.expectOrigins {
|
|
||||||
expectMap[origin] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
gotMap := make(map[string]struct{})
|
|
||||||
for _, origin := range gotOrigins {
|
|
||||||
gotMap[origin] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(expectMap, gotMap) {
|
|
||||||
t.Errorf("%d: Origins mismatch.\nExpected: %v\nGot: %v", i, test.expectOrigins, gotOrigins)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReplaceRemoteAdminServer(t *testing.T) {
|
|
||||||
const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw
|
|
||||||
MTAwMDAwMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
|
||||||
AAOCAQ8AMIIBCgKCAQEA4O4S6BSoYcoxvRqI+h7yPOjF6KjntjzVVm9M+uHK4lzX
|
|
||||||
F1L3pSxJ2nDD4wZEV3FJ5yFOHVFqkG2vXG3BIczOlYG7UeNmKbQnKc5kZj3HGUrS
|
|
||||||
VGEktA4OJbeZhhWP15gcXN5eDM2eH3g9BFXVX6AURxLiUXzhNBUEZuj/OEyH9yEF
|
|
||||||
/qPCE+EjzVvWxvBXwgz/io4r4yok/Vq/bxJ6FlV6R7DX5oJSXyO0VEHZPi9DIyNU
|
|
||||||
kK3F/r4U1sWiJGWOs8i3YQWZ2ejh1C0aLFZpPcCGGgMNpoF31gyYP6ZuPDUyCXsE
|
|
||||||
g36UUw1JHNtIXYcLhnXuqj4A8TybTDpgXLqvwA9DBQIDAQABo1MwUTAdBgNVHQ4E
|
|
||||||
FgQUc13z30pFC63rr/HGKOE7E82vjXwwHwYDVR0jBBgwFoAUc13z30pFC63rr/HG
|
|
||||||
KOE7E82vjXwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAHO3j
|
|
||||||
oeiUXXJ7xD4P8Wj5t9d+E8lE1Xv1Dk3Z+EdG5+dan+RcToE42JJp9zB7FIh5Qz8g
|
|
||||||
W77LAjqh5oyqz3A2VJcyVgfE3uJP1R1mJM7JfGHf84QH4TZF2Q1RZY4SZs0VQ6+q
|
|
||||||
5wSlIZ4NXDy4Q4XkIJBGS61wT8IzYFXYBpx4PCP1Qj0PIE4sevEGwjsBIgxK307o
|
|
||||||
BxF8AWe6N6e4YZmQLGjQ+SeH0iwZb6vpkHyAY8Kj2hvK+cq2P7vU3VGi0t3r1F8L
|
|
||||||
IvrXHCvO2BMNJ/1UK1M4YNX8LYJqQhg9hEsIROe1OE/m3VhxIYMJI+qZXk9yHfgJ
|
|
||||||
vq+SH04xKhtFudVBAQ==`
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
cfg *Config
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "nil config",
|
|
||||||
cfg: nil,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nil admin config",
|
|
||||||
cfg: &Config{
|
|
||||||
Admin: nil,
|
|
||||||
},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nil remote config",
|
|
||||||
cfg: &Config{
|
|
||||||
Admin: &AdminConfig{},
|
|
||||||
},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid listen address",
|
|
||||||
cfg: &Config{
|
|
||||||
Admin: &AdminConfig{
|
|
||||||
Remote: &RemoteAdmin{
|
|
||||||
Listen: "invalid:address",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid config",
|
|
||||||
cfg: &Config{
|
|
||||||
Admin: &AdminConfig{
|
|
||||||
Identity: &IdentityConfig{},
|
|
||||||
Remote: &RemoteAdmin{
|
|
||||||
Listen: "localhost:2021",
|
|
||||||
AccessControl: []*AdminAccess{
|
|
||||||
{
|
|
||||||
PublicKeys: []string{testCert},
|
|
||||||
Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid certificate",
|
|
||||||
cfg: &Config{
|
|
||||||
Admin: &AdminConfig{
|
|
||||||
Identity: &IdentityConfig{},
|
|
||||||
Remote: &RemoteAdmin{
|
|
||||||
Listen: "localhost:2021",
|
|
||||||
AccessControl: []*AdminAccess{
|
|
||||||
{
|
|
||||||
PublicKeys: []string{"invalid-cert-data"},
|
|
||||||
Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
ctx := Context{
|
|
||||||
Context: context.Background(),
|
|
||||||
cfg: test.cfg,
|
|
||||||
}
|
|
||||||
|
|
||||||
if test.cfg != nil {
|
|
||||||
test.cfg.storage = &certmagic.FileStorage{Path: t.TempDir()}
|
|
||||||
}
|
|
||||||
|
|
||||||
if test.cfg != nil && test.cfg.Admin != nil && test.cfg.Admin.Identity != nil {
|
|
||||||
identityCertCache = certmagic.NewCache(certmagic.CacheOptions{
|
|
||||||
GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
|
|
||||||
return &certmagic.Config{}, nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
err := replaceRemoteAdminServer(ctx, test.cfg)
|
|
||||||
|
|
||||||
if test.wantErr {
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error but got nil")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
if remoteAdminServer != nil {
|
|
||||||
_ = stopAdminServer(remoteAdminServer)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockIssuer struct {
|
|
||||||
configSet *certmagic.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) {
|
|
||||||
return &certmagic.IssuedCertificate{
|
|
||||||
Certificate: []byte(csr.Raw),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockIssuer) SetConfig(cfg *certmagic.Config) {
|
|
||||||
m.configSet = cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockIssuer) IssuerKey() string {
|
|
||||||
return "mock"
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockIssuerModule struct {
|
|
||||||
*mockIssuer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockIssuerModule) CaddyModule() ModuleInfo {
|
|
||||||
return ModuleInfo{
|
|
||||||
ID: "tls.issuance.acme",
|
|
||||||
New: func() Module {
|
|
||||||
return &mockIssuerModule{mockIssuer: new(mockIssuer)}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManageIdentity(t *testing.T) {
|
|
||||||
originalModules := make(map[string]ModuleInfo)
|
|
||||||
maps.Copy(originalModules, modules)
|
|
||||||
defer func() {
|
|
||||||
modules = originalModules
|
|
||||||
}()
|
|
||||||
|
|
||||||
RegisterModule(&mockIssuerModule{})
|
|
||||||
|
|
||||||
certPEM := []byte(`-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
|
|
||||||
BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
|
|
||||||
cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw
|
|
||||||
WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
|
|
||||||
TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp
|
|
||||||
bC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3lcub2pUwkjC
|
|
||||||
5GJQA2ZZfJJi6d1QHhEmkX9VxKYGp6gagZuRqJWy9TXP6++1ZzQQxqZLD0TkuxZ9
|
|
||||||
8i9Nz00000CCBjCCAQQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGgG
|
|
||||||
CCsGAQUFBwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29t
|
|
||||||
L0dJQUcyLmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5j
|
|
||||||
b20vb2NzcDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/
|
|
||||||
BAIwADAfBgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHREEEDAO
|
|
||||||
ggxtYWlsLmdvb2dsZTANBgkqhkiG9w0BAQUFAAOCAQEAMP6IWgNGZE8wP9TjFjSZ
|
|
||||||
3mmW3A1eIr0CuPwNZ2LJ5ZD1i70ojzcj4I9IdP5yPg9CAEV4hNASbM1LzfC7GmJE
|
|
||||||
tPzW5tRmpKVWZGRgTgZI8Hp/xZXMwLh9ZmXV4kESFAGj5G5FNvJyUV7R5Eh+7OZX
|
|
||||||
7G4jJ4ZGJh+5jzN9HdJJHQHGYNIYOzC7+HH9UMwCjX9vhQ4RjwFZJThS2Yb+y7pb
|
|
||||||
9yxTJZoXC6J0H5JpnZb7kZEJ+Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
||||||
-----END CERTIFICATE-----`)
|
|
||||||
|
|
||||||
keyPEM := []byte(`-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
|
|
||||||
...
|
|
||||||
-----END PRIVATE KEY-----`)
|
|
||||||
|
|
||||||
testStorage := certmagic.FileStorage{Path: t.TempDir()}
|
|
||||||
err := testStorage.Store(context.Background(), "localhost/localhost.crt", certPEM)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
err = testStorage.Store(context.Background(), "localhost/localhost.key", keyPEM)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
cfg *Config
|
|
||||||
wantErr bool
|
|
||||||
checkState func(*testing.T, *Config)
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "nil config",
|
|
||||||
cfg: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nil admin config",
|
|
||||||
cfg: &Config{
|
|
||||||
Admin: nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nil identity config",
|
|
||||||
cfg: &Config{
|
|
||||||
Admin: &AdminConfig{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "default issuer when none specified",
|
|
||||||
cfg: &Config{
|
|
||||||
Admin: &AdminConfig{
|
|
||||||
Identity: &IdentityConfig{
|
|
||||||
Identifiers: []string{"localhost"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
storage: &testStorage,
|
|
||||||
},
|
|
||||||
checkState: func(t *testing.T, cfg *Config) {
|
|
||||||
if len(cfg.Admin.Identity.issuers) == 0 {
|
|
||||||
t.Error("Expected at least 1 issuer to be configured")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule); !ok {
|
|
||||||
t.Error("Expected mock issuer to be configured")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "custom issuer",
|
|
||||||
cfg: &Config{
|
|
||||||
Admin: &AdminConfig{
|
|
||||||
Identity: &IdentityConfig{
|
|
||||||
Identifiers: []string{"localhost"},
|
|
||||||
IssuersRaw: []json.RawMessage{
|
|
||||||
json.RawMessage(`{"module": "acme"}`),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
storage: &certmagic.FileStorage{Path: "testdata"},
|
|
||||||
},
|
|
||||||
checkState: func(t *testing.T, cfg *Config) {
|
|
||||||
if len(cfg.Admin.Identity.issuers) != 1 {
|
|
||||||
t.Fatalf("Expected 1 issuer, got %d", len(cfg.Admin.Identity.issuers))
|
|
||||||
}
|
|
||||||
mockIss, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("Expected mock issuer")
|
|
||||||
}
|
|
||||||
if mockIss.configSet == nil {
|
|
||||||
t.Error("Issuer config was not set")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid issuer module",
|
|
||||||
cfg: &Config{
|
|
||||||
Admin: &AdminConfig{
|
|
||||||
Identity: &IdentityConfig{
|
|
||||||
Identifiers: []string{"localhost"},
|
|
||||||
IssuersRaw: []json.RawMessage{
|
|
||||||
json.RawMessage(`{"module": "doesnt_exist"}`),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
if identityCertCache != nil {
|
|
||||||
// Reset the cert cache before each test
|
|
||||||
identityCertCache.Stop()
|
|
||||||
identityCertCache = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := Context{
|
|
||||||
Context: context.Background(),
|
|
||||||
cfg: test.cfg,
|
|
||||||
moduleInstances: make(map[string][]Module),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := manageIdentity(ctx, test.cfg)
|
|
||||||
|
|
||||||
if test.wantErr {
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error but got nil")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if test.checkState != nil {
|
|
||||||
test.checkState(t, test.cfg)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@@ -81,17 +82,13 @@ type Config struct {
|
|||||||
// associated value.
|
// associated value.
|
||||||
AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
|
AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
|
||||||
|
|
||||||
apps map[string]App
|
apps map[string]App
|
||||||
|
storage certmagic.Storage
|
||||||
// failedApps is a map of apps that failed to provision with their underlying error.
|
|
||||||
failedApps map[string]error
|
|
||||||
storage certmagic.Storage
|
|
||||||
eventEmitter eventEmitter
|
|
||||||
|
|
||||||
cancelFunc context.CancelFunc
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// App is a thing that Caddy runs.
|
// App is a thing that Caddy runs.
|
||||||
@@ -403,7 +400,6 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
|||||||
func run(newCfg *Config, start bool) (Context, error) {
|
func run(newCfg *Config, start bool) (Context, error) {
|
||||||
ctx, err := provisionContext(newCfg, start)
|
ctx, err := provisionContext(newCfg, start)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
globalMetrics.configSuccess.Set(0)
|
|
||||||
return ctx, err
|
return ctx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,19 +407,6 @@ func run(newCfg *Config, start bool) (Context, error) {
|
|||||||
return ctx, nil
|
return ctx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
|
||||||
// if newCfg fails to start completely, clean up the already provisioned modules
|
|
||||||
// partially copied from provisionContext
|
|
||||||
if err != nil {
|
|
||||||
globalMetrics.configSuccess.Set(0)
|
|
||||||
ctx.cfg.cancelFunc()
|
|
||||||
|
|
||||||
if currentCtx.cfg != nil {
|
|
||||||
certmagic.Default.Storage = currentCtx.cfg.storage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Provision any admin routers which may need to access
|
// Provision any admin routers which may need to access
|
||||||
// some of the other apps at runtime
|
// some of the other apps at runtime
|
||||||
err = ctx.cfg.Admin.provisionAdminRouters(ctx)
|
err = ctx.cfg.Admin.provisionAdminRouters(ctx)
|
||||||
@@ -455,16 +438,10 @@ func run(newCfg *Config, start bool) (Context, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx, err
|
return ctx, err
|
||||||
}
|
}
|
||||||
globalMetrics.configSuccess.Set(1)
|
|
||||||
globalMetrics.configSuccessTime.SetToCurrentTime()
|
|
||||||
|
|
||||||
// TODO: This event is experimental and subject to change.
|
|
||||||
ctx.emitEvent("started", nil)
|
|
||||||
|
|
||||||
// now that the user's config is running, finish setting up anything else,
|
// now that the user's config is running, finish setting up anything else,
|
||||||
// such as remote admin endpoint, config loader, etc.
|
// such as remote admin endpoint, config loader, etc.
|
||||||
err = finishSettingUp(ctx, ctx.cfg)
|
return ctx, finishSettingUp(ctx, ctx.cfg)
|
||||||
return ctx, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// provisionContext creates a new context from the given configuration and provisions
|
// provisionContext creates a new context from the given configuration and provisions
|
||||||
@@ -495,7 +472,6 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
|
|||||||
ctx, cancel := NewContext(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)
|
|
||||||
// if there were any errors during startup,
|
// if there were any errors during startup,
|
||||||
// we should cancel the new context we created
|
// we should cancel the new context we created
|
||||||
// since the associated config won't be used;
|
// since the associated config won't be used;
|
||||||
@@ -520,12 +496,19 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
|
|||||||
return ctx, err
|
return ctx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// start the admin endpoint (and stop any prior one)
|
||||||
|
if replaceAdminServer {
|
||||||
|
err = replaceLocalAdminServer(newCfg)
|
||||||
|
if err != nil {
|
||||||
|
return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// create the new filesystem map
|
// create the new filesystem map
|
||||||
newCfg.fileSystems = &filesystems.FileSystemMap{}
|
newCfg.filesystems = &filesystems.FilesystemMap{}
|
||||||
|
|
||||||
// prepare the new config for use
|
// prepare the new config for use
|
||||||
newCfg.apps = make(map[string]App)
|
newCfg.apps = make(map[string]App)
|
||||||
newCfg.failedApps = make(map[string]error)
|
|
||||||
|
|
||||||
// set up global storage and make it CertMagic's default storage, too
|
// set up global storage and make it CertMagic's default storage, too
|
||||||
err = func() error {
|
err = func() error {
|
||||||
@@ -552,14 +535,6 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
|
|||||||
return ctx, err
|
return ctx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the admin endpoint (and stop any prior one)
|
|
||||||
if replaceAdminServer {
|
|
||||||
err = replaceLocalAdminServer(newCfg, ctx)
|
|
||||||
if err != nil {
|
|
||||||
return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load and Provision each app and their submodules
|
// Load and Provision each app and their submodules
|
||||||
err = func() error {
|
err = func() error {
|
||||||
for appName := range newCfg.AppsRaw {
|
for appName := range newCfg.AppsRaw {
|
||||||
@@ -717,9 +692,6 @@ func unsyncedStop(ctx Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This event is experimental and subject to change.
|
|
||||||
ctx.emitEvent("stopping", nil)
|
|
||||||
|
|
||||||
// stop each app
|
// stop each app
|
||||||
for name, a := range ctx.cfg.apps {
|
for name, a := range ctx.cfg.apps {
|
||||||
err := a.Stop()
|
err := a.Stop()
|
||||||
@@ -749,10 +721,8 @@ func Validate(cfg *Config) error {
|
|||||||
// Errors are logged along the way, and an appropriate exit
|
// Errors are logged along the way, and an appropriate exit
|
||||||
// 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
|
||||||
if !atomic.CompareAndSwapInt32(exiting, 0, 1) {
|
atomic.StoreInt32(exiting, 1)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// give the OS or service/process manager our 2 weeks' notice: we quit
|
// give the OS or service/process manager our 2 weeks' notice: we quit
|
||||||
if err := notify.Stopping(); err != nil {
|
if err := notify.Stopping(); err != nil {
|
||||||
@@ -809,7 +779,10 @@ func exitProcess(ctx context.Context, logger *zap.Logger) {
|
|||||||
} else {
|
} else {
|
||||||
logger.Error("unclean shutdown")
|
logger.Error("unclean shutdown")
|
||||||
}
|
}
|
||||||
os.Exit(exitCode)
|
// check if we are in test environment, and dont call exit if we are
|
||||||
|
if flag.Lookup("test.v") == nil && !strings.Contains(os.Args[0], ".test") {
|
||||||
|
os.Exit(exitCode)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if remoteAdminServer != nil {
|
if remoteAdminServer != nil {
|
||||||
@@ -975,11 +948,11 @@ func Version() (simple, full string) {
|
|||||||
if CustomVersion != "" {
|
if CustomVersion != "" {
|
||||||
full = CustomVersion
|
full = CustomVersion
|
||||||
simple = CustomVersion
|
simple = CustomVersion
|
||||||
return simple, full
|
return
|
||||||
}
|
}
|
||||||
full = "unknown"
|
full = "unknown"
|
||||||
simple = "unknown"
|
simple = "unknown"
|
||||||
return simple, full
|
return
|
||||||
}
|
}
|
||||||
// find the Caddy module in the dependency list
|
// find the Caddy module in the dependency list
|
||||||
for _, dep := range bi.Deps {
|
for _, dep := range bi.Deps {
|
||||||
@@ -1059,101 +1032,9 @@ func Version() (simple, full string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return simple, full
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event represents something that has happened or is happening.
|
|
||||||
// An Event value is not synchronized, so it should be copied if
|
|
||||||
// being used in goroutines.
|
|
||||||
//
|
|
||||||
// EXPERIMENTAL: Events are subject to change.
|
|
||||||
type Event struct {
|
|
||||||
// If non-nil, the event has been aborted, meaning
|
|
||||||
// propagation has stopped to other handlers and
|
|
||||||
// the code should stop what it was doing. Emitters
|
|
||||||
// may choose to use this as a signal to adjust their
|
|
||||||
// code path appropriately.
|
|
||||||
Aborted error
|
|
||||||
|
|
||||||
// The data associated with the event. Usually the
|
|
||||||
// original emitter will be the only one to set or
|
|
||||||
// change these values, but the field is exported
|
|
||||||
// so handlers can have full access if needed.
|
|
||||||
// However, this map is not synchronized, so
|
|
||||||
// handlers must not use this map directly in new
|
|
||||||
// goroutines; instead, copy the map to use it in a
|
|
||||||
// goroutine. Data may be nil.
|
|
||||||
Data map[string]any
|
|
||||||
|
|
||||||
id uuid.UUID
|
|
||||||
ts time.Time
|
|
||||||
name string
|
|
||||||
origin Module
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEvent creates a new event, but does not emit the event. To emit an
|
|
||||||
// event, call Emit() on the current instance of the caddyevents app insteaad.
|
|
||||||
//
|
|
||||||
// EXPERIMENTAL: Subject to change.
|
|
||||||
func NewEvent(ctx Context, name string, data map[string]any) (Event, error) {
|
|
||||||
id, err := uuid.NewRandom()
|
|
||||||
if err != nil {
|
|
||||||
return Event{}, fmt.Errorf("generating new event ID: %v", err)
|
|
||||||
}
|
|
||||||
name = strings.ToLower(name)
|
|
||||||
return Event{
|
|
||||||
Data: data,
|
|
||||||
id: id,
|
|
||||||
ts: time.Now(),
|
|
||||||
name: name,
|
|
||||||
origin: ctx.Module(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e Event) ID() uuid.UUID { return e.id }
|
|
||||||
func (e Event) Timestamp() time.Time { return e.ts }
|
|
||||||
func (e Event) Name() string { return e.name }
|
|
||||||
func (e Event) Origin() Module { return e.origin } // Returns the module that originated the event. May be nil, usually if caddy core emits the event.
|
|
||||||
|
|
||||||
// CloudEvent exports event e as a structure that, when
|
|
||||||
// serialized as JSON, is compatible with the
|
|
||||||
// CloudEvents spec.
|
|
||||||
func (e Event) CloudEvent() CloudEvent {
|
|
||||||
dataJSON, _ := json.Marshal(e.Data)
|
|
||||||
var source string
|
|
||||||
if e.Origin() == nil {
|
|
||||||
source = "caddy"
|
|
||||||
} else {
|
|
||||||
source = string(e.Origin().CaddyModule().ID)
|
|
||||||
}
|
|
||||||
return CloudEvent{
|
|
||||||
ID: e.id.String(),
|
|
||||||
Source: source,
|
|
||||||
SpecVersion: "1.0",
|
|
||||||
Type: e.name,
|
|
||||||
Time: e.ts,
|
|
||||||
DataContentType: "application/json",
|
|
||||||
Data: dataJSON,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloudEvent is a JSON-serializable structure that
|
|
||||||
// is compatible with the CloudEvents specification.
|
|
||||||
// See https://cloudevents.io.
|
|
||||||
// EXPERIMENTAL: Subject to change.
|
|
||||||
type CloudEvent struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Source string `json:"source"`
|
|
||||||
SpecVersion string `json:"specversion"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Time time.Time `json:"time"`
|
|
||||||
DataContentType string `json:"datacontenttype,omitempty"`
|
|
||||||
Data json.RawMessage `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrEventAborted cancels an event.
|
|
||||||
var ErrEventAborted = errors.New("event aborted")
|
|
||||||
|
|
||||||
// ActiveContext returns the currently-active context.
|
// ActiveContext returns the currently-active context.
|
||||||
// This function is experimental and might be changed
|
// This function is experimental and might be changed
|
||||||
// or removed in the future.
|
// or removed in the future.
|
||||||
@@ -1197,91 +1078,6 @@ var (
|
|||||||
rawCfgMu sync.RWMutex
|
rawCfgMu sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
// lastConfigFile and lastConfigAdapter remember the source config
|
|
||||||
// file and adapter used when Caddy was started via the CLI "run" command.
|
|
||||||
// These are consulted by the SIGUSR1 handler to attempt reloading from
|
|
||||||
// the same source. They are intentionally not set for other entrypoints
|
|
||||||
// such as "caddy start" or subcommands like file-server.
|
|
||||||
var (
|
|
||||||
lastConfigMu sync.RWMutex
|
|
||||||
lastConfigFile string
|
|
||||||
lastConfigAdapter string
|
|
||||||
)
|
|
||||||
|
|
||||||
// reloadFromSourceFunc is the type of stored callback
|
|
||||||
// which is called when we receive a SIGUSR1 signal.
|
|
||||||
type reloadFromSourceFunc func(file, adapter string) error
|
|
||||||
|
|
||||||
// reloadFromSourceCallback is the stored callback
|
|
||||||
// which is called when we receive a SIGUSR1 signal.
|
|
||||||
var reloadFromSourceCallback reloadFromSourceFunc
|
|
||||||
|
|
||||||
// errReloadFromSourceUnavailable is returned when no reload-from-source callback is set.
|
|
||||||
var errReloadFromSourceUnavailable = errors.New("reload from source unavailable in this process") //nolint:unused
|
|
||||||
|
|
||||||
// SetLastConfig records the given source file and adapter as the
|
|
||||||
// last-known external configuration source. Intended to be called
|
|
||||||
// only when starting via "caddy run --config <file> --adapter <adapter>".
|
|
||||||
func SetLastConfig(file, adapter string, fn reloadFromSourceFunc) {
|
|
||||||
lastConfigMu.Lock()
|
|
||||||
lastConfigFile = file
|
|
||||||
lastConfigAdapter = adapter
|
|
||||||
reloadFromSourceCallback = fn
|
|
||||||
lastConfigMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearLastConfigIfDifferent clears the recorded last-config if the provided
|
|
||||||
// source file/adapter do not match the recorded last-config. If both srcFile
|
|
||||||
// and srcAdapter are empty, the last-config is cleared.
|
|
||||||
func ClearLastConfigIfDifferent(srcFile, srcAdapter string) {
|
|
||||||
if (srcFile != "" || srcAdapter != "") && lastConfigMatches(srcFile, srcAdapter) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
SetLastConfig("", "", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getLastConfig returns the last-known config file and adapter.
|
|
||||||
func getLastConfig() (file, adapter string, fn reloadFromSourceFunc) {
|
|
||||||
lastConfigMu.RLock()
|
|
||||||
f, a, cb := lastConfigFile, lastConfigAdapter, reloadFromSourceCallback
|
|
||||||
lastConfigMu.RUnlock()
|
|
||||||
return f, a, cb
|
|
||||||
}
|
|
||||||
|
|
||||||
// lastConfigMatches returns true if the provided source file and/or adapter
|
|
||||||
// matches the recorded last-config. Matching rules (in priority order):
|
|
||||||
// 1. If srcAdapter is provided and differs from the recorded adapter, no match.
|
|
||||||
// 2. If srcFile exactly equals the recorded file, match.
|
|
||||||
// 3. If both sides can be made absolute and equal, match.
|
|
||||||
// 4. If basenames are equal, match.
|
|
||||||
func lastConfigMatches(srcFile, srcAdapter string) bool {
|
|
||||||
lf, la, _ := getLastConfig()
|
|
||||||
|
|
||||||
// If adapter is provided, it must match.
|
|
||||||
if srcAdapter != "" && srcAdapter != la {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quick equality check.
|
|
||||||
if srcFile == lf {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try absolute path comparison.
|
|
||||||
sAbs, sErr := filepath.Abs(srcFile)
|
|
||||||
lAbs, lErr := filepath.Abs(lf)
|
|
||||||
if sErr == nil && lErr == nil && sAbs == lAbs {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final fallback: basename equality.
|
|
||||||
if filepath.Base(srcFile) == filepath.Base(lf) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// errSameConfig is returned if the new config is the same
|
// errSameConfig is returned if the new config is the same
|
||||||
// as the old one. This isn't usually an actual, actionable
|
// as the old one. This isn't usually an actual, actionable
|
||||||
// error; it's mostly a sentinel value.
|
// error; it's mostly a sentinel value.
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
package caddy
|
package caddy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -73,21 +72,3 @@ func TestParseDuration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvent_CloudEvent_NilOrigin(t *testing.T) {
|
|
||||||
ctx, _ := NewContext(Context{Context: context.Background()}) // module will be nil by default
|
|
||||||
event, err := NewEvent(ctx, "started", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewEvent() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should not panic
|
|
||||||
ce := event.CloudEvent()
|
|
||||||
|
|
||||||
if ce.Source != "caddy" {
|
|
||||||
t.Errorf("Expected CloudEvent Source to be 'caddy', got '%s'", ce.Source)
|
|
||||||
}
|
|
||||||
if ce.Type != "started" {
|
|
||||||
t.Errorf("Expected CloudEvent Type to be 'started', got '%s'", ce.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func (a Adapter) Adapt(body []byte, options map[string]any) ([]byte, []caddyconf
|
|||||||
// TODO: also perform this check on imported files
|
// TODO: also perform this check on imported files
|
||||||
func FormattingDifference(filename string, body []byte) (caddyconfig.Warning, bool) {
|
func FormattingDifference(filename string, body []byte) (caddyconfig.Warning, bool) {
|
||||||
// replace windows-style newlines to normalize comparison
|
// replace windows-style newlines to normalize comparison
|
||||||
normalizedBody := bytes.ReplaceAll(body, []byte("\r\n"), []byte("\n"))
|
normalizedBody := bytes.Replace(body, []byte("\r\n"), []byte("\n"), -1)
|
||||||
|
|
||||||
formatted := Format(normalizedBody)
|
formatted := Format(normalizedBody)
|
||||||
if bytes.Equal(formatted, normalizedBody) {
|
if bytes.Equal(formatted, normalizedBody) {
|
||||||
|
|||||||
@@ -308,9 +308,9 @@ func (d *Dispenser) CountRemainingArgs() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RemainingArgs loads any more arguments (tokens on the same line)
|
// RemainingArgs loads any more arguments (tokens on the same line)
|
||||||
// into a slice of strings and returns them. Open curly brace tokens
|
// into a slice and returns them. Open curly brace tokens also indicate
|
||||||
// also indicate the end of arguments, and the curly brace is not
|
// the end of arguments, and the curly brace is not included in
|
||||||
// included in the return value nor is it loaded.
|
// the return value nor is it loaded.
|
||||||
func (d *Dispenser) RemainingArgs() []string {
|
func (d *Dispenser) RemainingArgs() []string {
|
||||||
var args []string
|
var args []string
|
||||||
for d.NextArg() {
|
for d.NextArg() {
|
||||||
@@ -320,9 +320,9 @@ func (d *Dispenser) RemainingArgs() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RemainingArgsRaw loads any more arguments (tokens on the same line,
|
// RemainingArgsRaw loads any more arguments (tokens on the same line,
|
||||||
// retaining quotes) into a slice of strings and returns them.
|
// retaining quotes) into a slice and returns them. Open curly brace
|
||||||
// Open curly brace tokens also indicate the end of arguments,
|
// tokens also indicate the end of arguments, and the curly brace is
|
||||||
// and the curly brace is not included in the return value nor is it loaded.
|
// not included in the return value nor is it loaded.
|
||||||
func (d *Dispenser) RemainingArgsRaw() []string {
|
func (d *Dispenser) RemainingArgsRaw() []string {
|
||||||
var args []string
|
var args []string
|
||||||
for d.NextArg() {
|
for d.NextArg() {
|
||||||
@@ -331,18 +331,6 @@ func (d *Dispenser) RemainingArgsRaw() []string {
|
|||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemainingArgsAsTokens loads any more arguments (tokens on the same line)
|
|
||||||
// into a slice of Token-structs and returns them. Open curly brace tokens
|
|
||||||
// also indicate the end of arguments, and the curly brace is not included
|
|
||||||
// in the return value nor is it loaded.
|
|
||||||
func (d *Dispenser) RemainingArgsAsTokens() []Token {
|
|
||||||
var args []Token
|
|
||||||
for d.NextArg() {
|
|
||||||
args = append(args, d.Token())
|
|
||||||
}
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFromNextSegment returns a new dispenser with a copy of
|
// NewFromNextSegment returns a new dispenser with a copy of
|
||||||
// the tokens from the current token until the end of the
|
// the tokens from the current token until the end of the
|
||||||
// "directive" whether that be to the end of the line or
|
// "directive" whether that be to the end of the line or
|
||||||
@@ -427,7 +415,7 @@ func (d *Dispenser) EOFErr() error {
|
|||||||
|
|
||||||
// Err generates a custom parse-time error with a message of msg.
|
// Err generates a custom parse-time error with a message of msg.
|
||||||
func (d *Dispenser) Err(msg string) error {
|
func (d *Dispenser) Err(msg string) error {
|
||||||
return d.WrapErr(errors.New(msg))
|
return d.Errf(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errf is like Err, but for formatted error messages
|
// Errf is like Err, but for formatted error messages
|
||||||
|
|||||||
@@ -274,66 +274,6 @@ func TestDispenser_RemainingArgs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDispenser_RemainingArgsAsTokens(t *testing.T) {
|
|
||||||
input := `dir1 arg1 arg2 arg3
|
|
||||||
dir2 arg4 arg5
|
|
||||||
dir3 arg6 { arg7
|
|
||||||
dir4`
|
|
||||||
d := NewTestDispenser(input)
|
|
||||||
|
|
||||||
d.Next() // dir1
|
|
||||||
|
|
||||||
args := d.RemainingArgsAsTokens()
|
|
||||||
|
|
||||||
tokenTexts := make([]string, 0, len(args))
|
|
||||||
for _, arg := range args {
|
|
||||||
tokenTexts = append(tokenTexts, arg.Text)
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected := []string{"arg1", "arg2", "arg3"}; !reflect.DeepEqual(tokenTexts, expected) {
|
|
||||||
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.Next() // dir2
|
|
||||||
|
|
||||||
args = d.RemainingArgsAsTokens()
|
|
||||||
|
|
||||||
tokenTexts = tokenTexts[:0]
|
|
||||||
for _, arg := range args {
|
|
||||||
tokenTexts = append(tokenTexts, arg.Text)
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected := []string{"arg4", "arg5"}; !reflect.DeepEqual(tokenTexts, expected) {
|
|
||||||
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.Next() // dir3
|
|
||||||
|
|
||||||
args = d.RemainingArgsAsTokens()
|
|
||||||
tokenTexts = tokenTexts[:0]
|
|
||||||
for _, arg := range args {
|
|
||||||
tokenTexts = append(tokenTexts, arg.Text)
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected := []string{"arg6"}; !reflect.DeepEqual(tokenTexts, expected) {
|
|
||||||
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.Next() // {
|
|
||||||
d.Next() // arg7
|
|
||||||
d.Next() // dir4
|
|
||||||
|
|
||||||
args = d.RemainingArgsAsTokens()
|
|
||||||
tokenTexts = tokenTexts[:0]
|
|
||||||
for _, arg := range args {
|
|
||||||
tokenTexts = append(tokenTexts, arg.Text)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) != 0 {
|
|
||||||
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", []string{}, tokenTexts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDispenser_ArgErr_Err(t *testing.T) {
|
func TestDispenser_ArgErr_Err(t *testing.T) {
|
||||||
input := `dir1 {
|
input := `dir1 {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ func Format(input []byte) []byte {
|
|||||||
|
|
||||||
newLines int // count of newlines consumed
|
newLines int // count of newlines consumed
|
||||||
|
|
||||||
comment bool // whether we're in a comment
|
comment bool // whether we're in a comment
|
||||||
quotes string // encountered quotes ('', '`', '"', '"`', '`"')
|
quoted bool // whether we're in a quoted segment
|
||||||
escaped bool // whether current char is escaped
|
escaped bool // whether current char is escaped
|
||||||
|
|
||||||
heredoc heredocState // whether we're in a heredoc
|
heredoc heredocState // whether we're in a heredoc
|
||||||
heredocEscaped bool // whether heredoc is escaped
|
heredocEscaped bool // whether heredoc is escaped
|
||||||
@@ -88,8 +88,9 @@ func Format(input []byte) []byte {
|
|||||||
}
|
}
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// detect whether we have the start of a heredoc
|
// detect whether we have the start of a heredoc
|
||||||
if quotes == "" && (heredoc == heredocClosed && !heredocEscaped) &&
|
if !quoted && !(heredoc != heredocClosed || heredocEscaped) &&
|
||||||
space && last == '<' && ch == '<' {
|
space && last == '<' && ch == '<' {
|
||||||
write(ch)
|
write(ch)
|
||||||
heredoc = heredocOpening
|
heredoc = heredocOpening
|
||||||
@@ -175,38 +176,16 @@ func Format(input []byte) []byte {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ch == '`' {
|
if quoted {
|
||||||
switch quotes {
|
|
||||||
case "\"`":
|
|
||||||
quotes = "\""
|
|
||||||
case "`":
|
|
||||||
quotes = ""
|
|
||||||
case "\"":
|
|
||||||
quotes = "\"`"
|
|
||||||
default:
|
|
||||||
quotes = "`"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if quotes == "\"" {
|
|
||||||
if ch == '"' {
|
if ch == '"' {
|
||||||
quotes = ""
|
quoted = false
|
||||||
}
|
}
|
||||||
write(ch)
|
write(ch)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ch == '"' {
|
if space && ch == '"' {
|
||||||
switch quotes {
|
quoted = true
|
||||||
case "":
|
|
||||||
if space {
|
|
||||||
quotes = "\""
|
|
||||||
}
|
|
||||||
case "`\"":
|
|
||||||
quotes = "`"
|
|
||||||
case "\"`":
|
|
||||||
quotes = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if unicode.IsSpace(ch) {
|
if unicode.IsSpace(ch) {
|
||||||
@@ -241,7 +220,7 @@ func Format(input []byte) []byte {
|
|||||||
openBrace = false
|
openBrace = false
|
||||||
if beginningOfLine {
|
if beginningOfLine {
|
||||||
indent()
|
indent()
|
||||||
} else if !openBraceSpace || !unicode.IsSpace(last) {
|
} else if !openBraceSpace {
|
||||||
write(' ')
|
write(' ')
|
||||||
}
|
}
|
||||||
write('{')
|
write('{')
|
||||||
@@ -257,23 +236,14 @@ func Format(input []byte) []byte {
|
|||||||
switch {
|
switch {
|
||||||
case ch == '{':
|
case ch == '{':
|
||||||
openBrace = true
|
openBrace = true
|
||||||
openBraceSpace = spacePrior && !beginningOfLine
|
|
||||||
if openBraceSpace && newLines == 0 {
|
|
||||||
write(' ')
|
|
||||||
}
|
|
||||||
openBraceWritten = false
|
openBraceWritten = false
|
||||||
if quotes == "`" {
|
openBraceSpace = spacePrior && !beginningOfLine
|
||||||
write('{')
|
if openBraceSpace {
|
||||||
openBraceWritten = true
|
write(' ')
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
case ch == '}' && (spacePrior || !openBrace):
|
case ch == '}' && (spacePrior || !openBrace):
|
||||||
if quotes == "`" {
|
|
||||||
write('}')
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if last != '\n' {
|
if last != '\n' {
|
||||||
nextLine()
|
nextLine()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -432,36 +432,6 @@ block2 {
|
|||||||
heredoc \<<HEREDOC
|
heredoc \<<HEREDOC
|
||||||
respond "More than one space will be eaten" 200
|
respond "More than one space will be eaten" 200
|
||||||
}
|
}
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Preserve braces wrapped by backquotes",
|
|
||||||
input: "block {respond `All braces should remain: {{now | date \"2006\"}}`}",
|
|
||||||
expect: "block {respond `All braces should remain: {{now | date \"2006\"}}`}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Preserve braces wrapped by quotes",
|
|
||||||
input: "block {respond \"All braces should remain: {{now | date `2006`}}\"}",
|
|
||||||
expect: "block {respond \"All braces should remain: {{now | date `2006`}}\"}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Preserve quoted backticks and backticked quotes",
|
|
||||||
input: "block { respond \"`\" } block { respond `\"`}",
|
|
||||||
expect: "block {\n\trespond \"`\"\n}\n\nblock {\n\trespond `\"`\n}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "No trailing space on line before env variable",
|
|
||||||
input: `{
|
|
||||||
a
|
|
||||||
|
|
||||||
{$ENV_VAR}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
expect: `{
|
|
||||||
a
|
|
||||||
|
|
||||||
{$ENV_VAR}
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ package caddyfile
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type adjacency map[string][]string
|
type adjacency map[string][]string
|
||||||
@@ -92,7 +91,12 @@ func (i *importGraph) areConnected(from, to string) bool {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return slices.Contains(al, to)
|
for _, v := range al {
|
||||||
|
if v == to {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *importGraph) willCycle(from, to string) bool {
|
func (i *importGraph) willCycle(from, to string) bool {
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ func (l *lexer) next() (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// detect whether we have the start of a heredoc
|
// detect whether we have the start of a heredoc
|
||||||
if (!quoted && !btQuoted) && (!inHeredoc && !heredocEscaped) &&
|
if !(quoted || btQuoted) && !(inHeredoc || heredocEscaped) &&
|
||||||
len(val) > 1 && string(val[:2]) == "<<" {
|
len(val) > 1 && string(val[:2]) == "<<" {
|
||||||
// a space means it's just a regular token and not a heredoc
|
// a space means it's just a regular token and not a heredoc
|
||||||
if ch == ' ' {
|
if ch == ' ' {
|
||||||
@@ -323,8 +323,7 @@ func (l *lexer) finalizeHeredoc(val []rune, marker string) ([]rune, error) {
|
|||||||
|
|
||||||
// if the padding doesn't match exactly at the start then we can't safely strip
|
// if the padding doesn't match exactly at the start then we can't safely strip
|
||||||
if index != 0 {
|
if index != 0 {
|
||||||
cleanLineText := strings.TrimRight(lineText, "\r\n")
|
return nil, fmt.Errorf("mismatched leading whitespace in heredoc <<%s on line #%d [%s], expected whitespace [%s] to match the closing marker", marker, l.line+lineNum+1, lineText, paddingToStrip)
|
||||||
return nil, fmt.Errorf("mismatched leading whitespace in heredoc <<%s on line #%d [%s], expected whitespace [%s] to match the closing marker", marker, l.line+lineNum+1, cleanLineText, paddingToStrip)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// strip, then append the line, with the newline, to the output.
|
// strip, then append the line, with the newline, to the output.
|
||||||
|
|||||||
@@ -264,13 +264,8 @@ func (p *parser) addresses() error {
|
|||||||
return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", value)
|
return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// After the above, a comma surrounded by spaces would result
|
token.Text = value
|
||||||
// in an empty token which we should ignore
|
p.block.Keys = append(p.block.Keys, token)
|
||||||
if value != "" {
|
|
||||||
// Add the token as a site address
|
|
||||||
token.Text = value
|
|
||||||
p.block.Keys = append(p.block.Keys, token)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance token and possibly break out of loop or return error
|
// Advance token and possibly break out of loop or return error
|
||||||
@@ -379,23 +374,28 @@ func (p *parser) doImport(nesting int) error {
|
|||||||
if len(blockTokens) > 0 {
|
if len(blockTokens) > 0 {
|
||||||
// use such tokens to create a new dispenser, and then use it to parse each block
|
// use such tokens to create a new dispenser, and then use it to parse each block
|
||||||
bd := NewDispenser(blockTokens)
|
bd := NewDispenser(blockTokens)
|
||||||
|
|
||||||
// one iteration processes one sub-block inside the import
|
|
||||||
for bd.Next() {
|
for bd.Next() {
|
||||||
currentMappingKey := bd.Val()
|
// see if we can grab a key
|
||||||
|
var currentMappingKey string
|
||||||
if currentMappingKey == "{" {
|
if bd.Val() == "{" {
|
||||||
return p.Err("anonymous blocks are not supported")
|
return p.Err("anonymous blocks are not supported")
|
||||||
}
|
}
|
||||||
|
currentMappingKey = bd.Val()
|
||||||
// load up all arguments (if there even are any)
|
currentMappingTokens := []Token{}
|
||||||
currentMappingTokens := bd.RemainingArgsAsTokens()
|
// read all args until end of line / {
|
||||||
|
if bd.NextArg() {
|
||||||
// load up the entire block
|
|
||||||
for mappingNesting := bd.Nesting(); bd.NextBlock(mappingNesting); {
|
|
||||||
currentMappingTokens = append(currentMappingTokens, bd.Token())
|
currentMappingTokens = append(currentMappingTokens, bd.Token())
|
||||||
|
for bd.NextArg() {
|
||||||
|
currentMappingTokens = append(currentMappingTokens, bd.Token())
|
||||||
|
}
|
||||||
|
// TODO(elee1766): we don't enter another mapping here because it's annoying to extract the { and } properly.
|
||||||
|
// maybe someone can do that in the future
|
||||||
|
} else {
|
||||||
|
// attempt to enter a block and add tokens to the currentMappingTokens
|
||||||
|
for mappingNesting := bd.Nesting(); bd.NextBlock(mappingNesting); {
|
||||||
|
currentMappingTokens = append(currentMappingTokens, bd.Token())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
blockMapping[currentMappingKey] = currentMappingTokens
|
blockMapping[currentMappingKey] = currentMappingTokens
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,7 +418,7 @@ func (p *parser) doImport(nesting int) error {
|
|||||||
// make path relative to the file of the _token_ being processed rather
|
// make path relative to the file of the _token_ being processed rather
|
||||||
// than current working directory (issue #867) and then use glob to get
|
// than current working directory (issue #867) and then use glob to get
|
||||||
// list of matching filenames
|
// list of matching filenames
|
||||||
absFile, err := caddy.FastAbs(p.Dispenser.File())
|
absFile, err := filepath.Abs(p.Dispenser.File())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.File(), err)
|
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.File(), err)
|
||||||
}
|
}
|
||||||
@@ -533,24 +533,29 @@ func (p *parser) doImport(nesting int) error {
|
|||||||
}
|
}
|
||||||
// if it is {block}, we substitute with all tokens in the block
|
// if it is {block}, we substitute with all tokens in the block
|
||||||
// if it is {blocks.*}, we substitute with the tokens in the mapping for the *
|
// if it is {blocks.*}, we substitute with the tokens in the mapping for the *
|
||||||
|
var skip bool
|
||||||
var tokensToAdd []Token
|
var tokensToAdd []Token
|
||||||
foundBlockDirective := false
|
|
||||||
switch {
|
switch {
|
||||||
case token.Text == "{block}":
|
case token.Text == "{block}":
|
||||||
foundBlockDirective = true
|
|
||||||
tokensToAdd = blockTokens
|
tokensToAdd = blockTokens
|
||||||
case strings.HasPrefix(token.Text, "{blocks.") && strings.HasSuffix(token.Text, "}"):
|
case strings.HasPrefix(token.Text, "{blocks.") && strings.HasSuffix(token.Text, "}"):
|
||||||
foundBlockDirective = true
|
|
||||||
// {blocks.foo.bar} will be extracted to key `foo.bar`
|
// {blocks.foo.bar} will be extracted to key `foo.bar`
|
||||||
blockKey := strings.TrimPrefix(strings.TrimSuffix(token.Text, "}"), "{blocks.")
|
blockKey := strings.TrimPrefix(strings.TrimSuffix(token.Text, "}"), "{blocks.")
|
||||||
val, ok := blockMapping[blockKey]
|
val, ok := blockMapping[blockKey]
|
||||||
if ok {
|
if ok {
|
||||||
tokensToAdd = val
|
tokensToAdd = val
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
skip = true
|
||||||
}
|
}
|
||||||
|
if !skip {
|
||||||
if foundBlockDirective {
|
if len(tokensToAdd) == 0 {
|
||||||
tokensCopy = append(tokensCopy, tokensToAdd...)
|
// if there is no content in the snippet block, don't do any replacement
|
||||||
|
// this allows snippets which contained {block}/{block.*} before this change to continue functioning as normal
|
||||||
|
tokensCopy = append(tokensCopy, token)
|
||||||
|
} else {
|
||||||
|
tokensCopy = append(tokensCopy, tokensToAdd...)
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,7 +617,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
|||||||
|
|
||||||
// Tack the file path onto these tokens so errors show the imported file's name
|
// Tack the file path onto these tokens so errors show the imported file's name
|
||||||
// (we use full, absolute path to avoid bugs: issue #1892)
|
// (we use full, absolute path to avoid bugs: issue #1892)
|
||||||
filename, err := caddy.FastAbs(importFile)
|
filename, err := filepath.Abs(importFile)
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -556,10 +555,6 @@ func TestParseAll(t *testing.T) {
|
|||||||
{"localhost:1234", "http://host2"},
|
{"localhost:1234", "http://host2"},
|
||||||
}},
|
}},
|
||||||
|
|
||||||
{`foo.example.com , example.com`, false, [][]string{
|
|
||||||
{"foo.example.com", "example.com"},
|
|
||||||
}},
|
|
||||||
|
|
||||||
{`localhost:1234, http://host2,`, true, [][]string{}},
|
{`localhost:1234, http://host2,`, true, [][]string{}},
|
||||||
|
|
||||||
{`http://host1.com, http://host2.com {
|
{`http://host1.com, http://host2.com {
|
||||||
@@ -619,8 +614,8 @@ func TestParseAll(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for j, block := range blocks {
|
for j, block := range blocks {
|
||||||
if len(block.Keys) != len(test.keys[j]) {
|
if len(block.Keys) != len(test.keys[j]) {
|
||||||
t.Errorf("Test %d: Expected %d keys in block %d, got %d: %v",
|
t.Errorf("Test %d: Expected %d keys in block %d, got %d",
|
||||||
i, len(test.keys[j]), j, len(block.Keys), block.Keys)
|
i, len(test.keys[j]), j, len(block.Keys))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for k, addr := range block.GetKeysText() {
|
for k, addr := range block.GetKeysText() {
|
||||||
@@ -885,51 +880,6 @@ func TestRejectsGlobalMatcher(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRejectAnonymousImportBlock(t *testing.T) {
|
|
||||||
p := testParser(`
|
|
||||||
(site) {
|
|
||||||
http://{args[0]} https://{args[0]} {
|
|
||||||
{block}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
import site test.domain {
|
|
||||||
{
|
|
||||||
header_up Host {host}
|
|
||||||
header_up X-Real-IP {remote_host}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
_, err := p.parseAll()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Expected an error, but got nil")
|
|
||||||
}
|
|
||||||
expected := "anonymous blocks are not supported"
|
|
||||||
if !strings.HasPrefix(err.Error(), "anonymous blocks are not supported") {
|
|
||||||
t.Errorf("Expected error to start with '%s' but got '%v'", expected, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAcceptSiteImportWithBraces(t *testing.T) {
|
|
||||||
p := testParser(`
|
|
||||||
(site) {
|
|
||||||
http://{args[0]} https://{args[0]} {
|
|
||||||
{block}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
import site test.domain {
|
|
||||||
reverse_proxy http://192.168.1.1:8080 {
|
|
||||||
header_up Host {host}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
_, err := p.parseAll()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected error to be nil but got '%v'", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testParser(input string) parser {
|
func testParser(input string) parser {
|
||||||
return parser{Dispenser: NewTestDispenser(input)}
|
return parser{Dispenser: NewTestDispenser(input)}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import (
|
|||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mapAddressToProtocolToServerBlocks returns a map of listener address to list of server
|
// mapAddressToServerBlocks returns a map of listener address to list of server
|
||||||
// blocks that will be served on that address. To do this, each server block is
|
// blocks that will be served on that address. To do this, each server block is
|
||||||
// expanded so that each one is considered individually, although keys of a
|
// expanded so that each one is considered individually, although keys of a
|
||||||
// server block that share the same address stay grouped together so the config
|
// server block that share the same address stay grouped together so the config
|
||||||
@@ -77,15 +77,10 @@ import (
|
|||||||
// repetition may be undesirable, so call consolidateAddrMappings() to map
|
// repetition may be undesirable, so call consolidateAddrMappings() to map
|
||||||
// multiple addresses to the same lists of server blocks (a many:many mapping).
|
// multiple addresses to the same lists of server blocks (a many:many mapping).
|
||||||
// (Doing this is essentially a map-reduce technique.)
|
// (Doing this is essentially a map-reduce technique.)
|
||||||
func (st *ServerType) mapAddressToProtocolToServerBlocks(originalServerBlocks []serverBlock,
|
func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock,
|
||||||
options map[string]any,
|
options map[string]any,
|
||||||
) (map[string]map[string][]serverBlock, error) {
|
) (map[string][]serverBlock, error) {
|
||||||
addrToProtocolToServerBlocks := map[string]map[string][]serverBlock{}
|
sbmap := make(map[string][]serverBlock)
|
||||||
|
|
||||||
type keyWithParsedKey struct {
|
|
||||||
key caddyfile.Token
|
|
||||||
parsedKey Address
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, sblock := range originalServerBlocks {
|
for i, sblock := range originalServerBlocks {
|
||||||
// within a server block, we need to map all the listener addresses
|
// within a server block, we need to map all the listener addresses
|
||||||
@@ -93,48 +88,27 @@ func (st *ServerType) mapAddressToProtocolToServerBlocks(originalServerBlocks []
|
|||||||
// will be served by them; this has the effect of treating each
|
// will be served by them; this has the effect of treating each
|
||||||
// key of a server block as its own, but without having to repeat its
|
// key of a server block as its own, but without having to repeat its
|
||||||
// contents in cases where multiple keys really can be served together
|
// contents in cases where multiple keys really can be served together
|
||||||
addrToProtocolToKeyWithParsedKeys := map[string]map[string][]keyWithParsedKey{}
|
addrToKeys := make(map[string][]caddyfile.Token)
|
||||||
for j, key := range sblock.block.Keys {
|
for j, key := range sblock.block.Keys {
|
||||||
parsedKey, err := ParseAddress(key.Text)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parsing key: %v", err)
|
|
||||||
}
|
|
||||||
parsedKey = parsedKey.Normalize()
|
|
||||||
|
|
||||||
// a key can have multiple listener addresses if there are multiple
|
// a key can have multiple listener addresses if there are multiple
|
||||||
// arguments to the 'bind' directive (although they will all have
|
// arguments to the 'bind' directive (although they will all have
|
||||||
// the same port, since the port is defined by the key or is implicit
|
// the same port, since the port is defined by the key or is implicit
|
||||||
// through automatic HTTPS)
|
// through automatic HTTPS)
|
||||||
listeners, err := st.listenersForServerBlockAddress(sblock, parsedKey, options)
|
addrs, err := st.listenerAddrsForServerBlockKey(sblock, key.Text, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key.Text, err)
|
return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key.Text, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// associate this key with its protocols and each listener address served with them
|
// associate this key with each listener address it is served on
|
||||||
kwpk := keyWithParsedKey{key, parsedKey}
|
for _, addr := range addrs {
|
||||||
for addr, protocols := range listeners {
|
addrToKeys[addr] = append(addrToKeys[addr], key)
|
||||||
protocolToKeyWithParsedKeys, ok := addrToProtocolToKeyWithParsedKeys[addr]
|
|
||||||
if !ok {
|
|
||||||
protocolToKeyWithParsedKeys = map[string][]keyWithParsedKey{}
|
|
||||||
addrToProtocolToKeyWithParsedKeys[addr] = protocolToKeyWithParsedKeys
|
|
||||||
}
|
|
||||||
|
|
||||||
// an empty protocol indicates the default, a nil or empty value in the ListenProtocols array
|
|
||||||
if len(protocols) == 0 {
|
|
||||||
protocols[""] = struct{}{}
|
|
||||||
}
|
|
||||||
for prot := range protocols {
|
|
||||||
protocolToKeyWithParsedKeys[prot] = append(
|
|
||||||
protocolToKeyWithParsedKeys[prot],
|
|
||||||
kwpk)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// make a slice of the map keys so we can iterate in sorted order
|
// make a slice of the map keys so we can iterate in sorted order
|
||||||
addrs := make([]string, 0, len(addrToProtocolToKeyWithParsedKeys))
|
addrs := make([]string, 0, len(addrToKeys))
|
||||||
for addr := range addrToProtocolToKeyWithParsedKeys {
|
for k := range addrToKeys {
|
||||||
addrs = append(addrs, addr)
|
addrs = append(addrs, k)
|
||||||
}
|
}
|
||||||
sort.Strings(addrs)
|
sort.Strings(addrs)
|
||||||
|
|
||||||
@@ -144,132 +118,85 @@ func (st *ServerType) mapAddressToProtocolToServerBlocks(originalServerBlocks []
|
|||||||
// server block are only the ones which use the address; but
|
// server block are only the ones which use the address; but
|
||||||
// the contents (tokens) are of course the same
|
// the contents (tokens) are of course the same
|
||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
protocolToKeyWithParsedKeys := addrToProtocolToKeyWithParsedKeys[addr]
|
keys := addrToKeys[addr]
|
||||||
|
// parse keys so that we only have to do it once
|
||||||
prots := make([]string, 0, len(protocolToKeyWithParsedKeys))
|
parsedKeys := make([]Address, 0, len(keys))
|
||||||
for prot := range protocolToKeyWithParsedKeys {
|
for _, key := range keys {
|
||||||
prots = append(prots, prot)
|
addr, err := ParseAddress(key.Text)
|
||||||
}
|
if err != nil {
|
||||||
sort.Strings(prots)
|
return nil, fmt.Errorf("parsing key '%s': %v", key.Text, err)
|
||||||
|
|
||||||
protocolToServerBlocks, ok := addrToProtocolToServerBlocks[addr]
|
|
||||||
if !ok {
|
|
||||||
protocolToServerBlocks = map[string][]serverBlock{}
|
|
||||||
addrToProtocolToServerBlocks[addr] = protocolToServerBlocks
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, prot := range prots {
|
|
||||||
keyWithParsedKeys := protocolToKeyWithParsedKeys[prot]
|
|
||||||
|
|
||||||
keys := make([]caddyfile.Token, len(keyWithParsedKeys))
|
|
||||||
parsedKeys := make([]Address, len(keyWithParsedKeys))
|
|
||||||
|
|
||||||
for k, keyWithParsedKey := range keyWithParsedKeys {
|
|
||||||
keys[k] = keyWithParsedKey.key
|
|
||||||
parsedKeys[k] = keyWithParsedKey.parsedKey
|
|
||||||
}
|
}
|
||||||
|
parsedKeys = append(parsedKeys, addr.Normalize())
|
||||||
protocolToServerBlocks[prot] = append(protocolToServerBlocks[prot], serverBlock{
|
|
||||||
block: caddyfile.ServerBlock{
|
|
||||||
Keys: keys,
|
|
||||||
Segments: sblock.block.Segments,
|
|
||||||
},
|
|
||||||
pile: sblock.pile,
|
|
||||||
parsedKeys: parsedKeys,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
sbmap[addr] = append(sbmap[addr], serverBlock{
|
||||||
}
|
block: caddyfile.ServerBlock{
|
||||||
|
Keys: keys,
|
||||||
return addrToProtocolToServerBlocks, nil
|
Segments: sblock.block.Segments,
|
||||||
}
|
},
|
||||||
|
pile: sblock.pile,
|
||||||
// consolidateAddrMappings eliminates repetition of identical server blocks in a mapping of
|
keys: parsedKeys,
|
||||||
// single listener addresses to protocols to lists of server blocks. Since multiple addresses
|
|
||||||
// may serve multiple protocols to identical sites (server block contents), this function turns
|
|
||||||
// a 1:many mapping into a many:many mapping. Server block contents (tokens) must be
|
|
||||||
// exactly identical so that reflect.DeepEqual returns true in order for the addresses to be combined.
|
|
||||||
// Identical entries are deleted from the addrToServerBlocks map. Essentially, each pairing (each
|
|
||||||
// association from multiple addresses to multiple server blocks; i.e. each element of
|
|
||||||
// the returned slice) becomes a server definition in the output JSON.
|
|
||||||
func (st *ServerType) consolidateAddrMappings(addrToProtocolToServerBlocks map[string]map[string][]serverBlock) []sbAddrAssociation {
|
|
||||||
sbaddrs := make([]sbAddrAssociation, 0, len(addrToProtocolToServerBlocks))
|
|
||||||
|
|
||||||
addrs := make([]string, 0, len(addrToProtocolToServerBlocks))
|
|
||||||
for addr := range addrToProtocolToServerBlocks {
|
|
||||||
addrs = append(addrs, addr)
|
|
||||||
}
|
|
||||||
sort.Strings(addrs)
|
|
||||||
|
|
||||||
for _, addr := range addrs {
|
|
||||||
protocolToServerBlocks := addrToProtocolToServerBlocks[addr]
|
|
||||||
|
|
||||||
prots := make([]string, 0, len(protocolToServerBlocks))
|
|
||||||
for prot := range protocolToServerBlocks {
|
|
||||||
prots = append(prots, prot)
|
|
||||||
}
|
|
||||||
sort.Strings(prots)
|
|
||||||
|
|
||||||
for _, prot := range prots {
|
|
||||||
serverBlocks := protocolToServerBlocks[prot]
|
|
||||||
|
|
||||||
// now find other addresses that map to identical
|
|
||||||
// server blocks and add them to our map of listener
|
|
||||||
// addresses and protocols, while removing them from
|
|
||||||
// the original map
|
|
||||||
listeners := map[string]map[string]struct{}{}
|
|
||||||
|
|
||||||
for otherAddr, otherProtocolToServerBlocks := range addrToProtocolToServerBlocks {
|
|
||||||
for otherProt, otherServerBlocks := range otherProtocolToServerBlocks {
|
|
||||||
if addr == otherAddr && prot == otherProt || reflect.DeepEqual(serverBlocks, otherServerBlocks) {
|
|
||||||
listener, ok := listeners[otherAddr]
|
|
||||||
if !ok {
|
|
||||||
listener = map[string]struct{}{}
|
|
||||||
listeners[otherAddr] = listener
|
|
||||||
}
|
|
||||||
listener[otherProt] = struct{}{}
|
|
||||||
delete(otherProtocolToServerBlocks, otherProt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addresses := make([]string, 0, len(listeners))
|
|
||||||
for lnAddr := range listeners {
|
|
||||||
addresses = append(addresses, lnAddr)
|
|
||||||
}
|
|
||||||
sort.Strings(addresses)
|
|
||||||
|
|
||||||
addressesWithProtocols := make([]addressWithProtocols, 0, len(listeners))
|
|
||||||
|
|
||||||
for _, lnAddr := range addresses {
|
|
||||||
lnProts := listeners[lnAddr]
|
|
||||||
prots := make([]string, 0, len(lnProts))
|
|
||||||
for prot := range lnProts {
|
|
||||||
prots = append(prots, prot)
|
|
||||||
}
|
|
||||||
sort.Strings(prots)
|
|
||||||
|
|
||||||
addressesWithProtocols = append(addressesWithProtocols, addressWithProtocols{
|
|
||||||
address: lnAddr,
|
|
||||||
protocols: prots,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sbaddrs = append(sbaddrs, sbAddrAssociation{
|
|
||||||
addressesWithProtocols: addressesWithProtocols,
|
|
||||||
serverBlocks: serverBlocks,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return sbmap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// consolidateAddrMappings eliminates repetition of identical server blocks in a mapping of
|
||||||
|
// single listener addresses to lists of server blocks. Since multiple addresses may serve
|
||||||
|
// identical sites (server block contents), this function turns a 1:many mapping into a
|
||||||
|
// many:many mapping. Server block contents (tokens) must be exactly identical so that
|
||||||
|
// reflect.DeepEqual returns true in order for the addresses to be combined. Identical
|
||||||
|
// entries are deleted from the addrToServerBlocks map. Essentially, each pairing (each
|
||||||
|
// association from multiple addresses to multiple server blocks; i.e. each element of
|
||||||
|
// the returned slice) becomes a server definition in the output JSON.
|
||||||
|
func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]serverBlock) []sbAddrAssociation {
|
||||||
|
sbaddrs := make([]sbAddrAssociation, 0, len(addrToServerBlocks))
|
||||||
|
for addr, sblocks := range addrToServerBlocks {
|
||||||
|
// we start with knowing that at least this address
|
||||||
|
// maps to these server blocks
|
||||||
|
a := sbAddrAssociation{
|
||||||
|
addresses: []string{addr},
|
||||||
|
serverBlocks: sblocks,
|
||||||
|
}
|
||||||
|
|
||||||
|
// now find other addresses that map to identical
|
||||||
|
// server blocks and add them to our list of
|
||||||
|
// addresses, while removing them from the map
|
||||||
|
for otherAddr, otherSblocks := range addrToServerBlocks {
|
||||||
|
if addr == otherAddr {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if reflect.DeepEqual(sblocks, otherSblocks) {
|
||||||
|
a.addresses = append(a.addresses, otherAddr)
|
||||||
|
delete(addrToServerBlocks, otherAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(a.addresses)
|
||||||
|
|
||||||
|
sbaddrs = append(sbaddrs, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort them by their first address (we know there will always be at least one)
|
||||||
|
// to avoid problems with non-deterministic ordering (makes tests flaky)
|
||||||
|
sort.Slice(sbaddrs, func(i, j int) bool {
|
||||||
|
return sbaddrs[i].addresses[0] < sbaddrs[j].addresses[0]
|
||||||
|
})
|
||||||
|
|
||||||
return sbaddrs
|
return sbaddrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// listenersForServerBlockAddress essentially converts the Caddyfile site addresses to a map from
|
// listenerAddrsForServerBlockKey essentially converts the Caddyfile
|
||||||
// Caddy listener addresses and the protocols to serve them with to the parsed address for each server block.
|
// site addresses to Caddy listener addresses for each server block.
|
||||||
func (st *ServerType) listenersForServerBlockAddress(sblock serverBlock, addr Address,
|
func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string,
|
||||||
options map[string]any,
|
options map[string]any,
|
||||||
) (map[string]map[string]struct{}, error) {
|
) ([]string, error) {
|
||||||
|
addr, err := ParseAddress(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing key: %v", err)
|
||||||
|
}
|
||||||
|
addr = addr.Normalize()
|
||||||
|
|
||||||
switch addr.Scheme {
|
switch addr.Scheme {
|
||||||
case "wss":
|
case "wss":
|
||||||
return nil, fmt.Errorf("the scheme wss:// is only supported in browsers; use https:// instead")
|
return nil, fmt.Errorf("the scheme wss:// is only supported in browsers; use https:// instead")
|
||||||
@@ -303,58 +230,55 @@ func (st *ServerType) listenersForServerBlockAddress(sblock serverBlock, addr Ad
|
|||||||
|
|
||||||
// error if scheme and port combination violate convention
|
// error if scheme and port combination violate convention
|
||||||
if (addr.Scheme == "http" && lnPort == httpsPort) || (addr.Scheme == "https" && lnPort == httpPort) {
|
if (addr.Scheme == "http" && lnPort == httpsPort) || (addr.Scheme == "https" && lnPort == httpPort) {
|
||||||
return nil, fmt.Errorf("[%s] scheme and port violate convention", addr.String())
|
return nil, fmt.Errorf("[%s] scheme and port violate convention", key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// the bind directive specifies hosts (and potentially network), and the protocols to serve them with, but is optional
|
// the bind directive specifies hosts (and potentially network), but is optional
|
||||||
lnCfgVals := make([]addressesWithProtocols, 0, len(sblock.pile["bind"]))
|
lnHosts := make([]string, 0, len(sblock.pile["bind"]))
|
||||||
for _, cfgVal := range sblock.pile["bind"] {
|
for _, cfgVal := range sblock.pile["bind"] {
|
||||||
if val, ok := cfgVal.Value.(addressesWithProtocols); ok {
|
lnHosts = append(lnHosts, cfgVal.Value.([]string)...)
|
||||||
lnCfgVals = append(lnCfgVals, val)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(lnCfgVals) == 0 {
|
if len(lnHosts) == 0 {
|
||||||
if defaultBindValues, ok := options["default_bind"].([]ConfigValue); ok {
|
if defaultBind, ok := options["default_bind"].([]string); ok {
|
||||||
for _, defaultBindValue := range defaultBindValues {
|
lnHosts = defaultBind
|
||||||
lnCfgVals = append(lnCfgVals, defaultBindValue.Value.(addressesWithProtocols))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
lnCfgVals = []addressesWithProtocols{{
|
lnHosts = []string{""}
|
||||||
addresses: []string{""},
|
|
||||||
protocols: nil,
|
|
||||||
}}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// use a map to prevent duplication
|
// use a map to prevent duplication
|
||||||
listeners := map[string]map[string]struct{}{}
|
listeners := make(map[string]struct{})
|
||||||
for _, lnCfgVal := range lnCfgVals {
|
for _, lnHost := range lnHosts {
|
||||||
for _, lnAddr := range lnCfgVal.addresses {
|
// normally we would simply append the port,
|
||||||
lnNetw, lnHost, _, err := caddy.SplitNetworkAddress(lnAddr)
|
// but if lnHost is IPv6, we need to ensure it
|
||||||
if err != nil {
|
// is enclosed in [ ]; net.JoinHostPort does
|
||||||
return nil, fmt.Errorf("splitting listener address: %v", err)
|
// this for us, but lnHost might also have a
|
||||||
}
|
// network type in front (e.g. "tcp/") leading
|
||||||
networkAddr, err := caddy.ParseNetworkAddress(caddy.JoinNetworkAddress(lnNetw, lnHost, lnPort))
|
// to "[tcp/::1]" which causes parsing failures
|
||||||
if err != nil {
|
// later; what we need is "tcp/[::1]", so we have
|
||||||
return nil, fmt.Errorf("parsing network address: %v", err)
|
// to split the network and host, then re-combine
|
||||||
}
|
network, host, ok := strings.Cut(lnHost, "/")
|
||||||
if _, ok := listeners[addr.String()]; !ok {
|
if !ok {
|
||||||
listeners[networkAddr.String()] = map[string]struct{}{}
|
host = network
|
||||||
}
|
network = ""
|
||||||
for _, protocol := range lnCfgVal.protocols {
|
|
||||||
listeners[networkAddr.String()][protocol] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
host = strings.Trim(host, "[]") // IPv6
|
||||||
|
networkAddr := caddy.JoinNetworkAddress(network, host, lnPort)
|
||||||
|
addr, err := caddy.ParseNetworkAddress(networkAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing network address: %v", err)
|
||||||
|
}
|
||||||
|
listeners[addr.String()] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return listeners, nil
|
// now turn map into list
|
||||||
}
|
listenersList := make([]string, 0, len(listeners))
|
||||||
|
for lnStr := range listeners {
|
||||||
|
listenersList = append(listenersList, lnStr)
|
||||||
|
}
|
||||||
|
sort.Strings(listenersList)
|
||||||
|
|
||||||
// addressesWithProtocols associates a list of listen addresses
|
return listenersList, nil
|
||||||
// with a list of protocols to serve them with
|
|
||||||
type addressesWithProtocols struct {
|
|
||||||
addresses []string
|
|
||||||
protocols []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Address represents a site address. It contains
|
// Address represents a site address. It contains
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
package httpcaddyfile
|
package httpcaddyfile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -25,7 +24,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/mholt/acmez/v3/acme"
|
"github.com/mholt/acmez/v2/acme"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
@@ -57,41 +56,21 @@ func init() {
|
|||||||
|
|
||||||
// parseBind parses the bind directive. Syntax:
|
// parseBind parses the bind directive. Syntax:
|
||||||
//
|
//
|
||||||
// bind <addresses...> [{
|
// bind <addresses...>
|
||||||
// protocols [h1|h2|h2c|h3] [...]
|
|
||||||
// }]
|
|
||||||
func parseBind(h Helper) ([]ConfigValue, error) {
|
func parseBind(h Helper) ([]ConfigValue, error) {
|
||||||
h.Next() // consume directive name
|
h.Next() // consume directive name
|
||||||
var addresses, protocols []string
|
return []ConfigValue{{Class: "bind", Value: h.RemainingArgs()}}, nil
|
||||||
addresses = h.RemainingArgs()
|
|
||||||
|
|
||||||
for h.NextBlock(0) {
|
|
||||||
switch h.Val() {
|
|
||||||
case "protocols":
|
|
||||||
protocols = h.RemainingArgs()
|
|
||||||
if len(protocols) == 0 {
|
|
||||||
return nil, h.Errf("protocols requires one or more arguments")
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, h.Errf("unknown subdirective: %s", h.Val())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return []ConfigValue{{Class: "bind", Value: addressesWithProtocols{
|
|
||||||
addresses: addresses,
|
|
||||||
protocols: protocols,
|
|
||||||
}}}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseTLS parses the tls directive. Syntax:
|
// parseTLS parses the tls directive. Syntax:
|
||||||
//
|
//
|
||||||
// tls [<email>|internal|force_automate]|[<cert_file> <key_file>] {
|
// tls [<email>|internal]|[<cert_file> <key_file>] {
|
||||||
// protocols <min> [<max>]
|
// protocols <min> [<max>]
|
||||||
// ciphers <cipher_suites...>
|
// ciphers <cipher_suites...>
|
||||||
// curves <curves...>
|
// curves <curves...>
|
||||||
// client_auth {
|
// client_auth {
|
||||||
// mode [request|require|verify_if_given|require_and_verify]
|
// mode [request|require|verify_if_given|require_and_verify]
|
||||||
// trust_pool <module_name> [...]
|
// trust_pool <module_name> [...]
|
||||||
// trusted_leaf_cert <base64_der>
|
// trusted_leaf_cert <base64_der>
|
||||||
// trusted_leaf_cert_file <filename>
|
// trusted_leaf_cert_file <filename>
|
||||||
// }
|
// }
|
||||||
@@ -100,7 +79,7 @@ func parseBind(h Helper) ([]ConfigValue, error) {
|
|||||||
// ca <acme_ca_endpoint>
|
// ca <acme_ca_endpoint>
|
||||||
// ca_root <pem_file>
|
// ca_root <pem_file>
|
||||||
// key_type [ed25519|p256|p384|rsa2048|rsa4096]
|
// key_type [ed25519|p256|p384|rsa2048|rsa4096]
|
||||||
// dns [<provider_name> [...]] (required, though, if DNS is not configured as global option)
|
// dns <provider_name> [...]
|
||||||
// propagation_delay <duration>
|
// propagation_delay <duration>
|
||||||
// propagation_timeout <duration>
|
// propagation_timeout <duration>
|
||||||
// resolvers <dns_servers...>
|
// resolvers <dns_servers...>
|
||||||
@@ -108,7 +87,6 @@ func parseBind(h Helper) ([]ConfigValue, error) {
|
|||||||
// dns_challenge_override_domain <domain>
|
// dns_challenge_override_domain <domain>
|
||||||
// on_demand
|
// on_demand
|
||||||
// reuse_private_keys
|
// reuse_private_keys
|
||||||
// force_automate
|
|
||||||
// eab <key_id> <mac_key>
|
// eab <key_id> <mac_key>
|
||||||
// issuer <module_name> [...]
|
// issuer <module_name> [...]
|
||||||
// get_certificate <module_name> [...]
|
// get_certificate <module_name> [...]
|
||||||
@@ -128,10 +106,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
var certManagers []certmagic.Manager
|
var certManagers []certmagic.Manager
|
||||||
var onDemand bool
|
var onDemand bool
|
||||||
var reusePrivateKeys bool
|
var reusePrivateKeys bool
|
||||||
var forceAutomate bool
|
|
||||||
|
|
||||||
// Track which DNS challenge options are set
|
|
||||||
var dnsOptionsSet []string
|
|
||||||
|
|
||||||
firstLine := h.RemainingArgs()
|
firstLine := h.RemainingArgs()
|
||||||
switch len(firstLine) {
|
switch len(firstLine) {
|
||||||
@@ -139,10 +113,8 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
case 1:
|
case 1:
|
||||||
if firstLine[0] == "internal" {
|
if firstLine[0] == "internal" {
|
||||||
internalIssuer = new(caddytls.InternalIssuer)
|
internalIssuer = new(caddytls.InternalIssuer)
|
||||||
} else if firstLine[0] == "force_automate" {
|
|
||||||
forceAutomate = true
|
|
||||||
} else if !strings.Contains(firstLine[0], "@") {
|
} else if !strings.Contains(firstLine[0], "@") {
|
||||||
return nil, h.Err("single argument must either be 'internal', 'force_automate', or an email address")
|
return nil, h.Err("single argument must either be 'internal' or an email address")
|
||||||
} else {
|
} else {
|
||||||
acmeIssuer = &caddytls.ACMEIssuer{
|
acmeIssuer = &caddytls.ACMEIssuer{
|
||||||
Email: firstLine[0],
|
Email: firstLine[0],
|
||||||
@@ -316,6 +288,10 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
certManagers = append(certManagers, certManager)
|
certManagers = append(certManagers, certManager)
|
||||||
|
|
||||||
case "dns":
|
case "dns":
|
||||||
|
if !h.NextArg() {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
provName := h.Val()
|
||||||
if acmeIssuer == nil {
|
if acmeIssuer == nil {
|
||||||
acmeIssuer = new(caddytls.ACMEIssuer)
|
acmeIssuer = new(caddytls.ACMEIssuer)
|
||||||
}
|
}
|
||||||
@@ -325,19 +301,12 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
if acmeIssuer.Challenges.DNS == nil {
|
if acmeIssuer.Challenges.DNS == nil {
|
||||||
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
||||||
}
|
}
|
||||||
// DNS provider configuration optional, since it may be configured globally via the TLS app with global options
|
modID := "dns.providers." + provName
|
||||||
if h.NextArg() {
|
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
|
||||||
provName := h.Val()
|
if err != nil {
|
||||||
modID := "dns.providers." + provName
|
return nil, err
|
||||||
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, h.warnings)
|
|
||||||
} else if h.Option("dns") == nil {
|
|
||||||
// if DNS is omitted locally, it needs to be configured globally
|
|
||||||
return nil, h.ArgErr()
|
|
||||||
}
|
}
|
||||||
|
acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, h.warnings)
|
||||||
|
|
||||||
case "resolvers":
|
case "resolvers":
|
||||||
args := h.RemainingArgs()
|
args := h.RemainingArgs()
|
||||||
@@ -353,7 +322,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
if acmeIssuer.Challenges.DNS == nil {
|
if acmeIssuer.Challenges.DNS == nil {
|
||||||
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
||||||
}
|
}
|
||||||
dnsOptionsSet = append(dnsOptionsSet, "resolvers")
|
|
||||||
acmeIssuer.Challenges.DNS.Resolvers = args
|
acmeIssuer.Challenges.DNS.Resolvers = args
|
||||||
|
|
||||||
case "propagation_delay":
|
case "propagation_delay":
|
||||||
@@ -375,7 +343,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
if acmeIssuer.Challenges.DNS == nil {
|
if acmeIssuer.Challenges.DNS == nil {
|
||||||
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
||||||
}
|
}
|
||||||
dnsOptionsSet = append(dnsOptionsSet, "propagation_delay")
|
|
||||||
acmeIssuer.Challenges.DNS.PropagationDelay = caddy.Duration(delay)
|
acmeIssuer.Challenges.DNS.PropagationDelay = caddy.Duration(delay)
|
||||||
|
|
||||||
case "propagation_timeout":
|
case "propagation_timeout":
|
||||||
@@ -403,7 +370,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
if acmeIssuer.Challenges.DNS == nil {
|
if acmeIssuer.Challenges.DNS == nil {
|
||||||
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
||||||
}
|
}
|
||||||
dnsOptionsSet = append(dnsOptionsSet, "propagation_timeout")
|
|
||||||
acmeIssuer.Challenges.DNS.PropagationTimeout = caddy.Duration(timeout)
|
acmeIssuer.Challenges.DNS.PropagationTimeout = caddy.Duration(timeout)
|
||||||
|
|
||||||
case "dns_ttl":
|
case "dns_ttl":
|
||||||
@@ -425,7 +391,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
if acmeIssuer.Challenges.DNS == nil {
|
if acmeIssuer.Challenges.DNS == nil {
|
||||||
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
||||||
}
|
}
|
||||||
dnsOptionsSet = append(dnsOptionsSet, "dns_ttl")
|
|
||||||
acmeIssuer.Challenges.DNS.TTL = caddy.Duration(ttl)
|
acmeIssuer.Challenges.DNS.TTL = caddy.Duration(ttl)
|
||||||
|
|
||||||
case "dns_challenge_override_domain":
|
case "dns_challenge_override_domain":
|
||||||
@@ -442,7 +407,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
if acmeIssuer.Challenges.DNS == nil {
|
if acmeIssuer.Challenges.DNS == nil {
|
||||||
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
||||||
}
|
}
|
||||||
dnsOptionsSet = append(dnsOptionsSet, "dns_challenge_override_domain")
|
|
||||||
acmeIssuer.Challenges.DNS.OverrideDomain = arg[0]
|
acmeIssuer.Challenges.DNS.OverrideDomain = arg[0]
|
||||||
|
|
||||||
case "ca_root":
|
case "ca_root":
|
||||||
@@ -478,18 +442,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate DNS challenge config: any DNS challenge option except "dns" requires a DNS provider
|
|
||||||
if acmeIssuer != nil && acmeIssuer.Challenges != nil && acmeIssuer.Challenges.DNS != nil {
|
|
||||||
dnsCfg := acmeIssuer.Challenges.DNS
|
|
||||||
providerSet := dnsCfg.ProviderRaw != nil || h.Option("dns") != nil || h.Option("acme_dns") != nil
|
|
||||||
if len(dnsOptionsSet) > 0 && !providerSet {
|
|
||||||
return nil, h.Errf(
|
|
||||||
"setting DNS challenge options [%s] requires a DNS provider (set with the 'dns' subdirective or 'acme_dns' global option)",
|
|
||||||
strings.Join(dnsOptionsSet, ", "),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// a naked tls directive is not allowed
|
// a naked tls directive is not allowed
|
||||||
if len(firstLine) == 0 && !hasBlock {
|
if len(firstLine) == 0 && !hasBlock {
|
||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
@@ -597,15 +549,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// if enabled, the names in the site addresses will be
|
|
||||||
// added to the automation policies
|
|
||||||
if forceAutomate {
|
|
||||||
configVals = append(configVals, ConfigValue{
|
|
||||||
Class: "tls.force_automate",
|
|
||||||
Value: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// custom certificate selection
|
// custom certificate selection
|
||||||
if len(certSelector.AnyTag) > 0 {
|
if len(certSelector.AnyTag) > 0 {
|
||||||
cp.CertSelection = &certSelector
|
cp.CertSelection = &certSelector
|
||||||
@@ -864,18 +807,13 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {
|
|||||||
return nil, h.Errf("segment was not parsed as a subroute")
|
return nil, h.Errf("segment was not parsed as a subroute")
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrap the subroutes
|
|
||||||
wrappingRoute := caddyhttp.Route{
|
|
||||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)},
|
|
||||||
}
|
|
||||||
subroute = &caddyhttp.Subroute{
|
|
||||||
Routes: []caddyhttp.Route{wrappingRoute},
|
|
||||||
}
|
|
||||||
if expression != "" {
|
if expression != "" {
|
||||||
statusMatcher := caddy.ModuleMap{
|
statusMatcher := caddy.ModuleMap{
|
||||||
"expression": h.JSON(caddyhttp.MatchExpression{Expr: expression}),
|
"expression": h.JSON(caddyhttp.MatchExpression{Expr: expression}),
|
||||||
}
|
}
|
||||||
subroute.Routes[0].MatcherSetsRaw = []caddy.ModuleMap{statusMatcher}
|
for i := range subroute.Routes {
|
||||||
|
subroute.Routes[i].MatcherSetsRaw = []caddy.ModuleMap{statusMatcher}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return []ConfigValue{
|
return []ConfigValue{
|
||||||
{
|
{
|
||||||
@@ -1023,50 +961,6 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
|||||||
}
|
}
|
||||||
cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings)
|
cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings)
|
||||||
|
|
||||||
case "sampling":
|
|
||||||
d := h.Dispenser.NewFromNextSegment()
|
|
||||||
for d.NextArg() {
|
|
||||||
// consume any tokens on the same line, if any.
|
|
||||||
}
|
|
||||||
|
|
||||||
sampling := &caddy.LogSampling{}
|
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
|
||||||
subdir := d.Val()
|
|
||||||
switch subdir {
|
|
||||||
case "interval":
|
|
||||||
if !d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
interval, err := time.ParseDuration(d.Val() + "ns")
|
|
||||||
if err != nil {
|
|
||||||
return nil, d.Errf("failed to parse interval: %v", err)
|
|
||||||
}
|
|
||||||
sampling.Interval = interval
|
|
||||||
case "first":
|
|
||||||
if !d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
first, err := strconv.Atoi(d.Val())
|
|
||||||
if err != nil {
|
|
||||||
return nil, d.Errf("failed to parse first: %v", err)
|
|
||||||
}
|
|
||||||
sampling.First = first
|
|
||||||
case "thereafter":
|
|
||||||
if !d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
thereafter, err := strconv.Atoi(d.Val())
|
|
||||||
if err != nil {
|
|
||||||
return nil, d.Errf("failed to parse thereafter: %v", err)
|
|
||||||
}
|
|
||||||
sampling.Thereafter = thereafter
|
|
||||||
default:
|
|
||||||
return nil, d.Errf("unrecognized subdirective: %s", subdir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cl.Sampling = sampling
|
|
||||||
|
|
||||||
case "core":
|
case "core":
|
||||||
if !h.NextArg() {
|
if !h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
@@ -1186,11 +1080,6 @@ func parseLogSkip(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
|||||||
if h.NextArg() {
|
if h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.NextBlock(0) {
|
|
||||||
return nil, h.Err("log_skip directive does not accept blocks")
|
|
||||||
}
|
|
||||||
|
|
||||||
return caddyhttp.VarsMiddleware{"log_skip": true}, nil
|
return caddyhttp.VarsMiddleware{"log_skip": true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,20 +62,6 @@ func TestLogDirectiveSyntax(t *testing.T) {
|
|||||||
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"core":{"module":"mock"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`,
|
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"core":{"module":"mock"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`,
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
input: `:8080 {
|
|
||||||
log {
|
|
||||||
sampling {
|
|
||||||
interval 2
|
|
||||||
first 3
|
|
||||||
thereafter 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
} {
|
} {
|
||||||
|
|
||||||
adapter := caddyfile.Adapter{
|
adapter := caddyfile.Adapter{
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ package httpcaddyfile
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"maps"
|
|
||||||
"net"
|
"net"
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -102,6 +100,17 @@ var defaultDirectiveOrder = []string{
|
|||||||
// plugins or by the user via the "order" global option.
|
// plugins or by the user via the "order" global option.
|
||||||
var directiveOrder = defaultDirectiveOrder
|
var directiveOrder = defaultDirectiveOrder
|
||||||
|
|
||||||
|
// directiveIsOrdered returns true if dir is
|
||||||
|
// a known, ordered (sorted) directive.
|
||||||
|
func directiveIsOrdered(dir string) bool {
|
||||||
|
for _, d := range directiveOrder {
|
||||||
|
if d == dir {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterDirective registers a unique directive dir with an
|
// RegisterDirective registers a unique directive dir with an
|
||||||
// associated unmarshaling (setup) function. When directive dir
|
// associated unmarshaling (setup) function. When directive dir
|
||||||
// is encountered in a Caddyfile, setupFunc will be called to
|
// is encountered in a Caddyfile, setupFunc will be called to
|
||||||
@@ -152,7 +161,7 @@ func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
|
|||||||
// EXPERIMENTAL: This API may change or be removed.
|
// EXPERIMENTAL: This API may change or be removed.
|
||||||
func RegisterDirectiveOrder(dir string, position Positional, standardDir string) {
|
func RegisterDirectiveOrder(dir string, position Positional, standardDir string) {
|
||||||
// check if directive was already ordered
|
// check if directive was already ordered
|
||||||
if slices.Contains(directiveOrder, dir) {
|
if directiveIsOrdered(dir) {
|
||||||
panic("directive '" + dir + "' already ordered")
|
panic("directive '" + dir + "' already ordered")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +172,12 @@ func RegisterDirectiveOrder(dir string, position Positional, standardDir string)
|
|||||||
// check if directive exists in standard distribution, since
|
// check if directive exists in standard distribution, since
|
||||||
// we can't allow plugins to depend on one another; we can't
|
// we can't allow plugins to depend on one another; we can't
|
||||||
// guarantee the order that plugins are loaded in.
|
// guarantee the order that plugins are loaded in.
|
||||||
foundStandardDir := slices.Contains(defaultDirectiveOrder, standardDir)
|
foundStandardDir := false
|
||||||
|
for _, d := range defaultDirectiveOrder {
|
||||||
|
if d == standardDir {
|
||||||
|
foundStandardDir = true
|
||||||
|
}
|
||||||
|
}
|
||||||
if !foundStandardDir {
|
if !foundStandardDir {
|
||||||
panic("the 3rd argument '" + standardDir + "' must be a directive that exists in the standard distribution of Caddy")
|
panic("the 3rd argument '" + standardDir + "' must be a directive that exists in the standard distribution of Caddy")
|
||||||
}
|
}
|
||||||
@@ -174,12 +188,10 @@ func RegisterDirectiveOrder(dir string, position Positional, standardDir string)
|
|||||||
if d != standardDir {
|
if d != standardDir {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch position {
|
if position == Before {
|
||||||
case Before:
|
|
||||||
newOrder = append(newOrder[:i], append([]string{dir}, newOrder[i:]...)...)
|
newOrder = append(newOrder[:i], append([]string{dir}, newOrder[i:]...)...)
|
||||||
case After:
|
} else if position == After {
|
||||||
newOrder = append(newOrder[:i+1], append([]string{dir}, newOrder[i+1:]...)...)
|
newOrder = append(newOrder[:i+1], append([]string{dir}, newOrder[i+1:]...)...)
|
||||||
case First, Last:
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -368,7 +380,9 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
|
|||||||
// copy existing matcher definitions so we can augment
|
// copy existing matcher definitions so we can augment
|
||||||
// new ones that are defined only in this scope
|
// new ones that are defined only in this scope
|
||||||
matcherDefs := make(map[string]caddy.ModuleMap, len(h.matcherDefs))
|
matcherDefs := make(map[string]caddy.ModuleMap, len(h.matcherDefs))
|
||||||
maps.Copy(matcherDefs, h.matcherDefs)
|
for key, val := range h.matcherDefs {
|
||||||
|
matcherDefs[key] = val
|
||||||
|
}
|
||||||
|
|
||||||
// find and extract any embedded matcher definitions in this scope
|
// find and extract any embedded matcher definitions in this scope
|
||||||
for i := 0; i < len(segments); i++ {
|
for i := 0; i < len(segments); i++ {
|
||||||
@@ -484,29 +498,12 @@ func sortRoutes(routes []ConfigValue) {
|
|||||||
// we can only confidently compare path lengths if both
|
// we can only confidently compare path lengths if both
|
||||||
// directives have a single path to match (issue #5037)
|
// directives have a single path to match (issue #5037)
|
||||||
if iPathLen > 0 && jPathLen > 0 {
|
if iPathLen > 0 && jPathLen > 0 {
|
||||||
// trim the trailing wildcard if there is one
|
|
||||||
iPathTrimmed := strings.TrimSuffix(iPM[0], "*")
|
|
||||||
jPathTrimmed := strings.TrimSuffix(jPM[0], "*")
|
|
||||||
|
|
||||||
// if both paths are the same except for a trailing wildcard,
|
// if both paths are the same except for a trailing wildcard,
|
||||||
// sort by the shorter path first (which is more specific)
|
// sort by the shorter path first (which is more specific)
|
||||||
if iPathTrimmed == jPathTrimmed {
|
if strings.TrimSuffix(iPM[0], "*") == strings.TrimSuffix(jPM[0], "*") {
|
||||||
return iPathLen < jPathLen
|
return iPathLen < jPathLen
|
||||||
}
|
}
|
||||||
|
|
||||||
// we use the trimmed length to compare the paths
|
|
||||||
// https://github.com/caddyserver/caddy/issues/7012#issuecomment-2870142195
|
|
||||||
// credit to https://github.com/Hellio404
|
|
||||||
// for sorts with many items, mixing matchers w/ and w/o wildcards will confuse the sort and result in incorrect orders
|
|
||||||
iPathLen = len(iPathTrimmed)
|
|
||||||
jPathLen = len(jPathTrimmed)
|
|
||||||
|
|
||||||
// if both paths have the same length, sort lexically
|
|
||||||
// https://github.com/caddyserver/caddy/pull/7015#issuecomment-2871993588
|
|
||||||
if iPathLen == jPathLen {
|
|
||||||
return iPathTrimmed < jPathTrimmed
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort most-specific (longest) path first
|
// sort most-specific (longest) path first
|
||||||
return iPathLen > jPathLen
|
return iPathLen > jPathLen
|
||||||
}
|
}
|
||||||
@@ -534,9 +531,9 @@ func sortRoutes(routes []ConfigValue) {
|
|||||||
// a "pile" of config values, keyed by class name,
|
// a "pile" of config values, keyed by class name,
|
||||||
// as well as its parsed keys for convenience.
|
// as well as its parsed keys for convenience.
|
||||||
type serverBlock struct {
|
type serverBlock struct {
|
||||||
block caddyfile.ServerBlock
|
block caddyfile.ServerBlock
|
||||||
pile map[string][]ConfigValue // config values obtained from directives
|
pile map[string][]ConfigValue // config values obtained from directives
|
||||||
parsedKeys []Address
|
keys []Address
|
||||||
}
|
}
|
||||||
|
|
||||||
// hostsFromKeys returns a list of all the non-empty hostnames found in
|
// hostsFromKeys returns a list of all the non-empty hostnames found in
|
||||||
@@ -553,7 +550,7 @@ type serverBlock struct {
|
|||||||
func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
|
func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
|
||||||
// ensure each entry in our list is unique
|
// ensure each entry in our list is unique
|
||||||
hostMap := make(map[string]struct{})
|
hostMap := make(map[string]struct{})
|
||||||
for _, addr := range sb.parsedKeys {
|
for _, addr := range sb.keys {
|
||||||
if addr.Host == "" {
|
if addr.Host == "" {
|
||||||
if !loggerMode {
|
if !loggerMode {
|
||||||
// server block contains a key like ":443", i.e. the host portion
|
// server block contains a key like ":443", i.e. the host portion
|
||||||
@@ -585,7 +582,7 @@ func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
|
|||||||
func (sb serverBlock) hostsFromKeysNotHTTP(httpPort string) []string {
|
func (sb serverBlock) hostsFromKeysNotHTTP(httpPort string) []string {
|
||||||
// ensure each entry in our list is unique
|
// ensure each entry in our list is unique
|
||||||
hostMap := make(map[string]struct{})
|
hostMap := make(map[string]struct{})
|
||||||
for _, addr := range sb.parsedKeys {
|
for _, addr := range sb.keys {
|
||||||
if addr.Host == "" {
|
if addr.Host == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -606,17 +603,23 @@ func (sb serverBlock) hostsFromKeysNotHTTP(httpPort string) []string {
|
|||||||
// hasHostCatchAllKey returns true if sb has a key that
|
// hasHostCatchAllKey returns true if sb has a key that
|
||||||
// omits a host portion, i.e. it "catches all" hosts.
|
// omits a host portion, i.e. it "catches all" hosts.
|
||||||
func (sb serverBlock) hasHostCatchAllKey() bool {
|
func (sb serverBlock) hasHostCatchAllKey() bool {
|
||||||
return slices.ContainsFunc(sb.parsedKeys, func(addr Address) bool {
|
for _, addr := range sb.keys {
|
||||||
return addr.Host == ""
|
if addr.Host == "" {
|
||||||
})
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// isAllHTTP returns true if all sb keys explicitly specify
|
// isAllHTTP returns true if all sb keys explicitly specify
|
||||||
// the http:// scheme
|
// the http:// scheme
|
||||||
func (sb serverBlock) isAllHTTP() bool {
|
func (sb serverBlock) isAllHTTP() bool {
|
||||||
return !slices.ContainsFunc(sb.parsedKeys, func(addr Address) bool {
|
for _, addr := range sb.keys {
|
||||||
return addr.Scheme != "http"
|
if addr.Scheme != "http" {
|
||||||
})
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Positional are the supported modes for ordering directives.
|
// Positional are the supported modes for ordering directives.
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func TestHostsFromKeys(t *testing.T) {
|
|||||||
[]string{"example.com:2015"},
|
[]string{"example.com:2015"},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
sb := serverBlock{parsedKeys: tc.keys}
|
sb := serverBlock{keys: tc.keys}
|
||||||
|
|
||||||
// test in normal mode
|
// test in normal mode
|
||||||
actual := sb.hostsFromKeys(false)
|
actual := sb.hostsFromKeys(false)
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
package httpcaddyfile
|
package httpcaddyfile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -172,7 +171,7 @@ func (st ServerType) Setup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// map
|
// map
|
||||||
sbmap, err := st.mapAddressToProtocolToServerBlocks(originalServerBlocks, options)
|
sbmap, err := st.mapAddressToServerBlocks(originalServerBlocks, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, warnings, err
|
return nil, warnings, err
|
||||||
}
|
}
|
||||||
@@ -187,25 +186,12 @@ func (st ServerType) Setup(
|
|||||||
return nil, warnings, err
|
return nil, warnings, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// hoist the metrics config from per-server to global
|
|
||||||
metrics, _ := options["metrics"].(*caddyhttp.Metrics)
|
|
||||||
for _, s := range servers {
|
|
||||||
if s.Metrics != nil {
|
|
||||||
metrics = cmp.Or(metrics, &caddyhttp.Metrics{})
|
|
||||||
metrics = &caddyhttp.Metrics{
|
|
||||||
PerHost: metrics.PerHost || s.Metrics.PerHost,
|
|
||||||
}
|
|
||||||
s.Metrics = nil // we don't need it anymore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// now that each server is configured, make the HTTP app
|
// now that each server is configured, make the HTTP app
|
||||||
httpApp := caddyhttp.App{
|
httpApp := caddyhttp.App{
|
||||||
HTTPPort: tryInt(options["http_port"], &warnings),
|
HTTPPort: tryInt(options["http_port"], &warnings),
|
||||||
HTTPSPort: tryInt(options["https_port"], &warnings),
|
HTTPSPort: tryInt(options["https_port"], &warnings),
|
||||||
GracePeriod: tryDuration(options["grace_period"], &warnings),
|
GracePeriod: tryDuration(options["grace_period"], &warnings),
|
||||||
ShutdownDelay: tryDuration(options["shutdown_delay"], &warnings),
|
ShutdownDelay: tryDuration(options["shutdown_delay"], &warnings),
|
||||||
Metrics: metrics,
|
|
||||||
Servers: servers,
|
Servers: servers,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +336,7 @@ func (st ServerType) Setup(
|
|||||||
|
|
||||||
// avoid duplicates by sorting + compacting
|
// avoid duplicates by sorting + compacting
|
||||||
sort.Strings(defaultLog.Exclude)
|
sort.Strings(defaultLog.Exclude)
|
||||||
defaultLog.Exclude = slices.Compact(defaultLog.Exclude)
|
defaultLog.Exclude = slices.Compact[[]string, string](defaultLog.Exclude)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// we may have not actually added anything, so remove if empty
|
// we may have not actually added anything, so remove if empty
|
||||||
@@ -416,20 +402,6 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
|
|||||||
options[opt] = append(existingOpts, logOpts...)
|
options[opt] = append(existingOpts, logOpts...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Also fold multiple "default_bind" options together into an
|
|
||||||
// array so that server blocks can have multiple binds by default.
|
|
||||||
if opt == "default_bind" {
|
|
||||||
existingOpts, ok := options[opt].([]ConfigValue)
|
|
||||||
if !ok {
|
|
||||||
existingOpts = []ConfigValue{}
|
|
||||||
}
|
|
||||||
defaultBindOpts, ok := val.([]ConfigValue)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("unexpected type from 'default_bind' global options: %T", val)
|
|
||||||
}
|
|
||||||
options[opt] = append(existingOpts, defaultBindOpts...)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
options[opt] = val
|
options[opt] = val
|
||||||
}
|
}
|
||||||
@@ -548,8 +520,8 @@ func (st *ServerType) serversFromPairings(
|
|||||||
if hsp, ok := options["https_port"].(int); ok {
|
if hsp, ok := options["https_port"].(int); ok {
|
||||||
httpsPort = strconv.Itoa(hsp)
|
httpsPort = strconv.Itoa(hsp)
|
||||||
}
|
}
|
||||||
autoHTTPS := []string{}
|
autoHTTPS := "on"
|
||||||
if ah, ok := options["auto_https"].([]string); ok {
|
if ah, ok := options["auto_https"].(string); ok {
|
||||||
autoHTTPS = ah
|
autoHTTPS = ah
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,74 +536,28 @@ func (st *ServerType) serversFromPairings(
|
|||||||
if k == j {
|
if k == j {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if slices.Contains(sblock2.block.GetKeysText(), key) {
|
if sliceContains(sblock2.block.GetKeysText(), key) {
|
||||||
return nil, fmt.Errorf("ambiguous site definition: %s", key)
|
return nil, fmt.Errorf("ambiguous site definition: %s", key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
addresses []string
|
|
||||||
protocols [][]string
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, addressWithProtocols := range p.addressesWithProtocols {
|
|
||||||
addresses = append(addresses, addressWithProtocols.address)
|
|
||||||
protocols = append(protocols, addressWithProtocols.protocols)
|
|
||||||
}
|
|
||||||
|
|
||||||
srv := &caddyhttp.Server{
|
srv := &caddyhttp.Server{
|
||||||
Listen: addresses,
|
Listen: p.addresses,
|
||||||
ListenProtocols: protocols,
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove srv.ListenProtocols[j] if it only contains the default protocols
|
|
||||||
for j, lnProtocols := range srv.ListenProtocols {
|
|
||||||
srv.ListenProtocols[j] = nil
|
|
||||||
for _, lnProtocol := range lnProtocols {
|
|
||||||
if lnProtocol != "" {
|
|
||||||
srv.ListenProtocols[j] = lnProtocols
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove srv.ListenProtocols if it only contains the default protocols for all listen addresses
|
|
||||||
listenProtocols := srv.ListenProtocols
|
|
||||||
srv.ListenProtocols = nil
|
|
||||||
for _, lnProtocols := range listenProtocols {
|
|
||||||
if lnProtocols != nil {
|
|
||||||
srv.ListenProtocols = listenProtocols
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle the auto_https global option
|
// handle the auto_https global option
|
||||||
for _, val := range autoHTTPS {
|
if autoHTTPS != "on" {
|
||||||
switch val {
|
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
|
||||||
|
switch autoHTTPS {
|
||||||
case "off":
|
case "off":
|
||||||
if srv.AutoHTTPS == nil {
|
|
||||||
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
|
|
||||||
}
|
|
||||||
srv.AutoHTTPS.Disabled = true
|
srv.AutoHTTPS.Disabled = true
|
||||||
|
|
||||||
case "disable_redirects":
|
case "disable_redirects":
|
||||||
if srv.AutoHTTPS == nil {
|
|
||||||
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
|
|
||||||
}
|
|
||||||
srv.AutoHTTPS.DisableRedir = true
|
srv.AutoHTTPS.DisableRedir = true
|
||||||
|
|
||||||
case "disable_certs":
|
case "disable_certs":
|
||||||
if srv.AutoHTTPS == nil {
|
|
||||||
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
|
|
||||||
}
|
|
||||||
srv.AutoHTTPS.DisableCerts = true
|
srv.AutoHTTPS.DisableCerts = true
|
||||||
|
|
||||||
case "ignore_loaded_certs":
|
case "ignore_loaded_certs":
|
||||||
if srv.AutoHTTPS == nil {
|
|
||||||
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
|
|
||||||
}
|
|
||||||
srv.AutoHTTPS.IgnoreLoadedCerts = true
|
srv.AutoHTTPS.IgnoreLoadedCerts = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -640,7 +566,7 @@ func (st *ServerType) serversFromPairings(
|
|||||||
// See ParseAddress() where parsing should later reject paths
|
// See ParseAddress() where parsing should later reject paths
|
||||||
// See https://github.com/caddyserver/caddy/pull/4728 for a full explanation
|
// See https://github.com/caddyserver/caddy/pull/4728 for a full explanation
|
||||||
for _, sblock := range p.serverBlocks {
|
for _, sblock := range p.serverBlocks {
|
||||||
for _, addr := range sblock.parsedKeys {
|
for _, addr := range sblock.keys {
|
||||||
if addr.Path != "" {
|
if addr.Path != "" {
|
||||||
caddy.Log().Named("caddyfile").Warn("Using a path in a site address is deprecated; please use the 'handle' directive instead", zap.String("address", addr.String()))
|
caddy.Log().Named("caddyfile").Warn("Using a path in a site address is deprecated; please use the 'handle' directive instead", zap.String("address", addr.String()))
|
||||||
}
|
}
|
||||||
@@ -658,7 +584,7 @@ func (st *ServerType) serversFromPairings(
|
|||||||
var iLongestPath, jLongestPath string
|
var iLongestPath, jLongestPath string
|
||||||
var iLongestHost, jLongestHost string
|
var iLongestHost, jLongestHost string
|
||||||
var iWildcardHost, jWildcardHost bool
|
var iWildcardHost, jWildcardHost bool
|
||||||
for _, addr := range p.serverBlocks[i].parsedKeys {
|
for _, addr := range p.serverBlocks[i].keys {
|
||||||
if strings.Contains(addr.Host, "*") || addr.Host == "" {
|
if strings.Contains(addr.Host, "*") || addr.Host == "" {
|
||||||
iWildcardHost = true
|
iWildcardHost = true
|
||||||
}
|
}
|
||||||
@@ -669,7 +595,7 @@ func (st *ServerType) serversFromPairings(
|
|||||||
iLongestPath = addr.Path
|
iLongestPath = addr.Path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, addr := range p.serverBlocks[j].parsedKeys {
|
for _, addr := range p.serverBlocks[j].keys {
|
||||||
if strings.Contains(addr.Host, "*") || addr.Host == "" {
|
if strings.Contains(addr.Host, "*") || addr.Host == "" {
|
||||||
jWildcardHost = true
|
jWildcardHost = true
|
||||||
}
|
}
|
||||||
@@ -701,7 +627,7 @@ func (st *ServerType) serversFromPairings(
|
|||||||
})
|
})
|
||||||
|
|
||||||
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
|
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
|
||||||
autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled
|
autoHTTPSWillAddConnPolicy := autoHTTPS != "off"
|
||||||
|
|
||||||
// if needed, the ServerLogConfig is initialized beforehand so
|
// if needed, the ServerLogConfig is initialized beforehand so
|
||||||
// that all server blocks can populate it with data, even when not
|
// that all server blocks can populate it with data, even when not
|
||||||
@@ -747,14 +673,6 @@ func (st *ServerType) serversFromPairings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// collect hosts that are forced to be automated
|
|
||||||
forceAutomatedNames := make(map[string]struct{})
|
|
||||||
if _, ok := sblock.pile["tls.force_automate"]; ok {
|
|
||||||
for _, host := range hosts {
|
|
||||||
forceAutomatedNames[host] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tls: connection policies
|
// tls: connection policies
|
||||||
if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
|
if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
|
||||||
// tls connection policies
|
// tls connection policies
|
||||||
@@ -785,21 +703,15 @@ func (st *ServerType) serversFromPairings(
|
|||||||
cp.FallbackSNI = fallbackSNI
|
cp.FallbackSNI = fallbackSNI
|
||||||
}
|
}
|
||||||
|
|
||||||
// only append this policy if it actually changes something,
|
// only append this policy if it actually changes something
|
||||||
// or if the configuration explicitly automates certs for
|
if !cp.SettingsEmpty() {
|
||||||
// these names (this is necessary to hoist a connection policy
|
|
||||||
// above one that may manually load a wildcard cert that would
|
|
||||||
// otherwise clobber the automated one; the code that appends
|
|
||||||
// policies that manually load certs comes later, so they're
|
|
||||||
// lower in the list)
|
|
||||||
if !cp.SettingsEmpty() || mapContains(forceAutomatedNames, hosts) {
|
|
||||||
srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
|
srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
|
||||||
hasCatchAllTLSConnPolicy = len(hosts) == 0
|
hasCatchAllTLSConnPolicy = len(hosts) == 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, addr := range sblock.parsedKeys {
|
for _, addr := range sblock.keys {
|
||||||
// if server only uses HTTP port, auto-HTTPS will not apply
|
// if server only uses HTTP port, auto-HTTPS will not apply
|
||||||
if listenersUseAnyPortOtherThan(srv.Listen, httpPort) {
|
if listenersUseAnyPortOtherThan(srv.Listen, httpPort) {
|
||||||
// exclude any hosts that were defined explicitly with "http://"
|
// exclude any hosts that were defined explicitly with "http://"
|
||||||
@@ -808,7 +720,7 @@ func (st *ServerType) serversFromPairings(
|
|||||||
if srv.AutoHTTPS == nil {
|
if srv.AutoHTTPS == nil {
|
||||||
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
|
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
|
||||||
}
|
}
|
||||||
if !slices.Contains(srv.AutoHTTPS.Skip, addr.Host) {
|
if !sliceContains(srv.AutoHTTPS.Skip, addr.Host) {
|
||||||
srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, addr.Host)
|
srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, addr.Host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -822,7 +734,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 && !sliceContains(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
|
||||||
@@ -830,7 +742,6 @@ func (st *ServerType) serversFromPairings(
|
|||||||
(addr.Scheme != "http" && addr.Port != httpPort && hasTLSEnabled) {
|
(addr.Scheme != "http" && addr.Port != httpPort && hasTLSEnabled) {
|
||||||
addressQualifiesForTLS = true
|
addressQualifiesForTLS = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// predict whether auto-HTTPS will add the conn policy for us; if so, we
|
// predict whether auto-HTTPS will add the conn policy for us; if so, we
|
||||||
// may not need to add one for this server
|
// may not need to add one for this server
|
||||||
autoHTTPSWillAddConnPolicy = autoHTTPSWillAddConnPolicy &&
|
autoHTTPSWillAddConnPolicy = autoHTTPSWillAddConnPolicy &&
|
||||||
@@ -851,20 +762,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)
|
||||||
@@ -976,10 +873,7 @@ func (st *ServerType) serversFromPairings(
|
|||||||
if addressQualifiesForTLS &&
|
if addressQualifiesForTLS &&
|
||||||
!hasCatchAllTLSConnPolicy &&
|
!hasCatchAllTLSConnPolicy &&
|
||||||
(len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "" || fallbackSNI != "") {
|
(len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "" || fallbackSNI != "") {
|
||||||
srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{
|
srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI, FallbackSNI: fallbackSNI})
|
||||||
DefaultSNI: defaultSNI,
|
|
||||||
FallbackSNI: fallbackSNI,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// tidy things up a bit
|
// tidy things up a bit
|
||||||
@@ -992,7 +886,8 @@ func (st *ServerType) serversFromPairings(
|
|||||||
servers[fmt.Sprintf("srv%d", i)] = srv
|
servers[fmt.Sprintf("srv%d", i)] = srv
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := applyServerOptions(servers, options, warnings); err != nil {
|
err := applyServerOptions(servers, options, warnings)
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("applying global server options: %v", err)
|
return nil, fmt.Errorf("applying global server options: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1037,7 +932,7 @@ func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock,
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, sblock := range serverBlocks {
|
for _, sblock := range serverBlocks {
|
||||||
for _, addr := range sblock.parsedKeys {
|
for _, addr := range sblock.keys {
|
||||||
if addr.Scheme == "http" || addr.Port == httpPort {
|
if addr.Scheme == "http" || addr.Port == httpPort {
|
||||||
if err := checkAndSetHTTP(addr); err != nil {
|
if err := checkAndSetHTTP(addr); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1075,40 +970,11 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
|
|||||||
|
|
||||||
// if they're exactly equal in every way, just keep one of them
|
// if they're exactly equal in every way, just keep one of them
|
||||||
if reflect.DeepEqual(cps[i], cps[j]) {
|
if reflect.DeepEqual(cps[i], cps[j]) {
|
||||||
cps = slices.Delete(cps, j, j+1)
|
cps = append(cps[:j], cps[j+1:]...)
|
||||||
i--
|
i--
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// as a special case, if there are adjacent TLS conn policies that are identical except
|
|
||||||
// by their matchers, and the matchers are specifically just ServerName ("sni") matchers
|
|
||||||
// (by far the most common), we can combine them into a single policy
|
|
||||||
if i == j-1 && len(cps[i].MatchersRaw) == 1 && len(cps[j].MatchersRaw) == 1 {
|
|
||||||
if iSNIMatcherJSON, ok := cps[i].MatchersRaw["sni"]; ok {
|
|
||||||
if jSNIMatcherJSON, ok := cps[j].MatchersRaw["sni"]; ok {
|
|
||||||
// position of policies and the matcher criteria check out; if settings are
|
|
||||||
// the same, then we can combine the policies; we have to unmarshal and
|
|
||||||
// remarshal the matchers though
|
|
||||||
if cps[i].SettingsEqual(*cps[j]) {
|
|
||||||
var iSNIMatcher caddytls.MatchServerName
|
|
||||||
if err := json.Unmarshal(iSNIMatcherJSON, &iSNIMatcher); err == nil {
|
|
||||||
var jSNIMatcher caddytls.MatchServerName
|
|
||||||
if err := json.Unmarshal(jSNIMatcherJSON, &jSNIMatcher); err == nil {
|
|
||||||
iSNIMatcher = append(iSNIMatcher, jSNIMatcher...)
|
|
||||||
cps[i].MatchersRaw["sni"], err = json.Marshal(iSNIMatcher)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("recombining SNI matchers: %v", err)
|
|
||||||
}
|
|
||||||
cps = slices.Delete(cps, j, j+1)
|
|
||||||
i--
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if they have the same matcher, try to reconcile each field: either they must
|
// if they have the same matcher, try to reconcile each field: either they must
|
||||||
// be identical, or we have to be able to combine them safely
|
// be identical, or we have to be able to combine them safely
|
||||||
if reflect.DeepEqual(cps[i].MatchersRaw, cps[j].MatchersRaw) {
|
if reflect.DeepEqual(cps[i].MatchersRaw, cps[j].MatchersRaw) {
|
||||||
@@ -1142,12 +1008,6 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
|
|||||||
return nil, fmt.Errorf("two policies with same match criteria have conflicting default SNI: %s vs. %s",
|
return nil, fmt.Errorf("two policies with same match criteria have conflicting default SNI: %s vs. %s",
|
||||||
cps[i].DefaultSNI, cps[j].DefaultSNI)
|
cps[i].DefaultSNI, cps[j].DefaultSNI)
|
||||||
}
|
}
|
||||||
if cps[i].FallbackSNI != "" &&
|
|
||||||
cps[j].FallbackSNI != "" &&
|
|
||||||
cps[i].FallbackSNI != cps[j].FallbackSNI {
|
|
||||||
return nil, fmt.Errorf("two policies with same match criteria have conflicting fallback SNI: %s vs. %s",
|
|
||||||
cps[i].FallbackSNI, cps[j].FallbackSNI)
|
|
||||||
}
|
|
||||||
if cps[i].ProtocolMin != "" &&
|
if cps[i].ProtocolMin != "" &&
|
||||||
cps[j].ProtocolMin != "" &&
|
cps[j].ProtocolMin != "" &&
|
||||||
cps[i].ProtocolMin != cps[j].ProtocolMin {
|
cps[i].ProtocolMin != cps[j].ProtocolMin {
|
||||||
@@ -1188,9 +1048,6 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
|
|||||||
if cps[i].DefaultSNI == "" && cps[j].DefaultSNI != "" {
|
if cps[i].DefaultSNI == "" && cps[j].DefaultSNI != "" {
|
||||||
cps[i].DefaultSNI = cps[j].DefaultSNI
|
cps[i].DefaultSNI = cps[j].DefaultSNI
|
||||||
}
|
}
|
||||||
if cps[i].FallbackSNI == "" && cps[j].FallbackSNI != "" {
|
|
||||||
cps[i].FallbackSNI = cps[j].FallbackSNI
|
|
||||||
}
|
|
||||||
if cps[i].ProtocolMin == "" && cps[j].ProtocolMin != "" {
|
if cps[i].ProtocolMin == "" && cps[j].ProtocolMin != "" {
|
||||||
cps[i].ProtocolMin = cps[j].ProtocolMin
|
cps[i].ProtocolMin = cps[j].ProtocolMin
|
||||||
}
|
}
|
||||||
@@ -1204,19 +1061,18 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
|
|||||||
} else if cps[i].CertSelection != nil && cps[j].CertSelection != nil {
|
} else if cps[i].CertSelection != nil && cps[j].CertSelection != nil {
|
||||||
// if both have one, then combine AnyTag
|
// if both have one, then combine AnyTag
|
||||||
for _, tag := range cps[j].CertSelection.AnyTag {
|
for _, tag := range cps[j].CertSelection.AnyTag {
|
||||||
if !slices.Contains(cps[i].CertSelection.AnyTag, tag) {
|
if !sliceContains(cps[i].CertSelection.AnyTag, tag) {
|
||||||
cps[i].CertSelection.AnyTag = append(cps[i].CertSelection.AnyTag, tag)
|
cps[i].CertSelection.AnyTag = append(cps[i].CertSelection.AnyTag, tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cps = slices.Delete(cps, j, j+1)
|
cps = append(cps[:j], cps[j+1:]...)
|
||||||
i--
|
i--
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cps, nil
|
return cps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1288,7 +1144,7 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
|
|||||||
func buildSubroute(routes []ConfigValue, groupCounter counter, needsSorting bool) (*caddyhttp.Subroute, error) {
|
func buildSubroute(routes []ConfigValue, groupCounter counter, needsSorting bool) (*caddyhttp.Subroute, error) {
|
||||||
if needsSorting {
|
if needsSorting {
|
||||||
for _, val := range routes {
|
for _, val := range routes {
|
||||||
if !slices.Contains(directiveOrder, val.directive) {
|
if !directiveIsOrdered(val.directive) {
|
||||||
return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here - try placing within a route block or using the order global option", val.directive)
|
return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here - try placing within a route block or using the order global option", val.directive)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1466,7 +1322,7 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod
|
|||||||
var matcherPairs []*hostPathPair
|
var matcherPairs []*hostPathPair
|
||||||
|
|
||||||
var catchAllHosts bool
|
var catchAllHosts bool
|
||||||
for _, addr := range sblock.parsedKeys {
|
for _, addr := range sblock.keys {
|
||||||
// choose a matcher pair that should be shared by this
|
// choose a matcher pair that should be shared by this
|
||||||
// server block; if none exists yet, create one
|
// server block; if none exists yet, create one
|
||||||
var chosenMatcherPair *hostPathPair
|
var chosenMatcherPair *hostPathPair
|
||||||
@@ -1498,16 +1354,25 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod
|
|||||||
|
|
||||||
// add this server block's keys to the matcher
|
// add this server block's keys to the matcher
|
||||||
// pair if it doesn't already exist
|
// pair if it doesn't already exist
|
||||||
if addr.Host != "" && !slices.Contains(chosenMatcherPair.hostm, addr.Host) {
|
if addr.Host != "" {
|
||||||
chosenMatcherPair.hostm = append(chosenMatcherPair.hostm, addr.Host)
|
var found bool
|
||||||
|
for _, h := range chosenMatcherPair.hostm {
|
||||||
|
if h == addr.Host {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
chosenMatcherPair.hostm = append(chosenMatcherPair.hostm, addr.Host)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// iterate each pairing of host and path matchers and
|
// iterate each pairing of host and path matchers and
|
||||||
// put them into a map for JSON encoding
|
// put them into a map for JSON encoding
|
||||||
var matcherSets []map[string]caddyhttp.RequestMatcherWithError
|
var matcherSets []map[string]caddyhttp.RequestMatcher
|
||||||
for _, mp := range matcherPairs {
|
for _, mp := range matcherPairs {
|
||||||
matcherSet := make(map[string]caddyhttp.RequestMatcherWithError)
|
matcherSet := make(map[string]caddyhttp.RequestMatcher)
|
||||||
if len(mp.hostm) > 0 {
|
if len(mp.hostm) > 0 {
|
||||||
matcherSet["host"] = mp.hostm
|
matcherSet["host"] = mp.hostm
|
||||||
}
|
}
|
||||||
@@ -1566,17 +1431,12 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
rm, ok := unm.(caddyhttp.RequestMatcher)
|
||||||
if rm, ok := unm.(caddyhttp.RequestMatcherWithError); ok {
|
if !ok {
|
||||||
matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
|
return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
// nolint:staticcheck
|
matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
|
||||||
if rm, ok := unm.(caddyhttp.RequestMatcher); ok {
|
return nil
|
||||||
matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the next token is quoted, we can assume it's not a matcher name
|
// if the next token is quoted, we can assume it's not a matcher name
|
||||||
@@ -1620,7 +1480,7 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcherWithError) (caddy.ModuleMap, error) {
|
func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.ModuleMap, error) {
|
||||||
msEncoded := make(caddy.ModuleMap)
|
msEncoded := make(caddy.ModuleMap)
|
||||||
for matcherName, val := range matchers {
|
for matcherName, val := range matchers {
|
||||||
jsonBytes, err := json.Marshal(val)
|
jsonBytes, err := json.Marshal(val)
|
||||||
@@ -1680,6 +1540,16 @@ func tryDuration(val any, warnings *[]caddyconfig.Warning) caddy.Duration {
|
|||||||
return durationVal
|
return durationVal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sliceContains returns true if needle is in haystack.
|
||||||
|
func sliceContains(haystack []string, needle string) bool {
|
||||||
|
for _, s := range haystack {
|
||||||
|
if s == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// listenersUseAnyPortOtherThan returns true if there are any
|
// listenersUseAnyPortOtherThan returns true if there are any
|
||||||
// listeners in addresses that use a port which is not otherPort.
|
// listeners in addresses that use a port which is not otherPort.
|
||||||
// Mostly borrowed from unexported method in caddyhttp package.
|
// Mostly borrowed from unexported method in caddyhttp package.
|
||||||
@@ -1700,18 +1570,6 @@ func listenersUseAnyPortOtherThan(addresses []string, otherPort string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapContains[K comparable, V any](m map[K]V, keys []K) bool {
|
|
||||||
if len(m) == 0 || len(keys) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, key := range keys {
|
|
||||||
if _, ok := m[key]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// specificity returns len(s) minus any wildcards (*) and
|
// specificity returns len(s) minus any wildcards (*) and
|
||||||
// placeholders ({...}). Basically, it's a length count
|
// placeholders ({...}). Basically, it's a length count
|
||||||
// that penalizes the use of wildcards and placeholders.
|
// that penalizes the use of wildcards and placeholders.
|
||||||
@@ -1755,19 +1613,12 @@ type namedCustomLog struct {
|
|||||||
noHostname bool
|
noHostname bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// addressWithProtocols associates a listen address with
|
|
||||||
// the protocols to serve it with
|
|
||||||
type addressWithProtocols struct {
|
|
||||||
address string
|
|
||||||
protocols []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// sbAddrAssociation is a mapping from a list of
|
// sbAddrAssociation is a mapping from a list of
|
||||||
// addresses with protocols, and a list of server
|
// addresses to a list of server blocks that are
|
||||||
// blocks that are served on those addresses.
|
// served on those addresses.
|
||||||
type sbAddrAssociation struct {
|
type sbAddrAssociation struct {
|
||||||
addressesWithProtocols []addressWithProtocols
|
addresses []string
|
||||||
serverBlocks []serverBlock
|
serverBlocks []serverBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -15,17 +15,14 @@
|
|||||||
package httpcaddyfile
|
package httpcaddyfile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/libdns/libdns"
|
"github.com/mholt/acmez/v2/acme"
|
||||||
"github.com/mholt/acmez/v3/acme"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,20 +30,19 @@ func init() {
|
|||||||
RegisterGlobalOption("debug", parseOptTrue)
|
RegisterGlobalOption("debug", parseOptTrue)
|
||||||
RegisterGlobalOption("http_port", parseOptHTTPPort)
|
RegisterGlobalOption("http_port", parseOptHTTPPort)
|
||||||
RegisterGlobalOption("https_port", parseOptHTTPSPort)
|
RegisterGlobalOption("https_port", parseOptHTTPSPort)
|
||||||
RegisterGlobalOption("default_bind", parseOptDefaultBind)
|
RegisterGlobalOption("default_bind", parseOptStringList)
|
||||||
RegisterGlobalOption("grace_period", parseOptDuration)
|
RegisterGlobalOption("grace_period", parseOptDuration)
|
||||||
RegisterGlobalOption("shutdown_delay", parseOptDuration)
|
RegisterGlobalOption("shutdown_delay", parseOptDuration)
|
||||||
RegisterGlobalOption("default_sni", parseOptSingleString)
|
RegisterGlobalOption("default_sni", parseOptSingleString)
|
||||||
RegisterGlobalOption("fallback_sni", parseOptSingleString)
|
RegisterGlobalOption("fallback_sni", parseOptSingleString)
|
||||||
RegisterGlobalOption("order", parseOptOrder)
|
RegisterGlobalOption("order", parseOptOrder)
|
||||||
RegisterGlobalOption("storage", parseOptStorage)
|
RegisterGlobalOption("storage", parseOptStorage)
|
||||||
RegisterGlobalOption("storage_check", parseStorageCheck)
|
RegisterGlobalOption("storage_clean_interval", parseOptDuration)
|
||||||
RegisterGlobalOption("storage_clean_interval", parseStorageCleanInterval)
|
|
||||||
RegisterGlobalOption("renew_interval", parseOptDuration)
|
RegisterGlobalOption("renew_interval", parseOptDuration)
|
||||||
RegisterGlobalOption("ocsp_interval", parseOptDuration)
|
RegisterGlobalOption("ocsp_interval", parseOptDuration)
|
||||||
RegisterGlobalOption("acme_ca", parseOptSingleString)
|
RegisterGlobalOption("acme_ca", parseOptSingleString)
|
||||||
RegisterGlobalOption("acme_ca_root", parseOptSingleString)
|
RegisterGlobalOption("acme_ca_root", parseOptSingleString)
|
||||||
RegisterGlobalOption("acme_dns", parseOptDNS)
|
RegisterGlobalOption("acme_dns", parseOptACMEDNS)
|
||||||
RegisterGlobalOption("acme_eab", parseOptACMEEAB)
|
RegisterGlobalOption("acme_eab", parseOptACMEEAB)
|
||||||
RegisterGlobalOption("cert_issuer", parseOptCertIssuer)
|
RegisterGlobalOption("cert_issuer", parseOptCertIssuer)
|
||||||
RegisterGlobalOption("skip_install_trust", parseOptTrue)
|
RegisterGlobalOption("skip_install_trust", parseOptTrue)
|
||||||
@@ -56,15 +52,12 @@ func init() {
|
|||||||
RegisterGlobalOption("local_certs", parseOptTrue)
|
RegisterGlobalOption("local_certs", parseOptTrue)
|
||||||
RegisterGlobalOption("key_type", parseOptSingleString)
|
RegisterGlobalOption("key_type", parseOptSingleString)
|
||||||
RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
|
RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
|
||||||
RegisterGlobalOption("metrics", parseMetricsOptions)
|
|
||||||
RegisterGlobalOption("servers", parseServerOptions)
|
RegisterGlobalOption("servers", parseServerOptions)
|
||||||
RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
|
RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
|
||||||
RegisterGlobalOption("cert_lifetime", parseOptDuration)
|
RegisterGlobalOption("cert_lifetime", parseOptDuration)
|
||||||
RegisterGlobalOption("log", parseLogOptions)
|
RegisterGlobalOption("log", parseLogOptions)
|
||||||
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
|
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
|
||||||
RegisterGlobalOption("persist_config", parseOptPersistConfig)
|
RegisterGlobalOption("persist_config", parseOptPersistConfig)
|
||||||
RegisterGlobalOption("dns", parseOptDNS)
|
|
||||||
RegisterGlobalOption("ech", parseOptECH)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil }
|
func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil }
|
||||||
@@ -117,12 +110,17 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
|
|||||||
}
|
}
|
||||||
pos := Positional(d.Val())
|
pos := Positional(d.Val())
|
||||||
|
|
||||||
// if directive already had an order, drop it
|
newOrder := directiveOrder
|
||||||
newOrder := slices.DeleteFunc(directiveOrder, func(d string) bool {
|
|
||||||
return d == dirName
|
|
||||||
})
|
|
||||||
|
|
||||||
// act on the positional; if it's First or Last, we're done right away
|
// if directive exists, first remove it
|
||||||
|
for i, d := range newOrder {
|
||||||
|
if d == dirName {
|
||||||
|
newOrder = append(newOrder[:i], newOrder[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// act on the positional
|
||||||
switch pos {
|
switch pos {
|
||||||
case First:
|
case First:
|
||||||
newOrder = append([]string{dirName}, newOrder...)
|
newOrder = append([]string{dirName}, newOrder...)
|
||||||
@@ -131,7 +129,6 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
|
|||||||
}
|
}
|
||||||
directiveOrder = newOrder
|
directiveOrder = newOrder
|
||||||
return newOrder, nil
|
return newOrder, nil
|
||||||
|
|
||||||
case Last:
|
case Last:
|
||||||
newOrder = append(newOrder, dirName)
|
newOrder = append(newOrder, dirName)
|
||||||
if d.NextArg() {
|
if d.NextArg() {
|
||||||
@@ -139,11 +136,8 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
|
|||||||
}
|
}
|
||||||
directiveOrder = newOrder
|
directiveOrder = newOrder
|
||||||
return newOrder, nil
|
return newOrder, nil
|
||||||
|
|
||||||
// if it's Before or After, continue
|
|
||||||
case Before:
|
case Before:
|
||||||
case After:
|
case After:
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, d.Errf("unknown positional '%s'", pos)
|
return nil, d.Errf("unknown positional '%s'", pos)
|
||||||
}
|
}
|
||||||
@@ -157,17 +151,17 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
|
|||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the position of the target directive
|
// insert directive into proper position
|
||||||
targetIndex := slices.Index(newOrder, otherDir)
|
for i, d := range newOrder {
|
||||||
if targetIndex == -1 {
|
if d == otherDir {
|
||||||
return nil, d.Errf("directive '%s' not found", otherDir)
|
if pos == Before {
|
||||||
|
newOrder = append(newOrder[:i], append([]string{dirName}, newOrder[i:]...)...)
|
||||||
|
} else if pos == After {
|
||||||
|
newOrder = append(newOrder[:i+1], append([]string{dirName}, newOrder[i+1:]...)...)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// if we're inserting after, we need to increment the index to go after
|
|
||||||
if pos == After {
|
|
||||||
targetIndex++
|
|
||||||
}
|
|
||||||
// insert the directive into the new order
|
|
||||||
newOrder = slices.Insert(newOrder, targetIndex, dirName)
|
|
||||||
|
|
||||||
directiveOrder = newOrder
|
directiveOrder = newOrder
|
||||||
|
|
||||||
@@ -193,40 +187,6 @@ func parseOptStorage(d *caddyfile.Dispenser, _ any) (any, error) {
|
|||||||
return storage, nil
|
return storage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseStorageCheck(d *caddyfile.Dispenser, _ any) (any, error) {
|
|
||||||
d.Next() // consume option name
|
|
||||||
if !d.Next() {
|
|
||||||
return "", d.ArgErr()
|
|
||||||
}
|
|
||||||
val := d.Val()
|
|
||||||
if d.Next() {
|
|
||||||
return "", d.ArgErr()
|
|
||||||
}
|
|
||||||
if val != "off" {
|
|
||||||
return "", d.Errf("storage_check must be 'off'")
|
|
||||||
}
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseStorageCleanInterval(d *caddyfile.Dispenser, _ any) (any, error) {
|
|
||||||
d.Next() // consume option name
|
|
||||||
if !d.Next() {
|
|
||||||
return "", d.ArgErr()
|
|
||||||
}
|
|
||||||
val := d.Val()
|
|
||||||
if d.Next() {
|
|
||||||
return "", d.ArgErr()
|
|
||||||
}
|
|
||||||
if val == "off" {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
dur, err := caddy.ParseDuration(d.Val())
|
|
||||||
if err != nil {
|
|
||||||
return nil, d.Errf("failed to parse storage_clean_interval, must be a duration or 'off' %w", err)
|
|
||||||
}
|
|
||||||
return caddy.Duration(dur), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) {
|
func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
if !d.Next() { // consume option name
|
if !d.Next() { // consume option name
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
@@ -241,6 +201,25 @@ func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) {
|
|||||||
return caddy.Duration(dur), nil
|
return caddy.Duration(dur), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
|
if !d.Next() { // consume option name
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
if !d.Next() { // get DNS module name
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
modID := "dns.providers." + d.Val()
|
||||||
|
unm, err := caddyfile.UnmarshalModule(d, modID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
prov, ok := unm.(certmagic.DNSProvider)
|
||||||
|
if !ok {
|
||||||
|
return nil, d.Errf("module %s (%T) is not a certmagic.DNSProvider", modID, unm)
|
||||||
|
}
|
||||||
|
return prov, nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) {
|
func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
eab := new(acme.EAB)
|
eab := new(acme.EAB)
|
||||||
d.Next() // consume option name
|
d.Next() // consume option name
|
||||||
@@ -305,32 +284,13 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) {
|
|||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptDefaultBind(d *caddyfile.Dispenser, _ any) (any, error) {
|
func parseOptStringList(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
d.Next() // consume option name
|
d.Next() // consume option name
|
||||||
|
val := d.RemainingArgs()
|
||||||
var addresses, protocols []string
|
if len(val) == 0 {
|
||||||
addresses = d.RemainingArgs()
|
return "", d.ArgErr()
|
||||||
|
|
||||||
if len(addresses) == 0 {
|
|
||||||
addresses = append(addresses, "")
|
|
||||||
}
|
}
|
||||||
|
return val, nil
|
||||||
for d.NextBlock(0) {
|
|
||||||
switch d.Val() {
|
|
||||||
case "protocols":
|
|
||||||
protocols = d.RemainingArgs()
|
|
||||||
if len(protocols) == 0 {
|
|
||||||
return nil, d.Errf("protocols requires one or more arguments")
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, d.Errf("unknown subdirective: %s", d.Val())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return []ConfigValue{{Class: "bind", Value: addressesWithProtocols{
|
|
||||||
addresses: addresses,
|
|
||||||
protocols: protocols,
|
|
||||||
}}}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) {
|
func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
@@ -415,10 +375,36 @@ func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) {
|
|||||||
ond.PermissionRaw = caddyconfig.JSONModuleObject(perm, "module", modName, nil)
|
ond.PermissionRaw = caddyconfig.JSONModuleObject(perm, "module", modName, nil)
|
||||||
|
|
||||||
case "interval":
|
case "interval":
|
||||||
return nil, d.Errf("the on_demand_tls 'interval' option is no longer supported, remove it from your config")
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
dur, err := caddy.ParseDuration(d.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ond == nil {
|
||||||
|
ond = new(caddytls.OnDemandConfig)
|
||||||
|
}
|
||||||
|
if ond.RateLimit == nil {
|
||||||
|
ond.RateLimit = new(caddytls.RateLimit)
|
||||||
|
}
|
||||||
|
ond.RateLimit.Interval = caddy.Duration(dur)
|
||||||
|
|
||||||
case "burst":
|
case "burst":
|
||||||
return nil, d.Errf("the on_demand_tls 'burst' option is no longer supported, remove it from your config")
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
burst, err := strconv.Atoi(d.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ond == nil {
|
||||||
|
ond = new(caddytls.OnDemandConfig)
|
||||||
|
}
|
||||||
|
if ond.RateLimit == nil {
|
||||||
|
ond.RateLimit = new(caddytls.RateLimit)
|
||||||
|
}
|
||||||
|
ond.RateLimit.Burst = burst
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
|
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
|
||||||
@@ -447,42 +433,19 @@ func parseOptPersistConfig(d *caddyfile.Dispenser, _ any) (any, error) {
|
|||||||
|
|
||||||
func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
|
func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
d.Next() // consume option name
|
d.Next() // consume option name
|
||||||
val := d.RemainingArgs()
|
if !d.Next() {
|
||||||
if len(val) == 0 {
|
|
||||||
return "", d.ArgErr()
|
return "", d.ArgErr()
|
||||||
}
|
}
|
||||||
for _, v := range val {
|
val := d.Val()
|
||||||
switch v {
|
if d.Next() {
|
||||||
case "off":
|
return "", d.ArgErr()
|
||||||
case "disable_redirects":
|
}
|
||||||
case "disable_certs":
|
if val != "off" && val != "disable_redirects" && val != "disable_certs" && val != "ignore_loaded_certs" {
|
||||||
case "ignore_loaded_certs":
|
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', or 'ignore_loaded_certs'")
|
||||||
case "prefer_wildcard":
|
|
||||||
default:
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmarshalCaddyfileMetricsOptions(d *caddyfile.Dispenser) (any, error) {
|
|
||||||
d.Next() // consume option name
|
|
||||||
metrics := new(caddyhttp.Metrics)
|
|
||||||
for d.NextBlock(0) {
|
|
||||||
switch d.Val() {
|
|
||||||
case "per_host":
|
|
||||||
metrics.PerHost = true
|
|
||||||
default:
|
|
||||||
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return metrics, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseMetricsOptions(d *caddyfile.Dispenser, _ any) (any, error) {
|
|
||||||
return unmarshalCaddyfileMetricsOptions(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseServerOptions(d *caddyfile.Dispenser, _ any) (any, error) {
|
func parseServerOptions(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
return unmarshalCaddyfileServerOptions(d)
|
return unmarshalCaddyfileServerOptions(d)
|
||||||
}
|
}
|
||||||
@@ -552,74 +515,3 @@ func parseOptPreferredChains(d *caddyfile.Dispenser, _ any) (any, error) {
|
|||||||
d.Next()
|
d.Next()
|
||||||
return caddytls.ParseCaddyfilePreferredChainsOptions(d)
|
return caddytls.ParseCaddyfilePreferredChainsOptions(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptDNS(d *caddyfile.Dispenser, _ any) (any, error) {
|
|
||||||
d.Next() // consume option name
|
|
||||||
optName := d.Val()
|
|
||||||
|
|
||||||
// get DNS module name
|
|
||||||
if !d.Next() {
|
|
||||||
// this is allowed if this is the "acme_dns" option since it may refer to the globally-configured "dns" option's value
|
|
||||||
if optName == "acme_dns" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
modID := "dns.providers." + d.Val()
|
|
||||||
unm, err := caddyfile.UnmarshalModule(d, modID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
switch unm.(type) {
|
|
||||||
case libdns.RecordGetter,
|
|
||||||
libdns.RecordSetter,
|
|
||||||
libdns.RecordAppender,
|
|
||||||
libdns.RecordDeleter:
|
|
||||||
default:
|
|
||||||
return nil, d.Errf("module %s (%T) is not a libdns provider", modID, unm)
|
|
||||||
}
|
|
||||||
return unm, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseOptECH(d *caddyfile.Dispenser, _ any) (any, error) {
|
|
||||||
d.Next() // consume option name
|
|
||||||
|
|
||||||
ech := new(caddytls.ECH)
|
|
||||||
|
|
||||||
publicNames := d.RemainingArgs()
|
|
||||||
for _, publicName := range publicNames {
|
|
||||||
ech.Configs = append(ech.Configs, caddytls.ECHConfiguration{
|
|
||||||
PublicName: publicName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(ech.Configs) == 0 {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
|
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
|
||||||
switch d.Val() {
|
|
||||||
case "dns":
|
|
||||||
if !d.Next() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
providerName := d.Val()
|
|
||||||
modID := "dns.providers." + providerName
|
|
||||||
unm, err := caddyfile.UnmarshalModule(d, modID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ech.Publication = append(ech.Publication, &caddytls.ECHPublication{
|
|
||||||
Configs: publicNames,
|
|
||||||
PublishersRaw: caddy.ModuleMap{
|
|
||||||
"dns": caddyconfig.JSON(caddytls.ECHDNSPublisher{
|
|
||||||
ProviderRaw: caddyconfig.JSONModuleObject(unm, "name", providerName, nil),
|
|
||||||
}, nil),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
return nil, d.Errf("ech: unrecognized subdirective '%s'", d.Val())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ech, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,8 +15,6 @@
|
|||||||
package httpcaddyfile
|
package httpcaddyfile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
@@ -180,15 +178,6 @@ func (st ServerType) buildPKIApp(
|
|||||||
if _, ok := options["skip_install_trust"]; ok {
|
if _, ok := options["skip_install_trust"]; ok {
|
||||||
skipInstallTrust = true
|
skipInstallTrust = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if auto_https is off - in that case we should not create
|
|
||||||
// any PKI infrastructure even with skip_install_trust directive
|
|
||||||
autoHTTPS := []string{}
|
|
||||||
if ah, ok := options["auto_https"].([]string); ok {
|
|
||||||
autoHTTPS = ah
|
|
||||||
}
|
|
||||||
autoHTTPSOff := slices.Contains(autoHTTPS, "off")
|
|
||||||
|
|
||||||
falseBool := false
|
falseBool := false
|
||||||
|
|
||||||
// Load the PKI app configured via global options
|
// Load the PKI app configured via global options
|
||||||
@@ -229,8 +218,7 @@ func (st ServerType) buildPKIApp(
|
|||||||
// if there was no CAs defined in any of the servers,
|
// if there was no CAs defined in any of the servers,
|
||||||
// and we were requested to not install trust, then
|
// and we were requested to not install trust, then
|
||||||
// add one for the default/local CA to do so
|
// add one for the default/local CA to do so
|
||||||
// only if auto_https is not completely disabled
|
if len(pkiApp.CAs) == 0 && skipInstallTrust {
|
||||||
if len(pkiApp.CAs) == 0 && skipInstallTrust && !autoHTTPSOff {
|
|
||||||
ca := new(caddypki.CA)
|
ca := new(caddypki.CA)
|
||||||
ca.ID = caddypki.DefaultCAID
|
ca.ID = caddypki.DefaultCAID
|
||||||
ca.InstallTrust = &falseBool
|
ca.InstallTrust = &falseBool
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ package httpcaddyfile
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
|
|
||||||
@@ -36,27 +34,23 @@ 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
|
MaxHeaderBytes int
|
||||||
KeepAliveIdle caddy.Duration
|
EnableFullDuplex bool
|
||||||
KeepAliveCount int
|
Protocols []string
|
||||||
MaxHeaderBytes int
|
StrictSNIHost *bool
|
||||||
EnableFullDuplex bool
|
TrustedProxiesRaw json.RawMessage
|
||||||
Protocols []string
|
TrustedProxiesStrict int
|
||||||
StrictSNIHost *bool
|
ClientIPHeaders []string
|
||||||
TrustedProxiesRaw json.RawMessage
|
ShouldLogCredentials bool
|
||||||
TrustedProxiesStrict int
|
Metrics *caddyhttp.Metrics
|
||||||
TrustedProxiesUnix bool
|
Trace bool // TODO: EXPERIMENTAL
|
||||||
ClientIPHeaders []string
|
|
||||||
ShouldLogCredentials bool
|
|
||||||
Metrics *caddyhttp.Metrics
|
|
||||||
Trace bool // TODO: EXPERIMENTAL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||||
@@ -100,26 +94,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() {
|
||||||
@@ -167,7 +141,6 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
|||||||
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
|
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "keepalive_interval":
|
case "keepalive_interval":
|
||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
@@ -178,26 +151,6 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
|||||||
}
|
}
|
||||||
serverOpts.KeepAliveInterval = caddy.Duration(dur)
|
serverOpts.KeepAliveInterval = caddy.Duration(dur)
|
||||||
|
|
||||||
case "keepalive_idle":
|
|
||||||
if !d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
dur, err := caddy.ParseDuration(d.Val())
|
|
||||||
if err != nil {
|
|
||||||
return nil, d.Errf("parsing keepalive idle duration: %v", err)
|
|
||||||
}
|
|
||||||
serverOpts.KeepAliveIdle = caddy.Duration(dur)
|
|
||||||
|
|
||||||
case "keepalive_count":
|
|
||||||
if !d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
cnt, err := strconv.ParseInt(d.Val(), 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return nil, d.Errf("parsing keepalive count int: %v", err)
|
|
||||||
}
|
|
||||||
serverOpts.KeepAliveCount = int(cnt)
|
|
||||||
|
|
||||||
case "max_header_size":
|
case "max_header_size":
|
||||||
var sizeStr string
|
var sizeStr string
|
||||||
if !d.AllArgs(&sizeStr) {
|
if !d.AllArgs(&sizeStr) {
|
||||||
@@ -227,7 +180,7 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
|||||||
if proto != "h1" && proto != "h2" && proto != "h2c" && proto != "h3" {
|
if proto != "h1" && proto != "h2" && proto != "h2c" && proto != "h3" {
|
||||||
return nil, d.Errf("unknown protocol '%s': expected h1, h2, h2c, or h3", proto)
|
return nil, d.Errf("unknown protocol '%s': expected h1, h2, h2c, or h3", proto)
|
||||||
}
|
}
|
||||||
if slices.Contains(serverOpts.Protocols, proto) {
|
if sliceContains(serverOpts.Protocols, proto) {
|
||||||
return nil, d.Errf("protocol %s specified more than once", proto)
|
return nil, d.Errf("protocol %s specified more than once", proto)
|
||||||
}
|
}
|
||||||
serverOpts.Protocols = append(serverOpts.Protocols, proto)
|
serverOpts.Protocols = append(serverOpts.Protocols, proto)
|
||||||
@@ -273,16 +226,10 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
|||||||
}
|
}
|
||||||
serverOpts.TrustedProxiesStrict = 1
|
serverOpts.TrustedProxiesStrict = 1
|
||||||
|
|
||||||
case "trusted_proxies_unix":
|
|
||||||
if d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
serverOpts.TrustedProxiesUnix = true
|
|
||||||
|
|
||||||
case "client_ip_headers":
|
case "client_ip_headers":
|
||||||
headers := d.RemainingArgs()
|
headers := d.RemainingArgs()
|
||||||
for _, header := range headers {
|
for _, header := range headers {
|
||||||
if slices.Contains(serverOpts.ClientIPHeaders, header) {
|
if sliceContains(serverOpts.ClientIPHeaders, header) {
|
||||||
return nil, d.Errf("client IP header %s specified more than once", header)
|
return nil, d.Errf("client IP header %s specified more than once", header)
|
||||||
}
|
}
|
||||||
serverOpts.ClientIPHeaders = append(serverOpts.ClientIPHeaders, header)
|
serverOpts.ClientIPHeaders = append(serverOpts.ClientIPHeaders, header)
|
||||||
@@ -292,16 +239,13 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "metrics":
|
case "metrics":
|
||||||
caddy.Log().Warn("The nested 'metrics' option inside `servers` is deprecated and will be removed in the next major version. Use the global 'metrics' option instead.")
|
if d.NextArg() {
|
||||||
serverOpts.Metrics = new(caddyhttp.Metrics)
|
return nil, d.ArgErr()
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
|
||||||
switch d.Val() {
|
|
||||||
case "per_host":
|
|
||||||
serverOpts.Metrics.PerHost = true
|
|
||||||
default:
|
|
||||||
return nil, d.Errf("unrecognized metrics option '%s'", d.Val())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if nesting := d.Nesting(); d.NextBlock(nesting) {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
serverOpts.Metrics = new(caddyhttp.Metrics)
|
||||||
|
|
||||||
case "trace":
|
case "trace":
|
||||||
if d.NextArg() {
|
if d.NextArg() {
|
||||||
@@ -344,26 +288,32 @@ func applyServerOptions(
|
|||||||
|
|
||||||
for key, server := range servers {
|
for key, server := range servers {
|
||||||
// find the options that apply to this server
|
// find the options that apply to this server
|
||||||
optsIndex := slices.IndexFunc(serverOpts, func(s serverOptions) bool {
|
opts := func() *serverOptions {
|
||||||
return s.ListenerAddress == "" || slices.Contains(server.Listen, s.ListenerAddress)
|
for _, entry := range serverOpts {
|
||||||
})
|
if entry.ListenerAddress == "" {
|
||||||
|
return &entry
|
||||||
|
}
|
||||||
|
for _, listener := range server.Listen {
|
||||||
|
if entry.ListenerAddress == listener {
|
||||||
|
return &entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
|
||||||
// if none apply, then move to the next server
|
// if none apply, then move to the next server
|
||||||
if optsIndex == -1 {
|
if opts == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
opts := serverOpts[optsIndex]
|
|
||||||
|
|
||||||
// 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
|
||||||
server.IdleTimeout = opts.IdleTimeout
|
server.IdleTimeout = opts.IdleTimeout
|
||||||
server.KeepAliveInterval = opts.KeepAliveInterval
|
server.KeepAliveInterval = opts.KeepAliveInterval
|
||||||
server.KeepAliveIdle = opts.KeepAliveIdle
|
|
||||||
server.KeepAliveCount = opts.KeepAliveCount
|
|
||||||
server.MaxHeaderBytes = opts.MaxHeaderBytes
|
server.MaxHeaderBytes = opts.MaxHeaderBytes
|
||||||
server.EnableFullDuplex = opts.EnableFullDuplex
|
server.EnableFullDuplex = opts.EnableFullDuplex
|
||||||
server.Protocols = opts.Protocols
|
server.Protocols = opts.Protocols
|
||||||
@@ -371,7 +321,6 @@ func applyServerOptions(
|
|||||||
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
|
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
|
||||||
server.ClientIPHeaders = opts.ClientIPHeaders
|
server.ClientIPHeaders = opts.ClientIPHeaders
|
||||||
server.TrustedProxiesStrict = opts.TrustedProxiesStrict
|
server.TrustedProxiesStrict = opts.TrustedProxiesStrict
|
||||||
server.TrustedProxiesUnix = opts.TrustedProxiesUnix
|
|
||||||
server.Metrics = opts.Metrics
|
server.Metrics = opts.Metrics
|
||||||
if opts.ShouldLogCredentials {
|
if opts.ShouldLogCredentials {
|
||||||
if server.Logs == nil {
|
if server.Logs == nil {
|
||||||
|
|||||||
@@ -52,30 +52,19 @@ func NewShorthandReplacer() ShorthandReplacer {
|
|||||||
// be used in the Caddyfile, and the right is the replacement.
|
// be used in the Caddyfile, and the right is the replacement.
|
||||||
func placeholderShorthands() []string {
|
func placeholderShorthands() []string {
|
||||||
return []string{
|
return []string{
|
||||||
|
"{dir}", "{http.request.uri.path.dir}",
|
||||||
|
"{file}", "{http.request.uri.path.file}",
|
||||||
"{host}", "{http.request.host}",
|
"{host}", "{http.request.host}",
|
||||||
"{hostport}", "{http.request.hostport}",
|
"{hostport}", "{http.request.hostport}",
|
||||||
"{port}", "{http.request.port}",
|
"{port}", "{http.request.port}",
|
||||||
"{orig_method}", "{http.request.orig_method}",
|
|
||||||
"{orig_uri}", "{http.request.orig_uri}",
|
|
||||||
"{orig_path}", "{http.request.orig_uri.path}",
|
|
||||||
"{orig_dir}", "{http.request.orig_uri.path.dir}",
|
|
||||||
"{orig_file}", "{http.request.orig_uri.path.file}",
|
|
||||||
"{orig_query}", "{http.request.orig_uri.query}",
|
|
||||||
"{orig_?query}", "{http.request.orig_uri.prefixed_query}",
|
|
||||||
"{method}", "{http.request.method}",
|
"{method}", "{http.request.method}",
|
||||||
"{uri}", "{http.request.uri}",
|
|
||||||
"{%uri}", "{http.request.uri_escaped}",
|
|
||||||
"{path}", "{http.request.uri.path}",
|
"{path}", "{http.request.uri.path}",
|
||||||
"{%path}", "{http.request.uri.path_escaped}",
|
|
||||||
"{dir}", "{http.request.uri.path.dir}",
|
|
||||||
"{file}", "{http.request.uri.path.file}",
|
|
||||||
"{query}", "{http.request.uri.query}",
|
"{query}", "{http.request.uri.query}",
|
||||||
"{%query}", "{http.request.uri.query_escaped}",
|
|
||||||
"{?query}", "{http.request.uri.prefixed_query}",
|
|
||||||
"{remote}", "{http.request.remote}",
|
"{remote}", "{http.request.remote}",
|
||||||
"{remote_host}", "{http.request.remote.host}",
|
"{remote_host}", "{http.request.remote.host}",
|
||||||
"{remote_port}", "{http.request.remote.port}",
|
"{remote_port}", "{http.request.remote.port}",
|
||||||
"{scheme}", "{http.request.scheme}",
|
"{scheme}", "{http.request.scheme}",
|
||||||
|
"{uri}", "{http.request.uri}",
|
||||||
"{uuid}", "{http.request.uuid}",
|
"{uuid}", "{http.request.uuid}",
|
||||||
"{tls_cipher}", "{http.request.tls.cipher_suite}",
|
"{tls_cipher}", "{http.request.tls.cipher_suite}",
|
||||||
"{tls_version}", "{http.request.tls.version}",
|
"{tls_version}", "{http.request.tls.version}",
|
||||||
|
|||||||
@@ -19,13 +19,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/mholt/acmez/v3/acme"
|
"github.com/mholt/acmez/v2/acme"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
@@ -45,8 +44,8 @@ func (st ServerType) buildTLSApp(
|
|||||||
if hp, ok := options["http_port"].(int); ok {
|
if hp, ok := options["http_port"].(int); ok {
|
||||||
httpPort = strconv.Itoa(hp)
|
httpPort = strconv.Itoa(hp)
|
||||||
}
|
}
|
||||||
autoHTTPS := []string{}
|
autoHTTPS := "on"
|
||||||
if ah, ok := options["auto_https"].([]string); ok {
|
if ah, ok := options["auto_https"].(string); ok {
|
||||||
autoHTTPS = ah
|
autoHTTPS = ah
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,25 +53,23 @@ func (st ServerType) buildTLSApp(
|
|||||||
// key, so that they don't get forgotten/omitted by auto-HTTPS
|
// key, so that they don't get forgotten/omitted by auto-HTTPS
|
||||||
// (since they won't appear in route matchers)
|
// (since they won't appear in route matchers)
|
||||||
httpsHostsSharedWithHostlessKey := make(map[string]struct{})
|
httpsHostsSharedWithHostlessKey := make(map[string]struct{})
|
||||||
if !slices.Contains(autoHTTPS, "off") {
|
if autoHTTPS != "off" {
|
||||||
for _, pair := range pairings {
|
for _, pair := range pairings {
|
||||||
for _, sb := range pair.serverBlocks {
|
for _, sb := range pair.serverBlocks {
|
||||||
for _, addr := range sb.parsedKeys {
|
for _, addr := range sb.keys {
|
||||||
if addr.Host != "" {
|
if addr.Host == "" {
|
||||||
continue
|
// this server block has a hostless key, now
|
||||||
}
|
// go through and add all the hosts to the set
|
||||||
|
for _, otherAddr := range sb.keys {
|
||||||
// this server block has a hostless key, now
|
if otherAddr.Original == addr.Original {
|
||||||
// go through and add all the hosts to the set
|
continue
|
||||||
for _, otherAddr := range sb.parsedKeys {
|
}
|
||||||
if otherAddr.Original == addr.Original {
|
if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
|
||||||
continue
|
httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
|
||||||
}
|
}
|
||||||
if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
|
|
||||||
httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
|
|
||||||
}
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,33 +89,9 @@ 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
|
|
||||||
|
|
||||||
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
|
if !listenersUseAnyPortOtherThan(p.addresses, httpPort) {
|
||||||
for _, addressWithProtocols := range p.addressesWithProtocols {
|
|
||||||
addresses = append(addresses, addressWithProtocols.address)
|
|
||||||
}
|
|
||||||
if !listenersUseAnyPortOtherThan(addresses, httpPort) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,12 +108,6 @@ func (st ServerType) buildTLSApp(
|
|||||||
return nil, warnings, err
|
return nil, warnings, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// make a plain copy so we can compare whether we made any changes
|
|
||||||
apCopy, err := newBaseAutomationPolicy(options, warnings, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, warnings, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sblockHosts := sblock.hostsFromKeys(false)
|
sblockHosts := sblock.hostsFromKeys(false)
|
||||||
if len(sblockHosts) == 0 && catchAllAP != nil {
|
if len(sblockHosts) == 0 && catchAllAP != nil {
|
||||||
ap = catchAllAP
|
ap = catchAllAP
|
||||||
@@ -151,13 +118,6 @@ func (st ServerType) buildTLSApp(
|
|||||||
ap.OnDemand = true
|
ap.OnDemand = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// collect hosts that are forced to have certs automated for their specific name
|
|
||||||
if _, ok := sblock.pile["tls.force_automate"]; ok {
|
|
||||||
for _, host := range sblockHosts {
|
|
||||||
forcedAutomatedNames[host] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// reuse private keys tls
|
// reuse private keys tls
|
||||||
if _, ok := sblock.pile["tls.reuse_private_keys"]; ok {
|
if _, ok := sblock.pile["tls.reuse_private_keys"]; ok {
|
||||||
ap.ReusePrivateKeys = true
|
ap.ReusePrivateKeys = true
|
||||||
@@ -221,8 +181,8 @@ func (st ServerType) buildTLSApp(
|
|||||||
if acmeIssuer.Challenges.BindHost == "" {
|
if acmeIssuer.Challenges.BindHost == "" {
|
||||||
// only binding to one host is supported
|
// only binding to one host is supported
|
||||||
var bindHost string
|
var bindHost string
|
||||||
if asserted, ok := cfgVal.Value.(addressesWithProtocols); ok && len(asserted.addresses) > 0 {
|
if bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 {
|
||||||
bindHost = asserted.addresses[0]
|
bindHost = bindHosts[0]
|
||||||
}
|
}
|
||||||
acmeIssuer.Challenges.BindHost = bindHost
|
acmeIssuer.Challenges.BindHost = bindHost
|
||||||
}
|
}
|
||||||
@@ -250,21 +210,9 @@ func (st ServerType) buildTLSApp(
|
|||||||
catchAllAP = ap
|
catchAllAP = ap
|
||||||
}
|
}
|
||||||
|
|
||||||
hostsNotHTTP := sblock.hostsFromKeysNotHTTP(httpPort)
|
|
||||||
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 = sblock.hostsFromKeysNotHTTP(httpPort)
|
||||||
|
sort.Strings(ap.SubjectsRaw) // solely for deterministic test results
|
||||||
|
|
||||||
// if a combination of public and internal names were given
|
// if a combination of public and internal names were given
|
||||||
// for this same server block and no issuer was specified, we
|
// for this same server block and no issuer was specified, we
|
||||||
@@ -303,7 +251,6 @@ func (st ServerType) buildTLSApp(
|
|||||||
ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
|
ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if tlsApp.Automation == nil {
|
if tlsApp.Automation == nil {
|
||||||
tlsApp.Automation = new(caddytls.AutomationConfig)
|
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||||
}
|
}
|
||||||
@@ -338,7 +285,7 @@ func (st ServerType) buildTLSApp(
|
|||||||
combined = reflect.New(reflect.TypeOf(cl)).Elem()
|
combined = reflect.New(reflect.TypeOf(cl)).Elem()
|
||||||
}
|
}
|
||||||
clVal := reflect.ValueOf(cl)
|
clVal := reflect.ValueOf(cl)
|
||||||
for i := range clVal.Len() {
|
for i := 0; i < clVal.Len(); i++ {
|
||||||
combined = reflect.Append(combined, clVal.Index(i))
|
combined = reflect.Append(combined, clVal.Index(i))
|
||||||
}
|
}
|
||||||
loadersByName[name] = combined.Interface().(caddytls.CertificateLoader)
|
loadersByName[name] = combined.Interface().(caddytls.CertificateLoader)
|
||||||
@@ -357,42 +304,6 @@ func (st ServerType) buildTLSApp(
|
|||||||
tlsApp.Automation.OnDemand = onDemand
|
tlsApp.Automation.OnDemand = onDemand
|
||||||
}
|
}
|
||||||
|
|
||||||
// set up "global" (to the TLS app) DNS provider config
|
|
||||||
if globalDNS, ok := options["dns"]; ok && globalDNS != nil {
|
|
||||||
tlsApp.DNSRaw = caddyconfig.JSONModuleObject(globalDNS, "name", globalDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up ECH from Caddyfile options
|
|
||||||
if ech, ok := options["ech"].(*caddytls.ECH); ok {
|
|
||||||
tlsApp.EncryptedClientHello = ech
|
|
||||||
|
|
||||||
// outer server names will need certificates, so make sure they're included
|
|
||||||
// in an automation policy for them that applies any global options
|
|
||||||
ap, err := newBaseAutomationPolicy(options, warnings, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, warnings, err
|
|
||||||
}
|
|
||||||
for _, cfg := range ech.Configs {
|
|
||||||
if cfg.PublicName != "" {
|
|
||||||
ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if tlsApp.Automation == nil {
|
|
||||||
tlsApp.Automation = new(caddytls.AutomationConfig)
|
|
||||||
}
|
|
||||||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the storage clean interval is a boolean, then it's "off" to disable cleaning
|
|
||||||
if sc, ok := options["storage_check"].(string); ok && sc == "off" {
|
|
||||||
tlsApp.DisableStorageCheck = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the storage clean interval is a boolean, then it's "off" to disable cleaning
|
|
||||||
if sci, ok := options["storage_clean_interval"].(bool); ok && !sci {
|
|
||||||
tlsApp.DisableStorageClean = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the storage clean interval if configured
|
// set the storage clean interval if configured
|
||||||
if storageCleanInterval, ok := options["storage_clean_interval"].(caddy.Duration); ok {
|
if storageCleanInterval, ok := options["storage_clean_interval"].(caddy.Duration); ok {
|
||||||
if tlsApp.Automation == nil {
|
if tlsApp.Automation == nil {
|
||||||
@@ -433,7 +344,7 @@ func (st ServerType) buildTLSApp(
|
|||||||
internalAP := &caddytls.AutomationPolicy{
|
internalAP := &caddytls.AutomationPolicy{
|
||||||
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
|
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
|
||||||
}
|
}
|
||||||
if !slices.Contains(autoHTTPS, "off") && !slices.Contains(autoHTTPS, "disable_certs") {
|
if autoHTTPS != "off" && autoHTTPS != "disable_certs" {
|
||||||
for h := range httpsHostsSharedWithHostlessKey {
|
for h := range httpsHostsSharedWithHostlessKey {
|
||||||
al = append(al, h)
|
al = append(al, h)
|
||||||
if !certmagic.SubjectQualifiesForPublicCert(h) {
|
if !certmagic.SubjectQualifiesForPublicCert(h) {
|
||||||
@@ -441,13 +352,6 @@ func (st ServerType) buildTLSApp(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for name := range forcedAutomatedNames {
|
|
||||||
if slices.Contains(al, name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
al = append(al, name)
|
|
||||||
}
|
|
||||||
slices.Sort(al) // to stabilize the adapt output
|
|
||||||
if len(al) > 0 {
|
if len(al) > 0 {
|
||||||
tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
|
tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
|
||||||
}
|
}
|
||||||
@@ -464,12 +368,12 @@ func (st ServerType) buildTLSApp(
|
|||||||
globalEmail := options["email"]
|
globalEmail := options["email"]
|
||||||
globalACMECA := options["acme_ca"]
|
globalACMECA := options["acme_ca"]
|
||||||
globalACMECARoot := options["acme_ca_root"]
|
globalACMECARoot := options["acme_ca_root"]
|
||||||
_, globalACMEDNS := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set
|
globalACMEDNS := options["acme_dns"]
|
||||||
globalACMEEAB := options["acme_eab"]
|
globalACMEEAB := options["acme_eab"]
|
||||||
globalPreferredChains := options["preferred_chains"]
|
globalPreferredChains := options["preferred_chains"]
|
||||||
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS || globalACMEEAB != nil || globalPreferredChains != nil
|
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil
|
||||||
if hasGlobalACMEDefaults {
|
if hasGlobalACMEDefaults {
|
||||||
for i := range tlsApp.Automation.Policies {
|
for i := 0; i < len(tlsApp.Automation.Policies); i++ {
|
||||||
ap := tlsApp.Automation.Policies[i]
|
ap := tlsApp.Automation.Policies[i]
|
||||||
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
|
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
|
||||||
// for public names, create default issuers which will later be filled in with configured global defaults
|
// for public names, create default issuers which will later be filled in with configured global defaults
|
||||||
@@ -549,12 +453,11 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
|||||||
globalEmail := options["email"]
|
globalEmail := options["email"]
|
||||||
globalACMECA := options["acme_ca"]
|
globalACMECA := options["acme_ca"]
|
||||||
globalACMECARoot := options["acme_ca_root"]
|
globalACMECARoot := options["acme_ca_root"]
|
||||||
globalACMEDNS, globalACMEDNSok := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set
|
globalACMEDNS := options["acme_dns"]
|
||||||
globalACMEEAB := options["acme_eab"]
|
globalACMEEAB := options["acme_eab"]
|
||||||
globalPreferredChains := options["preferred_chains"]
|
globalPreferredChains := options["preferred_chains"]
|
||||||
globalCertLifetime := options["cert_lifetime"]
|
globalCertLifetime := options["cert_lifetime"]
|
||||||
globalHTTPPort, globalHTTPSPort := options["http_port"], options["https_port"]
|
globalHTTPPort, globalHTTPSPort := options["http_port"], options["https_port"]
|
||||||
globalDefaultBind := options["default_bind"]
|
|
||||||
|
|
||||||
if globalEmail != nil && acmeIssuer.Email == "" {
|
if globalEmail != nil && acmeIssuer.Email == "" {
|
||||||
acmeIssuer.Email = globalEmail.(string)
|
acmeIssuer.Email = globalEmail.(string)
|
||||||
@@ -562,24 +465,14 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
|||||||
if globalACMECA != nil && acmeIssuer.CA == "" {
|
if globalACMECA != nil && acmeIssuer.CA == "" {
|
||||||
acmeIssuer.CA = globalACMECA.(string)
|
acmeIssuer.CA = globalACMECA.(string)
|
||||||
}
|
}
|
||||||
if globalACMECARoot != nil && !slices.Contains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
|
if globalACMECARoot != nil && !sliceContains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
|
||||||
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
|
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
|
||||||
}
|
}
|
||||||
if globalACMEDNSok && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil || acmeIssuer.Challenges.DNS.ProviderRaw == nil) {
|
if globalACMEDNS != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) {
|
||||||
globalDNS := options["dns"]
|
acmeIssuer.Challenges = &caddytls.ChallengesConfig{
|
||||||
if globalDNS == nil && globalACMEDNS == nil {
|
DNS: &caddytls.DNSChallengeConfig{
|
||||||
return fmt.Errorf("acme_dns specified without DNS provider config, but no provider specified with 'dns' global option")
|
ProviderRaw: caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil),
|
||||||
}
|
},
|
||||||
if acmeIssuer.Challenges == nil {
|
|
||||||
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
|
||||||
}
|
|
||||||
if acmeIssuer.Challenges.DNS == nil {
|
|
||||||
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
|
||||||
}
|
|
||||||
// If global `dns` is set, do NOT set provider in issuer, just set empty dns config
|
|
||||||
if globalDNS == nil && acmeIssuer.Challenges.DNS.ProviderRaw == nil {
|
|
||||||
// Set a global DNS provider if `acme_dns` is set and `dns` is NOT set
|
|
||||||
acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
|
if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
|
||||||
@@ -588,8 +481,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
|||||||
if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil {
|
if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil {
|
||||||
acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference)
|
acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference)
|
||||||
}
|
}
|
||||||
// only configure alt HTTP and TLS-ALPN ports if the DNS challenge is not enabled (wouldn't hurt, but isn't necessary since the DNS challenge is exclusive of others)
|
if globalHTTPPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.HTTP == nil || acmeIssuer.Challenges.HTTP.AlternatePort == 0) {
|
||||||
if globalHTTPPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.HTTP == nil || acmeIssuer.Challenges.HTTP.AlternatePort == 0) {
|
|
||||||
if acmeIssuer.Challenges == nil {
|
if acmeIssuer.Challenges == nil {
|
||||||
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
||||||
}
|
}
|
||||||
@@ -598,7 +490,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
|||||||
}
|
}
|
||||||
acmeIssuer.Challenges.HTTP.AlternatePort = globalHTTPPort.(int)
|
acmeIssuer.Challenges.HTTP.AlternatePort = globalHTTPPort.(int)
|
||||||
}
|
}
|
||||||
if globalHTTPSPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.TLSALPN == nil || acmeIssuer.Challenges.TLSALPN.AlternatePort == 0) {
|
if globalHTTPSPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.TLSALPN == nil || acmeIssuer.Challenges.TLSALPN.AlternatePort == 0) {
|
||||||
if acmeIssuer.Challenges == nil {
|
if acmeIssuer.Challenges == nil {
|
||||||
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
||||||
}
|
}
|
||||||
@@ -607,20 +499,6 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
|||||||
}
|
}
|
||||||
acmeIssuer.Challenges.TLSALPN.AlternatePort = globalHTTPSPort.(int)
|
acmeIssuer.Challenges.TLSALPN.AlternatePort = globalHTTPSPort.(int)
|
||||||
}
|
}
|
||||||
// If BindHost is still unset, fall back to the first default_bind address if set
|
|
||||||
// This avoids binding the automation policy to the wildcard socket, which is unexpected behavior when a more selective socket is specified via default_bind
|
|
||||||
// In BSD it is valid to bind to the wildcard socket even though a more selective socket is already open (still unexpected behavior by the caller though)
|
|
||||||
// In Linux the same call will error with EADDRINUSE whenever the listener for the automation policy is opened
|
|
||||||
if acmeIssuer.Challenges == nil || (acmeIssuer.Challenges.DNS == nil && acmeIssuer.Challenges.BindHost == "") {
|
|
||||||
if defBinds, ok := globalDefaultBind.([]ConfigValue); ok && len(defBinds) > 0 {
|
|
||||||
if abp, ok := defBinds[0].Value.(addressesWithProtocols); ok && len(abp.addresses) > 0 {
|
|
||||||
if acmeIssuer.Challenges == nil {
|
|
||||||
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
|
||||||
}
|
|
||||||
acmeIssuer.Challenges.BindHost = abp.addresses[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
|
if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
|
||||||
acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
|
acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
|
||||||
}
|
}
|
||||||
@@ -641,18 +519,12 @@ func newBaseAutomationPolicy(
|
|||||||
_, hasLocalCerts := options["local_certs"]
|
_, hasLocalCerts := options["local_certs"]
|
||||||
keyType, hasKeyType := options["key_type"]
|
keyType, hasKeyType := options["key_type"]
|
||||||
ocspStapling, hasOCSPStapling := options["ocsp_stapling"]
|
ocspStapling, hasOCSPStapling := options["ocsp_stapling"]
|
||||||
hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling
|
|
||||||
|
|
||||||
globalACMECA := options["acme_ca"]
|
hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling
|
||||||
globalACMECARoot := options["acme_ca_root"]
|
|
||||||
_, globalACMEDNS := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set
|
|
||||||
globalACMEEAB := options["acme_eab"]
|
|
||||||
globalPreferredChains := options["preferred_chains"]
|
|
||||||
hasGlobalACMEDefaults := globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS || globalACMEEAB != nil || globalPreferredChains != nil
|
|
||||||
|
|
||||||
// if there are no global options related to automation policies
|
// if there are no global options related to automation policies
|
||||||
// set, then we can just return right away
|
// set, then we can just return right away
|
||||||
if !hasGlobalAutomationOpts && !hasGlobalACMEDefaults {
|
if !hasGlobalAutomationOpts {
|
||||||
if always {
|
if always {
|
||||||
return new(caddytls.AutomationPolicy), nil
|
return new(caddytls.AutomationPolicy), nil
|
||||||
}
|
}
|
||||||
@@ -674,14 +546,6 @@ func newBaseAutomationPolicy(
|
|||||||
ap.Issuers = []certmagic.Issuer{new(caddytls.InternalIssuer)}
|
ap.Issuers = []certmagic.Issuer{new(caddytls.InternalIssuer)}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasGlobalACMEDefaults {
|
|
||||||
for i := range ap.Issuers {
|
|
||||||
if err := fillInGlobalACMEDefaults(ap.Issuers[i], options); err != nil {
|
|
||||||
return nil, fmt.Errorf("filling in global issuer defaults for issuer %d: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasOCSPStapling {
|
if hasOCSPStapling {
|
||||||
ocspConfig := ocspStapling.(certmagic.OCSPConfig)
|
ocspConfig := ocspStapling.(certmagic.OCSPConfig)
|
||||||
ap.DisableOCSPStapling = ocspConfig.DisableStapling
|
ap.DisableOCSPStapling = ocspConfig.DisableStapling
|
||||||
@@ -716,7 +580,7 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
|
|||||||
if !automationPolicyHasAllPublicNames(aps[i]) {
|
if !automationPolicyHasAllPublicNames(aps[i]) {
|
||||||
// if this automation policy has internal names, we might as well remove it
|
// if this automation policy has internal names, we might as well remove it
|
||||||
// so auto-https can implicitly use the internal issuer
|
// so auto-https can implicitly use the internal issuer
|
||||||
aps = slices.Delete(aps, i, i+1)
|
aps = append(aps[:i], aps[i+1:]...)
|
||||||
i--
|
i--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -733,7 +597,7 @@ outer:
|
|||||||
for j := i + 1; j < len(aps); j++ {
|
for j := i + 1; j < len(aps); j++ {
|
||||||
// if they're exactly equal in every way, just keep one of them
|
// if they're exactly equal in every way, just keep one of them
|
||||||
if reflect.DeepEqual(aps[i], aps[j]) {
|
if reflect.DeepEqual(aps[i], aps[j]) {
|
||||||
aps = slices.Delete(aps, j, j+1)
|
aps = append(aps[:j], aps[j+1:]...)
|
||||||
// must re-evaluate current i against next j; can't skip it!
|
// must re-evaluate current i against next j; can't skip it!
|
||||||
// even if i decrements to -1, will be incremented to 0 immediately
|
// even if i decrements to -1, will be incremented to 0 immediately
|
||||||
i--
|
i--
|
||||||
@@ -763,18 +627,18 @@ outer:
|
|||||||
// cause example.com to be served by the less specific policy for
|
// cause example.com to be served by the less specific policy for
|
||||||
// '*.com', which might be different (yes we've seen this happen)
|
// '*.com', which might be different (yes we've seen this happen)
|
||||||
if automationPolicyShadows(i, aps) >= j {
|
if automationPolicyShadows(i, aps) >= j {
|
||||||
aps = slices.Delete(aps, i, i+1)
|
aps = append(aps[:i], aps[i+1:]...)
|
||||||
i--
|
i--
|
||||||
continue outer
|
continue outer
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// avoid repeated subjects
|
// avoid repeated subjects
|
||||||
for _, subj := range aps[j].SubjectsRaw {
|
for _, subj := range aps[j].SubjectsRaw {
|
||||||
if !slices.Contains(aps[i].SubjectsRaw, subj) {
|
if !sliceContains(aps[i].SubjectsRaw, subj) {
|
||||||
aps[i].SubjectsRaw = append(aps[i].SubjectsRaw, subj)
|
aps[i].SubjectsRaw = append(aps[i].SubjectsRaw, subj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
aps = slices.Delete(aps, j, j+1)
|
aps = append(aps[:j], aps[j+1:]...)
|
||||||
j--
|
j--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -794,9 +658,13 @@ func automationPolicyIsSubset(a, b *caddytls.AutomationPolicy) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, aSubj := range a.SubjectsRaw {
|
for _, aSubj := range a.SubjectsRaw {
|
||||||
inSuperset := slices.ContainsFunc(b.SubjectsRaw, func(bSubj string) bool {
|
var inSuperset bool
|
||||||
return certmagic.MatchWildcard(aSubj, bSubj)
|
for _, bSubj := range b.SubjectsRaw {
|
||||||
})
|
if certmagic.MatchWildcard(aSubj, bSubj) {
|
||||||
|
inSuperset = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
if !inSuperset {
|
if !inSuperset {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -841,28 +709,14 @@ func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) b
|
|||||||
// 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 {
|
||||||
return !slices.ContainsFunc(ap.SubjectsRaw, func(i string) bool {
|
for _, subj := range ap.SubjectsRaw {
|
||||||
return !subjectQualifiesForPublicCert(ap, i) || isTailscaleDomain(i)
|
if !subjectQualifiesForPublicCert(ap, subj) || isTailscaleDomain(subj) {
|
||||||
})
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func init() {
|
|||||||
// If the response is not a JSON config, a config adapter must be specified
|
// If the response is not a JSON config, a config adapter must be specified
|
||||||
// either in the loader config (`adapter`), or in the Content-Type HTTP header
|
// either in the loader config (`adapter`), or in the Content-Type HTTP header
|
||||||
// returned in the HTTP response from the server. The Content-Type header is
|
// returned in the HTTP response from the server. The Content-Type header is
|
||||||
// read just like the admin API's `/load` endpoint. If you don't have control
|
// read just like the admin API's `/load` endpoint. Uf you don't have control
|
||||||
// over the HTTP server (but can still trust its response), you can override
|
// over the HTTP server (but can still trust its response), you can override
|
||||||
// the Content-Type header by setting the `adapter` property in this config.
|
// the Content-Type header by setting the `adapter` property in this config.
|
||||||
type HTTPLoader struct {
|
type HTTPLoader struct {
|
||||||
|
|||||||
@@ -121,13 +121,6 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this request changed the config, clear the last
|
|
||||||
// config info we have stored, if it is different from
|
|
||||||
// the original source.
|
|
||||||
caddy.ClearLastConfigIfDifferent(
|
|
||||||
r.Header.Get("Caddy-Config-Source-File"),
|
|
||||||
r.Header.Get("Caddy-Config-Source-Adapter"))
|
|
||||||
|
|
||||||
caddy.Log().Named("admin.api").Info("load complete")
|
caddy.Log().Named("admin.api").Info("load complete")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+156
-439
@@ -1,40 +1,29 @@
|
|||||||
package caddytest
|
package caddytest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"strconv"
|
||||||
"reflect"
|
|
||||||
"regexp"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aryann/difflib"
|
|
||||||
|
|
||||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
|
||||||
// plug in Caddy modules here
|
// plug in Caddy modules here
|
||||||
_ "github.com/caddyserver/caddy/v2/modules/standard"
|
_ "github.com/caddyserver/caddy/v2/modules/standard"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config store any configuration required to make the tests run
|
// Defaults store any configuration required to make the tests run
|
||||||
type Config struct {
|
type Defaults struct {
|
||||||
// Port we expect caddy to listening on
|
|
||||||
AdminPort int
|
|
||||||
// Certificates we expect to be loaded before attempting to run the tests
|
// Certificates we expect to be loaded before attempting to run the tests
|
||||||
Certificates []string
|
Certificates []string
|
||||||
// TestRequestTimeout is the time to wait for a http request to
|
// TestRequestTimeout is the time to wait for a http request to
|
||||||
@@ -44,31 +33,32 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default testing values
|
// Default testing values
|
||||||
var Default = Config{
|
var Default = Defaults{
|
||||||
AdminPort: 2999, // different from what a real server also running on a developer's machine might be
|
|
||||||
Certificates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
|
Certificates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
|
||||||
TestRequestTimeout: 5 * time.Second,
|
TestRequestTimeout: 5 * time.Second,
|
||||||
LoadRequestTimeout: 5 * time.Second,
|
LoadRequestTimeout: 5 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
matchKey = regexp.MustCompile(`(/[\w\d\.]+\.key)`)
|
|
||||||
matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tester represents an instance of a test client.
|
// Tester represents an instance of a test client.
|
||||||
type Tester struct {
|
type Tester struct {
|
||||||
Client *http.Client
|
Client *http.Client
|
||||||
configLoaded bool
|
|
||||||
t testing.TB
|
adminPort int
|
||||||
config Config
|
|
||||||
|
portOne int
|
||||||
|
portTwo int
|
||||||
|
|
||||||
|
started atomic.Bool
|
||||||
|
configLoaded bool
|
||||||
|
configFileName string
|
||||||
|
envFileName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTester will create a new testing client with an attached cookie jar
|
// NewTester will create a new testing client with an attached cookie jar
|
||||||
func NewTester(t testing.TB) *Tester {
|
func NewTester() (*Tester, error) {
|
||||||
jar, err := cookiejar.New(nil)
|
jar, err := cookiejar.New(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create cookiejar: %s", err)
|
return nil, fmt.Errorf("failed to create cookiejar: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Tester{
|
return &Tester{
|
||||||
@@ -78,28 +68,7 @@ func NewTester(t testing.TB) *Tester {
|
|||||||
Timeout: Default.TestRequestTimeout,
|
Timeout: Default.TestRequestTimeout,
|
||||||
},
|
},
|
||||||
configLoaded: false,
|
configLoaded: false,
|
||||||
t: t,
|
}, nil
|
||||||
config: Default,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithDefaultOverrides this will override the default test configuration with the provided values.
|
|
||||||
func (tc *Tester) WithDefaultOverrides(overrides Config) *Tester {
|
|
||||||
if overrides.AdminPort != 0 {
|
|
||||||
tc.config.AdminPort = overrides.AdminPort
|
|
||||||
}
|
|
||||||
if len(overrides.Certificates) > 0 {
|
|
||||||
tc.config.Certificates = overrides.Certificates
|
|
||||||
}
|
|
||||||
if overrides.TestRequestTimeout != 0 {
|
|
||||||
tc.config.TestRequestTimeout = overrides.TestRequestTimeout
|
|
||||||
tc.Client.Timeout = overrides.TestRequestTimeout
|
|
||||||
}
|
|
||||||
if overrides.LoadRequestTimeout != 0 {
|
|
||||||
tc.config.LoadRequestTimeout = overrides.LoadRequestTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
return tc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type configLoadError struct {
|
type configLoadError struct {
|
||||||
@@ -113,53 +82,73 @@ func timeElapsed(start time.Time, name string) {
|
|||||||
log.Printf("%s took %s", name, elapsed)
|
log.Printf("%s took %s", name, elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitServer this will configure the server with a configurion of a specific
|
// launch caddy will start the server
|
||||||
// type. The configType must be either "json" or the adapter type.
|
func (tc *Tester) LaunchCaddy() error {
|
||||||
func (tc *Tester) InitServer(rawConfig string, configType string) {
|
if !tc.started.CompareAndSwap(false, true) {
|
||||||
if err := tc.initServer(rawConfig, configType); err != nil {
|
return fmt.Errorf("already launched caddy with this tester")
|
||||||
tc.t.Logf("failed to load config: %s", err)
|
|
||||||
tc.t.Fail()
|
|
||||||
}
|
}
|
||||||
if err := tc.ensureConfigRunning(rawConfig, configType); err != nil {
|
if err := tc.startServer(); err != nil {
|
||||||
tc.t.Logf("failed ensuring config is running: %s", err)
|
return fmt.Errorf("failed to start server: %w", err)
|
||||||
tc.t.Fail()
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitServer this will configure the server with a configurion of a specific
|
func (tc *Tester) CleanupCaddy() error {
|
||||||
// type. The configType must be either "json" or the adapter type.
|
// now shutdown the server, since the test is done.
|
||||||
func (tc *Tester) initServer(rawConfig string, configType string) error {
|
defer func() {
|
||||||
if testing.Short() {
|
// try to remove pthe tmp config file we created
|
||||||
tc.t.SkipNow()
|
if tc.configFileName != "" {
|
||||||
return nil
|
os.Remove(tc.configFileName)
|
||||||
}
|
|
||||||
|
|
||||||
err := validateTestPrerequisites(tc)
|
|
||||||
if err != nil {
|
|
||||||
tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tc.t.Cleanup(func() {
|
|
||||||
if tc.t.Failed() && tc.configLoaded {
|
|
||||||
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort))
|
|
||||||
if err != nil {
|
|
||||||
tc.t.Log("unable to read the current config")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
body, _ := io.ReadAll(res.Body)
|
|
||||||
|
|
||||||
var out bytes.Buffer
|
|
||||||
_ = json.Indent(&out, body, "", " ")
|
|
||||||
tc.t.Logf("----------- failed with config -----------\n%s", out.String())
|
|
||||||
}
|
}
|
||||||
})
|
if tc.envFileName != "" {
|
||||||
|
os.Remove(tc.envFileName)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resp, err := http.Post(fmt.Sprintf("http://localhost:%d/stop", tc.adminPort), "", nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't stop caddytest server: %w", err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
for retries := 0; retries < 10; retries++ {
|
||||||
|
if tc.isCaddyAdminRunning() != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
rawConfig = prependCaddyFilePath(rawConfig)
|
return fmt.Errorf("timed out waiting for caddytest server to stop")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *Tester) AdminPort() int {
|
||||||
|
return tc.adminPort
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *Tester) PortOne() int {
|
||||||
|
return tc.portOne
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *Tester) PortTwo() int {
|
||||||
|
return tc.portTwo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *Tester) ReplaceTestingPlaceholders(x string) string {
|
||||||
|
x = strings.ReplaceAll(x, "{$TESTING_CADDY_ADMIN_BIND}", fmt.Sprintf("localhost:%d", tc.adminPort))
|
||||||
|
x = strings.ReplaceAll(x, "{$TESTING_CADDY_ADMIN_PORT}", fmt.Sprintf("%d", tc.adminPort))
|
||||||
|
x = strings.ReplaceAll(x, "{$TESTING_CADDY_PORT_ONE}", fmt.Sprintf("%d", tc.portOne))
|
||||||
|
x = strings.ReplaceAll(x, "{$TESTING_CADDY_PORT_TWO}", fmt.Sprintf("%d", tc.portTwo))
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig loads the config to the tester server and also ensures that the config was loaded
|
||||||
|
// it should not be run
|
||||||
|
func (tc *Tester) LoadConfig(rawConfig string, configType string) error {
|
||||||
|
if tc.adminPort == 0 {
|
||||||
|
return fmt.Errorf("load config called where startServer didnt succeed")
|
||||||
|
}
|
||||||
|
rawConfig = tc.ReplaceTestingPlaceholders(rawConfig)
|
||||||
|
// replace special testing placeholders so we can have our admin api be on a random port
|
||||||
// normalize JSON config
|
// normalize JSON config
|
||||||
if configType == "json" {
|
if configType == "json" {
|
||||||
tc.t.Logf("Before: %s", rawConfig)
|
|
||||||
var conf any
|
var conf any
|
||||||
if err := json.Unmarshal([]byte(rawConfig), &conf); err != nil {
|
if err := json.Unmarshal([]byte(rawConfig), &conf); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -169,16 +158,14 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rawConfig = string(c)
|
rawConfig = string(c)
|
||||||
tc.t.Logf("After: %s", rawConfig)
|
|
||||||
}
|
}
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: tc.config.LoadRequestTimeout,
|
Timeout: Default.LoadRequestTimeout,
|
||||||
}
|
}
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", tc.config.AdminPort), strings.NewReader(rawConfig))
|
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", tc.adminPort), strings.NewReader(rawConfig))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("failed to create request. %s", err)
|
return fmt.Errorf("failed to create request. %w", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if configType == "json" {
|
if configType == "json" {
|
||||||
@@ -189,16 +176,14 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
|
|||||||
|
|
||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("unable to contact caddy server. %s", err)
|
return fmt.Errorf("unable to contact caddy server. %w", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
timeElapsed(start, "caddytest: config load time")
|
timeElapsed(start, "caddytest: config load time")
|
||||||
|
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
body, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("unable to read response. %s", err)
|
return fmt.Errorf("unable to read response. %w", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode != 200 {
|
if res.StatusCode != 200 {
|
||||||
@@ -206,133 +191,115 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tc.configLoaded = true
|
tc.configLoaded = true
|
||||||
|
|
||||||
|
// if the config is not loaded at this point, it is a bug in caddy's config.Load
|
||||||
|
// the contract for config.Load states that the config must be loaded before it returns, and that it will
|
||||||
|
// error if the config fails to apply
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error {
|
func (tc *Tester) GetCurrentConfig(receiver any) error {
|
||||||
expectedBytes := []byte(prependCaddyFilePath(rawConfig))
|
client := &http.Client{
|
||||||
if configType != "json" {
|
Timeout: Default.LoadRequestTimeout,
|
||||||
adapter := caddyconfig.GetAdapter(configType)
|
|
||||||
if adapter == nil {
|
|
||||||
return fmt.Errorf("adapter of config type is missing: %s", configType)
|
|
||||||
}
|
|
||||||
expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var expected any
|
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.adminPort))
|
||||||
err := json.Unmarshal(expectedBytes, &expected)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
client := &http.Client{
|
actualBytes, err := io.ReadAll(resp.Body)
|
||||||
Timeout: tc.config.LoadRequestTimeout,
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
err = json.Unmarshal(actualBytes, receiver)
|
||||||
fetchConfig := func(client *http.Client) any {
|
if err != nil {
|
||||||
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort))
|
return err
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
actualBytes, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var actual any
|
|
||||||
err = json.Unmarshal(actualBytes, &actual)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return actual
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
for retries := 10; retries > 0; retries-- {
|
|
||||||
if reflect.DeepEqual(expected, fetchConfig(client)) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}
|
|
||||||
tc.t.Errorf("POSTed configuration isn't active")
|
|
||||||
return errors.New("EnsureConfigRunning: POSTed configuration isn't active")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const initConfig = `{
|
func getFreePort() (int, error) {
|
||||||
admin localhost:%d
|
lr, err := net.Listen("tcp", "localhost:0")
|
||||||
}
|
if err != nil {
|
||||||
`
|
return 0, err
|
||||||
|
|
||||||
// validateTestPrerequisites ensures the certificates are available in the
|
|
||||||
// designated path and Caddy sub-process is running.
|
|
||||||
func validateTestPrerequisites(tc *Tester) error {
|
|
||||||
// check certificates are found
|
|
||||||
for _, certName := range tc.config.Certificates {
|
|
||||||
if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) {
|
|
||||||
return fmt.Errorf("caddy integration test certificates (%s) not found", certName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
port := strings.Split(lr.Addr().String(), ":")
|
||||||
|
if len(port) < 2 {
|
||||||
|
return 0, fmt.Errorf("no port available")
|
||||||
|
}
|
||||||
|
i, err := strconv.Atoi(port[1])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
err = lr.Close()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to close listener: %w", err)
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
if isCaddyAdminRunning(tc) != nil {
|
// launches caddy, and then ensures the Caddy sub-process is running.
|
||||||
// setup the init config file, and set the cleanup afterwards
|
func (tc *Tester) startServer() error {
|
||||||
|
if tc.isCaddyAdminRunning() == nil {
|
||||||
|
return fmt.Errorf("caddy test admin port still in use")
|
||||||
|
}
|
||||||
|
a, err := getFreePort()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not find a open port to listen on: %w", err)
|
||||||
|
}
|
||||||
|
tc.adminPort = a
|
||||||
|
tc.portOne, err = getFreePort()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not find a open portOne: %w", err)
|
||||||
|
}
|
||||||
|
tc.portTwo, err = getFreePort()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not find a open portOne: %w", err)
|
||||||
|
}
|
||||||
|
// setup the init config file, and set the cleanup afterwards
|
||||||
|
{
|
||||||
f, err := os.CreateTemp("", "")
|
f, err := os.CreateTemp("", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tc.t.Cleanup(func() {
|
tc.configFileName = f.Name()
|
||||||
os.Remove(f.Name())
|
|
||||||
})
|
initConfig := fmt.Sprintf(`{
|
||||||
if _, err := fmt.Fprintf(f, initConfig, tc.config.AdminPort); err != nil {
|
admin localhost:%d
|
||||||
|
}`, a)
|
||||||
|
if _, err := f.WriteString(initConfig); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// start inprocess caddy server
|
// start inprocess caddy server
|
||||||
os.Args = []string{"caddy", "run", "--config", f.Name(), "--adapter", "caddyfile"}
|
go func() {
|
||||||
go func() {
|
_ = caddycmd.MainForTesting("run", "--config", tc.configFileName, "--adapter", "caddyfile")
|
||||||
caddycmd.Main()
|
}()
|
||||||
}()
|
// wait for caddy admin api to start. it should happen quickly.
|
||||||
|
for retries := 10; retries > 0 && tc.isCaddyAdminRunning() != nil; retries-- {
|
||||||
// wait for caddy to start serving the initial config
|
time.Sleep(100 * time.Millisecond)
|
||||||
for retries := 10; retries > 0 && isCaddyAdminRunning(tc) != nil; retries-- {
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// one more time to return the error
|
// one more time to return the error
|
||||||
return isCaddyAdminRunning(tc)
|
return tc.isCaddyAdminRunning()
|
||||||
}
|
}
|
||||||
|
|
||||||
func isCaddyAdminRunning(tc *Tester) error {
|
func (tc *Tester) isCaddyAdminRunning() error {
|
||||||
// assert that caddy is running
|
// assert that caddy is running
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: tc.config.LoadRequestTimeout,
|
Timeout: Default.LoadRequestTimeout,
|
||||||
}
|
}
|
||||||
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort))
|
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.adminPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", tc.config.AdminPort)
|
return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", tc.adminPort)
|
||||||
}
|
}
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIntegrationDir() string {
|
|
||||||
_, filename, _, ok := runtime.Caller(1)
|
|
||||||
if !ok {
|
|
||||||
panic("unable to determine the current file path")
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.Dir(filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
// use the convention to replace /[certificatename].[crt|key] with the full path
|
|
||||||
// this helps reduce the noise in test configurations and also allow this
|
|
||||||
// to run in any path
|
|
||||||
func prependCaddyFilePath(rawConfig string) string {
|
|
||||||
r := matchKey.ReplaceAllString(rawConfig, getIntegrationDir()+"$1")
|
|
||||||
r = matchCert.ReplaceAllString(r, getIntegrationDir()+"$1")
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
|
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
|
||||||
func CreateTestingTransport() *http.Transport {
|
func CreateTestingTransport() *http.Transport {
|
||||||
dialer := net.Dialer{
|
dialer := net.Dialer{
|
||||||
@@ -359,253 +326,3 @@ func CreateTestingTransport() *http.Transport {
|
|||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssertLoadError will load a config and expect an error
|
|
||||||
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
tc := NewTester(t)
|
|
||||||
|
|
||||||
err := tc.initServer(rawConfig, configType)
|
|
||||||
if !strings.Contains(err.Error(), expectedError) {
|
|
||||||
t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertRedirect makes a request and asserts the redirection happens
|
|
||||||
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
|
|
||||||
tc.t.Helper()
|
|
||||||
|
|
||||||
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
|
|
||||||
return http.ErrUseLastResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
// using the existing client, we override the check redirect policy for this test
|
|
||||||
old := tc.Client.CheckRedirect
|
|
||||||
tc.Client.CheckRedirect = redirectPolicyFunc
|
|
||||||
defer func() { tc.Client.CheckRedirect = old }()
|
|
||||||
|
|
||||||
resp, err := tc.Client.Get(requestURI)
|
|
||||||
if err != nil {
|
|
||||||
tc.t.Errorf("failed to call server %s", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if expectedStatusCode != resp.StatusCode {
|
|
||||||
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", requestURI, expectedStatusCode, resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
loc, err := resp.Location()
|
|
||||||
if err != nil {
|
|
||||||
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err)
|
|
||||||
}
|
|
||||||
if loc == nil && expectedToLocation != "" {
|
|
||||||
tc.t.Errorf("requesting \"%s\" expected a Location header, but didn't get one", requestURI)
|
|
||||||
}
|
|
||||||
if loc != nil {
|
|
||||||
if expectedToLocation != loc.String() {
|
|
||||||
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompareAdapt adapts a config and then compares it against an expected result
|
|
||||||
func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
cfgAdapter := caddyconfig.GetAdapter(adapterName)
|
|
||||||
if cfgAdapter == nil {
|
|
||||||
t.Logf("unrecognized config adapter '%s'", adapterName)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
options := make(map[string]any)
|
|
||||||
|
|
||||||
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("adapting config using %s adapter: %v", adapterName, err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// prettify results to keep tests human-manageable
|
|
||||||
var prettyBuf bytes.Buffer
|
|
||||||
err = json.Indent(&prettyBuf, result, "", "\t")
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
result = prettyBuf.Bytes()
|
|
||||||
|
|
||||||
if len(warnings) > 0 {
|
|
||||||
for _, w := range warnings {
|
|
||||||
t.Logf("warning: %s:%d: %s: %s", filename, w.Line, w.Directive, w.Message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diff := difflib.Diff(
|
|
||||||
strings.Split(expectedResponse, "\n"),
|
|
||||||
strings.Split(string(result), "\n"))
|
|
||||||
|
|
||||||
// scan for failure
|
|
||||||
failed := false
|
|
||||||
for _, d := range diff {
|
|
||||||
if d.Delta != difflib.Common {
|
|
||||||
failed = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if failed {
|
|
||||||
for _, d := range diff {
|
|
||||||
switch d.Delta {
|
|
||||||
case difflib.Common:
|
|
||||||
fmt.Printf(" %s\n", d.Payload)
|
|
||||||
case difflib.LeftOnly:
|
|
||||||
fmt.Printf(" - %s\n", d.Payload)
|
|
||||||
case difflib.RightOnly:
|
|
||||||
fmt.Printf(" + %s\n", d.Payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertAdapt adapts a config and then tests it against an expected result
|
|
||||||
func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
|
|
||||||
if !ok {
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic request functions
|
|
||||||
|
|
||||||
func applyHeaders(t testing.TB, req *http.Request, requestHeaders []string) {
|
|
||||||
requestContentType := ""
|
|
||||||
for _, requestHeader := range requestHeaders {
|
|
||||||
arr := strings.SplitAfterN(requestHeader, ":", 2)
|
|
||||||
k := strings.TrimRight(arr[0], ":")
|
|
||||||
v := strings.TrimSpace(arr[1])
|
|
||||||
if k == "Content-Type" {
|
|
||||||
requestContentType = v
|
|
||||||
}
|
|
||||||
t.Logf("Request header: %s => %s", k, v)
|
|
||||||
req.Header.Set(k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
if requestContentType == "" {
|
|
||||||
t.Logf("Content-Type header not provided")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
|
|
||||||
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
|
|
||||||
tc.t.Helper()
|
|
||||||
|
|
||||||
resp, err := tc.Client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
tc.t.Fatalf("failed to call server %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if expectedStatusCode != resp.StatusCode {
|
|
||||||
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.URL.RequestURI(), expectedStatusCode, resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertResponse request a URI and assert the status code and the body contains a string
|
|
||||||
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
|
||||||
tc.t.Helper()
|
|
||||||
|
|
||||||
resp := tc.AssertResponseCode(req, expectedStatusCode)
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
bytes, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
tc.t.Fatalf("unable to read the response body %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
body := string(bytes)
|
|
||||||
|
|
||||||
if body != expectedBody {
|
|
||||||
tc.t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, body
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verb specific test functions
|
|
||||||
|
|
||||||
// AssertGetResponse GET a URI and expect a statusCode and body text
|
|
||||||
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
|
||||||
tc.t.Helper()
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", requestURI, nil)
|
|
||||||
if err != nil {
|
|
||||||
tc.t.Fatalf("unable to create request %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertDeleteResponse request a URI and expect a statusCode and body text
|
|
||||||
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
|
||||||
tc.t.Helper()
|
|
||||||
|
|
||||||
req, err := http.NewRequest("DELETE", requestURI, nil)
|
|
||||||
if err != nil {
|
|
||||||
tc.t.Fatalf("unable to create request %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertPostResponseBody POST to a URI and assert the response code and body
|
|
||||||
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
|
||||||
tc.t.Helper()
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", requestURI, requestBody)
|
|
||||||
if err != nil {
|
|
||||||
tc.t.Errorf("failed to create request %s", err)
|
|
||||||
return nil, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
applyHeaders(tc.t, req, requestHeaders)
|
|
||||||
|
|
||||||
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertPutResponseBody PUT to a URI and assert the response code and body
|
|
||||||
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
|
||||||
tc.t.Helper()
|
|
||||||
|
|
||||||
req, err := http.NewRequest("PUT", requestURI, requestBody)
|
|
||||||
if err != nil {
|
|
||||||
tc.t.Errorf("failed to create request %s", err)
|
|
||||||
return nil, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
applyHeaders(tc.t, req, requestHeaders)
|
|
||||||
|
|
||||||
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
|
|
||||||
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
|
||||||
tc.t.Helper()
|
|
||||||
|
|
||||||
req, err := http.NewRequest("PATCH", requestURI, requestBody)
|
|
||||||
if err != nil {
|
|
||||||
tc.t.Errorf("failed to create request %s", err)
|
|
||||||
return nil, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
applyHeaders(tc.t, req, requestHeaders)
|
|
||||||
|
|
||||||
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package caddytest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aryann/difflib"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AssertLoadError will load a config and expect an error
|
||||||
|
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
|
||||||
|
tc, err := NewTester()
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = tc.LaunchCaddy()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = tc.LoadConfig(rawConfig, configType)
|
||||||
|
if !strings.Contains(err.Error(), expectedError) {
|
||||||
|
t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error())
|
||||||
|
}
|
||||||
|
_ = tc.CleanupCaddy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
cfgAdapter := caddyconfig.GetAdapter(adapterName)
|
||||||
|
if cfgAdapter == nil {
|
||||||
|
t.Logf("unrecognized config adapter '%s'", adapterName)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
options := make(map[string]any)
|
||||||
|
|
||||||
|
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("adapting config using %s adapter: %v", adapterName, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// prettify results to keep tests human-manageable
|
||||||
|
var prettyBuf bytes.Buffer
|
||||||
|
err = json.Indent(&prettyBuf, result, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
result = prettyBuf.Bytes()
|
||||||
|
|
||||||
|
if len(warnings) > 0 {
|
||||||
|
for _, w := range warnings {
|
||||||
|
t.Logf("warning: %s:%d: %s: %s", filename, w.Line, w.Directive, w.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diff := difflib.Diff(
|
||||||
|
strings.Split(expectedResponse, "\n"),
|
||||||
|
strings.Split(string(result), "\n"))
|
||||||
|
|
||||||
|
// scan for failure
|
||||||
|
failed := false
|
||||||
|
for _, d := range diff {
|
||||||
|
if d.Delta != difflib.Common {
|
||||||
|
failed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if failed {
|
||||||
|
for _, d := range diff {
|
||||||
|
switch d.Delta {
|
||||||
|
case difflib.Common:
|
||||||
|
fmt.Printf(" %s\n", d.Payload)
|
||||||
|
case difflib.LeftOnly:
|
||||||
|
fmt.Printf(" - %s\n", d.Payload)
|
||||||
|
case difflib.RightOnly:
|
||||||
|
fmt.Printf(" + %s\n", d.Payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertAdapt adapts a config and then tests it against an expected result
|
||||||
|
func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) {
|
||||||
|
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
|
||||||
|
if !ok {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic request functions
|
||||||
|
|
||||||
|
func applyHeaders(t testing.TB, req *http.Request, requestHeaders []string) {
|
||||||
|
requestContentType := ""
|
||||||
|
for _, requestHeader := range requestHeaders {
|
||||||
|
arr := strings.SplitAfterN(requestHeader, ":", 2)
|
||||||
|
k := strings.TrimRight(arr[0], ":")
|
||||||
|
v := strings.TrimSpace(arr[1])
|
||||||
|
if k == "Content-Type" {
|
||||||
|
requestContentType = v
|
||||||
|
}
|
||||||
|
t.Logf("Request header: %s => %s", k, v)
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if requestContentType == "" {
|
||||||
|
t.Logf("Content-Type header not provided")
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-11
@@ -1,21 +1,22 @@
|
|||||||
package caddytest
|
package caddytest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReplaceCertificatePaths(t *testing.T) {
|
func TestReplaceCertificatePaths(t *testing.T) {
|
||||||
rawConfig := `a.caddy.localhost:9443 {
|
rawConfig := `a.caddy.localhost:9443{
|
||||||
tls /caddy.localhost.crt /caddy.localhost.key {
|
tls /caddy.localhost.crt /caddy.localhost.key {
|
||||||
}
|
}
|
||||||
|
|
||||||
redir / https://b.caddy.localhost:9443/version 301
|
redir / https://b.caddy.localhost:9443/version 301
|
||||||
|
|
||||||
respond /version 200 {
|
respond /version 200 {
|
||||||
body "hello from a.caddy.localhost"
|
body "hello from a.caddy.localhost"
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
r := prependCaddyFilePath(rawConfig)
|
r := prependCaddyFilePath(rawConfig)
|
||||||
@@ -34,8 +35,8 @@ func TestReplaceCertificatePaths(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadUnorderedJSON(t *testing.T) {
|
func TestLoadUnorderedJSON(t *testing.T) {
|
||||||
tester := NewTester(t)
|
harness := StartHarness(t)
|
||||||
tester.InitServer(`
|
harness.LoadConfig(`
|
||||||
{
|
{
|
||||||
"logging": {
|
"logging": {
|
||||||
"logs": {
|
"logs": {
|
||||||
@@ -68,7 +69,7 @@ func TestLoadUnorderedJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"listen": "localhost:2999"
|
"listen": "{$TESTING_CADDY_ADMIN_BIND}"
|
||||||
},
|
},
|
||||||
"apps": {
|
"apps": {
|
||||||
"pki": {
|
"pki": {
|
||||||
@@ -79,12 +80,13 @@ func TestLoadUnorderedJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"http": {
|
"http": {
|
||||||
"http_port": 9080,
|
"http_port": {$TESTING_CADDY_PORT_ONE},
|
||||||
"https_port": 9443,
|
"https_port": {$TESTING_CADDY_PORT_TWO},
|
||||||
"servers": {
|
"servers": {
|
||||||
"s_server": {
|
"s_server": {
|
||||||
"listen": [
|
"listen": [
|
||||||
":9080"
|
":{$TESTING_CADDY_PORT_ONE}",
|
||||||
|
":{$TESTING_CADDY_PORT_TWO}"
|
||||||
],
|
],
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
@@ -119,10 +121,10 @@ func TestLoadUnorderedJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, "json")
|
`, "json")
|
||||||
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
|
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tester.AssertResponseCode(req, 200)
|
harness.AssertResponseCode(req, 200)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,20 +6,17 @@ import (
|
|||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/mholt/acmez/v3"
|
|
||||||
"github.com/mholt/acmez/v3/acme"
|
|
||||||
smallstepacme "github.com/smallstep/certificates/acme"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"go.uber.org/zap/exp/zapslog"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddytest"
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
|
"github.com/mholt/acmez/v2"
|
||||||
|
"github.com/mholt/acmez/v2/acme"
|
||||||
|
smallstepacme "github.com/smallstep/certificates/acme"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
const acmeChallengePort = 9081
|
const acmeChallengePort = 9081
|
||||||
@@ -27,19 +24,13 @@ const acmeChallengePort = 9081
|
|||||||
// Test the basic functionality of Caddy's ACME server
|
// Test the basic functionality of Caddy's ACME server
|
||||||
func TestACMEServerWithDefaults(t *testing.T) {
|
func TestACMEServerWithDefaults(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
logger, err := zap.NewDevelopment()
|
harness := caddytest.StartHarness(t)
|
||||||
if err != nil {
|
harness.LoadConfig(`
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
|
||||||
tester.InitServer(`
|
|
||||||
{
|
{
|
||||||
skip_install_trust
|
skip_install_trust
|
||||||
admin localhost:2999
|
admin {$TESTING_CADDY_ADMIN_BIND}
|
||||||
http_port 9080
|
http_port {$TESTING_CADDY_PORT_ONE}
|
||||||
https_port 9443
|
https_port {$TESTING_CADDY_PORT_TWO}
|
||||||
local_certs
|
local_certs
|
||||||
}
|
}
|
||||||
acme.localhost {
|
acme.localhost {
|
||||||
@@ -47,11 +38,12 @@ func TestACMEServerWithDefaults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
`, "caddyfile")
|
`, "caddyfile")
|
||||||
|
|
||||||
|
logger := caddy.Log().Named("acmeserver")
|
||||||
client := acmez.Client{
|
client := acmez.Client{
|
||||||
Client: &acme.Client{
|
Client: &acme.Client{
|
||||||
Directory: "https://acme.localhost:9443/acme/local/directory",
|
Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()),
|
||||||
HTTPClient: tester.Client,
|
HTTPClient: harness.Client(),
|
||||||
Logger: slog.New(zapslog.NewHandler(logger.Core())),
|
Logger: logger,
|
||||||
},
|
},
|
||||||
ChallengeSolvers: map[string]acmez.Solver{
|
ChallengeSolvers: map[string]acmez.Solver{
|
||||||
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
||||||
@@ -100,13 +92,13 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
logger := caddy.Log().Named("acmez")
|
logger := caddy.Log().Named("acmez")
|
||||||
|
|
||||||
tester := caddytest.NewTester(t)
|
harness := caddytest.StartHarness(t)
|
||||||
tester.InitServer(`
|
harness.LoadConfig(`
|
||||||
{
|
{
|
||||||
skip_install_trust
|
skip_install_trust
|
||||||
admin localhost:2999
|
admin {$TESTING_CADDY_ADMIN_BIND}
|
||||||
http_port 9080
|
http_port {$TESTING_CADDY_PORT_ONE}
|
||||||
https_port 9443
|
https_port {$TESTING_CADDY_PORT_TWO}
|
||||||
local_certs
|
local_certs
|
||||||
}
|
}
|
||||||
acme.localhost {
|
acme.localhost {
|
||||||
@@ -118,9 +110,9 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) {
|
|||||||
|
|
||||||
client := acmez.Client{
|
client := acmez.Client{
|
||||||
Client: &acme.Client{
|
Client: &acme.Client{
|
||||||
Directory: "https://acme.localhost:9443/acme/local/directory",
|
Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()),
|
||||||
HTTPClient: tester.Client,
|
HTTPClient: harness.Client(),
|
||||||
Logger: slog.New(zapslog.NewHandler(logger.Core())),
|
Logger: logger,
|
||||||
},
|
},
|
||||||
ChallengeSolvers: map[string]acmez.Solver{
|
ChallengeSolvers: map[string]acmez.Solver{
|
||||||
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
||||||
|
|||||||
@@ -5,53 +5,51 @@ import (
|
|||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"log/slog"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/mholt/acmez/v3"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/mholt/acmez/v3/acme"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"go.uber.org/zap/exp/zapslog"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddytest"
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
|
"github.com/mholt/acmez/v2"
|
||||||
|
"github.com/mholt/acmez/v2/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestACMEServerDirectory(t *testing.T) {
|
func TestACMEServerDirectory(t *testing.T) {
|
||||||
tester := caddytest.NewTester(t)
|
harness := caddytest.StartHarness(t)
|
||||||
tester.InitServer(`
|
harness.LoadConfig(`
|
||||||
{
|
{
|
||||||
skip_install_trust
|
skip_install_trust
|
||||||
local_certs
|
local_certs
|
||||||
admin localhost:2999
|
admin {$TESTING_CADDY_ADMIN_BIND}
|
||||||
http_port 9080
|
http_port {$TESTING_CADDY_PORT_ONE}
|
||||||
https_port 9443
|
https_port {$TESTING_CADDY_PORT_TWO}
|
||||||
pki {
|
pki {
|
||||||
ca local {
|
ca local {
|
||||||
name "Caddy Local Authority"
|
name "Caddy Local Authority"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
acme.localhost:9443 {
|
acme.localhost:{$TESTING_CADDY_PORT_TWO} {
|
||||||
acme_server
|
acme_server
|
||||||
}
|
}
|
||||||
`, "caddyfile")
|
`, "caddyfile")
|
||||||
tester.AssertGetResponse(
|
harness.AssertGetResponse(
|
||||||
"https://acme.localhost:9443/acme/local/directory",
|
fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()),
|
||||||
200,
|
200,
|
||||||
`{"newNonce":"https://acme.localhost:9443/acme/local/new-nonce","newAccount":"https://acme.localhost:9443/acme/local/new-account","newOrder":"https://acme.localhost:9443/acme/local/new-order","revokeCert":"https://acme.localhost:9443/acme/local/revoke-cert","keyChange":"https://acme.localhost:9443/acme/local/key-change"}
|
fmt.Sprintf(`{"newNonce":"https://acme.localhost:%[1]d/acme/local/new-nonce","newAccount":"https://acme.localhost:%[1]d/acme/local/new-account","newOrder":"https://acme.localhost:%[1]d/acme/local/new-order","revokeCert":"https://acme.localhost:%[1]d/acme/local/revoke-cert","keyChange":"https://acme.localhost:%[1]d/acme/local/key-change"}
|
||||||
`)
|
`, harness.Tester().PortTwo()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestACMEServerAllowPolicy(t *testing.T) {
|
func TestACMEServerAllowPolicy(t *testing.T) {
|
||||||
tester := caddytest.NewTester(t)
|
harness := caddytest.StartHarness(t)
|
||||||
tester.InitServer(`
|
harness.LoadConfig(`
|
||||||
{
|
{
|
||||||
skip_install_trust
|
skip_install_trust
|
||||||
local_certs
|
local_certs
|
||||||
admin localhost:2999
|
admin {$TESTING_CADDY_ADMIN_BIND}
|
||||||
http_port 9080
|
http_port {$TESTING_CADDY_PORT_ONE}
|
||||||
https_port 9443
|
https_port {$TESTING_CADDY_PORT_TWO}
|
||||||
pki {
|
pki {
|
||||||
ca local {
|
ca local {
|
||||||
name "Caddy Local Authority"
|
name "Caddy Local Authority"
|
||||||
@@ -69,17 +67,13 @@ func TestACMEServerAllowPolicy(t *testing.T) {
|
|||||||
`, "caddyfile")
|
`, "caddyfile")
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
logger, err := zap.NewDevelopment()
|
logger := caddy.Log().Named("acmez")
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client := acmez.Client{
|
client := acmez.Client{
|
||||||
Client: &acme.Client{
|
Client: &acme.Client{
|
||||||
Directory: "https://acme.localhost:9443/acme/local/directory",
|
Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()),
|
||||||
HTTPClient: tester.Client,
|
HTTPClient: harness.Client(),
|
||||||
Logger: slog.New(zapslog.NewHandler(logger.Core())),
|
Logger: logger,
|
||||||
},
|
},
|
||||||
ChallengeSolvers: map[string]acmez.Solver{
|
ChallengeSolvers: map[string]acmez.Solver{
|
||||||
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
||||||
@@ -134,14 +128,14 @@ func TestACMEServerAllowPolicy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestACMEServerDenyPolicy(t *testing.T) {
|
func TestACMEServerDenyPolicy(t *testing.T) {
|
||||||
tester := caddytest.NewTester(t)
|
harness := caddytest.StartHarness(t)
|
||||||
tester.InitServer(`
|
harness.LoadConfig(`
|
||||||
{
|
{
|
||||||
skip_install_trust
|
skip_install_trust
|
||||||
local_certs
|
local_certs
|
||||||
admin localhost:2999
|
admin {$TESTING_CADDY_ADMIN_BIND}
|
||||||
http_port 9080
|
http_port {$TESTING_CADDY_PORT_ONE}
|
||||||
https_port 9443
|
https_port {$TESTING_CADDY_PORT_TWO}
|
||||||
pki {
|
pki {
|
||||||
ca local {
|
ca local {
|
||||||
name "Caddy Local Authority"
|
name "Caddy Local Authority"
|
||||||
@@ -158,17 +152,13 @@ func TestACMEServerDenyPolicy(t *testing.T) {
|
|||||||
`, "caddyfile")
|
`, "caddyfile")
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
logger, err := zap.NewDevelopment()
|
logger := caddy.Log().Named("acmez")
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client := acmez.Client{
|
client := acmez.Client{
|
||||||
Client: &acme.Client{
|
Client: &acme.Client{
|
||||||
Directory: "https://acme.localhost:9443/acme/local/directory",
|
Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()),
|
||||||
HTTPClient: tester.Client,
|
HTTPClient: harness.Client(),
|
||||||
Logger: slog.New(zapslog.NewHandler(logger.Core())),
|
Logger: logger,
|
||||||
},
|
},
|
||||||
ChallengeSolvers: map[string]acmez.Solver{
|
ChallengeSolvers: map[string]acmez.Solver{
|
||||||
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
||||||
@@ -200,7 +190,7 @@ func TestACMEServerDenyPolicy(t *testing.T) {
|
|||||||
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"})
|
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("obtaining certificate for 'deny.localhost' domain")
|
t.Errorf("obtaining certificate for 'deny.localhost' domain")
|
||||||
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
|
} else if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
|
||||||
t.Logf("unexpected error: %v", err)
|
t.Logf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -8,69 +9,69 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) {
|
func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) {
|
||||||
tester := caddytest.NewTester(t)
|
harness := caddytest.StartHarness(t)
|
||||||
tester.InitServer(`
|
harness.LoadConfig(`
|
||||||
{
|
{
|
||||||
admin localhost:2999
|
admin {$TESTING_CADDY_ADMIN_BIND}
|
||||||
skip_install_trust
|
skip_install_trust
|
||||||
http_port 9080
|
http_port {$TESTING_CADDY_PORT_ONE}
|
||||||
https_port 9443
|
https_port {$TESTING_CADDY_PORT_TWO}
|
||||||
}
|
}
|
||||||
localhost
|
localhost
|
||||||
respond "Yahaha! You found me!"
|
respond "Yahaha! You found me!"
|
||||||
`, "caddyfile")
|
`, "caddyfile")
|
||||||
|
|
||||||
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
|
harness.AssertRedirect(fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), "https://localhost/", http.StatusPermanentRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) {
|
func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) {
|
||||||
tester := caddytest.NewTester(t)
|
harness := caddytest.StartHarness(t)
|
||||||
tester.InitServer(`
|
harness.LoadConfig(`
|
||||||
{
|
{
|
||||||
skip_install_trust
|
skip_install_trust
|
||||||
admin localhost:2999
|
admin {$TESTING_CADDY_ADMIN_BIND}
|
||||||
http_port 9080
|
http_port {$TESTING_CADDY_PORT_ONE}
|
||||||
https_port 9443
|
https_port {$TESTING_CADDY_PORT_TWO}
|
||||||
}
|
}
|
||||||
localhost:9443
|
localhost:{$TESTING_CADDY_PORT_TWO}
|
||||||
respond "Yahaha! You found me!"
|
respond "Yahaha! You found me!"
|
||||||
`, "caddyfile")
|
`, "caddyfile")
|
||||||
|
|
||||||
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
|
harness.AssertRedirect(fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), "https://localhost/", http.StatusPermanentRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T) {
|
func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T) {
|
||||||
tester := caddytest.NewTester(t)
|
harness := caddytest.StartHarness(t)
|
||||||
tester.InitServer(`
|
harness.LoadConfig(`
|
||||||
{
|
{
|
||||||
skip_install_trust
|
skip_install_trust
|
||||||
admin localhost:2999
|
admin {$TESTING_CADDY_ADMIN_BIND}
|
||||||
http_port 9080
|
http_port {$TESTING_CADDY_PORT_ONE}
|
||||||
https_port 9443
|
https_port {$TESTING_CADDY_PORT_TWO}
|
||||||
}
|
}
|
||||||
localhost:1234
|
localhost:1234
|
||||||
respond "Yahaha! You found me!"
|
respond "Yahaha! You found me!"
|
||||||
`, "caddyfile")
|
`, "caddyfile")
|
||||||
|
|
||||||
tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect)
|
harness.AssertRedirect(fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), "https://localhost:1234/", http.StatusPermanentRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
|
func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
|
||||||
tester := caddytest.NewTester(t)
|
harness := caddytest.StartHarness(t)
|
||||||
tester.InitServer(`
|
harness.LoadConfig(`
|
||||||
{
|
{
|
||||||
"admin": {
|
"admin": {
|
||||||
"listen": "localhost:2999"
|
"listen": "{$TESTING_CADDY_ADMIN_BIND}"
|
||||||
},
|
},
|
||||||
"apps": {
|
"apps": {
|
||||||
"http": {
|
"http": {
|
||||||
"http_port": 9080,
|
"http_port": {$TESTING_CADDY_PORT_ONE},
|
||||||
"https_port": 9443,
|
"https_port": {$TESTING_CADDY_PORT_TWO},
|
||||||
"servers": {
|
"servers": {
|
||||||
"ingress_server": {
|
"ingress_server": {
|
||||||
"listen": [
|
"listen": [
|
||||||
":9080",
|
":{$TESTING_CADDY_PORT_ONE}",
|
||||||
":9443"
|
":{$TESTING_CADDY_PORT_TWO}"
|
||||||
],
|
],
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
@@ -94,52 +95,52 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, "json")
|
`, "json")
|
||||||
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
|
harness.AssertRedirect(fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), "https://localhost/", http.StatusPermanentRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) {
|
func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) {
|
||||||
tester := caddytest.NewTester(t)
|
harness := caddytest.StartHarness(t)
|
||||||
tester.InitServer(`
|
harness.LoadConfig(`
|
||||||
{
|
{
|
||||||
skip_install_trust
|
skip_install_trust
|
||||||
admin localhost:2999
|
admin {$TESTING_CADDY_ADMIN_BIND}
|
||||||
http_port 9080
|
http_port {$TESTING_CADDY_PORT_ONE}
|
||||||
https_port 9443
|
https_port {$TESTING_CADDY_PORT_TWO}
|
||||||
local_certs
|
local_certs
|
||||||
}
|
}
|
||||||
http://:9080 {
|
http://:{$TESTING_CADDY_PORT_ONE} {
|
||||||
respond "Foo"
|
respond "Foo"
|
||||||
}
|
}
|
||||||
http://baz.localhost:9080 {
|
http://baz.localhost:{$TESTING_CADDY_PORT_ONE} {
|
||||||
respond "Baz"
|
respond "Baz"
|
||||||
}
|
}
|
||||||
bar.localhost {
|
bar.localhost {
|
||||||
respond "Bar"
|
respond "Bar"
|
||||||
}
|
}
|
||||||
`, "caddyfile")
|
`, "caddyfile")
|
||||||
tester.AssertRedirect("http://bar.localhost:9080/", "https://bar.localhost/", http.StatusPermanentRedirect)
|
harness.AssertRedirect(fmt.Sprintf("http://bar.localhost:%d/", harness.Tester().PortOne()), "https://bar.localhost/", http.StatusPermanentRedirect)
|
||||||
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
|
harness.AssertGetResponse(fmt.Sprintf("http://foo.localhost:%d/", harness.Tester().PortOne()), 200, "Foo")
|
||||||
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Baz")
|
harness.AssertGetResponse(fmt.Sprintf("http://baz.localhost:%d/", harness.Tester().PortOne()), 200, "Baz")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSite(t *testing.T) {
|
func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSite(t *testing.T) {
|
||||||
tester := caddytest.NewTester(t)
|
harness := caddytest.StartHarness(t)
|
||||||
tester.InitServer(`
|
harness.LoadConfig(`
|
||||||
{
|
{
|
||||||
skip_install_trust
|
skip_install_trust
|
||||||
admin localhost:2999
|
admin {$TESTING_CADDY_ADMIN_BIND}
|
||||||
http_port 9080
|
http_port {$TESTING_CADDY_PORT_ONE}
|
||||||
https_port 9443
|
https_port {$TESTING_CADDY_PORT_TWO}
|
||||||
local_certs
|
local_certs
|
||||||
}
|
}
|
||||||
http://:9080 {
|
http://:{$TESTING_CADDY_PORT_ONE} {
|
||||||
respond "Foo"
|
respond "Foo"
|
||||||
}
|
}
|
||||||
bar.localhost {
|
bar.localhost {
|
||||||
respond "Bar"
|
respond "Bar"
|
||||||
}
|
}
|
||||||
`, "caddyfile")
|
`, "caddyfile")
|
||||||
tester.AssertRedirect("http://bar.localhost:9080/", "https://bar.localhost/", http.StatusPermanentRedirect)
|
harness.AssertRedirect(fmt.Sprintf("http://bar.localhost:%d/", harness.Tester().PortOne()), "https://bar.localhost/", http.StatusPermanentRedirect)
|
||||||
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
|
harness.AssertGetResponse(fmt.Sprintf("http://foo.localhost:%d/", harness.Tester().PortOne()), 200, "Foo")
|
||||||
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Foo")
|
harness.AssertGetResponse(fmt.Sprintf("http://baz.localhost:%d/", harness.Tester().PortOne()), 200, "Foo")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
{
|
|
||||||
acme_dns mock foo
|
|
||||||
}
|
|
||||||
|
|
||||||
example.com {
|
|
||||||
respond "Hello World"
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "Hello World",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tls": {
|
|
||||||
"automation": {
|
|
||||||
"policies": [
|
|
||||||
{
|
|
||||||
"issuers": [
|
|
||||||
{
|
|
||||||
"challenges": {
|
|
||||||
"dns": {
|
|
||||||
"provider": {
|
|
||||||
"argument": "foo",
|
|
||||||
"name": "mock"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"module": "acme"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
dns mock
|
|
||||||
acme_dns
|
|
||||||
}
|
|
||||||
|
|
||||||
example.com {
|
|
||||||
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tls": {
|
|
||||||
"automation": {
|
|
||||||
"policies": [
|
|
||||||
{
|
|
||||||
"issuers": [
|
|
||||||
{
|
|
||||||
"challenges": {
|
|
||||||
"dns": {}
|
|
||||||
},
|
|
||||||
"module": "acme"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dns": {
|
|
||||||
"name": "mock"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
acme_dns
|
|
||||||
}
|
|
||||||
|
|
||||||
example.com {
|
|
||||||
respond "Hello World"
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
acme_dns specified without DNS provider config, but no provider specified with 'dns' global option
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
{
|
|
||||||
pki {
|
|
||||||
ca custom-ca {
|
|
||||||
name "Custom CA"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
acme.example.com {
|
|
||||||
acme_server {
|
|
||||||
ca custom-ca
|
|
||||||
allow {
|
|
||||||
domains host-1.internal.example.com host-2.internal.example.com
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"acme.example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"ca": "custom-ca",
|
|
||||||
"handler": "acme_server",
|
|
||||||
"policy": {
|
|
||||||
"allow": {
|
|
||||||
"domains": [
|
|
||||||
"host-1.internal.example.com",
|
|
||||||
"host-2.internal.example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pki": {
|
|
||||||
"certificate_authorities": {
|
|
||||||
"custom-ca": {
|
|
||||||
"name": "Custom CA"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
{
|
|
||||||
pki {
|
|
||||||
ca custom-ca {
|
|
||||||
name "Custom CA"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
acme.example.com {
|
|
||||||
acme_server {
|
|
||||||
ca custom-ca
|
|
||||||
allow {
|
|
||||||
domains host-1.internal.example.com host-2.internal.example.com
|
|
||||||
}
|
|
||||||
deny {
|
|
||||||
domains dc.internal.example.com
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"acme.example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"ca": "custom-ca",
|
|
||||||
"handler": "acme_server",
|
|
||||||
"policy": {
|
|
||||||
"allow": {
|
|
||||||
"domains": [
|
|
||||||
"host-1.internal.example.com",
|
|
||||||
"host-2.internal.example.com"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"deny": {
|
|
||||||
"domains": [
|
|
||||||
"dc.internal.example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pki": {
|
|
||||||
"certificate_authorities": {
|
|
||||||
"custom-ca": {
|
|
||||||
"name": "Custom CA"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
{
|
|
||||||
pki {
|
|
||||||
ca custom-ca {
|
|
||||||
name "Custom CA"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
acme.example.com {
|
|
||||||
acme_server {
|
|
||||||
ca custom-ca
|
|
||||||
deny {
|
|
||||||
domains dc.internal.example.com
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"acme.example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"ca": "custom-ca",
|
|
||||||
"handler": "acme_server",
|
|
||||||
"policy": {
|
|
||||||
"deny": {
|
|
||||||
"domains": [
|
|
||||||
"dc.internal.example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pki": {
|
|
||||||
"certificate_authorities": {
|
|
||||||
"custom-ca": {
|
|
||||||
"name": "Custom CA"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,15 +5,15 @@
|
|||||||
root_cn "Internal Root Cert"
|
root_cn "Internal Root Cert"
|
||||||
intermediate_cn "Internal Intermediate Cert"
|
intermediate_cn "Internal Intermediate Cert"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
acme.example.com {
|
acme.example.com {
|
||||||
acme_server {
|
acme_server {
|
||||||
ca internal
|
ca internal
|
||||||
sign_with_root
|
sign_with_root
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
"apps": {
|
"apps": {
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
example.com
|
|
||||||
handle {
|
|
||||||
respond "one"
|
|
||||||
}
|
|
||||||
|
|
||||||
example.com
|
|
||||||
handle {
|
|
||||||
respond "two"
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
Caddyfile:6: unrecognized directive: example.com
|
|
||||||
Did you mean to define a second site? If so, you must use curly braces around each site to separate their configurations.
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
:8080 {
|
|
||||||
respond "one"
|
|
||||||
}
|
|
||||||
|
|
||||||
:8080 {
|
|
||||||
respond "two"
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
ambiguous site definition: :8080
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
{
|
|
||||||
auto_https disable_redirects
|
|
||||||
admin off
|
|
||||||
}
|
|
||||||
|
|
||||||
http://localhost {
|
|
||||||
bind fd/{env.CADDY_HTTP_FD} {
|
|
||||||
protocols h1
|
|
||||||
}
|
|
||||||
log
|
|
||||||
respond "Hello, HTTP!"
|
|
||||||
}
|
|
||||||
|
|
||||||
https://localhost {
|
|
||||||
bind fd/{env.CADDY_HTTPS_FD} {
|
|
||||||
protocols h1 h2
|
|
||||||
}
|
|
||||||
bind fdgram/{env.CADDY_HTTP3_FD} {
|
|
||||||
protocols h3
|
|
||||||
}
|
|
||||||
log
|
|
||||||
respond "Hello, HTTPS!"
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"admin": {
|
|
||||||
"disabled": true
|
|
||||||
},
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
"fd/{env.CADDY_HTTPS_FD}",
|
|
||||||
"fdgram/{env.CADDY_HTTP3_FD}"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "Hello, HTTPS!",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"automatic_https": {
|
|
||||||
"disable_redirects": true
|
|
||||||
},
|
|
||||||
"logs": {
|
|
||||||
"logger_names": {
|
|
||||||
"localhost": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"listen_protocols": [
|
|
||||||
[
|
|
||||||
"h1",
|
|
||||||
"h2"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"h3"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"srv1": {
|
|
||||||
"automatic_https": {
|
|
||||||
"disable_redirects": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"srv2": {
|
|
||||||
"listen": [
|
|
||||||
"fd/{env.CADDY_HTTP_FD}"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "Hello, HTTP!",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"automatic_https": {
|
|
||||||
"disable_redirects": true,
|
|
||||||
"skip": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"logs": {
|
|
||||||
"logger_names": {
|
|
||||||
"localhost": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"listen_protocols": [
|
|
||||||
[
|
|
||||||
"h1"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
handle
|
|
||||||
|
|
||||||
respond "should not work"
|
|
||||||
----------
|
|
||||||
Caddyfile:1: parsed 'handle' as a site address, but it is a known directive; directives must appear in a site block
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
servers {
|
|
||||||
srv0 {
|
|
||||||
listen :8080
|
|
||||||
}
|
|
||||||
srv1 {
|
|
||||||
listen :8080
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
parsing caddyfile tokens for 'servers': unrecognized servers option 'srv0', at Caddyfile:3
|
|
||||||
@@ -21,8 +21,6 @@ encode {
|
|||||||
zstd
|
zstd
|
||||||
gzip 5
|
gzip 5
|
||||||
}
|
}
|
||||||
|
|
||||||
encode
|
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
"apps": {
|
"apps": {
|
||||||
@@ -78,17 +76,6 @@ encode
|
|||||||
"zstd",
|
"zstd",
|
||||||
"gzip"
|
"gzip"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"encodings": {
|
|
||||||
"gzip": {},
|
|
||||||
"zstd": {}
|
|
||||||
},
|
|
||||||
"handler": "encode",
|
|
||||||
"prefer": [
|
|
||||||
"zstd",
|
|
||||||
"gzip"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,29 +106,20 @@ example.com {
|
|||||||
"handler": "subroute",
|
"handler": "subroute",
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
|
"group": "group0",
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "subroute",
|
"handler": "rewrite",
|
||||||
"routes": [
|
"uri": "/{http.error.status_code}.html"
|
||||||
{
|
}
|
||||||
"group": "group0",
|
]
|
||||||
"handle": [
|
},
|
||||||
{
|
{
|
||||||
"handler": "rewrite",
|
"handle": [
|
||||||
"uri": "/{http.error.status_code}.html"
|
{
|
||||||
}
|
"handler": "file_server",
|
||||||
]
|
"hide": [
|
||||||
},
|
"./Caddyfile"
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "file_server",
|
|
||||||
"hide": [
|
|
||||||
"./Caddyfile"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -165,17 +165,8 @@ bar.localhost {
|
|||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "subroute",
|
"body": "404 or 410 error",
|
||||||
"routes": [
|
"handler": "static_response"
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "404 or 410 error",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"match": [
|
"match": [
|
||||||
@@ -187,17 +178,8 @@ bar.localhost {
|
|||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "subroute",
|
"body": "Error In range [500 .. 599]",
|
||||||
"routes": [
|
"handler": "static_response"
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "Error In range [500 .. 599]",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"match": [
|
"match": [
|
||||||
@@ -226,17 +208,8 @@ bar.localhost {
|
|||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "subroute",
|
"body": "404 or 410 error from second site",
|
||||||
"routes": [
|
"handler": "static_response"
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "404 or 410 error from second site",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"match": [
|
"match": [
|
||||||
@@ -248,17 +221,8 @@ bar.localhost {
|
|||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "subroute",
|
"body": "Error In range [500 .. 599] from second site",
|
||||||
"routes": [
|
"handler": "static_response"
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "Error In range [500 .. 599] from second site",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"match": [
|
"match": [
|
||||||
|
|||||||
@@ -96,17 +96,8 @@ localhost:3010 {
|
|||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "subroute",
|
"body": "Error in the [400 .. 499] range",
|
||||||
"routes": [
|
"handler": "static_response"
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "Error in the [400 .. 499] range",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"match": [
|
"match": [
|
||||||
|
|||||||
@@ -116,17 +116,8 @@ localhost:2099 {
|
|||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "subroute",
|
"body": "Error in the [400 .. 499] range",
|
||||||
"routes": [
|
"handler": "static_response"
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "Error in the [400 .. 499] range",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"match": [
|
"match": [
|
||||||
@@ -138,17 +129,8 @@ localhost:2099 {
|
|||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "subroute",
|
"body": "Error code is equal to 500 or in the [300..399] range",
|
||||||
"routes": [
|
"handler": "static_response"
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "Error code is equal to 500 or in the [300..399] range",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"match": [
|
"match": [
|
||||||
|
|||||||
@@ -96,17 +96,8 @@ localhost:3010 {
|
|||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "subroute",
|
"body": "404 or 410 error",
|
||||||
"routes": [
|
"handler": "static_response"
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "404 or 410 error",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"match": [
|
"match": [
|
||||||
|
|||||||
@@ -116,17 +116,8 @@ localhost:2099 {
|
|||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "subroute",
|
"body": "Error in the [400 .. 499] range",
|
||||||
"routes": [
|
"handler": "static_response"
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "Error in the [400 .. 499] range",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"match": [
|
"match": [
|
||||||
@@ -138,17 +129,8 @@ localhost:2099 {
|
|||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "subroute",
|
"body": "Fallback route: code outside the [400..499] range",
|
||||||
"routes": [
|
"handler": "static_response"
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "Fallback route: code outside the [400..499] range",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,260 +0,0 @@
|
|||||||
{
|
|
||||||
http_port 2099
|
|
||||||
}
|
|
||||||
localhost:2099 {
|
|
||||||
root * /var/www/
|
|
||||||
file_server
|
|
||||||
|
|
||||||
handle_errors 404 {
|
|
||||||
handle /en/* {
|
|
||||||
respond "not found" 404
|
|
||||||
}
|
|
||||||
handle /es/* {
|
|
||||||
respond "no encontrado"
|
|
||||||
}
|
|
||||||
handle {
|
|
||||||
respond "default not found"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handle_errors {
|
|
||||||
handle /en/* {
|
|
||||||
respond "English error"
|
|
||||||
}
|
|
||||||
handle /es/* {
|
|
||||||
respond "Spanish error"
|
|
||||||
}
|
|
||||||
handle {
|
|
||||||
respond "Default error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"http_port": 2099,
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":2099"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "vars",
|
|
||||||
"root": "/var/www/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"handler": "file_server",
|
|
||||||
"hide": [
|
|
||||||
"./Caddyfile"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"errors": {
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"localhost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"group": "group3",
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "not found",
|
|
||||||
"handler": "static_response",
|
|
||||||
"status_code": 404
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"path": [
|
|
||||||
"/en/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "group3",
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "no encontrado",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"path": [
|
|
||||||
"/es/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "group3",
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "default not found",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"expression": "{http.error.status_code} in [404]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"group": "group8",
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "English error",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"path": [
|
|
||||||
"/en/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "group8",
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "Spanish error",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"path": [
|
|
||||||
"/es/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "group8",
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "Default error",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
:80
|
|
||||||
|
|
||||||
file_server {
|
|
||||||
browse {
|
|
||||||
file_limit 4000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":80"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"browse": {
|
|
||||||
"file_limit": 4000
|
|
||||||
},
|
|
||||||
"handler": "file_server",
|
|
||||||
"hide": [
|
|
||||||
"./Caddyfile"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,10 +3,6 @@
|
|||||||
file_server {
|
file_server {
|
||||||
precompressed zstd br gzip
|
precompressed zstd br gzip
|
||||||
}
|
}
|
||||||
|
|
||||||
file_server {
|
|
||||||
precompressed
|
|
||||||
}
|
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
"apps": {
|
"apps": {
|
||||||
@@ -34,22 +30,6 @@ file_server {
|
|||||||
"br",
|
"br",
|
||||||
"gzip"
|
"gzip"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"handler": "file_server",
|
|
||||||
"hide": [
|
|
||||||
"./Caddyfile"
|
|
||||||
],
|
|
||||||
"precompressed": {
|
|
||||||
"br": {},
|
|
||||||
"gzip": {},
|
|
||||||
"zstd": {}
|
|
||||||
},
|
|
||||||
"precompressed_order": [
|
|
||||||
"br",
|
|
||||||
"zstd",
|
|
||||||
"gzip"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
:80
|
|
||||||
|
|
||||||
file_server {
|
|
||||||
browse {
|
|
||||||
sort size desc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":80"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"browse": {
|
|
||||||
"sort": [
|
|
||||||
"size",
|
|
||||||
"desc"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"handler": "file_server",
|
|
||||||
"hide": [
|
|
||||||
"./Caddyfile"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
app.example.com {
|
app.example.com {
|
||||||
forward_auth authelia:9091 {
|
forward_auth authelia:9091 {
|
||||||
uri /api/authz/forward-auth
|
uri /api/verify?rd=https://authelia.example.com
|
||||||
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
|
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,13 +39,6 @@ app.example.com {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "vars"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -54,104 +47,19 @@ app.example.com {
|
|||||||
"set": {
|
"set": {
|
||||||
"Remote-Email": [
|
"Remote-Email": [
|
||||||
"{http.reverse_proxy.header.Remote-Email}"
|
"{http.reverse_proxy.header.Remote-Email}"
|
||||||
]
|
],
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"not": [
|
|
||||||
{
|
|
||||||
"vars": {
|
|
||||||
"{http.reverse_proxy.header.Remote-Email}": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"set": {
|
|
||||||
"Remote-Groups": [
|
"Remote-Groups": [
|
||||||
"{http.reverse_proxy.header.Remote-Groups}"
|
"{http.reverse_proxy.header.Remote-Groups}"
|
||||||
]
|
],
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"not": [
|
|
||||||
{
|
|
||||||
"vars": {
|
|
||||||
"{http.reverse_proxy.header.Remote-Groups}": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"set": {
|
|
||||||
"Remote-Name": [
|
"Remote-Name": [
|
||||||
"{http.reverse_proxy.header.Remote-Name}"
|
"{http.reverse_proxy.header.Remote-Name}"
|
||||||
]
|
],
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"not": [
|
|
||||||
{
|
|
||||||
"vars": {
|
|
||||||
"{http.reverse_proxy.header.Remote-Name}": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"set": {
|
|
||||||
"Remote-User": [
|
"Remote-User": [
|
||||||
"{http.reverse_proxy.header.Remote-User}"
|
"{http.reverse_proxy.header.Remote-User}"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"not": [
|
|
||||||
{
|
|
||||||
"vars": {
|
|
||||||
"{http.reverse_proxy.header.Remote-User}": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -172,7 +80,7 @@ app.example.com {
|
|||||||
},
|
},
|
||||||
"rewrite": {
|
"rewrite": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"uri": "/api/authz/forward-auth"
|
"uri": "/api/verify?rd=https://authelia.example.com"
|
||||||
},
|
},
|
||||||
"upstreams": [
|
"upstreams": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,13 +28,6 @@ forward_auth localhost:9000 {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "vars"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -43,131 +36,22 @@ forward_auth localhost:9000 {
|
|||||||
"set": {
|
"set": {
|
||||||
"1": [
|
"1": [
|
||||||
"{http.reverse_proxy.header.A}"
|
"{http.reverse_proxy.header.A}"
|
||||||
]
|
],
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"not": [
|
|
||||||
{
|
|
||||||
"vars": {
|
|
||||||
"{http.reverse_proxy.header.A}": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"set": {
|
|
||||||
"B": [
|
|
||||||
"{http.reverse_proxy.header.B}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"not": [
|
|
||||||
{
|
|
||||||
"vars": {
|
|
||||||
"{http.reverse_proxy.header.B}": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"set": {
|
|
||||||
"3": [
|
"3": [
|
||||||
"{http.reverse_proxy.header.C}"
|
"{http.reverse_proxy.header.C}"
|
||||||
]
|
],
|
||||||
}
|
"5": [
|
||||||
}
|
"{http.reverse_proxy.header.E}"
|
||||||
}
|
],
|
||||||
],
|
"B": [
|
||||||
"match": [
|
"{http.reverse_proxy.header.B}"
|
||||||
{
|
],
|
||||||
"not": [
|
|
||||||
{
|
|
||||||
"vars": {
|
|
||||||
"{http.reverse_proxy.header.C}": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"set": {
|
|
||||||
"D": [
|
"D": [
|
||||||
"{http.reverse_proxy.header.D}"
|
"{http.reverse_proxy.header.D}"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"not": [
|
|
||||||
{
|
|
||||||
"vars": {
|
|
||||||
"{http.reverse_proxy.header.D}": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"set": {
|
|
||||||
"5": [
|
|
||||||
"{http.reverse_proxy.header.E}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"not": [
|
|
||||||
{
|
|
||||||
"vars": {
|
|
||||||
"{http.reverse_proxy.header.E}": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
storage file_system {
|
storage file_system {
|
||||||
root /data
|
root /data
|
||||||
}
|
}
|
||||||
storage_check off
|
|
||||||
storage_clean_interval off
|
|
||||||
acme_ca https://example.com
|
acme_ca https://example.com
|
||||||
acme_ca_root /path/to/ca.crt
|
acme_ca_root /path/to/ca.crt
|
||||||
ocsp_stapling off
|
ocsp_stapling off
|
||||||
@@ -19,6 +17,8 @@
|
|||||||
admin off
|
admin off
|
||||||
on_demand_tls {
|
on_demand_tls {
|
||||||
ask https://example.com
|
ask https://example.com
|
||||||
|
interval 30s
|
||||||
|
burst 20
|
||||||
}
|
}
|
||||||
local_certs
|
local_certs
|
||||||
key_type ed25519
|
key_type ed25519
|
||||||
@@ -72,12 +72,14 @@
|
|||||||
"permission": {
|
"permission": {
|
||||||
"endpoint": "https://example.com",
|
"endpoint": "https://example.com",
|
||||||
"module": "http"
|
"module": "http"
|
||||||
|
},
|
||||||
|
"rate_limit": {
|
||||||
|
"interval": 30000000000,
|
||||||
|
"burst": 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"disable_ocsp_stapling": true,
|
"disable_ocsp_stapling": true
|
||||||
"disable_storage_check": true,
|
|
||||||
"disable_storage_clean": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
admin off
|
admin off
|
||||||
on_demand_tls {
|
on_demand_tls {
|
||||||
ask https://example.com
|
ask https://example.com
|
||||||
|
interval 30s
|
||||||
|
burst 20
|
||||||
}
|
}
|
||||||
storage_clean_interval 7d
|
storage_clean_interval 7d
|
||||||
renew_interval 1d
|
renew_interval 1d
|
||||||
@@ -87,6 +89,10 @@
|
|||||||
"permission": {
|
"permission": {
|
||||||
"endpoint": "https://example.com",
|
"endpoint": "https://example.com",
|
||||||
"module": "http"
|
"module": "http"
|
||||||
|
},
|
||||||
|
"rate_limit": {
|
||||||
|
"interval": 30000000000,
|
||||||
|
"burst": 20
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ocsp_interval": 172800000000000,
|
"ocsp_interval": 172800000000000,
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
}
|
}
|
||||||
on_demand_tls {
|
on_demand_tls {
|
||||||
ask https://example.com
|
ask https://example.com
|
||||||
|
interval 30s
|
||||||
|
burst 20
|
||||||
}
|
}
|
||||||
local_certs
|
local_certs
|
||||||
key_type ed25519
|
key_type ed25519
|
||||||
@@ -72,6 +74,10 @@
|
|||||||
"permission": {
|
"permission": {
|
||||||
"endpoint": "https://example.com",
|
"endpoint": "https://example.com",
|
||||||
"module": "http"
|
"module": "http"
|
||||||
|
},
|
||||||
|
"rate_limit": {
|
||||||
|
"interval": 30000000000,
|
||||||
|
"burst": 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
log {
|
|
||||||
sampling {
|
|
||||||
interval 300
|
|
||||||
first 50
|
|
||||||
thereafter 40
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"logging": {
|
|
||||||
"logs": {
|
|
||||||
"default": {
|
|
||||||
"sampling": {
|
|
||||||
"interval": 300,
|
|
||||||
"first": 50,
|
|
||||||
"thereafter": 40
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -31,6 +31,9 @@ example.com
|
|||||||
"automation": {
|
"automation": {
|
||||||
"policies": [
|
"policies": [
|
||||||
{
|
{
|
||||||
|
"subjects": [
|
||||||
|
"example.com"
|
||||||
|
],
|
||||||
"issuers": [
|
"issuers": [
|
||||||
{
|
{
|
||||||
"module": "acme",
|
"module": "acme",
|
||||||
|
|||||||
@@ -18,9 +18,6 @@
|
|||||||
trusted_proxies static private_ranges
|
trusted_proxies static private_ranges
|
||||||
client_ip_headers Custom-Real-Client-IP X-Forwarded-For
|
client_ip_headers Custom-Real-Client-IP X-Forwarded-For
|
||||||
client_ip_headers A-Third-One
|
client_ip_headers A-Third-One
|
||||||
keepalive_interval 20s
|
|
||||||
keepalive_idle 20s
|
|
||||||
keepalive_count 10
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,9 +45,6 @@ foo.com {
|
|||||||
"read_header_timeout": 30000000000,
|
"read_header_timeout": 30000000000,
|
||||||
"write_timeout": 30000000000,
|
"write_timeout": 30000000000,
|
||||||
"idle_timeout": 30000000000,
|
"idle_timeout": 30000000000,
|
||||||
"keepalive_interval": 20000000000,
|
|
||||||
"keepalive_idle": 20000000000,
|
|
||||||
"keepalive_count": 10,
|
|
||||||
"max_header_bytes": 100000000,
|
"max_header_bytes": 100000000,
|
||||||
"enable_full_duplex": true,
|
"enable_full_duplex": true,
|
||||||
"routes": [
|
"routes": [
|
||||||
@@ -95,4 +89,4 @@ foo.com {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,14 +12,10 @@
|
|||||||
@images path /images/*
|
@images path /images/*
|
||||||
header @images {
|
header @images {
|
||||||
Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
|
Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
|
||||||
match {
|
|
||||||
status 200
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
header {
|
header {
|
||||||
+Link "Foo"
|
+Link "Foo"
|
||||||
+Link "Bar"
|
+Link "Bar"
|
||||||
match status 200
|
|
||||||
}
|
}
|
||||||
header >Set Defer
|
header >Set Defer
|
||||||
header >Replace Deferred Replacement
|
header >Replace Deferred Replacement
|
||||||
@@ -46,11 +42,6 @@
|
|||||||
{
|
{
|
||||||
"handler": "headers",
|
"handler": "headers",
|
||||||
"response": {
|
"response": {
|
||||||
"require": {
|
|
||||||
"status_code": [
|
|
||||||
200
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"set": {
|
"set": {
|
||||||
"Cache-Control": [
|
"Cache-Control": [
|
||||||
"public, max-age=3600, stale-while-revalidate=86400"
|
"public, max-age=3600, stale-while-revalidate=86400"
|
||||||
@@ -145,11 +136,6 @@
|
|||||||
"Foo",
|
"Foo",
|
||||||
"Bar"
|
"Bar"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"status_code": [
|
|
||||||
200
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
:80 {
|
|
||||||
header Test-Static ":443" "STATIC-WORKS"
|
|
||||||
header Test-Dynamic ":{http.request.local.port}" "DYNAMIC-WORKS"
|
|
||||||
header Test-Complex "port-{http.request.local.port}-end" "COMPLEX-{http.request.method}"
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":80"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"response": {
|
|
||||||
"replace": {
|
|
||||||
"Test-Static": [
|
|
||||||
{
|
|
||||||
"replace": "STATIC-WORKS",
|
|
||||||
"search_regexp": ":443"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"response": {
|
|
||||||
"replace": {
|
|
||||||
"Test-Dynamic": [
|
|
||||||
{
|
|
||||||
"replace": "DYNAMIC-WORKS",
|
|
||||||
"search_regexp": ":{http.request.local.port}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"response": {
|
|
||||||
"replace": {
|
|
||||||
"Test-Complex": [
|
|
||||||
{
|
|
||||||
"replace": "COMPLEX-{http.request.method}",
|
|
||||||
"search_regexp": "port-{http.request.local.port}-end"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
example.com {
|
example.com {
|
||||||
respond <<EOF
|
respond <<EOF
|
||||||
<html>
|
<html>
|
||||||
<head><title>Foo</title>
|
<head><title>Foo</title>
|
||||||
<body>Foo</body>
|
<body>Foo</body>
|
||||||
</html>
|
</html>
|
||||||
EOF 200
|
EOF 200
|
||||||
}
|
}
|
||||||
|
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
"apps": {
|
"apps": {
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
:80
|
|
||||||
|
|
||||||
handle {
|
|
||||||
respond <<END
|
|
||||||
line1
|
|
||||||
line2
|
|
||||||
END
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":80"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": " line1\n line2",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
:80
|
|
||||||
|
|
||||||
handle {
|
|
||||||
respond <<EOF
|
|
||||||
Hello
|
|
||||||
# missing EOF marker
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
mismatched leading whitespace in heredoc <<EOF on line #5 [ Hello], expected whitespace [# missing ] to match the closing marker
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
:80
|
|
||||||
|
|
||||||
handle {
|
|
||||||
respond <<END!
|
|
||||||
Hello
|
|
||||||
END!
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
heredoc marker on line #4 must contain only alpha-numeric characters, dashes and underscores; got 'END!'
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
:80
|
|
||||||
|
|
||||||
handle {
|
|
||||||
respond <<END
|
|
||||||
line1
|
|
||||||
line2
|
|
||||||
END
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
mismatched leading whitespace in heredoc <<END on line #5 [ line1], expected whitespace [ ] to match the closing marker
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
:80
|
|
||||||
|
|
||||||
handle {
|
|
||||||
respond <<
|
|
||||||
Hello
|
|
||||||
END
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
parsing caddyfile tokens for 'handle': unrecognized directive: Hello - are you sure your Caddyfile structure (nesting and braces) is correct?, at Caddyfile:7
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
:80
|
|
||||||
|
|
||||||
handle {
|
|
||||||
respond <<<END
|
|
||||||
Hello
|
|
||||||
END
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
too many '<' for heredoc on line #4; only use two, for example <<END
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
(site) {
|
|
||||||
http://{args[0]} https://{args[0]} {
|
|
||||||
{block}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
import site test.domain {
|
|
||||||
{
|
|
||||||
header_up Host {host}
|
|
||||||
header_up X-Real-IP {remote_host}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
anonymous blocks are not supported
|
|
||||||
-57
@@ -1,57 +0,0 @@
|
|||||||
(snippet) {
|
|
||||||
header {
|
|
||||||
reverse_proxy localhost:3000
|
|
||||||
{block}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
example.com {
|
|
||||||
import snippet
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"response": {
|
|
||||||
"set": {
|
|
||||||
"Reverse_proxy": [
|
|
||||||
"localhost:3000"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-57
@@ -1,57 +0,0 @@
|
|||||||
(snippet) {
|
|
||||||
header {
|
|
||||||
reverse_proxy localhost:3000
|
|
||||||
{blocks.content_type}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
example.com {
|
|
||||||
import snippet
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"response": {
|
|
||||||
"set": {
|
|
||||||
"Reverse_proxy": [
|
|
||||||
"localhost:3000"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
(site) {
|
|
||||||
https://{args[0]} {
|
|
||||||
{block}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
import site test.domain {
|
|
||||||
reverse_proxy http://192.168.1.1:8080 {
|
|
||||||
header_up Host {host}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"test.domain"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "reverse_proxy",
|
|
||||||
"headers": {
|
|
||||||
"request": {
|
|
||||||
"set": {
|
|
||||||
"Host": [
|
|
||||||
"{http.request.host}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upstreams": [
|
|
||||||
{
|
|
||||||
"dial": "192.168.1.1:8080"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
(import1) {
|
|
||||||
import import2
|
|
||||||
}
|
|
||||||
|
|
||||||
(import2) {
|
|
||||||
import import1
|
|
||||||
}
|
|
||||||
|
|
||||||
import import1
|
|
||||||
|
|
||||||
----------
|
|
||||||
a cycle of imports exists between Caddyfile:import2 and Caddyfile:import1
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
example.com {
|
|
||||||
invoke foo
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
cannot invoke named route 'foo', which was not defined
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
:80
|
|
||||||
|
|
||||||
log {
|
|
||||||
output stdout
|
|
||||||
format filter {
|
|
||||||
wrap console
|
|
||||||
|
|
||||||
# Multiple regexp filters for the same field - this should work now!
|
|
||||||
request>headers>Authorization regexp "Bearer\s+([A-Za-z0-9_-]+)" "Bearer [REDACTED]"
|
|
||||||
request>headers>Authorization regexp "Basic\s+([A-Za-z0-9+/=]+)" "Basic [REDACTED]"
|
|
||||||
request>headers>Authorization regexp "token=([^&\s]+)" "token=[REDACTED]"
|
|
||||||
|
|
||||||
# Single regexp filter - this should continue to work as before
|
|
||||||
request>headers>Cookie regexp "sessionid=[^;]+" "sessionid=[REDACTED]"
|
|
||||||
|
|
||||||
# Mixed filters (non-regexp) - these should work normally
|
|
||||||
request>headers>Server delete
|
|
||||||
request>remote_ip ip_mask {
|
|
||||||
ipv4 24
|
|
||||||
ipv6 32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"logging": {
|
|
||||||
"logs": {
|
|
||||||
"default": {
|
|
||||||
"exclude": [
|
|
||||||
"http.log.access.log0"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"log0": {
|
|
||||||
"writer": {
|
|
||||||
"output": "stdout"
|
|
||||||
},
|
|
||||||
"encoder": {
|
|
||||||
"fields": {
|
|
||||||
"request\u003eheaders\u003eAuthorization": {
|
|
||||||
"filter": "multi_regexp",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"regexp": "Bearer\\s+([A-Za-z0-9_-]+)",
|
|
||||||
"value": "Bearer [REDACTED]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"regexp": "Basic\\s+([A-Za-z0-9+/=]+)",
|
|
||||||
"value": "Basic [REDACTED]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"regexp": "token=([^\u0026\\s]+)",
|
|
||||||
"value": "token=[REDACTED]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"request\u003eheaders\u003eCookie": {
|
|
||||||
"filter": "regexp",
|
|
||||||
"regexp": "sessionid=[^;]+",
|
|
||||||
"value": "sessionid=[REDACTED]"
|
|
||||||
},
|
|
||||||
"request\u003eheaders\u003eServer": {
|
|
||||||
"filter": "delete"
|
|
||||||
},
|
|
||||||
"request\u003eremote_ip": {
|
|
||||||
"filter": "ip_mask",
|
|
||||||
"ipv4_cidr": 24,
|
|
||||||
"ipv6_cidr": 32
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"format": "filter",
|
|
||||||
"wrap": {
|
|
||||||
"format": "console"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"http.log.access.log0"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":80"
|
|
||||||
],
|
|
||||||
"logs": {
|
|
||||||
"default_logger_name": "log0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
:80 {
|
|
||||||
log {
|
|
||||||
sampling {
|
|
||||||
interval 300
|
|
||||||
first 50
|
|
||||||
thereafter 40
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
{
|
|
||||||
"logging": {
|
|
||||||
"logs": {
|
|
||||||
"default": {
|
|
||||||
"exclude": [
|
|
||||||
"http.log.access.log0"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"log0": {
|
|
||||||
"sampling": {
|
|
||||||
"interval": 300,
|
|
||||||
"first": 50,
|
|
||||||
"thereafter": 40
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"http.log.access.log0"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":80"
|
|
||||||
],
|
|
||||||
"logs": {
|
|
||||||
"default_logger_name": "log0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
example.com
|
example.com
|
||||||
|
|
||||||
map {host} {my_placeholder} {magic_number} {
|
map {host} {my_placeholder} {magic_number} {
|
||||||
# Should output boolean "true" and an integer
|
# Should output boolean "true" and an integer
|
||||||
example.com true 3
|
example.com true 3
|
||||||
|
|
||||||
# Should output a string and null
|
# Should output a string and null
|
||||||
foo.example.com "string value"
|
foo.example.com "string value"
|
||||||
|
|
||||||
# Should output two strings (quoted int)
|
# Should output two strings (quoted int)
|
||||||
(.*)\.example.com "${1} subdomain" "5"
|
(.*)\.example.com "${1} subdomain" "5"
|
||||||
|
|
||||||
# Should output null and a string (quoted int)
|
# Should output null and a string (quoted int)
|
||||||
~.*\.net$ - `7`
|
~.*\.net$ - `7`
|
||||||
|
|
||||||
# Should output a float and the string "false"
|
# Should output a float and the string "false"
|
||||||
~.*\.xyz$ 123.456 "false"
|
~.*\.xyz$ 123.456 "false"
|
||||||
|
|
||||||
# Should output two strings, second being escaped quote
|
# Should output two strings, second being escaped quote
|
||||||
default "unknown domain" \"""
|
default "unknown domain" \"""
|
||||||
}
|
}
|
||||||
|
|
||||||
vars foo bar
|
vars foo bar
|
||||||
@@ -27,7 +27,6 @@ vars {
|
|||||||
ghi 2.3
|
ghi 2.3
|
||||||
jkl "mn op"
|
jkl "mn op"
|
||||||
}
|
}
|
||||||
|
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
"apps": {
|
"apps": {
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
@foo {
|
|
||||||
path /foo
|
|
||||||
}
|
|
||||||
|
|
||||||
handle {
|
|
||||||
respond "should not work"
|
|
||||||
}
|
|
||||||
----------
|
|
||||||
request matchers may not be defined globally, they must be in a site block; found @foo, at Caddyfile:1
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user