Compare commits

..

3 Commits

Author SHA1 Message Date
Matthew Holt 432f174623 reverseproxy: Add more debug logs
This makes debug logging very noisy when reverse proxying, but I guess
that's the point.

This has shown to be useful in troubleshooting infrastructure issues.
2023-09-01 14:59:34 -06:00
jjiang-stripe c6f34011fb caddyhttp: Add a getter for Server.name (#5531) 2023-05-11 12:50:00 -06:00
Lukas Vogel 71e27b844b cmd: Avoid panic when printing version without build info (#5210)
* version: don't panic if read build info doesn't work

If `debug.ReadBuildInfo()` doesn't return the build information we
should not try to access it. Especially if users only want to build with
the `CustomVersion` we should not assume access to
`debug.ReadBuildInfo()`.

The build environment where this isn't available for me is when building
with bazel.

* exit early
2023-02-06 11:26:07 -07:00
231 changed files with 4408 additions and 13464 deletions
+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
+3 -3
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,7 +42,7 @@ 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.
-7
View File
@@ -1,7 +0,0 @@
---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
+37 -26
View File
@@ -18,22 +18,17 @@ 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.18', '1.19' ]
- macos-latest
- windows-latest
go:
- '1.20'
- '1.21'
include: include:
# Set the minimum Go patch version for the given Go minor # Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }} # Usable via ${{ matrix.GO_SEMVER }}
- go: '1.20' - go: '1.18'
GO_SEMVER: '~1.20.6' GO_SEMVER: '~1.18.4'
- go: '1.21' - go: '1.19'
GO_SEMVER: '~1.21.0' GO_SEMVER: '~1.19.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
@@ -53,15 +48,15 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Go - name: Install Go
uses: actions/setup-go@v4 uses: actions/setup-go@v3
with: with:
go-version: ${{ matrix.GO_SEMVER }} go-version: ${{ matrix.GO_SEMVER }}
check-latest: true check-latest: true
- name: Checkout code
uses: actions/checkout@v3
# 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
# - name: Install test and coverage analysis tools # - name: Install test and coverage analysis tools
@@ -69,11 +64,10 @@ 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
shell: bash
run: | run: |
printf "Using go at: $(which go)\n" printf "Using go at: $(which go)\n"
printf "Go version: $(go version)\n" printf "Go version: $(go version)\n"
@@ -83,7 +77,24 @@ 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)"
- name: Cache the build cache
uses: actions/cache@v2
with:
# In order:
# * Module download cache
# * Build cache (Linux)
# * Build cache (Mac)
# * Build cache (Windows)
path: |
~/go/pkg/mod
~/.cache/go-build
~/Library/Caches/go-build
~\AppData\Local\go-build
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: |
@@ -98,7 +109,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 }}
@@ -112,7 +123,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
@@ -132,11 +143,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@v4 uses: actions/checkout@v3
- 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
@@ -161,10 +172,10 @@ jobs:
goreleaser-check: goreleaser-check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- uses: goreleaser/goreleaser-action@v5 - uses: goreleaser/goreleaser-action@v2
with: with:
version: latest version: latest
args: check args: check
+21 -20
View File
@@ -15,35 +15,20 @@ 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.19' ]
- 'linux'
- 'solaris'
- 'illumos'
- 'dragonfly'
- 'freebsd'
- 'openbsd'
- 'plan9'
- 'windows'
- 'darwin'
- 'netbsd'
go:
- '1.21'
include: include:
# Set the minimum Go patch version for the given Go minor # Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }} # Usable via ${{ matrix.GO_SEMVER }}
- go: '1.21' - go: '1.19'
GO_SEMVER: '~1.21.0' GO_SEMVER: '~1.19.0'
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
steps: steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Go - name: Install Go
uses: actions/setup-go@v4 uses: actions/setup-go@v3
with: with:
go-version: ${{ matrix.GO_SEMVER }} go-version: ${{ matrix.GO_SEMVER }}
check-latest: true check-latest: true
@@ -58,6 +43,22 @@ jobs:
printf "\n\nSystem environment:\n\n" printf "\n\nSystem environment:\n\n"
env env
- name: Cache the build cache
uses: actions/cache@v2
with:
# In order:
# * Module download cache
# * Build cache (Linux)
path: |
~/go/pkg/mod
~/.cache/go-build
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@v3
- name: Run Build - name: Run Build
env: env:
CGO_ENABLED: 0 CGO_ENABLED: 0
+5 -31
View File
@@ -10,52 +10,26 @@ 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: strategy:
matrix: matrix:
os: os: [ubuntu-latest, macos-latest, windows-latest]
- ubuntu-latest
- macos-latest
- windows-latest
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v3
with: with:
go-version: '~1.21.0' go-version: '~1.18.4'
check-latest: true 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@v3
with: with:
version: v1.54 version: v1.47
# 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. # Windows times out frequently after about 5m50s if we don't set a longer timeout.
args: --timeout 10m 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
govulncheck:
runs-on: ubuntu-latest
steps:
- name: govulncheck
uses: golang/govulncheck-action@v1
with:
go-version-input: '~1.21.0'
check-latest: true
+31 -21
View File
@@ -10,16 +10,14 @@ jobs:
name: Release name: Release
strategy: strategy:
matrix: matrix:
os: os: [ ubuntu-latest ]
- ubuntu-latest go: [ '1.19' ]
go:
- '1.21'
include: include:
# Set the minimum Go patch version for the given Go minor # Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }} # Usable via ${{ matrix.GO_SEMVER }}
- go: '1.21' - go: '1.19'
GO_SEMVER: '~1.21.0' GO_SEMVER: '~1.19.0'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233 # https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
@@ -31,19 +29,19 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Go - name: Install Go
uses: actions/setup-go@v4 uses: actions/setup-go@v3
with: with:
go-version: ${{ matrix.GO_SEMVER }} go-version: ${{ matrix.GO_SEMVER }}
check-latest: true check-latest: true
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
# Force fetch upstream tags -- because 65 minutes # Force fetch upstream tags -- because 65 minutes
# tl;dr: actions/checkout@v4 runs this line: # tl;dr: actions/checkout@v3 runs this line:
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/ # 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 +61,8 @@ 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)"
# 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 +74,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,6 +94,18 @@ 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: Cache the build cache
uses: actions/cache@v2
with:
# In order:
# * Module download cache
# * Build cache (Linux)
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go${{ matrix.go }}-release
- name: Install Cosign - name: Install Cosign
uses: sigstore/cosign-installer@main uses: sigstore/cosign-installer@main
- name: Cosign version - name: Cosign version
@@ -106,10 +116,10 @@ jobs:
run: syft version 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@v5 uses: goreleaser/goreleaser-action@v2
with: with:
version: latest version: latest
args: release --clean --timeout 60m args: release --rm-dist --timeout 60m
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.vars.outputs.version_tag }} TAG: ${{ steps.vars.outputs.version_tag }}
+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
@@ -11,8 +11,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
cmd/caddy/.env
# mac specific # mac specific
.DS_Store .DS_Store
+9 -23
View File
@@ -2,27 +2,15 @@ linters-settings:
errcheck: errcheck:
ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.* ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.*
ignoretests: true ignoretests: true
gci:
sections:
- standard # Standard section: captures all standard packages.
- default # Default section: contains all imports that could not be matched to another section type.
- prefix(github.com/caddyserver/caddy/v2/cmd) # ensure that this is always at the top and always has a line break.
- prefix(github.com/caddyserver/caddy) # Custom section: groups all imports with the specified Prefix.
# Skip generated files.
# Default: true
skip-generated: true
# Enable custom order of sections.
# If `true`, make the section order the same as the order of `sections`.
# Default: false
custom-order: true
linters: linters:
disable-all: true disable-all: true
enable: enable:
- bodyclose - bodyclose
- deadcode
- errcheck - errcheck
- gci - gofmt
- gofumpt - goimports
- gosec - gosec
- gosimple - gosimple
- govet - govet
@@ -30,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
@@ -90,23 +80,19 @@ output:
issues: issues:
exclude-rules: exclude-rules:
# we aren't calling unknown URL # we aren't calling unknown URL
- text: 'G107' # G107: Url provided to HTTP request as taint input - text: "G107" # G107: Url provided to HTTP request as taint input
linters: linters:
- gosec - gosec
# as a web server that's expected to handle any template, this is totally in the hands of the user. # as a web server that's expected to handle any template, this is totally in the hands of the user.
- text: 'G203' # G203: Use of unescaped data in HTML templates - text: "G203" # G203: Use of unescaped data in HTML templates
linters: linters:
- gosec - gosec
# we're shelling out to known commands, not relying on user-defined input. # we're shelling out to known commands, not relying on user-defined input.
- text: 'G204' # G204: Audit use of command execution - text: "G204" # G204: Audit use of command execution
linters: linters:
- gosec - gosec
# the choice of weakrand is deliberate, hence the named import "weakrand" # the choice of weakrand is deliberate, hence the named import "weakrand"
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go - path: modules/caddyhttp/reverseproxy/selectionpolicies.go
text: 'G404' # G404: Insecure random number source (rand) text: "G404" # G404: Insecure random number source (rand)
linters:
- gosec
- path: modules/caddyhttp/reverseproxy/streaming.go
text: 'G404' # G404: Insecure random number source (rand)
linters: linters:
- gosec - gosec
+7 -62
View File
@@ -4,9 +4,7 @@ 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 - rm -rf caddy-build caddy-dist
# 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' - /bin/sh -c 'cd ./caddy-build && go mod init caddy'
@@ -16,8 +14,6 @@ before:
# 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 - mkdir -p caddy-dist/man
- go mod download - go mod download
@@ -43,7 +39,6 @@ builds:
- arm64 - arm64
- s390x - s390x
- ppc64le - ppc64le
- riscv64
goarm: goarm:
- "5" - "5"
- "6" - "6"
@@ -55,20 +50,14 @@ builds:
goarch: ppc64le goarch: ppc64le
- goos: darwin - goos: darwin
goarch: s390x goarch: s390x
- goos: darwin
goarch: riscv64
- goos: windows - goos: windows
goarch: ppc64le goarch: ppc64le
- goos: windows - goos: windows
goarch: s390x goarch: s390x
- goos: windows
goarch: riscv64
- goos: freebsd - goos: freebsd
goarch: ppc64le goarch: ppc64le
- goos: freebsd - goos: freebsd
goarch: s390x goarch: s390x
- goos: freebsd
goarch: riscv64
- goos: freebsd - goos: freebsd
goarch: arm goarch: arm
goarm: "5" goarm: "5"
@@ -77,69 +66,24 @@ builds:
- -mod=readonly - -mod=readonly
ldflags: ldflags:
- -s -w - -s -w
signs: signs:
- cmd: cosign - cmd: cosign
signature: "${artifact}.sig" signature: "${artifact}.sig"
certificate: '{{ trimsuffix (trimsuffix .Env.artifact ".zip") ".tar.gz" }}.pem' certificate: '{{ trimsuffix (trimsuffix .Env.artifact ".zip") ".tar.gz" }}.pem'
args: ["sign-blob", "--yes", "--output-signature=${signature}", "--output-certificate", "${certificate}", "${artifact}"] args: ["sign-blob", "--output-signature=${signature}", "--output-certificate", "${certificate}", "${artifact}"]
artifacts: all artifacts: all
sboms: sboms:
- artifacts: binary - artifacts: binary
documents: documents:
- >- - '{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{if .Arm}}v{{ .Arm }}{{end}}.sbom'
{{ .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 cmd: syft
args: ["$artifact", "--file", "${document}", "--output", "cyclonedx-json"] 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 }}
# package 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
@@ -184,6 +128,7 @@ nfpms:
preremove: ./caddy-dist/scripts/preremove.sh preremove: ./caddy-dist/scripts/preremove.sh
postremove: ./caddy-dist/scripts/postremove.sh postremove: ./caddy-dist/scripts/postremove.sh
release: release:
github: github:
owner: caddyserver owner: caddyserver
+8 -20
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>
@@ -70,7 +58,7 @@
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues - **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
- **Production-ready** after serving trillions of requests and managing millions of TLS certificates - **Production-ready** after serving trillions of requests and managing millions of TLS certificates
- **Scales to hundreds of thousands of sites** as proven in production - **Scales to hundreds of thousands of sites** as proven in production
- **HTTP/1.1, HTTP/2, and HTTP/3** all supported by default - **HTTP/1.1, HTTP/2, and HTTP/3** supported all by default
- **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
@@ -87,10 +75,10 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i
Requirements: Requirements:
- [Go 1.20 or newer](https://golang.org/dl/) - [Go 1.18 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
@@ -197,4 +185,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.
+8 -66
View File
@@ -46,17 +46,6 @@ import (
"go.uber.org/zap/zapcore" "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 +57,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. Accepts placeholders. 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
@@ -318,32 +300,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{}{}
@@ -615,13 +572,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 +587,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
@@ -1041,9 +995,9 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
id := parts[2] id := parts[2]
// map the ID to the expanded path // map the ID to the expanded path
rawCfgMu.RLock() currentCtxMu.RLock()
expanded, ok := rawCfgIndex[id] expanded, ok := rawCfgIndex[id]
rawCfgMu.RUnlock() defer currentCtxMu.RUnlock()
if !ok { if !ok {
return APIError{ return APIError{
HTTPStatus: http.StatusNotFound, HTTPStatus: http.StatusNotFound,
@@ -1196,27 +1150,15 @@ traverseLoop:
} }
case http.MethodPut: case http.MethodPut:
if _, ok := v[part]; ok { if _, ok := v[part]; ok {
return APIError{ return fmt.Errorf("[%s] key already exists: %s", path, part)
HTTPStatus: http.StatusConflict,
Err: fmt.Errorf("[%s] key already exists: %s", path, part),
}
} }
v[part] = val v[part] = val
case http.MethodPatch: case http.MethodPatch:
if _, ok := v[part]; !ok { if _, ok := v[part]; !ok {
return APIError{ return fmt.Errorf("[%s] key does not exist: %s", path, part)
HTTPStatus: http.StatusNotFound,
Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
}
} }
v[part] = val v[part] = val
case http.MethodDelete: case http.MethodDelete:
if _, ok := v[part]; !ok {
return APIError{
HTTPStatus: http.StatusNotFound,
Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
}
}
delete(v, part) delete(v, part)
default: default:
return fmt.Errorf("unrecognized method %s", method) return fmt.Errorf("unrecognized method %s", method)
@@ -1358,7 +1300,7 @@ var (
// 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, 0o600) err := os.WriteFile(filename, pid, 0600)
if err != nil { if err != nil {
return err return err
} }
-6
View File
@@ -75,12 +75,6 @@ func TestUnsyncedConfigAccess(t *testing.T) {
path: "/bar/qq", path: "/bar/qq",
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`, expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
}, },
{
method: "DELETE",
path: "/bar/qq",
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
shouldErr: true,
},
{ {
method: "POST", method: "POST",
path: "/list", path: "/list",
+20 -38
View File
@@ -34,11 +34,10 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/caddyserver/caddy/v2/notify"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/google/uuid" "github.com/google/uuid"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/caddyserver/caddy/v2/notify"
) )
// Config is the top (or beginning) of the Caddy configuration structure. // Config is the top (or beginning) of the Caddy configuration structure.
@@ -157,8 +156,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() currentCtxMu.Lock()
defer rawCfgMu.Unlock() defer currentCtxMu.Unlock()
if ifMatchHeader != "" { if ifMatchHeader != "" {
// expect the first and last character to be quotes // expect the first and last character to be quotes
@@ -258,8 +257,8 @@ 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() currentCtxMu.RLock()
defer rawCfgMu.RUnlock() defer currentCtxMu.RUnlock()
return unsyncedConfigAccess(http.MethodGet, path, nil, out) return unsyncedConfigAccess(http.MethodGet, path, nil, out)
} }
@@ -306,7 +305,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 currentCtxMu 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 {
@@ -315,7 +314,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
} }
@@ -341,10 +340,8 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
} }
// swap old context (including its config) with the new one // swap old context (including its config) with the new one
currentCtxMu.Lock()
oldCtx := currentCtx oldCtx := currentCtx
currentCtx = ctx currentCtx = ctx
currentCtxMu.Unlock()
// Stop, Cleanup each old app // Stop, Cleanup each old app
unsyncedStop(oldCtx) unsyncedStop(oldCtx)
@@ -357,13 +354,13 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
newCfg.Admin.Config.Persist == nil || newCfg.Admin.Config.Persist == nil ||
*newCfg.Admin.Config.Persist) { *newCfg.Admin.Config.Persist) {
dir := filepath.Dir(ConfigAutosavePath) dir := filepath.Dir(ConfigAutosavePath)
err := os.MkdirAll(dir, 0o700) err := os.MkdirAll(dir, 0700)
if err != nil { if err != nil {
Log().Error("unable to create folder for config autosave", Log().Error("unable to create folder for config autosave",
zap.String("dir", dir), zap.String("dir", dir),
zap.Error(err)) zap.Error(err))
} else { } else {
err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0o600) err := os.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 {
@@ -630,35 +627,22 @@ 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()
ctx := currentCtx
currentCtxMu.RUnlock()
rawCfgMu.Lock()
unsyncedStop(ctx)
currentCtxMu.Lock() currentCtxMu.Lock()
defer currentCtxMu.Unlock()
unsyncedStop(currentCtx)
currentCtx = Context{} 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.
//
// A lock on rawCfgMu is required, even though this
// function does not access rawCfg, that lock
// synchronizes the stop/start of apps.
func unsyncedStop(ctx Context) { func unsyncedStop(ctx Context) {
if ctx.cfg == nil { if ctx.cfg == nil {
return return
@@ -832,7 +816,7 @@ func InstanceID() (uuid.UUID, error) {
if err != nil { if err != nil {
return uuid, err return uuid, err
} }
err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0o600) err = os.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
@@ -985,12 +969,14 @@ type CtxKey string
// This group of variables pertains to the current configuration. // This group of variables pertains to the current configuration.
var ( var (
// currentCtxMu protects everything in this var block.
currentCtxMu sync.RWMutex
// currentCtx is the root context for the currently-running // currentCtx is the root context for the currently-running
// configuration, which can be accessed through this value. // configuration, which can be accessed through this value.
// If the Config contained in this value is not nil, then // If the Config contained in this value is not nil, then
// a config is currently active/running. // a config is currently active/running.
currentCtx Context currentCtx Context
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")
@@ -1008,10 +994,6 @@ 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 // errSameConfig is returned if the new config is the same
+4 -4
View File
@@ -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: "Caddyfile input is not formatted; run the 'caddy fmt' command to fix inconsistencies",
}, true }, true
} }
+60 -30
View File
@@ -101,12 +101,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 +122,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
} }
@@ -203,17 +203,14 @@ func (d *Dispenser) Val() string {
} }
// ValRaw gets the raw text of the current token (including quotes). // 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. // If there is no token loaded, it returns empty string.
func (d *Dispenser) ValRaw() string { func (d *Dispenser) ValRaw() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) { if d.cursor < 0 || d.cursor >= len(d.tokens) {
return "" return ""
} }
quote := d.tokens[d.cursor].wasQuoted quote := d.tokens[d.cursor].wasQuoted
if quote > 0 && quote != '<' { if quote > 0 {
// string literal return string(quote) + d.tokens[d.cursor].Text + string(quote) // string literal
return string(quote) + d.tokens[d.cursor].Text + string(quote)
} }
return d.tokens[d.cursor].Text return d.tokens[d.cursor].Text
} }
@@ -391,22 +388,22 @@ func (d *Dispenser) Reset() {
// an argument. // an argument.
func (d *Dispenser) ArgErr() error { func (d *Dispenser) ArgErr() error {
if d.Val() == "{" { if d.Val() == "{" {
return d.Err("unexpected token '{', expecting argument") return d.Err("Unexpected token '{', expecting argument")
} }
return d.Errf("wrong argument count or unexpected line ending after '%s'", d.Val()) return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
} }
// 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("syntax error: unexpected token '%s', expecting '%s', at %s:%d import chain: ['%s']", d.Val(), expected, d.File(), d.Line(), 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)
} }
// EOFErr returns an error indicating that the dispenser reached // EOFErr returns an error indicating that the dispenser reached
// the end of the input when searching for the next token. // the end of the input when searching for the next token.
func (d *Dispenser) EOFErr() error { func (d *Dispenser) EOFErr() error {
return d.Errf("unexpected EOF") return d.Errf("Unexpected EOF")
} }
// Err generates a custom parse-time error with a message of msg. // Err generates a custom parse-time error with a message of msg.
@@ -421,10 +418,7 @@ func (d *Dispenser) Errf(format string, args ...any) error {
// WrapErr takes an existing error and adds the Caddyfile file and line number. // WrapErr takes an existing error and adds the Caddyfile file and line number.
func (d *Dispenser) WrapErr(err error) error { func (d *Dispenser) WrapErr(err error) error {
if len(d.Token().imports) > 0 { return fmt.Errorf("%s:%d - Error during parsing: %w", d.File(), d.Line(), err)
return fmt.Errorf("%w, at %s:%d import chain ['%s']", err, d.File(), d.Line(), strings.Join(d.Token().imports, "','"))
}
return fmt.Errorf("%w, at %s:%d", err, d.File(), d.Line())
} }
// Delete deletes the current token and returns the updated slice // Delete deletes the current token and returns the updated slice
@@ -444,14 +438,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
@@ -467,7 +461,25 @@ func (d *Dispenser) isNewLine() bool {
prev := d.tokens[d.cursor-1] prev := d.tokens[d.cursor-1]
curr := d.tokens[d.cursor] curr := d.tokens[d.cursor]
return isNextOnNewLine(prev, curr)
// If the previous token is from a different file,
// we can assume it's from a different line
if prev.File != curr.File {
return true
}
// The previous token may contain line breaks if
// it was quoted and spanned multiple lines. e.g:
//
// dir "foo
// bar
// baz"
prevLineBreaks := d.numLineBreaks(d.cursor - 1)
// If the previous token (incl line breaks) ends
// on a line earlier than the current token,
// then the current token is on a new line
return prev.Line+prevLineBreaks < curr.Line
} }
// isNextOnNewLine determines whether the current token is on a different // isNextOnNewLine determines whether the current token is on a different
@@ -483,5 +495,23 @@ func (d *Dispenser) isNextOnNewLine() bool {
curr := d.tokens[d.cursor] curr := d.tokens[d.cursor]
next := d.tokens[d.cursor+1] next := d.tokens[d.cursor+1]
return isNextOnNewLine(curr, next)
// If the next token is from a different file,
// we can assume it's from a different line
if curr.File != next.File {
return true
}
// The current token may contain line breaks if
// it was quoted and spanned multiple lines. e.g:
//
// dir "foo
// bar
// baz"
currLineBreaks := d.numLineBreaks(d.cursor)
// If the current token (incl line breaks) ends
// on a line earlier than the next token,
// then the next token is on a new line
return curr.Line+currLineBreaks < next.Line
} }
-160
View File
@@ -1,160 +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"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
)
// 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
}
// A valid token may contain several placeholders, and
// they may be separated by ":". It's not variadic.
// https://github.com/caddyserver/caddy/issues/5716
if strings.Contains(start, "}") || strings.Contains(end, "{") {
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 {
// What's matched may be a substring of the key
if matches[0] != key {
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
}
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 {
// What's matched may be a substring of the key
if matches[0] != key {
return nil, false
}
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\[(.+)]`)
)
-3
View File
@@ -34,7 +34,6 @@ func (i *importGraph) addNode(name string) {
} }
i.nodes[name] = true i.nodes[name] = true
} }
func (i *importGraph) addNodes(names []string) { func (i *importGraph) addNodes(names []string) {
for _, name := range names { for _, name := range names {
i.addNode(name) i.addNode(name)
@@ -44,7 +43,6 @@ func (i *importGraph) addNodes(names []string) {
func (i *importGraph) removeNode(name string) { func (i *importGraph) removeNode(name string) {
delete(i.nodes, name) delete(i.nodes, name)
} }
func (i *importGraph) removeNodes(names []string) { func (i *importGraph) removeNodes(names []string) {
for _, name := range names { for _, name := range names {
i.removeNode(name) i.removeNode(name)
@@ -75,7 +73,6 @@ func (i *importGraph) addEdge(from, to string) error {
i.edges[from] = append(i.edges[from], to) i.edges[from] = append(i.edges[from], to)
return nil return nil
} }
func (i *importGraph) addEdges(from string, tos []string) error { func (i *importGraph) addEdges(from string, tos []string) error {
for _, to := range tos { for _, to := range tos {
err := i.addEdge(from, to) err := i.addEdge(from, to)
+32 -214
View File
@@ -17,10 +17,7 @@ package caddyfile
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"fmt"
"io" "io"
"regexp"
"strings"
"unicode" "unicode"
) )
@@ -38,41 +35,15 @@ 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 wasQuoted rune // enclosing quote character, if any
wasQuoted rune // enclosing quote character, if any inSnippet bool
heredocMarker string snippetName 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,107 +75,28 @@ 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(quoted rune) bool {
l.token.Text = string(val) l.token.Text = string(val)
l.token.wasQuoted = quoted 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(0)
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 !(quoted || btQuoted) && !(inHeredoc || heredocEscaped) &&
len(val) > 1 && string(val[:2]) == "<<" {
// a space means it's just a regular token and not a heredoc
if ch == ' ' {
return makeToken(0), nil
}
// skip CR, we only care about LF
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' {
if len(val) == 2 {
return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alpha-numeric characters, dashes and underscores; got empty string", l.line)
}
// check if there's too many <
if string(val[:3]) == "<<<" {
return false, fmt.Errorf("too many '<' for heredoc on line #%d; only use two, for example <<END", l.line)
}
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
@@ -219,29 +111,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
@@ -249,18 +138,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(0)
} }
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
} }
@@ -281,12 +166,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
} }
@@ -294,86 +174,24 @@ 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 { func (t Token) Quoted() bool {
return t.wasQuoted > 0 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
}
+10 -217
View File
@@ -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,219 +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(`escaped-heredoc \<< >>`),
expected: []Token{
{Line: 1, Text: `escaped-heredoc`},
{Line: 1, Text: `<<`},
{Line: 1, Text: `>>`},
},
},
{
input: []byte(`not-a-heredoc <EOF
content
`),
expected: []Token{
{Line: 1, Text: `not-a-heredoc`},
{Line: 1, Text: `<EOF`},
{Line: 2, Text: `content`},
},
},
{
input: []byte(`not-a-heredoc <<<EOF content`),
expected: []Token{
{Line: 1, Text: `not-a-heredoc`},
{Line: 1, Text: `<<<EOF`},
{Line: 1, Text: `content`},
},
},
{
input: []byte(`not-a-heredoc "<<" ">>"`),
expected: []Token{
{Line: 1, Text: `not-a-heredoc`},
{Line: 1, Text: `<<`},
{Line: 1, Text: `>>`},
},
},
{
input: []byte(`not-a-heredoc << >>`),
expected: []Token{
{Line: 1, Text: `not-a-heredoc`},
{Line: 1, Text: `<<`},
{Line: 1, Text: `>>`},
},
},
{
input: []byte(`not-a-heredoc <<HERE SAME LINE
content
HERE same-line-arg
`),
expected: []Token{
{Line: 1, Text: `not-a-heredoc`},
{Line: 1, Text: `<<HERE`},
{Line: 1, Text: `SAME`},
{Line: 1, Text: `LINE`},
{Line: 2, Text: `content`},
{Line: 3, Text: `HERE`},
{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("not-a-heredoc <<\n"),
expectErr: true,
errorMessage: "missing opening heredoc marker on line #1; must contain only alpha-numeric characters, dashes and underscores; got empty string",
},
{
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)
} }
@@ -469,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
} }
+41 -141
View File
@@ -20,11 +20,11 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"go.uber.org/zap"
"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
@@ -61,12 +61,20 @@ func Parse(filename string, input []byte) ([]ServerBlock, error) {
// It returns all the tokens from the input, unstructured // It returns all the tokens from the input, unstructured
// and in order. It may mutate input as it expands env vars. // and in order. It may mutate input as it expands env vars.
func allTokens(filename string, input []byte) ([]Token, error) { func allTokens(filename string, input []byte) ([]Token, error) {
return Tokenize(replaceEnvVars(input), filename) inputCopy, err := replaceEnvVars(input)
if err != nil {
return nil, err
}
tokens, err := Tokenize(inputCopy, filename)
if err != nil {
return nil, err
}
return tokens, nil
} }
// replaceEnvVars replaces all occurrences of environment variables. // replaceEnvVars replaces all occurrences of environment variables.
// It mutates the underlying array and returns the updated slice. // It mutates the underlying array and returns the updated slice.
func replaceEnvVars(input []byte) []byte { func replaceEnvVars(input []byte) ([]byte, error) {
var offset int var offset int
for { for {
begin := bytes.Index(input[offset:], spanOpen) begin := bytes.Index(input[offset:], spanOpen)
@@ -107,7 +115,7 @@ 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
} }
type parser struct { type parser struct {
@@ -149,6 +157,7 @@ func (p *parser) begin() error {
} }
err := p.addresses() err := p.addresses()
if err != nil { if err != nil {
return err return err
} }
@@ -159,25 +168,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{}
@@ -186,15 +176,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
} }
@@ -215,7 +206,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
} }
@@ -315,7 +306,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
} }
@@ -341,7 +332,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()
@@ -354,8 +345,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)
@@ -403,20 +397,6 @@ func (p *parser) doImport(nesting int) error {
} 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
@@ -431,7 +411,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)
@@ -442,69 +422,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
@@ -535,12 +459,6 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
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)
@@ -566,6 +484,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
// are loaded into the current server block for later use // are loaded into the current server block for later use
// by directive setup functions. // by directive setup functions.
func (p *parser) directive() error { func (p *parser) directive() error {
// a segment is a list of tokens associated with this directive // a segment is a list of tokens associated with this directive
var segment Segment var segment Segment
@@ -578,9 +497,6 @@ func (p *parser) directive() error {
if !p.isNextOnNewLine() && p.Token().wasQuoted == 0 { if !p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
return p.Err("Unexpected next token after '{' on same line") 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() == "{}" { } else if p.Val() == "{}" {
if p.isNextOnNewLine() && p.Token().wasQuoted == 0 { if p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
return p.Err("Unexpected '{}' at end of line") return p.Err("Unexpected '{}' at end of line")
@@ -593,7 +509,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 +550,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 +560,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 +591,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
+4 -142
View File
@@ -21,92 +21,6 @@ import (
"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,
},
{
input: "{args[0]}:{args[1]}:{args[2]}",
result: false,
},
} {
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"}
@@ -273,23 +187,6 @@ 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{}},
@@ -297,14 +194,6 @@ func TestParseOneAndImport(t *testing.T) {
// Unexpected next token after '{' on same line // Unexpected next token after '{' on same line
{`localhost {`localhost
dir1 { a b }`, true, []string{"localhost"}, []int{}}, dir1 { a b }`, true, []string{"localhost"}, []int{}},
// Unexpected '{' on a new line
{`localhost
dir1
{
a b
}`, true, []string{"localhost"}, []int{}},
// Workaround with quotes // Workaround with quotes
{`localhost {`localhost
dir1 "{" a b "}"`, false, []string{"localhost"}, []int{5}}, dir1 "{" a b "}"`, false, []string{"localhost"}, []int{5}},
@@ -715,38 +604,11 @@ func TestEnvironmentReplacement(t *testing.T) {
expect: "}{$", expect: "}{$",
}, },
} { } {
actual := replaceEnvVars([]byte(test.input)) actual, err := replaceEnvVars([]byte(test.input))
if !bytes.Equal(actual, []byte(test.expect)) { if err != nil {
t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual) t.Fatal(err)
} }
} if !bytes.Equal(actual, []byte(test.expect)) {
}
func TestImportReplacementInJSONWithBrace(t *testing.T) {
for i, test := range []struct {
args []string
input string
expect string
}{
{
args: []string{"123"},
input: "{args[0]}",
expect: "123",
},
{
args: []string{"123"},
input: `{"key":"{args[0]}"}`,
expect: `{"key":"123"}`,
},
{
args: []string{"123", "123"},
input: `{"key":[{args[0]},{args[1]}]}`,
expect: `{"key":[123,123]}`,
},
} {
repl := makeArgsReplacer(test.args)
actual := repl.ReplaceKnown(test.input, "")
if actual != 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)
} }
} }
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}
-7
View File
@@ -1,7 +0,0 @@
 
