mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-25 16:22:36 -04:00
Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 01828e38bb | |||
| 59aa8588c9 | |||
| a63a87e4ef | |||
| 501f525d35 | |||
| 47ef562bbd | |||
| d87760adab | |||
| 1904c37234 | |||
| 07d2aaf22e | |||
| f2199d48b2 | |||
| 8285eba842 | |||
| cb72512d17 | |||
| 895b56063a | |||
| 8d422f0d7f | |||
| 336d514797 | |||
| 6d89bc3942 | |||
| ddec1838b3 | |||
| 92c8bc7322 | |||
| abe0acabb6 | |||
| b54e870b26 | |||
| 6d90c7707d | |||
| cd1c203777 | |||
| 57587ed18e | |||
| 1e21b660c4 | |||
| 595aab8bc0 | |||
| 8aca108d2c | |||
| 156ce99d3a | |||
| a7885aabec | |||
| d186879da5 | |||
| f5f25d845a | |||
| 1ce2a13ad1 | |||
| d7185fd002 | |||
| 7fb39ec1e5 | |||
| 10ac7da037 | |||
| d115cd1042 | |||
| de6b78009b | |||
| 2ec28bca43 | |||
| 178294e9d7 | |||
| 13a4ec7597 | |||
| 2f1d270968 | |||
| 3c003deec6 | |||
| afbdcec08b | |||
| 65e0ddc221 | |||
| b2ab419922 | |||
| bc0e184130 | |||
| 1e82f9652e | |||
| 25be2f26fc | |||
| 0c8798fce3 | |||
| f5c3094050 | |||
| 39ace450de | |||
| 0ba8786b35 | |||
| bcd4055e89 | |||
| b462615439 | |||
| 012b4b3d40 | |||
| d9cc24f3df | |||
| 38848f7f25 | |||
| 5473eb95d8 | |||
| 2d0f3f887b | |||
| 39357d3e5c | |||
| 3553cfb6ad | |||
| 806fef85be | |||
| 6d73d85c1f | |||
| e0a8f9541d | |||
| b866a9e099 | |||
| 1db26128a6 | |||
| 02c9f0ff90 | |||
| 63ec1f4e1c | |||
| 293de94f34 | |||
| d8d359eca2 | |||
| 11a95cee6d | |||
| b7c022a61a | |||
| 5e2953670e | |||
| 551f793700 | |||
| 4564261d83 | |||
| 16fe83c7af | |||
| 3723e89585 | |||
| 14a63a26b9 | |||
| 67debd0e11 | |||
| b9710c6af4 | |||
| 493898d9bd | |||
| 1c596e3c5a | |||
| f11c780fdc | |||
| fdf610850b | |||
| 5125fbed41 | |||
| b15ed9b084 | |||
| 05acc5131e | |||
| 7590c9ca1b | |||
| b898873b90 | |||
| 09b53a753c | |||
| 4bfc3b95b5 | |||
| 49dac61b07 | |||
| 19ff47a63b | |||
| 007f4066f6 | |||
| 42c888ee1d | |||
| 731e6c2482 | |||
| 0badb071ef | |||
| 9f9f5ab4de | |||
| 535e40c342 | |||
| 218b3b192b | |||
| df59b09cad | |||
| c718744483 | |||
| e75fca007e | |||
| 25d94ffe2a | |||
| 7ea59f0d49 | |||
| ddc2ca3e10 | |||
| b34c13c5cf | |||
| 18a15d84ef | |||
| 9ba7ea76a9 | |||
| 01ae168f92 | |||
| 8d2ed344c1 | |||
| 3bdc6c035a | |||
| c1cdc25b77 | |||
| 0ecb1ba262 | |||
| eb6934f784 | |||
| 1b4bd3ee1b |
@@ -0,0 +1,31 @@
|
||||
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"'
|
||||
@@ -0,0 +1,5 @@
|
||||
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
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
|
||||
|
||||
## 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._
|
||||
@@ -0,0 +1,30 @@
|
||||
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
|
||||
+124
-17
@@ -13,6 +13,7 @@ on:
|
||||
- 2.*
|
||||
|
||||
env:
|
||||
GOFLAGS: '-tags=nobadger,nomysql,nopgx'
|
||||
# https://github.com/actions/setup-go/issues/491
|
||||
GOTOOLCHAIN: local
|
||||
|
||||
@@ -30,13 +31,13 @@ jobs:
|
||||
- mac
|
||||
- windows
|
||||
go:
|
||||
- '1.24'
|
||||
- '1.25'
|
||||
|
||||
include:
|
||||
# Set the minimum Go patch version for the given Go minor
|
||||
# Usable via ${{ matrix.GO_SEMVER }}
|
||||
- go: '1.24'
|
||||
GO_SEMVER: '~1.24.1'
|
||||
- go: '1.25'
|
||||
GO_SEMVER: '~1.25.0'
|
||||
|
||||
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
||||
# OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories)
|
||||
@@ -62,17 +63,18 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
actions: write # to allow uploading artifacts and cache
|
||||
checks: write
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ matrix.GO_SEMVER }}
|
||||
check-latest: true
|
||||
@@ -110,7 +112,7 @@ jobs:
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
run: |
|
||||
go build -tags nobadger,nomysql,nopgx -trimpath -ldflags="-w -s" -v
|
||||
go build -trimpath -ldflags="-w -s" -v
|
||||
|
||||
- name: Smoke test Caddy
|
||||
working-directory: ./cmd/caddy
|
||||
@@ -133,7 +135,7 @@ jobs:
|
||||
# continue-on-error: true
|
||||
run: |
|
||||
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
|
||||
go test -tags nobadger,nomysql,nopgx -v -coverprofile="cover-profile.out" -short -race ./...
|
||||
go test -v -coverprofile="cover-profile.out" -short -race ./...
|
||||
# echo "status=$?" >> $GITHUB_OUTPUT
|
||||
|
||||
# Relevant step if we reinvestigate publishing test/coverage reports
|
||||
@@ -151,6 +153,111 @@ jobs:
|
||||
# echo "step_test ${{ steps.step_test.outputs.status }}\n"
|
||||
# exit 1
|
||||
|
||||
spec-test:
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- linux
|
||||
go:
|
||||
- '1.25'
|
||||
|
||||
include:
|
||||
# Set the minimum Go patch version for the given Go minor
|
||||
# Usable via ${{ matrix.GO_SEMVER }}
|
||||
- go: '1.25'
|
||||
GO_SEMVER: '~1.25.0'
|
||||
|
||||
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
||||
# OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories)
|
||||
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
|
||||
# SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
|
||||
- os: linux
|
||||
OS_LABEL: ubuntu-latest
|
||||
CADDY_BIN_PATH: ./cmd/caddy/caddy
|
||||
SUCCESS: 0
|
||||
|
||||
runs-on: ${{ matrix.OS_LABEL }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: ${{ matrix.GO_SEMVER }}
|
||||
check-latest: true
|
||||
|
||||
- name: Print Go version and environment
|
||||
id: vars
|
||||
shell: bash
|
||||
run: |
|
||||
printf "curl version: $(curl --version)\n"
|
||||
printf "Using go at: $(which go)\n"
|
||||
printf "Go version: $(go version)\n"
|
||||
printf "\n\nGo environment:\n\n"
|
||||
go env
|
||||
printf "\n\nSystem environment:\n\n"
|
||||
env
|
||||
printf "Git version: $(git version)\n\n"
|
||||
# Calculate the short SHA1 hash of the git commit
|
||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get -v -t ./...
|
||||
# mkdir test-results
|
||||
- name: Build Caddy
|
||||
working-directory: ./cmd/caddy
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
run: |
|
||||
go build -cover -tags nobadger,nopgx,nomysql -trimpath -ldflags="-w -s" -v
|
||||
|
||||
- name: Install Hurl
|
||||
env:
|
||||
HURL_VERSION: "7.0.0"
|
||||
run: |
|
||||
curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/${HURL_VERSION}/hurl_${HURL_VERSION}_amd64.deb
|
||||
sudo dpkg -i hurl_${HURL_VERSION}_amd64.deb
|
||||
hurl --version
|
||||
|
||||
- name: Run Caddy
|
||||
run: |
|
||||
./cmd/caddy/caddy environ
|
||||
mkdir coverdir
|
||||
export GOCOVERDIR=./coverdir
|
||||
./cmd/caddy/caddy start
|
||||
sleep 5
|
||||
|
||||
- name: Run tests with Hurl
|
||||
run: |
|
||||
mkdir hurl-report
|
||||
find . -name *.hurl -exec hurl --jobs 1 --variables-file caddytest/spec/hurl_vars.properties --very-verbose --verbose --test --report-junit hurl-report/junit.xml --color {} \;
|
||||
|
||||
- name: Publish Test Results
|
||||
uses: EnricoMi/publish-unit-test-result-action@3a74b2957438d0b6e2e61d67b05318aa25c9e6c6 # v2.20.0
|
||||
with:
|
||||
files: |
|
||||
hurl-report/junit.xml
|
||||
|
||||
- name: Generate Coverage Data
|
||||
run: |
|
||||
export GOCOVERDIR=./coverdir
|
||||
./cmd/caddy/caddy stop
|
||||
go tool covdata textfmt -i=coverdir -o hurl-report/caddy_cover_${{ steps.vars.outputs.short_sha }}.txt
|
||||
go tool cover -html hurl-report/caddy_cover_${{ steps.vars.outputs.short_sha }}.txt -o hurl-report/caddy_cover_${{ steps.vars.outputs.short_sha }}.html
|
||||
|
||||
|
||||
- name: Publish Coverage Profile
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
path: hurl-report/caddy_cover_${{ steps.vars.outputs.short_sha }}.html
|
||||
compression-level: 0
|
||||
|
||||
s390x-test:
|
||||
name: test (s390x on IBM Z)
|
||||
permissions:
|
||||
@@ -161,13 +268,13 @@ jobs:
|
||||
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
allowed-endpoints: ci-s390x.caddyserver.com:22
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Run Tests
|
||||
run: |
|
||||
set +e
|
||||
@@ -190,7 +297,7 @@ jobs:
|
||||
retries=3
|
||||
exit_code=0
|
||||
while ((retries > 0)); do
|
||||
CGO_ENABLED=0 go test -p 1 -tags nobadger,nomysql,nopgx -v ./...
|
||||
CGO_ENABLED=0 go test -p 1 -v ./...
|
||||
exit_code=$?
|
||||
if ((exit_code == 0)); then
|
||||
break
|
||||
@@ -220,27 +327,27 @@ jobs:
|
||||
if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
|
||||
- uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
||||
with:
|
||||
version: latest
|
||||
args: check
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: "~1.24"
|
||||
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@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
|
||||
- uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
||||
with:
|
||||
version: latest
|
||||
args: build --single-target --snapshot
|
||||
|
||||
@@ -11,6 +11,8 @@ on:
|
||||
- 2.*
|
||||
|
||||
env:
|
||||
GOFLAGS: '-tags=nobadger,nomysql,nopgx'
|
||||
CGO_ENABLED: '0'
|
||||
# https://github.com/actions/setup-go/issues/491
|
||||
GOTOOLCHAIN: local
|
||||
|
||||
@@ -34,13 +36,13 @@ jobs:
|
||||
- 'darwin'
|
||||
- 'netbsd'
|
||||
go:
|
||||
- '1.24'
|
||||
- '1.25'
|
||||
|
||||
include:
|
||||
# Set the minimum Go patch version for the given Go minor
|
||||
# Usable via ${{ matrix.GO_SEMVER }}
|
||||
- go: '1.24'
|
||||
GO_SEMVER: '~1.24.1'
|
||||
- go: '1.25'
|
||||
GO_SEMVER: '~1.25.0'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -49,15 +51,15 @@ jobs:
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ matrix.GO_SEMVER }}
|
||||
check-latest: true
|
||||
@@ -74,11 +76,9 @@ jobs:
|
||||
|
||||
- name: Run Build
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goos == 'aix' && 'ppc64' || 'amd64' }}
|
||||
shell: bash
|
||||
continue-on-error: true
|
||||
working-directory: ./cmd/caddy
|
||||
run: |
|
||||
GOOS=$GOOS GOARCH=$GOARCH go build -tags=nobadger,nomysql,nopgx -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null
|
||||
run: go build -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null
|
||||
|
||||
@@ -45,14 +45,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: '~1.24'
|
||||
go-version: '~1.25'
|
||||
check-latest: true
|
||||
|
||||
- name: golangci-lint
|
||||
@@ -73,14 +73,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: govulncheck
|
||||
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
|
||||
with:
|
||||
go-version-input: '~1.24.1'
|
||||
go-version-input: '~1.25.0'
|
||||
check-latest: true
|
||||
|
||||
dependency-review:
|
||||
@@ -90,14 +90,14 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
|
||||
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
|
||||
|
||||
@@ -20,13 +20,13 @@ jobs:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
go:
|
||||
- '1.24'
|
||||
- '1.25'
|
||||
|
||||
include:
|
||||
# Set the minimum Go patch version for the given Go minor
|
||||
# Usable via ${{ matrix.GO_SEMVER }}
|
||||
- go: '1.24'
|
||||
GO_SEMVER: '~1.24.1'
|
||||
- go: '1.25'
|
||||
GO_SEMVER: '~1.25.0'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
|
||||
@@ -39,23 +39,23 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ matrix.GO_SEMVER }}
|
||||
check-latest: true
|
||||
|
||||
# Force fetch upstream tags -- because 65 minutes
|
||||
# tl;dr: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 runs this line:
|
||||
# tl;dr: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2 runs this line:
|
||||
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
|
||||
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
|
||||
# git fetch --prune --unshallow
|
||||
@@ -109,11 +109,11 @@ jobs:
|
||||
git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # main
|
||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # main
|
||||
- name: Cosign version
|
||||
run: cosign version
|
||||
- name: Install Syft
|
||||
uses: anchore/sbom-action/download-syft@7b36ad622f042cab6f59a75c2ac24ccb256e9b45 # main
|
||||
uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # main
|
||||
- name: Syft version
|
||||
run: syft version
|
||||
- name: Install xcaddy
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
xcaddy version
|
||||
# GoReleaser will take care of publishing those artifacts into the release
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
|
||||
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean --timeout 60m
|
||||
|
||||
@@ -24,12 +24,12 @@ jobs:
|
||||
|
||||
# See https://github.com/peter-evans/repository-dispatch
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Trigger event on caddyserver/dist
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
|
||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
||||
with:
|
||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||
repository: caddyserver/dist
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
|
||||
|
||||
- name: Trigger event on caddyserver/caddy-docker
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
|
||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
||||
with:
|
||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||
repository: caddyserver/caddy-docker
|
||||
|
||||
@@ -37,17 +37,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
|
||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -81,6 +81,6 @@ jobs:
|
||||
# Upload the results to GitHub's code scanning dashboard (optional).
|
||||
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||
uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -2,6 +2,10 @@ version: "2"
|
||||
run:
|
||||
issues-exit-code: 1
|
||||
tests: false
|
||||
build-tags:
|
||||
- nobadger
|
||||
- nomysql
|
||||
- nopgx
|
||||
output:
|
||||
formats:
|
||||
text:
|
||||
|
||||
@@ -89,7 +89,7 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i
|
||||
|
||||
Requirements:
|
||||
|
||||
- [Go 1.24.0 or newer](https://golang.org/dl/)
|
||||
- [Go 1.25.0 or newer](https://golang.org/dl/)
|
||||
|
||||
### For development
|
||||
|
||||
|
||||
@@ -1029,6 +1029,13 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// If this request changed the config, clear the last
|
||||
// config info we have stored, if it is different from
|
||||
// the original source.
|
||||
ClearLastConfigIfDifferent(
|
||||
r.Header.Get("Caddy-Config-Source-File"),
|
||||
r.Header.Get("Caddy-Config-Source-Adapter"))
|
||||
|
||||
default:
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusMethodNotAllowed,
|
||||
|
||||
+3
-5
@@ -149,11 +149,9 @@ func TestLoadConcurrent(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
wg.Go(func() {
|
||||
_ = Load(testCfg, true)
|
||||
wg.Done()
|
||||
}()
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -207,7 +205,7 @@ func TestETags(t *testing.T) {
|
||||
}
|
||||
|
||||
func BenchmarkLoad(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
Load(testCfg, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -975,11 +975,11 @@ func Version() (simple, full string) {
|
||||
if CustomVersion != "" {
|
||||
full = CustomVersion
|
||||
simple = CustomVersion
|
||||
return
|
||||
return simple, full
|
||||
}
|
||||
full = "unknown"
|
||||
simple = "unknown"
|
||||
return
|
||||
return simple, full
|
||||
}
|
||||
// find the Caddy module in the dependency list
|
||||
for _, dep := range bi.Deps {
|
||||
@@ -1059,7 +1059,7 @@ func Version() (simple, full string) {
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return simple, full
|
||||
}
|
||||
|
||||
// Event represents something that has happened or is happening.
|
||||
@@ -1197,6 +1197,91 @@ var (
|
||||
rawCfgMu sync.RWMutex
|
||||
)
|
||||
|
||||
// lastConfigFile and lastConfigAdapter remember the source config
|
||||
// file and adapter used when Caddy was started via the CLI "run" command.
|
||||
// These are consulted by the SIGUSR1 handler to attempt reloading from
|
||||
// the same source. They are intentionally not set for other entrypoints
|
||||
// such as "caddy start" or subcommands like file-server.
|
||||
var (
|
||||
lastConfigMu sync.RWMutex
|
||||
lastConfigFile string
|
||||
lastConfigAdapter string
|
||||
)
|
||||
|
||||
// reloadFromSourceFunc is the type of stored callback
|
||||
// which is called when we receive a SIGUSR1 signal.
|
||||
type reloadFromSourceFunc func(file, adapter string) error
|
||||
|
||||
// reloadFromSourceCallback is the stored callback
|
||||
// which is called when we receive a SIGUSR1 signal.
|
||||
var reloadFromSourceCallback reloadFromSourceFunc
|
||||
|
||||
// errReloadFromSourceUnavailable is returned when no reload-from-source callback is set.
|
||||
var errReloadFromSourceUnavailable = errors.New("reload from source unavailable in this process") //nolint:unused
|
||||
|
||||
// SetLastConfig records the given source file and adapter as the
|
||||
// last-known external configuration source. Intended to be called
|
||||
// only when starting via "caddy run --config <file> --adapter <adapter>".
|
||||
func SetLastConfig(file, adapter string, fn reloadFromSourceFunc) {
|
||||
lastConfigMu.Lock()
|
||||
lastConfigFile = file
|
||||
lastConfigAdapter = adapter
|
||||
reloadFromSourceCallback = fn
|
||||
lastConfigMu.Unlock()
|
||||
}
|
||||
|
||||
// ClearLastConfigIfDifferent clears the recorded last-config if the provided
|
||||
// source file/adapter do not match the recorded last-config. If both srcFile
|
||||
// and srcAdapter are empty, the last-config is cleared.
|
||||
func ClearLastConfigIfDifferent(srcFile, srcAdapter string) {
|
||||
if (srcFile != "" || srcAdapter != "") && lastConfigMatches(srcFile, srcAdapter) {
|
||||
return
|
||||
}
|
||||
SetLastConfig("", "", nil)
|
||||
}
|
||||
|
||||
// getLastConfig returns the last-known config file and adapter.
|
||||
func getLastConfig() (file, adapter string, fn reloadFromSourceFunc) {
|
||||
lastConfigMu.RLock()
|
||||
f, a, cb := lastConfigFile, lastConfigAdapter, reloadFromSourceCallback
|
||||
lastConfigMu.RUnlock()
|
||||
return f, a, cb
|
||||
}
|
||||
|
||||
// lastConfigMatches returns true if the provided source file and/or adapter
|
||||
// matches the recorded last-config. Matching rules (in priority order):
|
||||
// 1. If srcAdapter is provided and differs from the recorded adapter, no match.
|
||||
// 2. If srcFile exactly equals the recorded file, match.
|
||||
// 3. If both sides can be made absolute and equal, match.
|
||||
// 4. If basenames are equal, match.
|
||||
func lastConfigMatches(srcFile, srcAdapter string) bool {
|
||||
lf, la, _ := getLastConfig()
|
||||
|
||||
// If adapter is provided, it must match.
|
||||
if srcAdapter != "" && srcAdapter != la {
|
||||
return false
|
||||
}
|
||||
|
||||
// Quick equality check.
|
||||
if srcFile == lf {
|
||||
return true
|
||||
}
|
||||
|
||||
// Try absolute path comparison.
|
||||
sAbs, sErr := filepath.Abs(srcFile)
|
||||
lAbs, lErr := filepath.Abs(lf)
|
||||
if sErr == nil && lErr == nil && sAbs == lAbs {
|
||||
return true
|
||||
}
|
||||
|
||||
// Final fallback: basename equality.
|
||||
if filepath.Base(srcFile) == filepath.Base(lf) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// errSameConfig is returned if the new config is the same
|
||||
// as the old one. This isn't usually an actual, actionable
|
||||
// error; it's mostly a sentinel value.
|
||||
|
||||
@@ -308,9 +308,9 @@ func (d *Dispenser) CountRemainingArgs() int {
|
||||
}
|
||||
|
||||
// RemainingArgs loads any more arguments (tokens on the same line)
|
||||
// into a slice and returns them. Open curly brace tokens also indicate
|
||||
// the end of arguments, and the curly brace is not included in
|
||||
// the return value nor is it loaded.
|
||||
// into a slice of strings and returns them. Open curly brace tokens
|
||||
// also indicate the end of arguments, and the curly brace is not
|
||||
// included in the return value nor is it loaded.
|
||||
func (d *Dispenser) RemainingArgs() []string {
|
||||
var args []string
|
||||
for d.NextArg() {
|
||||
@@ -320,9 +320,9 @@ func (d *Dispenser) RemainingArgs() []string {
|
||||
}
|
||||
|
||||
// RemainingArgsRaw loads any more arguments (tokens on the same line,
|
||||
// retaining quotes) into a slice and returns them. Open curly brace
|
||||
// tokens also indicate the end of arguments, and the curly brace is
|
||||
// not included in the return value nor is it loaded.
|
||||
// retaining quotes) into a slice of strings and returns them.
|
||||
// Open curly brace tokens also indicate the end of arguments,
|
||||
// and the curly brace is not included in the return value nor is it loaded.
|
||||
func (d *Dispenser) RemainingArgsRaw() []string {
|
||||
var args []string
|
||||
for d.NextArg() {
|
||||
@@ -331,6 +331,18 @@ func (d *Dispenser) RemainingArgsRaw() []string {
|
||||
return args
|
||||
}
|
||||
|
||||
// RemainingArgsAsTokens loads any more arguments (tokens on the same line)
|
||||
// into a slice of Token-structs and returns them. Open curly brace tokens
|
||||
// also indicate the end of arguments, and the curly brace is not included
|
||||
// in the return value nor is it loaded.
|
||||
func (d *Dispenser) RemainingArgsAsTokens() []Token {
|
||||
var args []Token
|
||||
for d.NextArg() {
|
||||
args = append(args, d.Token())
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// NewFromNextSegment returns a new dispenser with a copy of
|
||||
// the tokens from the current token until the end of the
|
||||
// "directive" whether that be to the end of the line or
|
||||
|
||||
@@ -274,6 +274,66 @@ func TestDispenser_RemainingArgs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispenser_RemainingArgsAsTokens(t *testing.T) {
|
||||
input := `dir1 arg1 arg2 arg3
|
||||
dir2 arg4 arg5
|
||||
dir3 arg6 { arg7
|
||||
dir4`
|
||||
d := NewTestDispenser(input)
|
||||
|
||||
d.Next() // dir1
|
||||
|
||||
args := d.RemainingArgsAsTokens()
|
||||
|
||||
tokenTexts := make([]string, 0, len(args))
|
||||
for _, arg := range args {
|
||||
tokenTexts = append(tokenTexts, arg.Text)
|
||||
}
|
||||
|
||||
if expected := []string{"arg1", "arg2", "arg3"}; !reflect.DeepEqual(tokenTexts, expected) {
|
||||
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
|
||||
}
|
||||
|
||||
d.Next() // dir2
|
||||
|
||||
args = d.RemainingArgsAsTokens()
|
||||
|
||||
tokenTexts = tokenTexts[:0]
|
||||
for _, arg := range args {
|
||||
tokenTexts = append(tokenTexts, arg.Text)
|
||||
}
|
||||
|
||||
if expected := []string{"arg4", "arg5"}; !reflect.DeepEqual(tokenTexts, expected) {
|
||||
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
|
||||
}
|
||||
|
||||
d.Next() // dir3
|
||||
|
||||
args = d.RemainingArgsAsTokens()
|
||||
tokenTexts = tokenTexts[:0]
|
||||
for _, arg := range args {
|
||||
tokenTexts = append(tokenTexts, arg.Text)
|
||||
}
|
||||
|
||||
if expected := []string{"arg6"}; !reflect.DeepEqual(tokenTexts, expected) {
|
||||
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
|
||||
}
|
||||
|
||||
d.Next() // {
|
||||
d.Next() // arg7
|
||||
d.Next() // dir4
|
||||
|
||||
args = d.RemainingArgsAsTokens()
|
||||
tokenTexts = tokenTexts[:0]
|
||||
for _, arg := range args {
|
||||
tokenTexts = append(tokenTexts, arg.Text)
|
||||
}
|
||||
|
||||
if len(args) != 0 {
|
||||
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", []string{}, tokenTexts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispenser_ArgErr_Err(t *testing.T) {
|
||||
input := `dir1 {
|
||||
}
|
||||
|
||||
@@ -52,17 +52,16 @@ func Format(input []byte) []byte {
|
||||
|
||||
newLines int // count of newlines consumed
|
||||
|
||||
comment bool // whether we're in a comment
|
||||
quoted bool // whether we're in a quoted segment
|
||||
escaped bool // whether current char is escaped
|
||||
comment bool // whether we're in a comment
|
||||
quotes string // encountered quotes ('', '`', '"', '"`', '`"')
|
||||
escaped bool // whether current char is escaped
|
||||
|
||||
heredoc heredocState // whether we're in a heredoc
|
||||
heredocEscaped bool // whether heredoc is escaped
|
||||
heredocMarker []rune
|
||||
heredocClosingMarker []rune
|
||||
|
||||
nesting int // indentation level
|
||||
withinBackquote bool
|
||||
nesting int // indentation level
|
||||
)
|
||||
|
||||
write := func(ch rune) {
|
||||
@@ -89,12 +88,8 @@ func Format(input []byte) []byte {
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
if ch == '`' {
|
||||
withinBackquote = !withinBackquote
|
||||
}
|
||||
|
||||
// detect whether we have the start of a heredoc
|
||||
if !quoted && (heredoc == heredocClosed && !heredocEscaped) &&
|
||||
if quotes == "" && (heredoc == heredocClosed && !heredocEscaped) &&
|
||||
space && last == '<' && ch == '<' {
|
||||
write(ch)
|
||||
heredoc = heredocOpening
|
||||
@@ -180,16 +175,38 @@ func Format(input []byte) []byte {
|
||||
continue
|
||||
}
|
||||
|
||||
if quoted {
|
||||
if ch == '`' {
|
||||
switch quotes {
|
||||
case "\"`":
|
||||
quotes = "\""
|
||||
case "`":
|
||||
quotes = ""
|
||||
case "\"":
|
||||
quotes = "\"`"
|
||||
default:
|
||||
quotes = "`"
|
||||
}
|
||||
}
|
||||
|
||||
if quotes == "\"" {
|
||||
if ch == '"' {
|
||||
quoted = false
|
||||
quotes = ""
|
||||
}
|
||||
write(ch)
|
||||
continue
|
||||
}
|
||||
|
||||
if space && ch == '"' {
|
||||
quoted = true
|
||||
if ch == '"' {
|
||||
switch quotes {
|
||||
case "":
|
||||
if space {
|
||||
quotes = "\""
|
||||
}
|
||||
case "`\"":
|
||||
quotes = "`"
|
||||
case "\"`":
|
||||
quotes = ""
|
||||
}
|
||||
}
|
||||
|
||||
if unicode.IsSpace(ch) {
|
||||
@@ -224,7 +241,7 @@ func Format(input []byte) []byte {
|
||||
openBrace = false
|
||||
if beginningOfLine {
|
||||
indent()
|
||||
} else if !openBraceSpace {
|
||||
} else if !openBraceSpace || !unicode.IsSpace(last) {
|
||||
write(' ')
|
||||
}
|
||||
write('{')
|
||||
@@ -241,11 +258,11 @@ func Format(input []byte) []byte {
|
||||
case ch == '{':
|
||||
openBrace = true
|
||||
openBraceSpace = spacePrior && !beginningOfLine
|
||||
if openBraceSpace {
|
||||
if openBraceSpace && newLines == 0 {
|
||||
write(' ')
|
||||
}
|
||||
openBraceWritten = false
|
||||
if withinBackquote {
|
||||
if quotes == "`" {
|
||||
write('{')
|
||||
openBraceWritten = true
|
||||
continue
|
||||
@@ -253,7 +270,7 @@ func Format(input []byte) []byte {
|
||||
continue
|
||||
|
||||
case ch == '}' && (spacePrior || !openBrace):
|
||||
if withinBackquote {
|
||||
if quotes == "`" {
|
||||
write('}')
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -444,6 +444,26 @@ block2 {
|
||||
input: "block {respond \"All braces should remain: {{now | date `2006`}}\"}",
|
||||
expect: "block {respond \"All braces should remain: {{now | date `2006`}}\"}",
|
||||
},
|
||||
{
|
||||
description: "Preserve quoted backticks and backticked quotes",
|
||||
input: "block { respond \"`\" } block { respond `\"`}",
|
||||
expect: "block {\n\trespond \"`\"\n}\n\nblock {\n\trespond `\"`\n}",
|
||||
},
|
||||
{
|
||||
description: "No trailing space on line before env variable",
|
||||
input: `{
|
||||
a
|
||||
|
||||
{$ENV_VAR}
|
||||
}
|
||||
`,
|
||||
expect: `{
|
||||
a
|
||||
|
||||
{$ENV_VAR}
|
||||
}
|
||||
`,
|
||||
},
|
||||
} {
|
||||
// the formatter should output a trailing newline,
|
||||
// even if the tests aren't written to expect that
|
||||
|
||||
@@ -379,28 +379,23 @@ func (p *parser) doImport(nesting int) error {
|
||||
if len(blockTokens) > 0 {
|
||||
// use such tokens to create a new dispenser, and then use it to parse each block
|
||||
bd := NewDispenser(blockTokens)
|
||||
|
||||
// one iteration processes one sub-block inside the import
|
||||
for bd.Next() {
|
||||
// see if we can grab a key
|
||||
var currentMappingKey string
|
||||
if bd.Val() == "{" {
|
||||
currentMappingKey := bd.Val()
|
||||
|
||||
if currentMappingKey == "{" {
|
||||
return p.Err("anonymous blocks are not supported")
|
||||
}
|
||||
currentMappingKey = bd.Val()
|
||||
currentMappingTokens := []Token{}
|
||||
// read all args until end of line / {
|
||||
if bd.NextArg() {
|
||||
|
||||
// load up all arguments (if there even are any)
|
||||
currentMappingTokens := bd.RemainingArgsAsTokens()
|
||||
|
||||
// load up the entire block
|
||||
for mappingNesting := bd.Nesting(); bd.NextBlock(mappingNesting); {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -538,29 +533,24 @@ func (p *parser) doImport(nesting int) error {
|
||||
}
|
||||
// if it is {block}, we substitute with all tokens in the block
|
||||
// if it is {blocks.*}, we substitute with the tokens in the mapping for the *
|
||||
var skip bool
|
||||
var tokensToAdd []Token
|
||||
foundBlockDirective := false
|
||||
switch {
|
||||
case token.Text == "{block}":
|
||||
foundBlockDirective = true
|
||||
tokensToAdd = blockTokens
|
||||
case strings.HasPrefix(token.Text, "{blocks.") && strings.HasSuffix(token.Text, "}"):
|
||||
foundBlockDirective = true
|
||||
// {blocks.foo.bar} will be extracted to key `foo.bar`
|
||||
blockKey := strings.TrimPrefix(strings.TrimSuffix(token.Text, "}"), "{blocks.")
|
||||
val, ok := blockMapping[blockKey]
|
||||
if ok {
|
||||
tokensToAdd = val
|
||||
}
|
||||
default:
|
||||
skip = true
|
||||
}
|
||||
if !skip {
|
||||
if len(tokensToAdd) == 0 {
|
||||
// if there is no content in the snippet block, don't do any replacement
|
||||
// this allows snippets which contained {block}/{block.*} before this change to continue functioning as normal
|
||||
tokensCopy = append(tokensCopy, token)
|
||||
} else {
|
||||
tokensCopy = append(tokensCopy, tokensToAdd...)
|
||||
}
|
||||
|
||||
if foundBlockDirective {
|
||||
tokensCopy = append(tokensCopy, tokensToAdd...)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -884,6 +885,51 @@ func TestRejectsGlobalMatcher(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectAnonymousImportBlock(t *testing.T) {
|
||||
p := testParser(`
|
||||
(site) {
|
||||
http://{args[0]} https://{args[0]} {
|
||||
{block}
|
||||
}
|
||||
}
|
||||
|
||||
import site test.domain {
|
||||
{
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
}
|
||||
`)
|
||||
_, err := p.parseAll()
|
||||
if err == nil {
|
||||
t.Fatal("Expected an error, but got nil")
|
||||
}
|
||||
expected := "anonymous blocks are not supported"
|
||||
if !strings.HasPrefix(err.Error(), "anonymous blocks are not supported") {
|
||||
t.Errorf("Expected error to start with '%s' but got '%v'", expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptSiteImportWithBraces(t *testing.T) {
|
||||
p := testParser(`
|
||||
(site) {
|
||||
http://{args[0]} https://{args[0]} {
|
||||
{block}
|
||||
}
|
||||
}
|
||||
|
||||
import site test.domain {
|
||||
reverse_proxy http://192.168.1.1:8080 {
|
||||
header_up Host {host}
|
||||
}
|
||||
}
|
||||
`)
|
||||
_, err := p.parseAll()
|
||||
if err != nil {
|
||||
t.Errorf("Expected error to be nil but got '%v'", err)
|
||||
}
|
||||
}
|
||||
|
||||
func testParser(input string) parser {
|
||||
return parser{Dispenser: NewTestDispenser(input)}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func parseBind(h Helper) ([]ConfigValue, error) {
|
||||
// curves <curves...>
|
||||
// client_auth {
|
||||
// mode [request|require|verify_if_given|require_and_verify]
|
||||
// trust_pool <module_name> [...]
|
||||
// trust_pool <module_name> [...]
|
||||
// trusted_leaf_cert <base64_der>
|
||||
// trusted_leaf_cert_file <filename>
|
||||
// }
|
||||
@@ -481,7 +481,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
// Validate DNS challenge config: any DNS challenge option except "dns" requires a DNS provider
|
||||
if acmeIssuer != nil && acmeIssuer.Challenges != nil && acmeIssuer.Challenges.DNS != nil {
|
||||
dnsCfg := acmeIssuer.Challenges.DNS
|
||||
providerSet := dnsCfg.ProviderRaw != nil || h.Option("dns") != nil
|
||||
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)",
|
||||
|
||||
@@ -458,8 +458,6 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||
case "disable_certs":
|
||||
case "ignore_loaded_certs":
|
||||
case "prefer_wildcard":
|
||||
break
|
||||
|
||||
default:
|
||||
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', 'ignore_loaded_certs', or 'prefer_wildcard'")
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
package httpcaddyfile
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
@@ -178,6 +180,15 @@ func (st ServerType) buildPKIApp(
|
||||
if _, ok := options["skip_install_trust"]; ok {
|
||||
skipInstallTrust = true
|
||||
}
|
||||
|
||||
// check if auto_https is off - in that case we should not create
|
||||
// any PKI infrastructure even with skip_install_trust directive
|
||||
autoHTTPS := []string{}
|
||||
if ah, ok := options["auto_https"].([]string); ok {
|
||||
autoHTTPS = ah
|
||||
}
|
||||
autoHTTPSOff := slices.Contains(autoHTTPS, "off")
|
||||
|
||||
falseBool := false
|
||||
|
||||
// Load the PKI app configured via global options
|
||||
@@ -218,7 +229,8 @@ func (st ServerType) buildPKIApp(
|
||||
// if there was no CAs defined in any of the servers,
|
||||
// and we were requested to not install trust, then
|
||||
// add one for the default/local CA to do so
|
||||
if len(pkiApp.CAs) == 0 && skipInstallTrust {
|
||||
// only if auto_https is not completely disabled
|
||||
if len(pkiApp.CAs) == 0 && skipInstallTrust && !autoHTTPSOff {
|
||||
ca := new(caddypki.CA)
|
||||
ca.ID = caddypki.DefaultCAID
|
||||
ca.InstallTrust = &falseBool
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
@@ -42,12 +43,15 @@ type serverOptions struct {
|
||||
WriteTimeout caddy.Duration
|
||||
IdleTimeout caddy.Duration
|
||||
KeepAliveInterval caddy.Duration
|
||||
KeepAliveIdle caddy.Duration
|
||||
KeepAliveCount int
|
||||
MaxHeaderBytes int
|
||||
EnableFullDuplex bool
|
||||
Protocols []string
|
||||
StrictSNIHost *bool
|
||||
TrustedProxiesRaw json.RawMessage
|
||||
TrustedProxiesStrict int
|
||||
TrustedProxiesUnix bool
|
||||
ClientIPHeaders []string
|
||||
ShouldLogCredentials bool
|
||||
Metrics *caddyhttp.Metrics
|
||||
@@ -142,6 +146,7 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
|
||||
}
|
||||
}
|
||||
|
||||
case "keepalive_interval":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
@@ -152,6 +157,26 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||
}
|
||||
serverOpts.KeepAliveInterval = caddy.Duration(dur)
|
||||
|
||||
case "keepalive_idle":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
dur, err := caddy.ParseDuration(d.Val())
|
||||
if err != nil {
|
||||
return nil, d.Errf("parsing keepalive idle duration: %v", err)
|
||||
}
|
||||
serverOpts.KeepAliveIdle = caddy.Duration(dur)
|
||||
|
||||
case "keepalive_count":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
cnt, err := strconv.ParseInt(d.Val(), 10, 32)
|
||||
if err != nil {
|
||||
return nil, d.Errf("parsing keepalive count int: %v", err)
|
||||
}
|
||||
serverOpts.KeepAliveCount = int(cnt)
|
||||
|
||||
case "max_header_size":
|
||||
var sizeStr string
|
||||
if !d.AllArgs(&sizeStr) {
|
||||
@@ -227,6 +252,12 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||
}
|
||||
serverOpts.TrustedProxiesStrict = 1
|
||||
|
||||
case "trusted_proxies_unix":
|
||||
if d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
serverOpts.TrustedProxiesUnix = true
|
||||
|
||||
case "client_ip_headers":
|
||||
headers := d.RemainingArgs()
|
||||
for _, header := range headers {
|
||||
@@ -309,6 +340,8 @@ func applyServerOptions(
|
||||
server.WriteTimeout = opts.WriteTimeout
|
||||
server.IdleTimeout = opts.IdleTimeout
|
||||
server.KeepAliveInterval = opts.KeepAliveInterval
|
||||
server.KeepAliveIdle = opts.KeepAliveIdle
|
||||
server.KeepAliveCount = opts.KeepAliveCount
|
||||
server.MaxHeaderBytes = opts.MaxHeaderBytes
|
||||
server.EnableFullDuplex = opts.EnableFullDuplex
|
||||
server.Protocols = opts.Protocols
|
||||
@@ -316,6 +349,7 @@ func applyServerOptions(
|
||||
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
|
||||
server.ClientIPHeaders = opts.ClientIPHeaders
|
||||
server.TrustedProxiesStrict = opts.TrustedProxiesStrict
|
||||
server.TrustedProxiesUnix = opts.TrustedProxiesUnix
|
||||
server.Metrics = opts.Metrics
|
||||
if opts.ShouldLogCredentials {
|
||||
if server.Logs == nil {
|
||||
|
||||
@@ -64,10 +64,13 @@ func placeholderShorthands() []string {
|
||||
"{orig_?query}", "{http.request.orig_uri.prefixed_query}",
|
||||
"{method}", "{http.request.method}",
|
||||
"{uri}", "{http.request.uri}",
|
||||
"{%uri}", "{http.request.uri_escaped}",
|
||||
"{path}", "{http.request.uri.path}",
|
||||
"{%path}", "{http.request.uri.path_escaped}",
|
||||
"{dir}", "{http.request.uri.path.dir}",
|
||||
"{file}", "{http.request.uri.path.file}",
|
||||
"{query}", "{http.request.uri.query}",
|
||||
"{%query}", "{http.request.uri.query_escaped}",
|
||||
"{?query}", "{http.request.uri.prefixed_query}",
|
||||
"{remote}", "{http.request.remote}",
|
||||
"{remote_host}", "{http.request.remote.host}",
|
||||
|
||||
@@ -554,6 +554,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
||||
globalPreferredChains := options["preferred_chains"]
|
||||
globalCertLifetime := options["cert_lifetime"]
|
||||
globalHTTPPort, globalHTTPSPort := options["http_port"], options["https_port"]
|
||||
globalDefaultBind := options["default_bind"]
|
||||
|
||||
if globalEmail != nil && acmeIssuer.Email == "" {
|
||||
acmeIssuer.Email = globalEmail.(string)
|
||||
@@ -564,17 +565,21 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
||||
if globalACMECARoot != nil && !slices.Contains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
|
||||
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
|
||||
}
|
||||
if globalACMEDNSok && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) {
|
||||
if globalACMEDNS == nil {
|
||||
globalACMEDNS = options["dns"]
|
||||
if globalACMEDNS == nil {
|
||||
return fmt.Errorf("acme_dns specified without DNS provider config, but no provider specified with 'dns' global option")
|
||||
}
|
||||
if globalACMEDNSok && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil || acmeIssuer.Challenges.DNS.ProviderRaw == nil) {
|
||||
globalDNS := options["dns"]
|
||||
if globalDNS == nil && globalACMEDNS == nil {
|
||||
return fmt.Errorf("acme_dns specified without DNS provider config, but no provider specified with 'dns' global option")
|
||||
}
|
||||
acmeIssuer.Challenges = &caddytls.ChallengesConfig{
|
||||
DNS: &caddytls.DNSChallengeConfig{
|
||||
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 {
|
||||
@@ -602,6 +607,20 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
||||
}
|
||||
acmeIssuer.Challenges.TLSALPN.AlternatePort = globalHTTPSPort.(int)
|
||||
}
|
||||
// If BindHost is still unset, fall back to the first default_bind address if set
|
||||
// This avoids binding the automation policy to the wildcard socket, which is unexpected behavior when a more selective socket is specified via default_bind
|
||||
// In BSD it is valid to bind to the wildcard socket even though a more selective socket is already open (still unexpected behavior by the caller though)
|
||||
// In Linux the same call will error with EADDRINUSE whenever the listener for the automation policy is opened
|
||||
if acmeIssuer.Challenges == nil || (acmeIssuer.Challenges.DNS == nil && acmeIssuer.Challenges.BindHost == "") {
|
||||
if defBinds, ok := globalDefaultBind.([]ConfigValue); ok && len(defBinds) > 0 {
|
||||
if abp, ok := defBinds[0].Value.(addressesWithProtocols); ok && len(abp.addresses) > 0 {
|
||||
if acmeIssuer.Challenges == nil {
|
||||
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
||||
}
|
||||
acmeIssuer.Challenges.BindHost = abp.addresses[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
|
||||
acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
|
||||
}
|
||||
@@ -622,12 +641,18 @@ func newBaseAutomationPolicy(
|
||||
_, hasLocalCerts := options["local_certs"]
|
||||
keyType, hasKeyType := options["key_type"]
|
||||
ocspStapling, hasOCSPStapling := options["ocsp_stapling"]
|
||||
|
||||
hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling
|
||||
|
||||
globalACMECA := options["acme_ca"]
|
||||
globalACMECARoot := options["acme_ca_root"]
|
||||
_, globalACMEDNS := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set
|
||||
globalACMEEAB := options["acme_eab"]
|
||||
globalPreferredChains := options["preferred_chains"]
|
||||
hasGlobalACMEDefaults := globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS || globalACMEEAB != nil || globalPreferredChains != nil
|
||||
|
||||
// if there are no global options related to automation policies
|
||||
// set, then we can just return right away
|
||||
if !hasGlobalAutomationOpts {
|
||||
if !hasGlobalAutomationOpts && !hasGlobalACMEDefaults {
|
||||
if always {
|
||||
return new(caddytls.AutomationPolicy), nil
|
||||
}
|
||||
@@ -649,6 +674,14 @@ func newBaseAutomationPolicy(
|
||||
ap.Issuers = []certmagic.Issuer{new(caddytls.InternalIssuer)}
|
||||
}
|
||||
|
||||
if hasGlobalACMEDefaults {
|
||||
for i := range ap.Issuers {
|
||||
if err := fillInGlobalACMEDefaults(ap.Issuers[i], options); err != nil {
|
||||
return nil, fmt.Errorf("filling in global issuer defaults for issuer %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasOCSPStapling {
|
||||
ocspConfig := ocspStapling.(certmagic.OCSPConfig)
|
||||
ap.DisableOCSPStapling = ocspConfig.DisableStapling
|
||||
|
||||
@@ -121,6 +121,13 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
|
||||
}
|
||||
}
|
||||
|
||||
// If this request changed the config, clear the last
|
||||
// config info we have stored, if it is different from
|
||||
// the original source.
|
||||
caddy.ClearLastConfigIfDifferent(
|
||||
r.Header.Get("Caddy-Config-Source-File"),
|
||||
r.Header.Get("Caddy-Config-Source-Adapter"))
|
||||
|
||||
caddy.Log().Named("admin.api").Info("load complete")
|
||||
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
acme_dns mock foo
|
||||
}
|
||||
|
||||
example.com {
|
||||
respond "Hello World"
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "Hello World",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"argument": "foo",
|
||||
"name": "mock"
|
||||
}
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,17 +34,10 @@ example.com {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"example.com"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"name": "mock"
|
||||
}
|
||||
}
|
||||
"dns": {}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
acme_dns
|
||||
}
|
||||
|
||||
example.com {
|
||||
respond "Hello World"
|
||||
}
|
||||
----------
|
||||
acme_dns specified without DNS provider config, but no provider specified with 'dns' global option
|
||||
@@ -31,9 +31,6 @@ example.com
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"example.com"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"module": "acme",
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
trusted_proxies static private_ranges
|
||||
client_ip_headers Custom-Real-Client-IP X-Forwarded-For
|
||||
client_ip_headers A-Third-One
|
||||
keepalive_interval 20s
|
||||
keepalive_idle 20s
|
||||
keepalive_count 10
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +48,9 @@ foo.com {
|
||||
"read_header_timeout": 30000000000,
|
||||
"write_timeout": 30000000000,
|
||||
"idle_timeout": 30000000000,
|
||||
"keepalive_interval": 20000000000,
|
||||
"keepalive_idle": 20000000000,
|
||||
"keepalive_count": 10,
|
||||
"max_header_bytes": 100000000,
|
||||
"enable_full_duplex": true,
|
||||
"routes": [
|
||||
@@ -89,4 +95,4 @@ foo.com {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
(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
@@ -0,0 +1,57 @@
|
||||
(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
@@ -0,0 +1,57 @@
|
||||
(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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
(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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
:80
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format filter {
|
||||
wrap console
|
||||
|
||||
# Multiple regexp filters for the same field - this should work now!
|
||||
request>headers>Authorization regexp "Bearer\s+([A-Za-z0-9_-]+)" "Bearer [REDACTED]"
|
||||
request>headers>Authorization regexp "Basic\s+([A-Za-z0-9+/=]+)" "Basic [REDACTED]"
|
||||
request>headers>Authorization regexp "token=([^&\s]+)" "token=[REDACTED]"
|
||||
|
||||
# Single regexp filter - this should continue to work as before
|
||||
request>headers>Cookie regexp "sessionid=[^;]+" "sessionid=[REDACTED]"
|
||||
|
||||
# Mixed filters (non-regexp) - these should work normally
|
||||
request>headers>Server delete
|
||||
request>remote_ip ip_mask {
|
||||
ipv4 24
|
||||
ipv6 32
|
||||
}
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"logging": {
|
||||
"logs": {
|
||||
"default": {
|
||||
"exclude": [
|
||||
"http.log.access.log0"
|
||||
]
|
||||
},
|
||||
"log0": {
|
||||
"writer": {
|
||||
"output": "stdout"
|
||||
},
|
||||
"encoder": {
|
||||
"fields": {
|
||||
"request\u003eheaders\u003eAuthorization": {
|
||||
"filter": "multi_regexp",
|
||||
"operations": [
|
||||
{
|
||||
"regexp": "Bearer\\s+([A-Za-z0-9_-]+)",
|
||||
"value": "Bearer [REDACTED]"
|
||||
},
|
||||
{
|
||||
"regexp": "Basic\\s+([A-Za-z0-9+/=]+)",
|
||||
"value": "Basic [REDACTED]"
|
||||
},
|
||||
{
|
||||
"regexp": "token=([^\u0026\\s]+)",
|
||||
"value": "token=[REDACTED]"
|
||||
}
|
||||
]
|
||||
},
|
||||
"request\u003eheaders\u003eCookie": {
|
||||
"filter": "regexp",
|
||||
"regexp": "sessionid=[^;]+",
|
||||
"value": "sessionid=[REDACTED]"
|
||||
},
|
||||
"request\u003eheaders\u003eServer": {
|
||||
"filter": "delete"
|
||||
},
|
||||
"request\u003eremote_ip": {
|
||||
"filter": "ip_mask",
|
||||
"ipv4_cidr": 24,
|
||||
"ipv6_cidr": 32
|
||||
}
|
||||
},
|
||||
"format": "filter",
|
||||
"wrap": {
|
||||
"format": "console"
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"http.log.access.log0"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":80"
|
||||
],
|
||||
"logs": {
|
||||
"default_logger_name": "log0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
{
|
||||
servers {
|
||||
trusted_proxies_unix
|
||||
}
|
||||
}
|
||||
|
||||
example.com {
|
||||
reverse_proxy https://local:8080
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"transport": {
|
||||
"protocol": "http",
|
||||
"tls": {}
|
||||
},
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "local:8080"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
],
|
||||
"trusted_proxies_unix": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
acme_dns mock foo
|
||||
}
|
||||
|
||||
localhost {
|
||||
tls {
|
||||
dns mock bar
|
||||
resolvers 8.8.8.8 8.8.4.4
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"localhost"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"argument": "bar",
|
||||
"name": "mock"
|
||||
},
|
||||
"resolvers": [
|
||||
"8.8.8.8",
|
||||
"8.8.4.4"
|
||||
]
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"argument": "foo",
|
||||
"name": "mock"
|
||||
}
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
dns mock foo
|
||||
}
|
||||
|
||||
localhost {
|
||||
tls {
|
||||
dns mock bar
|
||||
resolvers 8.8.8.8 8.8.4.4
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"localhost"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"argument": "bar",
|
||||
"name": "mock"
|
||||
},
|
||||
"resolvers": [
|
||||
"8.8.8.8",
|
||||
"8.8.4.4"
|
||||
]
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"dns": {
|
||||
"argument": "foo",
|
||||
"name": "mock"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
{
|
||||
acme_dns mock
|
||||
}
|
||||
|
||||
localhost {
|
||||
tls {
|
||||
resolvers 8.8.8.8 8.8.4.4
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"localhost"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"name": "mock"
|
||||
},
|
||||
"resolvers": [
|
||||
"8.8.8.8",
|
||||
"8.8.4.4"
|
||||
]
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"name": "mock"
|
||||
}
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
)
|
||||
|
||||
func newH2ListenerWithVersionsWithTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.Tester {
|
||||
const baseConfig = `
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
servers :9443 {
|
||||
protocols %s
|
||||
}
|
||||
}
|
||||
localhost {
|
||||
respond "{http.request.tls.proto} {http.request.proto}"
|
||||
}
|
||||
`
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile")
|
||||
|
||||
tr := tester.Client.Transport.(*http.Transport)
|
||||
tr.TLSClientConfig.NextProtos = clientVersions
|
||||
tr.Protocols = new(http.Protocols)
|
||||
if slices.Contains(clientVersions, "h2") {
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
tr.Protocols.SetHTTP2(true)
|
||||
}
|
||||
if !slices.Contains(clientVersions, "http/1.1") {
|
||||
tr.Protocols.SetHTTP1(false)
|
||||
}
|
||||
|
||||
return tester
|
||||
}
|
||||
|
||||
func TestH2ListenerWithTLS(t *testing.T) {
|
||||
tests := []struct {
|
||||
serverVersions []string
|
||||
clientVersions []string
|
||||
expectedBody string
|
||||
failed bool
|
||||
}{
|
||||
{[]string{"h2"}, []string{"h2"}, "h2 HTTP/2.0", false},
|
||||
{[]string{"h2"}, []string{"http/1.1"}, "", true},
|
||||
{[]string{"h1"}, []string{"http/1.1"}, "http/1.1 HTTP/1.1", false},
|
||||
{[]string{"h1"}, []string{"h2"}, "", true},
|
||||
{[]string{"h2", "h1"}, []string{"h2"}, "h2 HTTP/2.0", false},
|
||||
{[]string{"h2", "h1"}, []string{"http/1.1"}, "http/1.1 HTTP/1.1", false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tester := newH2ListenerWithVersionsWithTLSTester(t, tc.serverVersions, tc.clientVersions)
|
||||
t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions)
|
||||
if tc.failed {
|
||||
resp, err := tester.Client.Get("https://localhost:9443")
|
||||
if err == nil {
|
||||
t.Errorf("unexpected response: %d", resp.StatusCode)
|
||||
}
|
||||
} else {
|
||||
tester.AssertGetResponse("https://localhost:9443", 200, tc.expectedBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newH2ListenerWithVersionsWithoutTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.Tester {
|
||||
const baseConfig = `
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
servers :9080 {
|
||||
protocols %s
|
||||
}
|
||||
}
|
||||
http://localhost {
|
||||
respond "{http.request.proto}"
|
||||
}
|
||||
`
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile")
|
||||
|
||||
tr := tester.Client.Transport.(*http.Transport)
|
||||
tr.Protocols = new(http.Protocols)
|
||||
if slices.Contains(clientVersions, "h2c") {
|
||||
tr.Protocols.SetHTTP1(false)
|
||||
tr.Protocols.SetUnencryptedHTTP2(true)
|
||||
} else if slices.Contains(clientVersions, "http/1.1") {
|
||||
tr.Protocols.SetHTTP1(true)
|
||||
tr.Protocols.SetUnencryptedHTTP2(false)
|
||||
}
|
||||
|
||||
return tester
|
||||
}
|
||||
|
||||
func TestH2ListenerWithoutTLS(t *testing.T) {
|
||||
tests := []struct {
|
||||
serverVersions []string
|
||||
clientVersions []string
|
||||
expectedBody string
|
||||
failed bool
|
||||
}{
|
||||
{[]string{"h2c"}, []string{"h2c"}, "HTTP/2.0", false},
|
||||
{[]string{"h2c"}, []string{"http/1.1"}, "", true},
|
||||
{[]string{"h1"}, []string{"http/1.1"}, "HTTP/1.1", false},
|
||||
{[]string{"h1"}, []string{"h2c"}, "", true},
|
||||
{[]string{"h2c", "h1"}, []string{"h2c"}, "HTTP/2.0", false},
|
||||
{[]string{"h2c", "h1"}, []string{"http/1.1"}, "HTTP/1.1", false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tester := newH2ListenerWithVersionsWithoutTLSTester(t, tc.serverVersions, tc.clientVersions)
|
||||
t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions)
|
||||
if tc.failed {
|
||||
resp, err := tester.Client.Get("http://localhost:9080")
|
||||
if err == nil {
|
||||
t.Errorf("unexpected response: %d", resp.StatusCode)
|
||||
}
|
||||
} else {
|
||||
tester.AssertGetResponse("http://localhost:9080", 200, tc.expectedBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,9 @@ func init() {
|
||||
}
|
||||
|
||||
// MockDNSProvider is a mock DNS provider, for testing config with DNS modules.
|
||||
type MockDNSProvider struct{}
|
||||
type MockDNSProvider struct {
|
||||
Argument string `json:"argument,omitempty"` // optional argument useful for testing
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (MockDNSProvider) CaddyModule() caddy.ModuleInfo {
|
||||
@@ -31,7 +33,15 @@ func (MockDNSProvider) Provision(ctx caddy.Context) error {
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
|
||||
func (MockDNSProvider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
func (p *MockDNSProvider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
d.Next() // consume directive name
|
||||
|
||||
if d.NextArg() {
|
||||
p.Argument = d.Val()
|
||||
}
|
||||
if d.NextArg() {
|
||||
return d.Errf("unexpected argument '%s'", d.Val())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Configure Caddy
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
debug
|
||||
}
|
||||
localhost {
|
||||
log
|
||||
basic_auth {
|
||||
john $2a$14$x4HlYwA9Zeer4RkMEYbUzug9XxWmncneR.dcMs.UjalR95URnHg5.
|
||||
}
|
||||
respond "Hello, World!"
|
||||
}
|
||||
```
|
||||
|
||||
# requests without `Authorization` header are rejected with 401
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 401
|
||||
[Asserts]
|
||||
header "WWW-Authenticate" == "Basic realm=\"restricted\""
|
||||
|
||||
|
||||
# requests with `Authorization` header are accepted with 200
|
||||
GET https://localhost:9443
|
||||
[BasicAuth]
|
||||
john:password
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
`Hello, World!`
|
||||
@@ -0,0 +1,150 @@
|
||||
# Configure Caddy with error directive
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
error /forbidden* "Access denied" 403
|
||||
respond "OK"
|
||||
}
|
||||
```
|
||||
|
||||
# error directive triggers 403 for matching paths
|
||||
GET https://localhost:9443/forbidden/resource
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 403
|
||||
|
||||
|
||||
# error directive does not trigger for non-matching paths
|
||||
GET https://localhost:9443/allowed
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "OK"
|
||||
|
||||
|
||||
# Configure Caddy with error and handle_errors
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
error /admin* "Forbidden" 403
|
||||
handle_errors {
|
||||
respond "Custom error: {err.status_code} - {err.status_text}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# error with handle_errors shows custom error page
|
||||
GET https://localhost:9443/admin/panel
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 403
|
||||
[Asserts]
|
||||
body == "Custom error: 403 - Forbidden"
|
||||
|
||||
|
||||
# Configure Caddy with conditional error
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
@admin path /admin*
|
||||
error @admin 404
|
||||
respond "Public content"
|
||||
}
|
||||
```
|
||||
|
||||
# error with named matcher triggers on match
|
||||
GET https://localhost:9443/admin/users
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 404
|
||||
|
||||
|
||||
# error with named matcher doesn't trigger on non-match
|
||||
GET https://localhost:9443/public
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "Public content"
|
||||
|
||||
|
||||
# Configure Caddy with error for specific methods
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
@post method POST
|
||||
error @post "Method not allowed" 405
|
||||
respond "GET OK"
|
||||
}
|
||||
```
|
||||
|
||||
# error blocks POST requests
|
||||
POST https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 405
|
||||
|
||||
|
||||
# error allows GET requests
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "GET OK"
|
||||
|
||||
|
||||
# Configure Caddy with dynamic error message
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
error /error* "Path {path} not found" 404
|
||||
handle_errors {
|
||||
respond "{err.message}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# error message can use placeholders
|
||||
GET https://localhost:9443/error/test
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 404
|
||||
[Asserts]
|
||||
body == "Path /error/test not found"
|
||||
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Index.html Title</title>
|
||||
</head>
|
||||
<body>
|
||||
Index.html
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
index.txt
|
||||
@@ -0,0 +1,119 @@
|
||||
# Configure Caddy with default configuration
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
debug
|
||||
}
|
||||
localhost {
|
||||
root {{indexed_root}}
|
||||
file_server
|
||||
}
|
||||
```
|
||||
|
||||
# requests without specific file receive index file per
|
||||
# the default index list: index.html, index.txt
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
```
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Index.html Title</title>
|
||||
</head>
|
||||
<body>
|
||||
Index.html
|
||||
</body>
|
||||
</html>```
|
||||
|
||||
|
||||
# if index.txt is specifically requested, we expect index.txt
|
||||
GET https://localhost:9443/index.txt
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "index.txt"
|
||||
|
||||
# requests for sub-folder followed by .. result in sanitized path
|
||||
GET https://localhost:9443/non-existent/../index.txt
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "index.txt"
|
||||
|
||||
# results out of root folder are sanitized,
|
||||
# and conform to default index list sequence.
|
||||
GET https://localhost:9443/../
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
```
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Index.html Title</title>
|
||||
</head>
|
||||
<body>
|
||||
Index.html
|
||||
</body>
|
||||
</html>```
|
||||
|
||||
|
||||
# Configure Caddy with custsom index "index.txt"
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
debug
|
||||
}
|
||||
localhost {
|
||||
root {{indexed_root}}
|
||||
file_server {
|
||||
index index.txt
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "index.txt"
|
||||
|
||||
|
||||
# Configure with a root not containing index files
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
debug
|
||||
}
|
||||
localhost {
|
||||
root {{unindexed_root}}
|
||||
file_server
|
||||
}
|
||||
```
|
||||
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 404
|
||||
@@ -0,0 +1,132 @@
|
||||
# Configure Caddy with forward_auth directive
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
forward_auth localhost:9080 {
|
||||
uri /auth
|
||||
}
|
||||
respond "Protected content"
|
||||
}
|
||||
http://localhost:9080 {
|
||||
handle /auth {
|
||||
respond 200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# forward_auth allows request when auth endpoint returns 2xx
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
delay: 500ms
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "Protected content"
|
||||
|
||||
|
||||
# Configure Caddy with forward_auth rejecting
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
forward_auth localhost:9080 {
|
||||
uri /auth
|
||||
}
|
||||
respond "Protected content"
|
||||
}
|
||||
http://localhost:9080 {
|
||||
handle /auth {
|
||||
respond 401
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# forward_auth blocks request when auth endpoint returns 4xx
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
delay: 500ms
|
||||
insecure: true
|
||||
HTTP 401
|
||||
|
||||
|
||||
# Configure Caddy with forward_auth copying headers
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
forward_auth localhost:9080 {
|
||||
uri /auth
|
||||
copy_headers X-User-ID X-User-Email
|
||||
}
|
||||
respond "User: {header.X-User-ID}, Email: {header.X-User-Email}"
|
||||
}
|
||||
http://localhost:9080 {
|
||||
handle /auth {
|
||||
header X-User-ID "user123"
|
||||
header X-User-Email "user@example.com"
|
||||
respond 200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# forward_auth copies specified headers from auth response
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
delay: 500ms
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "User: user123, Email: user@example.com"
|
||||
|
||||
|
||||
# Configure Caddy with forward_auth and custom headers
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
forward_auth localhost:9080 {
|
||||
uri /auth
|
||||
header_up X-Original-URL {uri}
|
||||
}
|
||||
respond "OK"
|
||||
}
|
||||
http://localhost:9080 {
|
||||
handle /auth {
|
||||
respond "{header.X-Original-URL}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# forward_auth can send custom headers to auth endpoint
|
||||
GET https://localhost:9443/test/path
|
||||
[Options]
|
||||
delay: 500ms
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "OK"
|
||||
@@ -0,0 +1,22 @@
|
||||
# Configure Caddy
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
debug
|
||||
}
|
||||
localhost {
|
||||
header "X-Custom-Header" "Custom-Value"
|
||||
}
|
||||
```
|
||||
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
header "X-Custom-Header" == "Custom-Value"
|
||||
@@ -0,0 +1,190 @@
|
||||
# Configure Caddy with request_header directive
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
request_header X-Custom-Header "CustomValue"
|
||||
respond "{header.X-Custom-Header}"
|
||||
}
|
||||
```
|
||||
|
||||
# request_header adds headers to request
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "CustomValue"
|
||||
|
||||
|
||||
# Configure Caddy with request_header removing headers
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
request_header -User-Agent
|
||||
respond "UA: {header.User-Agent}"
|
||||
}
|
||||
```
|
||||
|
||||
# request_header can remove headers
|
||||
GET https://localhost:9443
|
||||
User-Agent: TestAgent/1.0
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "UA: "
|
||||
|
||||
|
||||
# Configure Caddy with request_header replacing headers
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
request_header Host "example.com"
|
||||
respond "Host: {host}"
|
||||
}
|
||||
```
|
||||
|
||||
# request_header can replace Host header
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "Host: example.com"
|
||||
|
||||
|
||||
# Configure Caddy with request_header using placeholders
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
request_header X-Original-Path {path}
|
||||
respond "Path: {header.X-Original-Path}"
|
||||
}
|
||||
```
|
||||
|
||||
# request_header can use placeholders
|
||||
GET https://localhost:9443/test/path
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "Path: /test/path"
|
||||
|
||||
|
||||
# Configure Caddy with conditional request_header
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
@api path /api/*
|
||||
request_header @api X-API "true"
|
||||
respond "API: {header.X-API}"
|
||||
}
|
||||
```
|
||||
|
||||
# request_header applies conditionally based on matcher
|
||||
GET https://localhost:9443/api/test
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "API: true"
|
||||
|
||||
|
||||
# request_header doesn't apply when matcher doesn't match
|
||||
GET https://localhost:9443/other
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "API: "
|
||||
|
||||
|
||||
# Configure Caddy with multiple request_header operations
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
request_header X-First "1"
|
||||
request_header X-Second "2"
|
||||
request_header X-Third "3"
|
||||
respond "{header.X-First},{header.X-Second},{header.X-Third}"
|
||||
}
|
||||
```
|
||||
|
||||
# multiple request_header directives are applied
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "1,2,3"
|
||||
|
||||
|
||||
# Configure Caddy with request_header and reverse_proxy
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
debug
|
||||
}
|
||||
localhost {
|
||||
request_header X-Custom-Header "Value"
|
||||
reverse_proxy localhost:9450
|
||||
}
|
||||
http://localhost:9450 {
|
||||
respond "{header.X-Custom-Header}"
|
||||
}
|
||||
```
|
||||
|
||||
# request_header adds header before reverse_proxy
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "Value"
|
||||
@@ -0,0 +1,36 @@
|
||||
# Configure Caddy
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
log
|
||||
request_body {
|
||||
max_size 2B
|
||||
}
|
||||
reverse_proxy localhost:8000 # to fake body reading
|
||||
handle_errors 4xx {
|
||||
respond "OK"
|
||||
}
|
||||
}
|
||||
http://localhost:8000 {
|
||||
respond "Failed"
|
||||
}
|
||||
```
|
||||
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
delay: 1s
|
||||
insecure: true
|
||||
```
|
||||
Hello
|
||||
```
|
||||
HTTP 413
|
||||
`OK`
|
||||
|
||||
# TODO: how to test{read,write}_timeout?
|
||||
@@ -0,0 +1,66 @@
|
||||
# Configure Caddy
|
||||
POST http://localhost:2019/load
|
||||
User-Agent: hurl/ci
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
rewrite /from /to
|
||||
respond {uri}
|
||||
}
|
||||
```
|
||||
|
||||
# simple scenario: rewriting /from to /to produces expected result of seeing /to
|
||||
GET https://localhost:9443/from
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "/to"
|
||||
|
||||
# unmatched path is passed through unchanged
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "/"
|
||||
|
||||
# having a query parameter does not trip the rewrite and retains the query
|
||||
GET https://localhost:9443/from?query_param=value
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "/to?query_param=value"
|
||||
|
||||
|
||||
# Configure Caddy
|
||||
POST http://localhost:2019/load
|
||||
User-Agent: hurl/ci
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
rewrite /from /to?a=b
|
||||
respond {uri}
|
||||
}
|
||||
```
|
||||
|
||||
# a rewrite with query parameters affects the parameters
|
||||
GET https://localhost:9443/from?query_param=value
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "/to?a=b"
|
||||
@@ -0,0 +1,171 @@
|
||||
# Configure Caddy with route directive
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
route /api/* {
|
||||
uri strip_prefix /api
|
||||
respond "API: {uri}"
|
||||
}
|
||||
respond "Not API"
|
||||
}
|
||||
```
|
||||
|
||||
# route groups handlers and maintains order
|
||||
GET https://localhost:9443/api/users
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "API: /users"
|
||||
|
||||
|
||||
# route doesn't match non-matching paths
|
||||
GET https://localhost:9443/other
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "Not API"
|
||||
|
||||
|
||||
# Configure Caddy with nested routes
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
route /api/* {
|
||||
uri strip_prefix /api
|
||||
route /v1/* {
|
||||
uri strip_prefix /v1
|
||||
respond "API v1: {uri}"
|
||||
}
|
||||
respond "API: {uri}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# nested routes process sequentially
|
||||
GET https://localhost:9443/api/v1/users
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "API v1: /users"
|
||||
|
||||
|
||||
# outer route processes when inner doesn't match
|
||||
GET https://localhost:9443/api/users
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "API: /users"
|
||||
|
||||
|
||||
# Configure Caddy with route and terminal handlers
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
route {
|
||||
header X-First "1"
|
||||
respond "Response"
|
||||
header X-Second "2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# route stops at terminal handler (respond)
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
header "X-First" == "1"
|
||||
header "X-Second" not exists
|
||||
|
||||
|
||||
# Configure Caddy with route preserving handler order
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
route {
|
||||
vars step1 "done"
|
||||
vars step2 "done"
|
||||
vars step3 "done"
|
||||
respond "{vars.step1},{vars.step2},{vars.step3}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# route preserves exact handler order
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "done,done,done"
|
||||
|
||||
|
||||
# Configure Caddy with route and matchers
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
route {
|
||||
@api path /api/*
|
||||
vars @api type "api"
|
||||
vars type "other"
|
||||
respond "{vars.type}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# route applies matchers in sequence
|
||||
GET https://localhost:9443/api/test
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "other"
|
||||
|
||||
|
||||
# route continues when matcher doesn't match
|
||||
GET https://localhost:9443/test
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "other"
|
||||
@@ -0,0 +1,105 @@
|
||||
# Configure Caddy
|
||||
POST http://localhost:2019/load
|
||||
User-Agent: hurl/ci
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
log
|
||||
respond "Hello, World!"
|
||||
}
|
||||
```
|
||||
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
`Hello, World!`
|
||||
|
||||
|
||||
GET https://localhost:9443/foo
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
`Hello, World!`
|
||||
|
||||
# Configure Caddy
|
||||
POST http://localhost:2019/load
|
||||
User-Agent: hurl/ci
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
respond "New text!"
|
||||
}
|
||||
```
|
||||
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP/2 200
|
||||
[Asserts]
|
||||
`New text!`
|
||||
|
||||
|
||||
GET https://localhost:9443/foo
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP/2 200
|
||||
[Asserts]
|
||||
`New text!`
|
||||
|
||||
GET https://localhost:9443/foo
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP/2 200
|
||||
[Asserts]
|
||||
body != "Hello, World!"
|
||||
|
||||
# Configure Caddy
|
||||
# The body is a placeholder
|
||||
POST http://localhost:2019/load
|
||||
User-Agent: hurl/ci
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
log
|
||||
respond {http.request.body}
|
||||
}
|
||||
```
|
||||
|
||||
# handler responds with the "application/json" if the response body is valid JSON
|
||||
POST https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
```json
|
||||
{
|
||||
"greeting": "Hello, world!"
|
||||
}
|
||||
```
|
||||
HTTP/2 200
|
||||
[Asserts]
|
||||
header "Content-Type" == "application/json"
|
||||
```json
|
||||
{
|
||||
"greeting": "Hello, world!"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,191 @@
|
||||
# Configure Caddy with uri strip_prefix
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
uri strip_prefix /api
|
||||
respond {uri}
|
||||
}
|
||||
```
|
||||
|
||||
# strip_prefix removes the prefix from the URI
|
||||
GET https://localhost:9443/api/users
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "/users"
|
||||
|
||||
|
||||
# URI without prefix is unchanged
|
||||
GET https://localhost:9443/users
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "/users"
|
||||
|
||||
|
||||
# Configure Caddy with uri strip_suffix
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
uri strip_suffix .php
|
||||
respond {uri}
|
||||
}
|
||||
```
|
||||
|
||||
# strip_suffix removes the suffix from the URI
|
||||
GET https://localhost:9443/index.php
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "/index"
|
||||
|
||||
|
||||
# URI without suffix is unchanged
|
||||
GET https://localhost:9443/index.html
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "/index.html"
|
||||
|
||||
|
||||
# Configure Caddy with uri replace
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
uri replace old new
|
||||
respond {uri}
|
||||
}
|
||||
```
|
||||
|
||||
# replace substitutes all occurrences
|
||||
GET https://localhost:9443/old/path/old
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "/new/path/new"
|
||||
|
||||
|
||||
# Configure Caddy with uri path_regexp
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
uri path_regexp /([0-9]+) /$1/id
|
||||
respond {uri}
|
||||
}
|
||||
```
|
||||
|
||||
# path_regexp replaces using regular expressions
|
||||
GET https://localhost:9443/123
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "/123/id"
|
||||
|
||||
|
||||
# Configure Caddy with uri query operations
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
uri query +foo bar
|
||||
respond {query}
|
||||
}
|
||||
```
|
||||
|
||||
# query operations add parameters
|
||||
GET https://localhost:9443/?existing=value
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "existing=value&foo=bar"
|
||||
|
||||
|
||||
# Configure Caddy with uri query delete
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
uri query -sensitive
|
||||
respond {query}
|
||||
}
|
||||
```
|
||||
|
||||
# query operations delete parameters
|
||||
GET https://localhost:9443/?keep=this&sensitive=secret
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "keep=this"
|
||||
|
||||
|
||||
# Configure Caddy with uri query rename
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
uri query old>new
|
||||
respond {query}
|
||||
}
|
||||
```
|
||||
|
||||
# query operations rename parameters
|
||||
GET https://localhost:9443/?old=value
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "new=value"
|
||||
@@ -0,0 +1,125 @@
|
||||
# Configure Caddy with vars directive
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
vars my_var "custom_value"
|
||||
vars another_var "another_value"
|
||||
respond "{vars.my_var} {vars.another_var}"
|
||||
}
|
||||
```
|
||||
|
||||
# Variables are accessible in placeholders
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "custom_value another_value"
|
||||
|
||||
|
||||
# Configure Caddy with vars using placeholders
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
vars request_path {path}
|
||||
vars request_method {method}
|
||||
respond "Path: {vars.request_path}, Method: {vars.request_method}"
|
||||
}
|
||||
```
|
||||
|
||||
# Variables can be set from request placeholders
|
||||
GET https://localhost:9443/test/path
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "Path: /test/path, Method: GET"
|
||||
|
||||
|
||||
# POST method is captured correctly
|
||||
POST https://localhost:9443/another
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "Path: /another, Method: POST"
|
||||
|
||||
|
||||
# Configure Caddy with vars in route
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
route /api/* {
|
||||
vars api_version "v1"
|
||||
respond "API {vars.api_version}"
|
||||
}
|
||||
respond "Not API"
|
||||
}
|
||||
```
|
||||
|
||||
# Variables are scoped to their route
|
||||
GET https://localhost:9443/api/users
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "API v1"
|
||||
|
||||
|
||||
# Outside the route, variables are not set
|
||||
GET https://localhost:9443/other
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "Not API"
|
||||
|
||||
|
||||
# Configure Caddy with vars overwriting
|
||||
POST http://localhost:2019/load
|
||||
Content-Type: text/caddyfile
|
||||
```
|
||||
{
|
||||
skip_install_trust
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
localhost {
|
||||
# without `route`, middlewares are sorted an unstable sort
|
||||
route {
|
||||
vars my_var "2"
|
||||
vars my_var "1"
|
||||
}
|
||||
respond "{vars.my_var}"
|
||||
}
|
||||
```
|
||||
|
||||
# Later vars directives overwrite earlier ones
|
||||
GET https://localhost:9443
|
||||
[Options]
|
||||
insecure: true
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "1"
|
||||
@@ -0,0 +1,2 @@
|
||||
indexed_root=caddytest/spec/http/file_server/assets/indexed
|
||||
unindexed_root=caddytest/spec/http/file_server/assets/unindexed
|
||||
+70
-11
@@ -172,9 +172,19 @@ func cmdStart(fl Flags) (int, error) {
|
||||
func cmdRun(fl Flags) (int, error) {
|
||||
caddy.TrapSignals()
|
||||
|
||||
logger := caddy.Log()
|
||||
// set up buffered logging for early startup
|
||||
// so that we can hold onto logs until after
|
||||
// the config is loaded (or fails to load)
|
||||
// so that we can write the logs to the user's
|
||||
// configured output. we must be sure to flush
|
||||
// on any error before the config is loaded.
|
||||
logger, defaultLogger, logBuffer := caddy.BufferedLog()
|
||||
|
||||
undoMaxProcs := setResourceLimits(logger)
|
||||
defer undoMaxProcs()
|
||||
// release the local reference to the undo function so it can be GC'd;
|
||||
// the deferred call above has already captured the actual function value.
|
||||
undoMaxProcs = nil //nolint:ineffassign,wastedassign
|
||||
|
||||
configFlag := fl.String("config")
|
||||
configAdapterFlag := fl.String("adapter")
|
||||
@@ -187,6 +197,7 @@ func cmdRun(fl Flags) (int, error) {
|
||||
// load all additional envs as soon as possible
|
||||
err := handleEnvFileFlag(fl)
|
||||
if err != nil {
|
||||
logBuffer.FlushTo(defaultLogger)
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
@@ -204,6 +215,7 @@ func cmdRun(fl Flags) (int, error) {
|
||||
logger.Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
|
||||
resumeFlag = false
|
||||
} else if err != nil {
|
||||
logBuffer.FlushTo(defaultLogger)
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
} else {
|
||||
if configFlag == "" {
|
||||
@@ -219,9 +231,11 @@ func cmdRun(fl Flags) (int, error) {
|
||||
}
|
||||
// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
|
||||
var configFile string
|
||||
var adapterUsed string
|
||||
if !resumeFlag {
|
||||
config, configFile, err = LoadConfig(configFlag, configAdapterFlag)
|
||||
config, configFile, adapterUsed, err = LoadConfig(configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
logBuffer.FlushTo(defaultLogger)
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
}
|
||||
@@ -236,11 +250,35 @@ func cmdRun(fl Flags) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a source config file (we're running via 'caddy run --config ...'),
|
||||
// record it so SIGUSR1 can reload from the same file. Also provide a callback
|
||||
// that knows how to load/adapt that source when requested by the main process.
|
||||
if configFile != "" {
|
||||
caddy.SetLastConfig(configFile, adapterUsed, func(file, adapter string) error {
|
||||
cfg, _, _, err := LoadConfig(file, adapter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return caddy.Load(cfg, true)
|
||||
})
|
||||
}
|
||||
|
||||
// run the initial config
|
||||
err = caddy.Load(config, true)
|
||||
if err != nil {
|
||||
logBuffer.FlushTo(defaultLogger)
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err)
|
||||
}
|
||||
// release the reference to the config so it can be GC'd
|
||||
config = nil //nolint:ineffassign,wastedassign
|
||||
|
||||
// at this stage the config will have replaced the
|
||||
// default logger to the configured one, so we can
|
||||
// log normally, now that the config is running.
|
||||
// also clear our ref to the buffer so it can get GC'd
|
||||
logger = caddy.Log()
|
||||
defaultLogger = nil //nolint:ineffassign,wastedassign
|
||||
logBuffer = nil //nolint:wastedassign,ineffassign
|
||||
logger.Info("serving initial configuration")
|
||||
|
||||
// if we are to report to another process the successful start
|
||||
@@ -256,18 +294,22 @@ func cmdRun(fl Flags) (int, error) {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("dialing confirmation address: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
_, err = conn.Write(confirmationBytes)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("writing confirmation bytes to %s: %v", pingbackFlag, err)
|
||||
}
|
||||
// close (non-defer because we `select {}` below)
|
||||
// and release references so they can be GC'd
|
||||
conn.Close()
|
||||
confirmationBytes = nil //nolint:ineffassign,wastedassign
|
||||
conn = nil //nolint:wastedassign,ineffassign
|
||||
}
|
||||
|
||||
// if enabled, reload config file automatically on changes
|
||||
// (this better only be used in dev!)
|
||||
if watchFlag {
|
||||
go watchConfigFile(configFile, configAdapterFlag)
|
||||
go watchConfigFile(configFile, adapterUsed)
|
||||
}
|
||||
|
||||
// warn if the environment does not provide enough information about the disk
|
||||
@@ -289,6 +331,9 @@ func cmdRun(fl Flags) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// release the last local logger reference
|
||||
logger = nil //nolint:wastedassign,ineffassign
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
@@ -319,7 +364,7 @@ func cmdReload(fl Flags) (int, error) {
|
||||
forceFlag := fl.Bool("force")
|
||||
|
||||
// get the config in caddy's native format
|
||||
config, configFile, err := LoadConfig(configFlag, configAdapterFlag)
|
||||
config, configFile, adapterUsed, err := LoadConfig(configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
@@ -337,6 +382,10 @@ func cmdReload(fl Flags) (int, error) {
|
||||
if forceFlag {
|
||||
headers.Set("Cache-Control", "must-revalidate")
|
||||
}
|
||||
// Provide the source file/adapter to the running process so it can
|
||||
// preserve its last-config knowledge if this reload came from the same source.
|
||||
headers.Set("Caddy-Config-Source-File", configFile)
|
||||
headers.Set("Caddy-Config-Source-Adapter", adapterUsed)
|
||||
|
||||
resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
|
||||
if err != nil {
|
||||
@@ -441,16 +490,20 @@ func cmdEnviron(fl Flags) (int, error) {
|
||||
}
|
||||
|
||||
func cmdAdaptConfig(fl Flags) (int, error) {
|
||||
inputFlag := fl.String("config")
|
||||
configFlag := fl.String("config")
|
||||
adapterFlag := fl.String("adapter")
|
||||
prettyFlag := fl.Bool("pretty")
|
||||
validateFlag := fl.Bool("validate")
|
||||
|
||||
var err error
|
||||
inputFlag, err = configFileWithRespectToDefault(caddy.Log(), inputFlag)
|
||||
configFlag, err = configFileWithRespectToDefault(caddy.Log(), configFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
if configFlag == "" {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
|
||||
}
|
||||
|
||||
// load all additional envs as soon as possible
|
||||
err = handleEnvFileFlag(fl)
|
||||
@@ -469,13 +522,19 @@ func cmdAdaptConfig(fl Flags) (int, error) {
|
||||
fmt.Errorf("unrecognized config adapter: %s", adapterFlag)
|
||||
}
|
||||
|
||||
input, err := os.ReadFile(inputFlag)
|
||||
var input []byte
|
||||
// read from stdin if the file name is "-"
|
||||
if configFlag == "-" {
|
||||
input, err = io.ReadAll(os.Stdin)
|
||||
} else {
|
||||
input, err = os.ReadFile(configFlag)
|
||||
}
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("reading input file: %v", err)
|
||||
}
|
||||
|
||||
opts := map[string]any{"filename": inputFlag}
|
||||
opts := map[string]any{"filename": configFlag}
|
||||
|
||||
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
|
||||
if err != nil {
|
||||
@@ -541,7 +600,7 @@ func cmdValidateConfig(fl Flags) (int, error) {
|
||||
fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
|
||||
}
|
||||
|
||||
input, _, err := LoadConfig(configFlag, adapterFlag)
|
||||
input, _, _, err := LoadConfig(configFlag, adapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
@@ -756,7 +815,7 @@ func DetermineAdminAPIAddress(address string, config []byte, configFile, configA
|
||||
loadedConfig := config
|
||||
if len(loadedConfig) == 0 {
|
||||
// get the config in caddy's native format
|
||||
loadedConfig, loadedConfigFile, err = LoadConfig(configFile, configAdapter)
|
||||
loadedConfig, loadedConfigFile, _, err = LoadConfig(configFile, configAdapter)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
+3
-1
@@ -293,6 +293,8 @@ zero exit status will be returned.
|
||||
|
||||
If --envfile is specified, an environment file with environment variables
|
||||
in the KEY=VALUE format will be loaded into the Caddy process.
|
||||
|
||||
If you wish to use stdin instead of a regular file, use - as the path.
|
||||
`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("config", "c", "", "Configuration file to adapt (required)")
|
||||
@@ -390,7 +392,7 @@ lines will be prefixed with '-' and '+' where they differ. Note that
|
||||
unchanged lines are prefixed with two spaces for alignment, and that this
|
||||
is not a valid patch format.
|
||||
|
||||
If you wish you use stdin instead of a regular file, use - as the path.
|
||||
If you wish to use stdin instead of a regular file, use - as the path.
|
||||
When reading from stdin, the --overwrite flag has no effect: the result
|
||||
is always printed to stdout.
|
||||
`,
|
||||
|
||||
+18
-13
@@ -100,7 +100,12 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
|
||||
// there is no config available. It prints any warnings to stderr,
|
||||
// and returns the resulting JSON config bytes along with
|
||||
// the name of the loaded config file (if any).
|
||||
func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
|
||||
// The return values are:
|
||||
// - config bytes (nil if no config)
|
||||
// - config file used ("" if none)
|
||||
// - adapter used ("" if none)
|
||||
// - error, if any
|
||||
func LoadConfig(configFile, adapterName string) ([]byte, string, string, error) {
|
||||
return loadConfigWithLogger(caddy.Log(), configFile, adapterName)
|
||||
}
|
||||
|
||||
@@ -138,7 +143,7 @@ func isCaddyfile(configFile, adapterName string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, error) {
|
||||
func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, string, error) {
|
||||
// if no logger is provided, use a nop logger
|
||||
// just so we don't have to check for nil
|
||||
if logger == nil {
|
||||
@@ -147,7 +152,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
|
||||
// specifying an adapter without a config file is ambiguous
|
||||
if adapterName != "" && configFile == "" {
|
||||
return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)")
|
||||
return nil, "", "", fmt.Errorf("cannot adapt config without config file (use --config)")
|
||||
}
|
||||
|
||||
// load initial config and adapter
|
||||
@@ -158,13 +163,13 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
if configFile == "-" {
|
||||
config, err = io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("reading config from stdin: %v", err)
|
||||
return nil, "", "", fmt.Errorf("reading config from stdin: %v", err)
|
||||
}
|
||||
logger.Info("using config from stdin")
|
||||
} else {
|
||||
config, err = os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("reading config from file: %v", err)
|
||||
return nil, "", "", fmt.Errorf("reading config from file: %v", err)
|
||||
}
|
||||
logger.Info("using config from file", zap.String("file", configFile))
|
||||
}
|
||||
@@ -179,7 +184,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
cfgAdapter = nil
|
||||
} else if err != nil {
|
||||
// default Caddyfile exists, but error reading it
|
||||
return nil, "", fmt.Errorf("reading default Caddyfile: %v", err)
|
||||
return nil, "", "", fmt.Errorf("reading default Caddyfile: %v", err)
|
||||
} else {
|
||||
// success reading default Caddyfile
|
||||
configFile = "Caddyfile"
|
||||
@@ -191,14 +196,14 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
if yes, err := isCaddyfile(configFile, adapterName); yes {
|
||||
adapterName = "caddyfile"
|
||||
} else if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", "", err
|
||||
}
|
||||
|
||||
// load config adapter
|
||||
if adapterName != "" {
|
||||
cfgAdapter = caddyconfig.GetAdapter(adapterName)
|
||||
if cfgAdapter == nil {
|
||||
return nil, "", fmt.Errorf("unrecognized config adapter: %s", adapterName)
|
||||
return nil, "", "", fmt.Errorf("unrecognized config adapter: %s", adapterName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +213,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
"filename": configFile,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("adapting config using %s: %v", adapterName, err)
|
||||
return nil, "", "", fmt.Errorf("adapting config using %s: %v", adapterName, err)
|
||||
}
|
||||
logger.Info("adapted config to JSON", zap.String("adapter", adapterName))
|
||||
for _, warn := range warnings {
|
||||
@@ -226,11 +231,11 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
// validate that the config is at least valid JSON
|
||||
err = json.Unmarshal(config, new(any))
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("config is not valid JSON: %v; did you mean to use a config adapter (the --adapter flag)?", err)
|
||||
return nil, "", "", fmt.Errorf("config is not valid JSON: %v; did you mean to use a config adapter (the --adapter flag)?", err)
|
||||
}
|
||||
}
|
||||
|
||||
return config, configFile, nil
|
||||
return config, configFile, adapterName, nil
|
||||
}
|
||||
|
||||
// watchConfigFile watches the config file at filename for changes
|
||||
@@ -256,7 +261,7 @@ func watchConfigFile(filename, adapterName string) {
|
||||
}
|
||||
|
||||
// get current config
|
||||
lastCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
lastCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
if err != nil {
|
||||
logger().Error("unable to load latest config", zap.Error(err))
|
||||
return
|
||||
@@ -268,7 +273,7 @@ func watchConfigFile(filename, adapterName string) {
|
||||
//nolint:staticcheck
|
||||
for range time.Tick(1 * time.Second) {
|
||||
// get current config
|
||||
newCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
newCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
if err != nil {
|
||||
logger().Error("unable to load latest config", zap.Error(err))
|
||||
return
|
||||
|
||||
@@ -62,7 +62,7 @@ func splitModule(arg string) (module, version string, err error) {
|
||||
err = fmt.Errorf("module name is required")
|
||||
}
|
||||
|
||||
return
|
||||
return module, version, err
|
||||
}
|
||||
|
||||
func cmdAddPackage(fl Flags) (int, error) {
|
||||
@@ -217,7 +217,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
err = fmt.Errorf("no build info")
|
||||
return
|
||||
return standard, nonstandard, unknown, err
|
||||
}
|
||||
|
||||
for _, modID := range caddy.Modules() {
|
||||
@@ -260,7 +260,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
|
||||
nonstandard = append(nonstandard, caddyModGoMod)
|
||||
}
|
||||
}
|
||||
return
|
||||
return standard, nonstandard, unknown, err
|
||||
}
|
||||
|
||||
func listModules(path string) error {
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ type storVal struct {
|
||||
// determineStorage returns the top-level storage module from the given config.
|
||||
// It may return nil even if no error.
|
||||
func determineStorage(configFile string, configAdapter string) (*storVal, error) {
|
||||
cfg, _, err := LoadConfig(configFile, configAdapter)
|
||||
cfg, _, _, err := LoadConfig(configFile, configAdapter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,84 +1,116 @@
|
||||
module github.com/caddyserver/caddy/v2
|
||||
|
||||
go 1.24
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/KimMachineGun/automemlimit v0.7.1
|
||||
github.com/DeRuina/timberjack v1.3.9
|
||||
github.com/KimMachineGun/automemlimit v0.7.5
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/alecthomas/chroma/v2 v2.19.0
|
||||
github.com/alecthomas/chroma/v2 v2.20.0
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
|
||||
github.com/caddyserver/certmagic v0.23.0
|
||||
github.com/caddyserver/certmagic v0.25.0
|
||||
github.com/caddyserver/zerossl v0.1.3
|
||||
github.com/cloudflare/circl v1.6.1
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/go-chi/chi/v5 v5.2.2
|
||||
github.com/google/cel-go v0.24.1
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/google/cel-go v0.26.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/klauspost/compress v1.18.0
|
||||
github.com/klauspost/compress v1.18.1
|
||||
github.com/klauspost/cpuid/v2 v2.3.0
|
||||
github.com/mholt/acmez/v3 v3.1.2
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/quic-go/quic-go v0.54.0
|
||||
github.com/smallstep/certificates v0.26.1
|
||||
github.com/smallstep/nosql v0.6.1
|
||||
github.com/mholt/acmez/v3 v3.1.4
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/quic-go/quic-go v0.56.0
|
||||
github.com/smallstep/certificates v0.28.4
|
||||
github.com/smallstep/nosql v0.7.0
|
||||
github.com/smallstep/truststore v0.13.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.7
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53
|
||||
github.com/yuin/goldmark v1.7.8
|
||||
github.com/yuin/goldmark v1.7.13
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.42.0
|
||||
go.opentelemetry.io/otel v1.31.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0
|
||||
go.opentelemetry.io/otel/sdk v1.31.0
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.63.0
|
||||
go.opentelemetry.io/otel v1.38.0
|
||||
go.opentelemetry.io/otel/sdk v1.38.0
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
go.uber.org/zap v1.27.0
|
||||
go.uber.org/zap/exp v0.3.0
|
||||
golang.org/x/crypto v0.40.0
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810
|
||||
golang.org/x/net v0.42.0
|
||||
golang.org/x/sync v0.16.0
|
||||
golang.org/x/term v0.33.0
|
||||
golang.org/x/time v0.12.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99
|
||||
golang.org/x/net v0.46.0
|
||||
golang.org/x/sync v0.17.0
|
||||
golang.org/x/term v0.36.0
|
||||
golang.org/x/time v0.14.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.19.1 // indirect
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/ccoveille/go-safecast v1.6.1 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
|
||||
github.com/google/go-tpm v0.9.0 // indirect
|
||||
github.com/google/go-tpm v0.9.6 // indirect
|
||||
github.com/google/go-tspi v0.3.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.2 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/otlptranslator v0.0.2 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 // indirect
|
||||
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect
|
||||
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/smallstep/cli-utils v0.12.1 // indirect
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca // indirect
|
||||
github.com/smallstep/linkedca v0.23.0 // indirect
|
||||
github.com/smallstep/pkcs7 v0.2.1 // indirect
|
||||
github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.17.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.38.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.38.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
google.golang.org/api v0.254.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -87,72 +119,60 @@ require (
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/dgraph-io/badger v1.6.2 // indirect
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||
github.com/dgraph-io/ristretto v0.2.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-kit/kit v0.13.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.14.3 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgtype v1.14.0 // indirect
|
||||
github.com/jackc/pgx/v4 v4.18.3 // indirect
|
||||
github.com/libdns/libdns v1.0.0-beta.1
|
||||
github.com/libdns/libdns v1.1.1
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/miekg/dns v1.1.63 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964
|
||||
github.com/pires/go-proxyproto v0.8.1
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.5.0
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/rosedblabs/wal v1.3.6
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
github.com/prometheus/common v0.67.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/slackhq/nebula v1.6.1 // indirect
|
||||
github.com/slackhq/nebula v1.9.7 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||
github.com/urfave/cli v1.22.14 // indirect
|
||||
go.etcd.io/bbolt v1.3.9 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.31.0
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
go.step.sm/cli-utils v0.9.0 // indirect
|
||||
go.step.sm/crypto v0.45.0
|
||||
go.step.sm/linkedca v0.20.1 // indirect
|
||||
github.com/urfave/cli v1.22.17 // indirect
|
||||
go.etcd.io/bbolt v1.3.10 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
go.step.sm/crypto v0.74.0
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/sys v0.34.0
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
google.golang.org/grpc v1.67.1 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/sys v0.37.0
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// LogBufferCore is a zapcore.Core that buffers log entries in memory.
|
||||
type LogBufferCore struct {
|
||||
mu sync.Mutex
|
||||
entries []zapcore.Entry
|
||||
fields [][]zapcore.Field
|
||||
level zapcore.LevelEnabler
|
||||
}
|
||||
|
||||
type LogBufferCoreInterface interface {
|
||||
zapcore.Core
|
||||
FlushTo(*zap.Logger)
|
||||
}
|
||||
|
||||
func NewLogBufferCore(level zapcore.LevelEnabler) *LogBufferCore {
|
||||
return &LogBufferCore{
|
||||
level: level,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LogBufferCore) Enabled(lvl zapcore.Level) bool {
|
||||
return c.level.Enabled(lvl)
|
||||
}
|
||||
|
||||
func (c *LogBufferCore) With(fields []zapcore.Field) zapcore.Core {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *LogBufferCore) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
||||
if c.Enabled(entry.Level) {
|
||||
return ce.AddCore(entry, c)
|
||||
}
|
||||
return ce
|
||||
}
|
||||
|
||||
func (c *LogBufferCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.entries = append(c.entries, entry)
|
||||
c.fields = append(c.fields, fields)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *LogBufferCore) Sync() error { return nil }
|
||||
|
||||
// FlushTo flushes buffered logs to the given zap.Logger.
|
||||
func (c *LogBufferCore) FlushTo(logger *zap.Logger) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
for idx, entry := range c.entries {
|
||||
logger.WithOptions().Check(entry.Level, entry.Message).Write(c.fields[idx]...)
|
||||
}
|
||||
c.entries = nil
|
||||
c.fields = nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ zapcore.Core = (*LogBufferCore)(nil)
|
||||
_ LogBufferCoreInterface = (*LogBufferCore)(nil)
|
||||
)
|
||||
@@ -261,14 +261,14 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e
|
||||
if atomic.LoadInt32(&fcpc.closed) == 1 {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
if err = fcpc.SetReadDeadline(time.Time{}); err != nil {
|
||||
return
|
||||
return n, addr, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
return n, addr, err
|
||||
}
|
||||
|
||||
return
|
||||
return n, addr, err
|
||||
}
|
||||
|
||||
// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.
|
||||
|
||||
+87
-5
@@ -31,13 +31,17 @@ import (
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
"github.com/quic-go/quic-go/qlog"
|
||||
h3qlog "github.com/quic-go/quic-go/http3/qlog"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/internal"
|
||||
)
|
||||
|
||||
// listenFdsStart is the first file descriptor number for systemd socket activation.
|
||||
// File descriptors 0, 1, 2 are reserved for stdin, stdout, stderr.
|
||||
const listenFdsStart = 3
|
||||
|
||||
// NetworkAddress represents one or more network addresses.
|
||||
// It contains the individual components for a parsed network
|
||||
// address of the form accepted by ParseNetworkAddress().
|
||||
@@ -305,6 +309,64 @@ func IsFdNetwork(netw string) bool {
|
||||
return strings.HasPrefix(netw, "fd")
|
||||
}
|
||||
|
||||
// getFdByName returns the file descriptor number for the given
|
||||
// socket name from systemd's LISTEN_FDNAMES environment variable.
|
||||
// Socket names are provided by systemd via socket activation.
|
||||
//
|
||||
// The name can optionally include an index to handle multiple sockets
|
||||
// with the same name: "web:0" for first, "web:1" for second, etc.
|
||||
// If no index is specified, defaults to index 0 (first occurrence).
|
||||
func getFdByName(nameWithIndex string) (int, error) {
|
||||
if nameWithIndex == "" {
|
||||
return 0, fmt.Errorf("socket name cannot be empty")
|
||||
}
|
||||
|
||||
fdNamesStr := os.Getenv("LISTEN_FDNAMES")
|
||||
if fdNamesStr == "" {
|
||||
return 0, fmt.Errorf("LISTEN_FDNAMES environment variable not set")
|
||||
}
|
||||
|
||||
// Parse name and optional index
|
||||
parts := strings.Split(nameWithIndex, ":")
|
||||
if len(parts) > 2 {
|
||||
return 0, fmt.Errorf("invalid socket name format '%s': too many colons", nameWithIndex)
|
||||
}
|
||||
|
||||
name := parts[0]
|
||||
targetIndex := 0
|
||||
|
||||
if len(parts) > 1 {
|
||||
var err error
|
||||
targetIndex, err = strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid socket index '%s': %v", parts[1], err)
|
||||
}
|
||||
if targetIndex < 0 {
|
||||
return 0, fmt.Errorf("socket index cannot be negative: %d", targetIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the socket names
|
||||
names := strings.Split(fdNamesStr, ":")
|
||||
|
||||
// Find the Nth occurrence of the requested name
|
||||
matchCount := 0
|
||||
for i, fdName := range names {
|
||||
if fdName == name {
|
||||
if matchCount == targetIndex {
|
||||
return listenFdsStart + i, nil
|
||||
}
|
||||
matchCount++
|
||||
}
|
||||
}
|
||||
|
||||
if matchCount == 0 {
|
||||
return 0, fmt.Errorf("socket name '%s' not found in LISTEN_FDNAMES", name)
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("socket name '%s' found %d times, but index %d requested", name, matchCount, targetIndex)
|
||||
}
|
||||
|
||||
// ParseNetworkAddress parses addr into its individual
|
||||
// components. The input string is expected to be of
|
||||
// the form "network/host:port-range" where any part is
|
||||
@@ -336,9 +398,27 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
|
||||
}, err
|
||||
}
|
||||
if IsFdNetwork(network) {
|
||||
fdAddr := host
|
||||
|
||||
// Handle named socket activation (fdname/name, fdgramname/name)
|
||||
if strings.HasPrefix(network, "fdname") || strings.HasPrefix(network, "fdgramname") {
|
||||
fdNum, err := getFdByName(host)
|
||||
if err != nil {
|
||||
return NetworkAddress{}, fmt.Errorf("named socket activation: %v", err)
|
||||
}
|
||||
fdAddr = strconv.Itoa(fdNum)
|
||||
|
||||
// Normalize network to standard fd/fdgram
|
||||
if strings.HasPrefix(network, "fdname") {
|
||||
network = "fd"
|
||||
} else {
|
||||
network = "fdgram"
|
||||
}
|
||||
}
|
||||
|
||||
return NetworkAddress{
|
||||
Network: network,
|
||||
Host: host,
|
||||
Host: fdAddr,
|
||||
}, nil
|
||||
}
|
||||
var start, end uint64
|
||||
@@ -382,7 +462,7 @@ func SplitNetworkAddress(a string) (network, host, port string, err error) {
|
||||
a = afterSlash
|
||||
if IsUnixNetwork(network) || IsFdNetwork(network) {
|
||||
host = a
|
||||
return
|
||||
return network, host, port, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,7 +482,7 @@ func SplitNetworkAddress(a string) (network, host, port string, err error) {
|
||||
err = errors.Join(firstErr, err)
|
||||
}
|
||||
|
||||
return
|
||||
return network, host, port, err
|
||||
}
|
||||
|
||||
// JoinNetworkAddress combines network, host, and port into a single
|
||||
@@ -430,6 +510,7 @@ func JoinNetworkAddress(network, host, port string) string {
|
||||
// address instead.
|
||||
//
|
||||
// NOTE: This API is EXPERIMENTAL and may be changed or removed.
|
||||
// NOTE: user should close the returned listener twice, once to stop accepting new connections, the second time to free up the packet conn.
|
||||
func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config) (http3.QUICListener, error) {
|
||||
lnKey := listenerKey("quic"+na.Network, na.JoinHostPort(portOffset))
|
||||
|
||||
@@ -466,7 +547,7 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config
|
||||
http3.ConfigureTLSConfig(quicTlsConfig),
|
||||
&quic.Config{
|
||||
Allow0RTT: true,
|
||||
Tracer: qlog.DefaultConnectionTracer,
|
||||
Tracer: h3qlog.DefaultConnectionTracer,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -626,6 +707,7 @@ func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error)
|
||||
func (fcql *fakeCloseQuicListener) Close() error {
|
||||
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
|
||||
fcql.contextCancel()
|
||||
} else if atomic.CompareAndSwapInt32(&fcql.closed, 1, 2) {
|
||||
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
@@ -652,3 +653,286 @@ func TestSplitUnixSocketPermissionsBits(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetFdByName tests the getFdByName function for systemd socket activation.
|
||||
func TestGetFdByName(t *testing.T) {
|
||||
// Save original environment
|
||||
originalFdNames := os.Getenv("LISTEN_FDNAMES")
|
||||
|
||||
// Restore environment after test
|
||||
defer func() {
|
||||
if originalFdNames != "" {
|
||||
os.Setenv("LISTEN_FDNAMES", originalFdNames)
|
||||
} else {
|
||||
os.Unsetenv("LISTEN_FDNAMES")
|
||||
}
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fdNames string
|
||||
socketName string
|
||||
expectedFd int
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "simple http socket",
|
||||
fdNames: "http",
|
||||
socketName: "http",
|
||||
expectedFd: 3,
|
||||
},
|
||||
{
|
||||
name: "multiple different sockets - first",
|
||||
fdNames: "http:https:dns",
|
||||
socketName: "http",
|
||||
expectedFd: 3,
|
||||
},
|
||||
{
|
||||
name: "multiple different sockets - second",
|
||||
fdNames: "http:https:dns",
|
||||
socketName: "https",
|
||||
expectedFd: 4,
|
||||
},
|
||||
{
|
||||
name: "multiple different sockets - third",
|
||||
fdNames: "http:https:dns",
|
||||
socketName: "dns",
|
||||
expectedFd: 5,
|
||||
},
|
||||
{
|
||||
name: "duplicate names - first occurrence (no index)",
|
||||
fdNames: "web:web:api",
|
||||
socketName: "web",
|
||||
expectedFd: 3,
|
||||
},
|
||||
{
|
||||
name: "duplicate names - first occurrence (explicit index 0)",
|
||||
fdNames: "web:web:api",
|
||||
socketName: "web:0",
|
||||
expectedFd: 3,
|
||||
},
|
||||
{
|
||||
name: "duplicate names - second occurrence (index 1)",
|
||||
fdNames: "web:web:api",
|
||||
socketName: "web:1",
|
||||
expectedFd: 4,
|
||||
},
|
||||
{
|
||||
name: "complex duplicates - first api",
|
||||
fdNames: "web:api:web:api:dns",
|
||||
socketName: "api:0",
|
||||
expectedFd: 4,
|
||||
},
|
||||
{
|
||||
name: "complex duplicates - second api",
|
||||
fdNames: "web:api:web:api:dns",
|
||||
socketName: "api:1",
|
||||
expectedFd: 6,
|
||||
},
|
||||
{
|
||||
name: "complex duplicates - first web",
|
||||
fdNames: "web:api:web:api:dns",
|
||||
socketName: "web:0",
|
||||
expectedFd: 3,
|
||||
},
|
||||
{
|
||||
name: "complex duplicates - second web",
|
||||
fdNames: "web:api:web:api:dns",
|
||||
socketName: "web:1",
|
||||
expectedFd: 5,
|
||||
},
|
||||
{
|
||||
name: "socket not found",
|
||||
fdNames: "http:https",
|
||||
socketName: "missing",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "empty socket name",
|
||||
fdNames: "http",
|
||||
socketName: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "missing LISTEN_FDNAMES",
|
||||
fdNames: "",
|
||||
socketName: "http",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "index out of range",
|
||||
fdNames: "web:web",
|
||||
socketName: "web:2",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "negative index",
|
||||
fdNames: "web",
|
||||
socketName: "web:-1",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid index format",
|
||||
fdNames: "web",
|
||||
socketName: "web:abc",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "too many colons",
|
||||
fdNames: "web",
|
||||
socketName: "web:0:extra",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Set up environment
|
||||
if tc.fdNames != "" {
|
||||
os.Setenv("LISTEN_FDNAMES", tc.fdNames)
|
||||
} else {
|
||||
os.Unsetenv("LISTEN_FDNAMES")
|
||||
}
|
||||
|
||||
// Test the function
|
||||
fd, err := getFdByName(tc.socketName)
|
||||
|
||||
if tc.expectError {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error but got none")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error but got: %v", err)
|
||||
}
|
||||
if fd != tc.expectedFd {
|
||||
t.Errorf("Expected FD %d but got %d", tc.expectedFd, fd)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseNetworkAddressFdName tests parsing of fdname and fdgramname addresses.
|
||||
func TestParseNetworkAddressFdName(t *testing.T) {
|
||||
// Save and restore environment
|
||||
originalFdNames := os.Getenv("LISTEN_FDNAMES")
|
||||
defer func() {
|
||||
if originalFdNames != "" {
|
||||
os.Setenv("LISTEN_FDNAMES", originalFdNames)
|
||||
} else {
|
||||
os.Unsetenv("LISTEN_FDNAMES")
|
||||
}
|
||||
}()
|
||||
|
||||
// Set up test environment
|
||||
os.Setenv("LISTEN_FDNAMES", "http:https:dns")
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
expectAddr NetworkAddress
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
input: "fdname/http",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "fd",
|
||||
Host: "3",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "fdname/https",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "fd",
|
||||
Host: "4",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "fdname/dns",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "fd",
|
||||
Host: "5",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "fdname/http:0",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "fd",
|
||||
Host: "3",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "fdname/https:0",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "fd",
|
||||
Host: "4",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "fdgramname/http",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "fdgram",
|
||||
Host: "3",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "fdgramname/https",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "fdgram",
|
||||
Host: "4",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "fdgramname/http:0",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "fdgram",
|
||||
Host: "3",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "fdname/nonexistent",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
input: "fdgramname/nonexistent",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
input: "fdname/http:99",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
input: "fdname/invalid:abc",
|
||||
expectErr: true,
|
||||
},
|
||||
// Test that old fd/N syntax still works
|
||||
{
|
||||
input: "fd/7",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "fd",
|
||||
Host: "7",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "fdgram/8",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "fdgram",
|
||||
Host: "8",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range tests {
|
||||
actualAddr, err := ParseNetworkAddress(tc.input)
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("Test %d (%s): Expected error but got none", i, tc.input)
|
||||
}
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("Test %d (%s): Expected no error but got: %v", i, tc.input, err)
|
||||
}
|
||||
if !tc.expectErr && !reflect.DeepEqual(tc.expectAddr, actualAddr) {
|
||||
t.Errorf("Test %d (%s): Expected %+v but got %+v", i, tc.input, tc.expectAddr, actualAddr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -28,6 +28,8 @@ import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/internal"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -190,6 +192,13 @@ func (logging *Logging) setupNewDefault(ctx Context) error {
|
||||
)
|
||||
}
|
||||
|
||||
// if we had a buffered core, flush its contents ASAP
|
||||
// before we try to log anything else, so the order of
|
||||
// logs is preserved
|
||||
if oldBufferCore, ok := oldDefault.logger.Core().(*internal.LogBufferCore); ok {
|
||||
oldBufferCore.FlushTo(newDefault.logger)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -773,6 +782,21 @@ func Log() *zap.Logger {
|
||||
return defaultLogger.logger
|
||||
}
|
||||
|
||||
// BufferedLog sets the default logger to one that buffers
|
||||
// logs before a config is loaded.
|
||||
// Returns the buffered logger, the original default logger
|
||||
// (for flushing on errors), and the buffer core so that the
|
||||
// caller can flush the logs after the config is loaded or
|
||||
// fails to load.
|
||||
func BufferedLog() (*zap.Logger, *zap.Logger, *internal.LogBufferCore) {
|
||||
defaultLoggerMu.Lock()
|
||||
defer defaultLoggerMu.Unlock()
|
||||
origLogger := defaultLogger.logger
|
||||
bufferCore := internal.NewLogBufferCore(zap.InfoLevel)
|
||||
defaultLogger.logger = zap.New(bufferCore)
|
||||
return defaultLogger.logger, origLogger, bufferCore
|
||||
}
|
||||
|
||||
var (
|
||||
coloringEnabled = os.Getenv("NO_COLOR") == "" && os.Getenv("TERM") != "xterm-mono"
|
||||
defaultLogger, _ = newDefaultProductionLog()
|
||||
|
||||
+3
-1
@@ -345,9 +345,11 @@ func StrictUnmarshalJSON(data []byte, v any) error {
|
||||
return dec.Decode(v)
|
||||
}
|
||||
|
||||
var JSONRawMessageType = reflect.TypeFor[json.RawMessage]()
|
||||
|
||||
// isJSONRawMessage returns true if the type is encoding/json.RawMessage.
|
||||
func isJSONRawMessage(typ reflect.Type) bool {
|
||||
return typ.PkgPath() == "encoding/json" && typ.Name() == "RawMessage"
|
||||
return typ == JSONRawMessageType
|
||||
}
|
||||
|
||||
// isModuleMapType returns true if the type is map[string]json.RawMessage.
|
||||
|
||||
+106
-76
@@ -28,7 +28,6 @@ import (
|
||||
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyevents"
|
||||
@@ -151,6 +150,11 @@ type App struct {
|
||||
logger *zap.Logger
|
||||
tlsApp *caddytls.TLS
|
||||
|
||||
// stopped indicates whether the app has stopped
|
||||
// It can only happen if it has started successfully in the first place.
|
||||
// Otherwise, Cleanup will call Stop to clean up resources.
|
||||
stopped bool
|
||||
|
||||
// used temporarily between phases 1 and 2 of auto HTTPS
|
||||
allCertDomains map[string]struct{}
|
||||
}
|
||||
@@ -166,13 +170,15 @@ func (App) CaddyModule() caddy.ModuleInfo {
|
||||
// Provision sets up the app.
|
||||
func (app *App) Provision(ctx caddy.Context) error {
|
||||
// store some references
|
||||
app.logger = ctx.Logger()
|
||||
app.ctx = ctx
|
||||
|
||||
// provision TLS and events apps
|
||||
tlsAppIface, err := ctx.App("tls")
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting tls app: %v", err)
|
||||
}
|
||||
app.tlsApp = tlsAppIface.(*caddytls.TLS)
|
||||
app.ctx = ctx
|
||||
app.logger = ctx.Logger()
|
||||
|
||||
eventsAppIface, err := ctx.App("events")
|
||||
if err != nil {
|
||||
@@ -192,6 +198,8 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||
if app.Metrics != nil {
|
||||
app.Metrics.init = sync.Once{}
|
||||
app.Metrics.httpMetrics = &httpMetrics{}
|
||||
// Scan config for allowed hosts to prevent cardinality explosion
|
||||
app.Metrics.scanConfigForHosts(app)
|
||||
}
|
||||
// prepare each server
|
||||
oldContext := ctx.Context
|
||||
@@ -231,15 +239,6 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||
for _, srvProtocol := range srv.Protocols {
|
||||
srvProtocolsUnique[srvProtocol] = struct{}{}
|
||||
}
|
||||
_, h1ok := srvProtocolsUnique["h1"]
|
||||
_, h2ok := srvProtocolsUnique["h2"]
|
||||
_, h2cok := srvProtocolsUnique["h2c"]
|
||||
|
||||
// the Go standard library does not let us serve only HTTP/2 using
|
||||
// http.Server; we would probably need to write our own server
|
||||
if !h1ok && (h2ok || h2cok) {
|
||||
return fmt.Errorf("server %s: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName)
|
||||
}
|
||||
|
||||
if srv.ListenProtocols != nil {
|
||||
if len(srv.ListenProtocols) != len(srv.Listen) {
|
||||
@@ -273,19 +272,6 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
lnProtocolsIncludeUnique := map[string]struct{}{}
|
||||
for _, lnProtocol := range lnProtocolsInclude {
|
||||
lnProtocolsIncludeUnique[lnProtocol] = struct{}{}
|
||||
}
|
||||
_, h1ok := lnProtocolsIncludeUnique["h1"]
|
||||
_, h2ok := lnProtocolsIncludeUnique["h2"]
|
||||
_, h2cok := lnProtocolsIncludeUnique["h2c"]
|
||||
|
||||
// check if any listener protocols contain h2 or h2c without h1
|
||||
if !h1ok && (h2ok || h2cok) {
|
||||
return fmt.Errorf("server %s, listener %d: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName, i)
|
||||
}
|
||||
|
||||
srv.ListenProtocols[i] = lnProtocolsInclude
|
||||
}
|
||||
}
|
||||
@@ -443,6 +429,25 @@ func (app *App) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeTLSALPN(srv *Server, target string) {
|
||||
for _, cp := range srv.TLSConnPolicies {
|
||||
// the TLSConfig was already provisioned, so... manually remove it
|
||||
for i, np := range cp.TLSConfig.NextProtos {
|
||||
if np == target {
|
||||
cp.TLSConfig.NextProtos = append(cp.TLSConfig.NextProtos[:i], cp.TLSConfig.NextProtos[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
// remove it from the parent connection policy too, just to keep things tidy
|
||||
for i, alpn := range cp.ALPN {
|
||||
if alpn == target {
|
||||
cp.ALPN = append(cp.ALPN[:i], cp.ALPN[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start runs the app. It finishes automatic HTTPS if enabled,
|
||||
// including management of certificates.
|
||||
func (app *App) Start() error {
|
||||
@@ -461,32 +466,44 @@ func (app *App) Start() error {
|
||||
MaxHeaderBytes: srv.MaxHeaderBytes,
|
||||
Handler: srv,
|
||||
ErrorLog: serverLogger,
|
||||
Protocols: new(http.Protocols),
|
||||
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
|
||||
return context.WithValue(ctx, ConnCtxKey, c)
|
||||
if nc, ok := c.(interface{ tlsNetConn() net.Conn }); ok {
|
||||
getTlsConStateFunc := sync.OnceValue(func() *tls.ConnectionState {
|
||||
tlsConnState := nc.tlsNetConn().(connectionStater).ConnectionState()
|
||||
return &tlsConnState
|
||||
})
|
||||
ctx = context.WithValue(ctx, tlsConnectionStateFuncCtxKey, getTlsConStateFunc)
|
||||
}
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
h2server := new(http2.Server)
|
||||
|
||||
// disable HTTP/2, which we enabled by default during provisioning
|
||||
if !srv.protocol("h2") {
|
||||
srv.server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
|
||||
for _, cp := range srv.TLSConnPolicies {
|
||||
// the TLSConfig was already provisioned, so... manually remove it
|
||||
for i, np := range cp.TLSConfig.NextProtos {
|
||||
if np == "h2" {
|
||||
cp.TLSConfig.NextProtos = append(cp.TLSConfig.NextProtos[:i], cp.TLSConfig.NextProtos[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
// remove it from the parent connection policy too, just to keep things tidy
|
||||
for i, alpn := range cp.ALPN {
|
||||
if alpn == "h2" {
|
||||
cp.ALPN = append(cp.ALPN[:i], cp.ALPN[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
removeTLSALPN(srv, "h2")
|
||||
}
|
||||
if !srv.protocol("h1") {
|
||||
removeTLSALPN(srv, "http/1.1")
|
||||
}
|
||||
|
||||
// configure the http versions the server will serve
|
||||
if srv.protocol("h1") {
|
||||
srv.server.Protocols.SetHTTP1(true)
|
||||
}
|
||||
|
||||
if srv.protocol("h2") || srv.protocol("h2c") {
|
||||
// skip setting h2 because if NextProtos is present, it's list of alpn versions will take precedence.
|
||||
// it will always be present because http2.ConfigureServer will populate that field
|
||||
// enabling h2c because some listener wrapper will wrap the connection that is no longer *tls.Conn
|
||||
// However, we need to handle the case that if the connection is h2c but h2c is not enabled. We identify
|
||||
// this type of connection by checking if it's behind a TLS listener wrapper or if it implements tls.ConnectionState.
|
||||
srv.server.Protocols.SetUnencryptedHTTP2(true)
|
||||
// when h2c is enabled but h2 disabled, we already removed h2 from NextProtos
|
||||
// the handshake will never succeed with h2
|
||||
// http2.ConfigureServer will enable the server to handle both h2 and h2c
|
||||
h2server := new(http2.Server)
|
||||
//nolint:errcheck
|
||||
http2.ConfigureServer(srv.server, h2server)
|
||||
}
|
||||
@@ -496,11 +513,6 @@ func (app *App) Start() error {
|
||||
tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx)
|
||||
srv.configureServer(srv.server)
|
||||
|
||||
// enable H2C if configured
|
||||
if srv.protocol("h2c") {
|
||||
srv.server.Handler = h2c.NewHandler(srv, h2server)
|
||||
}
|
||||
|
||||
for lnIndex, lnAddr := range srv.Listen {
|
||||
listenAddr, err := caddy.ParseNetworkAddress(lnAddr)
|
||||
if err != nil {
|
||||
@@ -533,8 +545,10 @@ func (app *App) Start() error {
|
||||
// create the listener for this socket
|
||||
lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{
|
||||
KeepAliveConfig: net.KeepAliveConfig{
|
||||
Enable: srv.KeepAliveInterval != 0,
|
||||
Enable: srv.KeepAliveInterval >= 0,
|
||||
Interval: time.Duration(srv.KeepAliveInterval),
|
||||
Idle: time.Duration(srv.KeepAliveIdle),
|
||||
Count: srv.KeepAliveCount,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -565,15 +579,13 @@ func (app *App) Start() error {
|
||||
ln = srv.listenerWrappers[i].WrapListener(ln)
|
||||
}
|
||||
|
||||
// handle http2 if use tls listener wrapper
|
||||
if h2ok {
|
||||
http2lnWrapper := &http2Listener{
|
||||
Listener: ln,
|
||||
server: srv.server,
|
||||
h2server: h2server,
|
||||
}
|
||||
srv.h2listeners = append(srv.h2listeners, http2lnWrapper)
|
||||
ln = http2lnWrapper
|
||||
// check if the connection is h2c
|
||||
ln = &http2Listener{
|
||||
useTLS: useTLS,
|
||||
useH1: h1ok,
|
||||
useH2: h2ok || h2cok,
|
||||
Listener: ln,
|
||||
logger: app.logger,
|
||||
}
|
||||
|
||||
// if binding to port 0, the OS chooses a port for us;
|
||||
@@ -591,11 +603,8 @@ func (app *App) Start() error {
|
||||
|
||||
srv.listeners = append(srv.listeners, ln)
|
||||
|
||||
// enable HTTP/1 if configured
|
||||
if h1ok {
|
||||
//nolint:errcheck
|
||||
go srv.server.Serve(ln)
|
||||
}
|
||||
//nolint:errcheck
|
||||
go srv.server.Serve(ln)
|
||||
}
|
||||
|
||||
if h2ok && !useTLS {
|
||||
@@ -708,6 +717,11 @@ func (app *App) Stop() error {
|
||||
defer finishedShutdown.Done()
|
||||
startedShutdown.Done()
|
||||
|
||||
// possible if server failed to Start
|
||||
if server.server == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := server.server.Shutdown(ctx); err != nil {
|
||||
app.logger.Error("server shutdown",
|
||||
zap.Error(err),
|
||||
@@ -722,31 +736,36 @@ func (app *App) Stop() error {
|
||||
return
|
||||
}
|
||||
|
||||
// closing quic listeners won't affect accepted connections now
|
||||
// so like stdlib, close listeners first, but keep the net.PacketConns open
|
||||
for _, h3ln := range server.quicListeners {
|
||||
if err := h3ln.Close(); err != nil {
|
||||
app.logger.Error("http3 listener close",
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := server.h3server.Shutdown(ctx); err != nil {
|
||||
app.logger.Error("HTTP/3 server shutdown",
|
||||
zap.Error(err),
|
||||
zap.Strings("addresses", server.Listen))
|
||||
}
|
||||
}
|
||||
stopH2Listener := func(server *Server) {
|
||||
defer finishedShutdown.Done()
|
||||
startedShutdown.Done()
|
||||
|
||||
for i, s := range server.h2listeners {
|
||||
if err := s.Shutdown(ctx); err != nil {
|
||||
app.logger.Error("http2 listener shutdown",
|
||||
zap.Error(err),
|
||||
zap.Int("index", i))
|
||||
// close the underlying net.PacketConns now
|
||||
// see the comment for ListenQUIC
|
||||
for _, h3ln := range server.quicListeners {
|
||||
if err := h3ln.Close(); err != nil {
|
||||
app.logger.Error("http3 listener close socket",
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, server := range app.Servers {
|
||||
startedShutdown.Add(3)
|
||||
finishedShutdown.Add(3)
|
||||
startedShutdown.Add(2)
|
||||
finishedShutdown.Add(2)
|
||||
go stopServer(server)
|
||||
go stopH3Server(server)
|
||||
go stopH2Listener(server)
|
||||
}
|
||||
|
||||
// block until all the goroutines have been run by the scheduler;
|
||||
@@ -773,9 +792,20 @@ func (app *App) Stop() error {
|
||||
}
|
||||
}
|
||||
|
||||
app.stopped = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup will close remaining listeners if they still remain
|
||||
// because some of the servers fail to start.
|
||||
// It simply calls Stop because Stop won't be called when Start fails.
|
||||
func (app *App) Cleanup() error {
|
||||
if app.stopped {
|
||||
return nil
|
||||
}
|
||||
return app.Stop()
|
||||
}
|
||||
|
||||
func (app *App) httpPort() int {
|
||||
if app.HTTPPort == 0 {
|
||||
return DefaultHTTPPort
|
||||
|
||||
@@ -265,6 +265,22 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
||||
}
|
||||
}
|
||||
|
||||
// if all servers have auto_https disabled and no domains need certs,
|
||||
// skip the rest of the TLS automation setup to avoid creating
|
||||
// unnecessary PKI infrastructure and automation policies
|
||||
allServersDisabled := true
|
||||
for _, srv := range app.Servers {
|
||||
if srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled {
|
||||
allServersDisabled = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allServersDisabled && len(uniqueDomainsForCerts) == 0 {
|
||||
logger.Debug("all servers have automatic HTTPS disabled and no domains need certificates, skipping TLS automation setup")
|
||||
return nil
|
||||
}
|
||||
|
||||
// we now have a list of all the unique names for which we need certs
|
||||
var internal, tailscale []string
|
||||
uniqueDomainsLoop:
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyauth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(Argon2idHash{})
|
||||
}
|
||||
|
||||
const (
|
||||
argon2idName = "argon2id"
|
||||
defaultArgon2idTime = 1
|
||||
defaultArgon2idMemory = 46 * 1024
|
||||
defaultArgon2idThreads = 1
|
||||
defaultArgon2idKeylen = 32
|
||||
defaultSaltLength = 16
|
||||
)
|
||||
|
||||
// Argon2idHash implements the Argon2id password hashing.
|
||||
type Argon2idHash struct {
|
||||
salt []byte
|
||||
time uint32
|
||||
memory uint32
|
||||
threads uint8
|
||||
keyLen uint32
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (Argon2idHash) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "http.authentication.hashes.argon2id",
|
||||
New: func() caddy.Module { return new(Argon2idHash) },
|
||||
}
|
||||
}
|
||||
|
||||
// Compare checks if the plaintext password matches the given Argon2id hash.
|
||||
func (Argon2idHash) Compare(hashed, plaintext []byte) (bool, error) {
|
||||
argHash, storedKey, err := DecodeHash(hashed)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
computedKey := argon2.IDKey(
|
||||
plaintext,
|
||||
argHash.salt,
|
||||
argHash.time,
|
||||
argHash.memory,
|
||||
argHash.threads,
|
||||
argHash.keyLen,
|
||||
)
|
||||
|
||||
return subtle.ConstantTimeCompare(storedKey, computedKey) == 1, nil
|
||||
}
|
||||
|
||||
// Hash generates an Argon2id hash of the given plaintext using the configured parameters and salt.
|
||||
func (b Argon2idHash) Hash(plaintext []byte) ([]byte, error) {
|
||||
if b.salt == nil {
|
||||
s, err := generateSalt(defaultSaltLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.salt = s
|
||||
}
|
||||
|
||||
key := argon2.IDKey(
|
||||
plaintext,
|
||||
b.salt,
|
||||
b.time,
|
||||
b.memory,
|
||||
b.threads,
|
||||
b.keyLen,
|
||||
)
|
||||
|
||||
hash := fmt.Sprintf(
|
||||
"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||||
argon2.Version,
|
||||
b.memory,
|
||||
b.time,
|
||||
b.threads,
|
||||
base64.RawStdEncoding.EncodeToString(b.salt),
|
||||
base64.RawStdEncoding.EncodeToString(key),
|
||||
)
|
||||
|
||||
return []byte(hash), nil
|
||||
}
|
||||
|
||||
// DecodeHash parses an Argon2id PHC string into an Argon2idHash struct and returns the struct along with the derived key.
|
||||
func DecodeHash(hash []byte) (*Argon2idHash, []byte, error) {
|
||||
parts := strings.Split(string(hash), "$")
|
||||
if len(parts) != 6 {
|
||||
return nil, nil, fmt.Errorf("invalid hash format")
|
||||
}
|
||||
|
||||
if parts[1] != argon2idName {
|
||||
return nil, nil, fmt.Errorf("unsupported variant: %s", parts[1])
|
||||
}
|
||||
|
||||
version, err := strconv.Atoi(strings.TrimPrefix(parts[2], "v="))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid version: %w", err)
|
||||
}
|
||||
if version != argon2.Version {
|
||||
return nil, nil, fmt.Errorf("incompatible version: %d", version)
|
||||
}
|
||||
|
||||
params := strings.Split(parts[3], ",")
|
||||
if len(params) != 3 {
|
||||
return nil, nil, fmt.Errorf("invalid parameters")
|
||||
}
|
||||
|
||||
mem, err := strconv.ParseUint(strings.TrimPrefix(params[0], "m="), 10, 32)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid memory parameter: %w", err)
|
||||
}
|
||||
|
||||
iter, err := strconv.ParseUint(strings.TrimPrefix(params[1], "t="), 10, 32)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid iterations parameter: %w", err)
|
||||
}
|
||||
|
||||
threads, err := strconv.ParseUint(strings.TrimPrefix(params[2], "p="), 10, 8)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid parallelism parameter: %w", err)
|
||||
}
|
||||
|
||||
salt, err := base64.RawStdEncoding.Strict().DecodeString(parts[4])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("decode salt: %w", err)
|
||||
}
|
||||
|
||||
key, err := base64.RawStdEncoding.Strict().DecodeString(parts[5])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("decode key: %w", err)
|
||||
}
|
||||
|
||||
return &Argon2idHash{
|
||||
salt: salt,
|
||||
time: uint32(iter),
|
||||
memory: uint32(mem),
|
||||
threads: uint8(threads),
|
||||
keyLen: uint32(len(key)),
|
||||
}, key, nil
|
||||
}
|
||||
|
||||
// FakeHash returns a constant fake hash for timing attacks mitigation.
|
||||
func (Argon2idHash) FakeHash() []byte {
|
||||
// hashed with the following command:
|
||||
// caddy hash-password --plaintext "antitiming" --algorithm "argon2id"
|
||||
return []byte("$argon2id$v=19$m=47104,t=1,p=1$P2nzckEdTZ3bxCiBCkRTyA$xQL3Z32eo5jKl7u5tcIsnEKObYiyNZQQf5/4sAau6Pg")
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ Comparer = (*Argon2idHash)(nil)
|
||||
_ Hasher = (*Argon2idHash)(nil)
|
||||
)
|
||||
|
||||
func generateSalt(length int) ([]byte, error) {
|
||||
salt := make([]byte, length)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate salt: %w", err)
|
||||
}
|
||||
return salt, nil
|
||||
}
|
||||
@@ -15,6 +15,8 @@
|
||||
package caddyauth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
@@ -24,8 +26,18 @@ func init() {
|
||||
caddy.RegisterModule(BcryptHash{})
|
||||
}
|
||||
|
||||
// defaultBcryptCost cost 14 strikes a solid balance between security, usability, and hardware performance
|
||||
const (
|
||||
bcryptName = "bcrypt"
|
||||
defaultBcryptCost = 14
|
||||
)
|
||||
|
||||
// BcryptHash implements the bcrypt hash.
|
||||
type BcryptHash struct{}
|
||||
type BcryptHash struct {
|
||||
// cost is the bcrypt hashing difficulty factor (work factor).
|
||||
// Higher values increase computation time and security.
|
||||
cost int
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (BcryptHash) CaddyModule() caddy.ModuleInfo {
|
||||
@@ -38,7 +50,7 @@ func (BcryptHash) CaddyModule() caddy.ModuleInfo {
|
||||
// Compare compares passwords.
|
||||
func (BcryptHash) Compare(hashed, plaintext []byte) (bool, error) {
|
||||
err := bcrypt.CompareHashAndPassword(hashed, plaintext)
|
||||
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
@@ -48,8 +60,13 @@ func (BcryptHash) Compare(hashed, plaintext []byte) (bool, error) {
|
||||
}
|
||||
|
||||
// Hash hashes plaintext using a random salt.
|
||||
func (BcryptHash) Hash(plaintext []byte) ([]byte, error) {
|
||||
return bcrypt.GenerateFromPassword(plaintext, 14)
|
||||
func (b BcryptHash) Hash(plaintext []byte) ([]byte, error) {
|
||||
cost := b.cost
|
||||
if cost < bcrypt.MinCost || cost > bcrypt.MaxCost {
|
||||
cost = defaultBcryptCost
|
||||
}
|
||||
|
||||
return bcrypt.GenerateFromPassword(plaintext, cost)
|
||||
}
|
||||
|
||||
// FakeHash returns a fake hash.
|
||||
@@ -60,7 +60,8 @@ func (Authentication) CaddyModule() caddy.ModuleInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets up a.
|
||||
// Provision sets up an Authentication module by initializing its logger,
|
||||
// loading and registering all configured authentication providers.
|
||||
func (a *Authentication) Provision(ctx caddy.Context) error {
|
||||
a.logger = ctx.Logger()
|
||||
a.Providers = make(map[string]Authenticator)
|
||||
|
||||
@@ -51,7 +51,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||
var hashName string
|
||||
switch len(args) {
|
||||
case 0:
|
||||
hashName = "bcrypt"
|
||||
hashName = bcryptName
|
||||
case 1:
|
||||
hashName = args[0]
|
||||
case 2:
|
||||
@@ -62,8 +62,10 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||
}
|
||||
|
||||
switch hashName {
|
||||
case "bcrypt":
|
||||
case bcryptName:
|
||||
cmp = BcryptHash{}
|
||||
case argon2idName:
|
||||
cmp = Argon2idHash{}
|
||||
default:
|
||||
return nil, h.Errf("unrecognized hash algorithm: %s", hashName)
|
||||
}
|
||||
|
||||
@@ -32,21 +32,55 @@ import (
|
||||
func init() {
|
||||
caddycmd.RegisterCommand(caddycmd.Command{
|
||||
Name: "hash-password",
|
||||
Usage: "[--plaintext <password>] [--algorithm <name>]",
|
||||
Usage: "[--plaintext <password>] [--algorithm <argon2id|bcrypt>] [--bcrypt-cost <difficulty>] [--argon2id-time <iterations>] [--argon2id-memory <KiB>] [--argon2id-threads <n>] [--argon2id-keylen <bytes>]",
|
||||
Short: "Hashes a password and writes base64",
|
||||
Long: `
|
||||
Convenient way to hash a plaintext password. The resulting
|
||||
hash is written to stdout as a base64 string.
|
||||
|
||||
--plaintext, when omitted, will be read from stdin. If
|
||||
Caddy is attached to a controlling tty, the plaintext will
|
||||
not be echoed.
|
||||
--plaintext
|
||||
The password to hash. If omitted, it will be read from stdin.
|
||||
If Caddy is attached to a controlling TTY, the input will not be echoed.
|
||||
|
||||
--algorithm currently only supports 'bcrypt', and is the default.
|
||||
--algorithm
|
||||
Selects the hashing algorithm. Valid options are:
|
||||
* 'argon2id' (recommended for modern security)
|
||||
* 'bcrypt' (legacy, slower, configurable cost)
|
||||
|
||||
bcrypt-specific parameters:
|
||||
|
||||
--bcrypt-cost
|
||||
Sets the bcrypt hashing difficulty. Higher values increase security by
|
||||
making the hash computation slower and more CPU-intensive.
|
||||
Must be within the valid range [bcrypt.MinCost, bcrypt.MaxCost].
|
||||
If omitted or invalid, the default cost is used.
|
||||
|
||||
Argon2id-specific parameters:
|
||||
|
||||
--argon2id-time
|
||||
Number of iterations to perform. Increasing this makes
|
||||
hashing slower and more resistant to brute-force attacks.
|
||||
|
||||
--argon2id-memory
|
||||
Amount of memory to use during hashing.
|
||||
Larger values increase resistance to GPU/ASIC attacks.
|
||||
|
||||
--argon2id-threads
|
||||
Number of CPU threads to use. Increase for faster hashing
|
||||
on multi-core systems.
|
||||
|
||||
--argon2id-keylen
|
||||
Length of the resulting hash in bytes. Longer keys increase
|
||||
security but slightly increase storage size.
|
||||
`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("plaintext", "p", "", "The plaintext password")
|
||||
cmd.Flags().StringP("algorithm", "a", "bcrypt", "Name of the hash algorithm")
|
||||
cmd.Flags().StringP("algorithm", "a", bcryptName, "Name of the hash algorithm")
|
||||
cmd.Flags().Int("bcrypt-cost", defaultBcryptCost, "Bcrypt hashing cost (only used with 'bcrypt' algorithm)")
|
||||
cmd.Flags().Uint32("argon2id-time", defaultArgon2idTime, "Number of iterations for Argon2id hashing. Increasing this makes the hash slower and more resistant to brute-force attacks.")
|
||||
cmd.Flags().Uint32("argon2id-memory", defaultArgon2idMemory, "Memory to use in KiB for Argon2id hashing. Larger values increase resistance to GPU/ASIC attacks.")
|
||||
cmd.Flags().Uint8("argon2id-threads", defaultArgon2idThreads, "Number of CPU threads to use for Argon2id hashing. Increase for faster hashing on multi-core systems.")
|
||||
cmd.Flags().Uint32("argon2id-keylen", defaultArgon2idKeylen, "Length of the resulting Argon2id hash in bytes. Longer hashes increase security but slightly increase storage size.")
|
||||
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdHashPassword)
|
||||
},
|
||||
})
|
||||
@@ -57,6 +91,7 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
|
||||
|
||||
algorithm := fs.String("algorithm")
|
||||
plaintext := []byte(fs.String("plaintext"))
|
||||
bcryptCost := fs.Int("bcrypt-cost")
|
||||
|
||||
if len(plaintext) == 0 {
|
||||
fd := int(os.Stdin.Fd())
|
||||
@@ -107,8 +142,34 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
|
||||
var hash []byte
|
||||
var hashString string
|
||||
switch algorithm {
|
||||
case "bcrypt":
|
||||
hash, err = BcryptHash{}.Hash(plaintext)
|
||||
case bcryptName:
|
||||
hash, err = BcryptHash{cost: bcryptCost}.Hash(plaintext)
|
||||
hashString = string(hash)
|
||||
case argon2idName:
|
||||
time, err := fs.GetUint32("argon2id-time")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id time parameter: %w", err)
|
||||
}
|
||||
memory, err := fs.GetUint32("argon2id-memory")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id memory parameter: %w", err)
|
||||
}
|
||||
threads, err := fs.GetUint8("argon2id-threads")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id threads parameter: %w", err)
|
||||
}
|
||||
keyLen, err := fs.GetUint32("argon2id-keylen")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id keylen parameter: %w", err)
|
||||
}
|
||||
|
||||
hash, _ = Argon2idHash{
|
||||
time: time,
|
||||
memory: memory,
|
||||
threads: threads,
|
||||
keyLen: keyLen,
|
||||
}.Hash(plaintext)
|
||||
|
||||
hashString = string(hash)
|
||||
default:
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm)
|
||||
|
||||
@@ -665,7 +665,7 @@ func celMatcherJSONMacroExpander(funcName string) parser.MacroExpander {
|
||||
// map literals containing heterogeneous values, in this case string and list
|
||||
// of string.
|
||||
func CELValueToMapStrList(data ref.Val) (map[string][]string, error) {
|
||||
mapStrType := reflect.TypeOf(map[string]any{})
|
||||
mapStrType := reflect.TypeFor[map[string]any]()
|
||||
mapStrRaw, err := data.ConvertToNative(mapStrType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -535,7 +535,7 @@ func BenchmarkMatchExpressionMatch(b *testing.B) {
|
||||
}
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
tc.expression.MatchWithError(req)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -50,7 +50,7 @@ type Encode struct {
|
||||
// Only encode responses that are at least this many bytes long.
|
||||
MinLength int `json:"minimum_length,omitempty"`
|
||||
|
||||
// Only encode responses that match against this ResponseMmatcher.
|
||||
// Only encode responses that match against this ResponseMatcher.
|
||||
// The default is a collection of text-based Content-Type headers.
|
||||
Matcher *caddyhttp.ResponseMatcher `json:"match,omitempty"`
|
||||
|
||||
@@ -92,6 +92,7 @@ func (enc *Encode) Provision(ctx caddy.Context) error {
|
||||
"application/font*",
|
||||
"application/geo+json*",
|
||||
"application/graphql+json*",
|
||||
"application/graphql-response+json*",
|
||||
"application/javascript*",
|
||||
"application/json*",
|
||||
"application/ld+json*",
|
||||
@@ -176,7 +177,17 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
|
||||
break
|
||||
}
|
||||
}
|
||||
return next.ServeHTTP(w, r)
|
||||
|
||||
err := next.ServeHTTP(w, r)
|
||||
// If there was an error, disable encoding completely
|
||||
// This prevents corruption when handle_errors processes the response
|
||||
if err != nil {
|
||||
if ew, ok := w.(*responseWriter); ok {
|
||||
ew.disabled = true
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (enc *Encode) addEncoding(e Encoding) error {
|
||||
@@ -232,6 +243,7 @@ type responseWriter struct {
|
||||
statusCode int
|
||||
wroteHeader bool
|
||||
isConnect bool
|
||||
disabled bool // disable encoding (for error responses)
|
||||
}
|
||||
|
||||
// WriteHeader stores the status to write when the time comes
|
||||
@@ -424,7 +436,14 @@ func (rw *responseWriter) Unwrap() http.ResponseWriter {
|
||||
|
||||
// init should be called before we write a response, if rw.buf has contents.
|
||||
func (rw *responseWriter) init() {
|
||||
// Don't initialize encoder for error responses
|
||||
// This prevents response corruption when handle_errors is used
|
||||
if rw.disabled {
|
||||
return
|
||||
}
|
||||
|
||||
hdr := rw.Header()
|
||||
|
||||
if hdr.Get("Content-Encoding") == "" && isEncodeAllowed(hdr) &&
|
||||
rw.config.Match(rw) {
|
||||
rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
|
||||
@@ -452,8 +471,7 @@ func (rw *responseWriter) init() {
|
||||
|
||||
func hasVaryValue(hdr http.Header, target string) bool {
|
||||
for _, vary := range hdr.Values("Vary") {
|
||||
vals := strings.Split(vary, ",")
|
||||
for _, val := range vals {
|
||||
for val := range strings.SplitSeq(vary, ",") {
|
||||
if strings.EqualFold(strings.TrimSpace(val), target) {
|
||||
return true
|
||||
}
|
||||
@@ -478,7 +496,7 @@ func AcceptedEncodings(r *http.Request, preferredOrder []string) []string {
|
||||
|
||||
prefs := []encodingPreference{}
|
||||
|
||||
for _, accepted := range strings.Split(acceptEncHeader, ",") {
|
||||
for accepted := range strings.SplitSeq(acceptEncHeader, ",") {
|
||||
parts := strings.Split(accepted, ";")
|
||||
encName := strings.ToLower(strings.TrimSpace(parts[0]))
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
func BenchmarkOpenResponseWriter(b *testing.B) {
|
||||
enc := new(Encode)
|
||||
for n := 0; n < b.N; n++ {
|
||||
for b.Loop() {
|
||||
enc.openResponseWriter("test", nil, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +167,8 @@ type FileServer struct {
|
||||
// If set, file Etags will be read from sidecar files
|
||||
// with any of these suffixes, instead of generating
|
||||
// our own Etag.
|
||||
// Keep in mind that the Etag values in the files have to be quoted as per RFC7232.
|
||||
// See https://datatracker.ietf.org/doc/html/rfc7232#section-2.3 for a few examples.
|
||||
EtagFileExtensions []string `json:"etag_file_extensions,omitempty"`
|
||||
|
||||
fsmap caddy.FileSystems
|
||||
@@ -455,7 +457,14 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
|
||||
}
|
||||
defer file.Close()
|
||||
respHeader.Set("Content-Encoding", ae)
|
||||
respHeader.Del("Accept-Ranges")
|
||||
|
||||
// stdlib won't set Content-Length for non-range requests if Content-Encoding is set.
|
||||
// see: https://github.com/caddyserver/caddy/issues/7040
|
||||
// Setting the Range header manually will result in 206 Partial Content.
|
||||
// see: https://github.com/caddyserver/caddy/issues/7250
|
||||
if r.Header.Get("Range") == "" {
|
||||
respHeader.Set("Content-Length", strconv.FormatInt(compressedInfo.Size(), 10))
|
||||
}
|
||||
|
||||
// try to get the etag from pre computed files if an etag suffix list was provided
|
||||
if etag == "" && fsrv.EtagFileExtensions != nil {
|
||||
|
||||
@@ -159,7 +159,7 @@ func (ops *HeaderOps) Provision(_ caddy.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// containsCaddyPlaceholders checks if the string contains Caddy placeholder syntax {key}
|
||||
// containsPlaceholders checks if the string contains Caddy placeholder syntax {key}
|
||||
func containsPlaceholders(s string) bool {
|
||||
openIdx := strings.Index(s, "{")
|
||||
if openIdx == -1 {
|
||||
|
||||
@@ -1,102 +1,131 @@
|
||||
package caddyhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
weakrand "math/rand"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
// http2Listener wraps the listener to solve the following problems:
|
||||
// 1. server h2 natively without using h2c hack when listener handles tls connection but
|
||||
// don't return *tls.Conn
|
||||
// 2. graceful shutdown. the shutdown logic is copied from stdlib http.Server, it's an extra maintenance burden but
|
||||
// whatever, the shutdown logic maybe extracted to be used with h2c graceful shutdown. http2.Server supports graceful shutdown
|
||||
// sending GO_AWAY frame to connected clients, but doesn't track connection status. It requires explicit call of http2.ConfigureServer
|
||||
type http2Listener struct {
|
||||
cnt uint64
|
||||
net.Listener
|
||||
server *http.Server
|
||||
h2server *http2.Server
|
||||
}
|
||||
|
||||
type connectionStateConn interface {
|
||||
net.Conn
|
||||
type connectionStater interface {
|
||||
ConnectionState() tls.ConnectionState
|
||||
}
|
||||
|
||||
// http2Listener wraps the listener to solve the following problems:
|
||||
// 1. prevent genuine h2c connections from succeeding if h2c is not enabled
|
||||
// and the connection doesn't implment connectionStater or the resulting NegotiatedProtocol
|
||||
// isn't http2.
|
||||
// This does allow a connection to pass as tls enabled even if it's not, listener wrappers
|
||||
// can do this.
|
||||
// 2. After wrapping the connection doesn't implement connectionStater, emit a warning so that listener
|
||||
// wrapper authors will hopefully implement it.
|
||||
// 3. check if the connection matches a specific http version. h2/h2c has a distinct preface.
|
||||
type http2Listener struct {
|
||||
useTLS bool
|
||||
useH1 bool
|
||||
useH2 bool
|
||||
net.Listener
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func (h *http2Listener) Accept() (net.Conn, error) {
|
||||
for {
|
||||
conn, err := h.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := h.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if csc, ok := conn.(connectionStateConn); ok {
|
||||
// *tls.Conn will return empty string because it's only populated after handshake is complete
|
||||
if csc.ConnectionState().NegotiatedProtocol == http2.NextProtoTLS {
|
||||
go h.serveHttp2(csc)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// *tls.Conn doesn't need to be wrapped because we already removed unwanted alpns
|
||||
// and handshake won't succeed without mutually supported alpns
|
||||
if tlsConn, ok := conn.(*tls.Conn); ok {
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
_, isConnectionStater := conn.(connectionStater)
|
||||
// emit a warning
|
||||
if h.useTLS && !isConnectionStater {
|
||||
h.logger.Warn("tls is enabled, but listener wrapper returns a connection that doesn't implement connectionStater")
|
||||
} else if !h.useTLS && isConnectionStater {
|
||||
h.logger.Warn("tls is disabled, but listener wrapper returns a connection that implements connectionStater")
|
||||
}
|
||||
|
||||
// if both h1 and h2 are enabled, we don't need to check the preface
|
||||
if h.useH1 && h.useH2 {
|
||||
if isConnectionStater {
|
||||
return tlsStateConn{conn}, nil
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *http2Listener) serveHttp2(csc connectionStateConn) {
|
||||
atomic.AddUint64(&h.cnt, 1)
|
||||
h.runHook(csc, http.StateNew)
|
||||
defer func() {
|
||||
csc.Close()
|
||||
atomic.AddUint64(&h.cnt, ^uint64(0))
|
||||
h.runHook(csc, http.StateClosed)
|
||||
}()
|
||||
h.h2server.ServeConn(csc, &http2.ServeConnOpts{
|
||||
Context: h.server.ConnContext(context.Background(), csc),
|
||||
BaseConfig: h.server,
|
||||
Handler: h.server.Handler,
|
||||
})
|
||||
}
|
||||
|
||||
const shutdownPollIntervalMax = 500 * time.Millisecond
|
||||
|
||||
func (h *http2Listener) Shutdown(ctx context.Context) error {
|
||||
pollIntervalBase := time.Millisecond
|
||||
nextPollInterval := func() time.Duration {
|
||||
// Add 10% jitter.
|
||||
//nolint:gosec
|
||||
interval := pollIntervalBase + time.Duration(weakrand.Intn(int(pollIntervalBase/10)))
|
||||
// Double and clamp for next time.
|
||||
pollIntervalBase *= 2
|
||||
if pollIntervalBase > shutdownPollIntervalMax {
|
||||
pollIntervalBase = shutdownPollIntervalMax
|
||||
}
|
||||
return interval
|
||||
// impossible both are false, either useH1 or useH2 must be true,
|
||||
// or else the listener wouldn't be created
|
||||
h2Conn := &http2Conn{
|
||||
h2Expected: h.useH2,
|
||||
logger: h.logger,
|
||||
Conn: conn,
|
||||
}
|
||||
if isConnectionStater {
|
||||
return tlsStateConn{http2StateConn{h2Conn}}, nil
|
||||
}
|
||||
return h2Conn, nil
|
||||
}
|
||||
|
||||
timer := time.NewTimer(nextPollInterval())
|
||||
defer timer.Stop()
|
||||
for {
|
||||
if atomic.LoadUint64(&h.cnt) == 0 {
|
||||
return nil
|
||||
// tlsStateConn wraps a net.Conn that implements connectionStater to hide that method
|
||||
// we can call netConn to get the original net.Conn and get the tls connection state
|
||||
// golang 1.25 will call that method, and it breaks h2 with connections other than *tls.Conn
|
||||
type tlsStateConn struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (conn tlsStateConn) tlsNetConn() net.Conn {
|
||||
return conn.Conn
|
||||
}
|
||||
|
||||
type http2StateConn struct {
|
||||
*http2Conn
|
||||
}
|
||||
|
||||
func (conn http2StateConn) ConnectionState() tls.ConnectionState {
|
||||
return conn.Conn.(connectionStater).ConnectionState()
|
||||
}
|
||||
|
||||
type http2Conn struct {
|
||||
// current index where the preface should match,
|
||||
// no matching is done if idx is >= len(http2.ClientPreface)
|
||||
idx int
|
||||
// whether the connection is expected to be h2/h2c
|
||||
h2Expected bool
|
||||
// log if one such connection is detected
|
||||
logger *zap.Logger
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (c *http2Conn) Read(p []byte) (int, error) {
|
||||
if c.idx >= len(http2.ClientPreface) {
|
||||
return c.Conn.Read(p)
|
||||
}
|
||||
n, err := c.Conn.Read(p)
|
||||
for i := range n {
|
||||
// first mismatch
|
||||
if p[i] != http2.ClientPreface[c.idx] {
|
||||
// close the connection if h2 is expected
|
||||
if c.h2Expected {
|
||||
c.logger.Debug("h1 connection detected, but h1 is not enabled")
|
||||
_ = c.Conn.Close()
|
||||
return 0, io.EOF
|
||||
}
|
||||
// no need to continue matching anymore
|
||||
c.idx = len(http2.ClientPreface)
|
||||
return n, err
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
timer.Reset(nextPollInterval())
|
||||
c.idx++
|
||||
// matching complete
|
||||
if c.idx == len(http2.ClientPreface) && !c.h2Expected {
|
||||
c.logger.Debug("h2/h2c connection detected, but h2/h2c is not enabled")
|
||||
_ = c.Conn.Close()
|
||||
return 0, io.EOF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *http2Listener) runHook(conn net.Conn, state http.ConnState) {
|
||||
if h.server.ConnState != nil {
|
||||
h.server.ConnState(conn, state)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package intercept
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -175,10 +176,35 @@ func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
|
||||
c.Write(zap.Int("handler", rec.handlerIndex))
|
||||
}
|
||||
|
||||
// pass the request through the response handler routes
|
||||
return rec.handler.Routes.Compile(next).ServeHTTP(w, r)
|
||||
// response recorder doesn't create a new copy of the original headers, they're
|
||||
// present in the original response writer
|
||||
// create a new recorder to see if any response body from the new handler is present,
|
||||
// if not, use the already buffered response body
|
||||
recorder := caddyhttp.NewResponseRecorder(w, nil, nil)
|
||||
if err := rec.handler.Routes.Compile(emptyHandler).ServeHTTP(recorder, r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// no new response status and the status is not 0
|
||||
if recorder.Status() == 0 && rec.Status() != 0 {
|
||||
w.WriteHeader(rec.Status())
|
||||
}
|
||||
|
||||
// no new response body and there is some in the original response
|
||||
// TODO: what if the new response doesn't have a body by design?
|
||||
// see: https://github.com/caddyserver/caddy/pull/6232#issue-2235224400
|
||||
if recorder.Size() == 0 && buf.Len() > 0 {
|
||||
_, err := io.Copy(w, buf)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// this handler does nothing because everything we need is already buffered
|
||||
var emptyHandler caddyhttp.Handler = caddyhttp.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
|
||||
//
|
||||
// intercept [<matcher>] {
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
@@ -109,7 +108,7 @@ func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
[]*cel.Type{cel.ListType(cel.StringType)},
|
||||
// function to convert a constant list of strings to a MatchPath instance.
|
||||
func(data ref.Val) (RequestMatcherWithError, error) {
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
refStringList := stringSliceType
|
||||
strList, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -222,7 +221,7 @@ func (MatchClientIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
[]*cel.Type{cel.ListType(cel.StringType)},
|
||||
// function to convert a constant list of strings to a MatchPath instance.
|
||||
func(data ref.Val) (RequestMatcherWithError, error) {
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
refStringList := stringSliceType
|
||||
strList, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -209,7 +209,7 @@ func errLogValues(err error) (status int, msg string, fields func() []zapcore.Fi
|
||||
zap.String("err_trace", handlerErr.Trace),
|
||||
}
|
||||
}
|
||||
return
|
||||
return status, msg, fields
|
||||
}
|
||||
fields = func() []zapcore.Field {
|
||||
return []zapcore.Field{
|
||||
@@ -218,7 +218,7 @@ func errLogValues(err error) (status int, msg string, fields func() []zapcore.Fi
|
||||
}
|
||||
status = http.StatusInternalServerError
|
||||
msg = err.Error()
|
||||
return
|
||||
return status, msg, fields
|
||||
}
|
||||
|
||||
// ExtraLogFields is a list of extra fields to log with every request.
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"path"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
@@ -373,7 +372,7 @@ func (MatchHost) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
"host_match_request_list",
|
||||
[]*cel.Type{cel.ListType(cel.StringType)},
|
||||
func(data ref.Val) (RequestMatcherWithError, error) {
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
refStringList := stringSliceType
|
||||
strList, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -654,7 +653,7 @@ func (MatchPath) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
[]*cel.Type{cel.ListType(cel.StringType)},
|
||||
// function to convert a constant list of strings to a MatchPath instance.
|
||||
func(data ref.Val) (RequestMatcherWithError, error) {
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
refStringList := stringSliceType
|
||||
strList, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -733,7 +732,7 @@ func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
"path_regexp_request_string_string",
|
||||
[]*cel.Type{cel.StringType, cel.StringType},
|
||||
func(data ref.Val) (RequestMatcherWithError, error) {
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
refStringList := stringSliceType
|
||||
params, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -802,7 +801,7 @@ func (MatchMethod) CELLibrary(_ caddy.Context) (cel.Library, error) {
|
||||
"method_request_list",
|
||||
[]*cel.Type{cel.ListType(cel.StringType)},
|
||||
func(data ref.Val) (RequestMatcherWithError, error) {
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
refStringList := stringSliceType
|
||||
strList, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1173,7 +1172,7 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
"header_regexp_request_string_string",
|
||||
[]*cel.Type{cel.StringType, cel.StringType},
|
||||
func(data ref.Val) (RequestMatcherWithError, error) {
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
refStringList := stringSliceType
|
||||
params, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1196,7 +1195,7 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
"header_regexp_request_string_string_string",
|
||||
[]*cel.Type{cel.StringType, cel.StringType, cel.StringType},
|
||||
func(data ref.Val) (RequestMatcherWithError, error) {
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
refStringList := stringSliceType
|
||||
params, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -947,7 +947,7 @@ func BenchmarkHeaderREMatcher(b *testing.B) {
|
||||
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
|
||||
req = req.WithContext(ctx)
|
||||
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
|
||||
for run := 0; run < b.N; run++ {
|
||||
for b.Loop() {
|
||||
match.MatchWithError(req)
|
||||
}
|
||||
}
|
||||
@@ -992,8 +992,6 @@ func TestVarREMatcher(t *testing.T) {
|
||||
expect: true,
|
||||
},
|
||||
} {
|
||||
i := i // capture range value
|
||||
tc := tc // capture range value
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// compile the regexp and validate its name
|
||||
@@ -1180,8 +1178,7 @@ func BenchmarkLargeHostMatcher(b *testing.B) {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
matcher.MatchWithError(req)
|
||||
}
|
||||
}
|
||||
@@ -1194,8 +1191,7 @@ func BenchmarkHostMatcherWithoutPlaceholder(b *testing.B) {
|
||||
|
||||
match := MatchHost{"localhost"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
match.MatchWithError(req)
|
||||
}
|
||||
}
|
||||
@@ -1212,8 +1208,7 @@ func BenchmarkHostMatcherWithPlaceholder(b *testing.B) {
|
||||
req = req.WithContext(ctx)
|
||||
match := MatchHost{"{env.GO_BENCHMARK_DOMAIN}"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
match.MatchWithError(req)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,60 @@ import (
|
||||
|
||||
// Metrics configures metrics observations.
|
||||
// EXPERIMENTAL and subject to change or removal.
|
||||
//
|
||||
// Example configuration:
|
||||
//
|
||||
// {
|
||||
// "apps": {
|
||||
// "http": {
|
||||
// "metrics": {
|
||||
// "per_host": true,
|
||||
// "allow_catch_all_hosts": false
|
||||
// },
|
||||
// "servers": {
|
||||
// "srv0": {
|
||||
// "routes": [{
|
||||
// "match": [{"host": ["example.com", "www.example.com"]}],
|
||||
// "handle": [{"handler": "static_response", "body": "Hello"}]
|
||||
// }]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// In this configuration:
|
||||
// - Requests to example.com and www.example.com get individual host labels
|
||||
// - All other hosts (e.g., attacker.com) are aggregated under "_other" label
|
||||
// - This prevents unlimited cardinality from arbitrary Host headers
|
||||
type Metrics struct {
|
||||
// Enable per-host metrics. Enabling this option may
|
||||
// incur high-memory consumption, depending on the number of hosts
|
||||
// managed by Caddy.
|
||||
//
|
||||
// CARDINALITY PROTECTION: To prevent unbounded cardinality attacks,
|
||||
// only explicitly configured hosts (via host matchers) are allowed
|
||||
// by default. Other hosts are aggregated under the "_other" label.
|
||||
// See AllowCatchAllHosts to change this behavior.
|
||||
PerHost bool `json:"per_host,omitempty"`
|
||||
|
||||
init sync.Once
|
||||
httpMetrics *httpMetrics `json:"-"`
|
||||
// Allow metrics for catch-all hosts (hosts without explicit configuration).
|
||||
// When false (default), only hosts explicitly configured via host matchers
|
||||
// will get individual metrics labels. All other hosts will be aggregated
|
||||
// under the "_other" label to prevent cardinality explosion.
|
||||
//
|
||||
// This is automatically enabled for HTTPS servers (since certificates provide
|
||||
// some protection against unbounded cardinality), but disabled for HTTP servers
|
||||
// by default to prevent cardinality attacks from arbitrary Host headers.
|
||||
//
|
||||
// Set to true to allow all hosts to get individual metrics (NOT RECOMMENDED
|
||||
// for production environments exposed to the internet).
|
||||
AllowCatchAllHosts bool `json:"allow_catch_all_hosts,omitempty"`
|
||||
|
||||
init sync.Once
|
||||
httpMetrics *httpMetrics
|
||||
allowedHosts map[string]struct{}
|
||||
hasHTTPSServer bool
|
||||
}
|
||||
|
||||
type httpMetrics struct {
|
||||
@@ -101,6 +147,63 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) {
|
||||
}, httpLabels)
|
||||
}
|
||||
|
||||
// scanConfigForHosts scans the HTTP app configuration to build a set of allowed hosts
|
||||
// for metrics collection, similar to how auto-HTTPS scans for domain names.
|
||||
func (m *Metrics) scanConfigForHosts(app *App) {
|
||||
if !m.PerHost {
|
||||
return
|
||||
}
|
||||
|
||||
m.allowedHosts = make(map[string]struct{})
|
||||
m.hasHTTPSServer = false
|
||||
|
||||
for _, srv := range app.Servers {
|
||||
// Check if this server has TLS enabled
|
||||
serverHasTLS := len(srv.TLSConnPolicies) > 0
|
||||
if serverHasTLS {
|
||||
m.hasHTTPSServer = true
|
||||
}
|
||||
|
||||
// Collect hosts from route matchers
|
||||
for _, route := range srv.Routes {
|
||||
for _, matcherSet := range route.MatcherSets {
|
||||
for _, matcher := range matcherSet {
|
||||
if hm, ok := matcher.(*MatchHost); ok {
|
||||
for _, host := range *hm {
|
||||
// Only allow non-fuzzy hosts to prevent unbounded cardinality
|
||||
if !hm.fuzzy(host) {
|
||||
m.allowedHosts[strings.ToLower(host)] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shouldAllowHostMetrics determines if metrics should be collected for the given host.
|
||||
// This implements the cardinality protection by only allowing metrics for:
|
||||
// 1. Explicitly configured hosts
|
||||
// 2. Catch-all requests on HTTPS servers (if AllowCatchAllHosts is true or auto-enabled)
|
||||
// 3. Catch-all requests on HTTP servers only if explicitly allowed
|
||||
func (m *Metrics) shouldAllowHostMetrics(host string, isHTTPS bool) bool {
|
||||
if !m.PerHost {
|
||||
return true // host won't be used in labels anyway
|
||||
}
|
||||
|
||||
normalizedHost := strings.ToLower(host)
|
||||
|
||||
// Always allow explicitly configured hosts
|
||||
if _, exists := m.allowedHosts[normalizedHost]; exists {
|
||||
return true
|
||||
}
|
||||
|
||||
// For catch-all requests (not in allowed hosts)
|
||||
allowCatchAll := m.AllowCatchAllHosts || (isHTTPS && m.hasHTTPSServer)
|
||||
return allowCatchAll
|
||||
}
|
||||
|
||||
// serverNameFromContext extracts the current server name from the context.
|
||||
// Returns "UNKNOWN" if none is available (should probably never happen).
|
||||
func serverNameFromContext(ctx context.Context) string {
|
||||
@@ -133,9 +236,19 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
|
||||
// of a panic
|
||||
statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""}
|
||||
|
||||
// Determine if this is an HTTPS request
|
||||
isHTTPS := r.TLS != nil
|
||||
|
||||
if h.metrics.PerHost {
|
||||
labels["host"] = strings.ToLower(r.Host)
|
||||
statusLabels["host"] = strings.ToLower(r.Host)
|
||||
// Apply cardinality protection for host metrics
|
||||
if h.metrics.shouldAllowHostMetrics(r.Host, isHTTPS) {
|
||||
labels["host"] = strings.ToLower(r.Host)
|
||||
statusLabels["host"] = strings.ToLower(r.Host)
|
||||
} else {
|
||||
// Use a catch-all label for unallowed hosts to prevent cardinality explosion
|
||||
labels["host"] = "_other"
|
||||
statusLabels["host"] = "_other"
|
||||
}
|
||||
}
|
||||
|
||||
inFlight := h.metrics.httpMetrics.requestInFlight.With(labels)
|
||||
|
||||
@@ -2,6 +2,7 @@ package caddyhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -206,9 +207,11 @@ func TestMetricsInstrumentedHandler(t *testing.T) {
|
||||
func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
|
||||
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||
metrics := &Metrics{
|
||||
PerHost: true,
|
||||
init: sync.Once{},
|
||||
httpMetrics: &httpMetrics{},
|
||||
PerHost: true,
|
||||
AllowCatchAllHosts: true, // Allow all hosts for testing
|
||||
init: sync.Once{},
|
||||
httpMetrics: &httpMetrics{},
|
||||
allowedHosts: make(map[string]struct{}),
|
||||
}
|
||||
handlerErr := errors.New("oh noes")
|
||||
response := []byte("hello world!")
|
||||
@@ -379,6 +382,112 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsCardinalityProtection(t *testing.T) {
|
||||
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||
|
||||
// Test 1: Without AllowCatchAllHosts, arbitrary hosts should be mapped to "_other"
|
||||
metrics := &Metrics{
|
||||
PerHost: true,
|
||||
AllowCatchAllHosts: false, // Default - should map unknown hosts to "_other"
|
||||
init: sync.Once{},
|
||||
httpMetrics: &httpMetrics{},
|
||||
allowedHosts: make(map[string]struct{}),
|
||||
}
|
||||
|
||||
// Add one allowed host
|
||||
metrics.allowedHosts["allowed.com"] = struct{}{}
|
||||
|
||||
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
||||
w.Write([]byte("hello"))
|
||||
return nil
|
||||
})
|
||||
|
||||
ih := newMetricsInstrumentedHandler(ctx, "test", mh, metrics)
|
||||
|
||||
// Test request to allowed host
|
||||
r1 := httptest.NewRequest("GET", "http://allowed.com/", nil)
|
||||
r1.Host = "allowed.com"
|
||||
w1 := httptest.NewRecorder()
|
||||
ih.ServeHTTP(w1, r1, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
||||
|
||||
// Test request to unknown host (should be mapped to "_other")
|
||||
r2 := httptest.NewRequest("GET", "http://attacker.com/", nil)
|
||||
r2.Host = "attacker.com"
|
||||
w2 := httptest.NewRecorder()
|
||||
ih.ServeHTTP(w2, r2, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
||||
|
||||
// Test request to another unknown host (should also be mapped to "_other")
|
||||
r3 := httptest.NewRequest("GET", "http://evil.com/", nil)
|
||||
r3.Host = "evil.com"
|
||||
w3 := httptest.NewRecorder()
|
||||
ih.ServeHTTP(w3, r3, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
||||
|
||||
// Check that metrics contain:
|
||||
// - One entry for "allowed.com"
|
||||
// - One entry for "_other" (aggregating attacker.com and evil.com)
|
||||
expected := `
|
||||
# HELP caddy_http_requests_total Counter of HTTP(S) requests made.
|
||||
# TYPE caddy_http_requests_total counter
|
||||
caddy_http_requests_total{handler="test",host="_other",server="UNKNOWN"} 2
|
||||
caddy_http_requests_total{handler="test",host="allowed.com",server="UNKNOWN"} 1
|
||||
`
|
||||
|
||||
if err := testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expected),
|
||||
"caddy_http_requests_total",
|
||||
); err != nil {
|
||||
t.Errorf("Cardinality protection test failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsHTTPSCatchAll(t *testing.T) {
|
||||
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||
|
||||
// Test that HTTPS requests allow catch-all even when AllowCatchAllHosts is false
|
||||
metrics := &Metrics{
|
||||
PerHost: true,
|
||||
AllowCatchAllHosts: false,
|
||||
hasHTTPSServer: true, // Simulate having HTTPS servers
|
||||
init: sync.Once{},
|
||||
httpMetrics: &httpMetrics{},
|
||||
allowedHosts: make(map[string]struct{}), // Empty - no explicitly allowed hosts
|
||||
}
|
||||
|
||||
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
||||
w.Write([]byte("hello"))
|
||||
return nil
|
||||
})
|
||||
|
||||
ih := newMetricsInstrumentedHandler(ctx, "test", mh, metrics)
|
||||
|
||||
// Test HTTPS request (should be allowed even though not in allowedHosts)
|
||||
r1 := httptest.NewRequest("GET", "https://unknown.com/", nil)
|
||||
r1.Host = "unknown.com"
|
||||
r1.TLS = &tls.ConnectionState{} // Mark as TLS/HTTPS
|
||||
w1 := httptest.NewRecorder()
|
||||
ih.ServeHTTP(w1, r1, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
||||
|
||||
// Test HTTP request (should be mapped to "_other")
|
||||
r2 := httptest.NewRequest("GET", "http://unknown.com/", nil)
|
||||
r2.Host = "unknown.com"
|
||||
// No TLS field = HTTP request
|
||||
w2 := httptest.NewRecorder()
|
||||
ih.ServeHTTP(w2, r2, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
||||
|
||||
// Check that HTTPS request gets real host, HTTP gets "_other"
|
||||
expected := `
|
||||
# HELP caddy_http_requests_total Counter of HTTP(S) requests made.
|
||||
# TYPE caddy_http_requests_total counter
|
||||
caddy_http_requests_total{handler="test",host="_other",server="UNKNOWN"} 1
|
||||
caddy_http_requests_total{handler="test",host="unknown.com",server="UNKNOWN"} 1
|
||||
`
|
||||
|
||||
if err := testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expected),
|
||||
"caddy_http_requests_total",
|
||||
); err != nil {
|
||||
t.Errorf("HTTPS catch-all test failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
type middlewareHandlerFunc func(http.ResponseWriter, *http.Request, Handler) error
|
||||
|
||||
func (f middlewareHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, h Handler) error {
|
||||
|
||||
@@ -41,7 +41,7 @@ func parseLinkHeader(header string) []linkResource {
|
||||
return resources
|
||||
}
|
||||
|
||||
for _, link := range strings.Split(header, comma) {
|
||||
for link := range strings.SplitSeq(header, comma) {
|
||||
l := linkResource{params: make(map[string]string)}
|
||||
|
||||
li, ri := strings.Index(link, "<"), strings.Index(link, ">")
|
||||
@@ -51,7 +51,7 @@ func parseLinkHeader(header string) []linkResource {
|
||||
|
||||
l.uri = strings.TrimSpace(link[li+1 : ri])
|
||||
|
||||
for _, param := range strings.Split(strings.TrimSpace(link[ri+1:]), semicolon) {
|
||||
for param := range strings.SplitSeq(strings.TrimSpace(link[ri+1:]), semicolon) {
|
||||
before, after, isCut := strings.Cut(strings.TrimSpace(param), equal)
|
||||
key := strings.TrimSpace(before)
|
||||
if key == "" {
|
||||
|
||||
@@ -172,8 +172,12 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||
// current URI, including any internal rewrites
|
||||
case "http.request.uri":
|
||||
return req.URL.RequestURI(), true
|
||||
case "http.request.uri_escaped":
|
||||
return url.QueryEscape(req.URL.RequestURI()), true
|
||||
case "http.request.uri.path":
|
||||
return req.URL.Path, true
|
||||
case "http.request.uri.path_escaped":
|
||||
return url.QueryEscape(req.URL.Path), true
|
||||
case "http.request.uri.path.file":
|
||||
_, file := path.Split(req.URL.Path)
|
||||
return file, true
|
||||
@@ -186,6 +190,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||
return path.Ext(req.URL.Path), true
|
||||
case "http.request.uri.query":
|
||||
return req.URL.RawQuery, true
|
||||
case "http.request.uri.query_escaped":
|
||||
return url.QueryEscape(req.URL.RawQuery), true
|
||||
case "http.request.uri.prefixed_query":
|
||||
if req.URL.RawQuery == "" {
|
||||
return "", true
|
||||
@@ -283,7 +289,7 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||
return prefix.String(), true
|
||||
}
|
||||
|
||||
// hostname labels
|
||||
// hostname labels (case insensitive, so normalize to lowercase)
|
||||
if strings.HasPrefix(key, reqHostLabelsReplPrefix) {
|
||||
idxStr := key[len(reqHostLabelsReplPrefix):]
|
||||
idx, err := strconv.Atoi(idxStr)
|
||||
@@ -298,7 +304,7 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||
if idx >= len(hostLabels) {
|
||||
return "", true
|
||||
}
|
||||
return hostLabels[len(hostLabels)-idx-1], true
|
||||
return strings.ToLower(hostLabels[len(hostLabels)-idx-1]), true
|
||||
}
|
||||
|
||||
// path parts
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
)
|
||||
|
||||
func TestHTTPVarReplacement(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodGet, "/foo/bar.tar.gz", nil)
|
||||
req, _ := http.NewRequest(http.MethodGet, "/foo/bar.tar.gz?a=1&b=2", nil)
|
||||
repl := caddy.NewReplacer()
|
||||
localAddr, _ := net.ResolveTCPAddr("tcp", "192.168.159.1:80")
|
||||
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
|
||||
@@ -142,6 +142,22 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
|
||||
get: "http.request.host.labels.2",
|
||||
expect: "",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri",
|
||||
expect: "/foo/bar.tar.gz?a=1&b=2",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri_escaped",
|
||||
expect: "%2Ffoo%2Fbar.tar.gz%3Fa%3D1%26b%3D2",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri.path",
|
||||
expect: "/foo/bar.tar.gz",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri.path_escaped",
|
||||
expect: "%2Ffoo%2Fbar.tar.gz",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri.path.file",
|
||||
expect: "bar.tar.gz",
|
||||
@@ -155,6 +171,26 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
|
||||
get: "http.request.uri.path.file.ext",
|
||||
expect: ".gz",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri.query",
|
||||
expect: "a=1&b=2",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri.query_escaped",
|
||||
expect: "a%3D1%26b%3D2",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri.query.a",
|
||||
expect: "1",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri.query.b",
|
||||
expect: "2",
|
||||
},
|
||||
{
|
||||
get: "http.request.uri.prefixed_query",
|
||||
expect: "?a=1&b=2",
|
||||
},
|
||||
{
|
||||
get: "http.request.tls.cipher_suite",
|
||||
expect: "TLS_AES_256_GCM_SHA384",
|
||||
|
||||
@@ -116,7 +116,7 @@ func (ew errorWrapper) Read(p []byte) (n int, err error) {
|
||||
if errors.As(err, &mbe) {
|
||||
err = caddyhttp.Error(http.StatusRequestEntityTooLarge, err)
|
||||
}
|
||||
return
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Interface guard
|
||||
|
||||
@@ -75,8 +75,8 @@ For proxying:
|
||||
cmd.Flags().BoolP("insecure", "", false, "Disable TLS verification (WARNING: DISABLES SECURITY BY NOT VERIFYING TLS CERTIFICATES!)")
|
||||
cmd.Flags().BoolP("disable-redirects", "r", false, "Disable HTTP->HTTPS redirects")
|
||||
cmd.Flags().BoolP("internal-certs", "i", false, "Use internal CA for issuing certs")
|
||||
cmd.Flags().StringSliceP("header-up", "H", []string{}, "Set a request header to send to the upstream (format: \"Field: value\")")
|
||||
cmd.Flags().StringSliceP("header-down", "d", []string{}, "Set a response header to send back to the client (format: \"Field: value\")")
|
||||
cmd.Flags().StringArrayP("header-up", "H", []string{}, "Set a request header to send to the upstream (format: \"Field: value\")")
|
||||
cmd.Flags().StringArrayP("header-down", "d", []string{}, "Set a response header to send back to the client (format: \"Field: value\")")
|
||||
cmd.Flags().BoolP("access-log", "", false, "Enable the access log")
|
||||
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
|
||||
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdReverseProxy)
|
||||
@@ -182,7 +182,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
|
||||
}
|
||||
|
||||
// set up header_up
|
||||
headerUp, err := fs.GetStringSlice("header-up")
|
||||
headerUp, err := fs.GetStringArray("header-up")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err)
|
||||
}
|
||||
@@ -204,7 +204,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
|
||||
}
|
||||
|
||||
// set up header_down
|
||||
headerDown, err := fs.GetStringSlice("header-down")
|
||||
headerDown, err := fs.GetStringArray("header-down")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err)
|
||||
}
|
||||
|
||||
@@ -154,13 +154,13 @@ func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error)
|
||||
|
||||
err = writer.writeBeginRequest(uint16(Responder), 0)
|
||||
if err != nil {
|
||||
return
|
||||
return r, err
|
||||
}
|
||||
|
||||
writer.recType = Params
|
||||
err = writer.writePairs(p)
|
||||
if err != nil {
|
||||
return
|
||||
return r, err
|
||||
}
|
||||
|
||||
writer.recType = Stdin
|
||||
@@ -176,7 +176,7 @@ func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error)
|
||||
}
|
||||
|
||||
r = &streamReader{c: c}
|
||||
return
|
||||
return r, err
|
||||
}
|
||||
|
||||
// clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer
|
||||
@@ -213,7 +213,7 @@ func (f clientCloser) Close() error {
|
||||
func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) {
|
||||
r, err := c.Do(p, req)
|
||||
if err != nil {
|
||||
return
|
||||
return resp, err
|
||||
}
|
||||
|
||||
rb := bufio.NewReader(r)
|
||||
@@ -223,7 +223,7 @@ func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Respons
|
||||
// Parse the response headers.
|
||||
mimeHeader, err := tp.ReadMIMEHeader()
|
||||
if err != nil && err != io.EOF {
|
||||
return
|
||||
return resp, err
|
||||
}
|
||||
resp.Header = http.Header(mimeHeader)
|
||||
|
||||
@@ -231,7 +231,7 @@ func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Respons
|
||||
statusNumber, statusInfo, statusIsCut := strings.Cut(resp.Header.Get("Status"), " ")
|
||||
resp.StatusCode, err = strconv.Atoi(statusNumber)
|
||||
if err != nil {
|
||||
return
|
||||
return resp, err
|
||||
}
|
||||
if statusIsCut {
|
||||
resp.Status = statusInfo
|
||||
@@ -260,7 +260,7 @@ func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Respons
|
||||
}
|
||||
resp.Body = closer
|
||||
|
||||
return
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Get issues a GET request to the fcgi responder.
|
||||
@@ -329,7 +329,7 @@ func (c *client) PostFile(p map[string]string, data url.Values, file map[string]
|
||||
for _, v0 := range val {
|
||||
err = writer.WriteField(key, v0)
|
||||
if err != nil {
|
||||
return
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,13 +347,13 @@ func (c *client) PostFile(p map[string]string, data url.Values, file map[string]
|
||||
}
|
||||
_, err = io.Copy(part, fd)
|
||||
if err != nil {
|
||||
return
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
return
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return c.Post(p, "POST", bodyType, buf, int64(buf.Len()))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user