Compare commits

..

2 Commits

Author SHA1 Message Date
Francis Lavoie 5968ebd0f4 reverseproxy: Add support for specifying IDs in Caddyfile 2021-09-13 00:21:54 -04:00
Francis Lavoie a5f4fae145 reverseproxy: Add ID field for upstreams 2021-09-12 23:50:04 -04:00
309 changed files with 7397 additions and 29444 deletions
-5
View File
@@ -1,5 +0,0 @@
[*]
end_of_line = lf
[caddytest/integration/caddyfile_adapt/*.txt]
indent_style = tab
-1
View File
@@ -1 +0,0 @@
*.go text eol=lf
+6 -16
View File
@@ -1,7 +1,7 @@
Contributing to Caddy Contributing to Caddy
===================== =====================
Welcome! Thank you for choosing to be a part of our community. Caddy wouldn't be nearly as excellent without your involvement! Welcome! Thank you for choosing to be a part of our community. Caddy wouldn't be great without your involvement!
For starters, we invite you to join [the Caddy forum](https://caddy.community) where you can hang out with other Caddy users and developers. For starters, we invite you to join [the Caddy forum](https://caddy.community) where you can hang out with other Caddy users and developers.
@@ -35,29 +35,19 @@ Here are some of the expectations we have of contributors:
- **Keep related commits together in a PR.** We do want pull requests to be small, but you should also keep multiple related commits in the same PR if they rely on each other. - **Keep related commits together in a PR.** We do want pull requests to be small, but you should also keep multiple related commits in the same PR if they rely on each other.
- **Write tests.** Good, automated tests are very valuable! Written properly, they ensure your change works, and that other changes in the future won't break your change. CI checks should pass. - **Write tests.** Tests are essential! Written properly, they ensure your change works, and that other changes in the future won't break your change. CI checks should pass.
- **Benchmarks should be included for optimizations.** Optimizations sometimes make code harder to read or have changes that are less than obvious. They should be proven with benchmarks and profiling. - **Benchmarks should be included for optimizations.** Optimizations sometimes make code harder to read or have changes that are less than obvious. They should be proven with benchmarks or profiling.
- **[Squash](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) insignificant commits.** Every commit should be significant. Commits which merely rewrite a comment or fix a typo can be combined into another commit that has more substance. Interactive rebase can do this, or a simpler way is `git reset --soft <diverging-commit>` then `git commit -s`. - **[Squash](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) insignificant commits.** Every commit should be significant. Commits which merely rewrite a comment or fix a typo can be combined into another commit that has more substance. Interactive rebase can do this, or a simpler way is `git reset --soft <diverging-commit>` then `git commit -s`.
- **Be responsible for and maintain your contributions.** Caddy is a growing project, and it's much better when individual contributors help maintain their change after it is merged. - **Own your contributions.** Caddy is a growing project, and it's much better when individual contributors help maintain their change after it is merged.
- **Use comments properly.** We expect good godoc comments for package-level functions, types, and values. Comments are also useful whenever the purpose for a line of code is not obvious. - **Use comments properly.** We expect good godoc comments for package-level functions, types, and values. Comments are also useful whenever the purpose for a line of code is not obvious.
- **Pull requests may still get closed.** The longer a PR stays open and idle, the more likely it is to be closed. If we haven't reviewed it in a while, it probably means the change is not a priority. Please don't take this personally, we're trying to balance a lot of tasks! If nobody else has commented or reacted to the PR, it likely means your change is useful only to you. The reality is this happens quite a lot. We don't tend to accept PRs that aren't generally helpful. For these reasons or others, the PR may get closed even after a review. We are not obligated to accept all proposed changes, even if the best justification we can give is something vague like, "It doesn't sit right." Sometimes PRs are just the wrong thing or the wrong time. Because it is open source, you can always build your own modified version of Caddy with a change you need, even if we reject it in the official repo. Plus, because Caddy is extensible, it's possible your feature could make a great plugin instead! - **Pull requests may still get closed.** The longer a PR stays open and idle, the more likely it is to be closed. If we haven't reviewed it in a while, it probably means the change is not a priority. Please don't take this personally, we're trying to balance a lot of tasks! If nobody else has commented or reacted to the PR, it likely means your change is useful only to you. The reality is this happens quite a bit. We don't tend to accept PRs that aren't generally helpful. For these reasons or others, the PR may get closed even after a review. We are not obligated to accept all proposed changes, even if the best justification we can give is something vague like, "It doesn't sit right." Sometimes PRs are just the wrong thing or the wrong time. Because it is open source, you can always build your own modified version of Caddy with a change you need, even if we reject it in the official repo.
- **You certify that you wrote and comprehend the code you submit.** The Caddy project welcomes original contributions that comply with [our CLA](https://cla-assistant.io/caddyserver/caddy), meaning that authors must be able to certify that they created or have rights to the code they are contributing. In addition, we require that code is not simply copy-pasted from Q/A sites or AI language models without full comprehension and rigorous testing. In other words: contributors are allowed to refer to communities for assistance and use AI tools such as language models for inspiration, but code which originates from or is assisted by these resources MUST be: We often grant [collaborator status](#collaborator-instructions) to contributors who author one or more significant, high-quality PRs that are merged into the code base!
- Licensed for you to freely share
- Fully comprehended by you (be able to explain every line of code)
- Verified by automated tests when feasible, or thorough manual tests otherwise
We have found that current language models (LLMs, like ChatGPT) may understand code syntax and even problem spaces to an extent, but often fail in subtle ways to convey true knowledge and produce correct algorithms. Integrated tools such as GitHub Copilot and Sourcegraph Cody may be used for inspiration, but code generated by these tools still needs to meet our criteria for licensing, human comprehension, and testing. These tools may be used to help write code comments and tests as long as you can certify they are accurate and correct. Note that it is often more trouble than it's worth to certify that Copilot (for example) is not giving you code that is possibly plagiarised, unlicensed, or licensed with incompatible terms -- as the Caddy project cannot accept such contributions. If that's too difficult for you (or impossible), then we recommend using these resources only for inspiration and write your own code. Ultimately, you (the contributor) are responsible for the code you're submitting.
As a courtesy to reviewers, we kindly ask that you disclose when contributing code that was generated by an AI tool or copied from another website so we can be aware of what to look for in code review.
We often grant [collaborator status](#collaborator-instructions) to contributors who author one or more significant, high-quality PRs that are merged into the code base.
#### HOW TO MAKE A PULL REQUEST TO CADDY #### HOW TO MAKE A PULL REQUEST TO CADDY
+4 -4
View File
@@ -7,7 +7,7 @@ The Caddy project would like to make sure that it stays on top of all practicall
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| 2.x | ✔️ | | 2.x | :white_check_mark: |
| 1.x | :x: | | 1.x | :x: |
| < 1.x | :x: | | < 1.x | :x: |
@@ -24,7 +24,7 @@ We do not accept reports if the steps imply or require a compromised system or t
Client-side exploits are out of scope. In other words, it is not a bug in Caddy if the web browser does something unsafe, even if the downloaded content was served by Caddy. (Those kinds of exploits can generally be mitigated by proper configuration of HTTP headers.) As a general rule, the content served by Caddy is not considered in scope because content is configurable by the site owner or the associated web application. Client-side exploits are out of scope. In other words, it is not a bug in Caddy if the web browser does something unsafe, even if the downloaded content was served by Caddy. (Those kinds of exploits can generally be mitigated by proper configuration of HTTP headers.) As a general rule, the content served by Caddy is not considered in scope because content is configurable by the site owner or the associated web application.
Security bugs in code dependencies (including Go's standard library) are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code. Security bugs in code dependencies are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code.
## Reporting a Vulnerability ## Reporting a Vulnerability
@@ -42,13 +42,13 @@ We'll need enough information to verify the bug and make a patch. To speed thing
- Specific minimal steps to reproduce the issue from scratch - Specific minimal steps to reproduce the issue from scratch
- A working patch - A working patch
Please DO NOT use containers, VMs, cloud instances or services, or any other complex infrastructure in your steps. Always prefer `curl -v` instead of web browsers. Please DO NOT use containers, VMs, cloud instances or services, or any other complex infrastructure in your steps. Always prefer `curl` instead of web browsers.
We consider publicly-registered domain names to be public information. This necessary in order to maintain the integrity of certificate transparency, public DNS, and other public trust systems. Do not redact domain names from your reports. The actual content of your domain name affects Caddy's behavior, so we need the exact domain name(s) to reproduce with, or your report will be ignored. We consider publicly-registered domain names to be public information. This necessary in order to maintain the integrity of certificate transparency, public DNS, and other public trust systems. Do not redact domain names from your reports. The actual content of your domain name affects Caddy's behavior, so we need the exact domain name(s) to reproduce with, or your report will be ignored.
It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, depending on available resources. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. (We get a lot of invalid reports.) Thank you for understanding. It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, depending on available resources. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. (We get a lot of invalid reports.) Thank you for understanding.
When you are ready, please email Matt Holt (the author) directly: matt at dyanim dot com. When you are ready, please email Matt Holt (the author) directly: matt [at] lightcodelabs [dot com].
Please don't encrypt the email body. It only makes the process more complicated. Please don't encrypt the email body. It only makes the process more complicated.
-7
View File
@@ -1,7 +0,0 @@
---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
+30 -36
View File
@@ -18,26 +18,13 @@ jobs:
# Default is true, cancels jobs for other platforms in the matrix if one fails # Default is true, cancels jobs for other platforms in the matrix if one fails
fail-fast: false fail-fast: false
matrix: matrix:
os: os: [ ubuntu-latest, macos-latest, windows-latest ]
- ubuntu-latest go: [ '1.16', '1.17' ]
- macos-latest
- windows-latest
go:
- '1.20'
# - '1.21'
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.20'
GO_SEMVER: '~1.20.6'
# - go: '1.21'
# GO_SEMVER: '~1.21.0'
# Set some variables per OS, usable via ${{ matrix.VAR }} # Set some variables per OS, usable via ${{ matrix.VAR }}
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing # CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
# SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True') # SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
include:
- os: ubuntu-latest - os: ubuntu-latest
CADDY_BIN_PATH: ./cmd/caddy/caddy CADDY_BIN_PATH: ./cmd/caddy/caddy
SUCCESS: 0 SUCCESS: 0
@@ -53,14 +40,13 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install Go - name: Install Go
uses: actions/setup-go@v4 uses: actions/setup-go@v2
with: with:
go-version: ${{ matrix.GO_SEMVER }} go-version: ${{ matrix.go }}
check-latest: true
- name: Checkout code
uses: actions/checkout@v2
# These tools would be useful if we later decide to reinvestigate # These tools would be useful if we later decide to reinvestigate
# publishing test/coverage reports to some tool for easier consumption # publishing test/coverage reports to some tool for easier consumption
@@ -69,7 +55,7 @@ jobs:
# go get github.com/axw/gocov/gocov # go get github.com/axw/gocov/gocov
# go get github.com/AlekSi/gocov-xml # go get github.com/AlekSi/gocov-xml
# go get -u github.com/jstemmer/go-junit-report # go get -u github.com/jstemmer/go-junit-report
# echo "$(go env GOPATH)/bin" >> $GITHUB_PATH # echo "::add-path::$(go env GOPATH)/bin"
- name: Print Go version and environment - name: Print Go version and environment
id: vars id: vars
@@ -82,7 +68,16 @@ jobs:
env env
printf "Git version: $(git version)\n\n" printf "Git version: $(git version)\n\n"
# Calculate the short SHA1 hash of the git commit # Calculate the short SHA1 hash of the git commit
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
echo "::set-output name=go_cache::$(go env GOCACHE)"
- name: Cache the build cache
uses: actions/cache@v2
with:
path: ${{ steps.vars.outputs.go_cache }}
key: ${{ runner.os }}-${{ matrix.go }}-go-ci-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go }}-go-ci
- name: Get dependencies - name: Get dependencies
run: | run: |
@@ -97,7 +92,7 @@ jobs:
go build -trimpath -ldflags="-w -s" -v go build -trimpath -ldflags="-w -s" -v
- name: Publish Build Artifact - name: Publish Build Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v1
with: with:
name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }} name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
path: ${{ matrix.CADDY_BIN_PATH }} path: ${{ matrix.CADDY_BIN_PATH }}
@@ -111,7 +106,7 @@ jobs:
run: | run: |
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out # (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
go test -v -coverprofile="cover-profile.out" -short -race ./... go test -v -coverprofile="cover-profile.out" -short -race ./...
# echo "status=$?" >> $GITHUB_OUTPUT # echo "::set-output name=status::$?"
# Relevant step if we reinvestigate publishing test/coverage reports # Relevant step if we reinvestigate publishing test/coverage reports
# - name: Prepare coverage reports # - name: Prepare coverage reports
@@ -131,11 +126,11 @@ jobs:
s390x-test: s390x-test:
name: test (s390x on IBM Z) name: test (s390x on IBM Z)
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' if: github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
steps: steps:
- name: Checkout code - name: Checkout code into the Go module directory
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Run Tests - name: Run Tests
run: | run: |
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
@@ -144,26 +139,25 @@ jobs:
short_sha=$(git rev-parse --short HEAD) short_sha=$(git rev-parse --short HEAD)
# The environment is fresh, so there's no point in keeping accepting and adding the key. # The environment is fresh, so there's no point in keeping accepting and adding the key.
rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . "$CI_USER"@ci-s390x.caddyserver.com:/var/tmp/"$short_sha" rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . caddy-ci@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$CI_USER"@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -v ./..." ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t caddy-ci@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -v ./..."
test_result=$? test_result=$?
# There's no need leaving the files around # There's no need leaving the files around
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$CI_USER"@ci-s390x.caddyserver.com "rm -rf /var/tmp/'$short_sha'" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null caddy-ci@ci-s390x.caddyserver.com "rm -rf /var/tmp/'$short_sha'"
echo "Test exit code: $test_result" echo "Test exit code: $test_result"
exit $test_result exit $test_result
env: env:
SSH_KEY: ${{ secrets.S390X_SSH_KEY }} SSH_KEY: ${{ secrets.S390X_SSH_KEY }}
CI_USER: ${{ secrets.CI_USER }}
goreleaser-check: goreleaser-check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
- uses: goreleaser/goreleaser-action@v4 - uses: goreleaser/goreleaser-action@v2
with: with:
version: latest version: latest
args: check args: check
+16 -27
View File
@@ -15,38 +15,15 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
goos: goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
- 'android' go: [ '1.17' ]
- 'linux'
- 'solaris'
- 'illumos'
- 'dragonfly'
- 'freebsd'
- 'openbsd'
- 'plan9'
- 'windows'
- 'darwin'
- 'netbsd'
go:
- '1.20'
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.20'
GO_SEMVER: '~1.20.6'
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
steps: steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install Go - name: Install Go
uses: actions/setup-go@v4 uses: actions/setup-go@v2
with: with:
go-version: ${{ matrix.GO_SEMVER }} go-version: ${{ matrix.go }}
check-latest: true
- name: Print Go version and environment - name: Print Go version and environment
id: vars id: vars
@@ -57,6 +34,18 @@ jobs:
go env go env
printf "\n\nSystem environment:\n\n" printf "\n\nSystem environment:\n\n"
env env
echo "::set-output name=go_cache::$(go env GOCACHE)"
- name: Cache the build cache
uses: actions/cache@v2
with:
path: ${{ steps.vars.outputs.go_cache }}
key: cross-build-go${{ matrix.go }}-${{ matrix.goos }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
cross-build-go${{ matrix.go }}-${{ matrix.goos }}
- name: Checkout code into the Go module directory
uses: actions/checkout@v2
- name: Run Build - name: Run Build
env: env:
+4 -31
View File
@@ -10,43 +10,16 @@ on:
- master - master
- 2.* - 2.*
permissions:
contents: read
jobs: jobs:
# From https://github.com/golangci/golangci-lint-action # From https://github.com/golangci/golangci-lint-action
golangci: golangci:
permissions:
contents: read # for actions/checkout to fetch code
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
name: lint name: lint
strategy: runs-on: ubuntu-latest
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-go@v4
with:
go-version: '~1.20.6'
check-latest: true
# Workaround for https://github.com/golangci/golangci-lint-action/issues/135
skip-pkg-cache: true
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v2
with: with:
version: v1.53 version: v1.31
# Workaround for https://github.com/golangci/golangci-lint-action/issues/135
skip-pkg-cache: true
# Windows times out frequently after about 5m50s if we don't set a longer timeout.
args: --timeout 10m
# Optional: show only new issues if it's a pull request. The default value is `false`. # Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true # only-new-issues: true
+27 -44
View File
@@ -10,40 +10,23 @@ jobs:
name: Release name: Release
strategy: strategy:
matrix: matrix:
os: os: [ ubuntu-latest ]
- ubuntu-latest go: [ '1.17' ]
go:
- '1.20'
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.20'
GO_SEMVER: '~1.20.6'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
# https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings
permissions:
id-token: write
# https://docs.github.com/en/rest/overview/permissions-required-for-github-apps#permission-on-contents
# "Releases" is part of `contents`, so it needs the `write`
contents: write
steps: steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.GO_SEMVER }}
check-latest: true
# Force fetch upstream tags -- because 65 minutes # Force fetch upstream tags -- because 65 minutes
# tl;dr: actions/checkout@v3 runs this line: # tl;dr: actions/checkout@v2 runs this line:
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/ # git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran: # which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
# git fetch --prune --unshallow # git fetch --prune --unshallow
@@ -63,8 +46,9 @@ jobs:
go env go env
printf "\n\nSystem environment:\n\n" printf "\n\nSystem environment:\n\n"
env env
echo "version_tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\//}"
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
echo "::set-output name=go_cache::$(go env GOCACHE)"
# Add "pip install" CLI tools to PATH # Add "pip install" CLI tools to PATH
echo ~/.local/bin >> $GITHUB_PATH echo ~/.local/bin >> $GITHUB_PATH
@@ -76,10 +60,10 @@ jobs:
TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"` TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"`
TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"` TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"`
TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"` TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"`
echo "tag_major=${TAG_MAJOR}" >> $GITHUB_OUTPUT echo "::set-output name=tag_major::${TAG_MAJOR}"
echo "tag_minor=${TAG_MINOR}" >> $GITHUB_OUTPUT echo "::set-output name=tag_minor::${TAG_MINOR}"
echo "tag_patch=${TAG_PATCH}" >> $GITHUB_OUTPUT echo "::set-output name=tag_patch::${TAG_PATCH}"
echo "tag_special=${TAG_SPECIAL}" >> $GITHUB_OUTPUT echo "::set-output name=tag_special::${TAG_SPECIAL}"
# Cloudsmith CLI tooling for pushing releases # Cloudsmith CLI tooling for pushing releases
# See https://help.cloudsmith.io/docs/cli # See https://help.cloudsmith.io/docs/cli
@@ -96,27 +80,26 @@ jobs:
# tags are only accepted if signed by Matt's key # tags are only accepted if signed by Matt's key
git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1 git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1
- name: Install Cosign - name: Cache the build cache
uses: sigstore/cosign-installer@main uses: actions/cache@v2
- name: Cosign version with:
run: cosign version path: ${{ steps.vars.outputs.go_cache }}
- name: Install Syft key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }}
uses: anchore/sbom-action/download-syft@main restore-keys: |
- name: Syft version ${{ runner.os }}-go${{ matrix.go }}-release
run: syft version
# GoReleaser will take care of publishing those artifacts into the release # GoReleaser will take care of publishing those artifacts into the release
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v4 uses: goreleaser/goreleaser-action@v2
with: with:
version: latest version: latest
args: release --rm-dist --timeout 60m args: release --rm-dist
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.vars.outputs.version_tag }} TAG: ${{ steps.vars.outputs.version_tag }}
COSIGN_EXPERIMENTAL: 1
# Only publish on non-special tags (e.g. non-beta) # Only publish on non-special tags (e.g. non-beta)
# We will continue to push to Gemfury for the foreseeable future, although # We will continue to push to Gemfury for the forseeable future, although
# Cloudsmith is probably better, to not break things for existing users of Gemfury. # Cloudsmith is probably better, to not break things for existing users of Gemfury.
# See https://gemfury.com/caddy/deb:caddy # See https://gemfury.com/caddy/deb:caddy
- name: Publish .deb to Gemfury - name: Publish .deb to Gemfury
+3 -4
View File
@@ -10,15 +10,14 @@ jobs:
name: Release Published name: Release Published
strategy: strategy:
matrix: matrix:
os: os: [ ubuntu-latest ]
- ubuntu-latest
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
# See https://github.com/peter-evans/repository-dispatch # See https://github.com/peter-evans/repository-dispatch
- name: Trigger event on caddyserver/dist - name: Trigger event on caddyserver/dist
uses: peter-evans/repository-dispatch@v2 uses: peter-evans/repository-dispatch@v1
with: with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }} token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/dist repository: caddyserver/dist
@@ -26,7 +25,7 @@ jobs:
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}' client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
- name: Trigger event on caddyserver/caddy-docker - name: Trigger event on caddyserver/caddy-docker
uses: peter-evans/repository-dispatch@v2 uses: peter-evans/repository-dispatch@v1
with: with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }} token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/caddy-docker repository: caddyserver/caddy-docker
-2
View File
@@ -1,7 +1,6 @@
_gitignore/ _gitignore/
*.log *.log
Caddyfile Caddyfile
Caddyfile.*
!caddyfile/ !caddyfile/
# artifacts from pprof tooling # artifacts from pprof tooling
@@ -11,7 +10,6 @@ Caddyfile.*
# build artifacts and helpers # build artifacts and helpers
cmd/caddy/caddy cmd/caddy/caddy
cmd/caddy/caddy.exe cmd/caddy/caddy.exe
cmd/caddy/tmp/*.exe
# mac specific # mac specific
.DS_Store .DS_Store
+4 -5
View File
@@ -1,12 +1,13 @@
linters-settings: linters-settings:
errcheck: errcheck:
ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.* ignore: fmt:.*,io/ioutil:^Read.*,go.uber.org/zap/zapcore:^Add.*
ignoretests: true ignoretests: true
linters: linters:
disable-all: true disable-all: true
enable: enable:
- bodyclose - bodyclose
- deadcode
- errcheck - errcheck
- gofmt - gofmt
- goimports - goimports
@@ -17,9 +18,11 @@ linters:
- misspell - misspell
- prealloc - prealloc
- staticcheck - staticcheck
- structcheck
- typecheck - typecheck
- unconvert - unconvert
- unused - unused
- varcheck
# these are implicitly disabled: # these are implicitly disabled:
# - asciicheck # - asciicheck
# - depguard # - depguard
@@ -93,7 +96,3 @@ issues:
text: "G404" # G404: Insecure random number source (rand) text: "G404" # G404: Insecure random number source (rand)
linters: linters:
- gosec - gosec
- path: modules/caddyhttp/reverseproxy/streaming.go
text: "G404" # G404: Insecure random number source (rand)
linters:
- gosec
+12 -79
View File
@@ -4,26 +4,18 @@ before:
# This is so we can run goreleaser on tag without Git complaining of being dirty. The main.go in cmd/caddy directory # This is so we can run goreleaser on tag without Git complaining of being dirty. The main.go in cmd/caddy directory
# cannot be built within that directory due to changes necessary for the build causing Git to be dirty, which # cannot be built within that directory due to changes necessary for the build causing Git to be dirty, which
# subsequently causes gorleaser to refuse running. # subsequently causes gorleaser to refuse running.
- rm -rf caddy-build caddy-dist vendor
# vendor Caddy deps
- go mod vendor
- mkdir -p caddy-build - mkdir -p caddy-build
- cp cmd/caddy/main.go caddy-build/main.go - cp cmd/caddy/main.go caddy-build/main.go
- /bin/sh -c 'cd ./caddy-build && go mod init caddy' - cp ./go.mod caddy-build/go.mod
- sed -i.bkp 's|github.com/caddyserver/caddy/v2|caddy|g' ./caddy-build/go.mod
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env # GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate # so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
- go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod - go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod
# as of Go 1.16, `go` commands no longer automatically change go.{mod,sum}. We now have to explicitly # as of Go 1.16, `go` commands no longer automatically change go.{mod,sum}. We now have to explicitly
# run `go mod tidy`. The `/bin/sh -c '...'` is because goreleaser can't find cd in PATH without shell invocation. # run `go mod tidy`. The `/bin/sh -c '...'` is because goreleaser can't find cd in PATH without shell invocation.
- /bin/sh -c 'cd ./caddy-build && go mod tidy' - /bin/sh -c 'cd ./caddy-build && go mod tidy'
# vendor the deps of the prepared to-build module
- /bin/sh -c 'cd ./caddy-build && go mod vendor'
- git clone --depth 1 https://github.com/caddyserver/dist caddy-dist - git clone --depth 1 https://github.com/caddyserver/dist caddy-dist
- mkdir -p caddy-dist/man
- go mod download - go mod download
- go run cmd/caddy/main.go manpage --directory ./caddy-dist/man
- gzip -r ./caddy-dist/man/
- /bin/sh -c 'go run cmd/caddy/main.go completion bash > ./caddy-dist/scripts/bash-completion'
builds: builds:
- env: - env:
@@ -44,9 +36,9 @@ builds:
- s390x - s390x
- ppc64le - ppc64le
goarm: goarm:
- "5" - 5
- "6" - 6
- "7" - 7
ignore: ignore:
- goos: darwin - goos: darwin
goarch: arm goarch: arm
@@ -64,75 +56,18 @@ builds:
goarch: s390x goarch: s390x
- goos: freebsd - goos: freebsd
goarch: arm goarch: arm
goarm: "5" goarm: 5
flags: flags:
- -trimpath - -trimpath
- -mod=readonly
ldflags: ldflags:
- -s -w - -s -w
signs:
- cmd: cosign
signature: "${artifact}.sig"
certificate: '{{ trimsuffix (trimsuffix .Env.artifact ".zip") ".tar.gz" }}.pem'
args: ["sign-blob", "--yes", "--output-signature=${signature}", "--output-certificate", "${certificate}", "${artifact}"]
artifacts: all
sboms:
- artifacts: binary
documents:
- >-
{{ .ProjectName }}_
{{- .Version }}_
{{- if eq .Os "darwin" }}mac{{ else }}{{ .Os }}{{ end }}_
{{- .Arch }}
{{- with .Arm }}v{{ . }}{{ end }}
{{- with .Mips }}_{{ . }}{{ end }}
{{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}.sbom
cmd: syft
args: ["$artifact", "--file", "${document}", "--output", "cyclonedx-json"]
archives: archives:
- id: default - format_overrides:
format_overrides:
- goos: windows - goos: windows
format: zip format: zip
name_template: >- replacements:
{{ .ProjectName }}_ darwin: mac
{{- .Version }}_
{{- if eq .Os "darwin" }}mac{{ else }}{{ .Os }}{{ end }}_
{{- .Arch }}
{{- with .Arm }}v{{ . }}{{ end }}
{{- with .Mips }}_{{ . }}{{ end }}
{{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}
# packge the 'caddy-build' directory into a tarball,
# allowing users to build the exact same set of files as ours.
- id: source
meta: true
name_template: "{{ .ProjectName }}_{{ .Version }}_buildable-artifact"
files:
- src: LICENSE
dst: ./LICENSE
- src: README.md
dst: ./README.md
- src: AUTHORS
dst: ./AUTHORS
- src: ./caddy-build
dst: ./
source:
enabled: true
name_template: '{{ .ProjectName }}_{{ .Version }}_src'
format: 'tar.gz'
# Additional files/template/globs you want to add to the source archive.
#
# Default: empty.
files:
- vendor
checksum: checksum:
algorithm: sha512 algorithm: sha512
@@ -140,7 +75,7 @@ nfpms:
- id: default - id: default
package_name: caddy package_name: caddy
vendor: Dyanim vendor: Light Code Labs
homepage: https://caddyserver.com homepage: https://caddyserver.com
maintainer: Matthew Holt <mholt@users.noreply.github.com> maintainer: Matthew Holt <mholt@users.noreply.github.com>
description: | description: |
@@ -162,21 +97,19 @@ nfpms:
- src: ./caddy-dist/welcome/index.html - src: ./caddy-dist/welcome/index.html
dst: /usr/share/caddy/index.html dst: /usr/share/caddy/index.html
- src: ./caddy-dist/scripts/bash-completion - src: ./caddy-dist/scripts/completions/bash-completion
dst: /etc/bash_completion.d/caddy dst: /etc/bash_completion.d/caddy
- src: ./caddy-dist/config/Caddyfile - src: ./caddy-dist/config/Caddyfile
dst: /etc/caddy/Caddyfile dst: /etc/caddy/Caddyfile
type: config type: config
- src: ./caddy-dist/man/*
dst: /usr/share/man/man8/
scripts: scripts:
postinstall: ./caddy-dist/scripts/postinstall.sh postinstall: ./caddy-dist/scripts/postinstall.sh
preremove: ./caddy-dist/scripts/preremove.sh preremove: ./caddy-dist/scripts/preremove.sh
postremove: ./caddy-dist/scripts/postremove.sh postremove: ./caddy-dist/scripts/postremove.sh
release: release:
github: github:
owner: caddyserver owner: caddyserver
+14 -28
View File
@@ -1,19 +1,13 @@
<p align="center"> <p align="center">
<a href="https://caddyserver.com"> <a href="https://caddyserver.com"><img src="https://user-images.githubusercontent.com/1128849/36338535-05fb646a-136f-11e8-987b-e6901e717d5a.png" alt="Caddy" width="450"></a>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/1128849/210187358-e2c39003-9a5e-4dd5-a783-6deb6483ee72.svg">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1128849/210187356-dfb7f1c5-ac2e-43aa-bb23-fc014280ae1f.svg">
<img src="https://user-images.githubusercontent.com/1128849/210187356-dfb7f1c5-ac2e-43aa-bb23-fc014280ae1f.svg" alt="Caddy" width="550">
</picture>
</a>
<br> <br>
<h3 align="center">a <a href="https://zerossl.com"><img src="https://user-images.githubusercontent.com/55066419/208327323-2770dc16-ec09-43a0-9035-c5b872c2ad7f.svg" height="28" style="vertical-align: -7.7px" valign="middle"></a> project</h3> <h3 align="center">a <a href="https://zerossl.com"><img src="https://caddyserver.com/resources/images/zerossl-logo.svg" height="28" valign="middle"></a> project</h3>
</p> </p>
<hr> <hr>
<h3 align="center">Every site on HTTPS</h3> <h3 align="center">Every site on HTTPS</h3>
<p align="center">Caddy is an extensible server platform that uses TLS by default.</p> <p align="center">Caddy is an extensible server platform that uses TLS by default.</p>
<p align="center"> <p align="center">
<a href="https://github.com/caddyserver/caddy/actions/workflows/ci.yml"><img src="https://github.com/caddyserver/caddy/actions/workflows/ci.yml/badge.svg"></a> <a href="https://github.com/caddyserver/caddy/actions?query=workflow%3ACross-Platform"><img src="https://github.com/caddyserver/caddy/workflows/Cross-Platform/badge.svg"></a>
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-%23007d9c.svg"></a> <a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-%23007d9c.svg"></a>
<br> <br>
<a href="https://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" alt="@caddyserver on Twitter"></a> <a href="https://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" alt="@caddyserver on Twitter"></a>
@@ -46,13 +40,7 @@
<p align="center"> <p align="center">
<b>Powered by</b> <b>Powered by</b>
<br> <br>
<a href="https://github.com/caddyserver/certmagic"> <a href="https://github.com/caddyserver/certmagic"><img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250"></a>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/55066419/206946718-740b6371-3df3-4d72-a822-47e4c48af999.png">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png">
<img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250">
</picture>
</a>
</p> </p>
@@ -69,28 +57,28 @@
- Multi-issuer fallback - Multi-issuer fallback
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues - **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
- **Production-ready** after serving trillions of requests and managing millions of TLS certificates - **Production-ready** after serving trillions of requests and managing millions of TLS certificates
- **Scales to hundreds of thousands of sites** as proven in production - **Scales to tens of thousands of sites** ... and probably more
- **HTTP/1.1, HTTP/2, and HTTP/3** all supported by default - **HTTP/1.1, HTTP/2, and experimental HTTP/3** support
- **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat - **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat
- **Runs anywhere** with **no external dependencies** (not even libc) - **Runs anywhere** with **no external dependencies** (not even libc)
- Written in Go, a language with higher **memory safety guarantees** than other servers - Written in Go, a language with higher **memory safety guarantees** than other servers
- Actually **fun to use** - Actually **fun to use**
- So much more to [discover](https://caddyserver.com/v2) - So, so much more to [discover](https://caddyserver.com/v2)
## Install ## Install
The simplest, cross-platform way to get started is to download Caddy from [GitHub Releases](https://github.com/caddyserver/caddy/releases) and place the executable file in your PATH. The simplest, cross-platform way is to download from [GitHub Releases](https://github.com/caddyserver/caddy/releases) and place the executable file in your PATH.
See [our online documentation](https://caddyserver.com/docs/install) for other install instructions. For other install options, see https://caddyserver.com/docs/install.
## Build from source ## Build from source
Requirements: Requirements:
- [Go 1.20 or newer](https://golang.org/dl/) - [Go 1.16 or newer](https://golang.org/dl/)
### For development ### For development
_**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions in the next section._ _**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions in the next section._
```bash ```bash
@@ -176,9 +164,9 @@ The docs are also open source. You can contribute to them here: https://github.c
## Getting help ## Getting help
- We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed. - We **strongly recommend** that all professionals or companies using Caddy get a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed.
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way! We can offer private help to sponsors. If Caddy is benefitting your company, please consider a sponsorship. This not only helps fund full-time work to ensure the longevity of the project, it provides your company the resources, support, and discounts you need; along with being a great look for your company to your customers and potential customers! - A [sponsorship](https://github.com/sponsors/mholt) goes a long way! If Caddy is benefitting your company, please consider a sponsorship! This not only helps fund full-time work to ensure the longevity of the project, it's also a great look for your company to your customers and potential customers!
- Individuals can exchange help for free on our community forum at https://caddy.community. Remember that people give help out of their spare time and good will. The best way to get help is to give it first! - Individuals can exchange help for free on our community forum at https://caddy.community. Remember that people give help out of their spare time and good will. The best way to get help is to give it first!
@@ -188,8 +176,6 @@ Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only
## About ## About
Matthew Holt began developing Caddy in 2014 while studying computer science at Brigham Young University. (The name "Caddy" was chosen because this software helps with the tedious, mundane tasks of serving the Web, and is also a single place for multiple things to be organized together.) It soon became the first web server to use HTTPS automatically and by default, and now has hundreds of contributors and has served trillions of HTTPS requests.
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH. **The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH.
- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_ - _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_
@@ -197,4 +183,4 @@ Matthew Holt began developing Caddy in 2014 while studying computer science at B
Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company. Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company.
Debian package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that enables your organization to create, store and share packages in any format, to any place, with total confidence. Debian package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that enables your organization to create, store and share packages in any format, to any place, with total confidence.
+87 -231
View File
@@ -25,9 +25,8 @@ import (
"errors" "errors"
"expvar" "expvar"
"fmt" "fmt"
"hash"
"hash/fnv"
"io" "io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"net/http/pprof" "net/http/pprof"
@@ -40,23 +39,12 @@ import (
"sync" "sync"
"time" "time"
"github.com/caddyserver/caddy/v2/notify"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore"
) )
func init() {
// The hard-coded default `DefaultAdminListen` can be overidden
// by setting the `CADDY_ADMIN` environment variable.
// The environment variable may be used by packagers to change
// the default admin address to something more appropriate for
// that platform. See #5317 for discussion.
if env, exists := os.LookupEnv("CADDY_ADMIN"); exists {
DefaultAdminListen = env
}
}
// AdminConfig configures Caddy's API endpoint, which is used // AdminConfig configures Caddy's API endpoint, which is used
// to manage Caddy while it is running. // to manage Caddy while it is running.
type AdminConfig struct { type AdminConfig struct {
@@ -68,14 +56,7 @@ type AdminConfig struct {
// The address to which the admin endpoint's listener should // The address to which the admin endpoint's listener should
// bind itself. Can be any single network address that can be // bind itself. Can be any single network address that can be
// parsed by Caddy. Accepts placeholders. // parsed by Caddy. Default: localhost:2019
// Default: the value of the `CADDY_ADMIN` environment variable,
// or `localhost:2019` otherwise.
//
// Remember: When changing this value through a config reload,
// be sure to use the `--address` CLI flag to specify the current
// admin address if the currently-running admin endpoint is not
// the default address.
Listen string `json:"listen,omitempty"` Listen string `json:"listen,omitempty"`
// If true, CORS headers will be emitted, and requests to the // If true, CORS headers will be emitted, and requests to the
@@ -111,10 +92,6 @@ type AdminConfig struct {
// //
// EXPERIMENTAL: This feature is subject to change. // EXPERIMENTAL: This feature is subject to change.
Remote *RemoteAdmin `json:"remote,omitempty"` Remote *RemoteAdmin `json:"remote,omitempty"`
// Holds onto the routers so that we can later provision them
// if they require provisioning.
routers []AdminRouter
} }
// ConfigSettings configures the management of configuration. // ConfigSettings configures the management of configuration.
@@ -124,26 +101,20 @@ type ConfigSettings struct {
// are not persisted; only configs that are pushed to Caddy get persisted. // are not persisted; only configs that are pushed to Caddy get persisted.
Persist *bool `json:"persist,omitempty"` Persist *bool `json:"persist,omitempty"`
// Loads a new configuration. This is helpful if your configs are // Loads a configuration to use. This is helpful if your configs are
// managed elsewhere and you want Caddy to pull its config dynamically // managed elsewhere, and you want Caddy to pull its config dynamically
// when it starts. The pulled config completely replaces the current // when it starts. The pulled config completely replaces the current
// one, just like any other config load. It is an error if a pulled // one, just like any other config load. It is an error if a pulled
// config is configured to pull another config without a load_delay, // config is configured to pull another config.
// as this creates a tight loop.
// //
// EXPERIMENTAL: Subject to change. // EXPERIMENTAL: Subject to change.
LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"` LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"`
// The duration after which to load config. If set, config will be pulled // The interval to pull config. With a non-zero value, will pull config
// from the config loader after this duration. A delay is required if a // from config loader (eg. a http loader) with given interval.
// dynamically-loaded config is configured to load yet another config. To
// load configs on a regular interval, ensure this value is set the same
// on all loaded configs; it can also be variable if needed, and to stop
// the loop, simply remove dynamic config loading from the next-loaded
// config.
// //
// EXPERIMENTAL: Subject to change. // EXPERIMENTAL: Subject to change.
LoadDelay Duration `json:"load_delay,omitempty"` LoadInterval Duration `json:"load_interval,omitempty"`
} }
// IdentityConfig configures management of this server's identity. An identity // IdentityConfig configures management of this server's identity. An identity
@@ -174,7 +145,7 @@ type IdentityConfig struct {
// //
// EXPERIMENTAL: Subject to change. // EXPERIMENTAL: Subject to change.
type RemoteAdmin struct { type RemoteAdmin struct {
// The address on which to start the secure listener. Accepts placeholders. // The address on which to start the secure listener.
// Default: :2021 // Default: :2021
Listen string `json:"listen,omitempty"` Listen string `json:"listen,omitempty"`
@@ -213,7 +184,7 @@ type AdminPermissions struct {
// newAdminHandler reads admin's config and returns an http.Handler suitable // newAdminHandler reads admin's config and returns an http.Handler suitable
// for use in an admin endpoint server, which will be listening on listenAddr. // for use in an admin endpoint server, which will be listening on listenAddr.
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler { func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler {
muxWrap := adminHandler{mux: http.NewServeMux()} muxWrap := adminHandler{mux: http.NewServeMux()}
// secure the local or remote endpoint respectively // secure the local or remote endpoint respectively
@@ -222,7 +193,6 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) admi
} else { } else {
muxWrap.enforceHost = !addr.isWildcardInterface() muxWrap.enforceHost = !addr.isWildcardInterface()
muxWrap.allowedOrigins = admin.allowedOrigins(addr) muxWrap.allowedOrigins = admin.allowedOrigins(addr)
muxWrap.enforceOrigin = admin.EnforceOrigin
} }
addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) { addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
@@ -273,39 +243,17 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) admi
for _, route := range router.Routes() { for _, route := range router.Routes() {
addRoute(route.Pattern, handlerLabel, route.Handler) addRoute(route.Pattern, handlerLabel, route.Handler)
} }
admin.routers = append(admin.routers, router)
} }
return muxWrap return muxWrap
} }
// provisionAdminRouters provisions all the router modules
// in the admin.api namespace that need provisioning.
func (admin *AdminConfig) provisionAdminRouters(ctx Context) error {
for _, router := range admin.routers {
provisioner, ok := router.(Provisioner)
if !ok {
continue
}
err := provisioner.Provision(ctx)
if err != nil {
return err
}
}
// We no longer need the routers once provisioned, allow for GC
admin.routers = nil
return nil
}
// allowedOrigins returns a list of origins that are allowed. // allowedOrigins returns a list of origins that are allowed.
// If admin.Origins is nil (null), the provided listen address // If admin.Origins is nil (null), the provided listen address
// will be used as the default origin. If admin.Origins is // will be used as the default origin. If admin.Origins is
// empty, no origins will be allowed, effectively bricking the // empty, no origins will be allowed, effectively bricking the
// endpoint for non-unix-socket endpoints, but whatever. // endpoint for non-unix-socket endpoints, but whatever.
func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL { func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
uniqueOrigins := make(map[string]struct{}) uniqueOrigins := make(map[string]struct{})
for _, o := range admin.Origins { for _, o := range admin.Origins {
uniqueOrigins[o] = struct{}{} uniqueOrigins[o] = struct{}{}
@@ -318,32 +266,7 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
// messages. If the requested URI does not include an Internet host // messages. If the requested URI does not include an Internet host
// name for the service being requested, then the Host header field MUST // name for the service being requested, then the Host header field MUST
// be given with an empty value." // be given with an empty value."
//
// UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6.
// Understandable, but frustrating. See:
// https://github.com/golang/go/issues/60374
// See also the discussion here:
// https://github.com/golang/go/issues/61431
//
// We can no longer conform to RFC 2616 Section 14.26 from either Go or curl
// in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a
// bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin
// security checks, the infosec community assures me that it is secure to do
// so, because:
// 1) Browsers do not allow access to unix sockets
// 2) DNS is irrelevant to unix sockets
//
// I am not quite ready to trust either of those external factors, so instead
// of disabling Host/Origin checks, we now allow specific Host values when
// accessing the admin endpoint over unix sockets. I definitely don't trust
// DNS (e.g. I don't trust 'localhost' to always resolve to the local host),
// and IP shouldn't even be used, but if it is for some reason, I think we can
// at least be reasonably assured that 127.0.0.1 and ::1 route to the local
// machine, meaning that a hypothetical browser origin would have to be on the
// local machine as well.
uniqueOrigins[""] = struct{}{} uniqueOrigins[""] = struct{}{}
uniqueOrigins["127.0.0.1"] = struct{}{}
uniqueOrigins["::1"] = struct{}{}
} else { } else {
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{} uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{} uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
@@ -354,23 +277,8 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
uniqueOrigins[addr.JoinHostPort(0)] = struct{}{} uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
} }
} }
allowed := make([]*url.URL, 0, len(uniqueOrigins)) allowed := make([]string, 0, len(uniqueOrigins))
for originStr := range uniqueOrigins { for origin := range uniqueOrigins {
var origin *url.URL
if strings.Contains(originStr, "://") {
var err error
origin, err = url.Parse(originStr)
if err != nil {
continue
}
origin.Path = ""
origin.RawPath = ""
origin.Fragment = ""
origin.RawFragment = ""
origin.RawQuery = ""
} else {
origin = &url.URL{Host: originStr}
}
allowed = append(allowed, origin) allowed = append(allowed, origin)
} }
return allowed return allowed
@@ -382,19 +290,17 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
// that there is always an admin server (unless it is explicitly // that there is always an admin server (unless it is explicitly
// configured to be disabled). // configured to be disabled).
func replaceLocalAdminServer(cfg *Config) error { func replaceLocalAdminServer(cfg *Config) error {
// always* be sure to close down the old admin endpoint // always be sure to close down the old admin endpoint
// as gracefully as possible, even if the new one is // as gracefully as possible, even if the new one is
// disabled -- careful to use reference to the current // disabled -- careful to use reference to the current
// (old) admin endpoint since it will be different // (old) admin endpoint since it will be different
// when the function returns // when the function returns
// (* except if the new one fails to start)
oldAdminServer := localAdminServer oldAdminServer := localAdminServer
var err error
defer func() { defer func() {
// do the shutdown asynchronously so that any // do the shutdown asynchronously so that any
// current API request gets a response; this // current API request gets a response; this
// goroutine may last a few seconds // goroutine may last a few seconds
if oldAdminServer != nil && err == nil { if oldAdminServer != nil {
go func(oldAdminServer *http.Server) { go func(oldAdminServer *http.Server) {
err := stopAdminServer(oldAdminServer) err := stopAdminServer(oldAdminServer)
if err != nil { if err != nil {
@@ -404,28 +310,27 @@ func replaceLocalAdminServer(cfg *Config) error {
} }
}() }()
// set a default if admin wasn't otherwise configured // always get a valid admin config
if cfg.Admin == nil { adminConfig := DefaultAdminConfig
cfg.Admin = &AdminConfig{ if cfg != nil && cfg.Admin != nil {
Listen: DefaultAdminListen, adminConfig = cfg.Admin
}
} }
// if new admin endpoint is to be disabled, we're done // if new admin endpoint is to be disabled, we're done
if cfg.Admin.Disabled { if adminConfig.Disabled {
Log().Named("admin").Warn("admin endpoint disabled") Log().Named("admin").Warn("admin endpoint disabled")
return nil return nil
} }
// extract a singular listener address // extract a singular listener address
addr, err := parseAdminListenAddr(cfg.Admin.Listen, DefaultAdminListen) addr, err := parseAdminListenAddr(adminConfig.Listen, DefaultAdminListen)
if err != nil { if err != nil {
return err return err
} }
handler := cfg.Admin.newAdminHandler(addr, false) handler := adminConfig.newAdminHandler(addr, false)
ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{}) ln, err := Listen(addr.Network, addr.JoinHostPort(0))
if err != nil { if err != nil {
return err return err
} }
@@ -446,15 +351,15 @@ func replaceLocalAdminServer(cfg *Config) error {
serverMu.Lock() serverMu.Lock()
server := localAdminServer server := localAdminServer
serverMu.Unlock() serverMu.Unlock()
if err := server.Serve(ln.(net.Listener)); !errors.Is(err, http.ErrServerClosed) { if err := server.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err)) adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
} }
}() }()
adminLogger.Info("admin endpoint started", adminLogger.Info("admin endpoint started",
zap.String("address", addr.String()), zap.String("address", addr.String()),
zap.Bool("enforce_origin", cfg.Admin.EnforceOrigin), zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
zap.Array("origins", loggableURLArray(handler.allowedOrigins))) zap.Strings("origins", handler.allowedOrigins))
if !handler.enforceHost { if !handler.enforceHost {
adminLogger.Warn("admin endpoint on open interface; host checking disabled", adminLogger.Warn("admin endpoint on open interface; host checking disabled",
@@ -485,7 +390,7 @@ func manageIdentity(ctx Context, cfg *Config) error {
if err != nil { if err != nil {
return fmt.Errorf("loading identity issuer modules: %s", err) return fmt.Errorf("loading identity issuer modules: %s", err)
} }
for _, issVal := range val.([]any) { for _, issVal := range val.([]interface{}) {
cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer)) cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer))
} }
} }
@@ -562,9 +467,6 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
} }
// create TLS config that will enforce mutual authentication // create TLS config that will enforce mutual authentication
if identityCertCache == nil {
return fmt.Errorf("cannot enable remote admin without a certificate cache; configure identity management to initialize a certificate cache")
}
cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger, false) cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger, false)
tlsConfig := cmCfg.TLSConfig() tlsConfig := cmCfg.TLSConfig()
tlsConfig.NextProtos = nil // this server does not solve ACME challenges tlsConfig.NextProtos = nil // this server does not solve ACME challenges
@@ -592,11 +494,10 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
serverMu.Unlock() serverMu.Unlock()
// start listener // start listener
lnAny, err := addr.Listen(ctx, 0, net.ListenConfig{}) ln, err := Listen(addr.Network, addr.JoinHostPort(0))
if err != nil { if err != nil {
return err return err
} }
ln := lnAny.(net.Listener)
ln = tls.NewListener(ln, tlsConfig) ln = tls.NewListener(ln, tlsConfig)
go func() { go func() {
@@ -615,13 +516,12 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
} }
func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool) *certmagic.Config { func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool) *certmagic.Config {
var cmCfg *certmagic.Config
if ident == nil { if ident == nil {
// user might not have configured identity; that's OK, we can still make a // user might not have configured identity; that's OK, we can still make a
// certmagic config, although it'll be mostly useless for remote management // certmagic config, although it'll be mostly useless for remote management
ident = new(IdentityConfig) ident = new(IdentityConfig)
} }
template := certmagic.Config{ cmCfg := &certmagic.Config{
Storage: DefaultStorage, // do not act as part of a cluster (this is for the server's local identity) Storage: DefaultStorage, // do not act as part of a cluster (this is for the server's local identity)
Logger: logger, Logger: logger,
Issuers: ident.issuers, Issuers: ident.issuers,
@@ -631,11 +531,9 @@ func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool)
GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) { GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
return cmCfg, nil return cmCfg, nil
}, },
Logger: logger.Named("cache"),
}) })
} }
cmCfg = certmagic.New(identityCertCache, template) return certmagic.New(identityCertCache, *cmCfg)
return cmCfg
} }
// IdentityCredentials returns this instance's configured, managed identity credentials // IdentityCredentials returns this instance's configured, managed identity credentials
@@ -750,10 +648,10 @@ type AdminRoute struct {
type adminHandler struct { type adminHandler struct {
mux *http.ServeMux mux *http.ServeMux
// security for local/plaintext endpoint // security for local/plaintext) endpoint, on by default
enforceOrigin bool enforceOrigin bool
enforceHost bool enforceHost bool
allowedOrigins []*url.URL allowedOrigins []string
// security for remote/encrypted endpoint // security for remote/encrypted endpoint
remoteControl *RemoteAdmin remoteControl *RemoteAdmin
@@ -762,17 +660,11 @@ type adminHandler struct {
// ServeHTTP is the external entry point for API requests. // ServeHTTP is the external entry point for API requests.
// It will only be called once per request. // It will only be called once per request.
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ip, port, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
ip = r.RemoteAddr
port = ""
}
log := Log().Named("admin.api").With( log := Log().Named("admin.api").With(
zap.String("method", r.Method), zap.String("method", r.Method),
zap.String("host", r.Host), zap.String("host", r.Host),
zap.String("uri", r.RequestURI), zap.String("uri", r.RequestURI),
zap.String("remote_ip", ip), zap.String("remote_addr", r.RemoteAddr),
zap.String("remote_port", port),
zap.Reflect("headers", r.Header), zap.Reflect("headers", r.Header),
) )
if r.TLS != nil { if r.TLS != nil {
@@ -879,8 +771,8 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
// rebinding attacks. // rebinding attacks.
func (h adminHandler) checkHost(r *http.Request) error { func (h adminHandler) checkHost(r *http.Request) error {
var allowed bool var allowed bool
for _, allowedOrigin := range h.allowedOrigins { for _, allowedHost := range h.allowedOrigins {
if r.Host == allowedOrigin.Host { if r.Host == allowedHost {
allowed = true allowed = true
break break
} }
@@ -899,81 +791,59 @@ func (h adminHandler) checkHost(r *http.Request) error {
// sites from issuing requests to our listener. It // sites from issuing requests to our listener. It
// returns the origin that was obtained from r. // returns the origin that was obtained from r.
func (h adminHandler) checkOrigin(r *http.Request) (string, error) { func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
originStr, origin := h.getOrigin(r) origin := h.getOriginHost(r)
if origin == nil { if origin == "" {
return "", APIError{ return origin, APIError{
HTTPStatus: http.StatusForbidden, HTTPStatus: http.StatusForbidden,
Err: fmt.Errorf("required Origin header is missing or invalid"), Err: fmt.Errorf("missing required Origin header"),
} }
} }
if !h.originAllowed(origin) { if !h.originAllowed(origin) {
return "", APIError{ return origin, APIError{
HTTPStatus: http.StatusForbidden, HTTPStatus: http.StatusForbidden,
Err: fmt.Errorf("client is not allowed to access from origin '%s'", originStr), Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
} }
} }
return origin.String(), nil return origin, nil
} }
func (h adminHandler) getOrigin(r *http.Request) (string, *url.URL) { func (h adminHandler) getOriginHost(r *http.Request) string {
origin := r.Header.Get("Origin") origin := r.Header.Get("Origin")
if origin == "" { if origin == "" {
origin = r.Header.Get("Referer") origin = r.Header.Get("Referer")
} }
originURL, err := url.Parse(origin) originURL, err := url.Parse(origin)
if err != nil { if err == nil && originURL.Host != "" {
return origin, nil origin = originURL.Host
} }
originURL.Path = "" return origin
originURL.RawPath = ""
originURL.Fragment = ""
originURL.RawFragment = ""
originURL.RawQuery = ""
return origin, originURL
} }
func (h adminHandler) originAllowed(origin *url.URL) bool { func (h adminHandler) originAllowed(origin string) bool {
for _, allowedOrigin := range h.allowedOrigins { for _, allowedOrigin := range h.allowedOrigins {
if allowedOrigin.Scheme != "" && origin.Scheme != allowedOrigin.Scheme { originCopy := origin
continue if !strings.Contains(allowedOrigin, "://") {
// no scheme specified, so allow both
originCopy = strings.TrimPrefix(originCopy, "http://")
originCopy = strings.TrimPrefix(originCopy, "https://")
} }
if origin.Host == allowedOrigin.Host { if originCopy == allowedOrigin {
return true return true
} }
} }
return false return false
} }
// etagHasher returns a the hasher we used on the config to both
// produce and verify ETags.
func etagHasher() hash.Hash32 { return fnv.New32a() }
// makeEtag returns an Etag header value (including quotes) for
// the given config path and hash of contents at that path.
func makeEtag(path string, hash hash.Hash) string {
return fmt.Sprintf(`"%s %x"`, path, hash.Sum(nil))
}
func handleConfig(w http.ResponseWriter, r *http.Request) error { func handleConfig(w http.ResponseWriter, r *http.Request) error {
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
// Set the ETag as a trailer header.
// The alternative is to write the config to a buffer, and
// then hash that.
w.Header().Set("Trailer", "ETag")
hash := etagHasher() err := readConfig(r.URL.Path, w)
configWriter := io.MultiWriter(w, hash)
err := readConfig(r.URL.Path, configWriter)
if err != nil { if err != nil {
return APIError{HTTPStatus: http.StatusBadRequest, Err: err} return APIError{HTTPStatus: http.StatusBadRequest, Err: err}
} }
// we could consider setting up a sync.Pool for the summed
// hashes to reduce GC pressure.
w.Header().Set("Etag", makeEtag(r.URL.Path, hash))
return nil return nil
case http.MethodPost, case http.MethodPost,
@@ -1007,8 +877,8 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
forceReload := r.Header.Get("Cache-Control") == "must-revalidate" forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
err := changeConfig(r.Method, r.URL.Path, body, r.Header.Get("If-Match"), forceReload) err := changeConfig(r.Method, r.URL.Path, body, forceReload)
if err != nil && !errors.Is(err, errSameConfig) { if err != nil {
return err return err
} }
@@ -1027,28 +897,19 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
parts := strings.Split(idPath, "/") parts := strings.Split(idPath, "/")
if len(parts) < 3 || parts[2] == "" { if len(parts) < 3 || parts[2] == "" {
return APIError{ return fmt.Errorf("request path is missing object ID")
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("request path is missing object ID"),
}
} }
if parts[0] != "" || parts[1] != "id" { if parts[0] != "" || parts[1] != "id" {
return APIError{ return fmt.Errorf("malformed object path")
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed object path"),
}
} }
id := parts[2] id := parts[2]
// map the ID to the expanded path // map the ID to the expanded path
rawCfgMu.RLock() currentCfgMu.RLock()
expanded, ok := rawCfgIndex[id] expanded, ok := rawCfgIndex[id]
rawCfgMu.RUnlock() defer currentCfgMu.RUnlock()
if !ok { if !ok {
return APIError{ return fmt.Errorf("unknown object ID '%s'", id)
HTTPStatus: http.StatusNotFound,
Err: fmt.Errorf("unknown object ID '%s'", id),
}
} }
// piece the full URL path back together // piece the full URL path back together
@@ -1066,7 +927,11 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
} }
} }
exitProcess(context.Background(), Log().Named("admin.api")) if err := notify.NotifyStopping(); err != nil {
Log().Error("unable to notify stopping to service manager", zap.Error(err))
}
exitProcess(Log().Named("admin.api"))
return nil return nil
} }
@@ -1074,11 +939,11 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
// the operation at path according to method, using body and out as // the operation at path according to method, using body and out as
// needed. This is a low-level, unsynchronized function; most callers // needed. This is a low-level, unsynchronized function; most callers
// will want to use changeConfig or readConfig instead. This requires a // will want to use changeConfig or readConfig instead. This requires a
// read or write lock on currentCtxMu, depending on method (GET needs // read or write lock on currentCfgMu, depending on method (GET needs
// only a read lock; all others need a write lock). // only a read lock; all others need a write lock).
func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error { func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error {
var err error var err error
var val any var val interface{}
// if there is a request body, decode it into the // if there is a request body, decode it into the
// variable that will be set in the config according // variable that will be set in the config according
@@ -1115,16 +980,16 @@ func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error
parts = parts[:len(parts)-1] parts = parts[:len(parts)-1]
} }
var ptr any = rawCfg var ptr interface{} = rawCfg
traverseLoop: traverseLoop:
for i, part := range parts { for i, part := range parts {
switch v := ptr.(type) { switch v := ptr.(type) {
case map[string]any: case map[string]interface{}:
// if the next part enters a slice, and the slice is our destination, // if the next part enters a slice, and the slice is our destination,
// handle it specially (because appending to the slice copies the slice // handle it specially (because appending to the slice copies the slice
// header, which does not replace the original one like we want) // header, which does not replace the original one like we want)
if arr, ok := v[part].([]any); ok && i == len(parts)-2 { if arr, ok := v[part].([]interface{}); ok && i == len(parts)-2 {
var idx int var idx int
if method != http.MethodPost { if method != http.MethodPost {
idxStr := parts[len(parts)-1] idxStr := parts[len(parts)-1]
@@ -1146,7 +1011,7 @@ traverseLoop:
} }
case http.MethodPost: case http.MethodPost:
if ellipses { if ellipses {
valArray, ok := val.([]any) valArray, ok := val.([]interface{})
if !ok { if !ok {
return fmt.Errorf("final element is not an array") return fmt.Errorf("final element is not an array")
} }
@@ -1181,9 +1046,9 @@ traverseLoop:
case http.MethodPost: case http.MethodPost:
// if the part is an existing list, POST appends to // if the part is an existing list, POST appends to
// it, otherwise it just sets or creates the value // it, otherwise it just sets or creates the value
if arr, ok := v[part].([]any); ok { if arr, ok := v[part].([]interface{}); ok {
if ellipses { if ellipses {
valArray, ok := val.([]any) valArray, ok := val.([]interface{})
if !ok { if !ok {
return fmt.Errorf("final element is not an array") return fmt.Errorf("final element is not an array")
} }
@@ -1214,12 +1079,12 @@ traverseLoop:
// might not exist yet; that's OK but we need to make them as // might not exist yet; that's OK but we need to make them as
// we go, while we still have a pointer from the level above // we go, while we still have a pointer from the level above
if v[part] == nil && method == http.MethodPut { if v[part] == nil && method == http.MethodPut {
v[part] = make(map[string]any) v[part] = make(map[string]interface{})
} }
ptr = v[part] ptr = v[part]
} }
case []any: case []interface{}:
partInt, err := strconv.Atoi(part) partInt, err := strconv.Atoi(part)
if err != nil { if err != nil {
return fmt.Errorf("[/%s] invalid array index '%s': %v", return fmt.Errorf("[/%s] invalid array index '%s': %v",
@@ -1241,7 +1106,7 @@ traverseLoop:
// RemoveMetaFields removes meta fields like "@id" from a JSON message // RemoveMetaFields removes meta fields like "@id" from a JSON message
// by using a simple regular expression. (An alternate way to do this // by using a simple regular expression. (An alternate way to do this
// would be to delete them from the raw, map[string]any // would be to delete them from the raw, map[string]interface{}
// representation as they are indexed, then iterate the index we made // representation as they are indexed, then iterate the index we made
// and add them back after encoding as JSON, but this is simpler.) // and add them back after encoding as JSON, but this is simpler.)
func RemoveMetaFields(rawJSON []byte) []byte { func RemoveMetaFields(rawJSON []byte) []byte {
@@ -1293,10 +1158,7 @@ func (e APIError) Error() string {
// parseAdminListenAddr extracts a singular listen address from either addr // parseAdminListenAddr extracts a singular listen address from either addr
// or defaultAddr, returning the network and the address of the listener. // or defaultAddr, returning the network and the address of the listener.
func parseAdminListenAddr(addr string, defaultAddr string) (NetworkAddress, error) { func parseAdminListenAddr(addr string, defaultAddr string) (NetworkAddress, error) {
input, err := NewReplacer().ReplaceOrErr(addr, true, true) input := addr
if err != nil {
return NetworkAddress{}, fmt.Errorf("replacing listen address: %v", err)
}
if input == "" { if input == "" {
input = defaultAddr input = defaultAddr
} }
@@ -1319,18 +1181,6 @@ func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
return x509.ParseCertificate(derBytes) return x509.ParseCertificate(derBytes)
} }
type loggableURLArray []*url.URL
func (ua loggableURLArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
if ua == nil {
return nil
}
for _, u := range ua {
enc.AppendString(u.String())
}
return nil
}
var ( var (
// DefaultAdminListen is the address for the local admin // DefaultAdminListen is the address for the local admin
// listener, if none is specified at startup. // listener, if none is specified at startup.
@@ -1340,13 +1190,19 @@ var (
// (TLS-authenticated) admin listener, if enabled and not // (TLS-authenticated) admin listener, if enabled and not
// specified otherwise. // specified otherwise.
DefaultRemoteAdminListen = ":2021" DefaultRemoteAdminListen = ":2021"
// DefaultAdminConfig is the default configuration
// for the local administration endpoint.
DefaultAdminConfig = &AdminConfig{
Listen: DefaultAdminListen,
}
) )
// PIDFile writes a pidfile to the file at filename. It // PIDFile writes a pidfile to the file at filename. It
// will get deleted before the process gracefully exits. // will get deleted before the process gracefully exits.
func PIDFile(filename string) error { func PIDFile(filename string) error {
pid := []byte(strconv.Itoa(os.Getpid()) + "\n") pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
err := os.WriteFile(filename, pid, 0600) err := ioutil.WriteFile(filename, pid, 0600)
if err != nil { if err != nil {
return err return err
} }
@@ -1376,7 +1232,7 @@ const (
) )
var bufPool = sync.Pool{ var bufPool = sync.Pool{
New: func() any { New: func() interface{} {
return new(bytes.Buffer) return new(bytes.Buffer)
}, },
} }
+2 -51
View File
@@ -16,8 +16,6 @@ package caddy
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http"
"reflect" "reflect"
"sync" "sync"
"testing" "testing"
@@ -115,7 +113,7 @@ func TestUnsyncedConfigAccess(t *testing.T) {
} }
// decode the expected config so we can do a convenient DeepEqual // decode the expected config so we can do a convenient DeepEqual
var expectedDecoded any var expectedDecoded interface{}
err = json.Unmarshal([]byte(tc.expect), &expectedDecoded) err = json.Unmarshal([]byte(tc.expect), &expectedDecoded)
if err != nil { if err != nil {
t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err) t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err)
@@ -141,57 +139,10 @@ func TestLoadConcurrent(t *testing.T) {
wg.Done() wg.Done()
}() }()
} }
wg.Wait() wg.Wait()
} }
type fooModule struct {
IntField int
StrField string
}
func (fooModule) CaddyModule() ModuleInfo {
return ModuleInfo{
ID: "foo",
New: func() Module { return new(fooModule) },
}
}
func (fooModule) Start() error { return nil }
func (fooModule) Stop() error { return nil }
func TestETags(t *testing.T) {
RegisterModule(fooModule{})
if err := Load([]byte(`{"admin": {"listen": "localhost:2999"}, "apps": {"foo": {"strField": "abc", "intField": 0}}}`), true); err != nil {
t.Fatalf("loading: %s", err)
}
const key = "/" + rawConfigKey + "/apps/foo"
// try update the config with the wrong etag
err := changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}}`), fmt.Sprintf(`"/%s not_an_etag"`, rawConfigKey), false)
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
t.Fatalf("expected precondition failed; got %v", err)
}
// get the etag
hash := etagHasher()
if err := readConfig(key, hash); err != nil {
t.Fatalf("reading: %s", err)
}
// do the same update with the correct key
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}`), makeEtag(key, hash), false)
if err != nil {
t.Fatalf("expected update to work; got %v", err)
}
// now try another update. The hash should no longer match and we should get precondition failed
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 2}`), makeEtag(key, hash), false)
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
t.Fatalf("expected precondition failed; got %v", err)
}
}
func BenchmarkLoad(b *testing.B) { func BenchmarkLoad(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
Load(testCfg, true) Load(testCfg, true)
+117 -361
View File
@@ -17,11 +17,10 @@ package caddy
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
@@ -31,7 +30,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/caddyserver/caddy/v2/notify" "github.com/caddyserver/caddy/v2/notify"
@@ -103,50 +101,26 @@ func Run(cfg *Config) error {
// if it is different from the current config or // if it is different from the current config or
// forceReload is true. // forceReload is true.
func Load(cfgJSON []byte, forceReload bool) error { func Load(cfgJSON []byte, forceReload bool) error {
if err := notify.Reloading(); err != nil { if err := notify.NotifyReloading(); err != nil {
Log().Error("unable to notify service manager of reloading state", zap.Error(err)) Log().Error("unable to notify reloading to service manager", zap.Error(err))
} }
// after reload, notify system of success or, if
// failure, update with status (error message)
var err error
defer func() { defer func() {
if err != nil { if err := notify.NotifyReadiness(); err != nil {
if notifyErr := notify.Error(err, 0); notifyErr != nil { Log().Error("unable to notify readiness to service manager", zap.Error(err))
Log().Error("unable to notify to service manager of reload error",
zap.Error(notifyErr),
zap.String("reload_err", err.Error()))
}
return
}
if err := notify.Ready(); err != nil {
Log().Error("unable to notify to service manager of ready state", zap.Error(err))
} }
}() }()
err = changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, "", forceReload) return changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
if errors.Is(err, errSameConfig) {
err = nil // not really an error
}
return err
} }
// changeConfig changes the current config (rawCfg) according to the // changeConfig changes the current config (rawCfg) according to the
// method, traversed via the given path, and uses the given input as // method, traversed via the given path, and uses the given input as
// the new value (if applicable; i.e. "DELETE" doesn't have an input). // the new value (if applicable; i.e. "DELETE" doesn't have an input).
// If the resulting config is the same as the previous, no reload will // If the resulting config is the same as the previous, no reload will
// occur unless forceReload is true. If the config is unchanged and not // occur unless forceReload is true. This function is safe for
// forcefully reloaded, then errConfigUnchanged This function is safe for
// concurrent use. // concurrent use.
// The ifMatchHeader can optionally be given a string of the format: func changeConfig(method, path string, input []byte, forceReload bool) error {
//
// "<path> <hash>"
//
// where <path> is the absolute path in the config and <hash> is the expected hash of
// the config at that path. If the hash in the ifMatchHeader doesn't match
// the hash of the config, then an APIError with status 412 will be returned.
func changeConfig(method, path string, input []byte, ifMatchHeader string, forceReload bool) error {
switch method { switch method {
case http.MethodGet, case http.MethodGet,
http.MethodHead, http.MethodHead,
@@ -156,42 +130,8 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
return fmt.Errorf("method not allowed") return fmt.Errorf("method not allowed")
} }
rawCfgMu.Lock() currentCfgMu.Lock()
defer rawCfgMu.Unlock() defer currentCfgMu.Unlock()
if ifMatchHeader != "" {
// expect the first and last character to be quotes
if len(ifMatchHeader) < 2 || ifMatchHeader[0] != '"' || ifMatchHeader[len(ifMatchHeader)-1] != '"' {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed If-Match header; expect quoted string"),
}
}
// read out the parts
parts := strings.Fields(ifMatchHeader[1 : len(ifMatchHeader)-1])
if len(parts) != 2 {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed If-Match header; expect format \"<path> <hash>\""),
}
}
// get the current hash of the config
// at the given path
hash := etagHasher()
err := unsyncedConfigAccess(http.MethodGet, parts[0], nil, hash)
if err != nil {
return err
}
if hex.EncodeToString(hash.Sum(nil)) != parts[1] {
return APIError{
HTTPStatus: http.StatusPreconditionFailed,
Err: fmt.Errorf("If-Match header did not match current config hash"),
}
}
}
err := unsyncedConfigAccess(method, path, input, nil) err := unsyncedConfigAccess(method, path, input, nil)
if err != nil { if err != nil {
@@ -209,8 +149,8 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
// if nothing changed, no need to do a whole reload unless the client forces it // if nothing changed, no need to do a whole reload unless the client forces it
if !forceReload && bytes.Equal(rawCfgJSON, newCfg) { if !forceReload && bytes.Equal(rawCfgJSON, newCfg) {
Log().Info("config is unchanged") Log().Named("admin.api").Info("config is unchanged")
return errSameConfig return nil
} }
// find any IDs in this config and index them // find any IDs in this config and index them
@@ -232,7 +172,7 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
// with what caddy is still running; we need to // with what caddy is still running; we need to
// unmarshal it again because it's likely that // unmarshal it again because it's likely that
// pointers deep in our rawCfg map were modified // pointers deep in our rawCfg map were modified
var oldCfg any var oldCfg interface{}
err2 := json.Unmarshal(rawCfgJSON, &oldCfg) err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
if err2 != nil { if err2 != nil {
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2) err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
@@ -257,18 +197,18 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
// readConfig traverses the current config to path // readConfig traverses the current config to path
// and writes its JSON encoding to out. // and writes its JSON encoding to out.
func readConfig(path string, out io.Writer) error { func readConfig(path string, out io.Writer) error {
rawCfgMu.RLock() currentCfgMu.RLock()
defer rawCfgMu.RUnlock() defer currentCfgMu.RUnlock()
return unsyncedConfigAccess(http.MethodGet, path, nil, out) return unsyncedConfigAccess(http.MethodGet, path, nil, out)
} }
// indexConfigObjects recursively searches ptr for object fields named // indexConfigObjects recursively searches ptr for object fields named
// "@id" and maps that ID value to the full configPath in the index. // "@id" and maps that ID value to the full configPath in the index.
// This function is NOT safe for concurrent access; obtain a write lock // This function is NOT safe for concurrent access; obtain a write lock
// on currentCtxMu. // on currentCfgMu.
func indexConfigObjects(ptr any, configPath string, index map[string]string) error { func indexConfigObjects(ptr interface{}, configPath string, index map[string]string) error {
switch val := ptr.(type) { switch val := ptr.(type) {
case map[string]any: case map[string]interface{}:
for k, v := range val { for k, v := range val {
if k == idKey { if k == idKey {
switch idVal := v.(type) { switch idVal := v.(type) {
@@ -287,7 +227,7 @@ func indexConfigObjects(ptr any, configPath string, index map[string]string) err
return err return err
} }
} }
case []any: case []interface{}:
// traverse each element of the array recursively // traverse each element of the array recursively
for i := range val { for i := range val {
err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index) err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index)
@@ -305,7 +245,7 @@ func indexConfigObjects(ptr any, configPath string, index map[string]string) err
// it as the new config, replacing any other current config. // it as the new config, replacing any other current config.
// It does NOT update the raw config state, as this is a // It does NOT update the raw config state, as this is a
// lower-level function; most callers will want to use Load // lower-level function; most callers will want to use Load
// instead. A write lock on rawCfgMu is required! If // instead. A write lock on currentCfgMu is required! If
// allowPersist is false, it will not be persisted to disk, // allowPersist is false, it will not be persisted to disk,
// even if it is configured to. // even if it is configured to.
func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error { func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
@@ -314,7 +254,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
strippedCfgJSON := RemoveMetaFields(cfgJSON) strippedCfgJSON := RemoveMetaFields(cfgJSON)
var newCfg *Config var newCfg *Config
err := StrictUnmarshalJSON(strippedCfgJSON, &newCfg) err := strictUnmarshalJSON(strippedCfgJSON, &newCfg)
if err != nil { if err != nil {
return err return err
} }
@@ -329,24 +269,22 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
newCfg.Admin != nil && newCfg.Admin != nil &&
newCfg.Admin.Config != nil && newCfg.Admin.Config != nil &&
newCfg.Admin.Config.LoadRaw != nil && newCfg.Admin.Config.LoadRaw != nil &&
newCfg.Admin.Config.LoadDelay <= 0 { newCfg.Admin.Config.LoadInterval <= 0 {
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_delay") return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_interval")
} }
// run the new config and start all its apps // run the new config and start all its apps
ctx, err := run(newCfg, true) err = run(newCfg, true)
if err != nil { if err != nil {
return err return err
} }
// swap old context (including its config) with the new one // swap old config with the new one
currentCtxMu.Lock() oldCfg := currentCfg
oldCtx := currentCtx currentCfg = newCfg
currentCtx = ctx
currentCtxMu.Unlock()
// Stop, Cleanup each old app // Stop, Cleanup each old app
unsyncedStop(oldCtx) unsyncedStop(oldCfg)
// autosave a non-nil config, if not disabled // autosave a non-nil config, if not disabled
if allowPersist && if allowPersist &&
@@ -362,7 +300,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
zap.String("dir", dir), zap.String("dir", dir),
zap.Error(err)) zap.Error(err))
} else { } else {
err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0600) err := ioutil.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
if err == nil { if err == nil {
Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath)) Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath))
} else { } else {
@@ -390,7 +328,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
// This is a low-level function; most callers // This is a low-level function; most callers
// will want to use Run instead, which also // will want to use Run instead, which also
// updates the config's raw state. // updates the config's raw state.
func run(newCfg *Config, start bool) (Context, error) { func run(newCfg *Config, start bool) error {
// because we will need to roll back any state // because we will need to roll back any state
// modifications if this function errors, we // modifications if this function errors, we
// keep a single error value and scope all // keep a single error value and scope all
@@ -421,8 +359,8 @@ func run(newCfg *Config, start bool) (Context, error) {
cancel() cancel()
// also undo any other state changes we made // also undo any other state changes we made
if currentCtx.cfg != nil { if currentCfg != nil {
certmagic.Default.Storage = currentCtx.cfg.storage certmagic.Default.Storage = currentCfg.storage
} }
} }
}() }()
@@ -434,14 +372,14 @@ func run(newCfg *Config, start bool) (Context, error) {
} }
err = newCfg.Logging.openLogs(ctx) err = newCfg.Logging.openLogs(ctx)
if err != nil { if err != nil {
return ctx, err return err
} }
// start the admin endpoint (and stop any prior one) // start the admin endpoint (and stop any prior one)
if start { if start {
err = replaceLocalAdminServer(newCfg) err = replaceLocalAdminServer(newCfg)
if err != nil { if err != nil {
return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err) return fmt.Errorf("starting caddy administration endpoint: %v", err)
} }
} }
@@ -470,7 +408,7 @@ func run(newCfg *Config, start bool) (Context, error) {
return nil return nil
}() }()
if err != nil { if err != nil {
return ctx, err return err
} }
// Load and Provision each app and their submodules // Load and Provision each app and their submodules
@@ -483,23 +421,16 @@ func run(newCfg *Config, start bool) (Context, error) {
return nil return nil
}() }()
if err != nil { if err != nil {
return ctx, err return err
} }
if !start { if !start {
return ctx, nil return nil
}
// Provision any admin routers which may need to access
// some of the other apps at runtime
err = newCfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return ctx, err
} }
// Start // Start
err = func() error { err = func() error {
started := make([]string, 0, len(newCfg.apps)) var started []string
for name, a := range newCfg.apps { for name, a := range newCfg.apps {
err := a.Start() err := a.Start()
if err != nil { if err != nil {
@@ -519,12 +450,12 @@ func run(newCfg *Config, start bool) (Context, error) {
return nil return nil
}() }()
if err != nil { if err != nil {
return ctx, err return err
} }
// now that the user's config is running, finish setting up anything else, // now that the user's config is running, finish setting up anything else,
// such as remote admin endpoint, config loader, etc. // such as remote admin endpoint, config loader, etc.
return ctx, finishSettingUp(ctx, newCfg) return finishSettingUp(ctx, newCfg)
} }
// finishSettingUp should be run after all apps have successfully started. // finishSettingUp should be run after all apps have successfully started.
@@ -550,74 +481,49 @@ func finishSettingUp(ctx Context, cfg *Config) error {
if err != nil { if err != nil {
return fmt.Errorf("loading config loader module: %s", err) return fmt.Errorf("loading config loader module: %s", err)
} }
runLoadedConfig := func(config []byte) {
logger := Log().Named("config_loader").With( Log().Info("applying dynamically-loaded config", zap.String("loader_module", val.(Module).CaddyModule().ID.Name()), zap.Int("pull_interval", int(cfg.Admin.Config.LoadInterval)))
zap.String("module", val.(Module).CaddyModule().ID.Name()), currentCfgMu.Lock()
zap.Int("load_delay", int(cfg.Admin.Config.LoadDelay))) err := unsyncedDecodeAndRun(config, false)
currentCfgMu.Unlock()
runLoadedConfig := func(config []byte) error { if err == nil {
logger.Info("applying dynamically-loaded config") Log().Info("dynamically-loaded config applied successfully")
err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, "", false) } else {
if errors.Is(err, errSameConfig) { Log().Error("running dynamically-loaded config failed", zap.Error(err))
return err
} }
if err != nil {
logger.Error("failed to run dynamically-loaded config", zap.Error(err))
return err
}
logger.Info("successfully applied dynamically-loaded config")
return nil
} }
if cfg.Admin.Config.LoadInterval > 0 {
if cfg.Admin.Config.LoadDelay > 0 {
go func() { go func() {
// the loop is here to iterate ONLY if there is an error, a no-op config load, select {
// or an unchanged config; in which case we simply wait the delay and try again // if LoadInterval is positive, will wait for the interval and then run with new config
for { case <-time.After(time.Duration(cfg.Admin.Config.LoadInterval)):
timer := time.NewTimer(time.Duration(cfg.Admin.Config.LoadDelay)) loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
select { if err != nil {
case <-timer.C: Log().Error("loading dynamic config failed", zap.Error(err))
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx) return
if err != nil {
logger.Error("failed loading dynamic config; will retry", zap.Error(err))
continue
}
if loadedConfig == nil {
logger.Info("dynamically-loaded config was nil; will retry")
continue
}
err = runLoadedConfig(loadedConfig)
if errors.Is(err, errSameConfig) {
logger.Info("dynamically-loaded config was unchanged; will retry")
continue
}
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
logger.Info("stopping dynamic config loading")
} }
break runLoadedConfig(loadedConfig)
case <-ctx.Done():
return
} }
}() }()
} else { } else {
// if no LoadDelay is provided, will load config synchronously // if no LoadInterval is provided, will load config synchronously
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx) loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
if err != nil { if err != nil {
return fmt.Errorf("loading dynamic config from %T: %v", val, err) return fmt.Errorf("loading dynamic config from %T: %v", val, err)
} }
// do this in a goroutine so current config can finish being loaded; otherwise deadlock // do this in a goroutine so current config can finish being loaded; otherwise deadlock
go func() { _ = runLoadedConfig(loadedConfig) }() go runLoadedConfig(loadedConfig)
} }
} }
return nil return nil
} }
// ConfigLoader is a type that can load a Caddy config. If // ConfigLoader is a type that can load a Caddy config. The
// the return value is non-nil, it must be valid Caddy JSON; // returned config must be valid Caddy JSON.
// if nil or with non-nil error, it is considered to be a
// no-op load and may be retried later.
type ConfigLoader interface { type ConfigLoader interface {
LoadConfig(Context) ([]byte, error) LoadConfig(Context) ([]byte, error)
} }
@@ -629,42 +535,29 @@ type ConfigLoader interface {
// stop the others. Stop should only be called // stop the others. Stop should only be called
// if not replacing with a new config. // if not replacing with a new config.
func Stop() error { func Stop() error {
currentCtxMu.RLock() currentCfgMu.Lock()
ctx := currentCtx defer currentCfgMu.Unlock()
currentCtxMu.RUnlock() unsyncedStop(currentCfg)
currentCfg = nil
rawCfgMu.Lock()
unsyncedStop(ctx)
currentCtxMu.Lock()
currentCtx = Context{}
currentCtxMu.Unlock()
rawCfgJSON = nil rawCfgJSON = nil
rawCfgIndex = nil rawCfgIndex = nil
rawCfg[rawConfigKey] = nil rawCfg[rawConfigKey] = nil
rawCfgMu.Unlock()
return nil return nil
} }
// unsyncedStop stops ctx from running, but has // unsyncedStop stops cfg from running, but has
// no locking around ctx. It is a no-op if ctx has a // no locking around cfg. It is a no-op if cfg is
// nil cfg. If any app returns an error when stopping, // nil. If any app returns an error when stopping,
// it is logged and the function continues stopping // it is logged and the function continues stopping
// the next app. This function assumes all apps in // the next app. This function assumes all apps in
// ctx were successfully started first. // cfg were successfully started first.
// func unsyncedStop(cfg *Config) {
// A lock on rawCfgMu is required, even though this if cfg == nil {
// function does not access rawCfg, that lock
// synchronizes the stop/start of apps.
func unsyncedStop(ctx Context) {
if ctx.cfg == nil {
return return
} }
// stop each app // stop each app
for name, a := range ctx.cfg.apps { for name, a := range cfg.apps {
err := a.Stop() err := a.Stop()
if err != nil { if err != nil {
log.Printf("[ERROR] stop %s: %v", name, err) log.Printf("[ERROR] stop %s: %v", name, err)
@@ -672,13 +565,13 @@ func unsyncedStop(ctx Context) {
} }
// clean up all modules // clean up all modules
ctx.cfg.cancelFunc() cfg.cancelFunc()
} }
// Validate loads, provisions, and validates // Validate loads, provisions, and validates
// cfg, but does not start running it. // cfg, but does not start running it.
func Validate(cfg *Config) error { func Validate(cfg *Config) error {
_, err := run(cfg, false) err := run(cfg, false)
if err == nil { if err == nil {
cfg.cancelFunc() // call Cleanup on all modules cfg.cancelFunc() // call Cleanup on all modules
} }
@@ -691,15 +584,7 @@ func Validate(cfg *Config) error {
// PID file, and shuts down admin endpoint(s) in a goroutine. // PID file, and shuts down admin endpoint(s) in a goroutine.
// Errors are logged along the way, and an appropriate exit // Errors are logged along the way, and an appropriate exit
// code is emitted. // code is emitted.
func exitProcess(ctx context.Context, logger *zap.Logger) { func exitProcess(logger *zap.Logger) {
// let the rest of the program know we're quitting
atomic.StoreInt32(exiting, 1)
// give the OS or service/process manager our 2 weeks' notice: we quit
if err := notify.Stopping(); err != nil {
Log().Error("unable to notify service manager of stopping state", zap.Error(err))
}
if logger == nil { if logger == nil {
logger = Log() logger = Log()
} }
@@ -714,7 +599,7 @@ func exitProcess(ctx context.Context, logger *zap.Logger) {
} }
// clean up certmagic locks // clean up certmagic locks
certmagic.CleanUpOwnLocks(ctx, logger) certmagic.CleanUpOwnLocks(logger)
// remove pidfile // remove pidfile
if pidfile != "" { if pidfile != "" {
@@ -759,12 +644,6 @@ func exitProcess(ctx context.Context, logger *zap.Logger) {
}() }()
} }
var exiting = new(int32) // accessed atomically
// Exiting returns true if the process is exiting.
// EXPERIMENTAL API: subject to change or removal.
func Exiting() bool { return atomic.LoadInt32(exiting) == 1 }
// Duration can be an integer or a string. An integer is // Duration can be an integer or a string. An integer is
// interpreted as nanoseconds. If a string, it is a Go // interpreted as nanoseconds. If a string, it is a Go
// time.Duration value such as `300ms`, `1.5h`, or `2h45m`; // time.Duration value such as `300ms`, `1.5h`, or `2h45m`;
@@ -789,12 +668,8 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
// ParseDuration parses a duration string, adding // ParseDuration parses a duration string, adding
// support for the "d" unit meaning number of days, // support for the "d" unit meaning number of days,
// where a day is assumed to be 24h. The maximum // where a day is assumed to be 24h.
// input string length is 1024.
func ParseDuration(s string) (time.Duration, error) { func ParseDuration(s string) (time.Duration, error) {
if len(s) > 1024 {
return 0, fmt.Errorf("parsing duration: input string too long")
}
var inNumber bool var inNumber bool
var numStart int var numStart int
for i := 0; i < len(s); i++ { for i := 0; i < len(s); i++ {
@@ -825,13 +700,13 @@ func ParseDuration(s string) (time.Duration, error) {
// have its own unique ID. // have its own unique ID.
func InstanceID() (uuid.UUID, error) { func InstanceID() (uuid.UUID, error) {
uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid") uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid")
uuidFileBytes, err := os.ReadFile(uuidFilePath) uuidFileBytes, err := ioutil.ReadFile(uuidFilePath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
uuid, err := uuid.NewRandom() uuid, err := uuid.NewRandom()
if err != nil { if err != nil {
return uuid, err return uuid, err
} }
err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0600) err = ioutil.WriteFile(uuidFilePath, []byte(uuid.String()), 0600)
return uuid, err return uuid, err
} else if err != nil { } else if err != nil {
return [16]byte{}, err return [16]byte{}, err
@@ -839,144 +714,36 @@ func InstanceID() (uuid.UUID, error) {
return uuid.ParseBytes(uuidFileBytes) return uuid.ParseBytes(uuidFileBytes)
} }
// CustomVersion is an optional string that overrides Caddy's // GoModule returns the build info of this Caddy
// reported version. It can be helpful when downstream packagers // build from debug.BuildInfo (requires Go modules).
// need to manually set Caddy's version. If no other version // If no version information is available, a non-nil
// information is available, the short form version (see // value will still be returned, but with an
// Version()) will be set to CustomVersion, and the full version // unknown version.
// will include CustomVersion at the beginning. func GoModule() *debug.Module {
// var mod debug.Module
// Set this variable during `go build` with `-ldflags`: return goModule(&mod)
//
// -ldflags '-X github.com/caddyserver/caddy/v2.CustomVersion=v2.6.2'
//
// for example.
var CustomVersion string
// Version returns the Caddy version in a simple/short form, and
// a full version string. The short form will not have spaces and
// is intended for User-Agent strings and similar, but may be
// omitting valuable information. Note that Caddy must be compiled
// in a special way to properly embed complete version information.
// First this function tries to get the version from the embedded
// build info provided by go.mod dependencies; then it tries to
// get info from embedded VCS information, which requires having
// built Caddy from a git repository. If no version is available,
// this function returns "(devel)" because Go uses that, but for
// the simple form we change it to "unknown". If still no version
// is available (e.g. no VCS repo), then it will use CustomVersion;
// CustomVersion is always prepended to the full version string.
//
// See relevant Go issues: https://github.com/golang/go/issues/29228
// and https://github.com/golang/go/issues/50603.
//
// This function is experimental and subject to change or removal.
func Version() (simple, full string) {
// the currently-recommended way to build Caddy involves
// building it as a dependency so we can extract version
// information from go.mod tooling; once the upstream
// Go issues are fixed, we should just be able to use
// bi.Main... hopefully.
var module *debug.Module
bi, ok := debug.ReadBuildInfo()
if !ok {
if CustomVersion != "" {
full = CustomVersion
simple = CustomVersion
return
}
full = "unknown"
simple = "unknown"
return
}
// find the Caddy module in the dependency list
for _, dep := range bi.Deps {
if dep.Path == ImportPath {
module = dep
break
}
}
if module != nil {
simple, full = module.Version, module.Version
if module.Sum != "" {
full += " " + module.Sum
}
if module.Replace != nil {
full += " => " + module.Replace.Path
if module.Replace.Version != "" {
simple = module.Replace.Version + "_custom"
full += "@" + module.Replace.Version
}
if module.Replace.Sum != "" {
full += " " + module.Replace.Sum
}
}
}
if full == "" {
var vcsRevision string
var vcsTime time.Time
var vcsModified bool
for _, setting := range bi.Settings {
switch setting.Key {
case "vcs.revision":
vcsRevision = setting.Value
case "vcs.time":
vcsTime, _ = time.Parse(time.RFC3339, setting.Value)
case "vcs.modified":
vcsModified, _ = strconv.ParseBool(setting.Value)
}
}
if vcsRevision != "" {
var modified string
if vcsModified {
modified = "+modified"
}
full = fmt.Sprintf("%s%s (%s)", vcsRevision, modified, vcsTime.Format(time.RFC822))
simple = vcsRevision
// use short checksum for simple, if hex-only
if _, err := hex.DecodeString(simple); err == nil {
simple = simple[:8]
}
// append date to simple since it can be convenient
// to know the commit date as part of the version
if !vcsTime.IsZero() {
simple += "-" + vcsTime.Format("20060102")
}
}
}
if full == "" {
if CustomVersion != "" {
full = CustomVersion
} else {
full = "unknown"
}
} else if CustomVersion != "" {
full = CustomVersion + " " + full
}
if simple == "" || simple == "(devel)" {
if CustomVersion != "" {
simple = CustomVersion
} else {
simple = "unknown"
}
}
return
} }
// ActiveContext returns the currently-active context. // goModule holds the actual implementation of GoModule.
// This function is experimental and might be changed // Allocating debug.Module in GoModule() and passing a
// or removed in the future. // reference to goModule enables mid-stack inlining.
func ActiveContext() Context { func goModule(mod *debug.Module) *debug.Module {
currentCtxMu.RLock() mod.Version = "unknown"
defer currentCtxMu.RUnlock() bi, ok := debug.ReadBuildInfo()
return currentCtx if ok {
mod.Path = bi.Main.Path
// The recommended way to build Caddy involves
// creating a separate main module, which
// TODO: track related Go issue: https://github.com/golang/go/issues/29228
// once that issue is fixed, we should just be able to use bi.Main... hopefully.
for _, dep := range bi.Deps {
if dep.Path == ImportPath {
return dep
}
}
return &bi.Main
}
return mod
} }
// CtxKey is a value type for use with context.WithValue. // CtxKey is a value type for use with context.WithValue.
@@ -984,19 +751,18 @@ type CtxKey string
// This group of variables pertains to the current configuration. // This group of variables pertains to the current configuration.
var ( var (
// currentCtx is the root context for the currently-running // currentCfgMu protects everything in this var block.
// configuration, which can be accessed through this value. currentCfgMu sync.RWMutex
// If the Config contained in this value is not nil, then
// a config is currently active/running. // currentCfg is the currently-running configuration.
currentCtx Context currentCfg *Config
currentCtxMu sync.RWMutex
// rawCfg is the current, generic-decoded configuration; // rawCfg is the current, generic-decoded configuration;
// we initialize it as a map with one field ("config") // we initialize it as a map with one field ("config")
// to maintain parity with the API endpoint and to avoid // to maintain parity with the API endpoint and to avoid
// the special case of having to access/mutate the variable // the special case of having to access/mutate the variable
// directly without traversing into it. // directly without traversing into it.
rawCfg = map[string]any{ rawCfg = map[string]interface{}{
rawConfigKey: nil, rawConfigKey: nil,
} }
@@ -1007,17 +773,7 @@ var (
// rawCfgIndex is the map of user-assigned ID to expanded // rawCfgIndex is the map of user-assigned ID to expanded
// path, for converting /id/ paths to /config/ paths. // path, for converting /id/ paths to /config/ paths.
rawCfgIndex map[string]string rawCfgIndex map[string]string
// rawCfgMu protects all the rawCfg fields and also
// essentially synchronizes config changes/reloads.
rawCfgMu sync.RWMutex
) )
// 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.
var errSameConfig = errors.New("config is unchanged")
// ImportPath is the package import path for Caddy core. // ImportPath is the package import path for Caddy core.
// This identifier may be removed in the future.
const ImportPath = "github.com/caddyserver/caddy/v2" const ImportPath = "github.com/caddyserver/caddy/v2"
+7 -7
View File
@@ -29,12 +29,12 @@ type Adapter struct {
} }
// Adapt converts the Caddyfile config in body to Caddy JSON. // Adapt converts the Caddyfile config in body to Caddy JSON.
func (a Adapter) Adapt(body []byte, options map[string]any) ([]byte, []caddyconfig.Warning, error) { func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []caddyconfig.Warning, error) {
if a.ServerType == nil { if a.ServerType == nil {
return nil, nil, fmt.Errorf("no server type") return nil, nil, fmt.Errorf("no server type")
} }
if options == nil { if options == nil {
options = make(map[string]any) options = make(map[string]interface{})
} }
filename, _ := options["filename"].(string) filename, _ := options["filename"].(string)
@@ -54,7 +54,7 @@ func (a Adapter) Adapt(body []byte, options map[string]any) ([]byte, []caddyconf
// lint check: see if input was properly formatted; sometimes messy files files parse // lint check: see if input was properly formatted; sometimes messy files files parse
// successfully but result in logical errors (the Caddyfile is a bad format, I'm sorry) // successfully but result in logical errors (the Caddyfile is a bad format, I'm sorry)
if warning, different := FormattingDifference(filename, body); different { if warning, different := formattingDifference(filename, body); different {
warnings = append(warnings, warning) warnings = append(warnings, warning)
} }
@@ -63,10 +63,10 @@ func (a Adapter) Adapt(body []byte, options map[string]any) ([]byte, []caddyconf
return result, warnings, err return result, warnings, err
} }
// FormattingDifference returns a warning and true if the formatted version // formattingDifference returns a warning and true if the formatted version
// is any different from the input; empty warning and false otherwise. // is any different from the input; empty warning and false otherwise.
// TODO: also perform this check on imported files // TODO: also perform this check on imported files
func FormattingDifference(filename string, body []byte) (caddyconfig.Warning, bool) { func formattingDifference(filename string, body []byte) (caddyconfig.Warning, bool) {
// replace windows-style newlines to normalize comparison // replace windows-style newlines to normalize comparison
normalizedBody := bytes.Replace(body, []byte("\r\n"), []byte("\n"), -1) normalizedBody := bytes.Replace(body, []byte("\r\n"), []byte("\n"), -1)
@@ -88,7 +88,7 @@ func FormattingDifference(filename string, body []byte) (caddyconfig.Warning, bo
return caddyconfig.Warning{ return caddyconfig.Warning{
File: filename, File: filename,
Line: line, Line: line,
Message: "Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies", Message: "input is not formatted with 'caddy fmt'",
}, true }, true
} }
@@ -116,7 +116,7 @@ type ServerType interface {
// (e.g. CLI flags) and creates a Caddy // (e.g. CLI flags) and creates a Caddy
// config, along with any warnings or // config, along with any warnings or
// an error. // an error.
Setup([]ServerBlock, map[string]any) (*caddy.Config, []caddyconfig.Warning, error) Setup([]ServerBlock, map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error)
} }
// UnmarshalModule instantiates a module with the given ID and invokes // UnmarshalModule instantiates a module with the given ID and invokes
+25 -113
View File
@@ -19,7 +19,6 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"strconv"
"strings" "strings"
) )
@@ -101,12 +100,12 @@ func (d *Dispenser) nextOnSameLine() bool {
d.cursor++ d.cursor++
return true return true
} }
if d.cursor >= len(d.tokens)-1 { if d.cursor >= len(d.tokens) {
return false return false
} }
curr := d.tokens[d.cursor] if d.cursor < len(d.tokens)-1 &&
next := d.tokens[d.cursor+1] d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
if !isNextOnNewLine(curr, next) { d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
d.cursor++ d.cursor++
return true return true
} }
@@ -122,12 +121,12 @@ func (d *Dispenser) NextLine() bool {
d.cursor++ d.cursor++
return true return true
} }
if d.cursor >= len(d.tokens)-1 { if d.cursor >= len(d.tokens) {
return false return false
} }
curr := d.tokens[d.cursor] if d.cursor < len(d.tokens)-1 &&
next := d.tokens[d.cursor+1] (d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
if isNextOnNewLine(curr, next) { d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
d.cursor++ d.cursor++
return true return true
} }
@@ -146,15 +145,15 @@ func (d *Dispenser) NextLine() bool {
// //
// Proper use of this method looks like this: // Proper use of this method looks like this:
// //
// for nesting := d.Nesting(); d.NextBlock(nesting); { // for nesting := d.Nesting(); d.NextBlock(nesting); {
// } // }
// //
// However, in simple cases where it is known that the // However, in simple cases where it is known that the
// Dispenser is new and has not already traversed state // Dispenser is new and has not already traversed state
// by a loop over NextBlock(), this will do: // by a loop over NextBlock(), this will do:
// //
// for d.NextBlock(0) { // for d.NextBlock(0) {
// } // }
// //
// As with other token parsing logic, a loop over // As with other token parsing logic, a loop over
// NextBlock() should be contained within a loop over // NextBlock() should be contained within a loop over
@@ -202,46 +201,6 @@ func (d *Dispenser) Val() string {
return d.tokens[d.cursor].Text return d.tokens[d.cursor].Text
} }
// ValRaw gets the raw text of the current token (including quotes).
// If the token was a heredoc, then the delimiter is not included,
// because that is not relevant to any unmarshaling logic at this time.
// If there is no token loaded, it returns empty string.
func (d *Dispenser) ValRaw() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return ""
}
quote := d.tokens[d.cursor].wasQuoted
if quote > 0 && quote != '<' {
// string literal
return string(quote) + d.tokens[d.cursor].Text + string(quote)
}
return d.tokens[d.cursor].Text
}
// ScalarVal gets value of the current token, converted to the closest
// scalar type. If there is no token loaded, it returns nil.
func (d *Dispenser) ScalarVal() any {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return nil
}
quote := d.tokens[d.cursor].wasQuoted
text := d.tokens[d.cursor].Text
if quote > 0 {
return text // string literal
}
if num, err := strconv.Atoi(text); err == nil {
return num
}
if num, err := strconv.ParseFloat(text, 64); err == nil {
return num
}
if bool, err := strconv.ParseBool(text); err == nil {
return bool
}
return text
}
// Line gets the line number of the current token. // Line gets the line number of the current token.
// If there is no token loaded, it returns 0. // If there is no token loaded, it returns 0.
func (d *Dispenser) Line() int { func (d *Dispenser) Line() int {
@@ -290,19 +249,6 @@ func (d *Dispenser) AllArgs(targets ...*string) bool {
return true return true
} }
// CountRemainingArgs counts the amount of remaining arguments
// (tokens on the same line) without consuming the tokens.
func (d *Dispenser) CountRemainingArgs() int {
count := 0
for d.NextArg() {
count++
}
for i := 0; i < count; i++ {
d.Prev()
}
return count
}
// RemainingArgs loads any more arguments (tokens on the same line) // RemainingArgs loads any more arguments (tokens on the same line)
// into a slice and returns them. Open curly brace tokens also indicate // 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 end of arguments, and the curly brace is not included in
@@ -315,18 +261,6 @@ func (d *Dispenser) RemainingArgs() []string {
return args return args
} }
// 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.
func (d *Dispenser) RemainingArgsRaw() []string {
var args []string
for d.NextArg() {
args = append(args, d.ValRaw())
}
return args
}
// NewFromNextSegment returns a new dispenser with a copy of // NewFromNextSegment returns a new dispenser with a copy of
// the tokens from the current token until the end of the // the tokens from the current token until the end of the
// "directive" whether that be to the end of the line or // "directive" whether that be to the end of the line or
@@ -399,7 +333,7 @@ func (d *Dispenser) ArgErr() error {
// SyntaxErr creates a generic syntax error which explains what was // SyntaxErr creates a generic syntax error which explains what was
// found and what was expected. // found and what was expected.
func (d *Dispenser) SyntaxErr(expected string) error { func (d *Dispenser) SyntaxErr(expected string) error {
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s', import chain: ['%s']", d.File(), d.Line(), d.Val(), expected, strings.Join(d.Token().imports, "','")) msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
return errors.New(msg) return errors.New(msg)
} }
@@ -415,13 +349,9 @@ func (d *Dispenser) Err(msg string) error {
} }
// Errf is like Err, but for formatted error messages // Errf is like Err, but for formatted error messages
func (d *Dispenser) Errf(format string, args ...any) error { func (d *Dispenser) Errf(format string, args ...interface{}) error {
return d.WrapErr(fmt.Errorf(format, args...)) err := fmt.Errorf(format, args...)
} return fmt.Errorf("%s:%d - Error during parsing: %w", d.File(), d.Line(), err)
// WrapErr takes an existing error and adds the Caddyfile file and line number.
func (d *Dispenser) WrapErr(err error) error {
return fmt.Errorf("%s:%d - Error during parsing: %w, import chain: ['%s']", d.File(), d.Line(), err, strings.Join(d.Token().imports, "','"))
} }
// Delete deletes the current token and returns the updated slice // Delete deletes the current token and returns the updated slice
@@ -441,14 +371,14 @@ func (d *Dispenser) Delete() []Token {
return d.tokens return d.tokens
} }
// DeleteN is the same as Delete, but can delete many tokens at once. // numLineBreaks counts how many line breaks are in the token
// If there aren't N tokens available to delete, none are deleted. // value given by the token index tknIdx. It returns 0 if the
func (d *Dispenser) DeleteN(amount int) []Token { // token does not exist or there are no line breaks.
if amount > 0 && d.cursor >= (amount-1) && d.cursor <= len(d.tokens)-1 { func (d *Dispenser) numLineBreaks(tknIdx int) int {
d.tokens = append(d.tokens[:d.cursor-(amount-1)], d.tokens[d.cursor+1:]...) if tknIdx < 0 || tknIdx >= len(d.tokens) {
d.cursor -= amount return 0
} }
return d.tokens return strings.Count(d.tokens[tknIdx].Text, "\n")
} }
// isNewLine determines whether the current token is on a different // isNewLine determines whether the current token is on a different
@@ -461,24 +391,6 @@ func (d *Dispenser) isNewLine() bool {
if d.cursor > len(d.tokens)-1 { if d.cursor > len(d.tokens)-1 {
return false return false
} }
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
prev := d.tokens[d.cursor-1] d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
curr := d.tokens[d.cursor]
return isNextOnNewLine(prev, curr)
}
// isNextOnNewLine determines whether the current token is on a different
// line (higher line number) than the next token. It handles imported
// tokens correctly. If there isn't a next token, it returns true.
func (d *Dispenser) isNextOnNewLine() bool {
if d.cursor < 0 {
return false
}
if d.cursor >= len(d.tokens)-1 {
return true
}
curr := d.tokens[d.cursor]
next := d.tokens[d.cursor+1]
return isNextOnNewLine(curr, next)
} }
View File
+1 -4
View File
@@ -153,10 +153,7 @@ func Format(input []byte) []byte {
openBraceWritten = true openBraceWritten = true
nextLine() nextLine()
newLines = 0 newLines = 0
// prevent infinite nesting from ridiculous inputs (issue #4169) nesting++
if nesting < 10 {
nesting++
}
} }
switch { switch {
+1 -1
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build gofuzz // +build gofuzz
package caddyfile package caddyfile
-5
View File
@@ -179,11 +179,6 @@ d {
{$F} {$F}
}`, }`,
}, },
{
description: "env var placeholders with port",
input: `:{$PORT}`,
expect: `:{$PORT}`,
},
{ {
description: "comments", description: "comments",
input: `#a "\n" input: `#a "\n"
-142
View File
@@ -1,142 +0,0 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"regexp"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2"
"go.uber.org/zap"
)
// parseVariadic determines if the token is a variadic placeholder,
// and if so, determines the index range (start/end) of args to use.
// Returns a boolean signaling whether a variadic placeholder was found,
// and the start and end indices.
func parseVariadic(token Token, argCount int) (bool, int, int) {
if !strings.HasPrefix(token.Text, "{args[") {
return false, 0, 0
}
if !strings.HasSuffix(token.Text, "]}") {
return false, 0, 0
}
argRange := strings.TrimSuffix(strings.TrimPrefix(token.Text, "{args["), "]}")
if argRange == "" {
caddy.Log().Named("caddyfile").Warn(
"Placeholder "+token.Text+" cannot have an empty index",
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
return false, 0, 0
}
start, end, found := strings.Cut(argRange, ":")
// If no ":" delimiter is found, this is not a variadic.
// The replacer will pick this up.
if !found {
return false, 0, 0
}
var (
startIndex = 0
endIndex = argCount
err error
)
if start != "" {
startIndex, err = strconv.Atoi(start)
if err != nil {
caddy.Log().Named("caddyfile").Warn(
"Variadic placeholder "+token.Text+" has an invalid start index",
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
return false, 0, 0
}
}
if end != "" {
endIndex, err = strconv.Atoi(end)
if err != nil {
caddy.Log().Named("caddyfile").Warn(
"Variadic placeholder "+token.Text+" has an invalid end index",
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
return false, 0, 0
}
}
// bound check
if startIndex < 0 || startIndex > endIndex || endIndex > argCount {
caddy.Log().Named("caddyfile").Warn(
"Variadic placeholder "+token.Text+" indices are out of bounds, only "+strconv.Itoa(argCount)+" argument(s) exist",
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
return false, 0, 0
}
return true, startIndex, endIndex
}
// makeArgsReplacer prepares a Replacer which can replace
// non-variadic args placeholders in imported tokens.
func makeArgsReplacer(args []string) *caddy.Replacer {
repl := caddy.NewEmptyReplacer()
repl.Map(func(key string) (any, bool) {
// TODO: Remove the deprecated {args.*} placeholder
// support at some point in the future
if matches := argsRegexpIndexDeprecated.FindStringSubmatch(key); len(matches) > 0 {
value, err := strconv.Atoi(matches[1])
if err != nil {
caddy.Log().Named("caddyfile").Warn(
"Placeholder {args." + matches[1] + "} has an invalid index")
return nil, false
}
if value >= len(args) {
caddy.Log().Named("caddyfile").Warn(
"Placeholder {args." + matches[1] + "} index is out of bounds, only " + strconv.Itoa(len(args)) + " argument(s) exist")
return nil, false
}
caddy.Log().Named("caddyfile").Warn(
"Placeholder {args." + matches[1] + "} deprecated, use {args[" + matches[1] + "]} instead")
return args[value], true
}
// Handle args[*] form
if matches := argsRegexpIndex.FindStringSubmatch(key); len(matches) > 0 {
if strings.Contains(matches[1], ":") {
caddy.Log().Named("caddyfile").Warn(
"Variadic placeholder {args[" + matches[1] + "]} must be a token on its own")
return nil, false
}
value, err := strconv.Atoi(matches[1])
if err != nil {
caddy.Log().Named("caddyfile").Warn(
"Placeholder {args[" + matches[1] + "]} has an invalid index")
return nil, false
}
if value >= len(args) {
caddy.Log().Named("caddyfile").Warn(
"Placeholder {args[" + matches[1] + "]} index is out of bounds, only " + strconv.Itoa(len(args)) + " argument(s) exist")
return nil, false
}
return args[value], true
}
// Not an args placeholder, ignore
return nil, false
})
return repl
}
var (
argsRegexpIndexDeprecated = regexp.MustCompile(`args\.(.+)`)
argsRegexpIndex = regexp.MustCompile(`args\[(.+)]`)
)
Regular → Executable
+33 -207
View File
@@ -1,4 +1,4 @@
// Copyright 2015 Matthew Holt and The Caddy Authors // Copyright 2015 Light Code Labs, LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -17,10 +17,7 @@ package caddyfile
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"fmt"
"io" "io"
"regexp"
"strings"
"unicode" "unicode"
) )
@@ -38,41 +35,14 @@ type (
// Token represents a single parsable unit. // Token represents a single parsable unit.
Token struct { Token struct {
File string File string
imports []string Line int
Line int Text string
Text string inSnippet bool
wasQuoted rune // enclosing quote character, if any snippetName string
heredocMarker string
snippetName string
} }
) )
// Tokenize takes bytes as input and lexes it into
// a list of tokens that can be parsed as a Caddyfile.
// Also takes a filename to fill the token's File as
// the source of the tokens, which is important to
// determine relative paths for `import` directives.
func Tokenize(input []byte, filename string) ([]Token, error) {
l := lexer{}
if err := l.load(bytes.NewReader(input)); err != nil {
return nil, err
}
var tokens []Token
for {
found, err := l.next()
if err != nil {
return nil, err
}
if !found {
break
}
l.token.File = filename
tokens = append(tokens, l.token)
}
return tokens, nil
}
// load prepares the lexer to scan an input for tokens. // load prepares the lexer to scan an input for tokens.
// It discards any leading byte order mark. // It discards any leading byte order mark.
func (l *lexer) load(input io.Reader) error { func (l *lexer) load(input io.Reader) error {
@@ -104,93 +74,27 @@ func (l *lexer) load(input io.Reader) error {
// may be escaped. The rest of the line is skipped // may be escaped. The rest of the line is skipped
// if a "#" character is read in. Returns true if // if a "#" character is read in. Returns true if
// a token was loaded; false otherwise. // a token was loaded; false otherwise.
func (l *lexer) next() (bool, error) { func (l *lexer) next() bool {
var val []rune var val []rune
var comment, quoted, btQuoted, inHeredoc, heredocEscaped, escaped bool var comment, quoted, btQuoted, escaped bool
var heredocMarker string
makeToken := func(quoted rune) bool { makeToken := func() bool {
l.token.Text = string(val) l.token.Text = string(val)
l.token.wasQuoted = quoted
l.token.heredocMarker = heredocMarker
return true return true
} }
for { for {
// Read a character in; if err then if we had
// read some characters, make a token. If we
// reached EOF, then no more tokens to read.
// If no EOF, then we had a problem.
ch, _, err := l.reader.ReadRune() ch, _, err := l.reader.ReadRune()
if err != nil { if err != nil {
if len(val) > 0 { if len(val) > 0 {
if inHeredoc { return makeToken()
return false, fmt.Errorf("incomplete heredoc <<%s on line #%d, expected ending marker %s", heredocMarker, l.line+l.skippedLines, heredocMarker)
}
return makeToken(0), nil
} }
if err == io.EOF { if err == io.EOF {
return false, nil return false
} }
return false, err panic(err)
} }
// detect whether we have the start of a heredoc
if !inHeredoc && !heredocEscaped && len(val) > 1 && string(val[:2]) == "<<" {
if ch == '<' {
return false, fmt.Errorf("too many '<' for heredoc on line #%d; only use two, for example <<END", l.line)
}
if ch == '\r' {
continue
}
// after hitting a newline, we know that the heredoc marker
// is the characters after the two << and the newline.
// we reset the val because the heredoc is syntax we don't
// want to keep.
if ch == '\n' {
heredocMarker = string(val[2:])
if !heredocMarkerRegexp.Match([]byte(heredocMarker)) {
return false, fmt.Errorf("heredoc marker on line #%d must contain only alpha-numeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
}
inHeredoc = true
l.skippedLines++
val = nil
continue
}
val = append(val, ch)
continue
}
// if we're in a heredoc, all characters are read as-is
if inHeredoc {
val = append(val, ch)
if ch == '\n' {
l.skippedLines++
}
// check if we're done, i.e. that the last few characters are the marker
if len(val) > len(heredocMarker) && heredocMarker == string(val[len(val)-len(heredocMarker):]) {
// set the final value
val, err = l.finalizeHeredoc(val, heredocMarker)
if err != nil {
return false, err
}
// set the line counter, and make the token
l.line += l.skippedLines
l.skippedLines = 0
return makeToken('<'), nil
}
// stay in the heredoc until we find the ending marker
continue
}
// track whether we found an escape '\' for the next
// iteration to be contextually aware
if !escaped && !btQuoted && ch == '\\' { if !escaped && !btQuoted && ch == '\\' {
escaped = true escaped = true
continue continue
@@ -205,29 +109,26 @@ func (l *lexer) next() (bool, error) {
} }
escaped = false escaped = false
} else { } else {
if (quoted && ch == '"') || (btQuoted && ch == '`') { if quoted && ch == '"' {
return makeToken(ch), nil return makeToken()
}
if btQuoted && ch == '`' {
return makeToken()
} }
} }
// allow quoted text to wrap continue on multiple lines
if ch == '\n' { if ch == '\n' {
l.line += 1 + l.skippedLines l.line += 1 + l.skippedLines
l.skippedLines = 0 l.skippedLines = 0
} }
// collect this character as part of the quoted token
val = append(val, ch) val = append(val, ch)
continue continue
} }
if unicode.IsSpace(ch) { if unicode.IsSpace(ch) {
// ignore CR altogether, we only actually care about LF (\n)
if ch == '\r' { if ch == '\r' {
continue continue
} }
// end of the line
if ch == '\n' { if ch == '\n' {
// newlines can be escaped to chain arguments
// onto multiple lines; else, increment the line count
if escaped { if escaped {
l.skippedLines++ l.skippedLines++
escaped = false escaped = false
@@ -235,18 +136,14 @@ func (l *lexer) next() (bool, error) {
l.line += 1 + l.skippedLines l.line += 1 + l.skippedLines
l.skippedLines = 0 l.skippedLines = 0
} }
// comments (#) are single-line only
comment = false comment = false
} }
// any kind of space means we're at the end of this token
if len(val) > 0 { if len(val) > 0 {
return makeToken(0), nil return makeToken()
} }
continue continue
} }
// comments must be at the start of a token,
// in other words, preceded by space or newline
if ch == '#' && len(val) == 0 { if ch == '#' && len(val) == 0 {
comment = true comment = true
} }
@@ -267,12 +164,7 @@ func (l *lexer) next() (bool, error) {
} }
if escaped { if escaped {
// allow escaping the first < to skip the heredoc syntax val = append(val, '\\')
if ch == '<' {
heredocEscaped = true
} else {
val = append(val, '\\')
}
escaped = false escaped = false
} }
@@ -280,86 +172,20 @@ func (l *lexer) next() (bool, error) {
} }
} }
// finalizeHeredoc takes the runes read as the heredoc text and the marker, // Tokenize takes bytes as input and lexes it into
// and processes the text to strip leading whitespace, returning the final // a list of tokens that can be parsed as a Caddyfile.
// value without the leading whitespace. // Also takes a filename to fill the token's File as
func (l *lexer) finalizeHeredoc(val []rune, marker string) ([]rune, error) { // the source of the tokens, which is important to
stringVal := string(val) // determine relative paths for `import` directives.
func Tokenize(input []byte, filename string) ([]Token, error) {
// find the last newline of the heredoc, which is where the contents end l := lexer{}
lastNewline := strings.LastIndex(stringVal, "\n") if err := l.load(bytes.NewReader(input)); err != nil {
return nil, err
// collapse the content, then split into separate lines
lines := strings.Split(stringVal[:lastNewline+1], "\n")
// figure out how much whitespace we need to strip from the front of every line
// by getting the string that precedes the marker, on the last line
paddingToStrip := stringVal[lastNewline+1 : len(stringVal)-len(marker)]
// iterate over each line and strip the whitespace from the front
var out string
for lineNum, lineText := range lines[:len(lines)-1] {
// find an exact match for the padding
index := strings.Index(lineText, paddingToStrip)
// if the padding doesn't match exactly at the start then we can't safely strip
if index != 0 {
return nil, fmt.Errorf("mismatched leading whitespace in heredoc <<%s on line #%d [%s], expected whitespace [%s] to match the closing marker", marker, l.line+lineNum+1, lineText, paddingToStrip)
}
// strip, then append the line, with the newline, to the output.
// also removes all "\r" because Windows.
out += strings.ReplaceAll(lineText[len(paddingToStrip):]+"\n", "\r", "")
} }
var tokens []Token
// Remove the trailing newline from the loop for l.next() {
if len(out) > 0 && out[len(out)-1] == '\n' { l.token.File = filename
out = out[:len(out)-1] tokens = append(tokens, l.token)
} }
return tokens, nil
// return the final value
return []rune(out), nil
}
func (t Token) Quoted() bool {
return t.wasQuoted > 0
}
// NumLineBreaks counts how many line breaks are in the token text.
func (t Token) NumLineBreaks() int {
lineBreaks := strings.Count(t.Text, "\n")
if t.wasQuoted == '<' {
// heredocs have an extra linebreak because the opening
// delimiter is on its own line and is not included in the
// token Text itself, and the trailing newline is removed.
lineBreaks += 2
}
return lineBreaks
}
var heredocMarkerRegexp = regexp.MustCompile("^[A-Za-z0-9_-]+$")
// isNextOnNewLine tests whether t2 is on a different line from t1
func isNextOnNewLine(t1, t2 Token) bool {
// If the second token is from a different file,
// we can assume it's from a different line
if t1.File != t2.File {
return true
}
// If the second token is from a different import chain,
// we can assume it's from a different line
if len(t1.imports) != len(t2.imports) {
return true
}
for i, im := range t1.imports {
if im != t2.imports[i] {
return true
}
}
// If the first token (incl line breaks) ends
// on a line earlier than the next token,
// then the second token is on a new line
return t1.Line+t1.NumLineBreaks() < t2.Line
} }
+1 -1
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build gofuzz // +build gofuzz
package caddyfile package caddyfile
+11 -177
View File
@@ -1,4 +1,4 @@
// Copyright 2015 Matthew Holt and The Caddy Authors // Copyright 2015 Light Code Labs, LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -18,13 +18,13 @@ import (
"testing" "testing"
) )
type lexerTestCase struct {
input []byte
expected []Token
}
func TestLexer(t *testing.T) { func TestLexer(t *testing.T) {
testCases := []struct { testCases := []lexerTestCase{
input []byte
expected []Token
expectErr bool
errorMessage string
}{
{ {
input: []byte(`host:123`), input: []byte(`host:123`),
expected: []Token{ expected: []Token{
@@ -249,178 +249,12 @@ func TestLexer(t *testing.T) {
{Line: 1, Text: `quotes`}, {Line: 1, Text: `quotes`},
}, },
}, },
{
input: []byte(`heredoc <<EOF
content
EOF same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: "content"},
{Line: 3, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<VERY-LONG-MARKER
content
VERY-LONG-MARKER same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: "content"},
{Line: 3, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<EOF
extra-newline
EOF same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: "extra-newline\n"},
{Line: 4, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<EOF
EOF same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: ""},
{Line: 2, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<EOF
content
EOF same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: "content"},
{Line: 3, Text: `same-line-arg`},
},
},
{
input: []byte(`prev-line
heredoc <<EOF
multi
line
content
EOF same-line-arg
next-line
`),
expected: []Token{
{Line: 1, Text: `prev-line`},
{Line: 2, Text: `heredoc`},
{Line: 2, Text: "\tmulti\n\tline\n\tcontent"},
{Line: 6, Text: `same-line-arg`},
{Line: 7, Text: `next-line`},
},
},
{
input: []byte(`heredoc <EOF
content
EOF same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: `<EOF`},
{Line: 2, Text: `content`},
{Line: 3, Text: `EOF`},
{Line: 3, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<s
s
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: ""},
},
},
{
input: []byte("\u000Aheredoc \u003C\u003C\u0073\u0073\u000A\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F\u000A\u0073\u0073\u000A\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F\u000A\u00BF\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F"),
expected: []Token{
{
Line: 2,
Text: "heredoc",
},
{
Line: 2,
Text: "\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F",
},
{
Line: 5,
Text: "\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F",
},
{
Line: 6,
Text: "\u00BF\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F",
},
},
},
{
input: []byte(`heredoc <<HERE SAME LINE
content
HERE same-line-arg
`),
expectErr: true,
errorMessage: "heredoc marker on line #1 must contain only alpha-numeric characters, dashes and underscores; got 'HERE SAME LINE'",
},
{
input: []byte(`heredoc <<<EOF
content
EOF same-line-arg
`),
expectErr: true,
errorMessage: "too many '<' for heredoc on line #1; only use two, for example <<END",
},
{
input: []byte(`heredoc <<EOF
content
`),
expectErr: true,
errorMessage: "incomplete heredoc <<EOF on line #3, expected ending marker EOF",
},
{
input: []byte(`heredoc <<EOF
content
EOF
`),
expectErr: true,
errorMessage: "mismatched leading whitespace in heredoc <<EOF on line #2 [\tcontent], expected whitespace [\t\t] to match the closing marker",
},
{
input: []byte(`heredoc <<EOF
content
EOF
`),
expectErr: true,
errorMessage: "mismatched leading whitespace in heredoc <<EOF on line #2 [ content], expected whitespace [\t\t] to match the closing marker",
},
} }
for i, testCase := range testCases { for i, testCase := range testCases {
actual, err := Tokenize(testCase.input, "") actual, err := Tokenize(testCase.input, "")
if testCase.expectErr {
if err == nil {
t.Fatalf("expected error, got actual: %v", actual)
continue
}
if err.Error() != testCase.errorMessage {
t.Fatalf("expected error '%v', got: %v", testCase.errorMessage, err)
}
continue
}
if err != nil { if err != nil {
t.Fatalf("%v", err) t.Errorf("%v", err)
} }
lexerCompare(t, i, testCase.expected, actual) lexerCompare(t, i, testCase.expected, actual)
} }
@@ -428,17 +262,17 @@ EOF same-line-arg
func lexerCompare(t *testing.T, n int, expected, actual []Token) { func lexerCompare(t *testing.T, n int, expected, actual []Token) {
if len(expected) != len(actual) { if len(expected) != len(actual) {
t.Fatalf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual)) t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
} }
for i := 0; i < len(actual) && i < len(expected); i++ { for i := 0; i < len(actual) && i < len(expected); i++ {
if actual[i].Line != expected[i].Line { if actual[i].Line != expected[i].Line {
t.Fatalf("Test case %d token %d ('%s'): expected line %d but was line %d", t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d",
n, i, expected[i].Text, expected[i].Line, actual[i].Line) n, i, expected[i].Text, expected[i].Line, actual[i].Line)
break break
} }
if actual[i].Text != expected[i].Text { if actual[i].Text != expected[i].Text {
t.Fatalf("Test case %d token %d: expected text '%s' but was '%s'", t.Errorf("Test case %d token %d: expected text '%s' but was '%s'",
n, i, expected[i].Text, actual[i].Text) n, i, expected[i].Text, actual[i].Text)
break break
} }
Regular → Executable
+51 -165
View File
@@ -1,4 +1,4 @@
// Copyright 2015 Matthew Holt and The Caddy Authors // Copyright 2015 Light Code Labs, LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -17,13 +17,14 @@ package caddyfile
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io/ioutil"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"go.uber.org/zap"
) )
// Parse parses the input just enough to group tokens, in // Parse parses the input just enough to group tokens, in
@@ -36,13 +37,7 @@ import (
// Environment variables in {$ENVIRONMENT_VARIABLE} notation // Environment variables in {$ENVIRONMENT_VARIABLE} notation
// will be replaced before parsing begins. // will be replaced before parsing begins.
func Parse(filename string, input []byte) ([]ServerBlock, error) { func Parse(filename string, input []byte) ([]ServerBlock, error) {
// unfortunately, we must copy the input because parsing must tokens, err := allTokens(filename, input)
// remain a read-only operation, but we have to expand environment
// variables before we parse, which changes the underlying array (#4422)
inputCopy := make([]byte, len(input))
copy(inputCopy, input)
tokens, err := allTokens(filename, inputCopy)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -56,16 +51,8 @@ func Parse(filename string, input []byte) ([]ServerBlock, error) {
return p.parseAll() return p.parseAll()
} }
// allTokens lexes the entire input, but does not parse it.
// It returns all the tokens from the input, unstructured
// and in order. It may mutate input as it expands env vars.
func allTokens(filename string, input []byte) ([]Token, error) {
return Tokenize(replaceEnvVars(input), filename)
}
// replaceEnvVars replaces all occurrences of environment variables. // replaceEnvVars replaces all occurrences of environment variables.
// It mutates the underlying array and returns the updated slice. func replaceEnvVars(input []byte) ([]byte, error) {
func replaceEnvVars(input []byte) []byte {
var offset int var offset int
for { for {
begin := bytes.Index(input[offset:], spanOpen) begin := bytes.Index(input[offset:], spanOpen)
@@ -106,7 +93,22 @@ func replaceEnvVars(input []byte) []byte {
// continue at the end of the replacement // continue at the end of the replacement
offset = begin + len(envVarBytes) offset = begin + len(envVarBytes)
} }
return input return input, nil
}
// allTokens lexes the entire input, but does not parse it.
// It returns all the tokens from the input, unstructured
// and in order.
func allTokens(filename string, input []byte) ([]Token, error) {
input, err := replaceEnvVars(input)
if err != nil {
return nil, err
}
tokens, err := Tokenize(input, filename)
if err != nil {
return nil, err
}
return tokens, nil
} }
type parser struct { type parser struct {
@@ -148,6 +150,7 @@ func (p *parser) begin() error {
} }
err := p.addresses() err := p.addresses()
if err != nil { if err != nil {
return err return err
} }
@@ -158,25 +161,6 @@ func (p *parser) begin() error {
return nil return nil
} }
if ok, name := p.isNamedRoute(); ok {
// named routes only have one key, the route name
p.block.Keys = []string{name}
p.block.IsNamedRoute = true
// we just need a dummy leading token to ease parsing later
nameToken := p.Token()
nameToken.Text = name
// get all the tokens from the block, including the braces
tokens, err := p.blockTokens(true)
if err != nil {
return err
}
tokens = append([]Token{nameToken}, tokens...)
p.block.Segments = []Segment{tokens}
return nil
}
if ok, name := p.isSnippet(); ok { if ok, name := p.isSnippet(); ok {
if p.definedSnippets == nil { if p.definedSnippets == nil {
p.definedSnippets = map[string][]Token{} p.definedSnippets = map[string][]Token{}
@@ -185,15 +169,16 @@ func (p *parser) begin() error {
return p.Errf("redeclaration of previously declared snippet %s", name) return p.Errf("redeclaration of previously declared snippet %s", name)
} }
// consume all tokens til matched close brace // consume all tokens til matched close brace
tokens, err := p.blockTokens(false) tokens, err := p.snippetTokens()
if err != nil { if err != nil {
return err return err
} }
// Just as we need to track which file the token comes from, we need to // Just as we need to track which file the token comes from, we need to
// keep track of which snippet the token comes from. This is helpful // keep track of which snippets do the tokens come from. This is helpful
// in tracking import cycles across files/snippets by namespacing them. // in tracking import cycles across files/snippets by namespacing them. Without
// Without this, we end up with false-positives in cycle-detection. // this we end up with false-positives in cycle-detection.
for k, v := range tokens { for k, v := range tokens {
v.inSnippet = true
v.snippetName = name v.snippetName = name
tokens[k] = v tokens[k] = v
} }
@@ -214,7 +199,7 @@ func (p *parser) addresses() error {
// special case: import directive replaces tokens during parse-time // special case: import directive replaces tokens during parse-time
if tkn == "import" && p.isNewLine() { if tkn == "import" && p.isNewLine() {
err := p.doImport(0) err := p.doImport()
if err != nil { if err != nil {
return err return err
} }
@@ -314,7 +299,7 @@ func (p *parser) directives() error {
// special case: import directive replaces tokens during parse-time // special case: import directive replaces tokens during parse-time
if p.Val() == "import" { if p.Val() == "import" {
err := p.doImport(1) err := p.doImport()
if err != nil { if err != nil {
return err return err
} }
@@ -340,7 +325,7 @@ func (p *parser) directives() error {
// is on the token before where the import directive was. In // is on the token before where the import directive was. In
// other words, call Next() to access the first token that was // other words, call Next() to access the first token that was
// imported. // imported.
func (p *parser) doImport(nesting int) error { func (p *parser) doImport() error {
// syntax checks // syntax checks
if !p.NextArg() { if !p.NextArg() {
return p.ArgErr() return p.ArgErr()
@@ -353,8 +338,11 @@ func (p *parser) doImport(nesting int) error {
// grab remaining args as placeholder replacements // grab remaining args as placeholder replacements
args := p.RemainingArgs() args := p.RemainingArgs()
// set up a replacer for non-variadic args replacement // add args to the replacer
repl := makeArgsReplacer(args) repl := caddy.NewEmptyReplacer()
for index, arg := range args {
repl.Set("args."+strconv.Itoa(index), arg)
}
// splice out the import directive and its arguments // splice out the import directive and its arguments
// (2 tokens, plus the length of args) // (2 tokens, plus the length of args)
@@ -398,24 +386,10 @@ func (p *parser) doImport(nesting int) error {
} }
if len(matches) == 0 { if len(matches) == 0 {
if strings.ContainsAny(globPattern, "*?[]") { if strings.ContainsAny(globPattern, "*?[]") {
caddy.Log().Warn("No files matching import glob pattern", zap.String("pattern", importPattern)) log.Printf("[WARNING] No files matching import glob pattern: %s", importPattern)
} else { } else {
return p.Errf("File to import not found: %s", importPattern) return p.Errf("File to import not found: %s", importPattern)
} }
} else {
// See issue #5295 - should skip any files that start with a . when iterating over them.
sep := string(filepath.Separator)
segGlobPattern := strings.Split(globPattern, sep)
if strings.HasPrefix(segGlobPattern[len(segGlobPattern)-1], "*") {
var tmpMatches []string
for _, m := range matches {
seg := strings.Split(m, sep)
if !strings.HasPrefix(seg[len(seg)-1], ".") {
tmpMatches = append(tmpMatches, m)
}
}
matches = tmpMatches
}
} }
// collect all the imported tokens // collect all the imported tokens
@@ -430,7 +404,7 @@ func (p *parser) doImport(nesting int) error {
} }
nodeName := p.File() nodeName := p.File()
if p.Token().snippetName != "" { if p.Token().inSnippet {
nodeName += fmt.Sprintf(":%s", p.Token().snippetName) nodeName += fmt.Sprintf(":%s", p.Token().snippetName)
} }
p.importGraph.addNode(nodeName) p.importGraph.addNode(nodeName)
@@ -441,69 +415,13 @@ func (p *parser) doImport(nesting int) error {
} }
// copy the tokens so we don't overwrite p.definedSnippets // copy the tokens so we don't overwrite p.definedSnippets
tokensCopy := make([]Token, 0, len(importedTokens)) tokensCopy := make([]Token, len(importedTokens))
copy(tokensCopy, importedTokens)
var (
maybeSnippet bool
maybeSnippetId bool
index int
)
// run the argument replacer on the tokens // run the argument replacer on the tokens
// golang for range slice return a copy of value for index, token := range tokensCopy {
// similarly, append also copy value token.Text = repl.ReplaceKnown(token.Text, "")
for i, token := range importedTokens { tokensCopy[index] = token
// update the token's imports to refer to import directive filename, line number and snippet name if there is one
if token.snippetName != "" {
token.imports = append(token.imports, fmt.Sprintf("%s:%d (import %s)", p.File(), p.Line(), token.snippetName))
} else {
token.imports = append(token.imports, fmt.Sprintf("%s:%d (import)", p.File(), p.Line()))
}
// naive way of determine snippets, as snippets definition can only follow name + block
// format, won't check for nesting correctness or any other error, that's what parser does.
if !maybeSnippet && nesting == 0 {
// first of the line
if i == 0 || isNextOnNewLine(tokensCopy[i-1], token) {
index = 0
} else {
index++
}
if index == 0 && len(token.Text) >= 3 && strings.HasPrefix(token.Text, "(") && strings.HasSuffix(token.Text, ")") {
maybeSnippetId = true
}
}
switch token.Text {
case "{":
nesting++
if index == 1 && maybeSnippetId && nesting == 1 {
maybeSnippet = true
maybeSnippetId = false
}
case "}":
nesting--
if nesting == 0 && maybeSnippet {
maybeSnippet = false
}
}
if maybeSnippet {
tokensCopy = append(tokensCopy, token)
continue
}
foundVariadic, startIndex, endIndex := parseVariadic(token, len(args))
if foundVariadic {
for _, arg := range args[startIndex:endIndex] {
token.Text = arg
tokensCopy = append(tokensCopy, token)
}
} else {
token.Text = repl.ReplaceKnown(token.Text, "")
tokensCopy = append(tokensCopy, token)
}
} }
// splice the imported tokens in the place of the import statement // splice the imported tokens in the place of the import statement
@@ -529,17 +447,11 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
return nil, p.Errf("Could not import %s: is a directory", importFile) return nil, p.Errf("Could not import %s: is a directory", importFile)
} }
input, err := io.ReadAll(file) input, err := ioutil.ReadAll(file)
if err != nil { if err != nil {
return nil, p.Errf("Could not read imported file %s: %v", importFile, err) return nil, p.Errf("Could not read imported file %s: %v", importFile, err)
} }
// only warning in case of empty files
if len(input) == 0 || len(strings.TrimSpace(string(input))) == 0 {
caddy.Log().Warn("Import file is empty", zap.String("file", importFile))
return []Token{}, nil
}
importedTokens, err := allTokens(importFile, input) importedTokens, err := allTokens(importFile, input)
if err != nil { if err != nil {
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err) return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
@@ -575,16 +487,6 @@ func (p *parser) directive() error {
for p.Next() { for p.Next() {
if p.Val() == "{" { if p.Val() == "{" {
p.nesting++ p.nesting++
if !p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
return p.Err("Unexpected next token after '{' on same line")
}
if p.isNewLine() {
return p.Err("Unexpected '{' on a new line; did you mean to place the '{' on the previous line?")
}
} else if p.Val() == "{}" {
if p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
return p.Err("Unexpected '{}' at end of line")
}
} else if p.isNewLine() && p.nesting == 0 { } else if p.isNewLine() && p.nesting == 0 {
p.cursor-- // read too far p.cursor-- // read too far
break break
@@ -593,7 +495,7 @@ func (p *parser) directive() error {
} else if p.Val() == "}" && p.nesting == 0 { } else if p.Val() == "}" && p.nesting == 0 {
return p.Err("Unexpected '}' because no matching opening brace") return p.Err("Unexpected '}' because no matching opening brace")
} else if p.Val() == "import" && p.isNewLine() { } else if p.Val() == "import" && p.isNewLine() {
if err := p.doImport(1); err != nil { if err := p.doImport(); err != nil {
return err return err
} }
p.cursor-- // cursor is advanced when we continue, so roll back one more p.cursor-- // cursor is advanced when we continue, so roll back one more
@@ -634,15 +536,6 @@ func (p *parser) closeCurlyBrace() error {
return nil return nil
} }
func (p *parser) isNamedRoute() (bool, string) {
keys := p.block.Keys
// A named route block is a single key with parens, prefixed with &.
if len(keys) == 1 && strings.HasPrefix(keys[0], "&(") && strings.HasSuffix(keys[0], ")") {
return true, strings.TrimSuffix(keys[0][2:], ")")
}
return false, ""
}
func (p *parser) isSnippet() (bool, string) { func (p *parser) isSnippet() (bool, string) {
keys := p.block.Keys keys := p.block.Keys
// A snippet block is a single key with parens. Nothing else qualifies. // A snippet block is a single key with parens. Nothing else qualifies.
@@ -653,24 +546,18 @@ func (p *parser) isSnippet() (bool, string) {
} }
// read and store everything in a block for later replay. // read and store everything in a block for later replay.
func (p *parser) blockTokens(retainCurlies bool) ([]Token, error) { func (p *parser) snippetTokens() ([]Token, error) {
// block must have curlies. // snippet must have curlies.
err := p.openCurlyBrace() err := p.openCurlyBrace()
if err != nil { if err != nil {
return nil, err return nil, err
} }
nesting := 1 // count our own nesting nesting := 1 // count our own nesting in snippets
tokens := []Token{} tokens := []Token{}
if retainCurlies {
tokens = append(tokens, p.Token())
}
for p.Next() { for p.Next() {
if p.Val() == "}" { if p.Val() == "}" {
nesting-- nesting--
if nesting == 0 { if nesting == 0 {
if retainCurlies {
tokens = append(tokens, p.Token())
}
break break
} }
} }
@@ -690,10 +577,9 @@ func (p *parser) blockTokens(retainCurlies bool) ([]Token, error) {
// head of the server block with tokens, which are // head of the server block with tokens, which are
// grouped by segments. // grouped by segments.
type ServerBlock struct { type ServerBlock struct {
HasBraces bool HasBraces bool
Keys []string Keys []string
Segments []Segment Segments []Segment
IsNamedRoute bool
} }
// DispenseDirective returns a dispenser that contains // DispenseDirective returns a dispenser that contains
+11 -128
View File
@@ -1,4 +1,4 @@
// Copyright 2015 Matthew Holt and The Caddy Authors // Copyright 2015 Light Code Labs, LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -16,93 +16,12 @@ package caddyfile
import ( import (
"bytes" "bytes"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
) )
func TestParseVariadic(t *testing.T) {
var args = make([]string, 10)
for i, tc := range []struct {
input string
result bool
}{
{
input: "",
result: false,
},
{
input: "{args[1",
result: false,
},
{
input: "1]}",
result: false,
},
{
input: "{args[:]}aaaaa",
result: false,
},
{
input: "aaaaa{args[:]}",
result: false,
},
{
input: "{args.}",
result: false,
},
{
input: "{args.1}",
result: false,
},
{
input: "{args[]}",
result: false,
},
{
input: "{args[:]}",
result: true,
},
{
input: "{args[:]}",
result: true,
},
{
input: "{args[0:]}",
result: true,
},
{
input: "{args[:0]}",
result: true,
},
{
input: "{args[-1:]}",
result: false,
},
{
input: "{args[:11]}",
result: false,
},
{
input: "{args[10:0]}",
result: false,
},
{
input: "{args[0:10]}",
result: true,
},
} {
token := Token{
File: "test",
Line: 1,
Text: tc.input,
}
if v, _, _ := parseVariadic(token, len(args)); v != tc.result {
t.Errorf("Test %d error expectation failed Expected: %t, got %t", i, tc.result, v)
}
}
}
func TestAllTokens(t *testing.T) { func TestAllTokens(t *testing.T) {
input := []byte("a b c\nd e") input := []byte("a b c\nd e")
expected := []string{"a", "b", "c", "d", "e"} expected := []string{"a", "b", "c", "d", "e"}
@@ -269,49 +188,10 @@ func TestParseOneAndImport(t *testing.T) {
{`import testdata/not_found.txt`, true, []string{}, []int{}}, {`import testdata/not_found.txt`, true, []string{}, []int{}},
// empty file should just log a warning, and result in no tokens
{`import testdata/empty.txt`, false, []string{}, []int{}},
{`import testdata/only_white_space.txt`, false, []string{}, []int{}},
// import path/to/dir/* should skip any files that start with a . when iterating over them.
{`localhost
dir1 arg1
import testdata/glob/*`, false, []string{
"localhost",
}, []int{2, 3, 1}},
// import path/to/dir/.* should continue to read all dotfiles in a dir.
{`import testdata/glob/.*`, false, []string{
"host1",
}, []int{1, 2}},
{`""`, false, []string{}, []int{}}, {`""`, false, []string{}, []int{}},
{``, false, []string{}, []int{}}, {``, false, []string{}, []int{}},
// Unexpected next token after '{' on same line
{`localhost
dir1 { a b }`, true, []string{"localhost"}, []int{}},
// Unexpected '{' on a new line
{`localhost
dir1
{
a b
}`, true, []string{"localhost"}, []int{}},
// Workaround with quotes
{`localhost
dir1 "{" a b "}"`, false, []string{"localhost"}, []int{5}},
// Unexpected '{}' at end of line
{`localhost
dir1 {}`, true, []string{"localhost"}, []int{}},
// Workaround with quotes
{`localhost
dir1 "{}"`, false, []string{"localhost"}, []int{2}},
// import with args // import with args
{`import testdata/import_args0.txt a`, false, []string{"a"}, []int{}}, {`import testdata/import_args0.txt a`, false, []string{"a"}, []int{}},
{`import testdata/import_args1.txt a b`, false, []string{"a", "b"}, []int{}}, {`import testdata/import_args1.txt a b`, false, []string{"a", "b"}, []int{}},
@@ -400,7 +280,7 @@ func TestRecursiveImport(t *testing.T) {
} }
// test relative recursive import // test relative recursive import
err = os.WriteFile(recursiveFile1, []byte( err = ioutil.WriteFile(recursiveFile1, []byte(
`localhost `localhost
dir1 dir1
import recursive_import_test2`), 0644) import recursive_import_test2`), 0644)
@@ -409,7 +289,7 @@ func TestRecursiveImport(t *testing.T) {
} }
defer os.Remove(recursiveFile1) defer os.Remove(recursiveFile1)
err = os.WriteFile(recursiveFile2, []byte("dir2 1"), 0644) err = ioutil.WriteFile(recursiveFile2, []byte("dir2 1"), 0644)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -434,7 +314,7 @@ func TestRecursiveImport(t *testing.T) {
} }
// test absolute recursive import // test absolute recursive import
err = os.WriteFile(recursiveFile1, []byte( err = ioutil.WriteFile(recursiveFile1, []byte(
`localhost `localhost
dir1 dir1
import `+recursiveFile2), 0644) import `+recursiveFile2), 0644)
@@ -490,7 +370,7 @@ func TestDirectiveImport(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
err = os.WriteFile(directiveFile, []byte(`prop1 1 err = ioutil.WriteFile(directiveFile, []byte(`prop1 1
prop2 2`), 0644) prop2 2`), 0644)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -711,7 +591,10 @@ func TestEnvironmentReplacement(t *testing.T) {
expect: "}{$", expect: "}{$",
}, },
} { } {
actual := replaceEnvVars([]byte(test.input)) actual, err := replaceEnvVars([]byte(test.input))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(actual, []byte(test.expect)) { if !bytes.Equal(actual, []byte(test.expect)) {
t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual) t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
} }
@@ -750,7 +633,7 @@ func TestSnippets(t *testing.T) {
} }
func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) { func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) {
file, err := os.CreateTemp("", t.Name()) file, err := ioutil.TempFile("", t.Name())
if err != nil { if err != nil {
panic(err) // get a stack trace so we know where this was called from. panic(err) // get a stack trace so we know where this was called from.
} }
View File
-4
View File
@@ -1,4 +0,0 @@
host1 {
dir1
dir2 arg1
}
-2
View File
@@ -1,2 +0,0 @@
dir2 arg1 arg2
dir3
+1 -1
View File
@@ -1 +1 @@
{args[0]} {args.0}
+1 -1
View File
@@ -1 +1 @@
{args[0]} {args[1]} {args.0} {args.1}
View File
View File
View File
View File
View File
-7
View File
@@ -1,7 +0,0 @@
 
+5 -5
View File
@@ -24,7 +24,7 @@ import (
// Adapter is a type which can adapt a configuration to Caddy JSON. // Adapter is a type which can adapt a configuration to Caddy JSON.
// It returns the results and any warnings, or an error. // It returns the results and any warnings, or an error.
type Adapter interface { type Adapter interface {
Adapt(body []byte, options map[string]any) ([]byte, []Warning, error) Adapt(body []byte, options map[string]interface{}) ([]byte, []Warning, error)
} }
// Warning represents a warning or notice related to conversion. // Warning represents a warning or notice related to conversion.
@@ -48,7 +48,7 @@ func (w Warning) String() string {
// are converted to warnings. This is convenient when filling config // are converted to warnings. This is convenient when filling config
// structs that require a json.RawMessage, without having to worry // structs that require a json.RawMessage, without having to worry
// about errors. // about errors.
func JSON(val any, warnings *[]Warning) json.RawMessage { func JSON(val interface{}, warnings *[]Warning) json.RawMessage {
b, err := json.Marshal(val) b, err := json.Marshal(val)
if err != nil { if err != nil {
if warnings != nil { if warnings != nil {
@@ -64,9 +64,9 @@ func JSON(val any, warnings *[]Warning) json.RawMessage {
// for encoding module values where the module name has to be described within // for encoding module values where the module name has to be described within
// the object by a certain key; for example, `"handler": "file_server"` for a // the object by a certain key; for example, `"handler": "file_server"` for a
// file server HTTP handler (fieldName="handler" and fieldVal="file_server"). // file server HTTP handler (fieldName="handler" and fieldVal="file_server").
// The val parameter must encode into a map[string]any (i.e. it must be // The val parameter must encode into a map[string]interface{} (i.e. it must be
// a struct or map). Any errors are converted into warnings. // a struct or map). Any errors are converted into warnings.
func JSONModuleObject(val any, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage { func JSONModuleObject(val interface{}, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage {
// encode to a JSON object first // encode to a JSON object first
enc, err := json.Marshal(val) enc, err := json.Marshal(val)
if err != nil { if err != nil {
@@ -77,7 +77,7 @@ func JSONModuleObject(val any, fieldName, fieldVal string, warnings *[]Warning)
} }
// then decode the object // then decode the object
var tmp map[string]any var tmp map[string]interface{}
err = json.Unmarshal(enc, &tmp) err = json.Unmarshal(enc, &tmp)
if err != nil { if err != nil {
if warnings != nil { if warnings != nil {
+42 -53
View File
@@ -17,7 +17,6 @@ package httpcaddyfile
import ( import (
"fmt" "fmt"
"net" "net"
"net/netip"
"reflect" "reflect"
"sort" "sort"
"strconv" "strconv"
@@ -36,12 +35,12 @@ import (
// server block that share the same address stay grouped together so the config // server block that share the same address stay grouped together so the config
// isn't repeated unnecessarily. For example, this Caddyfile: // isn't repeated unnecessarily. For example, this Caddyfile:
// //
// example.com { // example.com {
// bind 127.0.0.1 // bind 127.0.0.1
// } // }
// www.example.com, example.net/path, localhost:9999 { // www.example.com, example.net/path, localhost:9999 {
// bind 127.0.0.1 1.2.3.4 // bind 127.0.0.1 1.2.3.4
// } // }
// //
// has two server blocks to start with. But expressed in this Caddyfile are // has two server blocks to start with. But expressed in this Caddyfile are
// actually 4 listener addresses: 127.0.0.1:443, 1.2.3.4:443, 127.0.0.1:9999, // actually 4 listener addresses: 127.0.0.1:443, 1.2.3.4:443, 127.0.0.1:9999,
@@ -77,7 +76,7 @@ import (
// multiple addresses to the same lists of server blocks (a many:many mapping). // multiple addresses to the same lists of server blocks (a many:many mapping).
// (Doing this is essentially a map-reduce technique.) // (Doing this is essentially a map-reduce technique.)
func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock, func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock,
options map[string]any) (map[string][]serverBlock, error) { options map[string]interface{}) (map[string][]serverBlock, error) {
sbmap := make(map[string][]serverBlock) sbmap := make(map[string][]serverBlock)
for i, sblock := range originalServerBlocks { for i, sblock := range originalServerBlocks {
@@ -103,20 +102,12 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBloc
} }
} }
// make a slice of the map keys so we can iterate in sorted order
addrs := make([]string, 0, len(addrToKeys))
for k := range addrToKeys {
addrs = append(addrs, k)
}
sort.Strings(addrs)
// now that we know which addresses serve which keys of this // now that we know which addresses serve which keys of this
// server block, we iterate that mapping and create a list of // server block, we iterate that mapping and create a list of
// new server blocks for each address where the keys of the // new server blocks for each address where the keys of the
// server block are only the ones which use the address; but // server block are only the ones which use the address; but
// the contents (tokens) are of course the same // the contents (tokens) are of course the same
for _, addr := range addrs { for addr, keys := range addrToKeys {
keys := addrToKeys[addr]
// parse keys so that we only have to do it once // parse keys so that we only have to do it once
parsedKeys := make([]Address, 0, len(keys)) parsedKeys := make([]Address, 0, len(keys))
for _, key := range keys { for _, key := range keys {
@@ -170,7 +161,6 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
delete(addrToServerBlocks, otherAddr) delete(addrToServerBlocks, otherAddr)
} }
} }
sort.Strings(a.addresses)
sbaddrs = append(sbaddrs, a) sbaddrs = append(sbaddrs, a)
} }
@@ -184,10 +174,8 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
return sbaddrs return sbaddrs
} }
// listenerAddrsForServerBlockKey essentially converts the Caddyfile
// site addresses to Caddy listener addresses for each server block.
func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string, func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string,
options map[string]any) ([]string, error) { options map[string]interface{}) ([]string, error) {
addr, err := ParseAddress(key) addr, err := ParseAddress(key)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing key: %v", err) return nil, fmt.Errorf("parsing key: %v", err)
@@ -219,42 +207,24 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
return nil, fmt.Errorf("[%s] scheme and port violate convention", key) return nil, fmt.Errorf("[%s] scheme and port violate convention", key)
} }
// the bind directive specifies hosts (and potentially network), but is optional // the bind directive specifies hosts, but is optional
lnHosts := make([]string, 0, len(sblock.pile["bind"])) lnHosts := make([]string, 0, len(sblock.pile))
for _, cfgVal := range sblock.pile["bind"] { for _, cfgVal := range sblock.pile["bind"] {
lnHosts = append(lnHosts, cfgVal.Value.([]string)...) lnHosts = append(lnHosts, cfgVal.Value.([]string)...)
} }
if len(lnHosts) == 0 { if len(lnHosts) == 0 {
if defaultBind, ok := options["default_bind"].([]string); ok { lnHosts = []string{""}
lnHosts = defaultBind
} else {
lnHosts = []string{""}
}
} }
// use a map to prevent duplication // use a map to prevent duplication
listeners := make(map[string]struct{}) listeners := make(map[string]struct{})
for _, lnHost := range lnHosts { for _, host := range lnHosts {
// normally we would simply append the port, addr, err := caddy.ParseNetworkAddress(host)
// but if lnHost is IPv6, we need to ensure it if err == nil && addr.IsUnixNetwork() {
// is enclosed in [ ]; net.JoinHostPort does listeners[host] = struct{}{}
// this for us, but lnHost might also have a } else {
// network type in front (e.g. "tcp/") leading listeners[net.JoinHostPort(host, lnPort)] = struct{}{}
// to "[tcp/::1]" which causes parsing failures
// later; what we need is "tcp/[::1]", so we have
// to split the network and host, then re-combine
network, host, ok := strings.Cut(lnHost, "/")
if !ok {
host = network
network = ""
} }
host = strings.Trim(host, "[]") // IPv6
networkAddr := caddy.JoinNetworkAddress(network, host, lnPort)
addr, err := caddy.ParseNetworkAddress(networkAddr)
if err != nil {
return nil, fmt.Errorf("parsing network address: %v", err)
}
listeners[addr.String()] = struct{}{}
} }
// now turn map into list // now turn map into list
@@ -262,7 +232,6 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
for lnStr := range listeners { for lnStr := range listeners {
listenersList = append(listenersList, lnStr) listenersList = append(listenersList, lnStr)
} }
sort.Strings(listenersList)
return listenersList, nil return listenersList, nil
} }
@@ -367,10 +336,8 @@ func (a Address) Normalize() Address {
// ensure host is normalized if it's an IP address // ensure host is normalized if it's an IP address
host := strings.TrimSpace(a.Host) host := strings.TrimSpace(a.Host)
if ip, err := netip.ParseAddr(host); err == nil { if ip := net.ParseIP(host); ip != nil {
if ip.Is6() && !ip.Is4() && !ip.Is4In6() { host = ip.String()
host = ip.String()
}
} }
return Address{ return Address{
@@ -382,6 +349,28 @@ func (a Address) Normalize() Address {
} }
} }
// Key returns a string form of a, much like String() does, but this
// method doesn't add anything default that wasn't in the original.
func (a Address) Key() string {
res := ""
if a.Scheme != "" {
res += a.Scheme + "://"
}
if a.Host != "" {
res += a.Host
}
// insert port only if the original has its own explicit port
if a.Port != "" &&
len(a.Original) >= len(res) &&
strings.HasPrefix(a.Original[len(res):], ":"+a.Port) {
res += ":" + a.Port
}
if a.Path != "" {
res += a.Path
}
return res
}
// lowerExceptPlaceholders lowercases s except within // lowerExceptPlaceholders lowercases s except within
// placeholders (substrings in non-escaped '{ }' spans). // placeholders (substrings in non-escaped '{ }' spans).
// See https://github.com/caddyserver/caddy/issues/3264 // See https://github.com/caddyserver/caddy/issues/3264
+1 -1
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build gofuzz // +build gofuzz
package httpcaddyfile package httpcaddyfile
+32 -102
View File
@@ -106,128 +106,67 @@ func TestAddressString(t *testing.T) {
func TestKeyNormalization(t *testing.T) { func TestKeyNormalization(t *testing.T) {
testCases := []struct { testCases := []struct {
input string input string
expect Address expect string
}{ }{
{ {
input: "example.com", input: "example.com",
expect: Address{ expect: "example.com",
Host: "example.com",
},
}, },
{ {
input: "http://host:1234/path", input: "http://host:1234/path",
expect: Address{ expect: "http://host:1234/path",
Scheme: "http",
Host: "host",
Port: "1234",
Path: "/path",
},
}, },
{ {
input: "HTTP://A/ABCDEF", input: "HTTP://A/ABCDEF",
expect: Address{ expect: "http://a/ABCDEF",
Scheme: "http",
Host: "a",
Path: "/ABCDEF",
},
}, },
{ {
input: "A/ABCDEF", input: "A/ABCDEF",
expect: Address{ expect: "a/ABCDEF",
Host: "a",
Path: "/ABCDEF",
},
}, },
{ {
input: "A:2015/Path", input: "A:2015/Path",
expect: Address{ expect: "a:2015/Path",
Host: "a",
Port: "2015",
Path: "/Path",
},
}, },
{ {
input: "sub.{env.MY_DOMAIN}", input: "sub.{env.MY_DOMAIN}",
expect: Address{ expect: "sub.{env.MY_DOMAIN}",
Host: "sub.{env.MY_DOMAIN}",
},
}, },
{ {
input: "sub.ExAmPle", input: "sub.ExAmPle",
expect: Address{ expect: "sub.example",
Host: "sub.example",
},
}, },
{ {
input: "sub.\\{env.MY_DOMAIN\\}", input: "sub.\\{env.MY_DOMAIN\\}",
expect: Address{ expect: "sub.\\{env.my_domain\\}",
Host: "sub.\\{env.my_domain\\}",
},
}, },
{ {
input: "sub.{env.MY_DOMAIN}.com", input: "sub.{env.MY_DOMAIN}.com",
expect: Address{ expect: "sub.{env.MY_DOMAIN}.com",
Host: "sub.{env.MY_DOMAIN}.com",
},
}, },
{ {
input: ":80", input: ":80",
expect: Address{ expect: ":80",
Port: "80",
},
}, },
{ {
input: ":443", input: ":443",
expect: Address{ expect: ":443",
Port: "443",
},
}, },
{ {
input: ":1234", input: ":1234",
expect: Address{ expect: ":1234",
Port: "1234",
},
}, },
{ {
input: "", input: "",
expect: Address{}, expect: "",
}, },
{ {
input: ":", input: ":",
expect: Address{}, expect: "",
}, },
{ {
input: "[::]", input: "[::]",
expect: Address{ expect: "::",
Host: "::",
},
},
{
input: "127.0.0.1",
expect: Address{
Host: "127.0.0.1",
},
},
{
input: "[2001:db8:85a3:8d3:1319:8a2e:370:7348]:1234",
expect: Address{
Host: "2001:db8:85a3:8d3:1319:8a2e:370:7348",
Port: "1234",
},
},
{
// IPv4 address in IPv6 form (#4381)
input: "[::ffff:cff4:e77d]:1234",
expect: Address{
Host: "::ffff:cff4:e77d",
Port: "1234",
},
},
{
input: "::ffff:cff4:e77d",
expect: Address{
Host: "::ffff:cff4:e77d",
},
}, },
} }
for i, tc := range testCases { for i, tc := range testCases {
@@ -236,18 +175,9 @@ func TestKeyNormalization(t *testing.T) {
t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err) t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err)
continue continue
} }
actual := addr.Normalize() if actual := addr.Normalize().Key(); actual != tc.expect {
if actual.Scheme != tc.expect.Scheme { t.Errorf("Test %d: Input '%s': Expected '%s' but got '%s'", i, tc.input, tc.expect, actual)
t.Errorf("Test %d: Input '%s': Expected Scheme='%s' but got Scheme='%s'", i, tc.input, tc.expect.Scheme, actual.Scheme)
}
if actual.Host != tc.expect.Host {
t.Errorf("Test %d: Input '%s': Expected Host='%s' but got Host='%s'", i, tc.input, tc.expect.Host, actual.Host)
}
if actual.Port != tc.expect.Port {
t.Errorf("Test %d: Input '%s': Expected Port='%s' but got Port='%s'", i, tc.input, tc.expect.Port, actual.Port)
}
if actual.Path != tc.expect.Path {
t.Errorf("Test %d: Input '%s': Expected Path='%s' but got Path='%s'", i, tc.input, tc.expect.Path, actual.Path)
} }
} }
} }
+71 -279
View File
@@ -19,12 +19,11 @@ import (
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"html" "html"
"io/ioutil"
"net/http" "net/http"
"os"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
@@ -40,7 +39,6 @@ func init() {
RegisterDirective("bind", parseBind) RegisterDirective("bind", parseBind)
RegisterDirective("tls", parseTLS) RegisterDirective("tls", parseTLS)
RegisterHandlerDirective("root", parseRoot) RegisterHandlerDirective("root", parseRoot)
RegisterHandlerDirective("vars", parseVars)
RegisterHandlerDirective("redir", parseRedir) RegisterHandlerDirective("redir", parseRedir)
RegisterHandlerDirective("respond", parseRespond) RegisterHandlerDirective("respond", parseRespond)
RegisterHandlerDirective("abort", parseAbort) RegisterHandlerDirective("abort", parseAbort)
@@ -48,14 +46,13 @@ func init() {
RegisterHandlerDirective("route", parseRoute) RegisterHandlerDirective("route", parseRoute)
RegisterHandlerDirective("handle", parseHandle) RegisterHandlerDirective("handle", parseHandle)
RegisterDirective("handle_errors", parseHandleErrors) RegisterDirective("handle_errors", parseHandleErrors)
RegisterHandlerDirective("invoke", parseInvoke)
RegisterDirective("log", parseLog) RegisterDirective("log", parseLog)
RegisterHandlerDirective("skip_log", parseSkipLog)
} }
// parseBind parses the bind directive. Syntax: // parseBind parses the bind directive. Syntax:
// //
// bind <addresses...> // bind <addresses...>
//
func parseBind(h Helper) ([]ConfigValue, error) { func parseBind(h Helper) ([]ConfigValue, error) {
var lnHosts []string var lnHosts []string
for h.Next() { for h.Next() {
@@ -66,34 +63,27 @@ func parseBind(h Helper) ([]ConfigValue, error) {
// parseTLS parses the tls directive. Syntax: // parseTLS parses the tls directive. Syntax:
// //
// tls [<email>|internal]|[<cert_file> <key_file>] { // tls [<email>|internal]|[<cert_file> <key_file>] {
// protocols <min> [<max>] // protocols <min> [<max>]
// ciphers <cipher_suites...> // ciphers <cipher_suites...>
// curves <curves...> // curves <curves...>
// client_auth { // client_auth {
// mode [request|require|verify_if_given|require_and_verify] // mode [request|require|verify_if_given|require_and_verify]
// trusted_ca_cert <base64_der> // trusted_ca_cert <base64_der>
// trusted_ca_cert_file <filename> // trusted_ca_cert_file <filename>
// trusted_leaf_cert <base64_der> // trusted_leaf_cert <base64_der>
// trusted_leaf_cert_file <filename> // trusted_leaf_cert_file <filename>
// } // }
// alpn <values...> // alpn <values...>
// load <paths...> // load <paths...>
// ca <acme_ca_endpoint> // ca <acme_ca_endpoint>
// ca_root <pem_file> // ca_root <pem_file>
// key_type [ed25519|p256|p384|rsa2048|rsa4096] // dns <provider_name> [...]
// dns <provider_name> [...] // on_demand
// propagation_delay <duration> // eab <key_id> <mac_key>
// propagation_timeout <duration> // issuer <module_name> [...]
// resolvers <dns_servers...> // }
// dns_ttl <duration> //
// dns_challenge_override_domain <domain>
// on_demand
// eab <key_id> <mac_key>
// issuer <module_name> [...]
// get_certificate <module_name> [...]
// insecure_secrets_log <log_file>
// }
func parseTLS(h Helper) ([]ConfigValue, error) { func parseTLS(h Helper) ([]ConfigValue, error) {
cp := new(caddytls.ConnectionPolicy) cp := new(caddytls.ConnectionPolicy)
var fileLoader caddytls.FileLoader var fileLoader caddytls.FileLoader
@@ -103,7 +93,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
var keyType string var keyType string
var internalIssuer *caddytls.InternalIssuer var internalIssuer *caddytls.InternalIssuer
var issuers []certmagic.Issuer var issuers []certmagic.Issuer
var certManagers []certmagic.Manager
var onDemand bool var onDemand bool
for h.Next() { for h.Next() {
@@ -241,7 +230,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
return nil, h.ArgErr() return nil, h.ArgErr()
} }
filename := h.Val() filename := h.Val()
certDataPEM, err := os.ReadFile(filename) certDataPEM, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -318,22 +307,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
} }
issuers = append(issuers, issuer) issuers = append(issuers, issuer)
case "get_certificate":
if !h.NextArg() {
return nil, h.ArgErr()
}
modName := h.Val()
modID := "tls.get_certificate." + modName
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
if err != nil {
return nil, err
}
certManager, ok := unm.(certmagic.Manager)
if !ok {
return nil, h.Errf("module %s (%T) is not a certmagic.CertificateManager", modID, unm)
}
certManagers = append(certManagers, certManager)
case "dns": case "dns":
if !h.NextArg() { if !h.NextArg() {
return nil, h.ArgErr() return nil, h.ArgErr()
@@ -371,91 +344,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
} }
acmeIssuer.Challenges.DNS.Resolvers = args acmeIssuer.Challenges.DNS.Resolvers = args
case "propagation_delay":
arg := h.RemainingArgs()
if len(arg) != 1 {
return nil, h.ArgErr()
}
delayStr := arg[0]
delay, err := caddy.ParseDuration(delayStr)
if err != nil {
return nil, h.Errf("invalid propagation_delay duration %s: %v", delayStr, err)
}
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.DNS == nil {
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
}
acmeIssuer.Challenges.DNS.PropagationDelay = caddy.Duration(delay)
case "propagation_timeout":
arg := h.RemainingArgs()
if len(arg) != 1 {
return nil, h.ArgErr()
}
timeoutStr := arg[0]
var timeout time.Duration
if timeoutStr == "-1" {
timeout = time.Duration(-1)
} else {
var err error
timeout, err = caddy.ParseDuration(timeoutStr)
if err != nil {
return nil, h.Errf("invalid propagation_timeout duration %s: %v", timeoutStr, err)
}
}
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.DNS == nil {
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
}
acmeIssuer.Challenges.DNS.PropagationTimeout = caddy.Duration(timeout)
case "dns_ttl":
arg := h.RemainingArgs()
if len(arg) != 1 {
return nil, h.ArgErr()
}
ttlStr := arg[0]
ttl, err := caddy.ParseDuration(ttlStr)
if err != nil {
return nil, h.Errf("invalid dns_ttl duration %s: %v", ttlStr, err)
}
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.DNS == nil {
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
}
acmeIssuer.Challenges.DNS.TTL = caddy.Duration(ttl)
case "dns_challenge_override_domain":
arg := h.RemainingArgs()
if len(arg) != 1 {
return nil, h.ArgErr()
}
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.DNS == nil {
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
}
acmeIssuer.Challenges.DNS.OverrideDomain = arg[0]
case "ca_root": case "ca_root":
arg := h.RemainingArgs() arg := h.RemainingArgs()
if len(arg) != 1 { if len(arg) != 1 {
@@ -472,12 +360,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
} }
onDemand = true onDemand = true
case "insecure_secrets_log":
if !h.NextArg() {
return nil, h.ArgErr()
}
cp.InsecureSecretsLog = h.Val()
default: default:
return nil, h.Errf("unknown subdirective: %s", h.Val()) return nil, h.Errf("unknown subdirective: %s", h.Val())
} }
@@ -571,12 +453,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
Value: true, Value: true,
}) })
} }
for _, certManager := range certManagers {
configVals = append(configVals, ConfigValue{
Class: "tls.cert_manager",
Value: certManager,
})
}
// custom certificate selection // custom certificate selection
if len(certSelector.AnyTag) > 0 { if len(certSelector.AnyTag) > 0 {
@@ -598,7 +474,8 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
// parseRoot parses the root directive. Syntax: // parseRoot parses the root directive. Syntax:
// //
// root [<matcher>] <path> // root [<matcher>] <path>
//
func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) { func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) {
var root string var root string
for h.Next() { for h.Next() {
@@ -613,22 +490,10 @@ func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) {
return caddyhttp.VarsMiddleware{"root": root}, nil return caddyhttp.VarsMiddleware{"root": root}, nil
} }
// parseVars parses the vars directive. See its UnmarshalCaddyfile method for syntax.
func parseVars(h Helper) (caddyhttp.MiddlewareHandler, error) {
v := new(caddyhttp.VarsMiddleware)
err := v.UnmarshalCaddyfile(h.Dispenser)
return v, err
}
// parseRedir parses the redir directive. Syntax: // parseRedir parses the redir directive. Syntax:
// //
// redir [<matcher>] <to> [<code>] // redir [<matcher>] <to> [<code>]
// //
// <code> can be "permanent" for 301, "temporary" for 302 (default),
// a placeholder, or any number in the 3xx range or 401. The special
// code "html" can be used to redirect only browser clients (will
// respond with HTTP 200 and no Location header; redirect is performed
// with JS and a meta tag).
func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) { func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
if !h.Next() { if !h.Next() {
return nil, h.ArgErr() return nil, h.ArgErr()
@@ -645,7 +510,6 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
} }
var body string var body string
var hdr http.Header
switch code { switch code {
case "permanent": case "permanent":
code = "301" code = "301"
@@ -666,37 +530,20 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
` `
safeTo := html.EscapeString(to) safeTo := html.EscapeString(to)
body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo) body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo)
code = "200" // don't redirect non-browser clients code = "302"
default: default:
// Allow placeholders for the code
if strings.HasPrefix(code, "{") {
break
}
// Try to validate as an integer otherwise
codeInt, err := strconv.Atoi(code) codeInt, err := strconv.Atoi(code)
if err != nil { if err != nil {
return nil, h.Errf("Not a supported redir code type or not valid integer: '%s'", code) return nil, h.Errf("Not a supported redir code type or not valid integer: '%s'", code)
} }
// Sometimes, a 401 with Location header is desirable because if codeInt < 300 || codeInt > 399 {
// requests made with XHR will "eat" the 3xx redirect; so if return nil, h.Errf("Redir code not in the 3xx range: '%v'", codeInt)
// the intent was to redirect to an auth page, a 3xx won't
// work. Responding with 401 allows JS code to read the
// Location header and do a window.location redirect manually.
// see https://stackoverflow.com/a/2573589/846934
// see https://github.com/oauth2-proxy/oauth2-proxy/issues/1522
if codeInt < 300 || (codeInt > 399 && codeInt != 401) {
return nil, h.Errf("Redir code not in the 3xx range or 401: '%v'", codeInt)
} }
} }
// don't redirect non-browser clients
if code != "200" {
hdr = http.Header{"Location": []string{to}}
}
return caddyhttp.StaticResponse{ return caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(code), StatusCode: caddyhttp.WeakString(code),
Headers: hdr, Headers: http.Header{"Location": []string{to}},
Body: body, Body: body,
}, nil }, nil
} }
@@ -732,20 +579,29 @@ func parseError(h Helper) (caddyhttp.MiddlewareHandler, error) {
// parseRoute parses the route directive. // parseRoute parses the route directive.
func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) { func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) {
sr := new(caddyhttp.Subroute)
allResults, err := parseSegmentAsConfig(h) allResults, err := parseSegmentAsConfig(h)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, result := range allResults { for _, result := range allResults {
switch result.Value.(type) { switch handler := result.Value.(type) {
case caddyhttp.Route, caddyhttp.Subroute: case caddyhttp.Route:
sr.Routes = append(sr.Routes, handler)
case caddyhttp.Subroute:
// directives which return a literal subroute instead of a route
// means they intend to keep those handlers together without
// them being reordered; we're doing that anyway since we're in
// the route directive, so just append its handlers
sr.Routes = append(sr.Routes, handler.Routes...)
default: default:
return nil, h.Errf("%s directive returned something other than an HTTP route or subroute: %#v (only handler directives can be used in routes)", result.directive, result.Value) return nil, h.Errf("%s directive returned something other than an HTTP route or subroute: %#v (only handler directives can be used in routes)", result.directive, result.Value)
} }
} }
return buildSubroute(allResults, h.groupCounter, false) return sr, nil
} }
func parseHandle(h Helper) (caddyhttp.MiddlewareHandler, error) { func parseHandle(h Helper) (caddyhttp.MiddlewareHandler, error) {
@@ -765,35 +621,14 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {
}, nil }, nil
} }
// parseInvoke parses the invoke directive.
func parseInvoke(h Helper) (caddyhttp.MiddlewareHandler, error) {
h.Next() // consume directive
if !h.NextArg() {
return nil, h.ArgErr()
}
for h.Next() || h.NextBlock(0) {
return nil, h.ArgErr()
}
// remember that we're invoking this name
// to populate the server with these named routes
if h.State[namedRouteKey] == nil {
h.State[namedRouteKey] = map[string]struct{}{}
}
h.State[namedRouteKey].(map[string]struct{})[h.Val()] = struct{}{}
// return the handler
return &caddyhttp.Invoke{Name: h.Val()}, nil
}
// parseLog parses the log directive. Syntax: // parseLog parses the log directive. Syntax:
// //
// log <logger_name> { // log {
// hostnames <hostnames...> // output <writer_module> ...
// output <writer_module> ... // format <encoder_module> ...
// format <encoder_module> ... // level <level>
// level <level> // }
// } //
func parseLog(h Helper) ([]ConfigValue, error) { func parseLog(h Helper) ([]ConfigValue, error) {
return parseLogHelper(h, nil) return parseLogHelper(h, nil)
} }
@@ -810,13 +645,11 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
var configValues []ConfigValue var configValues []ConfigValue
for h.Next() { for h.Next() {
// Logic below expects that a name is always present when a // Logic below expects that a name is always present when a
// global option is being parsed; or an optional override // global option is being parsed.
// is supported for access logs. var globalLogName string
var logName string
if parseAsGlobalOption { if parseAsGlobalOption {
if h.NextArg() { if h.NextArg() {
logName = h.Val() globalLogName = h.Val()
// Only a single argument is supported. // Only a single argument is supported.
if h.NextArg() { if h.NextArg() {
@@ -827,47 +660,26 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
// reference the default logger. See the // reference the default logger. See the
// setupNewDefault function in the logging // setupNewDefault function in the logging
// package for where this is configured. // package for where this is configured.
logName = caddy.DefaultLoggerName globalLogName = "default"
} }
// Verify this name is unused. // Verify this name is unused.
_, used := globalLogNames[logName] _, used := globalLogNames[globalLogName]
if used { if used {
return nil, h.Err("duplicate global log option for: " + logName) return nil, h.Err("duplicate global log option for: " + globalLogName)
} }
globalLogNames[logName] = struct{}{} globalLogNames[globalLogName] = struct{}{}
} else { } else {
// An optional override of the logger name can be provided; // No arguments are supported for the server block log directive
// otherwise a default will be used, like "log0", "log1", etc.
if h.NextArg() { if h.NextArg() {
logName = h.Val() return nil, h.ArgErr()
// Only a single argument is supported.
if h.NextArg() {
return nil, h.ArgErr()
}
} }
} }
cl := new(caddy.CustomLog) cl := new(caddy.CustomLog)
// allow overriding the current site block's hostnames for this logger;
// this is useful for setting up loggers per subdomain in a site block
// with a wildcard domain
customHostnames := []string{}
for h.NextBlock(0) { for h.NextBlock(0) {
switch h.Val() { switch h.Val() {
case "hostnames":
if parseAsGlobalOption {
return nil, h.Err("hostnames is not allowed in the log global options")
}
args := h.RemainingArgs()
if len(args) == 0 {
return nil, h.ArgErr()
}
customHostnames = append(customHostnames, args...)
case "output": case "output":
if !h.NextArg() { if !h.NextArg() {
return nil, h.ArgErr() return nil, h.ArgErr()
@@ -926,16 +738,18 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
} }
case "include": case "include":
// This configuration is only allowed in the global options
if !parseAsGlobalOption { if !parseAsGlobalOption {
return nil, h.Err("include is not allowed in the log directive") return nil, h.ArgErr()
} }
for h.NextArg() { for h.NextArg() {
cl.Include = append(cl.Include, h.Val()) cl.Include = append(cl.Include, h.Val())
} }
case "exclude": case "exclude":
// This configuration is only allowed in the global options
if !parseAsGlobalOption { if !parseAsGlobalOption {
return nil, h.Err("exclude is not allowed in the log directive") return nil, h.ArgErr()
} }
for h.NextArg() { for h.NextArg() {
cl.Exclude = append(cl.Exclude, h.Val()) cl.Exclude = append(cl.Exclude, h.Val())
@@ -947,34 +761,24 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
} }
var val namedCustomLog var val namedCustomLog
val.hostnames = customHostnames
isEmptyConfig := reflect.DeepEqual(cl, new(caddy.CustomLog))
// Skip handling of empty logging configs // Skip handling of empty logging configs
if !reflect.DeepEqual(cl, new(caddy.CustomLog)) {
if parseAsGlobalOption { if parseAsGlobalOption {
// Use indicated name for global log options // Use indicated name for global log options
val.name = logName val.name = globalLogName
} else { val.log = cl
if logName != "" { } else {
val.name = logName
} else if !isEmptyConfig {
// Construct a log name for server log streams // Construct a log name for server log streams
logCounter, ok := h.State["logCounter"].(int) logCounter, ok := h.State["logCounter"].(int)
if !ok { if !ok {
logCounter = 0 logCounter = 0
} }
val.name = fmt.Sprintf("log%d", logCounter) val.name = fmt.Sprintf("log%d", logCounter)
cl.Include = []string{"http.log.access." + val.name}
val.log = cl
logCounter++ logCounter++
h.State["logCounter"] = logCounter h.State["logCounter"] = logCounter
} }
if val.name != "" {
cl.Include = []string{"http.log.access." + val.name}
}
}
if !isEmptyConfig {
val.log = cl
} }
configValues = append(configValues, ConfigValue{ configValues = append(configValues, ConfigValue{
Class: "custom_log", Class: "custom_log",
@@ -983,15 +787,3 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
} }
return configValues, nil return configValues, nil
} }
// parseSkipLog parses the skip_log directive. Syntax:
//
// skip_log [<matcher>]
func parseSkipLog(h Helper) (caddyhttp.MiddlewareHandler, error) {
for h.Next() {
if h.NextArg() {
return nil, h.ArgErr()
}
}
return caddyhttp.VarsMiddleware{"skip_log": true}, nil
}
+11 -163
View File
@@ -1,7 +1,6 @@
package httpcaddyfile package httpcaddyfile
import ( import (
"strings"
"testing" "testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -38,7 +37,8 @@ func TestLogDirectiveSyntax(t *testing.T) {
format filter { format filter {
wrap console wrap console
fields { fields {
request>remote_ip ip_mask { common_log delete
request>remote_addr ip_mask {
ipv4 24 ipv4 24
ipv6 32 ipv6 32
} }
@@ -47,18 +47,17 @@ func TestLogDirectiveSyntax(t *testing.T) {
} }
} }
`, `,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"encoder":{"fields":{"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":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`, output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"encoder":{"fields":{"common_log":{"filter":"delete"},"request\u003eremote_addr":{"filter":"ip_mask","ipv4_cidr":24,"ipv6_cidr":32}},"format":"filter","wrap":{"format":"console"}},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
expectError: false, expectError: false,
}, },
{ {
input: `:8080 { input: `:8080 {
log name-override { log invalid {
output file foo.log output file foo.log
} }
} }
`, `,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`, expectError: true,
expectError: false,
}, },
} { } {
@@ -150,27 +149,6 @@ func TestRedirDirectiveSyntax(t *testing.T) {
}`, }`,
expectError: false, expectError: false,
}, },
{
// this is now allowed so a Location header
// can be written and consumed by JS
// in the case of XHR requests
input: `:8080 {
redir * :8081 401
}`,
expectError: false,
},
{
input: `:8080 {
redir * :8081 402
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 {http.reverse_proxy.status_code}
}`,
expectError: false,
},
{ {
input: `:8080 { input: `:8080 {
redir /old.html /new.html htlm redir /old.html /new.html htlm
@@ -183,6 +161,12 @@ func TestRedirDirectiveSyntax(t *testing.T) {
}`, }`,
expectError: true, expectError: true,
}, },
{
input: `:8080 {
redir * :8081 400
}`,
expectError: true,
},
{ {
input: `:8080 { input: `:8080 {
redir * :8081 temp redir * :8081 temp
@@ -215,139 +199,3 @@ func TestRedirDirectiveSyntax(t *testing.T) {
} }
} }
} }
func TestImportErrorLine(t *testing.T) {
for i, tc := range []struct {
input string
errorFunc func(err error) bool
}{
{
input: `(t1) {
abort {args[:]}
}
:8080 {
import t1
import t1 true
}`,
errorFunc: func(err error) bool {
return err != nil && strings.Contains(err.Error(), "Caddyfile:6 (import t1)")
},
},
{
input: `(t1) {
abort {args[:]}
}
:8080 {
import t1 true
}`,
errorFunc: func(err error) bool {
return err != nil && strings.Contains(err.Error(), "Caddyfile:5 (import t1)")
},
},
{
input: `
import testdata/import_variadic_snippet.txt
:8080 {
import t1 true
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
{
input: `
import testdata/import_variadic_with_import.txt
:8080 {
import t1 true
import t2 true
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
} {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
_, _, err := adapter.Adapt([]byte(tc.input), nil)
if !tc.errorFunc(err) {
t.Errorf("Test %d error expectation failed, got %s", i, err)
continue
}
}
}
func TestNestedImport(t *testing.T) {
for i, tc := range []struct {
input string
errorFunc func(err error) bool
}{
{
input: `(t1) {
respond {args[0]} {args[1]}
}
(t2) {
import t1 {args[0]} 202
}
:8080 {
handle {
import t2 "foobar"
}
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
{
input: `(t1) {
respond {args[:]}
}
(t2) {
import t1 {args[0]} {args[1]}
}
:8080 {
handle {
import t2 "foobar" 202
}
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
{
input: `(t1) {
respond {args[0]} {args[1]}
}
(t2) {
import t1 {args[:]}
}
:8080 {
handle {
import t2 "foobar" 202
}
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
} {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
_, _, err := adapter.Adapt([]byte(tc.input), nil)
if !tc.errorFunc(err) {
t.Errorf("Test %d error expectation failed, got %s", i, err)
continue
}
}
}
+18 -65
View File
@@ -37,35 +37,27 @@ import (
// The header directive goes second so that headers // The header directive goes second so that headers
// can be manipulated before doing redirects. // can be manipulated before doing redirects.
var directiveOrder = []string{ var directiveOrder = []string{
"tracing",
"map", "map",
"vars",
"root", "root",
"skip_log",
"header", "header",
"copy_response_headers", // only in reverse_proxy's handle_response
"request_body", "request_body",
"redir", "redir",
// incoming request manipulation // URI manipulation
"method",
"rewrite", "rewrite",
"uri", "uri",
"try_files", "try_files",
// middleware handlers; some wrap responses // middleware handlers; some wrap responses
"basicauth", "basicauth",
"forward_auth",
"request_header", "request_header",
"encode", "encode",
"push", "push",
"templates", "templates",
// special routing & dispatching directives // special routing & dispatching directives
"invoke",
"handle", "handle",
"handle_path", "handle_path",
"route", "route",
@@ -73,7 +65,6 @@ var directiveOrder = []string{
// handlers that typically respond to requests // handlers that typically respond to requests
"abort", "abort",
"error", "error",
"copy_response", // only in reverse_proxy's handle_response
"respond", "respond",
"metrics", "metrics",
"reverse_proxy", "reverse_proxy",
@@ -144,8 +135,8 @@ func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) {
type Helper struct { type Helper struct {
*caddyfile.Dispenser *caddyfile.Dispenser
// State stores intermediate variables during caddyfile adaptation. // State stores intermediate variables during caddyfile adaptation.
State map[string]any State map[string]interface{}
options map[string]any options map[string]interface{}
warnings *[]caddyconfig.Warning warnings *[]caddyconfig.Warning
matcherDefs map[string]caddy.ModuleMap matcherDefs map[string]caddy.ModuleMap
parentBlock caddyfile.ServerBlock parentBlock caddyfile.ServerBlock
@@ -153,7 +144,7 @@ type Helper struct {
} }
// Option gets the option keyed by name. // Option gets the option keyed by name.
func (h Helper) Option(name string) any { func (h Helper) Option(name string) interface{} {
return h.options[name] return h.options[name]
} }
@@ -173,12 +164,11 @@ func (h Helper) Caddyfiles() []string {
for file := range files { for file := range files {
filesSlice = append(filesSlice, file) filesSlice = append(filesSlice, file)
} }
sort.Strings(filesSlice)
return filesSlice return filesSlice
} }
// JSON converts val into JSON. Any errors are added to warnings. // JSON converts val into JSON. Any errors are added to warnings.
func (h Helper) JSON(val any) json.RawMessage { func (h Helper) JSON(val interface{}) json.RawMessage {
return caddyconfig.JSON(val, h.warnings) return caddyconfig.JSON(val, h.warnings)
} }
@@ -291,7 +281,7 @@ func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
return nil, err return nil, err
} }
return buildSubroute(allResults, h.groupCounter, true) return buildSubroute(allResults, h.groupCounter)
} }
// parseSegmentAsConfig parses the segment such that its subdirectives // parseSegmentAsConfig parses the segment such that its subdirectives
@@ -350,9 +340,6 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
if err != nil { if err != nil {
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err) return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
} }
dir = normalizeDirectiveName(dir)
for _, result := range results { for _, result := range results {
result.directive = dir result.directive = dir
allResults = append(allResults, result) allResults = append(allResults, result)
@@ -378,7 +365,7 @@ type ConfigValue struct {
// The value to be used when building the config. // The value to be used when building the config.
// Generally its type is associated with the // Generally its type is associated with the
// name of the Class. // name of the Class.
Value any Value interface{}
directive string directive string
} }
@@ -409,7 +396,7 @@ func sortRoutes(routes []ConfigValue) {
return false return false
} }
// decode the path matchers if there is just one matcher set // decode the path matchers, if there is just one of them
var iPM, jPM caddyhttp.MatchPath var iPM, jPM caddyhttp.MatchPath
if len(iRoute.MatcherSetsRaw) == 1 { if len(iRoute.MatcherSetsRaw) == 1 {
_ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &iPM) _ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &iPM)
@@ -418,47 +405,24 @@ func sortRoutes(routes []ConfigValue) {
_ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &jPM) _ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &jPM)
} }
// if there is only one path in the path matcher, sort by longer path // sort by longer path (more specific) first; missing path
// (more specific) first; missing path matchers or multi-matchers are // matchers or multi-matchers are treated as zero-length paths
// treated as zero-length paths
var iPathLen, jPathLen int var iPathLen, jPathLen int
if len(iPM) == 1 { if len(iPM) > 0 {
iPathLen = len(iPM[0]) iPathLen = len(iPM[0])
} }
if len(jPM) == 1 { if len(jPM) > 0 {
jPathLen = len(jPM[0]) jPathLen = len(jPM[0])
} }
sortByPath := func() bool { // if both directives have no path matcher, use whichever one
// we can only confidently compare path lengths if both // has any kind of matcher defined first.
// directives have a single path to match (issue #5037) if iPathLen == 0 && jPathLen == 0 {
if iPathLen > 0 && jPathLen > 0 {
// if both paths are the same except for a trailing wildcard,
// sort by the shorter path first (which is more specific)
if strings.TrimSuffix(iPM[0], "*") == strings.TrimSuffix(jPM[0], "*") {
return iPathLen < jPathLen
}
// sort most-specific (longest) path first
return iPathLen > jPathLen
}
// if both directives don't have a single path to compare,
// sort whichever one has a matcher first; if both have
// a matcher, sort equally (stable sort preserves order)
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0 return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
}()
// some directives involve setting values which can overwrite
// each other, so it makes most sense to reverse the order so
// that the least-specific matcher is first, allowing the last
// matching one to win
if iDir == "vars" {
return !sortByPath
} }
// everything else is most-specific matcher first // sort with the most-specific (longest) path first
return sortByPath return iPathLen > jPathLen
}) })
} }
@@ -546,17 +510,6 @@ func (sb serverBlock) hasHostCatchAllKey() bool {
return false return false
} }
// isAllHTTP returns true if all sb keys explicitly specify
// the http:// scheme
func (sb serverBlock) isAllHTTP() bool {
for _, addr := range sb.keys {
if addr.Scheme != "http" {
return false
}
}
return true
}
type ( type (
// UnmarshalFunc is a function which can unmarshal Caddyfile // UnmarshalFunc is a function which can unmarshal Caddyfile
// tokens into zero or more config values using a Helper type. // tokens into zero or more config values using a Helper type.
@@ -578,7 +531,7 @@ type (
// tokens from a global option. It is passed the tokens to parse and // tokens from a global option. It is passed the tokens to parse and
// existing value from the previous instance of this global option // existing value from the previous instance of this global option
// (if any). It returns the value to associate with this global option. // (if any). It returns the value to associate with this global option.
UnmarshalGlobalFunc func(d *caddyfile.Dispenser, existingVal any) (any, error) UnmarshalGlobalFunc func(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error)
) )
var registeredDirectives = make(map[string]UnmarshalFunc) var registeredDirectives = make(map[string]UnmarshalFunc)
+135 -398
View File
@@ -17,6 +17,7 @@ package httpcaddyfile
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"reflect" "reflect"
"regexp" "regexp"
"sort" "sort"
@@ -29,8 +30,6 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddypki" "github.com/caddyserver/caddy/v2/modules/caddypki"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"go.uber.org/zap"
"golang.org/x/exp/slices"
) )
func init() { func init() {
@@ -53,21 +52,28 @@ type ServerType struct {
} }
// Setup makes a config from the tokens. // Setup makes a config from the tokens.
func (st ServerType) Setup( func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
inputServerBlocks []caddyfile.ServerBlock, options map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error) {
options map[string]any,
) (*caddy.Config, []caddyconfig.Warning, error) {
var warnings []caddyconfig.Warning var warnings []caddyconfig.Warning
gc := counter{new(int)} gc := counter{new(int)}
state := make(map[string]any) state := make(map[string]interface{})
// load all the server blocks and associate them with a "pile" of config values // load all the server blocks and associate them with a "pile"
// of config values; also prohibit duplicate keys because they
// can make a config confusing if more than one server block is
// chosen to handle a request - we actually will make each
// server block's route terminal so that only one will run
sbKeys := make(map[string]struct{})
originalServerBlocks := make([]serverBlock, 0, len(inputServerBlocks)) originalServerBlocks := make([]serverBlock, 0, len(inputServerBlocks))
for _, sblock := range inputServerBlocks { for i, sblock := range inputServerBlocks {
for j, k := range sblock.Keys { for j, k := range sblock.Keys {
if j == 0 && strings.HasPrefix(k, "@") { if j == 0 && strings.HasPrefix(k, "@") {
return nil, warnings, fmt.Errorf("cannot define a matcher outside of a site block: '%s'", k) return nil, warnings, fmt.Errorf("cannot define a matcher outside of a site block: '%s'", k)
} }
if _, ok := sbKeys[k]; ok {
return nil, warnings, fmt.Errorf("duplicate site address not allowed: '%s' in %v (site block %d, key %d)", k, sblock.Keys, i, j)
}
sbKeys[k] = struct{}{}
} }
originalServerBlocks = append(originalServerBlocks, serverBlock{ originalServerBlocks = append(originalServerBlocks, serverBlock{
block: sblock, block: sblock,
@@ -82,15 +88,33 @@ func (st ServerType) Setup(
return nil, warnings, err return nil, warnings, err
} }
originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings) // replace shorthand placeholders (which are
if err != nil { // convenient when writing a Caddyfile) with
return nil, warnings, err // their actual placeholder identifiers or
} // variable names
replacer := strings.NewReplacer(
// replace shorthand placeholders (which are convenient "{dir}", "{http.request.uri.path.dir}",
// when writing a Caddyfile) with their actual placeholder "{file}", "{http.request.uri.path.file}",
// identifiers or variable names "{host}", "{http.request.host}",
replacer := strings.NewReplacer(placeholderShorthands()...) "{hostport}", "{http.request.hostport}",
"{port}", "{http.request.port}",
"{method}", "{http.request.method}",
"{path}", "{http.request.uri.path}",
"{query}", "{http.request.uri.query}",
"{remote}", "{http.request.remote}",
"{remote_host}", "{http.request.remote.host}",
"{remote_port}", "{http.request.remote.port}",
"{scheme}", "{http.request.scheme}",
"{uri}", "{http.request.uri}",
"{tls_cipher}", "{http.request.tls.cipher_suite}",
"{tls_version}", "{http.request.tls.version}",
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
"{tls_client_serial}", "{http.request.tls.client.serial}",
"{tls_client_subject}", "{http.request.tls.client.subject}",
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
)
// these are placeholders that allow a user-defined final // these are placeholders that allow a user-defined final
// parameters, but we still want to provide a shorthand // parameters, but we still want to provide a shorthand
@@ -99,17 +123,11 @@ func (st ServerType) Setup(
search *regexp.Regexp search *regexp.Regexp
replace string replace string
}{ }{
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
{regexp.MustCompile(`{cookie\.([\w-]*)}`), "{http.request.cookie.$1}"},
{regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"},
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
{regexp.MustCompile(`{file\.([\w-]*)}`), "{http.request.uri.path.file.$1}"},
{regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"}, {regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"},
{regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"},
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
{regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"}, {regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
{regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
{regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
{regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"},
{regexp.MustCompile(`{file_match\.([\w-]*)}`), "{http.matchers.file.$1}"},
} }
for _, sb := range originalServerBlocks { for _, sb := range originalServerBlocks {
@@ -174,24 +192,18 @@ func (st ServerType) Setup(
return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err) return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err)
} }
dir = normalizeDirectiveName(dir) // As a special case, we want "handle_path" to be sorted
// at the same level as "handle", so we force them to use
// the same directive name after their parsing is complete.
// See https://github.com/caddyserver/caddy/issues/3675#issuecomment-678042377
if dir == "handle_path" {
dir = "handle"
}
for _, result := range results { for _, result := range results {
result.directive = dir result.directive = dir
sb.pile[result.Class] = append(sb.pile[result.Class], result) sb.pile[result.Class] = append(sb.pile[result.Class], result)
} }
// specially handle named routes that were pulled out from
// the invoke directive, which could be nested anywhere within
// some subroutes in this directive; we add them to the pile
// for this server block
if state[namedRouteKey] != nil {
for name := range state[namedRouteKey].(map[string]struct{}) {
result := ConfigValue{Class: namedRouteKey, Value: name}
sb.pile[result.Class] = append(sb.pile[result.Class], result)
}
state[namedRouteKey] = nil
}
} }
} }
@@ -213,11 +225,10 @@ func (st ServerType) Setup(
// now that each server is configured, make the HTTP app // now that each server is configured, make the HTTP app
httpApp := caddyhttp.App{ httpApp := caddyhttp.App{
HTTPPort: tryInt(options["http_port"], &warnings), HTTPPort: tryInt(options["http_port"], &warnings),
HTTPSPort: tryInt(options["https_port"], &warnings), HTTPSPort: tryInt(options["https_port"], &warnings),
GracePeriod: tryDuration(options["grace_period"], &warnings), GracePeriod: tryDuration(options["grace_period"], &warnings),
ShutdownDelay: tryDuration(options["shutdown_delay"], &warnings), Servers: servers,
Servers: servers,
} }
// then make the TLS app // then make the TLS app
@@ -239,44 +250,40 @@ func (st ServerType) Setup(
if ncl.name == "" { if ncl.name == "" {
return return
} }
if ncl.name == caddy.DefaultLoggerName { if ncl.name == "default" {
hasDefaultLog = true hasDefaultLog = true
} }
if _, ok := options["debug"]; ok && ncl.log != nil && ncl.log.Level == "" { if _, ok := options["debug"]; ok && ncl.log.Level == "" {
ncl.log.Level = zap.DebugLevel.CapitalString() ncl.log.Level = "DEBUG"
} }
customLogs = append(customLogs, ncl) customLogs = append(customLogs, ncl)
} }
// Apply global log options, when set // Apply global log options, when set
if options["log"] != nil { if options["log"] != nil {
for _, logValue := range options["log"].([]ConfigValue) { for _, logValue := range options["log"].([]ConfigValue) {
addCustomLog(logValue.Value.(namedCustomLog)) addCustomLog(logValue.Value.(namedCustomLog))
} }
} }
// Apply server-specific log options
for _, p := range pairings {
for _, sb := range p.serverBlocks {
for _, clVal := range sb.pile["custom_log"] {
addCustomLog(clVal.Value.(namedCustomLog))
}
}
}
if !hasDefaultLog { if !hasDefaultLog {
// if the default log was not customized, ensure we // if the default log was not customized, ensure we
// configure it with any applicable options // configure it with any applicable options
if _, ok := options["debug"]; ok { if _, ok := options["debug"]; ok {
customLogs = append(customLogs, namedCustomLog{ customLogs = append(customLogs, namedCustomLog{
name: caddy.DefaultLoggerName, name: "default",
log: &caddy.CustomLog{ log: &caddy.CustomLog{Level: "DEBUG"},
BaseLog: caddy.BaseLog{Level: zap.DebugLevel.CapitalString()},
},
}) })
} }
} }
// Apply server-specific log options
for _, p := range pairings {
for _, sb := range p.serverBlocks {
for _, clVal := range sb.pile["custom_log"] {
addCustomLog(clVal.Value.(namedCustomLog))
}
}
}
// annnd the top-level config, then we're done! // annnd the top-level config, then we're done!
cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)} cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)}
@@ -308,61 +315,28 @@ func (st ServerType) Setup(
if adminConfig, ok := options["admin"].(*caddy.AdminConfig); ok && adminConfig != nil { if adminConfig, ok := options["admin"].(*caddy.AdminConfig); ok && adminConfig != nil {
cfg.Admin = adminConfig cfg.Admin = adminConfig
} }
if pc, ok := options["persist_config"].(string); ok && pc == "off" {
if cfg.Admin == nil {
cfg.Admin = new(caddy.AdminConfig)
}
if cfg.Admin.Config == nil {
cfg.Admin.Config = new(caddy.ConfigSettings)
}
cfg.Admin.Config.Persist = new(bool)
}
if len(customLogs) > 0 { if len(customLogs) > 0 {
if cfg.Logging == nil { if cfg.Logging == nil {
cfg.Logging = &caddy.Logging{ cfg.Logging = &caddy.Logging{
Logs: make(map[string]*caddy.CustomLog), Logs: make(map[string]*caddy.CustomLog),
} }
} }
// Add the default log first if defined, so that it doesn't
// accidentally get re-created below due to the Exclude logic
for _, ncl := range customLogs { for _, ncl := range customLogs {
if ncl.name == caddy.DefaultLoggerName && ncl.log != nil {
cfg.Logging.Logs[caddy.DefaultLoggerName] = ncl.log
break
}
}
// Add the rest of the custom logs
for _, ncl := range customLogs {
if ncl.log == nil || ncl.name == caddy.DefaultLoggerName {
continue
}
if ncl.name != "" { if ncl.name != "" {
cfg.Logging.Logs[ncl.name] = ncl.log cfg.Logging.Logs[ncl.name] = ncl.log
} }
// most users seem to prefer not writing access logs // most users seem to prefer not writing access logs
// to the default log when they are directed to a // to the default log when they are directed to a
// file or have any other special customization // file or have any other special customization
if ncl.name != caddy.DefaultLoggerName && len(ncl.log.Include) > 0 { if ncl.name != "default" && len(ncl.log.Include) > 0 {
defaultLog, ok := cfg.Logging.Logs[caddy.DefaultLoggerName] defaultLog, ok := cfg.Logging.Logs["default"]
if !ok { if !ok {
defaultLog = new(caddy.CustomLog) defaultLog = new(caddy.CustomLog)
cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog cfg.Logging.Logs["default"] = defaultLog
} }
defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...) defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...)
// avoid duplicates by sorting + compacting
sort.Strings(defaultLog.Exclude)
defaultLog.Exclude = slices.Compact[[]string, string](defaultLog.Exclude)
} }
} }
// we may have not actually added anything, so remove if empty
if len(cfg.Logging.Logs) == 0 {
cfg.Logging = nil
}
} }
return cfg, warnings, nil return cfg, warnings, nil
@@ -372,14 +346,14 @@ func (st ServerType) Setup(
// which is expected to be the first server block if it has zero // which is expected to be the first server block if it has zero
// keys. It returns the updated list of server blocks with the // keys. It returns the updated list of server blocks with the
// global options block removed, and updates options accordingly. // global options block removed, and updates options accordingly.
func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options map[string]any) ([]serverBlock, error) { func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options map[string]interface{}) ([]serverBlock, error) {
if len(serverBlocks) == 0 || len(serverBlocks[0].block.Keys) > 0 { if len(serverBlocks) == 0 || len(serverBlocks[0].block.Keys) > 0 {
return serverBlocks, nil return serverBlocks, nil
} }
for _, segment := range serverBlocks[0].block.Segments { for _, segment := range serverBlocks[0].block.Segments {
opt := segment.Directive() opt := segment.Directive()
var val any var val interface{}
var err error var err error
disp := caddyfile.NewDispenser(segment) disp := caddyfile.NewDispenser(segment)
@@ -445,88 +419,16 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
return serverBlocks[1:], nil return serverBlocks[1:], nil
} }
// extractNamedRoutes pulls out any named route server blocks
// so they don't get parsed as sites, and stores them in options
// for later.
func (ServerType) extractNamedRoutes(
serverBlocks []serverBlock,
options map[string]any,
warnings *[]caddyconfig.Warning,
) ([]serverBlock, error) {
namedRoutes := map[string]*caddyhttp.Route{}
gc := counter{new(int)}
state := make(map[string]any)
// copy the server blocks so we can
// splice out the named route ones
filtered := append([]serverBlock{}, serverBlocks...)
index := -1
for _, sb := range serverBlocks {
index++
if !sb.block.IsNamedRoute {
continue
}
// splice out this block, because we know it's not a real server
filtered = append(filtered[:index], filtered[index+1:]...)
index--
if len(sb.block.Segments) == 0 {
continue
}
// zip up all the segments since ParseSegmentAsSubroute
// was designed to take a directive+
wholeSegment := caddyfile.Segment{}
for _, segment := range sb.block.Segments {
wholeSegment = append(wholeSegment, segment...)
}
h := Helper{
Dispenser: caddyfile.NewDispenser(wholeSegment),
options: options,
warnings: warnings,
matcherDefs: nil,
parentBlock: sb.block,
groupCounter: gc,
State: state,
}
handler, err := ParseSegmentAsSubroute(h)
if err != nil {
return nil, err
}
subroute := handler.(*caddyhttp.Subroute)
route := caddyhttp.Route{}
if len(subroute.Routes) == 1 && len(subroute.Routes[0].MatcherSetsRaw) == 0 {
// if there's only one route with no matcher, then we can simplify
route.HandlersRaw = append(route.HandlersRaw, subroute.Routes[0].HandlersRaw[0])
} else {
// otherwise we need the whole subroute
route.HandlersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", subroute.CaddyModule().ID.Name(), h.warnings)}
}
namedRoutes[sb.block.Keys[0]] = &route
}
options["named_routes"] = namedRoutes
return filtered, nil
}
// serversFromPairings creates the servers for each pairing of addresses // serversFromPairings creates the servers for each pairing of addresses
// to server blocks. Each pairing is essentially a server definition. // to server blocks. Each pairing is essentially a server definition.
func (st *ServerType) serversFromPairings( func (st *ServerType) serversFromPairings(
pairings []sbAddrAssociation, pairings []sbAddrAssociation,
options map[string]any, options map[string]interface{},
warnings *[]caddyconfig.Warning, warnings *[]caddyconfig.Warning,
groupCounter counter, groupCounter counter,
) (map[string]*caddyhttp.Server, error) { ) (map[string]*caddyhttp.Server, error) {
servers := make(map[string]*caddyhttp.Server) servers := make(map[string]*caddyhttp.Server)
defaultSNI := tryString(options["default_sni"], warnings) defaultSNI := tryString(options["default_sni"], warnings)
fallbackSNI := tryString(options["fallback_sni"], warnings)
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort) httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
if hp, ok := options["http_port"].(int); ok { if hp, ok := options["http_port"].(int); ok {
@@ -542,23 +444,6 @@ func (st *ServerType) serversFromPairings(
} }
for i, p := range pairings { for i, p := range pairings {
// detect ambiguous site definitions: server blocks which
// have the same host bound to the same interface (listener
// address), otherwise their routes will improperly be added
// to the same server (see issue #4635)
for j, sblock1 := range p.serverBlocks {
for _, key := range sblock1.block.Keys {
for k, sblock2 := range p.serverBlocks {
if k == j {
continue
}
if sliceContains(sblock2.block.Keys, key) {
return nil, fmt.Errorf("ambiguous site definition: %s", key)
}
}
}
}
srv := &caddyhttp.Server{ srv := &caddyhttp.Server{
Listen: p.addresses, Listen: p.addresses,
} }
@@ -566,26 +451,14 @@ func (st *ServerType) serversFromPairings(
// handle the auto_https global option // handle the auto_https global option
if autoHTTPS != "on" { if autoHTTPS != "on" {
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
switch autoHTTPS { if autoHTTPS == "off" {
case "off":
srv.AutoHTTPS.Disabled = true srv.AutoHTTPS.Disabled = true
case "disable_redirects":
srv.AutoHTTPS.DisableRedir = true
case "disable_certs":
srv.AutoHTTPS.DisableCerts = true
case "ignore_loaded_certs":
srv.AutoHTTPS.IgnoreLoadedCerts = true
} }
} if autoHTTPS == "disable_redirects" {
srv.AutoHTTPS.DisableRedir = true
// Using paths in site addresses is deprecated }
// See ParseAddress() where parsing should later reject paths if autoHTTPS == "ignore_loaded_certs" {
// See https://github.com/caddyserver/caddy/pull/4728 for a full explanation srv.AutoHTTPS.IgnoreLoadedCerts = true
for _, sblock := range p.serverBlocks {
for _, addr := range sblock.keys {
if addr.Path != "" {
caddy.Log().Named("caddyfile").Warn("Using a path in a site address is deprecated; please use the 'handle' directive instead", zap.String("address", addr.String()))
}
} }
} }
@@ -645,6 +518,15 @@ func (st *ServerType) serversFromPairings(
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
autoHTTPSWillAddConnPolicy := autoHTTPS != "off" autoHTTPSWillAddConnPolicy := autoHTTPS != "off"
// if a catch-all server block (one which accepts all hostnames) exists in this pairing,
// we need to know that so that we can configure logs properly (see #3878)
var catchAllSblockExists bool
for _, sblock := range p.serverBlocks {
if len(sblock.hostsFromKeys(false)) == 0 {
catchAllSblockExists = true
}
}
// if needed, the ServerLogConfig is initialized beforehand so // if needed, the ServerLogConfig is initialized beforehand so
// that all server blocks can populate it with data, even when not // that all server blocks can populate it with data, even when not
// coming with a log directive // coming with a log directive
@@ -655,24 +537,6 @@ func (st *ServerType) serversFromPairings(
} }
} }
// add named routes to the server if 'invoke' was used inside of it
configuredNamedRoutes := options["named_routes"].(map[string]*caddyhttp.Route)
for _, sblock := range p.serverBlocks {
if len(sblock.pile[namedRouteKey]) == 0 {
continue
}
for _, value := range sblock.pile[namedRouteKey] {
if srv.NamedRoutes == nil {
srv.NamedRoutes = map[string]*caddyhttp.Route{}
}
name := value.Value.(string)
if configuredNamedRoutes[name] == nil {
return nil, fmt.Errorf("cannot invoke named route '%s', which was not defined", name)
}
srv.NamedRoutes[name] = configuredNamedRoutes[name]
}
}
// create a subroute for each site in the server block // create a subroute for each site in the server block
for _, sblock := range p.serverBlocks { for _, sblock := range p.serverBlocks {
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock) matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
@@ -685,7 +549,7 @@ func (st *ServerType) serversFromPairings(
// emit warnings if user put unspecified IP addresses; they probably want the bind directive // emit warnings if user put unspecified IP addresses; they probably want the bind directive
for _, h := range hosts { for _, h := range hosts {
if h == "0.0.0.0" || h == "::" { if h == "0.0.0.0" || h == "::" {
caddy.Log().Named("caddyfile").Warn("Site block has an unspecified IP address which only matches requests having that Host header; you probably want the 'bind' directive to configure the socket", zap.String("address", h)) log.Printf("[WARNING] Site block has unspecified IP address %s which only matches requests having that Host header; you probably want the 'bind' directive to configure the socket", h)
} }
} }
@@ -702,11 +566,6 @@ func (st *ServerType) serversFromPairings(
cp.DefaultSNI = defaultSNI cp.DefaultSNI = defaultSNI
break break
} }
if h == fallbackSNI {
hosts = append(hosts, "")
cp.FallbackSNI = fallbackSNI
break
}
} }
if len(hosts) > 0 { if len(hosts) > 0 {
@@ -715,7 +574,6 @@ func (st *ServerType) serversFromPairings(
} }
} else { } else {
cp.DefaultSNI = defaultSNI cp.DefaultSNI = defaultSNI
cp.FallbackSNI = fallbackSNI
} }
// only append this policy if it actually changes something // only append this policy if it actually changes something
@@ -727,7 +585,7 @@ func (st *ServerType) serversFromPairings(
} }
for _, addr := range sblock.keys { for _, addr := range sblock.keys {
// if server only uses HTTP port, auto-HTTPS will not apply // if server only uses HTTPS port, auto-HTTPS will not apply
if listenersUseAnyPortOtherThan(srv.Listen, httpPort) { if listenersUseAnyPortOtherThan(srv.Listen, httpPort) {
// exclude any hosts that were defined explicitly with "http://" // exclude any hosts that were defined explicitly with "http://"
// in the key from automated cert management (issue #2998) // in the key from automated cert management (issue #2998)
@@ -769,7 +627,7 @@ func (st *ServerType) serversFromPairings(
// set up each handler directive, making sure to honor directive order // set up each handler directive, making sure to honor directive order
dirRoutes := sblock.pile["route"] dirRoutes := sblock.pile["route"]
siteSubroute, err := buildSubroute(dirRoutes, groupCounter, true) siteSubroute, err := buildSubroute(dirRoutes, groupCounter)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -793,25 +651,25 @@ func (st *ServerType) serversFromPairings(
sblockLogHosts := sblock.hostsFromKeys(true) sblockLogHosts := sblock.hostsFromKeys(true)
for _, cval := range sblock.pile["custom_log"] { for _, cval := range sblock.pile["custom_log"] {
ncl := cval.Value.(namedCustomLog) ncl := cval.Value.(namedCustomLog)
if sblock.hasHostCatchAllKey() && len(ncl.hostnames) == 0 { if sblock.hasHostCatchAllKey() {
// all requests for hosts not able to be listed should use // all requests for hosts not able to be listed should use
// this log because it's a catch-all-hosts server block // this log because it's a catch-all-hosts server block
srv.Logs.DefaultLoggerName = ncl.name srv.Logs.DefaultLoggerName = ncl.name
} else if len(ncl.hostnames) > 0 {
// if the logger overrides the hostnames, map that to the logger name
for _, h := range ncl.hostnames {
if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string)
}
srv.Logs.LoggerNames[h] = ncl.name
}
} else { } else {
// otherwise, map each host to the logger name // map each host to the user's desired logger name
for _, h := range sblockLogHosts { for _, h := range sblockLogHosts {
if srv.Logs.LoggerNames == nil { // if the custom logger name is non-empty, add it to the map;
srv.Logs.LoggerNames = make(map[string]string) // otherwise, only map to an empty logger name if this or
// another site block on this server has a catch-all host (in
// which case only requests with mapped hostnames will be
// access-logged, so it'll be necessary to add them to the
// map even if they use default logger)
if ncl.name != "" || catchAllSblockExists {
if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string)
}
srv.Logs.LoggerNames[h] = ncl.name
} }
srv.Logs.LoggerNames[h] = ncl.name
} }
} }
} }
@@ -849,8 +707,8 @@ func (st *ServerType) serversFromPairings(
// policy missing for any HTTPS-enabled hosts, if so, add it... maybe? // policy missing for any HTTPS-enabled hosts, if so, add it... maybe?
if addressQualifiesForTLS && if addressQualifiesForTLS &&
!hasCatchAllTLSConnPolicy && !hasCatchAllTLSConnPolicy &&
(len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "" || fallbackSNI != "") { (len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "") {
srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI, FallbackSNI: fallbackSNI}) srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI})
} }
// tidy things up a bit // tidy things up a bit
@@ -865,13 +723,13 @@ func (st *ServerType) serversFromPairings(
err := applyServerOptions(servers, options, warnings) err := applyServerOptions(servers, options, warnings)
if err != nil { if err != nil {
return nil, fmt.Errorf("applying global server options: %v", err) return nil, err
} }
return servers, nil return servers, nil
} }
func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock, options map[string]any) error { func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock, options map[string]interface{}) error {
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort) httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
if hp, ok := options["http_port"].(int); ok { if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp) httpPort = strconv.Itoa(hp)
@@ -1066,32 +924,11 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
return routeList return routeList
} }
// No need to wrap the handlers in a subroute if this is the only server block
// and there is no matcher for it (doing so would produce unnecessarily nested
// JSON), *unless* there is a host matcher within this site block; if so, then
// we still need to wrap in a subroute because otherwise the host matcher from
// the inside of the site block would be a top-level host matcher, which is
// subject to auto-HTTPS (cert management), and using a host matcher within
// a site block is a valid, common pattern for excluding domains from cert
// management, leading to unexpected behavior; see issue #5124.
wrapInSubroute := true
if len(matcherSetsEnc) == 0 && len(p.serverBlocks) == 1 { if len(matcherSetsEnc) == 0 && len(p.serverBlocks) == 1 {
var hasHostMatcher bool // no need to wrap the handlers in a subroute if this is
outer: // the only server block and there is no matcher for it
for _, route := range subroute.Routes { routeList = append(routeList, subroute.Routes...)
for _, ms := range route.MatcherSetsRaw { } else {
for matcherName := range ms {
if matcherName == "host" {
hasHostMatcher = true
break outer
}
}
}
}
wrapInSubroute = hasHostMatcher
}
if wrapInSubroute {
route := caddyhttp.Route{ route := caddyhttp.Route{
// the semantics of a site block in the Caddyfile dictate // the semantics of a site block in the Caddyfile dictate
// that only the first matching one is evaluated, since // that only the first matching one is evaluated, since
@@ -1109,26 +946,21 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
if len(route.MatcherSetsRaw) > 0 || len(route.HandlersRaw) > 0 { if len(route.MatcherSetsRaw) > 0 || len(route.HandlersRaw) > 0 {
routeList = append(routeList, route) routeList = append(routeList, route)
} }
} else {
routeList = append(routeList, subroute.Routes...)
} }
return routeList return routeList
} }
// buildSubroute turns the config values, which are expected to be routes // buildSubroute turns the config values, which are expected to be routes
// into a clean and orderly subroute that has all the routes within it. // into a clean and orderly subroute that has all the routes within it.
func buildSubroute(routes []ConfigValue, groupCounter counter, needsSorting bool) (*caddyhttp.Subroute, error) { func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subroute, error) {
if needsSorting { for _, val := range routes {
for _, val := range routes { if !directiveIsOrdered(val.directive) {
if !directiveIsOrdered(val.directive) { return nil, fmt.Errorf("directive '%s' is not ordered, so it cannot be used here", val.directive)
return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here - try placing within a route block or using the order global option", val.directive)
}
} }
sortRoutes(routes)
} }
sortRoutes(routes)
subroute := new(caddyhttp.Subroute) subroute := new(caddyhttp.Subroute)
// some directives are mutually exclusive (only first matching // some directives are mutually exclusive (only first matching
@@ -1228,19 +1060,6 @@ func buildSubroute(routes []ConfigValue, groupCounter counter, needsSorting bool
return subroute, nil return subroute, nil
} }
// normalizeDirectiveName ensures directives that should be sorted
// at the same level are named the same before sorting happens.
func normalizeDirectiveName(directive string) string {
// As a special case, we want "handle_path" to be sorted
// at the same level as "handle", so we force them to use
// the same directive name after their parsing is complete.
// See https://github.com/caddyserver/caddy/issues/3675#issuecomment-678042377
if directive == "handle_path" {
directive = "handle"
}
return directive
}
// consolidateRoutes combines routes with the same properties // consolidateRoutes combines routes with the same properties
// (same matchers, same Terminal and Group settings) for a // (same matchers, same Terminal and Group settings) for a
// cleaner overall output. // cleaner overall output.
@@ -1371,7 +1190,6 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod
func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.ModuleMap) error { func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.ModuleMap) error {
for d.Next() { for d.Next() {
// this is the "name" for "named matchers"
definitionName := d.Val() definitionName := d.Val()
if _, ok := matchers[definitionName]; ok { if _, ok := matchers[definitionName]; ok {
@@ -1379,9 +1197,16 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
} }
matchers[definitionName] = make(caddy.ModuleMap) matchers[definitionName] = make(caddy.ModuleMap)
// given a matcher name and the tokens following it, parse // in case there are multiple instances of the same matcher, concatenate
// the tokens as a matcher module and record it // their tokens (we expect that UnmarshalCaddyfile should be able to
makeMatcher := func(matcherName string, tokens []caddyfile.Token) error { // handle more than one segment); otherwise, we'd overwrite other
// instances of the matcher in this set
tokensByMatcherName := make(map[string][]caddyfile.Token)
for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
matcherName := d.Val()
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
}
for matcherName, tokens := range tokensByMatcherName {
mod, err := caddy.GetModule("http.matchers." + matcherName) mod, err := caddy.GetModule("http.matchers." + matcherName)
if err != nil { if err != nil {
return fmt.Errorf("getting matcher module '%s': %v", matcherName, err) return fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
@@ -1399,39 +1224,6 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
} }
matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil) matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
return nil
}
// if the next token is quoted, we can assume it's not a matcher name
// and that it's probably an 'expression' matcher
if d.NextArg() {
if d.Token().Quoted() {
err := makeMatcher("expression", []caddyfile.Token{d.Token()})
if err != nil {
return err
}
continue
}
// if it wasn't quoted, then we need to rewind after calling
// d.NextArg() so the below properly grabs the matcher name
d.Prev()
}
// in case there are multiple instances of the same matcher, concatenate
// their tokens (we expect that UnmarshalCaddyfile should be able to
// handle more than one segment); otherwise, we'd overwrite other
// instances of the matcher in this set
tokensByMatcherName := make(map[string][]caddyfile.Token)
for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
matcherName := d.Val()
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
}
for matcherName, tokens := range tokensByMatcherName {
err := makeMatcher(matcherName, tokens)
if err != nil {
return err
}
} }
} }
return nil return nil
@@ -1449,62 +1241,9 @@ func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.Modul
return msEncoded, nil return msEncoded, nil
} }
// placeholderShorthands returns a slice of old-new string pairs,
// where the left of the pair is a placeholder shorthand that may
// be used in the Caddyfile, and the right is the replacement.
func placeholderShorthands() []string {
return []string{
"{dir}", "{http.request.uri.path.dir}",
"{file}", "{http.request.uri.path.file}",
"{host}", "{http.request.host}",
"{hostport}", "{http.request.hostport}",
"{port}", "{http.request.port}",
"{method}", "{http.request.method}",
"{path}", "{http.request.uri.path}",
"{query}", "{http.request.uri.query}",
"{remote}", "{http.request.remote}",
"{remote_host}", "{http.request.remote.host}",
"{remote_port}", "{http.request.remote.port}",
"{scheme}", "{http.request.scheme}",
"{uri}", "{http.request.uri}",
"{tls_cipher}", "{http.request.tls.cipher_suite}",
"{tls_version}", "{http.request.tls.version}",
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
"{tls_client_serial}", "{http.request.tls.client.serial}",
"{tls_client_subject}", "{http.request.tls.client.subject}",
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
"{client_ip}", "{http.vars.client_ip}",
}
}
// WasReplacedPlaceholderShorthand checks if a token string was
// likely a replaced shorthand of the known Caddyfile placeholder
// replacement outputs. Useful to prevent some user-defined map
// output destinations from overlapping with one of the
// predefined shorthands.
func WasReplacedPlaceholderShorthand(token string) string {
prev := ""
for i, item := range placeholderShorthands() {
// only look at every 2nd item, which is the replacement
if i%2 == 0 {
prev = item
continue
}
if strings.Trim(token, "{}") == strings.Trim(item, "{}") {
// we return the original shorthand so it
// can be used for an error message
return prev
}
}
return ""
}
// tryInt tries to convert val to an integer. If it fails, // tryInt tries to convert val to an integer. If it fails,
// it downgrades the error to a warning and returns 0. // it downgrades the error to a warning and returns 0.
func tryInt(val any, warnings *[]caddyconfig.Warning) int { func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int {
intVal, ok := val.(int) intVal, ok := val.(int)
if val != nil && !ok && warnings != nil { if val != nil && !ok && warnings != nil {
*warnings = append(*warnings, caddyconfig.Warning{Message: "not an integer type"}) *warnings = append(*warnings, caddyconfig.Warning{Message: "not an integer type"})
@@ -1512,7 +1251,7 @@ func tryInt(val any, warnings *[]caddyconfig.Warning) int {
return intVal return intVal
} }
func tryString(val any, warnings *[]caddyconfig.Warning) string { func tryString(val interface{}, warnings *[]caddyconfig.Warning) string {
stringVal, ok := val.(string) stringVal, ok := val.(string)
if val != nil && !ok && warnings != nil { if val != nil && !ok && warnings != nil {
*warnings = append(*warnings, caddyconfig.Warning{Message: "not a string type"}) *warnings = append(*warnings, caddyconfig.Warning{Message: "not a string type"})
@@ -1520,7 +1259,7 @@ func tryString(val any, warnings *[]caddyconfig.Warning) string {
return stringVal return stringVal
} }
func tryDuration(val any, warnings *[]caddyconfig.Warning) caddy.Duration { func tryDuration(val interface{}, warnings *[]caddyconfig.Warning) caddy.Duration {
durationVal, ok := val.(caddy.Duration) durationVal, ok := val.(caddy.Duration)
if val != nil && !ok && warnings != nil { if val != nil && !ok && warnings != nil {
*warnings = append(*warnings, caddyconfig.Warning{Message: "not a duration type"}) *warnings = append(*warnings, caddyconfig.Warning{Message: "not a duration type"})
@@ -1595,9 +1334,8 @@ func (c counter) nextGroup() string {
} }
type namedCustomLog struct { type namedCustomLog struct {
name string name string
hostnames []string log *caddy.CustomLog
log *caddy.CustomLog
} }
// sbAddrAssociation is a mapping from a list of // sbAddrAssociation is a mapping from a list of
@@ -1609,7 +1347,6 @@ type sbAddrAssociation struct {
} }
const matcherPrefix = "@" const matcherPrefix = "@"
const namedRouteKey = "named_route"
// Interface guard // Interface guard
var _ caddyfile.ServerType = (*ServerType)(nil) var _ caddyfile.ServerType = (*ServerType)(nil)
+27 -56
View File
@@ -29,16 +29,11 @@ func init() {
RegisterGlobalOption("debug", parseOptTrue) RegisterGlobalOption("debug", parseOptTrue)
RegisterGlobalOption("http_port", parseOptHTTPPort) RegisterGlobalOption("http_port", parseOptHTTPPort)
RegisterGlobalOption("https_port", parseOptHTTPSPort) RegisterGlobalOption("https_port", parseOptHTTPSPort)
RegisterGlobalOption("default_bind", parseOptStringList)
RegisterGlobalOption("grace_period", parseOptDuration) RegisterGlobalOption("grace_period", parseOptDuration)
RegisterGlobalOption("shutdown_delay", parseOptDuration)
RegisterGlobalOption("default_sni", parseOptSingleString) RegisterGlobalOption("default_sni", parseOptSingleString)
RegisterGlobalOption("fallback_sni", parseOptSingleString)
RegisterGlobalOption("order", parseOptOrder) RegisterGlobalOption("order", parseOptOrder)
RegisterGlobalOption("storage", parseOptStorage) RegisterGlobalOption("storage", parseOptStorage)
RegisterGlobalOption("storage_clean_interval", parseOptDuration) RegisterGlobalOption("storage_clean_interval", parseOptDuration)
RegisterGlobalOption("renew_interval", parseOptDuration)
RegisterGlobalOption("ocsp_interval", parseOptDuration)
RegisterGlobalOption("acme_ca", parseOptSingleString) RegisterGlobalOption("acme_ca", parseOptSingleString)
RegisterGlobalOption("acme_ca_root", parseOptSingleString) RegisterGlobalOption("acme_ca_root", parseOptSingleString)
RegisterGlobalOption("acme_dns", parseOptACMEDNS) RegisterGlobalOption("acme_dns", parseOptACMEDNS)
@@ -55,12 +50,11 @@ func init() {
RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions) RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
RegisterGlobalOption("log", parseLogOptions) RegisterGlobalOption("log", parseLogOptions)
RegisterGlobalOption("preferred_chains", parseOptPreferredChains) RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
RegisterGlobalOption("persist_config", parseOptPersistConfig)
} }
func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil } func parseOptTrue(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { return true, nil }
func parseOptHTTPPort(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptHTTPPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
var httpPort int var httpPort int
for d.Next() { for d.Next() {
var httpPortStr string var httpPortStr string
@@ -76,7 +70,7 @@ func parseOptHTTPPort(d *caddyfile.Dispenser, _ any) (any, error) {
return httpPort, nil return httpPort, nil
} }
func parseOptHTTPSPort(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptHTTPSPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
var httpsPort int var httpsPort int
for d.Next() { for d.Next() {
var httpsPortStr string var httpsPortStr string
@@ -92,7 +86,7 @@ func parseOptHTTPSPort(d *caddyfile.Dispenser, _ any) (any, error) {
return httpsPort, nil return httpsPort, nil
} }
func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptOrder(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
newOrder := directiveOrder newOrder := directiveOrder
for d.Next() { for d.Next() {
@@ -168,7 +162,7 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
return newOrder, nil return newOrder, nil
} }
func parseOptStorage(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptStorage(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
if !d.Next() { // consume option name if !d.Next() { // consume option name
return nil, d.ArgErr() return nil, d.ArgErr()
} }
@@ -187,7 +181,7 @@ func parseOptStorage(d *caddyfile.Dispenser, _ any) (any, error) {
return storage, nil return storage, nil
} }
func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptDuration(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
if !d.Next() { // consume option name if !d.Next() { // consume option name
return nil, d.ArgErr() return nil, d.ArgErr()
} }
@@ -201,7 +195,7 @@ func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) {
return caddy.Duration(dur), nil return caddy.Duration(dur), nil
} }
func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptACMEDNS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
if !d.Next() { // consume option name if !d.Next() { // consume option name
return nil, d.ArgErr() return nil, d.ArgErr()
} }
@@ -220,7 +214,7 @@ func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) {
return prov, nil return prov, nil
} }
func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptACMEEAB(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
eab := new(acme.EAB) eab := new(acme.EAB)
for d.Next() { for d.Next() {
if d.NextArg() { if d.NextArg() {
@@ -248,7 +242,7 @@ func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) {
return eab, nil return eab, nil
} }
func parseOptCertIssuer(d *caddyfile.Dispenser, existing any) (any, error) { func parseOptCertIssuer(d *caddyfile.Dispenser, existing interface{}) (interface{}, error) {
var issuers []certmagic.Issuer var issuers []certmagic.Issuer
if existing != nil { if existing != nil {
issuers = existing.([]certmagic.Issuer) issuers = existing.([]certmagic.Issuer)
@@ -271,7 +265,7 @@ func parseOptCertIssuer(d *caddyfile.Dispenser, existing any) (any, error) {
return issuers, nil return issuers, nil
} }
func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptSingleString(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() // consume parameter name d.Next() // consume parameter name
if !d.Next() { if !d.Next() {
return "", d.ArgErr() return "", d.ArgErr()
@@ -283,16 +277,7 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) {
return val, nil return val, nil
} }
func parseOptStringList(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() // consume parameter name
val := d.RemainingArgs()
if len(val) == 0 {
return "", d.ArgErr()
}
return val, nil
}
func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) {
adminCfg := new(caddy.AdminConfig) adminCfg := new(caddy.AdminConfig)
for d.Next() { for d.Next() {
if d.NextArg() { if d.NextArg() {
@@ -328,7 +313,7 @@ func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) {
return adminCfg, nil return adminCfg, nil
} }
func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptOnDemand(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
var ond *caddytls.OnDemandConfig var ond *caddytls.OnDemandConfig
for d.Next() { for d.Next() {
if d.NextArg() { if d.NextArg() {
@@ -388,7 +373,7 @@ func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) {
return ond, nil return ond, nil
} }
func parseOptPersistConfig(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() // consume parameter name d.Next() // consume parameter name
if !d.Next() { if !d.Next() {
return "", d.ArgErr() return "", d.ArgErr()
@@ -397,32 +382,17 @@ func parseOptPersistConfig(d *caddyfile.Dispenser, _ any) (any, error) {
if d.Next() { if d.Next() {
return "", d.ArgErr() return "", d.ArgErr()
} }
if val != "off" { if val != "off" && val != "disable_redirects" && val != "ignore_loaded_certs" {
return "", d.Errf("persist_config must be 'off'") return "", d.Errf("auto_https must be one of 'off', 'disable_redirects' or 'ignore_loaded_certs'")
} }
return val, nil return val, nil
} }
func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) { func parseServerOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() // consume parameter name
if !d.Next() {
return "", d.ArgErr()
}
val := d.Val()
if d.Next() {
return "", d.ArgErr()
}
if val != "off" && val != "disable_redirects" && val != "disable_certs" && val != "ignore_loaded_certs" {
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', or 'ignore_loaded_certs'")
}
return val, nil
}
func parseServerOptions(d *caddyfile.Dispenser, _ any) (any, error) {
return unmarshalCaddyfileServerOptions(d) return unmarshalCaddyfileServerOptions(d)
} }
func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ any) (any, error) { func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() // consume option name d.Next() // consume option name
var val string var val string
if !d.AllArgs(&val) { if !d.AllArgs(&val) {
@@ -438,17 +408,18 @@ func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ any) (any, error) {
// parseLogOptions parses the global log option. Syntax: // parseLogOptions parses the global log option. Syntax:
// //
// log [name] { // log [name] {
// output <writer_module> ... // output <writer_module> ...
// format <encoder_module> ... // format <encoder_module> ...
// level <level> // level <level>
// include <namespaces...> // include <namespaces...>
// exclude <namespaces...> // exclude <namespaces...>
// } // }
// //
// When the name argument is unspecified, this directive modifies the default // When the name argument is unspecified, this directive modifies the default
// logger. // logger.
func parseLogOptions(d *caddyfile.Dispenser, existingVal any) (any, error) { //
func parseLogOptions(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) {
currentNames := make(map[string]struct{}) currentNames := make(map[string]struct{})
if existingVal != nil { if existingVal != nil {
innerVals, ok := existingVal.([]ConfigValue) innerVals, ok := existingVal.([]ConfigValue)
@@ -483,7 +454,7 @@ func parseLogOptions(d *caddyfile.Dispenser, existingVal any) (any, error) {
return configValues, nil return configValues, nil
} }
func parseOptPreferredChains(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptPreferredChains(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() d.Next()
return caddytls.ParseCaddyfilePreferredChainsOptions(d) return caddytls.ParseCaddyfilePreferredChainsOptions(d)
} }
+4 -173
View File
@@ -15,188 +15,24 @@
package httpcaddyfile package httpcaddyfile
import ( import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddypki" "github.com/caddyserver/caddy/v2/modules/caddypki"
) )
func init() {
RegisterGlobalOption("pki", parsePKIApp)
}
// parsePKIApp parses the global log option. Syntax:
//
// pki {
// ca [<id>] {
// name <name>
// root_cn <name>
// intermediate_cn <name>
// intermediate_lifetime <duration>
// root {
// cert <path>
// key <path>
// format <format>
// }
// intermediate {
// cert <path>
// key <path>
// format <format>
// }
// }
// }
//
// When the CA ID is unspecified, 'local' is assumed.
func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) {
pki := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
for d.Next() {
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "ca":
pkiCa := new(caddypki.CA)
if d.NextArg() {
pkiCa.ID = d.Val()
if d.NextArg() {
return nil, d.ArgErr()
}
}
if pkiCa.ID == "" {
pkiCa.ID = caddypki.DefaultCAID
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "name":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Name = d.Val()
case "root_cn":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.RootCommonName = d.Val()
case "intermediate_cn":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.IntermediateCommonName = d.Val()
case "intermediate_lifetime":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, err
}
pkiCa.IntermediateLifetime = caddy.Duration(dur)
case "root":
if pkiCa.Root == nil {
pkiCa.Root = new(caddypki.KeyPair)
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "cert":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Root.Certificate = d.Val()
case "key":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Root.PrivateKey = d.Val()
case "format":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Root.Format = d.Val()
default:
return nil, d.Errf("unrecognized pki ca root option '%s'", d.Val())
}
}
case "intermediate":
if pkiCa.Intermediate == nil {
pkiCa.Intermediate = new(caddypki.KeyPair)
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "cert":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Intermediate.Certificate = d.Val()
case "key":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Intermediate.PrivateKey = d.Val()
case "format":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Intermediate.Format = d.Val()
default:
return nil, d.Errf("unrecognized pki ca intermediate option '%s'", d.Val())
}
}
default:
return nil, d.Errf("unrecognized pki ca option '%s'", d.Val())
}
}
pki.CAs[pkiCa.ID] = pkiCa
default:
return nil, d.Errf("unrecognized pki option '%s'", d.Val())
}
}
}
return pki, nil
}
func (st ServerType) buildPKIApp( func (st ServerType) buildPKIApp(
pairings []sbAddrAssociation, pairings []sbAddrAssociation,
options map[string]any, options map[string]interface{},
warnings []caddyconfig.Warning, warnings []caddyconfig.Warning,
) (*caddypki.PKI, []caddyconfig.Warning, error) { ) (*caddypki.PKI, []caddyconfig.Warning, error) {
pkiApp := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
skipInstallTrust := false skipInstallTrust := false
if _, ok := options["skip_install_trust"]; ok { if _, ok := options["skip_install_trust"]; ok {
skipInstallTrust = true skipInstallTrust = true
} }
falseBool := false falseBool := false
// Load the PKI app configured via global options
var pkiApp *caddypki.PKI
unwrappedPki, ok := options["pki"].(*caddypki.PKI)
if ok {
pkiApp = unwrappedPki
} else {
pkiApp = &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
}
for _, ca := range pkiApp.CAs {
if skipInstallTrust {
ca.InstallTrust = &falseBool
}
pkiApp.CAs[ca.ID] = ca
}
// Add in the CAs configured via directives
for _, p := range pairings { for _, p := range pairings {
for _, sblock := range p.serverBlocks { for _, sblock := range p.serverBlocks {
// find all the CAs that were defined and add them to the app config // find all the CAs that were defined and add them to the app config
@@ -206,12 +42,7 @@ func (st ServerType) buildPKIApp(
if skipInstallTrust { if skipInstallTrust {
ca.InstallTrust = &falseBool ca.InstallTrust = &falseBool
} }
pkiApp.CAs[ca.ID] = ca
// the CA might already exist from global options, so
// don't overwrite it in that case
if _, ok := pkiApp.CAs[ca.ID]; !ok {
pkiApp.CAs[ca.ID] = ca
}
} }
} }
} }
+35 -169
View File
@@ -33,24 +33,18 @@ type serverOptions struct {
ListenerAddress string ListenerAddress string
// These will all map 1:1 to the caddyhttp.Server struct // These will all map 1:1 to the caddyhttp.Server struct
Name string ListenerWrappersRaw []json.RawMessage
ListenerWrappersRaw []json.RawMessage ReadTimeout caddy.Duration
ReadTimeout caddy.Duration ReadHeaderTimeout caddy.Duration
ReadHeaderTimeout caddy.Duration WriteTimeout caddy.Duration
WriteTimeout caddy.Duration IdleTimeout caddy.Duration
IdleTimeout caddy.Duration MaxHeaderBytes int
KeepAliveInterval caddy.Duration AllowH2C bool
MaxHeaderBytes int ExperimentalHTTP3 bool
EnableFullDuplex bool StrictSNIHost *bool
Protocols []string
StrictSNIHost *bool
TrustedProxiesRaw json.RawMessage
ClientIPHeaders []string
ShouldLogCredentials bool
Metrics *caddyhttp.Metrics
} }
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) { func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
serverOpts := serverOptions{} serverOpts := serverOptions{}
for d.Next() { for d.Next() {
if d.NextArg() { if d.NextArg() {
@@ -61,15 +55,6 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
} }
for nesting := d.Nesting(); d.NextBlock(nesting); { for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() { switch d.Val() {
case "name":
if serverOpts.ListenerAddress == "" {
return nil, d.Errf("cannot set a name for a server without a listener address")
}
if !d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.Name = d.Val()
case "listener_wrappers": case "listener_wrappers":
for nesting := d.Nesting(); d.NextBlock(nesting); { for nesting := d.Nesting(); d.NextBlock(nesting); {
modID := "caddy.listeners." + d.Val() modID := "caddy.listeners." + d.Val()
@@ -137,15 +122,6 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val()) return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
} }
} }
case "keepalive_interval":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing keepalive interval duration: %v", err)
}
serverOpts.KeepAliveInterval = caddy.Duration(dur)
case "max_header_size": case "max_header_size":
var sizeStr string var sizeStr string
@@ -158,113 +134,27 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
} }
serverOpts.MaxHeaderBytes = int(size) serverOpts.MaxHeaderBytes = int(size)
case "enable_full_duplex":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.EnableFullDuplex = true
case "log_credentials":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.ShouldLogCredentials = true
case "protocols":
protos := d.RemainingArgs()
for _, proto := range protos {
if proto != "h1" && proto != "h2" && proto != "h2c" && proto != "h3" {
return nil, d.Errf("unknown protocol '%s': expected h1, h2, h2c, or h3", proto)
}
if sliceContains(serverOpts.Protocols, proto) {
return nil, d.Errf("protocol %s specified more than once", proto)
}
serverOpts.Protocols = append(serverOpts.Protocols, proto)
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
case "strict_sni_host":
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
}
boolVal := true
if d.Val() == "insecure_off" {
boolVal = false
}
serverOpts.StrictSNIHost = &boolVal
case "trusted_proxies":
if !d.NextArg() {
return nil, d.Err("trusted_proxies expects an IP range source module name as its first argument")
}
modID := "http.ip_sources." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
source, ok := unm.(caddyhttp.IPRangeSource)
if !ok {
return nil, fmt.Errorf("module %s (%T) is not an IP range source", modID, unm)
}
jsonSource := caddyconfig.JSONModuleObject(
source,
"source",
source.(caddy.Module).CaddyModule().ID.Name(),
nil,
)
serverOpts.TrustedProxiesRaw = jsonSource
case "client_ip_headers":
headers := d.RemainingArgs()
for _, header := range headers {
if sliceContains(serverOpts.ClientIPHeaders, header) {
return nil, d.Errf("client IP header %s specified more than once", header)
}
serverOpts.ClientIPHeaders = append(serverOpts.ClientIPHeaders, header)
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
case "metrics":
if d.NextArg() {
return nil, d.ArgErr()
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
serverOpts.Metrics = new(caddyhttp.Metrics)
// TODO: DEPRECATED. (August 2022)
case "protocol": case "protocol":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol sub-option will be removed soon")
for nesting := d.Nesting(); d.NextBlock(nesting); { for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() { switch d.Val() {
case "allow_h2c": case "allow_h2c":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: allow_h2c will be removed soon; use protocols option instead")
if d.NextArg() { if d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
} }
if sliceContains(serverOpts.Protocols, "h2c") { serverOpts.AllowH2C = true
return nil, d.Errf("protocol h2c already specified")
case "experimental_http3":
if d.NextArg() {
return nil, d.ArgErr()
} }
serverOpts.Protocols = append(serverOpts.Protocols, "h2c") serverOpts.ExperimentalHTTP3 = true
case "strict_sni_host": case "strict_sni_host":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol > strict_sni_host in this position will be removed soon; move up to the servers block instead") if d.NextArg() {
return nil, d.ArgErr()
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
} }
boolVal := true trueBool := true
if d.Val() == "insecure_off" { serverOpts.StrictSNIHost = &trueBool
boolVal = false
}
serverOpts.StrictSNIHost = &boolVal
default: default:
return nil, d.Errf("unrecognized protocol option '%s'", d.Val()) return nil, d.Errf("unrecognized protocol option '%s'", d.Val())
@@ -282,30 +172,26 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
// applyServerOptions sets the server options on the appropriate servers // applyServerOptions sets the server options on the appropriate servers
func applyServerOptions( func applyServerOptions(
servers map[string]*caddyhttp.Server, servers map[string]*caddyhttp.Server,
options map[string]any, options map[string]interface{},
warnings *[]caddyconfig.Warning, warnings *[]caddyconfig.Warning,
) error { ) error {
// If experimental HTTP/3 is enabled, enable it on each server.
// We already know there won't be a conflict with serverOptions because
// we validated earlier that "experimental_http3" cannot be set at the same
// time as "servers"
if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
*warnings = append(*warnings, caddyconfig.Warning{Message: "the 'experimental_http3' global option is deprecated, please use the 'servers > protocol > experimental_http3' option instead"})
for _, srv := range servers {
srv.ExperimentalHTTP3 = true
}
}
serverOpts, ok := options["servers"].([]serverOptions) serverOpts, ok := options["servers"].([]serverOptions)
if !ok { if !ok {
return nil return nil
} }
// check for duplicate names, which would clobber the config for _, server := range servers {
existingNames := map[string]bool{}
for _, opts := range serverOpts {
if opts.Name == "" {
continue
}
if existingNames[opts.Name] {
return fmt.Errorf("cannot use duplicate server name '%s'", opts.Name)
}
existingNames[opts.Name] = true
}
// collect the server name overrides
nameReplacements := map[string]string{}
for key, server := range servers {
// find the options that apply to this server // find the options that apply to this server
opts := func() *serverOptions { opts := func() *serverOptions {
for _, entry := range serverOpts { for _, entry := range serverOpts {
@@ -332,30 +218,10 @@ func applyServerOptions(
server.ReadHeaderTimeout = opts.ReadHeaderTimeout server.ReadHeaderTimeout = opts.ReadHeaderTimeout
server.WriteTimeout = opts.WriteTimeout server.WriteTimeout = opts.WriteTimeout
server.IdleTimeout = opts.IdleTimeout server.IdleTimeout = opts.IdleTimeout
server.KeepAliveInterval = opts.KeepAliveInterval
server.MaxHeaderBytes = opts.MaxHeaderBytes server.MaxHeaderBytes = opts.MaxHeaderBytes
server.EnableFullDuplex = opts.EnableFullDuplex server.AllowH2C = opts.AllowH2C
server.Protocols = opts.Protocols server.ExperimentalHTTP3 = opts.ExperimentalHTTP3
server.StrictSNIHost = opts.StrictSNIHost server.StrictSNIHost = opts.StrictSNIHost
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
server.ClientIPHeaders = opts.ClientIPHeaders
server.Metrics = opts.Metrics
if opts.ShouldLogCredentials {
if server.Logs == nil {
server.Logs = &caddyhttp.ServerLogConfig{}
}
server.Logs.ShouldLogCredentials = opts.ShouldLogCredentials
}
if opts.Name != "" {
nameReplacements[key] = opts.Name
}
}
// rename the servers if marked to do so
for old, new := range nameReplacements {
servers[new] = servers[old]
delete(servers, old)
} }
return nil return nil
@@ -1,9 +0,0 @@
(t2) {
respond 200 {
body {args[:]}
}
}
:8082 {
import t2 false
}
@@ -1,9 +0,0 @@
(t1) {
respond 200 {
body {args[:]}
}
}
:8081 {
import t1 false
}
@@ -1,15 +0,0 @@
(t1) {
respond 200 {
body {args[:]}
}
}
:8081 {
import t1 false
}
import import_variadic.txt
:8083 {
import t2 true
}
+70 -113
View File
@@ -33,7 +33,7 @@ import (
func (st ServerType) buildTLSApp( func (st ServerType) buildTLSApp(
pairings []sbAddrAssociation, pairings []sbAddrAssociation,
options map[string]any, options map[string]interface{},
warnings []caddyconfig.Warning, warnings []caddyconfig.Warning,
) (*caddytls.TLS, []caddyconfig.Warning, error) { ) (*caddytls.TLS, []caddyconfig.Warning, error) {
@@ -44,32 +44,37 @@ func (st ServerType) buildTLSApp(
if hp, ok := options["http_port"].(int); ok { if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp) httpPort = strconv.Itoa(hp)
} }
autoHTTPS := "on" httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
if ah, ok := options["auto_https"].(string); ok { if hsp, ok := options["https_port"].(int); ok {
autoHTTPS = ah httpsPort = strconv.Itoa(hsp)
} }
// find all hosts that share a server block with a hostless // count how many server blocks have a TLS-enabled key with
// key, so that they don't get forgotten/omitted by auto-HTTPS // no host, and find all hosts that share a server block with
// (since they won't appear in route matchers) // a hostless key, so that they don't get forgotten/omitted
// by auto-HTTPS (since they won't appear in route matchers)
var serverBlocksWithTLSHostlessKey int
httpsHostsSharedWithHostlessKey := make(map[string]struct{}) httpsHostsSharedWithHostlessKey := make(map[string]struct{})
if autoHTTPS != "off" { for _, pair := range pairings {
for _, pair := range pairings { for _, sb := range pair.serverBlocks {
for _, sb := range pair.serverBlocks { for _, addr := range sb.keys {
for _, addr := range sb.keys { if addr.Host == "" {
if addr.Host == "" { // this address has no hostname, but if it's explicitly set
// this server block has a hostless key, now // to HTTPS, then we need to count it as being TLS-enabled
// go through and add all the hosts to the set if addr.Scheme == "https" || addr.Port == httpsPort {
for _, otherAddr := range sb.keys { serverBlocksWithTLSHostlessKey++
if otherAddr.Original == addr.Original {
continue
}
if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
}
}
break
} }
// this server block has a hostless key, now
// go through and add all the hosts to the set
for _, otherAddr := range sb.keys {
if otherAddr.Original == addr.Original {
continue
}
if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
}
}
break
} }
} }
} }
@@ -96,12 +101,6 @@ func (st ServerType) buildTLSApp(
} }
for _, sblock := range p.serverBlocks { for _, sblock := range p.serverBlocks {
// check the scheme of all the site addresses,
// skip building AP if they all had http://
if sblock.isAllHTTP() {
continue
}
// get values that populate an automation policy for this block // get values that populate an automation policy for this block
ap, err := newBaseAutomationPolicy(options, warnings, true) ap, err := newBaseAutomationPolicy(options, warnings, true)
if err != nil { if err != nil {
@@ -129,31 +128,11 @@ func (st ServerType) buildTLSApp(
issuers = append(issuers, issuerVal.Value.(certmagic.Issuer)) issuers = append(issuers, issuerVal.Value.(certmagic.Issuer))
} }
if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) { if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) {
// this more correctly implements an error check that was removed
// below; try it with this config:
//
// :443 {
// bind 127.0.0.1
// }
//
// :443 {
// bind ::1
// tls {
// issuer acme
// }
// }
return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers) return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers)
} }
ap.Issuers = issuers ap.Issuers = issuers
} }
// certificate managers
if certManagerVals, ok := sblock.pile["tls.cert_manager"]; ok {
for _, certManager := range certManagerVals {
certGetterName := certManager.Value.(caddy.Module).CaddyModule().ID.Name()
ap.ManagersRaw = append(ap.ManagersRaw, caddyconfig.JSONModuleObject(certManager.Value, "via", certGetterName, &warnings))
}
}
// custom bind host // custom bind host
for _, cfgVal := range sblock.pile["bind"] { for _, cfgVal := range sblock.pile["bind"] {
for _, iss := range ap.Issuers { for _, iss := range ap.Issuers {
@@ -184,30 +163,34 @@ func (st ServerType) buildTLSApp(
} }
} }
// we used to ensure this block is allowed to create an automation policy; // first make sure this block is allowed to create an automation policy;
// doing so was forbidden if it has a key with no host (i.e. ":443") // doing so is forbidden if it has a key with no host (i.e. ":443")
// and if there is a different server block that also has a key with no // and if there is a different server block that also has a key with no
// host -- since a key with no host matches any host, we need its // host -- since a key with no host matches any host, we need its
// associated automation policy to have an empty Subjects list, i.e. no // associated automation policy to have an empty Subjects list, i.e. no
// host filter, which is indistinguishable between the two server blocks // host filter, which is indistinguishable between the two server blocks
// because automation is not done in the context of a particular server... // because automation is not done in the context of a particular server...
// this is an example of a poor mapping from Caddyfile to JSON but that's // this is an example of a poor mapping from Caddyfile to JSON but that's
// the least-leaky abstraction I could figure out -- however, this check // the least-leaky abstraction I could figure out
// was preventing certain listeners, like those provided by plugins, from if len(sblockHosts) == 0 {
// being used as desired (see the Tailscale listener plugin), so I removed if serverBlocksWithTLSHostlessKey > 1 {
// the check: and I think since I originally wrote the check I added a new // this server block and at least one other has a key with no host,
// check above which *properly* detects this ambiguity without breaking the // making the two indistinguishable; it is misleading to define such
// listener plugin; see the check above with a commented example config // a policy within one server block since it actually will apply to
if len(sblockHosts) == 0 && catchAllAP == nil { // others as well
// this server block has a key with no hosts, but there is not yet return nil, warnings, fmt.Errorf("cannot make a TLS automation policy from a server block that has a host-less address when there are other TLS-enabled server block addresses lacking a host")
// a catch-all automation policy (probably because no global options }
// were set), so this one becomes it if catchAllAP == nil {
catchAllAP = ap // this server block has a key with no hosts, but there is not yet
// a catch-all automation policy (probably because no global options
// were set), so this one becomes it
catchAllAP = ap
}
} }
// associate our new automation policy with this server block's hosts // associate our new automation policy with this server block's hosts
ap.SubjectsRaw = sblock.hostsFromKeysNotHTTP(httpPort) ap.Subjects = sblock.hostsFromKeysNotHTTP(httpPort)
sort.Strings(ap.SubjectsRaw) // solely for deterministic test results sort.Strings(ap.Subjects) // solely for deterministic test results
// if a combination of public and internal names were given // if a combination of public and internal names were given
// for this same server block and no issuer was specified, we // for this same server block and no issuer was specified, we
@@ -217,11 +200,7 @@ func (st ServerType) buildTLSApp(
var ap2 *caddytls.AutomationPolicy var ap2 *caddytls.AutomationPolicy
if len(ap.Issuers) == 0 { if len(ap.Issuers) == 0 {
var internal, external []string var internal, external []string
for _, s := range ap.SubjectsRaw { for _, s := range ap.Subjects {
// do not create Issuers for Tailscale domains; they will be given a Manager instead
if strings.HasSuffix(strings.ToLower(s), ".ts.net") {
continue
}
if !certmagic.SubjectQualifiesForCert(s) { if !certmagic.SubjectQualifiesForCert(s) {
return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s) return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s)
} }
@@ -239,10 +218,10 @@ func (st ServerType) buildTLSApp(
} }
} }
if len(external) > 0 && len(internal) > 0 { if len(external) > 0 && len(internal) > 0 {
ap.SubjectsRaw = external ap.Subjects = external
apCopy := *ap apCopy := *ap
ap2 = &apCopy ap2 = &apCopy
ap2.SubjectsRaw = internal ap2.Subjects = internal
ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)} ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
} }
} }
@@ -307,27 +286,6 @@ func (st ServerType) buildTLSApp(
tlsApp.Automation.StorageCleanInterval = storageCleanInterval tlsApp.Automation.StorageCleanInterval = storageCleanInterval
} }
// set the expired certificates renew interval if configured
if renewCheckInterval, ok := options["renew_interval"].(caddy.Duration); ok {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.RenewCheckInterval = renewCheckInterval
}
// set the OCSP check interval if configured
if ocspCheckInterval, ok := options["ocsp_interval"].(caddy.Duration); ok {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.OCSPCheckInterval = ocspCheckInterval
}
// set whether OCSP stapling should be disabled for manually-managed certificates
if ocspConfig, ok := options["ocsp_stapling"].(certmagic.OCSPConfig); ok {
tlsApp.DisableOCSPStapling = ocspConfig.DisableStapling
}
// if any hostnames appear on the same server block as a key with // if any hostnames appear on the same server block as a key with
// no host, they will not be used with route matchers because the // no host, they will not be used with route matchers because the
// hostless key matches all hosts, therefore, it wouldn't be // hostless key matches all hosts, therefore, it wouldn't be
@@ -339,18 +297,16 @@ func (st ServerType) buildTLSApp(
internalAP := &caddytls.AutomationPolicy{ internalAP := &caddytls.AutomationPolicy{
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)}, IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
} }
if autoHTTPS != "off" { for h := range httpsHostsSharedWithHostlessKey {
for h := range httpsHostsSharedWithHostlessKey { al = append(al, h)
al = append(al, h) if !certmagic.SubjectQualifiesForPublicCert(h) {
if !certmagic.SubjectQualifiesForPublicCert(h) { internalAP.Subjects = append(internalAP.Subjects, h)
internalAP.SubjectsRaw = append(internalAP.SubjectsRaw, h)
}
} }
} }
if len(al) > 0 { if len(al) > 0 {
tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings) tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
} }
if len(internalAP.SubjectsRaw) > 0 { if len(internalAP.Subjects) > 0 {
if tlsApp.Automation == nil { if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig) tlsApp.Automation = new(caddytls.AutomationConfig)
} }
@@ -368,6 +324,7 @@ func (st ServerType) buildTLSApp(
globalPreferredChains := options["preferred_chains"] globalPreferredChains := options["preferred_chains"]
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil
if hasGlobalACMEDefaults { if hasGlobalACMEDefaults {
// for _, ap := range tlsApp.Automation.Policies {
for i := 0; i < len(tlsApp.Automation.Policies); i++ { for i := 0; i < len(tlsApp.Automation.Policies); i++ {
ap := tlsApp.Automation.Policies[i] ap := tlsApp.Automation.Policies[i]
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) { if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
@@ -416,7 +373,7 @@ func (st ServerType) buildTLSApp(
// for convenience) // for convenience)
automationHostSet := make(map[string]struct{}) automationHostSet := make(map[string]struct{})
for _, ap := range tlsApp.Automation.Policies { for _, ap := range tlsApp.Automation.Policies {
for _, s := range ap.SubjectsRaw { for _, s := range ap.Subjects {
if _, ok := automationHostSet[s]; ok { if _, ok := automationHostSet[s]; ok {
return nil, warnings, fmt.Errorf("hostname appears in more than one automation policy, making certificate management ambiguous: %s", s) return nil, warnings, fmt.Errorf("hostname appears in more than one automation policy, making certificate management ambiguous: %s", s)
} }
@@ -438,7 +395,7 @@ func (st ServerType) buildTLSApp(
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer } type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) error { func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interface{}) error {
acmeWrapper, ok := issuer.(acmeCapable) acmeWrapper, ok := issuer.(acmeCapable)
if !ok { if !ok {
return nil return nil
@@ -485,7 +442,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
// for any other automation policies. A nil policy (and no error) will be // for any other automation policies. A nil policy (and no error) will be
// returned if there are no default/global options. However, if always is // returned if there are no default/global options. However, if always is
// true, a non-nil value will always be returned (unless there is an error). // true, a non-nil value will always be returned (unless there is an error).
func newBaseAutomationPolicy(options map[string]any, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) { func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
issuers, hasIssuers := options["cert_issuer"] issuers, hasIssuers := options["cert_issuer"]
_, hasLocalCerts := options["local_certs"] _, hasLocalCerts := options["local_certs"]
keyType, hasKeyType := options["key_type"] keyType, hasKeyType := options["key_type"]
@@ -537,7 +494,7 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
if automationPolicyIsSubset(aps[j], aps[i]) { if automationPolicyIsSubset(aps[j], aps[i]) {
return false return false
} }
return len(aps[i].SubjectsRaw) > len(aps[j].SubjectsRaw) return len(aps[i].Subjects) > len(aps[j].Subjects)
}) })
emptyAPCount := 0 emptyAPCount := 0
@@ -545,7 +502,7 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
// compute the number of empty policies (disregarding subjects) - see #4128 // compute the number of empty policies (disregarding subjects) - see #4128
emptyAP := new(caddytls.AutomationPolicy) emptyAP := new(caddytls.AutomationPolicy)
for i := 0; i < len(aps); i++ { for i := 0; i < len(aps); i++ {
emptyAP.SubjectsRaw = aps[i].SubjectsRaw emptyAP.Subjects = aps[i].Subjects
if reflect.DeepEqual(aps[i], emptyAP) { if reflect.DeepEqual(aps[i], emptyAP) {
emptyAPCount++ emptyAPCount++
if !automationPolicyHasAllPublicNames(aps[i]) { if !automationPolicyHasAllPublicNames(aps[i]) {
@@ -587,7 +544,7 @@ outer:
aps[i].KeyType == aps[j].KeyType && aps[i].KeyType == aps[j].KeyType &&
aps[i].OnDemand == aps[j].OnDemand && aps[i].OnDemand == aps[j].OnDemand &&
aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio { aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio {
if len(aps[i].SubjectsRaw) > 0 && len(aps[j].SubjectsRaw) == 0 { if len(aps[i].Subjects) > 0 && len(aps[j].Subjects) == 0 {
// later policy (at j) has no subjects ("catch-all"), so we can // later policy (at j) has no subjects ("catch-all"), so we can
// remove the identical-but-more-specific policy that comes first // remove the identical-but-more-specific policy that comes first
// AS LONG AS it is not shadowed by another policy before it; e.g. // AS LONG AS it is not shadowed by another policy before it; e.g.
@@ -602,9 +559,9 @@ outer:
} }
} else { } else {
// avoid repeated subjects // avoid repeated subjects
for _, subj := range aps[j].SubjectsRaw { for _, subj := range aps[j].Subjects {
if !sliceContains(aps[i].SubjectsRaw, subj) { if !sliceContains(aps[i].Subjects, subj) {
aps[i].SubjectsRaw = append(aps[i].SubjectsRaw, subj) aps[i].Subjects = append(aps[i].Subjects, subj)
} }
} }
aps = append(aps[:j], aps[j+1:]...) aps = append(aps[:j], aps[j+1:]...)
@@ -620,15 +577,15 @@ outer:
// automationPolicyIsSubset returns true if a's subjects are a subset // automationPolicyIsSubset returns true if a's subjects are a subset
// of b's subjects. // of b's subjects.
func automationPolicyIsSubset(a, b *caddytls.AutomationPolicy) bool { func automationPolicyIsSubset(a, b *caddytls.AutomationPolicy) bool {
if len(b.SubjectsRaw) == 0 { if len(b.Subjects) == 0 {
return true return true
} }
if len(a.SubjectsRaw) == 0 { if len(a.Subjects) == 0 {
return false return false
} }
for _, aSubj := range a.SubjectsRaw { for _, aSubj := range a.Subjects {
var inSuperset bool var inSuperset bool
for _, bSubj := range b.SubjectsRaw { for _, bSubj := range b.Subjects {
if certmagic.MatchWildcard(aSubj, bSubj) { if certmagic.MatchWildcard(aSubj, bSubj) {
inSuperset = true inSuperset = true
break break
@@ -666,7 +623,7 @@ func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) b
} }
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool { func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
for _, subj := range ap.SubjectsRaw { for _, subj := range ap.Subjects {
if !subjectQualifiesForPublicCert(ap, subj) { if !subjectQualifiesForPublicCert(ap, subj) {
return false return false
} }
+2 -2
View File
@@ -47,8 +47,8 @@ func TestAutomationPolicyIsSubset(t *testing.T) {
expect: false, expect: false,
}, },
} { } {
apA := &caddytls.AutomationPolicy{SubjectsRaw: test.a} apA := &caddytls.AutomationPolicy{Subjects: test.a}
apB := &caddytls.AutomationPolicy{SubjectsRaw: test.b} apB := &caddytls.AutomationPolicy{Subjects: test.b}
if actual := automationPolicyIsSubset(apA, apB); actual != test.expect { if actual := automationPolicyIsSubset(apA, apB); actual != test.expect {
t.Errorf("Test %d: Expected %t but got %t (A: %v B: %v)", i, test.expect, actual, test.a, test.b) t.Errorf("Test %d: Expected %t but got %t (A: %v B: %v)", i, test.expect, actual, test.a, test.b)
} }
+12 -82
View File
@@ -1,26 +1,11 @@
// 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 caddyconfig package caddyconfig
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"io" "io/ioutil"
"net/http" "net/http"
"os"
"time" "time"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
@@ -30,14 +15,8 @@ func init() {
caddy.RegisterModule(HTTPLoader{}) caddy.RegisterModule(HTTPLoader{})
} }
// HTTPLoader can load Caddy configs over HTTP(S). // HTTPLoader can load Caddy configs over HTTP(S). It can adapt the config
// // based on the Content-Type header of the HTTP response.
// If the response is not a JSON config, a config adapter must be specified
// either in the loader config (`adapter`), or in the Content-Type HTTP header
// returned in the HTTP response from the server. The Content-Type header is
// read just like the admin API's `/load` endpoint. Uf you don't have control
// over the HTTP server (but can still trust its response), you can override
// the Content-Type header by setting the `adapter` property in this config.
type HTTPLoader struct { type HTTPLoader struct {
// The method for the request. Default: GET // The method for the request. Default: GET
Method string `json:"method,omitempty"` Method string `json:"method,omitempty"`
@@ -51,11 +30,6 @@ type HTTPLoader struct {
// Maximum time allowed for a complete connection and request. // Maximum time allowed for a complete connection and request.
Timeout caddy.Duration `json:"timeout,omitempty"` Timeout caddy.Duration `json:"timeout,omitempty"`
// The name of the config adapter to use, if any. Only needed
// if the HTTP response is not a JSON config and if the server's
// Content-Type header is missing or incorrect.
Adapter string `json:"adapter,omitempty"`
TLS *struct { TLS *struct {
// Present this instance's managed remote identity credentials to the server. // Present this instance's managed remote identity credentials to the server.
UseServerIdentity bool `json:"use_server_identity,omitempty"` UseServerIdentity bool `json:"use_server_identity,omitempty"`
@@ -82,30 +56,23 @@ func (HTTPLoader) CaddyModule() caddy.ModuleInfo {
// LoadConfig loads a Caddy config. // LoadConfig loads a Caddy config.
func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) { func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
repl := caddy.NewReplacer()
client, err := hl.makeClient(ctx) client, err := hl.makeClient(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
method := repl.ReplaceAll(hl.Method, "") method := hl.Method
if method == "" { if method == "" {
method = http.MethodGet method = http.MethodGet
} }
url := repl.ReplaceAll(hl.URL, "") req, err := http.NewRequest(method, hl.URL, nil)
req, err := http.NewRequest(method, url, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for key, vals := range hl.Headers { req.Header = hl.Headers
for _, val := range vals {
req.Header.Add(repl.ReplaceAll(key, ""), repl.ReplaceKnown(val, ""))
}
}
resp, err := doHttpCallWithRetries(ctx, client, req) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -114,59 +81,22 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
return nil, fmt.Errorf("server responded with HTTP %d", resp.StatusCode) return nil, fmt.Errorf("server responded with HTTP %d", resp.StatusCode)
} }
body, err := io.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// adapt the config based on either manually-configured adapter or server's response header result, warnings, err := adaptByContentType(resp.Header.Get("Content-Type"), body)
ct := resp.Header.Get("Content-Type")
if hl.Adapter != "" {
ct = "text/" + hl.Adapter
}
result, warnings, err := adaptByContentType(ct, body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, warn := range warnings { for _, warn := range warnings {
ctx.Logger().Warn(warn.String()) ctx.Logger(hl).Warn(warn.String())
} }
return result, nil return result, nil
} }
func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response, error) {
resp, err := client.Do(request)
if err != nil {
return nil, fmt.Errorf("problem calling http loader url: %v", err)
} else if resp.StatusCode < 200 || resp.StatusCode > 499 {
resp.Body.Close()
return nil, fmt.Errorf("bad response status code from http loader url: %v", resp.StatusCode)
}
return resp, nil
}
func doHttpCallWithRetries(ctx caddy.Context, client *http.Client, request *http.Request) (*http.Response, error) {
var resp *http.Response
var err error
const maxAttempts = 10
for i := 0; i < maxAttempts; i++ {
resp, err = attemptHttpCall(client, request)
if err != nil && i < maxAttempts-1 {
select {
case <-time.After(time.Millisecond * 500):
case <-ctx.Done():
return resp, ctx.Err()
}
} else {
break
}
}
return resp, err
}
func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) { func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
client := &http.Client{ client := &http.Client{
Timeout: time.Duration(hl.Timeout), Timeout: time.Duration(hl.Timeout),
@@ -177,7 +107,7 @@ func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
// client authentication // client authentication
if hl.TLS.UseServerIdentity { if hl.TLS.UseServerIdentity {
certs, err := ctx.IdentityCredentials(ctx.Logger()) certs, err := ctx.IdentityCredentials(ctx.Logger(hl))
if err != nil { if err != nil {
return nil, fmt.Errorf("getting server identity credentials: %v", err) return nil, fmt.Errorf("getting server identity credentials: %v", err)
} }
@@ -200,7 +130,7 @@ func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
if len(hl.TLS.RootCAPEMFiles) > 0 { if len(hl.TLS.RootCAPEMFiles) > 0 {
rootPool := x509.NewCertPool() rootPool := x509.NewCertPool()
for _, pemFile := range hl.TLS.RootCAPEMFiles { for _, pemFile := range hl.TLS.RootCAPEMFiles {
pemData, err := os.ReadFile(pemFile) pemData, err := ioutil.ReadFile(pemFile)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed reading ca cert: %v", err) return nil, fmt.Errorf("failed reading ca cert: %v", err)
} }
+5 -49
View File
@@ -58,10 +58,6 @@ func (al adminLoad) Routes() []caddy.AdminRoute {
Pattern: "/load", Pattern: "/load",
Handler: caddy.AdminHandlerFunc(al.handleLoad), Handler: caddy.AdminHandlerFunc(al.handleLoad),
}, },
{
Pattern: "/adapt",
Handler: caddy.AdminHandlerFunc(al.handleAdapt),
},
} }
} }
@@ -126,48 +122,7 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
return nil return nil
} }
// handleAdapt adapts the given Caddy config to JSON and responds with the result. // adaptByContentType adapts body to Caddy JSON using the adapter specified by contenType.
func (adminLoad) handleAdapt(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodPost {
return caddy.APIError{
HTTPStatus: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed"),
}
}
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
_, err := io.Copy(buf, r.Body)
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("reading request body: %v", err),
}
}
result, warnings, err := adaptByContentType(r.Header.Get("Content-Type"), buf.Bytes())
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: err,
}
}
out := struct {
Warnings []Warning `json:"warnings,omitempty"`
Result json.RawMessage `json:"result"`
}{
Warnings: warnings,
Result: result,
}
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(out)
}
// adaptByContentType adapts body to Caddy JSON using the adapter specified by contentType.
// If contentType is empty or ends with "/json", the input will be returned, as a no-op. // If contentType is empty or ends with "/json", the input will be returned, as a no-op.
func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, error) { func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, error) {
// assume JSON as the default // assume JSON as the default
@@ -189,11 +144,12 @@ func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, err
} }
// adapter name should be suffix of MIME type // adapter name should be suffix of MIME type
_, adapterName, slashFound := strings.Cut(ct, "/") slashIdx := strings.Index(ct, "/")
if !slashFound { if slashIdx < 0 {
return nil, nil, fmt.Errorf("malformed Content-Type") return nil, nil, fmt.Errorf("malformed Content-Type")
} }
adapterName := ct[slashIdx+1:]
cfgAdapter := GetAdapter(adapterName) cfgAdapter := GetAdapter(adapterName)
if cfgAdapter == nil { if cfgAdapter == nil {
return nil, nil, fmt.Errorf("unrecognized config adapter '%s'", adapterName) return nil, nil, fmt.Errorf("unrecognized config adapter '%s'", adapterName)
@@ -208,7 +164,7 @@ func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, err
} }
var bufPool = sync.Pool{ var bufPool = sync.Pool{
New: func() any { New: func() interface{} {
return new(bytes.Buffer) return new(bytes.Buffer)
}, },
} }
+18 -35
View File
@@ -7,7 +7,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
@@ -43,7 +43,7 @@ type Defaults struct {
// Default testing values // Default testing values
var Default = Defaults{ var Default = Defaults{
AdminPort: 2999, // different from what a real server also running on a developer's machine might be AdminPort: 2019,
Certifcates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"}, Certifcates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
TestRequestTimeout: 5 * time.Second, TestRequestTimeout: 5 * time.Second,
LoadRequestTimeout: 5 * time.Second, LoadRequestTimeout: 5 * time.Second,
@@ -100,7 +100,7 @@ func (tc *Tester) InitServer(rawConfig string, configType string) {
tc.t.Fail() tc.t.Fail()
} }
if err := tc.ensureConfigRunning(rawConfig, configType); err != nil { if err := tc.ensureConfigRunning(rawConfig, configType); err != nil {
tc.t.Logf("failed ensuring config is running: %s", err) tc.t.Logf("failed ensurng config is running: %s", err)
tc.t.Fail() tc.t.Fail()
} }
} }
@@ -114,7 +114,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
return nil return nil
} }
err := validateTestPrerequisites(tc.t) err := validateTestPrerequisites()
if err != nil { if err != nil {
tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err) tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err)
return nil return nil
@@ -129,7 +129,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
return return
} }
defer res.Body.Close() defer res.Body.Close()
body, _ := io.ReadAll(res.Body) body, _ := ioutil.ReadAll(res.Body)
var out bytes.Buffer var out bytes.Buffer
_ = json.Indent(&out, body, "", " ") _ = json.Indent(&out, body, "", " ")
@@ -162,7 +162,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
timeElapsed(start, "caddytest: config load time") timeElapsed(start, "caddytest: config load time")
defer res.Body.Close() defer res.Body.Close()
body, err := io.ReadAll(res.Body) body, err := ioutil.ReadAll(res.Body)
if err != nil { if err != nil {
tc.t.Errorf("unable to read response. %s", err) tc.t.Errorf("unable to read response. %s", err)
return err return err
@@ -186,7 +186,7 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil) expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil)
} }
var expected any var expected interface{}
err := json.Unmarshal(expectedBytes, &expected) err := json.Unmarshal(expectedBytes, &expected)
if err != nil { if err != nil {
return err return err
@@ -196,17 +196,17 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
Timeout: Default.LoadRequestTimeout, Timeout: Default.LoadRequestTimeout,
} }
fetchConfig := func(client *http.Client) any { fetchConfig := func(client *http.Client) interface{} {
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil { if err != nil {
return nil return nil
} }
defer resp.Body.Close() defer resp.Body.Close()
actualBytes, err := io.ReadAll(resp.Body) actualBytes, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil return nil
} }
var actual any var actual interface{}
err = json.Unmarshal(actualBytes, &actual) err = json.Unmarshal(actualBytes, &actual)
if err != nil { if err != nil {
return nil return nil
@@ -214,24 +214,19 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
return actual return actual
} }
for retries := 10; retries > 0; retries-- { for retries := 4; retries > 0; retries-- {
if reflect.DeepEqual(expected, fetchConfig(client)) { if reflect.DeepEqual(expected, fetchConfig(client)) {
return nil return nil
} }
time.Sleep(1 * time.Second) time.Sleep(10 * time.Millisecond)
} }
tc.t.Errorf("POSTed configuration isn't active") tc.t.Errorf("POSTed configuration isn't active")
return errors.New("EnsureConfigRunning: POSTed configuration isn't active") return errors.New("EnsureConfigRunning: POSTed configuration isn't active")
} }
const initConfig = `{
admin localhost:2999
}
`
// validateTestPrerequisites ensures the certificates are available in the // validateTestPrerequisites ensures the certificates are available in the
// designated path and Caddy sub-process is running. // designated path and Caddy sub-process is running.
func validateTestPrerequisites(t *testing.T) error { func validateTestPrerequisites() error {
// check certificates are found // check certificates are found
for _, certName := range Default.Certifcates { for _, certName := range Default.Certifcates {
@@ -241,27 +236,15 @@ func validateTestPrerequisites(t *testing.T) error {
} }
if isCaddyAdminRunning() != nil { if isCaddyAdminRunning() != nil {
// setup the init config file, and set the cleanup afterwards
f, err := os.CreateTemp("", "")
if err != nil {
return err
}
t.Cleanup(func() {
os.Remove(f.Name())
})
if _, err := f.WriteString(initConfig); err != nil {
return err
}
// start inprocess caddy server // start inprocess caddy server
os.Args = []string{"caddy", "run", "--config", f.Name(), "--adapter", "caddyfile"} os.Args = []string{"caddy", "run"}
go func() { go func() {
caddycmd.Main() caddycmd.Main()
}() }()
// wait for caddy to start serving the initial config // wait for caddy to start serving the initial config
for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- { for retries := 4; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
time.Sleep(1 * time.Second) time.Sleep(10 * time.Millisecond)
} }
} }
@@ -388,7 +371,7 @@ func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string,
return false return false
} }
options := make(map[string]any) options := make(map[string]interface{})
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options) result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
if err != nil { if err != nil {
@@ -488,7 +471,7 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
resp := tc.AssertResponseCode(req, expectedStatusCode) resp := tc.AssertResponseCode(req, expectedStatusCode)
defer resp.Body.Close() defer resp.Body.Close()
bytes, err := io.ReadAll(resp.Body) bytes, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
tc.t.Fatalf("unable to read the response body %s", err) tc.t.Fatalf("unable to read the response body %s", err)
} }
+1 -21
View File
@@ -11,8 +11,6 @@ func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
admin localhost:2999
skip_install_trust
http_port 9080 http_port 9080
https_port 9443 https_port 9443
} }
@@ -27,8 +25,6 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
skip_install_trust
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
} }
@@ -43,8 +39,6 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
skip_install_trust
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
} }
@@ -59,9 +53,6 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
"admin": {
"listen": "localhost:2999"
},
"apps": { "apps": {
"http": { "http": {
"http_port": 9080, "http_port": 9080,
@@ -83,14 +74,7 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
] ]
} }
} }
}, }
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
}
} }
} }
`, "json") `, "json")
@@ -101,8 +85,6 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
skip_install_trust
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
local_certs local_certs
@@ -126,8 +108,6 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSit
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
skip_install_trust
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
local_certs local_certs
@@ -1,108 +0,0 @@
{
pki {
ca internal {
name "Internal"
root_cn "Internal Root Cert"
intermediate_cn "Internal Intermediate Cert"
}
ca internal-long-lived {
name "Long-lived"
root_cn "Internal Root Cert 2"
intermediate_cn "Internal Intermediate Cert 2"
}
}
}
acme-internal.example.com {
acme_server {
ca internal
}
}
acme-long-lived.example.com {
acme_server {
ca internal-long-lived
lifetime 7d
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme-long-lived.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "internal-long-lived",
"handler": "acme_server",
"lifetime": 604800000000000
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"acme-internal.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "internal",
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"internal": {
"name": "Internal",
"root_common_name": "Internal Root Cert",
"intermediate_common_name": "Internal Intermediate Cert"
},
"internal-long-lived": {
"name": "Long-lived",
"root_common_name": "Internal Root Cert 2",
"intermediate_common_name": "Internal Intermediate Cert 2"
}
}
}
}
}
@@ -1,29 +0,0 @@
example.com {
bind tcp6/[::]
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
"tcp6/[::]:443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -1,114 +0,0 @@
example.com
@a expression {http.error.status_code} == 400
abort @a
@b expression {http.error.status_code} == "401"
abort @b
@c expression {http.error.status_code} == `402`
abort @c
@d expression "{http.error.status_code} == 403"
abort @d
@e expression `{http.error.status_code} == 404`
abort @e
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} == 400"
}
]
},
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} == \"401\""
}
]
},
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} == `402`"
}
]
},
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} == 403"
}
]
},
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} == 404"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -1,32 +0,0 @@
:80
file_server {
pass_thru
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
],
"pass_thru": true
}
]
}
]
}
}
}
}
}
@@ -1,111 +0,0 @@
app.example.com {
forward_auth authelia:9091 {
uri /api/verify?rd=https://authelia.example.com
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
}
reverse_proxy backend:8080
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"app.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handle_response": [
{
"match": {
"status_code": [
2
]
},
"routes": [
{
"handle": [
{
"handler": "headers",
"request": {
"set": {
"Remote-Email": [
"{http.reverse_proxy.header.Remote-Email}"
],
"Remote-Groups": [
"{http.reverse_proxy.header.Remote-Groups}"
],
"Remote-Name": [
"{http.reverse_proxy.header.Remote-Name}"
],
"Remote-User": [
"{http.reverse_proxy.header.Remote-User}"
]
}
}
}
]
}
]
}
],
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"X-Forwarded-Method": [
"{http.request.method}"
],
"X-Forwarded-Uri": [
"{http.request.uri}"
]
}
}
},
"rewrite": {
"method": "GET",
"uri": "/api/verify?rd=https://authelia.example.com"
},
"upstreams": [
{
"dial": "authelia:9091"
}
]
},
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "backend:8080"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -1,90 +0,0 @@
:8881
forward_auth localhost:9000 {
uri /auth
copy_headers A>1 B C>3 {
D
E>5
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"routes": [
{
"handle": [
{
"handle_response": [
{
"match": {
"status_code": [
2
]
},
"routes": [
{
"handle": [
{
"handler": "headers",
"request": {
"set": {
"1": [
"{http.reverse_proxy.header.A}"
],
"3": [
"{http.reverse_proxy.header.C}"
],
"5": [
"{http.reverse_proxy.header.E}"
],
"B": [
"{http.reverse_proxy.header.B}"
],
"D": [
"{http.reverse_proxy.header.D}"
]
}
}
}
]
}
]
}
],
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"X-Forwarded-Method": [
"{http.request.method}"
],
"X-Forwarded-Uri": [
"{http.request.uri}"
]
}
}
},
"rewrite": {
"method": "GET",
"uri": "/auth"
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
]
}
]
}
}
}
}
}
@@ -3,7 +3,6 @@
http_port 8080 http_port 8080
https_port 8443 https_port 8443
grace_period 5s grace_period 5s
shutdown_delay 10s
default_sni localhost default_sni localhost
order root first order root first
storage file_system { storage file_system {
@@ -11,7 +10,6 @@
} }
acme_ca https://example.com acme_ca https://example.com
acme_ca_root /path/to/ca.crt acme_ca_root /path/to/ca.crt
ocsp_stapling off
email test@example.com email test@example.com
admin off admin off
@@ -46,7 +44,6 @@
"http_port": 8080, "http_port": 8080,
"https_port": 8443, "https_port": 8443,
"grace_period": 5000000000, "grace_period": 5000000000,
"shutdown_delay": 10000000000,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
@@ -64,8 +61,7 @@
"module": "internal" "module": "internal"
} }
], ],
"key_type": "ed25519", "key_type": "ed25519"
"disable_ocsp_stapling": true
} }
], ],
"on_demand": { "on_demand": {
@@ -75,8 +71,7 @@
}, },
"ask": "https://example.com" "ask": "https://example.com"
} }
}, }
"disable_ocsp_stapling": true
} }
} }
} }
@@ -21,8 +21,6 @@
burst 20 burst 20
} }
storage_clean_interval 7d storage_clean_interval 7d
renew_interval 1d
ocsp_interval 2d
key_type ed25519 key_type ed25519
} }
@@ -84,8 +82,6 @@
}, },
"ask": "https://example.com" "ask": "https://example.com"
}, },
"ocsp_interval": 172800000000000,
"renew_interval": 86400000000000,
"storage_clean_interval": 604800000000000 "storage_clean_interval": 604800000000000
} }
} }
@@ -1,36 +0,0 @@
{
http_port 8080
persist_config off
admin {
origins localhost:2019 [::1]:2019 127.0.0.1:2019 192.168.10.128
}
}
:80
----------
{
"admin": {
"listen": "localhost:2019",
"origins": [
"localhost:2019",
"[::1]:2019",
"127.0.0.1:2019",
"192.168.10.128"
],
"config": {
"persist": false
}
},
"apps": {
"http": {
"http_port": 8080,
"servers": {
"srv0": {
"listen": [
":80"
]
}
}
}
}
}
@@ -1,45 +0,0 @@
{
debug
}
:8881 {
log {
format console
}
}
----------
{
"logging": {
"logs": {
"default": {
"level": "DEBUG",
"exclude": [
"http.log.access.log0"
]
},
"log0": {
"encoder": {
"format": "console"
},
"level": "DEBUG",
"include": [
"http.log.access.log0"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"logs": {
"default_logger_name": "log0"
}
}
}
}
}
}
@@ -1,54 +0,0 @@
{
default_bind tcp4/0.0.0.0 tcp6/[::]
}
example.com {
}
example.org:12345 {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
"tcp4/0.0.0.0:12345",
"tcp6/[::]:12345"
],
"routes": [
{
"match": [
{
"host": [
"example.org"
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
"tcp4/0.0.0.0:443",
"tcp6/[::]:443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -3,7 +3,8 @@
format filter { format filter {
wrap console wrap console
fields { fields {
request>remote_ip ip_mask { common_log delete
request>remote_addr ip_mask {
ipv4 24 ipv4 24
ipv6 32 ipv6 32
} }
@@ -18,7 +19,10 @@
"custom-logger": { "custom-logger": {
"encoder": { "encoder": {
"fields": { "fields": {
"request\u003eremote_ip": { "common_log": {
"filter": "delete"
},
"request\u003eremote_addr": {
"filter": "ip_mask", "filter": "ip_mask",
"ipv4_cidr": 24, "ipv4_cidr": 24,
"ipv6_cidr": 32 "ipv6_cidr": 32
@@ -1,25 +0,0 @@
{
persist_config off
}
:8881 {
}
----------
{
"admin": {
"config": {
"persist": false
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
]
}
}
}
}
}
@@ -1,44 +1,10 @@
{ {
skip_install_trust skip_install_trust
pki {
ca {
name "Local"
root_cn "Custom Local Root Name"
intermediate_cn "Custom Local Intermediate Name"
root {
cert /path/to/cert.pem
key /path/to/key.pem
format pem_file
}
intermediate {
cert /path/to/cert.pem
key /path/to/key.pem
format pem_file
}
}
ca foo {
name "Foo"
root_cn "Custom Foo Root Name"
intermediate_cn "Custom Foo Intermediate Name"
}
}
} }
a.example.com { a.example.com {
tls internal tls internal
} }
acme.example.com {
acme_server {
ca foo
}
}
acme-bar.example.com {
acme_server {
ca bar
}
}
---------- ----------
{ {
"apps": { "apps": {
@@ -49,56 +15,6 @@ acme-bar.example.com {
":443" ":443"
], ],
"routes": [ "routes": [
{
"match": [
{
"host": [
"acme-bar.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "bar",
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"acme.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "foo",
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
},
{ {
"match": [ "match": [
{ {
@@ -115,42 +31,14 @@ acme-bar.example.com {
}, },
"pki": { "pki": {
"certificate_authorities": { "certificate_authorities": {
"bar": {
"install_trust": false
},
"foo": {
"name": "Foo",
"root_common_name": "Custom Foo Root Name",
"intermediate_common_name": "Custom Foo Intermediate Name",
"install_trust": false
},
"local": { "local": {
"name": "Local", "install_trust": false
"root_common_name": "Custom Local Root Name",
"intermediate_common_name": "Custom Local Intermediate Name",
"install_trust": false,
"root": {
"certificate": "/path/to/cert.pem",
"private_key": "/path/to/key.pem",
"format": "pem_file"
},
"intermediate": {
"certificate": "/path/to/cert.pem",
"private_key": "/path/to/key.pem",
"format": "pem_file"
}
} }
} }
}, },
"tls": { "tls": {
"automation": { "automation": {
"policies": [ "policies": [
{
"subjects": [
"acme-bar.example.com",
"acme.example.com"
]
},
{ {
"subjects": [ "subjects": [
"a.example.com" "a.example.com"
@@ -165,4 +53,4 @@ acme-bar.example.com {
} }
} }
} }
} }
@@ -3,7 +3,6 @@
timeouts { timeouts {
idle 90s idle 90s
} }
strict_sni_host insecure_off
} }
servers :80 { servers :80 {
timeouts { timeouts {
@@ -14,7 +13,6 @@
timeouts { timeouts {
idle 30s idle 30s
} }
strict_sni_host
} }
} }
@@ -48,8 +46,7 @@ http://bar.com {
], ],
"terminal": true "terminal": true
} }
], ]
"strict_sni_host": true
}, },
"srv1": { "srv1": {
"listen": [ "listen": [
@@ -73,8 +70,7 @@ http://bar.com {
"listen": [ "listen": [
":8080" ":8080"
], ],
"idle_timeout": 90000000000, "idle_timeout": 90000000000
"strict_sni_host": false
} }
} }
} }
@@ -1,7 +1,6 @@
{ {
servers { servers {
listener_wrappers { listener_wrappers {
http_redirect
tls tls
} }
timeouts { timeouts {
@@ -11,13 +10,11 @@
idle 30s idle 30s
} }
max_header_size 100MB max_header_size 100MB
enable_full_duplex protocol {
log_credentials allow_h2c
protocols h1 h2 h2c h3 experimental_http3
strict_sni_host strict_sni_host
trusted_proxies static private_ranges }
client_ip_headers Custom-Real-Client-IP X-Forwarded-For
client_ip_headers A-Third-One
} }
} }
@@ -34,9 +31,6 @@ foo.com {
":443" ":443"
], ],
"listener_wrappers": [ "listener_wrappers": [
{
"wrapper": "http_redirect"
},
{ {
"wrapper": "tls" "wrapper": "tls"
} }
@@ -46,7 +40,6 @@ foo.com {
"write_timeout": 30000000000, "write_timeout": 30000000000,
"idle_timeout": 30000000000, "idle_timeout": 30000000000,
"max_header_bytes": 100000000, "max_header_bytes": 100000000,
"enable_full_duplex": true,
"routes": [ "routes": [
{ {
"match": [ "match": [
@@ -60,31 +53,8 @@ foo.com {
} }
], ],
"strict_sni_host": true, "strict_sni_host": true,
"trusted_proxies": { "experimental_http3": true,
"ranges": [ "allow_h2c": true
"192.168.0.0/16",
"172.16.0.0/12",
"10.0.0.0/8",
"127.0.0.1/8",
"fd00::/8",
"::1"
],
"source": "static"
},
"client_ip_headers": [
"Custom-Real-Client-IP",
"X-Forwarded-For",
"A-Third-One"
],
"logs": {
"should_log_credentials": true
},
"protocols": [
"h1",
"h2",
"h2c",
"h3"
]
} }
} }
} }
@@ -1,78 +0,0 @@
:8881 {
route {
handle /foo/* {
respond "Foo"
}
handle {
respond "Bar"
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Foo",
"handler": "static_response"
}
]
}
]
}
],
"match": [
{
"path": [
"/foo/*"
]
}
]
},
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Bar",
"handler": "static_response"
}
]
}
]
}
]
}
]
}
]
}
]
}
}
}
}
}
@@ -13,12 +13,6 @@
header @images { header @images {
Cache-Control "public, max-age=3600, stale-while-revalidate=86400" Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
} }
header {
+Link "Foo"
+Link "Bar"
}
header >Set Defer
header >Replace Deferred Replacement
} }
---------- ----------
{ {
@@ -127,42 +121,6 @@
] ]
} }
} }
},
{
"handler": "headers",
"response": {
"add": {
"Link": [
"Foo",
"Bar"
]
}
}
},
{
"handler": "headers",
"response": {
"deferred": true,
"set": {
"Set": [
"Defer"
]
}
}
},
{
"handler": "headers",
"response": {
"deferred": true,
"replace": {
"Replace": [
{
"replace": "Replacement",
"search_regexp": "Deferred"
}
]
}
}
} }
] ]
} }
@@ -1,50 +0,0 @@
example.com {
respond <<EOF
<html>
<head><title>Foo</title>
<body>Foo</body>
</html>
EOF 200
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "\u003chtml\u003e\n \u003chead\u003e\u003ctitle\u003eFoo\u003c/title\u003e\n \u003cbody\u003eFoo\u003c/body\u003e\n\u003c/html\u003e",
"handler": "static_response",
"status_code": 200
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -1,6 +1,6 @@
(logging) { (logging) {
log { log {
output file /var/log/caddy/{args[0]}.access.log output file /var/log/caddy/{args.0}.access.log
} }
} }
@@ -1,154 +0,0 @@
&(first) {
@first path /first
vars @first first 1
respond "first"
}
&(second) {
respond "second"
}
:8881 {
invoke first
route {
invoke second
}
}
:8882 {
handle {
invoke second
}
}
:8883 {
respond "no invoke"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"routes": [
{
"handle": [
{
"handler": "invoke",
"name": "first"
},
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "invoke",
"name": "second"
}
]
}
]
}
]
}
],
"named_routes": {
"first": {
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"first": 1,
"handler": "vars"
}
],
"match": [
{
"path": [
"/first"
]
}
]
},
{
"handle": [
{
"body": "first",
"handler": "static_response"
}
]
}
]
}
]
},
"second": {
"handle": [
{
"body": "second",
"handler": "static_response"
}
]
}
}
},
"srv1": {
"listen": [
":8882"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "invoke",
"name": "second"
}
]
}
]
}
]
}
],
"named_routes": {
"second": {
"handle": [
{
"body": "second",
"handler": "static_response"
}
]
}
}
},
"srv2": {
"listen": [
":8883"
],
"routes": [
{
"handle": [
{
"body": "no invoke",
"handler": "static_response"
}
]
}
]
}
}
}
}
}
@@ -1,7 +1,5 @@
http://localhost:2020 { http://localhost:2020 {
log log
skip_log /first-hidden*
skip_log /second-hidden*
respond 200 respond 200
} }
@@ -30,36 +28,6 @@ http://localhost:2020 {
{ {
"handler": "subroute", "handler": "subroute",
"routes": [ "routes": [
{
"handle": [
{
"handler": "vars",
"skip_log": true
}
],
"match": [
{
"path": [
"/second-hidden*"
]
}
]
},
{
"handle": [
{
"handler": "vars",
"skip_log": true
}
],
"match": [
{
"path": [
"/first-hidden*"
]
}
]
},
{ {
"handle": [ "handle": [
{ {
@@ -5,24 +5,12 @@ log {
format filter { format filter {
wrap console wrap console
fields { fields {
uri query {
replace foo REDACTED
delete bar
hash baz
}
request>headers>Authorization replace REDACTED request>headers>Authorization replace REDACTED
request>headers>Server delete request>headers>Server delete
request>headers>Cookie cookie { request>remote_addr ip_mask {
replace foo REDACTED
delete bar
hash baz
}
request>remote_ip ip_mask {
ipv4 24 ipv4 24
ipv6 32 ipv6 32
} }
request>headers>Regexp regexp secret REDACTED
request>headers>Hash hash
} }
} }
} }
@@ -45,57 +33,13 @@ log {
"filter": "replace", "filter": "replace",
"value": "REDACTED" "value": "REDACTED"
}, },
"request\u003eheaders\u003eCookie": {
"actions": [
{
"name": "foo",
"type": "replace",
"value": "REDACTED"
},
{
"name": "bar",
"type": "delete"
},
{
"name": "baz",
"type": "hash"
}
],
"filter": "cookie"
},
"request\u003eheaders\u003eHash": {
"filter": "hash"
},
"request\u003eheaders\u003eRegexp": {
"filter": "regexp",
"regexp": "secret",
"value": "REDACTED"
},
"request\u003eheaders\u003eServer": { "request\u003eheaders\u003eServer": {
"filter": "delete" "filter": "delete"
}, },
"request\u003eremote_ip": { "request\u003eremote_addr": {
"filter": "ip_mask", "filter": "ip_mask",
"ipv4_cidr": 24, "ipv4_cidr": 24,
"ipv6_cidr": 32 "ipv6_cidr": 32
},
"uri": {
"actions": [
{
"parameter": "foo",
"type": "replace",
"value": "REDACTED"
},
{
"parameter": "bar",
"type": "delete"
},
{
"parameter": "baz",
"type": "hash"
}
],
"filter": "query"
} }
}, },
"format": "filter", "format": "filter",
@@ -1,71 +0,0 @@
*.example.com {
log {
hostnames foo.example.com bar.example.com
output file /foo-bar.txt
}
log {
hostnames baz.example.com
output file /baz.txt
}
}
----------
{
"logging": {
"logs": {
"default": {
"exclude": [
"http.log.access.log0",
"http.log.access.log1"
]
},
"log0": {
"writer": {
"filename": "/foo-bar.txt",
"output": "file"
},
"include": [
"http.log.access.log0"
]
},
"log1": {
"writer": {
"filename": "/baz.txt",
"output": "file"
},
"include": [
"http.log.access.log1"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"*.example.com"
]
}
],
"terminal": true
}
],
"logs": {
"logger_names": {
"bar.example.com": "log0",
"baz.example.com": "log1",
"foo.example.com": "log0"
}
}
}
}
}
}
}
@@ -1,86 +0,0 @@
{
log access-console {
include http.log.access.foo
output file access-localhost.log
format console
}
log access-json {
include http.log.access.foo
output file access-localhost.json
format json
}
}
http://localhost:8881 {
log foo
}
----------
{
"logging": {
"logs": {
"access-console": {
"writer": {
"filename": "access-localhost.log",
"output": "file"
},
"encoder": {
"format": "console"
},
"include": [
"http.log.access.foo"
]
},
"access-json": {
"writer": {
"filename": "access-localhost.json",
"output": "file"
},
"encoder": {
"format": "json"
},
"include": [
"http.log.access.foo"
]
},
"default": {
"exclude": [
"http.log.access.foo"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
},
"logs": {
"logger_names": {
"localhost:8881": "foo"
}
}
}
}
}
}
}
@@ -1,91 +0,0 @@
{
debug
log access-console {
include http.log.access.foo
output file access-localhost.log
format console
}
log access-json {
include http.log.access.foo
output file access-localhost.json
format json
}
}
http://localhost:8881 {
log foo
}
----------
{
"logging": {
"logs": {
"access-console": {
"writer": {
"filename": "access-localhost.log",
"output": "file"
},
"encoder": {
"format": "console"
},
"level": "DEBUG",
"include": [
"http.log.access.foo"
]
},
"access-json": {
"writer": {
"filename": "access-localhost.json",
"output": "file"
},
"encoder": {
"format": "json"
},
"level": "DEBUG",
"include": [
"http.log.access.foo"
]
},
"default": {
"level": "DEBUG",
"exclude": [
"http.log.access.foo"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
},
"logs": {
"logger_names": {
"localhost:8881": "foo"
}
}
}
}
}
}
}
@@ -3,8 +3,6 @@
log { log {
output file /var/log/access.log { output file /var/log/access.log {
roll_size 1gb roll_size 1gb
roll_uncompressed
roll_local_time
roll_keep 5 roll_keep 5
roll_keep_for 90d roll_keep_for 90d
} }
@@ -22,10 +20,8 @@ log {
"writer": { "writer": {
"filename": "/var/log/access.log", "filename": "/var/log/access.log",
"output": "file", "output": "file",
"roll_gzip": false,
"roll_keep": 5, "roll_keep": 5,
"roll_keep_days": 90, "roll_keep_days": 90,
"roll_local_time": true,
"roll_size_mb": 954 "roll_size_mb": 954
}, },
"include": [ "include": [
@@ -62,9 +62,6 @@ example.com {
} }
], ],
"logs": { "logs": {
"logger_names": {
"one.example.com": ""
},
"skip_hosts": [ "skip_hosts": [
"three.example.com", "three.example.com",
"two.example.com", "two.example.com",
@@ -1,126 +0,0 @@
example.com
map {host} {my_placeholder} {magic_number} {
# Should output boolean "true" and an integer
example.com true 3
# Should output a string and null
foo.example.com "string value"
# Should output two strings (quoted int)
(.*)\.example.com "${1} subdomain" "5"
# Should output null and a string (quoted int)
~.*\.net$ - `7`
# Should output a float and the string "false"
~.*\.xyz$ 123.456 "false"
# Should output two strings, second being escaped quote
default "unknown domain" \"""
}
vars foo bar
vars {
abc true
def 1
ghi 2.3
jkl "mn op"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"defaults": [
"unknown domain",
"\""
],
"destinations": [
"{my_placeholder}",
"{magic_number}"
],
"handler": "map",
"mappings": [
{
"input": "example.com",
"outputs": [
true,
3
]
},
{
"input": "foo.example.com",
"outputs": [
"string value",
null
]
},
{
"input": "(.*)\\.example.com",
"outputs": [
"${1} subdomain",
"5"
]
},
{
"input_regexp": ".*\\.net$",
"outputs": [
null,
"7"
]
},
{
"input_regexp": ".*\\.xyz$",
"outputs": [
123.456,
"false"
]
}
],
"source": "{http.request.host}"
},
{
"abc": true,
"def": 1,
"ghi": 2.3,
"handler": "vars",
"jkl": "mn op"
},
{
"foo": "bar",
"handler": "vars"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -19,33 +19,24 @@
@matcher6 vars_regexp "{http.request.uri}" `\.([a-f0-9]{6})\.(css|js)$` @matcher6 vars_regexp "{http.request.uri}" `\.([a-f0-9]{6})\.(css|js)$`
respond @matcher6 "from vars_regexp matcher without name" respond @matcher6 "from vars_regexp matcher without name"
@matcher7 `path('/foo*') && method('GET')` @matcher7 {
respond @matcher7 "inline expression matcher shortcut"
@matcher8 {
header Foo bar header Foo bar
header Foo foobar header Foo foobar
header Bar foo header Bar foo
} }
respond @matcher8 "header matcher merging values of the same field" respond @matcher7 "header matcher merging values of the same field"
@matcher9 { @matcher8 {
query foo=bar foo=baz bar=foo query foo=bar foo=baz bar=foo
query bar=baz query bar=baz
} }
respond @matcher9 "query matcher merging pairs with the same keys" respond @matcher8 "query matcher merging pairs with the same keys"
@matcher10 { @matcher9 {
header !Foo header !Foo
header Bar foo header Bar foo
} }
respond @matcher10 "header matcher with null field matcher" respond @matcher9 "header matcher with null field matcher"
@matcher11 remote_ip private_ranges
respond @matcher11 "remote_ip matcher with private ranges"
@matcher12 client_ip private_ranges
respond @matcher12 "client_ip matcher with private ranges"
} }
---------- ----------
{ {
@@ -110,9 +101,7 @@
"match": [ "match": [
{ {
"vars": { "vars": {
"{http.request.uri}": [ "{http.request.uri}": "/vars-matcher"
"/vars-matcher"
]
} }
} }
], ],
@@ -158,19 +147,6 @@
} }
] ]
}, },
{
"match": [
{
"expression": "path('/foo*') \u0026\u0026 method('GET')"
}
],
"handle": [
{
"body": "inline expression matcher shortcut",
"handler": "static_response"
}
]
},
{ {
"match": [ "match": [
{ {
@@ -231,50 +207,6 @@
"handler": "static_response" "handler": "static_response"
} }
] ]
},
{
"match": [
{
"remote_ip": {
"ranges": [
"192.168.0.0/16",
"172.16.0.0/12",
"10.0.0.0/8",
"127.0.0.1/8",
"fd00::/8",
"::1"
]
}
}
],
"handle": [
{
"body": "remote_ip matcher with private ranges",
"handler": "static_response"
}
]
},
{
"match": [
{
"client_ip": {
"ranges": [
"192.168.0.0/16",
"172.16.0.0/12",
"10.0.0.0/8",
"127.0.0.1/8",
"fd00::/8",
"::1"
]
}
}
],
"handle": [
{
"body": "client_ip matcher with private ranges",
"handler": "static_response"
}
]
} }
] ]
} }
@@ -1,27 +0,0 @@
:8080 {
method FOO
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"handle": [
{
"handler": "rewrite",
"method": "FOO"
}
]
}
]
}
}
}
}
}
@@ -8,7 +8,7 @@ route {
} }
not path */ not path */
} }
redir @canonicalPath {http.request.orig_uri.path}/ 308 redir @canonicalPath {path}/ 308
# If the requested file does not exist, try index files # If the requested file does not exist, try index files
@indexFiles { @indexFiles {
@@ -50,7 +50,7 @@ route {
"handler": "static_response", "handler": "static_response",
"headers": { "headers": {
"Location": [ "Location": [
"{http.request.orig_uri.path}/" "{http.request.uri.path}/"
] ]
}, },
"status_code": 308 "status_code": 308
@@ -74,7 +74,6 @@ route {
] ]
}, },
{ {
"group": "group0",
"handle": [ "handle": [
{ {
"handler": "rewrite", "handler": "rewrite",
@@ -130,4 +129,4 @@ route {
} }
} }
} }
} }
@@ -1,145 +1,145 @@
:8881 { :8881 {
php_fastcgi app:9000 { php_fastcgi app:9000 {
env FOO bar env FOO bar
@error status 4xx @error status 4xx
handle_response @error { handle_response @error {
root * /errors root * /errors
rewrite * /{http.reverse_proxy.status_code}.html rewrite * /{http.reverse_proxy.status_code}.html
file_server file_server
} }
} }
} }
---------- ----------
{ {
"apps": { "apps": {
"http": { "http": {
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
":8881" ":8881"
], ],
"routes": [ "routes": [
{ {
"match": [ "match": [
{ {
"file": { "file": {
"try_files": [ "try_files": [
"{http.request.uri.path}/index.php" "{http.request.uri.path}/index.php"
] ]
}, },
"not": [ "not": [
{ {
"path": [ "path": [
"*/" "*/"
] ]
} }
] ]
} }
], ],
"handle": [ "handle": [
{ {
"handler": "static_response", "handler": "static_response",
"headers": { "headers": {
"Location": [ "Location": [
"{http.request.orig_uri.path}/" "{http.request.uri.path}/"
] ]
}, },
"status_code": 308 "status_code": 308
} }
] ]
}, },
{ {
"match": [ "match": [
{ {
"file": { "file": {
"try_files": [ "try_files": [
"{http.request.uri.path}", "{http.request.uri.path}",
"{http.request.uri.path}/index.php", "{http.request.uri.path}/index.php",
"index.php" "index.php"
], ],
"split_path": [ "split_path": [
".php" ".php"
] ]
} }
} }
], ],
"handle": [ "handle": [
{ {
"handler": "rewrite", "handler": "rewrite",
"uri": "{http.matchers.file.relative}" "uri": "{http.matchers.file.relative}"
} }
] ]
}, },
{ {
"match": [ "match": [
{ {
"path": [ "path": [
"*.php" "*.php"
] ]
} }
], ],
"handle": [ "handle": [
{ {
"handle_response": [ "handle_response": [
{ {
"match": { "match": {
"status_code": [ "status_code": [
4 4
] ]
}, },
"routes": [ "routes": [
{ {
"handle": [ "handle": [
{ {
"handler": "vars", "handler": "vars",
"root": "/errors" "root": "/errors"
} }
] ]
}, },
{ {
"group": "group0", "group": "group0",
"handle": [ "handle": [
{ {
"handler": "rewrite", "handler": "rewrite",
"uri": "/{http.reverse_proxy.status_code}.html" "uri": "/{http.reverse_proxy.status_code}.html"
} }
] ]
}, },
{ {
"handle": [ "handle": [
{ {
"handler": "file_server", "handler": "file_server",
"hide": [ "hide": [
"./Caddyfile" "./Caddyfile"
] ]
} }
] ]
} }
] ]
} }
], ],
"handler": "reverse_proxy", "handler": "reverse_proxy",
"transport": { "transport": {
"env": { "env": {
"FOO": "bar" "FOO": "bar"
}, },
"protocol": "fastcgi", "protocol": "fastcgi",
"split_path": [ "split_path": [
".php" ".php"
] ]
}, },
"upstreams": [ "upstreams": [
{ {
"dial": "app:9000" "dial": "app:9000"
} }
] ]
} }
] ]
} }
] ]
} }
} }
} }
} }
} }
@@ -1,12 +1,7 @@
:8884 :8884
# the use of a host matcher here should cause this @api host example.com
# site block to be wrapped in a subroute, even though php_fastcgi @api localhost:9000
# the site block does not have a hostname; this is
# to prevent auto-HTTPS from picking up on this host
# matcher because it is not a key on the site block
@test host example.com
php_fastcgi @test localhost:9000
---------- ----------
{ {
"apps": { "apps": {
@@ -18,6 +13,13 @@ php_fastcgi @test localhost:9000
], ],
"routes": [ "routes": [
{ {
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [ "handle": [
{ {
"handler": "subroute", "handler": "subroute",
@@ -25,99 +27,82 @@ php_fastcgi @test localhost:9000
{ {
"handle": [ "handle": [
{ {
"handler": "subroute", "handler": "static_response",
"routes": [ "headers": {
"Location": [
"{http.request.uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{ {
"handle": [ "path": [
{ "*/"
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
] ]
}, }
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{ {
"handle": [ "dial": "localhost:9000"
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
],
"match": [
{
"path": [
"*.php"
]
}
]
} }
] ]
} }
], ],
"match": [ "match": [
{ {
"host": [ "path": [
"example.com" "*.php"
] ]
} }
] ]
} }
] ]
} }
], ]
"terminal": true
} }
] ]
} }
@@ -43,7 +43,7 @@ php_fastcgi localhost:9000 {
"handler": "static_response", "handler": "static_response",
"headers": { "headers": {
"Location": [ "Location": [
"{http.request.orig_uri.path}/" "{http.request.uri.path}/"
] ]
}, },
"status_code": 308 "status_code": 308
@@ -1,124 +0,0 @@
:8884
php_fastcgi localhost:9000 {
# some php_fastcgi-specific subdirectives
split .php .php5
env VAR1 value1
env VAR2 value2
root /var/www
try_files {path} {path}/index.php =404
dial_timeout 3s
read_timeout 10s
write_timeout 20s
# passed through to reverse_proxy (directive order doesn't matter!)
lb_policy random
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
],
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
}
]
},
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"=404"
],
"split_path": [
".php",
".php5"
]
}
}
],
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
]
},
{
"match": [
{
"path": [
"*.php",
"*.php5"
]
}
],
"handle": [
{
"handler": "reverse_proxy",
"load_balancing": {
"selection_policy": {
"policy": "random"
}
},
"transport": {
"dial_timeout": 3000000000,
"env": {
"VAR1": "value1",
"VAR2": "value2"
},
"protocol": "fastcgi",
"read_timeout": 10000000000,
"root": "/var/www",
"split_path": [
".php",
".php5"
],
"write_timeout": 20000000000
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
]
}
]
}
}
}
}
}
@@ -1,120 +0,0 @@
:8884 {
reverse_proxy {
dynamic a foo 9000
}
reverse_proxy {
dynamic a {
name foo
port 9000
refresh 5m
resolvers 8.8.8.8 8.8.4.4
dial_timeout 2s
dial_fallback_delay 300ms
versions ipv6
}
}
}
:8885 {
reverse_proxy {
dynamic srv _api._tcp.example.com
}
reverse_proxy {
dynamic srv {
service api
proto tcp
name example.com
refresh 5m
resolvers 8.8.8.8 8.8.4.4
dial_timeout 1s
dial_fallback_delay -1s
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"dynamic_upstreams": {
"name": "foo",
"port": "9000",
"source": "a"
},
"handler": "reverse_proxy"
},
{
"dynamic_upstreams": {
"dial_fallback_delay": 300000000,
"dial_timeout": 2000000000,
"name": "foo",
"port": "9000",
"refresh": 300000000000,
"resolver": {
"addresses": [
"8.8.8.8",
"8.8.4.4"
]
},
"source": "a",
"versions": {
"ipv6": true
}
},
"handler": "reverse_proxy"
}
]
}
]
},
"srv1": {
"listen": [
":8885"
],
"routes": [
{
"handle": [
{
"dynamic_upstreams": {
"name": "_api._tcp.example.com",
"source": "srv"
},
"handler": "reverse_proxy"
},
{
"dynamic_upstreams": {
"dial_fallback_delay": -1000000000,
"dial_timeout": 1000000000,
"name": "example.com",
"proto": "tcp",
"refresh": 300000000000,
"resolver": {
"addresses": [
"8.8.8.8",
"8.8.4.4"
]
},
"service": "api",
"source": "srv"
},
"handler": "reverse_proxy"
}
]
}
]
}
}
}
}
}
@@ -1,8 +1,6 @@
:8884 :8884
reverse_proxy h2c://localhost:8080 reverse_proxy h2c://localhost:8080
reverse_proxy unix+h2c//run/app.sock
---------- ----------
{ {
"apps": { "apps": {
@@ -29,21 +27,6 @@ reverse_proxy unix+h2c//run/app.sock
"dial": "localhost:8080" "dial": "localhost:8080"
} }
] ]
},
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http",
"versions": [
"h2c",
"2"
]
},
"upstreams": [
{
"dial": "unix//run/app.sock"
}
]
} }
] ]
} }
@@ -1,14 +1,6 @@
:8884 :8884
reverse_proxy 127.0.0.1:65535 { reverse_proxy 127.0.0.1:65535 {
@500 status 500
replace_status @500 400
@all status 2xx 3xx 4xx 5xx
replace_status @all {http.error.status_code}
replace_status {http.error.status_code}
@accel header X-Accel-Redirect * @accel header X-Accel-Redirect *
handle_response @accel { handle_response @accel {
respond "Header X-Accel-Redirect!" respond "Header X-Accel-Redirect!"
@@ -47,19 +39,8 @@ reverse_proxy 127.0.0.1:65535 {
respond "Headers Foo, Bar AND statuses 401, 403 and 404!" respond "Headers Foo, Bar AND statuses 401, 403 and 404!"
} }
@200 status 200 @changeStatus status 500
handle_response @200 { handle_response @changeStatus 400
copy_response_headers {
include Foo Bar
}
respond "Copied headers from the response"
}
@201 status 201
handle_response @201 {
header Foo "Copying the response"
copy_response 404
}
} }
---------- ----------
{ {
@@ -75,25 +56,6 @@ reverse_proxy 127.0.0.1:65535 {
"handle": [ "handle": [
{ {
"handle_response": [ "handle_response": [
{
"match": {
"status_code": [
500
]
},
"status_code": 400
},
{
"match": {
"status_code": [
2,
3,
4,
5
]
},
"status_code": "{http.error.status_code}"
},
{ {
"match": { "match": {
"headers": { "headers": {
@@ -196,56 +158,10 @@ reverse_proxy 127.0.0.1:65535 {
{ {
"match": { "match": {
"status_code": [ "status_code": [
200 500
] ]
}, },
"routes": [ "status_code": 400
{
"handle": [
{
"handler": "copy_response_headers",
"include": [
"Foo",
"Bar"
]
},
{
"body": "Copied headers from the response",
"handler": "static_response"
}
]
}
]
},
{
"match": {
"status_code": [
201
]
},
"routes": [
{
"handle": [
{
"handler": "headers",
"response": {
"set": {
"Foo": [
"Copying the response"
]
}
}
},
{
"handler": "copy_response",
"status_code": 404
}
]
}
]
},
{
"status_code": "{http.error.status_code}"
}, },
{ {
"routes": [ "routes": [
@@ -7,7 +7,6 @@ reverse_proxy 127.0.0.1:65535 {
X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO
X-Empty-Value X-Empty-Value
} }
health_uri /health
} }
---------- ----------
{ {
@@ -39,8 +38,7 @@ reverse_proxy 127.0.0.1:65535 {
"VbG4NZwWnipo", "VbG4NZwWnipo",
"335Q9/MhqcNU3s2TO" "335Q9/MhqcNU3s2TO"
] ]
}, }
"uri": "/health"
} }
}, },
"upstreams": [ "upstreams": [

Some files were not shown because too many files have changed in this diff Show More