+3 -17
View File
@@ -24,11 +24,10 @@ import (
"strings" "strings"
"unicode" "unicode"
"github.com/caddyserver/certmagic"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/certmagic"
) )
// mapAddressToServerBlocks returns a map of listener address to list of server // mapAddressToServerBlocks returns a map of listener address to list of server
@@ -78,8 +77,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, options map[string]any) (map[string][]serverBlock, error) {
) (map[string][]serverBlock, error) {
sbmap := make(map[string][]serverBlock) sbmap := make(map[string][]serverBlock)
for i, sblock := range originalServerBlocks { for i, sblock := range originalServerBlocks {
@@ -189,25 +187,13 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
// listenerAddrsForServerBlockKey essentially converts the Caddyfile // listenerAddrsForServerBlockKey essentially converts the Caddyfile
// site addresses to Caddy listener addresses for each server block. // 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, options map[string]any) ([]string, error) {
) ([]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)
} }
addr = addr.Normalize() addr = addr.Normalize()
switch addr.Scheme {
case "wss":
return nil, fmt.Errorf("the scheme wss:// is only supported in browsers; use https:// instead")
case "ws":
return nil, fmt.Errorf("the scheme ws:// is only supported in browsers; use http:// instead")
case "https", "http", "":
// Do nothing or handle the valid schemes
default:
return nil, fmt.Errorf("unsupported URL scheme %s://", addr.Scheme)
}
// figure out the HTTP and HTTPS ports; either // figure out the HTTP and HTTPS ports; either
// use defaults, or override with user config // use defaults, or override with user config
httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort) httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort)
+50 -172
View File
@@ -24,17 +24,15 @@ import (
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
"go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
"go.uber.org/zap/zapcore"
) )
func init() { func init() {
@@ -49,7 +47,6 @@ 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) RegisterHandlerDirective("skip_log", parseSkipLog)
} }
@@ -78,22 +75,16 @@ func parseBind(h Helper) ([]ConfigValue, error) {
// 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> [...]
// propagation_delay <duration>
// propagation_timeout <duration>
// resolvers <dns_servers...>
// dns_ttl <duration>
// dns_challenge_override_domain <domain>
// on_demand // on_demand
// eab <key_id> <mac_key> // eab <key_id> <mac_key>
// issuer <module_name> [...] // issuer <module_name> [...]
// get_certificate <module_name> [...] // get_certificate <module_name> [...]
// insecure_secrets_log <log_file> // 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)
@@ -181,17 +172,17 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
case "protocols": case "protocols":
args := h.RemainingArgs() args := h.RemainingArgs()
if len(args) == 0 { if len(args) == 0 {
return nil, h.Errf("protocols requires one or two arguments") return nil, h.SyntaxErr("one or two protocols")
} }
if len(args) > 0 { if len(args) > 0 {
if _, ok := caddytls.SupportedProtocols[args[0]]; !ok { if _, ok := caddytls.SupportedProtocols[args[0]]; !ok {
return nil, h.Errf("wrong protocol name or protocol not supported: '%s'", args[0]) return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[0])
} }
cp.ProtocolMin = args[0] cp.ProtocolMin = args[0]
} }
if len(args) > 1 { if len(args) > 1 {
if _, ok := caddytls.SupportedProtocols[args[1]]; !ok { if _, ok := caddytls.SupportedProtocols[args[1]]; !ok {
return nil, h.Errf("wrong protocol name or protocol not supported: '%s'", args[1]) return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[1])
} }
cp.ProtocolMax = args[1] cp.ProtocolMax = args[1]
} }
@@ -199,7 +190,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
case "ciphers": case "ciphers":
for h.NextArg() { for h.NextArg() {
if !caddytls.CipherSuiteNameSupported(h.Val()) { if !caddytls.CipherSuiteNameSupported(h.Val()) {
return nil, h.Errf("wrong cipher suite name or cipher suite not supported: '%s'", h.Val()) return nil, h.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", h.Val())
} }
cp.CipherSuites = append(cp.CipherSuites, h.Val()) cp.CipherSuites = append(cp.CipherSuites, h.Val())
} }
@@ -372,75 +363,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": case "dns_challenge_override_domain":
arg := h.RemainingArgs() arg := h.RemainingArgs()
if len(arg) != 1 { if len(arg) != 1 {
@@ -733,20 +655,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) {
@@ -766,31 +697,9 @@ 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>
@@ -811,13 +720,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() {
@@ -828,47 +735,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 = caddy.DefaultLoggerName
} }
// 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()
@@ -927,16 +813,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())
@@ -948,34 +836,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",
+2 -140
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"
@@ -52,13 +51,12 @@ func TestLogDirectiveSyntax(t *testing.T) {
}, },
{ {
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,
}, },
} { } {
@@ -215,139 +213,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 -22
View File
@@ -65,7 +65,6 @@ var directiveOrder = []string{
"templates", "templates",
// special routing & dispatching directives // special routing & dispatching directives
"invoke",
"handle", "handle",
"handle_path", "handle_path",
"route", "route",
@@ -173,7 +172,6 @@ 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
} }
@@ -217,8 +215,7 @@ func (h Helper) ExtractMatcherSet() (caddy.ModuleMap, error) {
// NewRoute returns config values relevant to creating a new HTTP route. // NewRoute returns config values relevant to creating a new HTTP route.
func (h Helper) NewRoute(matcherSet caddy.ModuleMap, func (h Helper) NewRoute(matcherSet caddy.ModuleMap,
handler caddyhttp.MiddlewareHandler, handler caddyhttp.MiddlewareHandler) []ConfigValue {
) []ConfigValue {
mod, err := caddy.GetModule(caddy.GetModuleID(handler)) mod, err := caddy.GetModule(caddy.GetModuleID(handler))
if err != nil { if err != nil {
*h.warnings = append(*h.warnings, caddyconfig.Warning{ *h.warnings = append(*h.warnings, caddyconfig.Warning{
@@ -292,7 +289,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
@@ -430,16 +427,26 @@ func sortRoutes(routes []ConfigValue) {
jPathLen = len(jPM[0]) jPathLen = len(jPM[0])
} }
sortByPath := func() bool { // some directives involve setting values which can overwrite
// each other, so it makes most sense to reverse the order so
// that the lease specific matcher is first; everything else
// has most-specific matcher first
if iDir == "vars" {
// we can only confidently compare path lengths if both // we can only confidently compare path lengths if both
// directives have a single path to match (issue #5037) // directives have a single path to match (issue #5037)
if iPathLen > 0 && jPathLen > 0 { if iPathLen > 0 && jPathLen > 0 {
// if both paths are the same except for a trailing wildcard, // sort least-specific (shortest) path first
// sort by the shorter path first (which is more specific) return iPathLen < jPathLen
if strings.TrimSuffix(iPM[0], "*") == strings.TrimSuffix(jPM[0], "*") { }
return iPathLen < jPathLen
}
// if both directives don't have a single path to compare,
// sort whichever one has no matcher first; if both have
// no matcher, sort equally (stable sort preserves order)
return len(iRoute.MatcherSetsRaw) == 0 && len(jRoute.MatcherSetsRaw) > 0
} else {
// we can only confidently compare path lengths if both
// directives have a single path to match (issue #5037)
if iPathLen > 0 && jPathLen > 0 {
// sort most-specific (longest) path first // sort most-specific (longest) path first
return iPathLen > jPathLen return iPathLen > jPathLen
} }
@@ -448,18 +455,7 @@ func sortRoutes(routes []ConfigValue) {
// sort whichever one has a matcher first; if both have // sort whichever one has a matcher first; if both have
// a matcher, sort equally (stable sort preserves order) // 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
return sortByPath
}) })
} }
+88 -215
View File
@@ -17,21 +17,19 @@ package httpcaddyfile
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"reflect" "reflect"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"go.uber.org/zap"
"golang.org/x/exp/slices"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"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"
) )
func init() { func init() {
@@ -50,13 +48,12 @@ type App struct {
} }
// ServerType can set up a config from an HTTP Caddyfile. // ServerType can set up a config from an HTTP Caddyfile.
type ServerType struct{} 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]any) (*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]any)
@@ -82,18 +79,41 @@ func (st ServerType) Setup(
return nil, warnings, err return nil, warnings, err
} }
// this will replace both static and user-defined placeholder shorthands // replace shorthand placeholders (which are convenient
// with actual identifiers used by Caddy // when writing a Caddyfile) with their actual placeholder
replacer := NewShorthandReplacer() // identifiers or variable names
replacer := strings.NewReplacer(placeholderShorthands()...)
originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings, replacer) // these are placeholders that allow a user-defined final
if err != nil { // parameters, but we still want to provide a shorthand
return nil, warnings, err // for those, so we use a regexp to replace
regexpReplacements := []struct {
search *regexp.Regexp
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(`{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 {
for i := range sb.block.Segments { for _, segment := range sb.block.Segments {
replacer.ApplyToSegment(&sb.block.Segments[i]) for i := 0; i < len(segment); i++ {
// simple string replacements
segment[i].Text = replacer.Replace(segment[i].Text)
// complex regexp replacements
for _, r := range regexpReplacements {
segment[i].Text = r.search.ReplaceAllString(segment[i].Text, r.replace)
}
}
} }
if len(sb.block.Keys) == 0 { if len(sb.block.Keys) == 0 {
@@ -152,18 +172,6 @@ func (st ServerType) Setup(
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
}
} }
} }
@@ -214,7 +222,7 @@ func (st ServerType) Setup(
if ncl.name == caddy.DefaultLoggerName { if ncl.name == caddy.DefaultLoggerName {
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 = zap.DebugLevel.CapitalString()
} }
customLogs = append(customLogs, ncl) customLogs = append(customLogs, ncl)
@@ -233,9 +241,7 @@ func (st ServerType) Setup(
if _, ok := options["debug"]; ok { if _, ok := options["debug"]; ok {
customLogs = append(customLogs, namedCustomLog{ customLogs = append(customLogs, namedCustomLog{
name: caddy.DefaultLoggerName, name: caddy.DefaultLoggerName,
log: &caddy.CustomLog{ log: &caddy.CustomLog{Level: zap.DebugLevel.CapitalString()},
BaseLog: caddy.BaseLog{Level: zap.DebugLevel.CapitalString()},
},
}) })
} }
} }
@@ -280,38 +286,13 @@ 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
} }
@@ -325,16 +306,8 @@ func (st ServerType) Setup(
cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog cfg.Logging.Logs[caddy.DefaultLoggerName] = 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
@@ -417,81 +390,6 @@ 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,
replacer ShorthandReplacer,
) ([]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
}
wholeSegment := caddyfile.Segment{}
for i := range sb.block.Segments {
// replace user-defined placeholder shorthands in extracted named routes
replacer.ApplyToSegment(&sb.block.Segments[i])
// zip up all the segments since ParseSegmentAsSubroute
// was designed to take a directive+
wholeSegment = append(wholeSegment, sb.block.Segments[i]...)
}
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(
@@ -502,7 +400,6 @@ func (st *ServerType) serversFromPairings(
) (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 {
@@ -631,24 +528,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)
@@ -678,21 +557,14 @@ 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 {
slices.Sort(hosts) // for deterministic JSON output
cp.MatchersRaw = caddy.ModuleMap{ cp.MatchersRaw = caddy.ModuleMap{
"sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones "sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones
} }
} 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
@@ -718,20 +590,10 @@ func (st *ServerType) serversFromPairings(
} }
} }
// If TLS is specified as directive, it will also result in 1 or more connection policy being created
// Thus, catch-all address with non-standard port, e.g. :8443, can have TLS enabled without
// specifying prefix "https://"
// Second part of the condition is to allow creating TLS conn policy even though `auto_https` has been disabled
// ensuring compatibility with behavior described in below link
// https://caddy.community/t/making-sense-of-auto-https-and-why-disabling-it-still-serves-https-instead-of-http/9761
createdTLSConnPolicies, ok := sblock.pile["tls.connection_policy"]
hasTLSEnabled := (ok && len(createdTLSConnPolicies) > 0) ||
(addr.Host != "" && srv.AutoHTTPS != nil && !sliceContains(srv.AutoHTTPS.Skip, addr.Host))
// we'll need to remember if the address qualifies for auto-HTTPS, so we // we'll need to remember if the address qualifies for auto-HTTPS, so we
// can add a TLS conn policy if necessary // can add a TLS conn policy if necessary
if addr.Scheme == "https" || if addr.Scheme == "https" ||
(addr.Scheme != "http" && addr.Port != httpPort && hasTLSEnabled) { (addr.Scheme != "http" && addr.Host != "" && addr.Port != httpPort) {
addressQualifiesForTLS = true addressQualifiesForTLS = true
} }
// predict whether auto-HTTPS will add the conn policy for us; if so, we // predict whether auto-HTTPS will add the conn policy for us; if so, we
@@ -756,7 +618,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
} }
@@ -780,30 +642,17 @@ 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 srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string) srv.Logs.LoggerNames = make(map[string]string)
} }
// strip the port from the host, if any srv.Logs.LoggerNames[h] = ncl.name
host, _, err := net.SplitHostPort(h)
if err != nil {
host = h
}
srv.Logs.LoggerNames[host] = ncl.name
} }
} }
} }
@@ -841,8 +690,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
@@ -857,7 +706,7 @@ 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
@@ -1051,8 +900,8 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
subroute *caddyhttp.Subroute, subroute *caddyhttp.Subroute,
matcherSetsEnc []caddy.ModuleMap, matcherSetsEnc []caddy.ModuleMap,
p sbAddrAssociation, p sbAddrAssociation,
warnings *[]caddyconfig.Warning, warnings *[]caddyconfig.Warning) caddyhttp.RouteList {
) caddyhttp.RouteList {
// nothing to do if... there's nothing to do // nothing to do if... there's nothing to do
if len(matcherSetsEnc) == 0 && len(subroute.Routes) == 0 && subroute.Errors == nil { if len(matcherSetsEnc) == 0 && len(subroute.Routes) == 0 && subroute.Errors == nil {
return routeList return routeList
@@ -1110,17 +959,15 @@ func appendSubrouteToRouteList(routeList caddyhttp.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 an ordered HTTP handler, 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
@@ -1441,6 +1288,36 @@ 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}",
}
}
// WasReplacedPlaceholderShorthand checks if a token string was // WasReplacedPlaceholderShorthand checks if a token string was
// likely a replaced shorthand of the known Caddyfile placeholder // likely a replaced shorthand of the known Caddyfile placeholder
// replacement outputs. Useful to prevent some user-defined map // replacement outputs. Useful to prevent some user-defined map
@@ -1556,9 +1433,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
@@ -1569,10 +1445,7 @@ type sbAddrAssociation struct {
serverBlocks []serverBlock serverBlocks []serverBlock
} }
const ( const matcherPrefix = "@"
matcherPrefix = "@"
namedRouteKey = "named_route"
)
// Interface guard // Interface guard
var _ caddyfile.ServerType = (*ServerType)(nil) var _ caddyfile.ServerType = (*ServerType)(nil)
+2 -20
View File
@@ -17,13 +17,12 @@ package httpcaddyfile
import ( import (
"strconv" "strconv"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
) )
func init() { func init() {
@@ -34,7 +33,6 @@ func init() {
RegisterGlobalOption("grace_period", parseOptDuration) RegisterGlobalOption("grace_period", parseOptDuration)
RegisterGlobalOption("shutdown_delay", parseOptDuration) RegisterGlobalOption("shutdown_delay", parseOptDuration)
RegisterGlobalOption("default_sni", parseOptSingleString) RegisterGlobalOption("default_sni", parseOptSingleString)
RegisterGlobalOption("fallback_sni", parseOptSingleString)
RegisterGlobalOption("order", parseOptOrder) RegisterGlobalOption("order", parseOptOrder)
RegisterGlobalOption("storage", parseOptStorage) RegisterGlobalOption("storage", parseOptStorage)
RegisterGlobalOption("storage_clean_interval", parseOptDuration) RegisterGlobalOption("storage_clean_interval", parseOptDuration)
@@ -56,7 +54,6 @@ 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, _ any) (any, error) { return true, nil }
@@ -389,21 +386,6 @@ func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) {
return ond, nil return ond, nil
} }
func parseOptPersistConfig(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume parameter name
if !d.Next() {
return "", d.ArgErr()
}
val := d.Val()
if d.Next() {
return "", d.ArgErr()
}
if val != "off" {
return "", d.Errf("persist_config must be 'off'")
}
return val, nil
}
func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume parameter name d.Next() // consume parameter name
if !d.Next() { if !d.Next() {
+18 -29
View File
@@ -15,7 +15,6 @@
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/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddypki" "github.com/caddyserver/caddy/v2/modules/caddypki"
@@ -27,24 +26,23 @@ func init() {
// parsePKIApp parses the global log option. Syntax: // parsePKIApp parses the global log option. Syntax:
// //
// pki { // pki {
// ca [<id>] { // ca [<id>] {
// name <name> // name <name>
// root_cn <name> // root_cn <name>
// intermediate_cn <name> // intermediate_cn <name>
// intermediate_lifetime <duration> // root {
// root { // cert <path>
// cert <path> // key <path>
// key <path> // format <format>
// format <format> // }
// } // intermediate {
// intermediate { // cert <path>
// cert <path> // key <path>
// key <path> // format <format>
// format <format> // }
// } // }
// } // }
// }
// //
// When the CA ID is unspecified, 'local' is assumed. // When the CA ID is unspecified, 'local' is assumed.
func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) { func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) {
@@ -85,16 +83,6 @@ func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) {
} }
pkiCa.IntermediateCommonName = d.Val() 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": case "root":
if pkiCa.Root == nil { if pkiCa.Root == nil {
pkiCa.Root = new(caddypki.KeyPair) pkiCa.Root = new(caddypki.KeyPair)
@@ -174,6 +162,7 @@ func (st ServerType) buildPKIApp(
options map[string]any, options map[string]any,
warnings []caddyconfig.Warning, warnings []caddyconfig.Warning,
) (*caddypki.PKI, []caddyconfig.Warning, error) { ) (*caddypki.PKI, []caddyconfig.Warning, error) {
skipInstallTrust := false skipInstallTrust := false
if _, ok := options["skip_install_trust"]; ok { if _, ok := options["skip_install_trust"]; ok {
skipInstallTrust = true skipInstallTrust = true
+2 -83
View File
@@ -18,12 +18,11 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/dustin/go-humanize"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/dustin/go-humanize"
) )
// serverOptions collects server config overrides parsed from Caddyfile global options // serverOptions collects server config overrides parsed from Caddyfile global options
@@ -34,7 +33,6 @@ 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
@@ -42,11 +40,8 @@ type serverOptions struct {
IdleTimeout caddy.Duration IdleTimeout caddy.Duration
KeepAliveInterval caddy.Duration KeepAliveInterval caddy.Duration
MaxHeaderBytes int MaxHeaderBytes int
EnableFullDuplex bool
Protocols []string Protocols []string
StrictSNIHost *bool StrictSNIHost *bool
TrustedProxiesRaw json.RawMessage
ClientIPHeaders []string
ShouldLogCredentials bool ShouldLogCredentials bool
Metrics *caddyhttp.Metrics Metrics *caddyhttp.Metrics
} }
@@ -62,15 +57,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()
@@ -159,12 +145,6 @@ 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": case "log_credentials":
if d.NextArg() { if d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
@@ -196,39 +176,6 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
} }
serverOpts.StrictSNIHost = &boolVal 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": case "metrics":
if d.NextArg() { if d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
@@ -291,22 +238,7 @@ func applyServerOptions(
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 {
@@ -335,11 +267,8 @@ func applyServerOptions(
server.IdleTimeout = opts.IdleTimeout server.IdleTimeout = opts.IdleTimeout
server.KeepAliveInterval = opts.KeepAliveInterval server.KeepAliveInterval = opts.KeepAliveInterval
server.MaxHeaderBytes = opts.MaxHeaderBytes server.MaxHeaderBytes = opts.MaxHeaderBytes
server.EnableFullDuplex = opts.EnableFullDuplex
server.Protocols = opts.Protocols server.Protocols = opts.Protocols
server.StrictSNIHost = opts.StrictSNIHost server.StrictSNIHost = opts.StrictSNIHost
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
server.ClientIPHeaders = opts.ClientIPHeaders
server.Metrics = opts.Metrics server.Metrics = opts.Metrics
if opts.ShouldLogCredentials { if opts.ShouldLogCredentials {
if server.Logs == nil { if server.Logs == nil {
@@ -347,16 +276,6 @@ func applyServerOptions(
} }
server.Logs.ShouldLogCredentials = opts.ShouldLogCredentials 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
-92
View File
@@ -1,92 +0,0 @@
package httpcaddyfile
import (
"regexp"
"strings"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
type ComplexShorthandReplacer struct {
search *regexp.Regexp
replace string
}
type ShorthandReplacer struct {
complex []ComplexShorthandReplacer
simple *strings.Replacer
}
func NewShorthandReplacer() ShorthandReplacer {
// replace shorthand placeholders (which are convenient
// when writing a Caddyfile) with their actual placeholder
// identifiers or variable names
replacer := strings.NewReplacer(placeholderShorthands()...)
// these are placeholders that allow a user-defined final
// parameters, but we still want to provide a shorthand
// for those, so we use a regexp to replace
regexpReplacements := []ComplexShorthandReplacer{
{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(`{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}"},
}
return ShorthandReplacer{
complex: regexpReplacements,
simple: replacer,
}
}
// 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}",
}
}
// ApplyToSegment replaces shorthand placeholder to its full placeholder, understandable by Caddy.
func (s ShorthandReplacer) ApplyToSegment(segment *caddyfile.Segment) {
if segment != nil {
for i := 0; i < len(*segment); i++ {
// simple string replacements
(*segment)[i].Text = s.simple.Replace((*segment)[i].Text)
// complex regexp replacements
for _, r := range s.complex {
(*segment)[i].Text = r.search.ReplaceAllString((*segment)[i].Text, r.replace)
}
}
}
}
@@ -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
}
+22 -27
View File
@@ -23,13 +23,12 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
) )
func (st ServerType) buildTLSApp( func (st ServerType) buildTLSApp(
@@ -37,6 +36,7 @@ func (st ServerType) buildTLSApp(
options map[string]any, options map[string]any,
warnings []caddyconfig.Warning, warnings []caddyconfig.Warning,
) (*caddytls.TLS, []caddyconfig.Warning, error) { ) (*caddytls.TLS, []caddyconfig.Warning, error) {
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)} tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
var certLoaders []caddytls.CertificateLoader var certLoaders []caddytls.CertificateLoader
@@ -206,8 +206,8 @@ func (st ServerType) buildTLSApp(
} }
// 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 +217,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 +235,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)}
} }
} }
@@ -343,14 +339,14 @@ func (st ServerType) buildTLSApp(
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.SubjectsRaw = append(internalAP.SubjectsRaw, h) internalAP.Subjects = append(internalAP.Subjects, 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)
} }
@@ -416,7 +412,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)
} }
@@ -537,7 +533,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 +541,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]) {
@@ -582,13 +578,12 @@ outer:
// eaten up by the one with subjects; and if both have subjects, we // eaten up by the one with subjects; and if both have subjects, we
// need to combine their lists // need to combine their lists
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) && if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) &&
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) && bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
aps[i].MustStaple == aps[j].MustStaple && aps[i].MustStaple == aps[j].MustStaple &&
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.
@@ -603,9 +598,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:]...)
@@ -621,15 +616,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
@@ -667,7 +662,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)
} }
+5 -22
View File
@@ -30,14 +30,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 +45,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"`
@@ -119,12 +108,7 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
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
} }
@@ -140,7 +124,6 @@ func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response
if err != nil { if err != nil {
return nil, fmt.Errorf("problem calling http loader url: %v", err) return nil, fmt.Errorf("problem calling http loader url: %v", err)
} else if resp.StatusCode < 200 || resp.StatusCode > 499 { } else if resp.StatusCode < 200 || resp.StatusCode > 499 {
resp.Body.Close()
return nil, fmt.Errorf("bad response status code from http loader url: %v", resp.StatusCode) return nil, fmt.Errorf("bad response status code from http loader url: %v", resp.StatusCode)
} }
return resp, nil return resp, nil
@@ -151,16 +134,16 @@ func doHttpCallWithRetries(ctx caddy.Context, client *http.Client, request *http
var err error var err error
const maxAttempts = 10 const maxAttempts = 10
// attempt up to 10 times
for i := 0; i < maxAttempts; i++ { for i := 0; i < maxAttempts; i++ {
resp, err = attemptHttpCall(client, request) resp, err = attemptHttpCall(client, request)
if err != nil && i < maxAttempts-1 { if err != nil && i < maxAttempts-1 {
// wait 500ms before reattempting, or until context is done
select { select {
case <-time.After(time.Millisecond * 500): case <-time.After(time.Millisecond * 500):
case <-ctx.Done(): case <-ctx.Done():
return resp, ctx.Err() return resp, ctx.Err()
} }
} else {
break
} }
} }
+23 -25
View File
@@ -22,10 +22,9 @@ import (
"time" "time"
"github.com/aryann/difflib" "github.com/aryann/difflib"
"github.com/caddyserver/caddy/v2/caddyconfig"
caddycmd "github.com/caddyserver/caddy/v2/cmd" caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/caddyserver/caddy/v2/caddyconfig"
// plug in Caddy modules here // plug in Caddy modules here
_ "github.com/caddyserver/caddy/v2/modules/standard" _ "github.com/caddyserver/caddy/v2/modules/standard"
) )
@@ -64,6 +63,7 @@ type Tester struct {
// NewTester will create a new testing client with an attached cookie jar // NewTester will create a new testing client with an attached cookie jar
func NewTester(t *testing.T) *Tester { func NewTester(t *testing.T) *Tester {
jar, err := cookiejar.New(nil) jar, err := cookiejar.New(nil)
if err != nil { if err != nil {
t.Fatalf("failed to create cookiejar: %s", err) t.Fatalf("failed to create cookiejar: %s", err)
@@ -94,6 +94,7 @@ func timeElapsed(start time.Time, name string) {
// InitServer this will configure the server with a configurion of a specific // InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type. // type. The configType must be either "json" or the adapter type.
func (tc *Tester) InitServer(rawConfig string, configType string) { func (tc *Tester) InitServer(rawConfig string, configType string) {
if err := tc.initServer(rawConfig, configType); err != nil { if err := tc.initServer(rawConfig, configType); err != nil {
tc.t.Logf("failed to load config: %s", err) tc.t.Logf("failed to load config: %s", err)
tc.t.Fail() tc.t.Fail()
@@ -107,12 +108,13 @@ func (tc *Tester) InitServer(rawConfig string, configType string) {
// InitServer this will configure the server with a configurion of a specific // InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type. // type. The configType must be either "json" or the adapter type.
func (tc *Tester) initServer(rawConfig string, configType string) error { func (tc *Tester) initServer(rawConfig string, configType string) error {
if testing.Short() { if testing.Short() {
tc.t.SkipNow() tc.t.SkipNow()
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
@@ -216,20 +218,16 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
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 {
if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) { if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) {
@@ -238,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", "--config", "./test.init.config", "--adapter", "caddyfile"}
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 := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
time.Sleep(1 * time.Second) time.Sleep(10 * time.Millisecond)
} }
} }
@@ -281,6 +267,7 @@ func isCaddyAdminRunning() error {
} }
func getIntegrationDir() string { func getIntegrationDir() string {
_, filename, _, ok := runtime.Caller(1) _, filename, _, ok := runtime.Caller(1)
if !ok { if !ok {
panic("unable to determine the current file path") panic("unable to determine the current file path")
@@ -300,6 +287,7 @@ func prependCaddyFilePath(rawConfig string) string {
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally // CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
func CreateTestingTransport() *http.Transport { func CreateTestingTransport() *http.Transport {
dialer := net.Dialer{ dialer := net.Dialer{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
KeepAlive: 5 * time.Second, KeepAlive: 5 * time.Second,
@@ -327,6 +315,7 @@ func CreateTestingTransport() *http.Transport {
// AssertLoadError will load a config and expect an error // AssertLoadError will load a config and expect an error
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) { func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
tc := NewTester(t) tc := NewTester(t)
err := tc.initServer(rawConfig, configType) err := tc.initServer(rawConfig, configType)
@@ -337,6 +326,7 @@ func AssertLoadError(t *testing.T, rawConfig string, configType string, expected
// AssertRedirect makes a request and asserts the redirection happens // AssertRedirect makes a request and asserts the redirection happens
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response { func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error { redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse return http.ErrUseLastResponse
} }
@@ -374,6 +364,7 @@ func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, e
// CompareAdapt adapts a config and then compares it against an expected result // CompareAdapt adapts a config and then compares it against an expected result
func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string, expectedResponse string) bool { func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string, expectedResponse string) bool {
cfgAdapter := caddyconfig.GetAdapter(adapterName) cfgAdapter := caddyconfig.GetAdapter(adapterName)
if cfgAdapter == nil { if cfgAdapter == nil {
t.Logf("unrecognized config adapter '%s'", adapterName) t.Logf("unrecognized config adapter '%s'", adapterName)
@@ -461,13 +452,14 @@ func applyHeaders(t *testing.T, req *http.Request, requestHeaders []string) {
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions // AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response { func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
resp, err := tc.Client.Do(req) resp, err := tc.Client.Do(req)
if err != nil { if err != nil {
tc.t.Fatalf("failed to call server %s", err) tc.t.Fatalf("failed to call server %s", err)
} }
if expectedStatusCode != resp.StatusCode { if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.URL.RequestURI(), expectedStatusCode, resp.StatusCode) tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.RequestURI, expectedStatusCode, resp.StatusCode)
} }
return resp return resp
@@ -475,6 +467,7 @@ func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int)
// AssertResponse request a URI and assert the status code and the body contains a string // AssertResponse request a URI and assert the status code and the body contains a string
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
resp := tc.AssertResponseCode(req, expectedStatusCode) resp := tc.AssertResponseCode(req, expectedStatusCode)
defer resp.Body.Close() defer resp.Body.Close()
@@ -496,6 +489,7 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
// AssertGetResponse GET a URI and expect a statusCode and body text // AssertGetResponse GET a URI and expect a statusCode and body text
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("GET", requestURI, nil) req, err := http.NewRequest("GET", requestURI, nil)
if err != nil { if err != nil {
tc.t.Fatalf("unable to create request %s", err) tc.t.Fatalf("unable to create request %s", err)
@@ -506,6 +500,7 @@ func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, e
// AssertDeleteResponse request a URI and expect a statusCode and body text // AssertDeleteResponse request a URI and expect a statusCode and body text
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("DELETE", requestURI, nil) req, err := http.NewRequest("DELETE", requestURI, nil)
if err != nil { if err != nil {
tc.t.Fatalf("unable to create request %s", err) tc.t.Fatalf("unable to create request %s", err)
@@ -516,6 +511,7 @@ func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int
// AssertPostResponseBody POST to a URI and assert the response code and body // AssertPostResponseBody POST to a URI and assert the response code and body
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("POST", requestURI, requestBody) req, err := http.NewRequest("POST", requestURI, requestBody)
if err != nil { if err != nil {
tc.t.Errorf("failed to create request %s", err) tc.t.Errorf("failed to create request %s", err)
@@ -529,6 +525,7 @@ func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []str
// AssertPutResponseBody PUT to a URI and assert the response code and body // AssertPutResponseBody PUT to a URI and assert the response code and body
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PUT", requestURI, requestBody) req, err := http.NewRequest("PUT", requestURI, requestBody)
if err != nil { if err != nil {
tc.t.Errorf("failed to create request %s", err) tc.t.Errorf("failed to create request %s", err)
@@ -542,6 +539,7 @@ func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []stri
// AssertPatchResponseBody PATCH to a URI and assert the response code and body // AssertPatchResponseBody PATCH to a URI and assert the response code and body
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PATCH", requestURI, requestBody) req, err := http.NewRequest("PATCH", requestURI, requestBody)
if err != nil { if err != nil {
tc.t.Errorf("failed to create request %s", err) tc.t.Errorf("failed to create request %s", err)
-33
View File
@@ -1,33 +0,0 @@
package integration
import (
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestACMEServerDirectory(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
local_certs
admin localhost:2999
http_port 9080
https_port 9443
pki {
ca local {
name "Caddy Local Authority"
}
}
}
acme.localhost:9443 {
acme_server
}
`, "caddyfile")
tester.AssertGetResponse(
"https://acme.localhost:9443/acme/local/directory",
200,
`{"newNonce":"https://acme.localhost:9443/acme/local/new-nonce","newAccount":"https://acme.localhost:9443/acme/local/new-account","newOrder":"https://acme.localhost:9443/acme/local/new-order","revokeCert":"https://acme.localhost:9443/acme/local/revoke-cert","keyChange":"https://acme.localhost:9443/acme/local/key-change"}
`)
}
@@ -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,37 +0,0 @@
:8443 {
tls internal {
on_demand
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8443"
],
"tls_connection_policies": [
{}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"issuers": [
{
"module": "internal"
}
],
"on_demand": true
}
]
}
}
}
}
@@ -11,7 +11,6 @@ encode gzip zstd {
header Content-Type application/xhtml+xml* header Content-Type application/xhtml+xml*
header Content-Type application/atom+xml* header Content-Type application/atom+xml*
header Content-Type application/rss+xml* header Content-Type application/rss+xml*
header Content-Type application/wasm*
header Content-Type image/svg+xml* header Content-Type image/svg+xml*
} }
} }
@@ -48,7 +47,6 @@ encode {
"application/xhtml+xml*", "application/xhtml+xml*",
"application/atom+xml*", "application/atom+xml*",
"application/rss+xml*", "application/rss+xml*",
"application/wasm*",
"image/svg+xml*" "image/svg+xml*"
] ]
}, },
@@ -69,11 +69,11 @@
} }
], ],
"on_demand": { "on_demand": {
"ask": "https://example.com",
"rate_limit": { "rate_limit": {
"interval": 30000000000, "interval": 30000000000,
"burst": 20 "burst": 20
} },
"ask": "https://example.com"
} }
}, },
"disable_ocsp_stapling": true "disable_ocsp_stapling": true
@@ -78,11 +78,11 @@
} }
], ],
"on_demand": { "on_demand": {
"ask": "https://example.com",
"rate_limit": { "rate_limit": {
"interval": 30000000000, "interval": 30000000000,
"burst": 20 "burst": 20
} },
"ask": "https://example.com"
}, },
"ocsp_interval": 172800000000000, "ocsp_interval": 172800000000000,
"renew_interval": 86400000000000, "renew_interval": 86400000000000,
@@ -71,11 +71,11 @@
} }
], ],
"on_demand": { "on_demand": {
"ask": "https://example.com",
"rate_limit": { "rate_limit": {
"interval": 30000000000, "interval": 30000000000,
"burst": 20 "burst": 20
} },
"ask": "https://example.com"
} }
} }
} }
@@ -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,25 +0,0 @@
{
persist_config off
}
:8881 {
}
----------
{
"admin": {
"config": {
"persist": false
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
]
}
}
}
}
}
@@ -165,4 +165,4 @@ acme-bar.example.com {
} }
} }
} }
} }
@@ -11,13 +11,9 @@
idle 30s idle 30s
} }
max_header_size 100MB max_header_size 100MB
enable_full_duplex
log_credentials log_credentials
protocols h1 h2 h2c h3 protocols h1 h2 h2c h3
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
} }
} }
@@ -46,7 +42,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,22 +55,6 @@ foo.com {
} }
], ],
"strict_sni_host": true, "strict_sni_host": true,
"trusted_proxies": {
"ranges": [
"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": { "logs": {
"should_log_credentials": true "should_log_credentials": true
}, },
@@ -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"
}
]
}
]
}
]
}
]
}
]
}
]
}
}
}
}
}
@@ -17,8 +17,6 @@
+Link "Foo" +Link "Foo"
+Link "Bar" +Link "Bar"
} }
header >Set Defer
header >Replace Deferred Replacement
} }
---------- ----------
{ {
@@ -138,31 +136,6 @@
] ]
} }
} }
},
{
"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"
}
]
}
]
}
}
}
}
}
@@ -99,7 +99,7 @@ http://localhost:2020 {
}, },
"logs": { "logs": {
"logger_names": { "logger_names": {
"localhost": "" "localhost:2020": ""
}, },
"skip_unmapped_hosts": true "skip_unmapped_hosts": true
} }
@@ -1,109 +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
}
}
example.com:8443 {
log {
output file /port.txt
}
}
----------
{
"logging": {
"logs": {
"default": {
"exclude": [
"http.log.access.log0",
"http.log.access.log1",
"http.log.access.log2"
]
},
"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"
]
},
"log2": {
"writer": {
"filename": "/port.txt",
"output": "file"
},
"include": [
"http.log.access.log2"
]
}
}
},
"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"
}
}
},
"srv1": {
"listen": [
":8443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
],
"logs": {
"logger_names": {
"example.com": "log2"
}
}
}
}
}
}
}
@@ -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": "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": "foo"
}
}
}
}
}
}
}
@@ -100,16 +100,16 @@ vars {
], ],
"source": "{http.request.host}" "source": "{http.request.host}"
}, },
{
"foo": "bar",
"handler": "vars"
},
{ {
"abc": true, "abc": true,
"def": 1, "def": 1,
"ghi": 2.3, "ghi": 2.3,
"handler": "vars", "handler": "vars",
"jkl": "mn op" "jkl": "mn op"
},
{
"foo": "bar",
"handler": "vars"
} }
] ]
} }
@@ -43,9 +43,6 @@
@matcher11 remote_ip private_ranges @matcher11 remote_ip private_ranges
respond @matcher11 "remote_ip matcher with private ranges" respond @matcher11 "remote_ip matcher with private ranges"
@matcher12 client_ip private_ranges
respond @matcher12 "client_ip matcher with private ranges"
} }
---------- ----------
{ {
@@ -253,28 +250,6 @@
"handler": "static_response" "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"
}
]
} }
] ]
} }
@@ -74,7 +74,6 @@ route {
] ]
}, },
{ {
"group": "group0",
"handle": [ "handle": [
{ {
"handler": "rewrite", "handler": "rewrite",
@@ -130,4 +129,4 @@ route {
} }
} }
} }
} }
@@ -1,100 +0,0 @@
*.sandbox.localhost {
@sandboxPort {
header_regexp first_label Host ^([0-9]{3})\.sandbox\.
}
handle @sandboxPort {
reverse_proxy {re.first_label.1}
}
handle {
redir {scheme}://application.localhost
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"*.sandbox.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "{http.regexp.first_label.1}"
}
]
}
]
}
]
}
],
"match": [
{
"header_regexp": {
"Host": {
"name": "first_label",
"pattern": "^([0-9]{3})\\.sandbox\\."
}
}
}
]
},
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.scheme}://application.localhost"
]
},
"status_code": 302
}
]
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -1,100 +0,0 @@
*.sandbox.localhost {
@sandboxPort {
header_regexp port Host ^([0-9]{3})\.sandbox\.
}
handle @sandboxPort {
reverse_proxy app:6{re.port.1}
}
handle {
redir {scheme}://application.localhost
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"*.sandbox.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "app:6{http.regexp.port.1}"
}
]
}
]
}
]
}
],
"match": [
{
"header_regexp": {
"Host": {
"name": "port",
"pattern": "^([0-9]{3})\\.sandbox\\."
}
}
}
]
},
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.scheme}://application.localhost"
]
},
"status_code": 302
}
]
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -1,100 +0,0 @@
*.sandbox.localhost {
@sandboxPort {
header_regexp port Host ^([0-9]{3})\.sandbox\.
}
handle @sandboxPort {
reverse_proxy app:{re.port.1}
}
handle {
redir {scheme}://application.localhost
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"*.sandbox.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "app:{http.regexp.port.1}"
}
]
}
]
}
]
}
],
"match": [
{
"header_regexp": {
"Host": {
"name": "port",
"pattern": "^([0-9]{3})\\.sandbox\\."
}
}
}
]
},
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.scheme}://application.localhost"
]
},
"status_code": 302
}
]
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -1,58 +0,0 @@
https://example.com {
reverse_proxy https://localhost:54321 {
request_buffers unlimited
response_buffers unlimited
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"request_buffers": -1,
"response_buffers": -1,
"transport": {
"protocol": "http",
"tls": {}
},
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -11,7 +11,6 @@
resolvers 8.8.8.8 8.8.4.4 resolvers 8.8.8.8 8.8.4.4
dial_timeout 2s dial_timeout 2s
dial_fallback_delay 300ms dial_fallback_delay 300ms
versions ipv6
} }
} }
} }
@@ -67,10 +66,7 @@
"8.8.4.4" "8.8.4.4"
] ]
}, },
"source": "a", "source": "a"
"versions": {
"ipv6": true
}
}, },
"handler": "reverse_proxy" "handler": "reverse_proxy"
} }
@@ -117,4 +113,4 @@
} }
} }
} }
} }
@@ -6,9 +6,6 @@ reverse_proxy 127.0.0.1:65535 {
X-Header-Key 95ca39e3cbe7 X-Header-Key 95ca39e3cbe7
X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO
X-Empty-Value X-Empty-Value
Same-Key 1
Same-Key 2
X-System-Hostname {system.hostname}
} }
health_uri /health health_uri /health
} }
@@ -32,10 +29,6 @@ reverse_proxy 127.0.0.1:65535 {
"Host": [ "Host": [
"example.com" "example.com"
], ],
"Same-Key": [
"1",
"2"
],
"X-Empty-Value": [ "X-Empty-Value": [
"" ""
], ],
@@ -45,9 +38,6 @@ reverse_proxy 127.0.0.1:65535 {
"X-Header-Keys": [ "X-Header-Keys": [
"VbG4NZwWnipo", "VbG4NZwWnipo",
"335Q9/MhqcNU3s2TO" "335Q9/MhqcNU3s2TO"
],
"X-System-Hostname": [
"{system.hostname}"
] ]
}, },
"uri": "/health" "uri": "/health"
@@ -1,71 +0,0 @@
:8884
reverse_proxy 127.0.0.1:65535 127.0.0.1:35535 {
lb_policy weighted_round_robin 10 1
lb_retries 5
lb_try_duration 10s
lb_try_interval 500ms
lb_retry_match {
path /foo*
method POST
}
lb_retry_match path /bar*
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"load_balancing": {
"retries": 5,
"retry_match": [
{
"method": [
"POST"
],
"path": [
"/foo*"
]
},
{
"path": [
"/bar*"
]
}
],
"selection_policy": {
"policy": "weighted_round_robin",
"weights": [
10,
1
]
},
"try_duration": 10000000000,
"try_interval": 500000000
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
},
{
"dial": "127.0.0.1:35535"
}
]
}
]
}
]
}
}
}
}
}
@@ -1,3 +1,4 @@
https://example.com { https://example.com {
reverse_proxy /path https://localhost:54321 { reverse_proxy /path https://localhost:54321 {
header_up Host {upstream_hostport} header_up Host {upstream_hostport}
@@ -6,7 +7,7 @@ https://example.com {
method GET method GET
rewrite /rewritten?uri={uri} rewrite /rewritten?uri={uri}
request_buffers 4KB buffer_requests
transport http { transport http {
read_buffer 10MB read_buffer 10MB
@@ -23,12 +24,13 @@ https://example.com {
max_conns_per_host 5 max_conns_per_host 5
keepalive_idle_conns_per_host 2 keepalive_idle_conns_per_host 2
keepalive_interval 30s keepalive_interval 30s
tls_renegotiation freely tls_renegotiation freely
tls_except_ports 8181 8182 tls_except_ports 8181 8182
} }
} }
} }
---------- ----------
{ {
"apps": { "apps": {
@@ -54,6 +56,7 @@ https://example.com {
{ {
"handle": [ "handle": [
{ {
"buffer_requests": true,
"handler": "reverse_proxy", "handler": "reverse_proxy",
"headers": { "headers": {
"request": { "request": {
@@ -67,7 +70,6 @@ https://example.com {
} }
} }
}, },
"request_buffers": 4000,
"rewrite": { "rewrite": {
"method": "GET", "method": "GET",
"uri": "/rewritten?uri={http.request.uri}" "uri": "/rewritten?uri={http.request.uri}"
@@ -1,67 +0,0 @@
:8884 {
# Port range
reverse_proxy localhost:8001-8002
# Port range with placeholder
reverse_proxy {host}:8001-8002
# Port range with scheme
reverse_proxy https://localhost:8001-8002
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "localhost:8001"
},
{
"dial": "localhost:8002"
}
]
},
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "{http.request.host}:8001"
},
{
"dial": "{http.request.host}:8002"
}
]
},
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http",
"tls": {}
},
"upstreams": [
{
"dial": "localhost:8001"
},
{
"dial": "localhost:8002"
}
]
}
]
}
]
}
}
}
}
}
@@ -1,77 +0,0 @@
{
servers :443 {
name https
}
servers :8000 {
name app1
}
servers :8001 {
name app2
}
servers 123.123.123.123:8002 {
name bind-server
}
}
example.com {
}
:8000 {
}
:8001, :8002 {
}
:8002 {
bind 123.123.123.123 222.222.222.222
}
----------
{
"apps": {
"http": {
"servers": {
"app1": {
"listen": [
":8000"
]
},
"app2": {
"listen": [
":8001"
]
},
"bind-server": {
"listen": [
"123.123.123.123:8002",
"222.222.222.222:8002"
]
},
"https": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
},
"srv4": {
"listen": [
":8002"
]
}
}
}
}
}
@@ -1,14 +1,11 @@
*.example.com { *.example.com {
@foo host foo.example.com @foo host foo.example.com
handle @foo { handle @foo {
handle_path /strip { handle_path /strip* {
respond "this should be first" respond "this should be first"
} }
handle_path /strip* {
respond "this should be second"
}
handle { handle {
respond "this should be last" respond "this should be second"
} }
} }
handle { handle {
@@ -38,13 +35,13 @@
"handler": "subroute", "handler": "subroute",
"routes": [ "routes": [
{ {
"group": "group6", "group": "group5",
"handle": [ "handle": [
{ {
"handler": "subroute", "handler": "subroute",
"routes": [ "routes": [
{ {
"group": "group3", "group": "group2",
"handle": [ "handle": [
{ {
"handler": "subroute", "handler": "subroute",
@@ -68,39 +65,6 @@
] ]
} }
], ],
"match": [
{
"path": [
"/strip"
]
}
]
},
{
"group": "group3",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "rewrite",
"strip_path_prefix": "/strip"
}
]
},
{
"handle": [
{
"body": "this should be second",
"handler": "static_response"
}
]
}
]
}
],
"match": [ "match": [
{ {
"path": [ "path": [
@@ -110,7 +74,7 @@
] ]
}, },
{ {
"group": "group3", "group": "group2",
"handle": [ "handle": [
{ {
"handler": "subroute", "handler": "subroute",
@@ -118,7 +82,7 @@
{ {
"handle": [ "handle": [
{ {
"body": "this should be last", "body": "this should be second",
"handler": "static_response" "handler": "static_response"
} }
] ]
@@ -139,7 +103,7 @@
] ]
}, },
{ {
"group": "group6", "group": "group5",
"handle": [ "handle": [
{ {
"handler": "subroute", "handler": "subroute",
@@ -1,8 +1,7 @@
:80 :80
vars /foobar foo last vars /foobar foo last
vars /foo foo middle-last vars /foo foo middle
vars /foo* foo middle-first
vars * foo first vars * foo first
---------- ----------
{ {
@@ -22,21 +21,6 @@ vars * foo first
} }
] ]
}, },
{
"match": [
{
"path": [
"/foo*"
]
}
],
"handle": [
{
"foo": "middle-first",
"handler": "vars"
}
]
},
{ {
"match": [ "match": [
{ {
@@ -47,7 +31,7 @@ vars * foo first
], ],
"handle": [ "handle": [
{ {
"foo": "middle-last", "foo": "middle",
"handler": "vars" "handler": "vars"
} }
] ]
@@ -1,67 +0,0 @@
# example from https://caddy.community/t/21415
a.com {
tls {
get_certificate http http://foo.com/get
}
}
b.com {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"b.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"a.com"
],
"get_certificate": [
{
"url": "http://foo.com/get",
"via": "http"
}
]
},
{
"subjects": [
"b.com"
]
}
]
}
}
}
}
@@ -1,76 +0,0 @@
localhost
respond "hello from localhost"
tls {
dns_ttl 5m10s
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"localhost"
],
"issuers": [
{
"challenges": {
"dns": {
"ttl": 310000000000
}
},
"module": "acme"
},
{
"challenges": {
"dns": {
"ttl": 310000000000
}
},
"module": "zerossl"
}
]
}
]
}
}
}
}
@@ -1,81 +0,0 @@
localhost
respond "hello from localhost"
tls {
issuer acme {
dns_ttl 5m10s
}
issuer zerossl {
dns_ttl 10m20s
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"localhost"
],
"issuers": [
{
"challenges": {
"dns": {
"ttl": 310000000000
}
},
"module": "acme"
},
{
"challenges": {
"dns": {
"ttl": 620000000000
}
},
"module": "zerossl"
}
]
}
]
}
}
}
}
@@ -1,79 +0,0 @@
localhost
respond "hello from localhost"
tls {
propagation_delay 5m10s
propagation_timeout 10m20s
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"localhost"
],
"issuers": [
{
"challenges": {
"dns": {
"propagation_delay": 310000000000,
"propagation_timeout": 620000000000
}
},
"module": "acme"
},
{
"challenges": {
"dns": {
"propagation_delay": 310000000000,
"propagation_timeout": 620000000000
}
},
"module": "zerossl"
}
]
}
]
}
}
}
}
-349
View File
@@ -135,352 +135,3 @@ func TestReplIndex(t *testing.T) {
// act and assert // act and assert
tester.AssertGetResponse("http://localhost:9080/", 200, "") tester.AssertGetResponse("http://localhost:9080/", 200, "")
} }
func TestInvalidPrefix(t *testing.T) {
type testCase struct {
config, expectedError string
}
failureCases := []testCase{
{
config: `wss://localhost`,
expectedError: `the scheme wss:// is only supported in browsers; use https:// instead`,
},
{
config: `ws://localhost`,
expectedError: `the scheme ws:// is only supported in browsers; use http:// instead`,
},
{
config: `someInvalidPrefix://localhost`,
expectedError: "unsupported URL scheme someinvalidprefix://",
},
{
config: `h2c://localhost`,
expectedError: `unsupported URL scheme h2c://`,
},
{
config: `localhost, wss://localhost`,
expectedError: `the scheme wss:// is only supported in browsers; use https:// instead`,
},
{
config: `localhost {
reverse_proxy ws://localhost"
}`,
expectedError: `the scheme ws:// is only supported in browsers; use http:// instead`,
},
{
config: `localhost {
reverse_proxy someInvalidPrefix://localhost"
}`,
expectedError: `unsupported URL scheme someinvalidprefix://`,
},
}
for _, failureCase := range failureCases {
caddytest.AssertLoadError(t, failureCase.config, "caddyfile", failureCase.expectedError)
}
}
func TestValidPrefix(t *testing.T) {
type testCase struct {
rawConfig, expectedResponse string
}
successCases := []testCase{
{
"localhost",
`{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
]
}
}
}
}
}`,
},
{
"https://localhost",
`{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
]
}
}
}
}
}`,
},
{
"http://localhost",
`{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
]
}
}
}
}
}`,
},
{
`localhost {
reverse_proxy http://localhost:3000
}`,
`{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "localhost:3000"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}`,
},
{
`localhost {
reverse_proxy https://localhost:3000
}`,
`{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http",
"tls": {}
},
"upstreams": [
{
"dial": "localhost:3000"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}`,
},
{
`localhost {
reverse_proxy h2c://localhost:3000
}`,
`{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http",
"versions": [
"h2c",
"2"
]
},
"upstreams": [
{
"dial": "localhost:3000"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}`,
},
{
`localhost {
reverse_proxy localhost:3000
}`,
`{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "localhost:3000"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}`,
},
}
for _, successCase := range successCases {
caddytest.AssertAdapt(t, successCase.rawConfig, "caddyfile", successCase.expectedResponse)
}
}
-94
View File
@@ -1,94 +0,0 @@
package integration
import (
"bytes"
"fmt"
"math/rand"
"net"
"net/http"
"strings"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddytest.Tester {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %s", err)
}
mux := http.NewServeMux()
mux.Handle("/", handlerFunc)
srv := &http.Server{
Handler: mux,
}
go srv.Serve(l)
t.Cleanup(func() {
_ = srv.Close()
_ = l.Close()
})
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
local_certs
servers :9443 {
listener_wrappers {
http_redirect
tls
}
}
}
localhost {
reverse_proxy %s
}
`, l.Addr().String()), "caddyfile")
return tester
}
func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) {
const uploadSize = (1024 * 1024) + 1 // 1 MB + 1 byte
// 1 more than an MB
body := make([]byte, uploadSize)
rand.New(rand.NewSource(0)).Read(body)
tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) {
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(request.Body)
if err != nil {
t.Fatalf("failed to read body: %s", err)
}
if !bytes.Equal(buf.Bytes(), body) {
t.Fatalf("body not the same")
}
writer.WriteHeader(http.StatusNoContent)
})
resp, err := tester.Client.Post("https://localhost:9443", "application/octet-stream", bytes.NewReader(body))
if err != nil {
t.Fatalf("failed to post: %s", err)
}
if resp.StatusCode != http.StatusNoContent {
t.Fatalf("unexpected status: %d != %d", resp.StatusCode, http.StatusNoContent)
}
}
func TestLargeHttpRequest(t *testing.T) {
tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) {
t.Fatal("not supposed to handle a request")
})
// We never read the body in any way, set an extra long header instead.
req, _ := http.NewRequest("POST", "http://localhost:9443", nil)
req.Header.Set("Long-Header", strings.Repeat("X", 1024*1024))
_, err := tester.Client.Do(req)
if err == nil {
t.Fatal("not supposed to succeed")
}
}
-101
View File
@@ -1,101 +0,0 @@
package integration
import (
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestLeafCertLifetimeLessThanIntermediate(t *testing.T) {
caddytest.AssertLoadError(t, `
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "internal",
"handler": "acme_server",
"lifetime": 604800000000000
}
]
}
]
}
]
}
]
}
}
},
"pki": {
"certificate_authorities": {
"internal": {
"install_trust": false,
"intermediate_lifetime": 604800000000000,
"name": "Internal CA"
}
}
}
}
}
`, "json", "certificate lifetime (168h0m0s) should be less than intermediate certificate lifetime (168h0m0s)")
}
func TestIntermediateLifetimeLessThanRoot(t *testing.T) {
caddytest.AssertLoadError(t, `
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "internal",
"handler": "acme_server",
"lifetime": 2592000000000000
}
]
}
]
}
]
}
]
}
}
},
"pki": {
"certificate_authorities": {
"internal": {
"install_trust": false,
"intermediate_lifetime": 311040000000000000,
"name": "Internal CA"
}
}
}
}
}
`, "json", "intermediate certificate lifetime must be less than root certificate lifetime (86400h0m0s)")
}
+160 -73
View File
@@ -22,38 +22,80 @@ func TestSRVReverseProxy(t *testing.T) {
}, },
"apps": { "apps": {
"pki": { "pki": {
"certificate_authorities": { "certificate_authorities" : {
"local": { "local" : {
"install_trust": false "install_trust": false
} }
} }
}, },
"http": { "http": {
"grace_period": 1, "grace_period": 1,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
":18080" ":8080"
], ],
"routes": [ "routes": [
{ {
"handle": [ "handle": [
{ {
"handler": "reverse_proxy", "handler": "reverse_proxy",
"dynamic_upstreams": { "upstreams": [
"source": "srv", {
"name": "srv.host.service.consul" "lookup_srv": "srv.host.service.consul"
} }
}
]
}
] ]
} }
} ]
}
]
}
} }
}
} }
} }
`, "json") `, "json")
}
func TestSRVWithDial(t *testing.T) {
caddytest.AssertLoadError(t, `
{
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "tcp/address.to.upstream:80",
"lookup_srv": "srv.host.service.consul"
}
]
}
]
}
]
}
}
}
}
}
`, "json", `upstream: specifying dial address is incompatible with lookup_srv: 0: {\"dial\": \"tcp/address.to.upstream:80\", \"lookup_srv\": \"srv.host.service.consul\"}`)
} }
func TestDialWithPlaceholderUnix(t *testing.T) { func TestDialWithPlaceholderUnix(t *testing.T) {
@@ -96,41 +138,41 @@ func TestDialWithPlaceholderUnix(t *testing.T) {
}, },
"apps": { "apps": {
"pki": { "pki": {
"certificate_authorities": { "certificate_authorities" : {
"local": { "local" : {
"install_trust": false "install_trust": false
} }
} }
}, },
"http": { "http": {
"grace_period": 1, "grace_period": 1,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
":18080" ":8080"
], ],
"routes": [ "routes": [
{ {
"handle": [ "handle": [
{ {
"handler": "reverse_proxy", "handler": "reverse_proxy",
"upstreams": [ "upstreams": [
{ {
"dial": "unix/{http.request.header.X-Caddy-Upstream-Dial}" "dial": "unix/{http.request.header.X-Caddy-Upstream-Dial}"
} }
]
}
]
}
] ]
} }
} ]
} }
]
}
}
}
} }
} }
`, "json") `, "json")
req, err := http.NewRequest(http.MethodGet, "http://localhost:18080", nil) req, err := http.NewRequest(http.MethodGet, "http://localhost:8080", nil)
if err != nil { if err != nil {
t.Fail() t.Fail()
return return
@@ -148,18 +190,18 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
}, },
"apps": { "apps": {
"pki": { "pki": {
"certificate_authorities": { "certificate_authorities" : {
"local": { "local" : {
"install_trust": false "install_trust": false
} }
} }
}, },
"http": { "http": {
"grace_period": 1, "grace_period": 1,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
":18080" ":8080"
], ],
"routes": [ "routes": [
{ {
@@ -222,14 +264,14 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
} }
} }
} }
`, "json") `, "json")
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080", nil) req, err := http.NewRequest(http.MethodGet, "http://localhost:9080", nil)
if err != nil { if err != nil {
t.Fail() t.Fail()
return return
} }
req.Header.Set("X-Caddy-Upstream-Dial", "localhost:18080") req.Header.Set("X-Caddy-Upstream-Dial", "localhost:8080")
tester.AssertResponse(req, 200, "Hello, World!") tester.AssertResponse(req, 200, "Hello, World!")
} }
@@ -242,18 +284,18 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
}, },
"apps": { "apps": {
"pki": { "pki": {
"certificate_authorities": { "certificate_authorities" : {
"local": { "local" : {
"install_trust": false "install_trust": false
} }
} }
}, },
"http": { "http": {
"grace_period": 1, "grace_period": 1,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
":18080" ":8080"
], ],
"routes": [ "routes": [
{ {
@@ -298,7 +340,7 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
"handler": "reverse_proxy", "handler": "reverse_proxy",
"upstreams": [ "upstreams": [
{ {
"dial": "tcp/{http.request.header.X-Caddy-Upstream-Dial}:18080" "dial": "tcp/{http.request.header.X-Caddy-Upstream-Dial}:8080"
} }
] ]
} }
@@ -316,7 +358,7 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
} }
} }
} }
`, "json") `, "json")
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080", nil) req, err := http.NewRequest(http.MethodGet, "http://localhost:9080", nil)
if err != nil { if err != nil {
@@ -327,6 +369,51 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
tester.AssertResponse(req, 200, "Hello, World!") tester.AssertResponse(req, 200, "Hello, World!")
} }
func TestSRVWithActiveHealthcheck(t *testing.T) {
caddytest.AssertLoadError(t, `
{
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"health_checks": {
"active": {
"path": "/ok"
}
},
"upstreams": [
{
"lookup_srv": "srv.host.service.consul"
}
]
}
]
}
]
}
}
}
}
}
`, "json", `upstream: lookup_srv is incompatible with active health checks: 0: {\"dial\": \"\", \"lookup_srv\": \"srv.host.service.consul\"}`)
}
func TestReverseProxyHealthCheck(t *testing.T) { func TestReverseProxyHealthCheck(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
@@ -353,7 +440,7 @@ func TestReverseProxyHealthCheck(t *testing.T) {
health_timeout 100ms health_timeout 100ms
} }
} }
`, "caddyfile") `, "caddyfile")
time.Sleep(100 * time.Millisecond) // TODO: for some reason this test seems particularly flaky, getting 503 when it should be 200, unless we wait time.Sleep(100 * time.Millisecond) // TODO: for some reason this test seems particularly flaky, getting 503 when it should be 200, unless we wait
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!") tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
+3 -1
View File
@@ -176,7 +176,9 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
w.Header().Set("Cache-Control", "no-store") w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(200) w.WriteHeader(200)
http.NewResponseController(w).Flush() if f, ok := w.(http.Flusher); ok {
f.Flush()
}
buf := make([]byte, 4*1024) buf := make([]byte, 4*1024)
+3
View File
@@ -0,0 +1,3 @@
{
admin localhost:2999
}
+1 -1
View File
@@ -1 +1 @@
respond "'I am {args[0]}', hears {args[1]}" respond "'I am {args.0}', hears {args.1}"
+4 -4
View File
@@ -19,10 +19,10 @@
// There is no need to modify the Caddy source code to customize your // There is no need to modify the Caddy source code to customize your
// builds. You can easily build a custom Caddy with these simple steps: // builds. You can easily build a custom Caddy with these simple steps:
// //
// 1. Copy this file (main.go) into a new folder // 1. Copy this file (main.go) into a new folder
// 2. Edit the imports below to include the modules you want plugged in // 2. Edit the imports below to include the modules you want plugged in
// 3. Run `go mod init caddy` // 3. Run `go mod init caddy`
// 4. Run `go install` or `go build` - you now have a custom binary! // 4. Run `go install` or `go build` - you now have a custom binary!
// //
// Or you can use xcaddy which does it all for you as a command: // Or you can use xcaddy which does it all for you as a command:
// https://github.com/caddyserver/xcaddy // https://github.com/caddyserver/xcaddy
+8 -45
View File
@@ -1,11 +1,7 @@
package caddycmd package caddycmd
import ( import (
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/caddyserver/caddy/v2"
) )
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@@ -99,59 +95,26 @@ https://caddyserver.com/docs/running
// kind of annoying to have all the help text printed out if // kind of annoying to have all the help text printed out if
// caddy has an error provisioning its modules, for instance... // caddy has an error provisioning its modules, for instance...
SilenceUsage: true, SilenceUsage: true,
Version: onlyVersionText(),
} }
const fullDocsFooter = `Full documentation is available at: const fullDocsFooter = `Full documentation is available at:
https://caddyserver.com/docs/command-line` https://caddyserver.com/docs/command-line`
func init() { func init() {
rootCmd.SetVersionTemplate("{{.Version}}\n") rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter)
rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
} }
func onlyVersionText() string { func caddyCmdToCoral(caddyCmd Command) *cobra.Command {
_, f := caddy.Version()
return f
}
func caddyCmdToCobra(caddyCmd Command) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: caddyCmd.Name, Use: caddyCmd.Name,
Short: caddyCmd.Short, Short: caddyCmd.Short,
Long: caddyCmd.Long, Long: caddyCmd.Long,
RunE: func(cmd *cobra.Command, _ []string) error {
fls := cmd.Flags()
_, err := caddyCmd.Func(Flags{fls})
return err
},
} }
if caddyCmd.CobraFunc != nil { cmd.Flags().AddGoFlagSet(caddyCmd.Flags)
caddyCmd.CobraFunc(cmd)
} else {
cmd.RunE = WrapCommandFuncForCobra(caddyCmd.Func)
cmd.Flags().AddGoFlagSet(caddyCmd.Flags)
}
return cmd return cmd
} }
// WrapCommandFuncForCobra wraps a Caddy CommandFunc for use
// in a cobra command's RunE field.
func WrapCommandFuncForCobra(f CommandFunc) func(cmd *cobra.Command, _ []string) error {
return func(cmd *cobra.Command, _ []string) error {
status, err := f(Flags{cmd.Flags()})
if status > 1 {
cmd.SilenceErrors = true
return &exitError{ExitCode: status, Err: err}
}
return err
}
}
// exitError carries the exit code from CommandFunc to Main()
type exitError struct {
ExitCode int
Err error
}
func (e *exitError) Error() string {
if e.Err == nil {
return fmt.Sprintf("exiting with status %d", e.ExitCode)
}
return e.Err.Error()
}
+113 -206
View File
@@ -22,7 +22,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"log" "log"
"net" "net"
"net/http" "net/http"
@@ -33,27 +32,18 @@ import (
"strings" "strings"
"github.com/aryann/difflib" "github.com/aryann/difflib"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/internal" "go.uber.org/zap"
) )
func cmdStart(fl Flags) (int, error) { func cmdStart(fl Flags) (int, error) {
configFlag := fl.String("config") startCmdConfigFlag := fl.String("config")
configAdapterFlag := fl.String("adapter") startCmdConfigAdapterFlag := fl.String("adapter")
pidfileFlag := fl.String("pidfile") startCmdPidfileFlag := fl.String("pidfile")
watchFlag := fl.Bool("watch") startCmdWatchFlag := fl.Bool("watch")
startCmdEnvfileFlag := fl.String("envfile")
var err error
var envfileFlag []string
envfileFlag, err = fl.GetStringSlice("envfile")
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading envfile flag: %v", err)
}
// open a listener to which the child process will connect when // open a listener to which the child process will connect when
// it is ready to confirm that it has successfully started // it is ready to confirm that it has successfully started
@@ -74,23 +64,22 @@ func cmdStart(fl Flags) (int, error) {
// sure by giving it some random bytes and having it echo // sure by giving it some random bytes and having it echo
// them back to us) // them back to us)
cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String()) cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
if configFlag != "" { if startCmdConfigFlag != "" {
cmd.Args = append(cmd.Args, "--config", configFlag) cmd.Args = append(cmd.Args, "--config", startCmdConfigFlag)
} }
if startCmdEnvfileFlag != "" {
for _, envfile := range envfileFlag { cmd.Args = append(cmd.Args, "--envfile", startCmdEnvfileFlag)
cmd.Args = append(cmd.Args, "--envfile", envfile)
} }
if configAdapterFlag != "" { if startCmdConfigAdapterFlag != "" {
cmd.Args = append(cmd.Args, "--adapter", configAdapterFlag) cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag)
} }
if watchFlag { if startCmdWatchFlag {
cmd.Args = append(cmd.Args, "--watch") cmd.Args = append(cmd.Args, "--watch")
} }
if pidfileFlag != "" { if startCmdPidfileFlag != "" {
cmd.Args = append(cmd.Args, "--pidfile", pidfileFlag) cmd.Args = append(cmd.Args, "--pidfile", startCmdPidfileFlag)
} }
stdinPipe, err := cmd.StdinPipe() stdinpipe, err := cmd.StdinPipe()
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("creating stdin pipe: %v", err) fmt.Errorf("creating stdin pipe: %v", err)
@@ -102,8 +91,7 @@ func cmdStart(fl Flags) (int, error) {
expect := make([]byte, 32) expect := make([]byte, 32)
_, err = rand.Read(expect) _, err = rand.Read(expect)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup, fmt.Errorf("generating random confirmation bytes: %v", err)
fmt.Errorf("generating random confirmation bytes: %v", err)
} }
// begin writing the confirmation bytes to the child's // begin writing the confirmation bytes to the child's
@@ -111,15 +99,14 @@ func cmdStart(fl Flags) (int, error) {
// started yet, and writing synchronously would result // started yet, and writing synchronously would result
// in a deadlock // in a deadlock
go func() { go func() {
_, _ = stdinPipe.Write(expect) _, _ = stdinpipe.Write(expect)
stdinPipe.Close() stdinpipe.Close()
}() }()
// start the process // start the process
err = cmd.Start() err = cmd.Start()
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy process: %v", err)
fmt.Errorf("starting caddy process: %v", err)
} }
// there are two ways we know we're done: either // there are two ways we know we're done: either
@@ -167,37 +154,41 @@ func cmdStart(fl Flags) (int, error) {
func cmdRun(fl Flags) (int, error) { func cmdRun(fl Flags) (int, error) {
caddy.TrapSignals() caddy.TrapSignals()
configFlag := fl.String("config") runCmdConfigFlag := fl.String("config")
configAdapterFlag := fl.String("adapter") runCmdConfigAdapterFlag := fl.String("adapter")
resumeFlag := fl.Bool("resume") runCmdResumeFlag := fl.Bool("resume")
printEnvFlag := fl.Bool("environ") runCmdLoadEnvfileFlag := fl.String("envfile")
watchFlag := fl.Bool("watch") runCmdPrintEnvFlag := fl.Bool("environ")
pidfileFlag := fl.String("pidfile") runCmdWatchFlag := fl.Bool("watch")
pingbackFlag := fl.String("pingback") runCmdPidfileFlag := fl.String("pidfile")
runCmdPingbackFlag := fl.String("pingback")
// load all additional envs as soon as possible // load all additional envs as soon as possible
err := handleEnvFileFlag(fl) if runCmdLoadEnvfileFlag != "" {
if err != nil { if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup,
fmt.Errorf("loading additional environment variables: %v", err)
}
} }
// if we are supposed to print the environment, do that first // if we are supposed to print the environment, do that first
if printEnvFlag { if runCmdPrintEnvFlag {
printEnvironment() printEnvironment()
} }
// load the config, depending on flags // load the config, depending on flags
var config []byte var config []byte
if resumeFlag { var err error
if runCmdResumeFlag {
config, err = os.ReadFile(caddy.ConfigAutosavePath) config, err = os.ReadFile(caddy.ConfigAutosavePath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
// not a bad error; just can't resume if autosave file doesn't exist // not a bad error; just can't resume if autosave file doesn't exist
caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath)) caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
resumeFlag = false runCmdResumeFlag = false
} else if err != nil { } else if err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} else { } else {
if configFlag == "" { if runCmdConfigFlag == "" {
caddy.Log().Info("resuming from last configuration", caddy.Log().Info("resuming from last configuration",
zap.String("autosave_file", caddy.ConfigAutosavePath)) zap.String("autosave_file", caddy.ConfigAutosavePath))
} else { } else {
@@ -210,23 +201,13 @@ func cmdRun(fl Flags) (int, error) {
} }
// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive // we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
var configFile string var configFile string
if !resumeFlag { if !runCmdResumeFlag {
config, configFile, err = LoadConfig(configFlag, configAdapterFlag) config, configFile, err = LoadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} }
} }
// create pidfile now, in case loading config takes a while (issue #5477)
if pidfileFlag != "" {
err := caddy.PIDFile(pidfileFlag)
if err != nil {
caddy.Log().Error("unable to write PID file",
zap.String("pidfile", pidfileFlag),
zap.Error(err))
}
}
// run the initial config // run the initial config
err = caddy.Load(config, true) err = caddy.Load(config, true)
if err != nil { if err != nil {
@@ -236,13 +217,13 @@ func cmdRun(fl Flags) (int, error) {
// if we are to report to another process the successful start // if we are to report to another process the successful start
// of the server, do so now by echoing back contents of stdin // of the server, do so now by echoing back contents of stdin
if pingbackFlag != "" { if runCmdPingbackFlag != "" {
confirmationBytes, err := io.ReadAll(os.Stdin) confirmationBytes, err := io.ReadAll(os.Stdin)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading confirmation bytes from stdin: %v", err) fmt.Errorf("reading confirmation bytes from stdin: %v", err)
} }
conn, err := net.Dial("tcp", pingbackFlag) conn, err := net.Dial("tcp", runCmdPingbackFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("dialing confirmation address: %v", err) fmt.Errorf("dialing confirmation address: %v", err)
@@ -251,14 +232,24 @@ func cmdRun(fl Flags) (int, error) {
_, err = conn.Write(confirmationBytes) _, err = conn.Write(confirmationBytes)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("writing confirmation bytes to %s: %v", pingbackFlag, err) fmt.Errorf("writing confirmation bytes to %s: %v", runCmdPingbackFlag, err)
} }
} }
// if enabled, reload config file automatically on changes // if enabled, reload config file automatically on changes
// (this better only be used in dev!) // (this better only be used in dev!)
if watchFlag { if runCmdWatchFlag {
go watchConfigFile(configFile, configAdapterFlag) go watchConfigFile(configFile, runCmdConfigAdapterFlag)
}
// create pidfile
if runCmdPidfileFlag != "" {
err := caddy.PIDFile(runCmdPidfileFlag)
if err != nil {
caddy.Log().Error("unable to write PID file",
zap.String("pidfile", runCmdPidfileFlag),
zap.Error(err))
}
} }
// warn if the environment does not provide enough information about the disk // warn if the environment does not provide enough information about the disk
@@ -284,11 +275,11 @@ func cmdRun(fl Flags) (int, error) {
} }
func cmdStop(fl Flags) (int, error) { func cmdStop(fl Flags) (int, error) {
addressFlag := fl.String("address") addrFlag := fl.String("address")
configFlag := fl.String("config") configFlag := fl.String("config")
configAdapterFlag := fl.String("adapter") configAdapterFlag := fl.String("adapter")
adminAddr, err := DetermineAdminAPIAddress(addressFlag, nil, configFlag, configAdapterFlag) adminAddr, err := DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
} }
@@ -306,7 +297,7 @@ func cmdStop(fl Flags) (int, error) {
func cmdReload(fl Flags) (int, error) { func cmdReload(fl Flags) (int, error) {
configFlag := fl.String("config") configFlag := fl.String("config")
configAdapterFlag := fl.String("adapter") configAdapterFlag := fl.String("adapter")
addressFlag := fl.String("address") addrFlag := fl.String("address")
forceFlag := fl.Bool("force") forceFlag := fl.Bool("force")
// get the config in caddy's native format // get the config in caddy's native format
@@ -318,7 +309,7 @@ func cmdReload(fl Flags) (int, error) {
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load") return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
} }
adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFlag, configAdapterFlag) adminAddr, err := DetermineAdminAPIAddress(addrFlag, config, configFlag, configAdapterFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
} }
@@ -420,60 +411,60 @@ func cmdListModules(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
func cmdEnviron(fl Flags) (int, error) { func cmdEnviron(_ Flags) (int, error) {
// load all additional envs as soon as possible
err := handleEnvFileFlag(fl)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
printEnvironment() printEnvironment()
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
func cmdAdaptConfig(fl Flags) (int, error) { func cmdAdaptConfig(fl Flags) (int, error) {
inputFlag := fl.String("config") adaptCmdInputFlag := fl.String("config")
adapterFlag := fl.String("adapter") adaptCmdAdapterFlag := fl.String("adapter")
prettyFlag := fl.Bool("pretty") adaptCmdPrettyFlag := fl.Bool("pretty")
validateFlag := fl.Bool("validate") adaptCmdValidateFlag := fl.Bool("validate")
var err error // if no input file was specified, try a default
inputFlag, err = configFileWithRespectToDefault(caddy.Log(), inputFlag) // Caddyfile if the Caddyfile adapter is plugged in
if err != nil { if adaptCmdInputFlag == "" && caddyconfig.GetAdapter("caddyfile") != nil {
return caddy.ExitCodeFailedStartup, err _, err := os.Stat("Caddyfile")
if err == nil {
// default Caddyfile exists
adaptCmdInputFlag = "Caddyfile"
caddy.Log().Info("using adjacent Caddyfile")
} else if !os.IsNotExist(err) {
// default Caddyfile exists, but error accessing it
return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing default Caddyfile: %v", err)
}
} }
// load all additional envs as soon as possible if adaptCmdInputFlag == "" {
err = handleEnvFileFlag(fl) return caddy.ExitCodeFailedStartup,
if err != nil { fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
return caddy.ExitCodeFailedStartup, err
} }
if adaptCmdAdapterFlag == "" {
if adapterFlag == "" {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)") fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)")
} }
cfgAdapter := caddyconfig.GetAdapter(adapterFlag) cfgAdapter := caddyconfig.GetAdapter(adaptCmdAdapterFlag)
if cfgAdapter == nil { if cfgAdapter == nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("unrecognized config adapter: %s", adapterFlag) fmt.Errorf("unrecognized config adapter: %s", adaptCmdAdapterFlag)
} }
input, err := os.ReadFile(inputFlag) input, err := os.ReadFile(adaptCmdInputFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading input file: %v", err) fmt.Errorf("reading input file: %v", err)
} }
opts := map[string]any{"filename": inputFlag} opts := map[string]any{"filename": adaptCmdInputFlag}
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts) adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} }
if prettyFlag { if adaptCmdPrettyFlag {
var prettyBuf bytes.Buffer var prettyBuf bytes.Buffer
err = json.Indent(&prettyBuf, adaptedConfig, "", "\t") err = json.Indent(&prettyBuf, adaptedConfig, "", "\t")
if err != nil { if err != nil {
@@ -491,15 +482,15 @@ func cmdAdaptConfig(fl Flags) (int, error) {
if warn.Directive != "" { if warn.Directive != "" {
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message) msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
} }
caddy.Log().Named(adapterFlag).Warn(msg, caddy.Log().Named(adaptCmdAdapterFlag).Warn(msg,
zap.String("file", warn.File), zap.String("file", warn.File),
zap.Int("line", warn.Line)) zap.Int("line", warn.Line))
} }
// validate output if requested // validate output if requested
if validateFlag { if adaptCmdValidateFlag {
var cfg *caddy.Config var cfg *caddy.Config
err = caddy.StrictUnmarshalJSON(adaptedConfig, &cfg) err = json.Unmarshal(adaptedConfig, &cfg)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err)
} }
@@ -513,33 +504,17 @@ func cmdAdaptConfig(fl Flags) (int, error) {
} }
func cmdValidateConfig(fl Flags) (int, error) { func cmdValidateConfig(fl Flags) (int, error) {
configFlag := fl.String("config") validateCmdConfigFlag := fl.String("config")
adapterFlag := fl.String("adapter") validateCmdAdapterFlag := fl.String("adapter")
// load all additional envs as soon as possible input, _, err := LoadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
err := handleEnvFileFlag(fl)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// use default config and ensure a config file is specified
configFlag, err = configFileWithRespectToDefault(caddy.Log(), configFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
if configFlag == "" {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
}
input, _, err := LoadConfig(configFlag, adapterFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} }
input = caddy.RemoveMetaFields(input) input = caddy.RemoveMetaFields(input)
var cfg *caddy.Config var cfg *caddy.Config
err = caddy.StrictUnmarshalJSON(input, &cfg) err = json.Unmarshal(input, &cfg)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err)
} }
@@ -555,13 +530,13 @@ func cmdValidateConfig(fl Flags) (int, error) {
} }
func cmdFmt(fl Flags) (int, error) { func cmdFmt(fl Flags) (int, error) {
configFile := fl.Arg(0) formatCmdConfigFile := fl.Arg(0)
if configFile == "" { if formatCmdConfigFile == "" {
configFile = "Caddyfile" formatCmdConfigFile = "Caddyfile"
} }
// as a special case, read from stdin if the file name is "-" // as a special case, read from stdin if the file name is "-"
if configFile == "-" { if formatCmdConfigFile == "-" {
input, err := io.ReadAll(os.Stdin) input, err := io.ReadAll(os.Stdin)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
@@ -571,7 +546,7 @@ func cmdFmt(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
input, err := os.ReadFile(configFile) input, err := os.ReadFile(formatCmdConfigFile)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading input file: %v", err) fmt.Errorf("reading input file: %v", err)
@@ -580,13 +555,10 @@ func cmdFmt(fl Flags) (int, error) {
output := caddyfile.Format(input) output := caddyfile.Format(input)
if fl.Bool("overwrite") { if fl.Bool("overwrite") {
if err := os.WriteFile(configFile, output, 0o600); err != nil { if err := os.WriteFile(formatCmdConfigFile, output, 0600); err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
} }
return caddy.ExitCodeSuccess, nil } else if fl.Bool("diff") {
}
if fl.Bool("diff") {
diff := difflib.Diff( diff := difflib.Diff(
strings.Split(string(input), "\n"), strings.Split(string(input), "\n"),
strings.Split(string(output), "\n")) strings.Split(string(output), "\n"))
@@ -604,35 +576,9 @@ func cmdFmt(fl Flags) (int, error) {
fmt.Print(string(output)) fmt.Print(string(output))
} }
if warning, diff := caddyfile.FormattingDifference(configFile, input); diff {
return caddy.ExitCodeFailedStartup, fmt.Errorf(`%s:%d: Caddyfile input is not formatted; Tip: use '--overwrite' to update your Caddyfile in-place instead of previewing it. Consult '--help' for more options`,
warning.File,
warning.Line,
)
}
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
// handleEnvFileFlag loads the environment variables from the given --envfile
// flag if specified. This should be called as early in the command function.
func handleEnvFileFlag(fl Flags) error {
var err error
var envfileFlag []string
envfileFlag, err = fl.GetStringSlice("envfile")
if err != nil {
return fmt.Errorf("reading envfile flag: %v", err)
}
for _, envfile := range envfileFlag {
if err := loadEnvFromFile(envfile); err != nil {
return fmt.Errorf("loading additional environment variables: %v", err)
}
}
return nil
}
// AdminAPIRequest makes an API request according to the CLI flags given, // AdminAPIRequest makes an API request according to the CLI flags given,
// with the given HTTP method and request URI. If body is non-nil, it will // with the given HTTP method and request URI. If body is non-nil, it will
// be assumed to be Content-Type application/json. The caller should close // be assumed to be Content-Type application/json. The caller should close
@@ -645,17 +591,7 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
} }
origin := "http://" + parsedAddr.JoinHostPort(0) origin := "http://" + parsedAddr.JoinHostPort(0)
if parsedAddr.IsUnixNetwork() { if parsedAddr.IsUnixNetwork() {
origin = "http://127.0.0.1" // bogus host is a hack so that http.NewRequest() is happy origin = "http://unixsocket" // hack so that http.NewRequest() is happy
// the unix address at this point might still contain the optional
// unix socket permissions, which are part of the address/host.
// those need to be removed first, as they aren't part of the
// resulting unix file path
addr, _, err := internal.SplitUnixSocketPermissionsBits(parsedAddr.Host)
if err != nil {
return nil, err
}
parsedAddr.Host = addr
} }
// form the request // form the request
@@ -664,24 +600,20 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
return nil, fmt.Errorf("making request: %v", err) return nil, fmt.Errorf("making request: %v", err)
} }
if parsedAddr.IsUnixNetwork() { if parsedAddr.IsUnixNetwork() {
// We used to conform to RFC 2616 Section 14.26 which requires // When listening on a unix socket, the admin endpoint doesn't
// an empty host header when there is no host, as is the case // accept any Host header because there is no host:port for
// with unix sockets. However, Go required a Host value so we // a unix socket's address. The server's host check is fairly
// used a hack of a space character as the host (it would see // strict for security reasons, so we don't allow just any
// the Host was non-empty, then trim the space later). As of // Host header. For unix sockets, the Host header must be
// Go 1.20.6 (July 2023), this hack no longer works. See: // empty. Unfortunately, Go makes it impossible to make HTTP
// https://github.com/golang/go/issues/60374 // requests with an empty Host header... except with this one
// See also the discussion here: // weird trick. (Hopefully they don't fix it. It's already
// https://github.com/golang/go/issues/61431 // hard enough to use HTTP over unix sockets.)
// //
// After that, we now require a Host value of either 127.0.0.1 // An equivalent curl command would be something like:
// or ::1 if one is set. Above I choose to use 127.0.0.1. Even // $ curl --unix-socket caddy.sock http:/:$REQUEST_URI
// though the value should be completely irrelevant (it could be req.URL.Host = " "
// "srldkjfsd"), if for some reason the Host *is* used, at least req.Host = ""
// we can have some reasonable assurance it will stay on the local
// machine and that browsers, if they ever allow access to unix
// sockets, can still enforce CORS, ensuring it is still coming
// from the local machine.
} else { } else {
req.Header.Set("Origin", origin) req.Header.Set("Origin", origin)
} }
@@ -770,31 +702,6 @@ func DetermineAdminAPIAddress(address string, config []byte, configFile, configA
return caddy.DefaultAdminListen, nil return caddy.DefaultAdminListen, nil
} }
// configFileWithRespectToDefault returns the filename to use for loading the config, based
// on whether a config file is already specified and a supported default config file exists.
func configFileWithRespectToDefault(logger *zap.Logger, configFile string) (string, error) {
const defaultCaddyfile = "Caddyfile"
// if no input file was specified, try a default Caddyfile if the Caddyfile adapter is plugged in
if configFile == "" && caddyconfig.GetAdapter("caddyfile") != nil {
_, err := os.Stat(defaultCaddyfile)
if err == nil {
// default Caddyfile exists
if logger != nil {
logger.Info("using adjacent Caddyfile")
}
return defaultCaddyfile, nil
}
if !errors.Is(err, fs.ErrNotExist) {
// problem checking
return configFile, fmt.Errorf("checking if default Caddyfile exists: %v", err)
}
}
// default config file does not exist or is irrelevant
return configFile, nil
}
type moduleInfo struct { type moduleInfo struct {
caddyModuleID string caddyModuleID string
goModule *debug.Module goModule *debug.Module
+158 -217
View File
@@ -21,10 +21,9 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/caddyserver/caddy/v2"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/cobra/doc" "github.com/spf13/cobra/doc"
"github.com/caddyserver/caddy/v2"
) )
// Command represents a subcommand. Name, Func, // Command represents a subcommand. Name, Func,
@@ -35,6 +34,12 @@ type Command struct {
// Required. // Required.
Name string Name string
// Func is a function that executes a subcommand using
// the parsed flags. It returns an exit code and any
// associated error.
// Required.
Func CommandFunc
// Usage is a brief message describing the syntax of // Usage is a brief message describing the syntax of
// the subcommand's flags and args. Use [] to indicate // the subcommand's flags and args. Use [] to indicate
// optional parameters and <> to enclose literal values // optional parameters and <> to enclose literal values
@@ -55,21 +60,7 @@ type Command struct {
Long string Long string
// Flags is the flagset for command. // Flags is the flagset for command.
// This is ignored if CobraFunc is set.
Flags *flag.FlagSet Flags *flag.FlagSet
// Func is a function that executes a subcommand using
// the parsed flags. It returns an exit code and any
// associated error.
// Required if CobraFunc is not set.
Func CommandFunc
// CobraFunc allows further configuration of the command
// via cobra's APIs. If this is set, then Func and Flags
// are ignored, with the assumption that they are set in
// this function. A caddycmd.WrapCommandFuncForCobra helper
// exists to simplify porting CommandFunc to Cobra's RunE.
CobraFunc func(*cobra.Command)
} }
// CommandFunc is a command's function. It runs the // CommandFunc is a command's function. It runs the
@@ -88,32 +79,34 @@ var commands = make(map[string]Command)
func init() { func init() {
RegisterCommand(Command{ RegisterCommand(Command{
Name: "start", Name: "start",
Func: cmdStart,
Usage: "[--config <path> [--adapter <name>]] [--envfile <path>] [--watch] [--pidfile <file>]", Usage: "[--config <path> [--adapter <name>]] [--envfile <path>] [--watch] [--pidfile <file>]",
Short: "Starts the Caddy process in the background and then returns", Short: "Starts the Caddy process in the background and then returns",
Long: ` Long: `
Starts the Caddy process, optionally bootstrapped with an initial config file. Starts the Caddy process, optionally bootstrapped with an initial config file.
This command unblocks after the server starts running or fails to run. This command unblocks after the server starts running or fails to run.
If --envfile is specified, an environment file with environment variables If --envfile is specified, an environment file with environment variables in
in the KEY=VALUE format will be loaded into the Caddy process. the KEY=VALUE format will be loaded into the Caddy process.
On Windows, the spawned child process will remain attached to the terminal, so On Windows, the spawned child process will remain attached to the terminal, so
closing the window will forcefully stop Caddy; to avoid forgetting this, try closing the window will forcefully stop Caddy; to avoid forgetting this, try
using 'caddy run' instead to keep it in the foreground. using 'caddy run' instead to keep it in the foreground.`,
`, Flags: func() *flag.FlagSet {
CobraFunc: func(cmd *cobra.Command) { fs := flag.NewFlagSet("start", flag.ExitOnError)
cmd.Flags().StringP("config", "c", "", "Configuration file") fs.String("config", "", "Configuration file")
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply") fs.String("envfile", "", "Environment file to load")
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load") fs.String("adapter", "", "Name of config adapter to apply")
cmd.Flags().BoolP("watch", "w", false, "Reload changed config file automatically") fs.String("pidfile", "", "Path of file to which to write process ID")
cmd.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID") fs.Bool("watch", false, "Reload changed config file automatically")
cmd.RunE = WrapCommandFuncForCobra(cmdStart) return fs
}, }(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "run", Name: "run",
Usage: "[--config <path> [--adapter <name>]] [--envfile <path>] [--environ] [--resume] [--watch] [--pidfile <file>]", Func: cmdRun,
Usage: "[--config <path> [--adapter <name>]] [--envfile <path>] [--environ] [--resume] [--watch] [--pidfile <fil>]",
Short: `Starts the Caddy process and blocks indefinitely`, Short: `Starts the Caddy process and blocks indefinitely`,
Long: ` Long: `
Starts the Caddy process, optionally bootstrapped with an initial config file, Starts the Caddy process, optionally bootstrapped with an initial config file,
@@ -133,8 +126,8 @@ As a special case, if the current working directory has a file called
that file will be loaded and used to configure Caddy, even without any command that file will be loaded and used to configure Caddy, even without any command
line flags. line flags.
If --envfile is specified, an environment file with environment variables If --envfile is specified, an environment file with environment variables in
in the KEY=VALUE format will be loaded into the Caddy process. the KEY=VALUE format will be loaded into the Caddy process.
If --environ is specified, the environment as seen by the Caddy process will If --environ is specified, the environment as seen by the Caddy process will
be printed before starting. This is the same as the environ command but does be printed before starting. This is the same as the environ command but does
@@ -145,42 +138,44 @@ save file. It is not an error if --resume is used and no autosave file exists.
If --watch is specified, the config file will be loaded automatically after If --watch is specified, the config file will be loaded automatically after
changes. This can make unintentional config changes easier; only use this changes. This can make unintentional config changes easier; only use this
option in a local development environment. option in a local development environment.`,
`, Flags: func() *flag.FlagSet {
CobraFunc: func(cmd *cobra.Command) { fs := flag.NewFlagSet("run", flag.ExitOnError)
cmd.Flags().StringP("config", "c", "", "Configuration file") fs.String("config", "", "Configuration file")
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply") fs.String("adapter", "", "Name of config adapter to apply")
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load") fs.String("envfile", "", "Environment file to load")
cmd.Flags().BoolP("environ", "e", false, "Print environment") fs.Bool("environ", false, "Print environment")
cmd.Flags().BoolP("resume", "r", false, "Use saved config, if any (and prefer over --config file)") fs.Bool("resume", false, "Use saved config, if any (and prefer over --config file)")
cmd.Flags().BoolP("watch", "w", false, "Watch config file for changes and reload it automatically") fs.Bool("watch", false, "Watch config file for changes and reload it automatically")
cmd.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID") fs.String("pidfile", "", "Path of file to which to write process ID")
cmd.Flags().StringP("pingback", "", "", "Echo confirmation bytes to this address on success") fs.String("pingback", "", "Echo confirmation bytes to this address on success")
cmd.RunE = WrapCommandFuncForCobra(cmdRun) return fs
}, }(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "stop", Name: "stop",
Usage: "[--config <path> [--adapter <name>]] [--address <interface>]", Func: cmdStop,
Usage: "[--address <interface>] [--config <path> [--adapter <name>]]",
Short: "Gracefully stops a started Caddy process", Short: "Gracefully stops a started Caddy process",
Long: ` Long: `
Stops the background Caddy process as gracefully as possible. Stops the background Caddy process as gracefully as possible.
It requires that the admin API is enabled and accessible, since it will It requires that the admin API is enabled and accessible, since it will
use the API's /stop endpoint. The address of this request can be customized use the API's /stop endpoint. The address of this request can be customized
using the --address flag, or from the given --config, if not the default. using the --address flag, or from the given --config, if not the default.`,
`, Flags: func() *flag.FlagSet {
CobraFunc: func(cmd *cobra.Command) { fs := flag.NewFlagSet("stop", flag.ExitOnError)
cmd.Flags().StringP("config", "c", "", "Configuration file to use to parse the admin address, if --address is not used") fs.String("address", "", "The address to use to reach the admin API endpoint, if not the default")
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply (when --config is used)") fs.String("config", "", "Configuration file to use to parse the admin address, if --address is not used")
cmd.Flags().StringP("address", "", "", "The address to use to reach the admin API endpoint, if not the default") fs.String("adapter", "", "Name of config adapter to apply (when --config is used)")
cmd.RunE = WrapCommandFuncForCobra(cmdStop) return fs
}, }(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "reload", Name: "reload",
Func: cmdReload,
Usage: "--config <path> [--adapter <name>] [--address <interface>]", Usage: "--config <path> [--adapter <name>] [--address <interface>]",
Short: "Changes the config of the running Caddy instance", Short: "Changes the config of the running Caddy instance",
Long: ` Long: `
@@ -190,19 +185,20 @@ workflows revolving around config files.
Since the admin endpoint is configurable, the endpoint configuration is loaded Since the admin endpoint is configurable, the endpoint configuration is loaded
from the --address flag if specified; otherwise it is loaded from the given from the --address flag if specified; otherwise it is loaded from the given
config file; otherwise the default is assumed. config file; otherwise the default is assumed.`,
`, Flags: func() *flag.FlagSet {
CobraFunc: func(cmd *cobra.Command) { fs := flag.NewFlagSet("reload", flag.ExitOnError)
cmd.Flags().StringP("config", "c", "", "Configuration file (required)") fs.String("config", "", "Configuration file (required)")
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply") fs.String("adapter", "", "Name of config adapter to apply")
cmd.Flags().StringP("address", "", "", "Address of the administration listener, if different from config") fs.String("address", "", "Address of the administration listener, if different from config")
cmd.Flags().BoolP("force", "f", false, "Force config reload, even if it is the same") fs.Bool("force", false, "Force config reload, even if it is the same")
cmd.RunE = WrapCommandFuncForCobra(cmdReload) return fs
}, }(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "version", Name: "version",
Func: cmdVersion,
Short: "Prints the version", Short: "Prints the version",
Long: ` Long: `
Prints the version of this Caddy binary. Prints the version of this Caddy binary.
@@ -217,30 +213,31 @@ detailed version information is printed as given by Go modules.
For more details about the full version string, see the Go module For more details about the full version string, see the Go module
documentation: https://go.dev/doc/modules/version-numbers documentation: https://go.dev/doc/modules/version-numbers
`, `,
Func: cmdVersion,
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "list-modules", Name: "list-modules",
Usage: "[--packages] [--versions] [--skip-standard]", Func: cmdListModules,
Usage: "[--packages] [--versions]",
Short: "Lists the installed Caddy modules", Short: "Lists the installed Caddy modules",
CobraFunc: func(cmd *cobra.Command) { Flags: func() *flag.FlagSet {
cmd.Flags().BoolP("packages", "", false, "Print package paths") fs := flag.NewFlagSet("list-modules", flag.ExitOnError)
cmd.Flags().BoolP("versions", "", false, "Print version information") fs.Bool("packages", false, "Print package paths")
cmd.Flags().BoolP("skip-standard", "s", false, "Skip printing standard modules") fs.Bool("versions", false, "Print version information")
cmd.RunE = WrapCommandFuncForCobra(cmdListModules) fs.Bool("skip-standard", false, "Skip printing standard modules")
}, return fs
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "build-info", Name: "build-info",
Short: "Prints information about this build",
Func: cmdBuildInfo, Func: cmdBuildInfo,
Short: "Prints information about this build",
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "environ", Name: "environ",
Usage: "[--envfile <path>]", Func: cmdEnviron,
Short: "Prints the environment", Short: "Prints the environment",
Long: ` Long: `
Prints the environment as seen by this Caddy process. Prints the environment as seen by this Caddy process.
@@ -250,9 +247,6 @@ configuration uses environment variables (e.g. "{env.VARIABLE}") then
this command can be useful for verifying that the variables will have this command can be useful for verifying that the variables will have
the values you expect in your config. the values you expect in your config.
If --envfile is specified, an environment file with environment variables
in the KEY=VALUE format will be loaded into the Caddy process.
Note that environments may be different depending on how you run Caddy. Note that environments may be different depending on how you run Caddy.
Environments for Caddy instances started by service managers such as Environments for Caddy instances started by service managers such as
systemd are often different than the environment inherited from your systemd are often different than the environment inherited from your
@@ -263,15 +257,12 @@ by adding the "--environ" flag.
Environments may contain sensitive data. Environments may contain sensitive data.
`, `,
CobraFunc: func(cmd *cobra.Command) {
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
cmd.RunE = WrapCommandFuncForCobra(cmdEnviron)
},
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "adapt", Name: "adapt",
Usage: "--config <path> [--adapter <name>] [--pretty] [--validate] [--envfile <path>]", Func: cmdAdaptConfig,
Usage: "--config <path> [--adapter <name>] [--pretty] [--validate]",
Short: "Adapts a configuration to Caddy's native JSON", Short: "Adapts a configuration to Caddy's native JSON",
Long: ` Long: `
Adapts a configuration to Caddy's native JSON format and writes the Adapts a configuration to Caddy's native JSON format and writes the
@@ -282,94 +273,38 @@ for human readability.
If --validate is used, the adapted config will be checked for validity. If --validate is used, the adapted config will be checked for validity.
If the config is invalid, an error will be printed to stderr and a non- If the config is invalid, an error will be printed to stderr and a non-
zero exit status will be returned. zero exit status will be returned.`,
Flags: func() *flag.FlagSet {
If --envfile is specified, an environment file with environment variables fs := flag.NewFlagSet("adapt", flag.ExitOnError)
in the KEY=VALUE format will be loaded into the Caddy process. fs.String("config", "", "Configuration file to adapt (required)")
`, fs.String("adapter", "caddyfile", "Name of config adapter")
CobraFunc: func(cmd *cobra.Command) { fs.Bool("pretty", false, "Format the output for human readability")
cmd.Flags().StringP("config", "c", "", "Configuration file to adapt (required)") fs.Bool("validate", false, "Validate the output")
cmd.Flags().StringP("adapter", "a", "caddyfile", "Name of config adapter") return fs
cmd.Flags().BoolP("pretty", "p", false, "Format the output for human readability") }(),
cmd.Flags().BoolP("validate", "", false, "Validate the output")
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
cmd.RunE = WrapCommandFuncForCobra(cmdAdaptConfig)
},
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "validate", Name: "validate",
Usage: "--config <path> [--adapter <name>] [--envfile <path>]", Func: cmdValidateConfig,
Usage: "--config <path> [--adapter <name>]",
Short: "Tests whether a configuration file is valid", Short: "Tests whether a configuration file is valid",
Long: ` Long: `
Loads and provisions the provided config, but does not start running it. Loads and provisions the provided config, but does not start running it.
This reveals any errors with the configuration through the loading and This reveals any errors with the configuration through the loading and
provisioning stages. provisioning stages.`,
Flags: func() *flag.FlagSet {
If --envfile is specified, an environment file with environment variables fs := flag.NewFlagSet("validate", flag.ExitOnError)
in the KEY=VALUE format will be loaded into the Caddy process. fs.String("config", "", "Input configuration file")
`, fs.String("adapter", "", "Name of config adapter")
CobraFunc: func(cmd *cobra.Command) { return fs
cmd.Flags().StringP("config", "c", "", "Input configuration file") }(),
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter")
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
cmd.RunE = WrapCommandFuncForCobra(cmdValidateConfig)
},
})
RegisterCommand(Command{
Name: "storage",
Short: "Commands for working with Caddy's storage (EXPERIMENTAL)",
Long: `
Allows exporting and importing Caddy's storage contents. The two commands can be
combined in a pipeline to transfer directly from one storage to another:
$ caddy storage export --config Caddyfile.old --output - |
> caddy storage import --config Caddyfile.new --input -
The - argument refers to stdout and stdin, respectively.
NOTE: When importing to or exporting from file_system storage (the default), the command
should be run as the user that owns the associated root path.
EXPERIMENTAL: May be changed or removed.
`,
CobraFunc: func(cmd *cobra.Command) {
exportCmd := &cobra.Command{
Use: "export --config <path> --output <path>",
Short: "Exports storage assets as a tarball",
Long: `
The contents of the configured storage module (TLS certificates, etc)
are exported via a tarball.
--output is required, - can be given for stdout.
`,
RunE: WrapCommandFuncForCobra(cmdExportStorage),
}
exportCmd.Flags().StringP("config", "c", "", "Input configuration file (required)")
exportCmd.Flags().StringP("output", "o", "", "Output path")
cmd.AddCommand(exportCmd)
importCmd := &cobra.Command{
Use: "import --config <path> --input <path>",
Short: "Imports storage assets from a tarball.",
Long: `
Imports storage assets to the configured storage module. The import file must be
a tar archive.
--input is required, - can be given for stdin.
`,
RunE: WrapCommandFuncForCobra(cmdImportStorage),
}
importCmd.Flags().StringP("config", "c", "", "Configuration file to load (required)")
importCmd.Flags().StringP("input", "i", "", "Tar of assets to load (required)")
cmd.AddCommand(importCmd)
},
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "fmt", Name: "fmt",
Usage: "[--overwrite] [--diff] [<path>]", Func: cmdFmt,
Usage: "[--overwrite] [<path>]",
Short: "Formats a Caddyfile", Short: "Formats a Caddyfile",
Long: ` Long: `
Formats the Caddyfile by adding proper indentation and spaces to improve Formats the Caddyfile by adding proper indentation and spaces to improve
@@ -385,41 +320,44 @@ is not a valid patch format.
If you wish you use stdin instead of a regular file, use - as the path. If you wish you use stdin instead of a regular file, use - as the path.
When reading from stdin, the --overwrite flag has no effect: the result When reading from stdin, the --overwrite flag has no effect: the result
is always printed to stdout. is always printed to stdout.`,
`, Flags: func() *flag.FlagSet {
CobraFunc: func(cmd *cobra.Command) { fs := flag.NewFlagSet("fmt", flag.ExitOnError)
cmd.Flags().BoolP("overwrite", "w", false, "Overwrite the input file with the results") fs.Bool("overwrite", false, "Overwrite the input file with the results")
cmd.Flags().BoolP("diff", "d", false, "Print the differences between the input file and the formatted output") fs.Bool("diff", false, "Print the differences between the input file and the formatted output")
cmd.RunE = WrapCommandFuncForCobra(cmdFmt) return fs
}, }(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "upgrade", Name: "upgrade",
Func: cmdUpgrade,
Short: "Upgrade Caddy (EXPERIMENTAL)", Short: "Upgrade Caddy (EXPERIMENTAL)",
Long: ` Long: `
Downloads an updated Caddy binary with the same modules/plugins at the Downloads an updated Caddy binary with the same modules/plugins at the
latest versions. EXPERIMENTAL: May be changed or removed. latest versions. EXPERIMENTAL: May be changed or removed.`,
`, Flags: func() *flag.FlagSet {
CobraFunc: func(cmd *cobra.Command) { fs := flag.NewFlagSet("upgrade", flag.ExitOnError)
cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it") fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it")
cmd.RunE = WrapCommandFuncForCobra(cmdUpgrade) return fs
}, }(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "add-package", Name: "add-package",
Func: cmdAddPackage,
Usage: "<packages...>", Usage: "<packages...>",
Short: "Adds Caddy packages (EXPERIMENTAL)", Short: "Adds Caddy packages (EXPERIMENTAL)",
Long: ` Long: `
Downloads an updated Caddy binary with the specified packages (module/plugin) Downloads an updated Caddy binary with the specified packages (module/plugin)
added. Retains existing packages. Returns an error if the any of packages are added. Retains existing packages. Returns an error if the any of packages are
already included. EXPERIMENTAL: May be changed or removed. already included. EXPERIMENTAL: May be changed or removed.
`, `,
CobraFunc: func(cmd *cobra.Command) { Flags: func() *flag.FlagSet {
cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it") fs := flag.NewFlagSet("add-package", flag.ExitOnError)
cmd.RunE = WrapCommandFuncForCobra(cmdAddPackage) fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it")
}, return fs
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
@@ -428,18 +366,35 @@ already included. EXPERIMENTAL: May be changed or removed.
Usage: "<packages...>", Usage: "<packages...>",
Short: "Removes Caddy packages (EXPERIMENTAL)", Short: "Removes Caddy packages (EXPERIMENTAL)",
Long: ` Long: `
Downloads an updated Caddy binaries without the specified packages (module/plugin). Downloads an updated Caddy binaries without the specified packages (module/plugin).
Returns an error if any of the packages are not included. Returns an error if any of the packages are not included.
EXPERIMENTAL: May be changed or removed. EXPERIMENTAL: May be changed or removed.
`, `,
CobraFunc: func(cmd *cobra.Command) { Flags: func() *flag.FlagSet {
cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it") fs := flag.NewFlagSet("remove-package", flag.ExitOnError)
cmd.RunE = WrapCommandFuncForCobra(cmdRemovePackage) fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it")
}, return fs
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "manpage", Name: "manpage",
Func: func(fl Flags) (int, error) {
dir := strings.TrimSpace(fl.String("directory"))
if dir == "" {
return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
}
if err := os.MkdirAll(dir, 0755); err != nil {
return caddy.ExitCodeFailedQuit, err
}
if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
Title: "Caddy",
Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
}, dir); err != nil {
return caddy.ExitCodeFailedQuit, err
}
return caddy.ExitCodeSuccess, nil
},
Usage: "--directory <path>", Usage: "--directory <path>",
Short: "Generates the manual pages for Caddy commands", Short: "Generates the manual pages for Caddy commands",
Long: ` Long: `
@@ -449,25 +404,11 @@ tagged into section 8 (System Administration).
The manual page files are generated into the directory specified by the The manual page files are generated into the directory specified by the
argument of --directory. If the directory does not exist, it will be created. argument of --directory. If the directory does not exist, it will be created.
`, `,
CobraFunc: func(cmd *cobra.Command) { Flags: func() *flag.FlagSet {
cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated") fs := flag.NewFlagSet("manpage", flag.ExitOnError)
cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) { fs.String("directory", "", "The output directory where the manpages are generated")
dir := strings.TrimSpace(fl.String("directory")) return fs
if dir == "" { }(),
return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
}
if err := os.MkdirAll(dir, 0o755); err != nil {
return caddy.ExitCodeFailedQuit, err
}
if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
Title: "Caddy",
Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
}, dir); err != nil {
return caddy.ExitCodeFailedQuit, err
}
return caddy.ExitCodeSuccess, nil
})
},
}) })
// source: https://github.com/spf13/cobra/blob/main/shell_completions.md // source: https://github.com/spf13/cobra/blob/main/shell_completions.md
@@ -475,47 +416,47 @@ argument of --directory. If the directory does not exist, it will be created.
Use: "completion [bash|zsh|fish|powershell]", Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script", Short: "Generate completion script",
Long: fmt.Sprintf(`To load completions: Long: fmt.Sprintf(`To load completions:
Bash: Bash:
$ source <(%[1]s completion bash) $ source <(%[1]s completion bash)
# To load completions for each session, execute once: # To load completions for each session, execute once:
# Linux: # Linux:
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s $ %[1]s completion bash > /etc/bash_completion.d/%[1]s
# macOS: # macOS:
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s $ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
Zsh: Zsh:
# If shell completion is not already enabled in your environment, # If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once: # you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc $ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once: # To load completions for each session, execute once:
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s" $ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
# You will need to start a new shell for this setup to take effect. # You will need to start a new shell for this setup to take effect.
fish: fish:
$ %[1]s completion fish | source $ %[1]s completion fish | source
# To load completions for each session, execute once: # To load completions for each session, execute once:
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish $ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
PowerShell: PowerShell:
PS> %[1]s completion powershell | Out-String | Invoke-Expression PS> %[1]s completion powershell | Out-String | Invoke-Expression
# To load completions for every new session, run: # To load completions for every new session, run:
PS> %[1]s completion powershell > %[1]s.ps1 PS> %[1]s completion powershell > %[1]s.ps1
# and source this file from your PowerShell profile. # and source this file from your PowerShell profile.
`, rootCmd.Root().Name()), `, rootCmd.Root().Name()),
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), Args: cobra.ExactValidArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] { switch args[0] {
case "bash": case "bash":
@@ -551,7 +492,7 @@ func RegisterCommand(cmd Command) {
if cmd.Name == "" { if cmd.Name == "" {
panic("command name is required") panic("command name is required")
} }
if cmd.Func == nil && cmd.CobraFunc == nil { if cmd.Func == nil {
panic("command function missing") panic("command function missing")
} }
if cmd.Short == "" { if cmd.Short == "" {
@@ -563,7 +504,7 @@ func RegisterCommand(cmd Command) {
if !commandNameRegex.MatchString(cmd.Name) { if !commandNameRegex.MatchString(cmd.Name) {
panic("invalid command name") panic("invalid command name")
} }
rootCmd.AddCommand(caddyCmdToCobra(cmd)) rootCmd.AddCommand(caddyCmdToCoral(cmd))
} }
var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`) var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)
+60 -54
View File
@@ -17,7 +17,6 @@ package caddycmd
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"errors"
"flag" "flag"
"fmt" "fmt"
"io" "io"
@@ -31,12 +30,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
) )
func init() { func init() {
@@ -64,10 +62,6 @@ func Main() {
} }
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
var exitError *exitError
if errors.As(err, &exitError) {
os.Exit(exitError.ExitCode)
}
os.Exit(1) os.Exit(1)
} }
} }
@@ -95,10 +89,6 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
// and returns the resulting JSON config bytes along with // and returns the resulting JSON config bytes along with
// the name of the loaded config file (if any). // the name of the loaded config file (if any).
func LoadConfig(configFile, adapterName string) ([]byte, string, error) { func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
return loadConfigWithLogger(caddy.Log(), configFile, adapterName)
}
func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, error) {
// specifying an adapter without a config file is ambiguous // specifying an adapter without a config file is ambiguous
if adapterName != "" && configFile == "" { if adapterName != "" && configFile == "" {
return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)") return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)")
@@ -117,14 +107,13 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
if err != nil { if err != nil {
return nil, "", fmt.Errorf("reading config file: %v", err) return nil, "", fmt.Errorf("reading config file: %v", err)
} }
if logger != nil { caddy.Log().Info("using provided configuration",
logger.Info("using provided configuration", zap.String("config_file", configFile),
zap.String("config_file", configFile), zap.String("config_adapter", adapterName))
zap.String("config_adapter", adapterName))
}
} else if adapterName == "" { } else if adapterName == "" {
// if the Caddyfile adapter is plugged in, we can try using an // as a special case when no config file or adapter
// adjacent Caddyfile by default // is specified, see if the Caddyfile adapter is
// plugged in, and if so, try using a default Caddyfile
cfgAdapter = caddyconfig.GetAdapter("caddyfile") cfgAdapter = caddyconfig.GetAdapter("caddyfile")
if cfgAdapter != nil { if cfgAdapter != nil {
config, err = os.ReadFile("Caddyfile") config, err = os.ReadFile("Caddyfile")
@@ -137,9 +126,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
} else { } else {
// success reading default Caddyfile // success reading default Caddyfile
configFile = "Caddyfile" configFile = "Caddyfile"
if logger != nil { caddy.Log().Info("using adjacent Caddyfile")
logger.Info("using adjacent Caddyfile")
}
} }
} }
} }
@@ -174,9 +161,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
if warn.Directive != "" { if warn.Directive != "" {
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message) msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
} }
if logger != nil { caddy.Log().Warn(msg, zap.String("adapter", adapterName), zap.String("file", warn.File), zap.Int("line", warn.Line))
logger.Warn(msg, zap.String("adapter", adapterName), zap.String("file", warn.File), zap.Int("line", warn.Line))
}
} }
config = adaptedConfig config = adaptedConfig
} }
@@ -189,8 +174,6 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
// blocks indefinitely; it only quits if the poller has errors for // blocks indefinitely; it only quits if the poller has errors for
// long enough time. The filename passed in must be the actual // long enough time. The filename passed in must be the actual
// config file used, not one to be discovered. // config file used, not one to be discovered.
// Each second the config files is loaded and parsed into an object
// and is compared to the last config object that was loaded
func watchConfigFile(filename, adapterName string) { func watchConfigFile(filename, adapterName string) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
@@ -206,36 +189,64 @@ func watchConfigFile(filename, adapterName string) {
With(zap.String("config_file", filename)) With(zap.String("config_file", filename))
} }
// get current config // get the initial timestamp on the config file
lastCfg, _, err := loadConfigWithLogger(nil, filename, adapterName) info, err := os.Stat(filename)
if err != nil { if err != nil {
logger().Error("unable to load latest config", zap.Error(err)) logger().Error("cannot watch config file", zap.Error(err))
return return
} }
lastModified := info.ModTime()
logger().Info("watching config file for changes") logger().Info("watching config file for changes")
// if the file disappears or something, we can
// stop polling if the error lasts long enough
var lastErr time.Time
finalError := func(err error) bool {
if lastErr.IsZero() {
lastErr = time.Now()
return false
}
if time.Since(lastErr) > 30*time.Second {
logger().Error("giving up watching config file; too many errors",
zap.Error(err))
return true
}
return false
}
// begin poller // begin poller
//nolint:staticcheck //nolint:staticcheck
for range time.Tick(1 * time.Second) { for range time.Tick(1 * time.Second) {
// get current config // get the file info
newCfg, _, err := loadConfigWithLogger(nil, filename, adapterName) info, err := os.Stat(filename)
if err != nil { if err != nil {
logger().Error("unable to load latest config", zap.Error(err)) if finalError(err) {
return return
} }
// if it hasn't changed, nothing to do
if bytes.Equal(lastCfg, newCfg) {
continue continue
} }
lastErr = time.Time{} // no error, so clear any memory of one
// if it hasn't changed, nothing to do
if !info.ModTime().After(lastModified) {
continue
}
logger().Info("config file changed; reloading") logger().Info("config file changed; reloading")
// remember the current config // remember this timestamp
lastCfg = newCfg lastModified = info.ModTime()
// load the contents of the file
config, _, err := LoadConfig(filename, adapterName)
if err != nil {
logger().Error("unable to load latest config", zap.Error(err))
continue
}
// apply the updated config // apply the updated config
err = caddy.Load(lastCfg, false) err = caddy.Load(config, false)
if err != nil { if err != nil {
logger().Error("applying latest config", zap.Error(err)) logger().Error("applying latest config", zap.Error(err))
continue continue
@@ -305,12 +316,8 @@ func loadEnvFromFile(envFile string) error {
} }
for k, v := range envMap { for k, v := range envMap {
// do not overwrite existing environment variables if err := os.Setenv(k, v); err != nil {
_, exists := os.LookupEnv(k) return fmt.Errorf("setting environment variables: %v", err)
if !exists {
if err := os.Setenv(k, v); err != nil {
return fmt.Errorf("setting environment variables: %v", err)
}
} }
} }
@@ -367,19 +374,18 @@ func parseEnvFile(envInput io.Reader) (map[string]string, error) {
} }
// quoted value: support newlines // quoted value: support newlines
if strings.HasPrefix(val, `"`) || strings.HasPrefix(val, "'") { if strings.HasPrefix(val, `"`) {
quote := string(val[0]) for !(strings.HasSuffix(line, `"`) && !strings.HasSuffix(line, `\"`)) {
for !(strings.HasSuffix(line, quote) && !strings.HasSuffix(line, `\`+quote)) { val = strings.ReplaceAll(val, `\"`, `"`)
val = strings.ReplaceAll(val, `\`+quote, quote)
if !scanner.Scan() { if !scanner.Scan() {
break break
} }
lineNumber++ lineNumber++
line = strings.ReplaceAll(scanner.Text(), `\`+quote, quote) line = strings.ReplaceAll(scanner.Text(), `\"`, `"`)
val += "\n" + line val += "\n" + line
} }
val = strings.TrimPrefix(val, quote) val = strings.TrimPrefix(val, `"`)
val = strings.TrimSuffix(val, quote) val = strings.TrimSuffix(val, `"`)
} }
envMap[key] = val envMap[key] = val
+1 -12
View File
@@ -22,15 +22,13 @@ import (
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"reflect" "reflect"
"runtime" "runtime"
"runtime/debug" "runtime/debug"
"strings" "strings"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"go.uber.org/zap"
) )
func cmdUpgrade(fl Flags) (int, error) { func cmdUpgrade(fl Flags) (int, error) {
@@ -104,15 +102,6 @@ func upgradeBuild(pluginPkgs map[string]struct{}, fl Flags) (int, error) {
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err)
} }
if thisExecStat.Mode()&os.ModeSymlink == os.ModeSymlink {
symSource := thisExecPath
// we are a symlink; resolve it
thisExecPath, err = filepath.EvalSymlinks(thisExecPath)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("resolving current executable symlink: %v", err)
}
l.Info("this executable is a symlink", zap.String("source", symSource), zap.String("target", thisExecPath))
}
l.Info("this executable will be replaced", zap.String("path", thisExecPath)) l.Info("this executable will be replaced", zap.String("path", thisExecPath))
// build the request URL to download this custom build // build the request URL to download this custom build
-221
View File
@@ -1,221 +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 caddycmd
import (
"archive/tar"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"github.com/caddyserver/certmagic"
"github.com/caddyserver/caddy/v2"
)
type storVal struct {
StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
}
// determineStorage returns the top-level storage module from the given config.
// It may return nil even if no error.
func determineStorage(configFile string, configAdapter string) (*storVal, error) {
cfg, _, err := LoadConfig(configFile, configAdapter)
if err != nil {
return nil, err
}
// storage defaults to FileStorage if not explicitly
// defined in the config, so the config can be valid
// json but unmarshaling will fail.
if !json.Valid(cfg) {
return nil, &json.SyntaxError{}
}
var tmpStruct storVal
err = json.Unmarshal(cfg, &tmpStruct)
if err != nil {
// default case, ignore the error
var jsonError *json.SyntaxError
if errors.As(err, &jsonError) {
return nil, nil
}
return nil, err
}
return &tmpStruct, nil
}
func cmdImportStorage(fl Flags) (int, error) {
importStorageCmdConfigFlag := fl.String("config")
importStorageCmdImportFile := fl.String("input")
if importStorageCmdConfigFlag == "" {
return caddy.ExitCodeFailedStartup, errors.New("--config is required")
}
if importStorageCmdImportFile == "" {
return caddy.ExitCodeFailedStartup, errors.New("--input is required")
}
// extract storage from config if possible
storageCfg, err := determineStorage(importStorageCmdConfigFlag, "")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// load specified storage or fallback to default
var stor certmagic.Storage
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
if storageCfg != nil && storageCfg.StorageRaw != nil {
val, err := ctx.LoadModule(storageCfg, "StorageRaw")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
stor, err = val.(caddy.StorageConverter).CertMagicStorage()
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
} else {
stor = caddy.DefaultStorage
}
// setup input
var f *os.File
if importStorageCmdImportFile == "-" {
f = os.Stdin
} else {
f, err = os.Open(importStorageCmdImportFile)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("opening input file: %v", err)
}
defer f.Close()
}
// store each archive element
tr := tar.NewReader(f)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
}
b, err := io.ReadAll(tr)
if err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
}
err = stor.Store(ctx, hdr.Name, b)
if err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
}
}
fmt.Println("Successfully imported storage")
return caddy.ExitCodeSuccess, nil
}
func cmdExportStorage(fl Flags) (int, error) {
exportStorageCmdConfigFlag := fl.String("config")
exportStorageCmdOutputFlag := fl.String("output")
if exportStorageCmdConfigFlag == "" {
return caddy.ExitCodeFailedStartup, errors.New("--config is required")
}
if exportStorageCmdOutputFlag == "" {
return caddy.ExitCodeFailedStartup, errors.New("--output is required")
}
// extract storage from config if possible
storageCfg, err := determineStorage(exportStorageCmdConfigFlag, "")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// load specified storage or fallback to default
var stor certmagic.Storage
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
if storageCfg != nil && storageCfg.StorageRaw != nil {
val, err := ctx.LoadModule(storageCfg, "StorageRaw")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
stor, err = val.(caddy.StorageConverter).CertMagicStorage()
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
} else {
stor = caddy.DefaultStorage
}
// enumerate all keys
keys, err := stor.List(ctx, "", true)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// setup output
var f *os.File
if exportStorageCmdOutputFlag == "-" {
f = os.Stdout
} else {
f, err = os.Create(exportStorageCmdOutputFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("opening output file: %v", err)
}
defer f.Close()
}
// `IsTerminal: true` keys hold the values we
// care about, write them out
tw := tar.NewWriter(f)
for _, k := range keys {
info, err := stor.Stat(ctx, k)
if err != nil {
return caddy.ExitCodeFailedQuit, err
}
if info.IsTerminal {
v, err := stor.Load(ctx, k)
if err != nil {
return caddy.ExitCodeFailedQuit, err
}
hdr := &tar.Header{
Name: k,
Mode: 0o600,
Size: int64(len(v)),
}
if err = tw.WriteHeader(hdr); err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
}
if _, err = tw.Write(v); err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
}
}
}
if err = tw.Close(); err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
}
return caddy.ExitCodeSuccess, nil
}
+9 -25
View File
@@ -326,7 +326,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
// fill in its config only if there is a config to fill in // fill in its config only if there is a config to fill in
if len(rawMsg) > 0 { if len(rawMsg) > 0 {
err := StrictUnmarshalJSON(rawMsg, &val) err := strictUnmarshalJSON(rawMsg, &val)
if err != nil { if err != nil {
return nil, fmt.Errorf("decoding module config: %s: %v", modInfo, err) return nil, fmt.Errorf("decoding module config: %s: %v", modInfo, err)
} }
@@ -410,11 +410,6 @@ func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.
// called during the Provision/Validate phase to reference a // called during the Provision/Validate phase to reference a
// module's own host app (since the parent app module is still // module's own host app (since the parent app module is still
// in the process of being provisioned, it is not yet ready). // in the process of being provisioned, it is not yet ready).
//
// We return any type instead of the App type because it is NOT
// intended for the caller of this method to be the one to start
// or stop App modules. The caller is expected to assert to the
// concrete type.
func (ctx Context) App(name string) (any, error) { func (ctx Context) App(name string) (any, error) {
if app, ok := ctx.cfg.apps[name]; ok { if app, ok := ctx.cfg.apps[name]; ok {
return app, nil return app, nil
@@ -431,23 +426,15 @@ func (ctx Context) App(name string) (any, error) {
return modVal, nil return modVal, nil
} }
// AppIfConfigured returns an app by its name if it has been // AppIsConfigured returns whether an app named name has been
// configured. Can be called instead of App() to avoid // configured. Can be called before calling App() to avoid
// instantiating an empty app when that's not desirable. If // instantiating an empty app when that's not desirable.
// the app has not been loaded, nil is returned. func (ctx Context) AppIsConfigured(name string) bool {
// if _, ok := ctx.cfg.apps[name]; ok {
// We return any type instead of the App type because it is not return true
// intended for the caller of this method to be the one to start
// or stop App modules. The caller is expected to assert to the
// concrete type.
func (ctx Context) AppIfConfigured(name string) any {
if ctx.cfg == nil {
// this can happen if the currently-active context
// is being accessed, but no config has successfully
// been loaded yet
return nil
} }
return ctx.cfg.apps[name] appRaw := ctx.cfg.AppsRaw[name]
return appRaw != nil
} }
// Storage returns the configured Caddy storage implementation. // Storage returns the configured Caddy storage implementation.
@@ -488,9 +475,6 @@ func (ctx Context) Logger(module ...Module) *zap.Logger {
if len(module) > 0 { if len(module) > 0 {
mod = module[0] mod = module[0]
} }
if mod == nil {
return Log()
}
return ctx.cfg.Logging.Logger(mod) return ctx.cfg.Logging.Logger(mod)
} }

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