Compare commits

..

2 Commits

Author SHA1 Message Date
Matthew Holt 1e6eed42bd Also reject null byte 2022-06-14 11:37:37 -06:00
Matthew Holt 98cd4333a1 caddyhttp: Introduce strict HTTP mode 2022-06-14 11:03:53 -06:00
215 changed files with 4793 additions and 12438 deletions
-1
View File
@@ -1 +0,0 @@
*.go text eol=lf
-7
View File
@@ -1,7 +0,0 @@
---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
+15 -16
View File
@@ -19,16 +19,16 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
go: [ '1.18', '1.20' ]
go: [ '1.17', '1.18' ]
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.18'
GO_SEMVER: '~1.18.4'
- go: '1.17'
GO_SEMVER: '~1.17.9'
- go: '1.20'
GO_SEMVER: '~1.20.0'
- go: '1.18'
GO_SEMVER: '~1.18.1'
# Set some variables per OS, usable via ${{ matrix.VAR }}
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
@@ -64,7 +64,7 @@ jobs:
# go get github.com/axw/gocov/gocov
# go get github.com/AlekSi/gocov-xml
# 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
id: vars
@@ -77,10 +77,10 @@ jobs:
env
printf "Git version: $(git version)\n\n"
# Calculate the short SHA1 hash of the git commit
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
- name: Cache the build cache
uses: actions/cache@v3
uses: actions/cache@v2
with:
# In order:
# * Module download cache
@@ -109,7 +109,7 @@ jobs:
go build -trimpath -ldflags="-w -s" -v
- name: Publish Build Artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v1
with:
name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
path: ${{ matrix.CADDY_BIN_PATH }}
@@ -123,7 +123,7 @@ jobs:
run: |
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
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
# - name: Prepare coverage reports
@@ -143,7 +143,7 @@ jobs:
s390x-test:
name: test (s390x on IBM Z)
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
steps:
- name: Checkout code into the Go module directory
@@ -156,18 +156,17 @@ jobs:
short_sha=$(git rev-parse --short HEAD)
# The environment is fresh, so there's no point in keeping accepting and adding the key.
rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . "$CI_USER"@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$CI_USER"@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -v ./..."
rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . caddy-ci@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t caddy-ci@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -v ./..."
test_result=$?
# There's no need leaving the files around
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$CI_USER"@ci-s390x.caddyserver.com "rm -rf /var/tmp/'$short_sha'"
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null caddy-ci@ci-s390x.caddyserver.com "rm -rf /var/tmp/'$short_sha'"
echo "Test exit code: $test_result"
exit $test_result
env:
SSH_KEY: ${{ secrets.S390X_SSH_KEY }}
CI_USER: ${{ secrets.CI_USER }}
goreleaser-check:
runs-on: ubuntu-latest
@@ -175,7 +174,7 @@ jobs:
- name: checkout
uses: actions/checkout@v3
- uses: goreleaser/goreleaser-action@v4
- uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: check
+4 -4
View File
@@ -16,13 +16,13 @@ jobs:
fail-fast: false
matrix:
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
go: [ '1.20' ]
go: [ '1.18' ]
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.20'
GO_SEMVER: '~1.20.0'
- go: '1.18'
GO_SEMVER: '~1.18.1'
runs-on: ubuntu-latest
continue-on-error: true
@@ -44,7 +44,7 @@ jobs:
env
- name: Cache the build cache
uses: actions/cache@v3
uses: actions/cache@v2
with:
# In order:
# * Module download cache
+3 -14
View File
@@ -10,32 +10,21 @@ on:
- master
- 2.*
permissions:
contents: read
jobs:
# From https://github.com/golangci/golangci-lint-action
golangci:
permissions:
contents: read # for actions/checkout to fetch code
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
name: lint
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '~1.18.4'
go-version: '~1.17.9'
check-latest: true
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.50
# Windows times out frequently after about 5m50s if we don't set a longer timeout.
args: --timeout 10m
version: v1.44
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true
+13 -28
View File
@@ -11,22 +11,15 @@ jobs:
strategy:
matrix:
os: [ ubuntu-latest ]
go: [ '1.20' ]
go: [ '1.18' ]
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.20'
GO_SEMVER: '~1.20.0'
- go: '1.18'
GO_SEMVER: '~1.18.1'
runs-on: ${{ matrix.os }}
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
# https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings
permissions:
id-token: write
# https://docs.github.com/en/rest/overview/permissions-required-for-github-apps#permission-on-contents
# "Releases" is part of `contents`, so it needs the `write`
contents: write
steps:
- name: Install Go
@@ -61,8 +54,8 @@ jobs:
go env
printf "\n\nSystem environment:\n\n"
env
echo "version_tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\//}"
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
# Add "pip install" CLI tools to PATH
echo ~/.local/bin >> $GITHUB_PATH
@@ -74,10 +67,10 @@ jobs:
TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"`
TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"`
TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"`
echo "tag_major=${TAG_MAJOR}" >> $GITHUB_OUTPUT
echo "tag_minor=${TAG_MINOR}" >> $GITHUB_OUTPUT
echo "tag_patch=${TAG_PATCH}" >> $GITHUB_OUTPUT
echo "tag_special=${TAG_SPECIAL}" >> $GITHUB_OUTPUT
echo "::set-output name=tag_major::${TAG_MAJOR}"
echo "::set-output name=tag_minor::${TAG_MINOR}"
echo "::set-output name=tag_patch::${TAG_PATCH}"
echo "::set-output name=tag_special::${TAG_SPECIAL}"
# Cloudsmith CLI tooling for pushing releases
# See https://help.cloudsmith.io/docs/cli
@@ -95,7 +88,7 @@ jobs:
git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1
- name: Cache the build cache
uses: actions/cache@v3
uses: actions/cache@v2
with:
# In order:
# * Module download cache
@@ -106,24 +99,16 @@ jobs:
key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go${{ matrix.go }}-release
- name: Install Cosign
uses: sigstore/cosign-installer@main
- name: Cosign version
run: cosign version
- name: Install Syft
uses: anchore/sbom-action/download-syft@main
- name: Syft version
run: syft version
# GoReleaser will take care of publishing those artifacts into the release
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v4
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --rm-dist --timeout 60m
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.vars.outputs.version_tag }}
COSIGN_EXPERIMENTAL: 1
# Only publish on non-special tags (e.g. non-beta)
# We will continue to push to Gemfury for the foreseeable future, although
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
# See https://github.com/peter-evans/repository-dispatch
- name: Trigger event on caddyserver/dist
uses: peter-evans/repository-dispatch@v2
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/dist
@@ -25,7 +25,7 @@ jobs:
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
- name: Trigger event on caddyserver/caddy-docker
uses: peter-evans/repository-dispatch@v2
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/caddy-docker
-4
View File
@@ -96,7 +96,3 @@ issues:
text: "G404" # G404: Insecure random number source (rand)
linters:
- gosec
- path: modules/caddyhttp/reverseproxy/streaming.go
text: "G404" # G404: Insecure random number source (rand)
linters:
- gosec
+4 -40
View File
@@ -4,7 +4,6 @@ before:
# 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
# subsequently causes gorleaser to refuse running.
- rm -rf caddy-build caddy-dist
- mkdir -p caddy-build
- cp cmd/caddy/main.go caddy-build/main.go
- /bin/sh -c 'cd ./caddy-build && go mod init caddy'
@@ -15,11 +14,7 @@ before:
# 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'
- git clone --depth 1 https://github.com/caddyserver/dist caddy-dist
- mkdir -p caddy-dist/man
- go mod download
- go run cmd/caddy/main.go manpage --directory ./caddy-dist/man
- gzip -r ./caddy-dist/man/
- /bin/sh -c 'go run cmd/caddy/main.go completion bash > ./caddy-dist/scripts/bash-completion'
builds:
- env:
@@ -63,44 +58,15 @@ builds:
goarm: "5"
flags:
- -trimpath
- -mod=readonly
ldflags:
- -s -w
signs:
- cmd: cosign
signature: "${artifact}.sig"
certificate: '{{ trimsuffix (trimsuffix .Env.artifact ".zip") ".tar.gz" }}.pem'
args: ["sign-blob", "--output-signature=${signature}", "--output-certificate", "${certificate}", "${artifact}"]
artifacts: all
sboms:
- artifacts: binary
documents:
- >-
{{ .ProjectName }}_
{{- .Version }}_
{{- if eq .Os "darwin" }}mac{{ else }}{{ .Os }}{{ end }}_
{{- .Arch }}
{{- with .Arm }}v{{ . }}{{ end }}
{{- with .Mips }}_{{ . }}{{ end }}
{{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}.sbom
cmd: syft
args: ["$artifact", "--file", "${document}", "--output", "cyclonedx-json"]
archives:
- format_overrides:
- goos: windows
format: zip
name_template: >-
{{ .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 }}
replacements:
darwin: mac
checksum:
algorithm: sha512
@@ -130,21 +96,19 @@ nfpms:
- src: ./caddy-dist/welcome/index.html
dst: /usr/share/caddy/index.html
- src: ./caddy-dist/scripts/bash-completion
- src: ./caddy-dist/scripts/completions/bash-completion
dst: /etc/bash_completion.d/caddy
- src: ./caddy-dist/config/Caddyfile
dst: /etc/caddy/Caddyfile
type: config
- src: ./caddy-dist/man/*
dst: /usr/share/man/man8/
scripts:
postinstall: ./caddy-dist/scripts/postinstall.sh
preremove: ./caddy-dist/scripts/preremove.sh
postremove: ./caddy-dist/scripts/postremove.sh
release:
github:
owner: caddyserver
+14 -26
View File
@@ -1,19 +1,13 @@
<p align="center">
<a href="https://caddyserver.com">
<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>
<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>
<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>
<hr>
<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">
<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>
<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>
@@ -46,13 +40,7 @@
<p align="center">
<b>Powered by</b>
<br>
<a href="https://github.com/caddyserver/certmagic">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/55066419/206946718-740b6371-3df3-4d72-a822-47e4c48af999.png">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png">
<img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250">
</picture>
</a>
<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>
</p>
@@ -69,28 +57,28 @@
- Multi-issuer fallback
- **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
- **Scales to hundreds of thousands of sites** as proven in production
- **HTTP/1.1, HTTP/2, and HTTP/3** supported all by default
- **Scales to tens of thousands of sites** ... and probably more
- **HTTP/1.1, HTTP/2, and experimental HTTP/3** support
- **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat
- **Runs anywhere** with **no external dependencies** (not even libc)
- Written in Go, a language with higher **memory safety guarantees** than other servers
- Actually **fun to use**
- So much more to [discover](https://caddyserver.com/v2)
- So, so much more to [discover](https://caddyserver.com/v2)
## Install
The simplest, cross-platform way to get started is to download Caddy from [GitHub Releases](https://github.com/caddyserver/caddy/releases) and place the executable file in your PATH.
The simplest, cross-platform way is to download from [GitHub Releases](https://github.com/caddyserver/caddy/releases) and place the executable file in your PATH.
See [our online documentation](https://caddyserver.com/docs/install) for other install instructions.
For other install options, see https://caddyserver.com/docs/install.
## Build from source
Requirements:
- [Go 1.18 or newer](https://golang.org/dl/)
- [Go 1.17 or newer](https://golang.org/dl/)
### 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._
```bash
@@ -176,9 +164,9 @@ The docs are also open source. You can contribute to them here: https://github.c
## Getting help
- We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed.
- We **strongly recommend** that all professionals or companies using Caddy get a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed.
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way! We can offer private help to sponsors. If Caddy is benefitting your company, please consider a sponsorship. This not only helps fund full-time work to ensure the longevity of the project, it provides your company the resources, support, and discounts you need; along with being a great look for your company to your customers and potential customers!
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way! If Caddy is benefitting your company, please consider a sponsorship! This not only helps fund full-time work to ensure the longevity of the project, it's also a great look for your company to your customers and potential customers!
- Individuals can exchange help for free on our community forum at https://caddy.community. Remember that people give help out of their spare time and good will. The best way to get help is to give it first!
@@ -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.
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.
+32 -71
View File
@@ -25,8 +25,6 @@ import (
"errors"
"expvar"
"fmt"
"hash"
"hash/fnv"
"io"
"net"
"net/http"
@@ -40,23 +38,13 @@ import (
"sync"
"time"
"github.com/caddyserver/caddy/v2/notify"
"github.com/caddyserver/certmagic"
"github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func init() {
// The hard-coded default `DefaultAdminListen` can be overidden
// by setting the `CADDY_ADMIN` environment variable.
// The environment variable may be used by packagers to change
// the default admin address to something more appropriate for
// that platform. See #5317 for discussion.
if env, exists := os.LookupEnv("CADDY_ADMIN"); exists {
DefaultAdminListen = env
}
}
// AdminConfig configures Caddy's API endpoint, which is used
// to manage Caddy while it is running.
type AdminConfig struct {
@@ -68,9 +56,7 @@ type AdminConfig struct {
// The address to which the admin endpoint's listener should
// bind itself. Can be any single network address that can be
// parsed by Caddy. Accepts placeholders.
// Default: the value of the `CADDY_ADMIN` environment variable,
// or `localhost:2019` otherwise.
// parsed by Caddy. Default: localhost:2019
Listen string `json:"listen,omitempty"`
// If true, CORS headers will be emitted, and requests to the
@@ -169,7 +155,7 @@ type IdentityConfig struct {
//
// EXPERIMENTAL: Subject to change.
type RemoteAdmin struct {
// The address on which to start the secure listener. Accepts placeholders.
// The address on which to start the secure listener.
// Default: :2021
Listen string `json:"listen,omitempty"`
@@ -352,19 +338,17 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
// that there is always an admin server (unless it is explicitly
// configured to be disabled).
func replaceLocalAdminServer(cfg *Config) error {
// always* be sure to close down the old admin endpoint
// always be sure to close down the old admin endpoint
// as gracefully as possible, even if the new one is
// disabled -- careful to use reference to the current
// (old) admin endpoint since it will be different
// when the function returns
// (* except if the new one fails to start)
oldAdminServer := localAdminServer
var err error
defer func() {
// do the shutdown asynchronously so that any
// current API request gets a response; this
// goroutine may last a few seconds
if oldAdminServer != nil && err == nil {
if oldAdminServer != nil {
go func(oldAdminServer *http.Server) {
err := stopAdminServer(oldAdminServer)
if err != nil {
@@ -395,7 +379,7 @@ func replaceLocalAdminServer(cfg *Config) error {
handler := cfg.Admin.newAdminHandler(addr, false)
ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{})
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
if err != nil {
return err
}
@@ -416,7 +400,7 @@ func replaceLocalAdminServer(cfg *Config) error {
serverMu.Lock()
server := localAdminServer
serverMu.Unlock()
if err := server.Serve(ln.(net.Listener)); !errors.Is(err, http.ErrServerClosed) {
if err := server.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
}
}()
@@ -455,7 +439,7 @@ func manageIdentity(ctx Context, cfg *Config) error {
if err != nil {
return fmt.Errorf("loading identity issuer modules: %s", err)
}
for _, issVal := range val.([]any) {
for _, issVal := range val.([]interface{}) {
cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer))
}
}
@@ -562,11 +546,10 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
serverMu.Unlock()
// start listener
lnAny, err := addr.Listen(ctx, 0, net.ListenConfig{})
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
if err != nil {
return err
}
ln := lnAny.(net.Listener)
ln = tls.NewListener(ln, tlsConfig)
go func() {
@@ -585,13 +568,12 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
}
func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool) *certmagic.Config {
var cmCfg *certmagic.Config
if ident == nil {
// 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
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)
Logger: logger,
Issuers: ident.issuers,
@@ -601,11 +583,9 @@ func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool)
GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
return cmCfg, nil
},
Logger: logger.Named("cache"),
})
}
cmCfg = certmagic.New(identityCertCache, template)
return cmCfg
return certmagic.New(identityCertCache, *cmCfg)
}
// IdentityCredentials returns this instance's configured, managed identity credentials
@@ -914,36 +894,16 @@ func (h adminHandler) originAllowed(origin *url.URL) bool {
return false
}
// etagHasher returns a the hasher we used on the config to both
// produce and verify ETags.
func etagHasher() hash.Hash32 { return fnv.New32a() }
// makeEtag returns an Etag header value (including quotes) for
// the given config path and hash of contents at that path.
func makeEtag(path string, hash hash.Hash) string {
return fmt.Sprintf(`"%s %x"`, path, hash.Sum(nil))
}
func handleConfig(w http.ResponseWriter, r *http.Request) error {
switch r.Method {
case http.MethodGet:
w.Header().Set("Content-Type", "application/json")
// Set the ETag as a trailer header.
// The alternative is to write the config to a buffer, and
// then hash that.
w.Header().Set("Trailer", "ETag")
hash := etagHasher()
configWriter := io.MultiWriter(w, hash)
err := readConfig(r.URL.Path, configWriter)
err := readConfig(r.URL.Path, w)
if err != nil {
return APIError{HTTPStatus: http.StatusBadRequest, Err: err}
}
// we could consider setting up a sync.Pool for the summed
// hashes to reduce GC pressure.
w.Header().Set("Etag", makeEtag(r.URL.Path, hash))
return nil
case http.MethodPost,
@@ -977,7 +937,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
err := changeConfig(r.Method, r.URL.Path, body, r.Header.Get("If-Match"), forceReload)
err := changeConfig(r.Method, r.URL.Path, body, forceReload)
if err != nil && !errors.Is(err, errSameConfig) {
return err
}
@@ -1011,9 +971,9 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
id := parts[2]
// map the ID to the expanded path
currentCtxMu.RLock()
currentCfgMu.RLock()
expanded, ok := rawCfgIndex[id]
defer currentCtxMu.RUnlock()
defer currentCfgMu.RUnlock()
if !ok {
return APIError{
HTTPStatus: http.StatusNotFound,
@@ -1036,6 +996,10 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
}
}
if err := notify.NotifyStopping(); err != nil {
Log().Error("unable to notify stopping to service manager", zap.Error(err))
}
exitProcess(context.Background(), Log().Named("admin.api"))
return nil
}
@@ -1044,11 +1008,11 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
// the operation at path according to method, using body and out as
// needed. This is a low-level, unsynchronized function; most callers
// will want to use changeConfig or readConfig instead. This requires a
// read or write lock on currentCtxMu, depending on method (GET needs
// read or write lock on currentCfgMu, depending on method (GET needs
// only a read lock; all others need a write lock).
func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error {
var err error
var val any
var val interface{}
// if there is a request body, decode it into the
// variable that will be set in the config according
@@ -1085,16 +1049,16 @@ func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error
parts = parts[:len(parts)-1]
}
var ptr any = rawCfg
var ptr interface{} = rawCfg
traverseLoop:
for i, part := range parts {
switch v := ptr.(type) {
case map[string]any:
case map[string]interface{}:
// if the next part enters a slice, and the slice is our destination,
// handle it specially (because appending to the slice copies the slice
// header, which does not replace the original one like we want)
if arr, ok := v[part].([]any); ok && i == len(parts)-2 {
if arr, ok := v[part].([]interface{}); ok && i == len(parts)-2 {
var idx int
if method != http.MethodPost {
idxStr := parts[len(parts)-1]
@@ -1116,7 +1080,7 @@ traverseLoop:
}
case http.MethodPost:
if ellipses {
valArray, ok := val.([]any)
valArray, ok := val.([]interface{})
if !ok {
return fmt.Errorf("final element is not an array")
}
@@ -1151,9 +1115,9 @@ traverseLoop:
case http.MethodPost:
// if the part is an existing list, POST appends to
// it, otherwise it just sets or creates the value
if arr, ok := v[part].([]any); ok {
if arr, ok := v[part].([]interface{}); ok {
if ellipses {
valArray, ok := val.([]any)
valArray, ok := val.([]interface{})
if !ok {
return fmt.Errorf("final element is not an array")
}
@@ -1184,12 +1148,12 @@ traverseLoop:
// might not exist yet; that's OK but we need to make them as
// we go, while we still have a pointer from the level above
if v[part] == nil && method == http.MethodPut {
v[part] = make(map[string]any)
v[part] = make(map[string]interface{})
}
ptr = v[part]
}
case []any:
case []interface{}:
partInt, err := strconv.Atoi(part)
if err != nil {
return fmt.Errorf("[/%s] invalid array index '%s': %v",
@@ -1211,7 +1175,7 @@ traverseLoop:
// RemoveMetaFields removes meta fields like "@id" from a JSON message
// by using a simple regular expression. (An alternate way to do this
// would be to delete them from the raw, map[string]any
// would be to delete them from the raw, map[string]interface{}
// representation as they are indexed, then iterate the index we made
// and add them back after encoding as JSON, but this is simpler.)
func RemoveMetaFields(rawJSON []byte) []byte {
@@ -1263,10 +1227,7 @@ func (e APIError) Error() string {
// parseAdminListenAddr extracts a singular listen address from either addr
// or defaultAddr, returning the network and the address of the listener.
func parseAdminListenAddr(addr string, defaultAddr string) (NetworkAddress, error) {
input, err := NewReplacer().ReplaceOrErr(addr, true, true)
if err != nil {
return NetworkAddress{}, fmt.Errorf("replacing listen address: %v", err)
}
input := addr
if input == "" {
input = defaultAddr
}
@@ -1346,7 +1307,7 @@ const (
)
var bufPool = sync.Pool{
New: func() any {
New: func() interface{} {
return new(bytes.Buffer)
},
}
+2 -51
View File
@@ -16,8 +16,6 @@ package caddy
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"sync"
"testing"
@@ -115,7 +113,7 @@ func TestUnsyncedConfigAccess(t *testing.T) {
}
// decode the expected config so we can do a convenient DeepEqual
var expectedDecoded any
var expectedDecoded interface{}
err = json.Unmarshal([]byte(tc.expect), &expectedDecoded)
if err != nil {
t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err)
@@ -141,57 +139,10 @@ func TestLoadConcurrent(t *testing.T) {
wg.Done()
}()
}
wg.Wait()
}
type fooModule struct {
IntField int
StrField string
}
func (fooModule) CaddyModule() ModuleInfo {
return ModuleInfo{
ID: "foo",
New: func() Module { return new(fooModule) },
}
}
func (fooModule) Start() error { return nil }
func (fooModule) Stop() error { return nil }
func TestETags(t *testing.T) {
RegisterModule(fooModule{})
if err := Load([]byte(`{"admin": {"listen": "localhost:2999"}, "apps": {"foo": {"strField": "abc", "intField": 0}}}`), true); err != nil {
t.Fatalf("loading: %s", err)
}
const key = "/" + rawConfigKey + "/apps/foo"
// try update the config with the wrong etag
err := changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}}`), fmt.Sprintf(`"/%s not_an_etag"`, rawConfigKey), false)
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
t.Fatalf("expected precondition failed; got %v", err)
}
// get the etag
hash := etagHasher()
if err := readConfig(key, hash); err != nil {
t.Fatalf("reading: %s", err)
}
// do the same update with the correct key
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}`), makeEtag(key, hash), false)
if err != nil {
t.Fatalf("expected update to work; got %v", err)
}
// now try another update. The hash should no longer match and we should get precondition failed
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 2}`), makeEtag(key, hash), false)
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
t.Fatalf("expected precondition failed; got %v", err)
}
}
func BenchmarkLoad(b *testing.B) {
for i := 0; i < b.N; i++ {
Load(testCfg, true)
+76 -261
View File
@@ -17,7 +17,6 @@ package caddy
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
@@ -31,7 +30,6 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/caddyserver/caddy/v2/notify"
@@ -103,32 +101,20 @@ func Run(cfg *Config) error {
// if it is different from the current config or
// forceReload is true.
func Load(cfgJSON []byte, forceReload bool) error {
if err := notify.Reloading(); err != nil {
Log().Error("unable to notify service manager of reloading state", zap.Error(err))
if err := notify.NotifyReloading(); err != nil {
Log().Error("unable to notify reloading to service manager", zap.Error(err))
}
// after reload, notify system of success or, if
// failure, update with status (error message)
var err error
defer func() {
if err != nil {
if notifyErr := notify.Error(err, 0); notifyErr != nil {
Log().Error("unable to notify to service manager of reload error",
zap.Error(notifyErr),
zap.String("reload_err", err.Error()))
}
return
}
if err := notify.Ready(); err != nil {
Log().Error("unable to notify to service manager of ready state", zap.Error(err))
if err := notify.NotifyReadiness(); err != nil {
Log().Error("unable to notify readiness to service manager", zap.Error(err))
}
}()
err = changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, "", forceReload)
err := changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
if errors.Is(err, errSameConfig) {
err = nil // not really an error
}
return err
}
@@ -139,14 +125,7 @@ func Load(cfgJSON []byte, forceReload bool) error {
// occur unless forceReload is true. If the config is unchanged and not
// forcefully reloaded, then errConfigUnchanged This function is safe for
// concurrent use.
// The ifMatchHeader can optionally be given a string of the format:
//
// "<path> <hash>"
//
// where <path> is the absolute path in the config and <hash> is the expected hash of
// the config at that path. If the hash in the ifMatchHeader doesn't match
// the hash of the config, then an APIError with status 412 will be returned.
func changeConfig(method, path string, input []byte, ifMatchHeader string, forceReload bool) error {
func changeConfig(method, path string, input []byte, forceReload bool) error {
switch method {
case http.MethodGet,
http.MethodHead,
@@ -156,42 +135,8 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
return fmt.Errorf("method not allowed")
}
currentCtxMu.Lock()
defer currentCtxMu.Unlock()
if ifMatchHeader != "" {
// expect the first and last character to be quotes
if len(ifMatchHeader) < 2 || ifMatchHeader[0] != '"' || ifMatchHeader[len(ifMatchHeader)-1] != '"' {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed If-Match header; expect quoted string"),
}
}
// read out the parts
parts := strings.Fields(ifMatchHeader[1 : len(ifMatchHeader)-1])
if len(parts) != 2 {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed If-Match header; expect format \"<path> <hash>\""),
}
}
// get the current hash of the config
// at the given path
hash := etagHasher()
err := unsyncedConfigAccess(http.MethodGet, parts[0], nil, hash)
if err != nil {
return err
}
if hex.EncodeToString(hash.Sum(nil)) != parts[1] {
return APIError{
HTTPStatus: http.StatusPreconditionFailed,
Err: fmt.Errorf("If-Match header did not match current config hash"),
}
}
}
currentCfgMu.Lock()
defer currentCfgMu.Unlock()
err := unsyncedConfigAccess(method, path, input, nil)
if err != nil {
@@ -232,7 +177,7 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
// with what caddy is still running; we need to
// unmarshal it again because it's likely that
// pointers deep in our rawCfg map were modified
var oldCfg any
var oldCfg interface{}
err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
if err2 != nil {
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
@@ -257,18 +202,18 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
// readConfig traverses the current config to path
// and writes its JSON encoding to out.
func readConfig(path string, out io.Writer) error {
currentCtxMu.RLock()
defer currentCtxMu.RUnlock()
currentCfgMu.RLock()
defer currentCfgMu.RUnlock()
return unsyncedConfigAccess(http.MethodGet, path, nil, out)
}
// indexConfigObjects recursively searches ptr for object fields named
// "@id" and maps that ID value to the full configPath in the index.
// This function is NOT safe for concurrent access; obtain a write lock
// on currentCtxMu.
func indexConfigObjects(ptr any, configPath string, index map[string]string) error {
// on currentCfgMu.
func indexConfigObjects(ptr interface{}, configPath string, index map[string]string) error {
switch val := ptr.(type) {
case map[string]any:
case map[string]interface{}:
for k, v := range val {
if k == idKey {
switch idVal := v.(type) {
@@ -287,7 +232,7 @@ func indexConfigObjects(ptr any, configPath string, index map[string]string) err
return err
}
}
case []any:
case []interface{}:
// traverse each element of the array recursively
for i := range val {
err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index)
@@ -305,7 +250,7 @@ func indexConfigObjects(ptr any, configPath string, index map[string]string) err
// it as the new config, replacing any other current config.
// It does NOT update the raw config state, as this is a
// lower-level function; most callers will want to use Load
// instead. A write lock on currentCtxMu is required! If
// instead. A write lock on currentCfgMu is required! If
// allowPersist is false, it will not be persisted to disk,
// even if it is configured to.
func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
@@ -334,17 +279,17 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
}
// run the new config and start all its apps
ctx, err := run(newCfg, true)
err = run(newCfg, true)
if err != nil {
return err
}
// swap old context (including its config) with the new one
oldCtx := currentCtx
currentCtx = ctx
// swap old config with the new one
oldCfg := currentCfg
currentCfg = newCfg
// Stop, Cleanup each old app
unsyncedStop(oldCtx)
unsyncedStop(oldCfg)
// autosave a non-nil config, if not disabled
if allowPersist &&
@@ -388,7 +333,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
// This is a low-level function; most callers
// will want to use Run instead, which also
// updates the config's raw state.
func run(newCfg *Config, start bool) (Context, error) {
func run(newCfg *Config, start bool) error {
// because we will need to roll back any state
// modifications if this function errors, we
// keep a single error value and scope all
@@ -419,8 +364,8 @@ func run(newCfg *Config, start bool) (Context, error) {
cancel()
// also undo any other state changes we made
if currentCtx.cfg != nil {
certmagic.Default.Storage = currentCtx.cfg.storage
if currentCfg != nil {
certmagic.Default.Storage = currentCfg.storage
}
}
}()
@@ -432,14 +377,14 @@ func run(newCfg *Config, start bool) (Context, error) {
}
err = newCfg.Logging.openLogs(ctx)
if err != nil {
return ctx, err
return err
}
// start the admin endpoint (and stop any prior one)
if start {
err = replaceLocalAdminServer(newCfg)
if err != nil {
return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
return fmt.Errorf("starting caddy administration endpoint: %v", err)
}
}
@@ -468,7 +413,7 @@ func run(newCfg *Config, start bool) (Context, error) {
return nil
}()
if err != nil {
return ctx, err
return err
}
// Load and Provision each app and their submodules
@@ -481,18 +426,18 @@ func run(newCfg *Config, start bool) (Context, error) {
return nil
}()
if err != nil {
return ctx, err
return err
}
if !start {
return ctx, nil
return nil
}
// Provision any admin routers which may need to access
// some of the other apps at runtime
err = newCfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return ctx, err
return err
}
// Start
@@ -517,12 +462,12 @@ func run(newCfg *Config, start bool) (Context, error) {
return nil
}()
if err != nil {
return ctx, err
return err
}
// now that the user's config is running, finish setting up anything else,
// such as remote admin endpoint, config loader, etc.
return ctx, finishSettingUp(ctx, newCfg)
return finishSettingUp(ctx, newCfg)
}
// finishSettingUp should be run after all apps have successfully started.
@@ -555,7 +500,7 @@ func finishSettingUp(ctx Context, cfg *Config) error {
runLoadedConfig := func(config []byte) error {
logger.Info("applying dynamically-loaded config")
err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, "", false)
err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, false)
if errors.Is(err, errSameConfig) {
return err
}
@@ -627,10 +572,10 @@ type ConfigLoader interface {
// stop the others. Stop should only be called
// if not replacing with a new config.
func Stop() error {
currentCtxMu.Lock()
defer currentCtxMu.Unlock()
unsyncedStop(currentCtx)
currentCtx = Context{}
currentCfgMu.Lock()
defer currentCfgMu.Unlock()
unsyncedStop(currentCfg)
currentCfg = nil
rawCfgJSON = nil
rawCfgIndex = nil
rawCfg[rawConfigKey] = nil
@@ -643,13 +588,13 @@ func Stop() error {
// it is logged and the function continues stopping
// the next app. This function assumes all apps in
// cfg were successfully started first.
func unsyncedStop(ctx Context) {
if ctx.cfg == nil {
func unsyncedStop(cfg *Config) {
if cfg == nil {
return
}
// stop each app
for name, a := range ctx.cfg.apps {
for name, a := range cfg.apps {
err := a.Stop()
if err != nil {
log.Printf("[ERROR] stop %s: %v", name, err)
@@ -657,13 +602,13 @@ func unsyncedStop(ctx Context) {
}
// clean up all modules
ctx.cfg.cancelFunc()
cfg.cancelFunc()
}
// Validate loads, provisions, and validates
// cfg, but does not start running it.
func Validate(cfg *Config) error {
_, err := run(cfg, false)
err := run(cfg, false)
if err == nil {
cfg.cancelFunc() // call Cleanup on all modules
}
@@ -677,14 +622,6 @@ func Validate(cfg *Config) error {
// Errors are logged along the way, and an appropriate exit
// code is emitted.
func exitProcess(ctx context.Context, logger *zap.Logger) {
// let the rest of the program know we're quitting
atomic.StoreInt32(exiting, 1)
// give the OS or service/process manager our 2 weeks' notice: we quit
if err := notify.Stopping(); err != nil {
Log().Error("unable to notify service manager of stopping state", zap.Error(err))
}
if logger == nil {
logger = Log()
}
@@ -744,12 +681,6 @@ func exitProcess(ctx context.Context, logger *zap.Logger) {
}()
}
var exiting = new(int32) // accessed atomically
// Exiting returns true if the process is exiting.
// EXPERIMENTAL API: subject to change or removal.
func Exiting() bool { return atomic.LoadInt32(exiting) == 1 }
// Duration can be an integer or a string. An integer is
// interpreted as nanoseconds. If a string, it is a Go
// time.Duration value such as `300ms`, `1.5h`, or `2h45m`;
@@ -774,12 +705,8 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
// ParseDuration parses a duration string, adding
// support for the "d" unit meaning number of days,
// where a day is assumed to be 24h. The maximum
// input string length is 1024.
// where a day is assumed to be 24h.
func ParseDuration(s string) (time.Duration, error) {
if len(s) > 1024 {
return 0, fmt.Errorf("parsing duration: input string too long")
}
var inNumber bool
var numStart int
for i := 0; i < len(s); i++ {
@@ -824,144 +751,36 @@ func InstanceID() (uuid.UUID, error) {
return uuid.ParseBytes(uuidFileBytes)
}
// CustomVersion is an optional string that overrides Caddy's
// reported version. It can be helpful when downstream packagers
// need to manually set Caddy's version. If no other version
// information is available, the short form version (see
// Version()) will be set to CustomVersion, and the full version
// will include CustomVersion at the beginning.
//
// Set this variable during `go build` with `-ldflags`:
//
// -ldflags '-X github.com/caddyserver/caddy/v2.CustomVersion=v2.6.2'
//
// for example.
var CustomVersion string
// Version returns the Caddy version in a simple/short form, and
// a full version string. The short form will not have spaces and
// is intended for User-Agent strings and similar, but may be
// omitting valuable information. Note that Caddy must be compiled
// in a special way to properly embed complete version information.
// First this function tries to get the version from the embedded
// build info provided by go.mod dependencies; then it tries to
// get info from embedded VCS information, which requires having
// built Caddy from a git repository. If no version is available,
// this function returns "(devel)" because Go uses that, but for
// the simple form we change it to "unknown". If still no version
// is available (e.g. no VCS repo), then it will use CustomVersion;
// CustomVersion is always prepended to the full version string.
//
// See relevant Go issues: https://github.com/golang/go/issues/29228
// and https://github.com/golang/go/issues/50603.
//
// This function is experimental and subject to change or removal.
func Version() (simple, full string) {
// the currently-recommended way to build Caddy involves
// building it as a dependency so we can extract version
// information from go.mod tooling; once the upstream
// Go issues are fixed, we should just be able to use
// bi.Main... hopefully.
var module *debug.Module
bi, ok := debug.ReadBuildInfo()
if !ok {
if CustomVersion != "" {
full = CustomVersion
simple = CustomVersion
return
}
full = "unknown"
simple = "unknown"
return
}
// find the Caddy module in the dependency list
for _, dep := range bi.Deps {
if dep.Path == ImportPath {
module = dep
break
}
}
if module != nil {
simple, full = module.Version, module.Version
if module.Sum != "" {
full += " " + module.Sum
}
if module.Replace != nil {
full += " => " + module.Replace.Path
if module.Replace.Version != "" {
simple = module.Replace.Version + "_custom"
full += "@" + module.Replace.Version
}
if module.Replace.Sum != "" {
full += " " + module.Replace.Sum
}
}
}
if full == "" {
var vcsRevision string
var vcsTime time.Time
var vcsModified bool
for _, setting := range bi.Settings {
switch setting.Key {
case "vcs.revision":
vcsRevision = setting.Value
case "vcs.time":
vcsTime, _ = time.Parse(time.RFC3339, setting.Value)
case "vcs.modified":
vcsModified, _ = strconv.ParseBool(setting.Value)
}
}
if vcsRevision != "" {
var modified string
if vcsModified {
modified = "+modified"
}
full = fmt.Sprintf("%s%s (%s)", vcsRevision, modified, vcsTime.Format(time.RFC822))
simple = vcsRevision
// use short checksum for simple, if hex-only
if _, err := hex.DecodeString(simple); err == nil {
simple = simple[:8]
}
// append date to simple since it can be convenient
// to know the commit date as part of the version
if !vcsTime.IsZero() {
simple += "-" + vcsTime.Format("20060102")
}
}
}
if full == "" {
if CustomVersion != "" {
full = CustomVersion
} else {
full = "unknown"
}
} else if CustomVersion != "" {
full = CustomVersion + " " + full
}
if simple == "" || simple == "(devel)" {
if CustomVersion != "" {
simple = CustomVersion
} else {
simple = "unknown"
}
}
return
// GoModule returns the build info of this Caddy
// build from debug.BuildInfo (requires Go modules).
// If no version information is available, a non-nil
// value will still be returned, but with an
// unknown version.
func GoModule() *debug.Module {
var mod debug.Module
return goModule(&mod)
}
// ActiveContext returns the currently-active context.
// This function is experimental and might be changed
// or removed in the future.
func ActiveContext() Context {
currentCtxMu.RLock()
defer currentCtxMu.RUnlock()
return currentCtx
// goModule holds the actual implementation of GoModule.
// Allocating debug.Module in GoModule() and passing a
// reference to goModule enables mid-stack inlining.
func goModule(mod *debug.Module) *debug.Module {
mod.Version = "unknown"
bi, ok := debug.ReadBuildInfo()
if ok {
mod.Path = bi.Main.Path
// The recommended way to build Caddy involves
// creating a separate main module, which
// TODO: track related Go issue: https://github.com/golang/go/issues/29228
// once that issue is fixed, we should just be able to use bi.Main... hopefully.
for _, dep := range bi.Deps {
if dep.Path == ImportPath {
return dep
}
}
return &bi.Main
}
return mod
}
// CtxKey is a value type for use with context.WithValue.
@@ -969,21 +788,18 @@ type CtxKey string
// This group of variables pertains to the current configuration.
var (
// currentCtxMu protects everything in this var block.
currentCtxMu sync.RWMutex
// currentCfgMu protects everything in this var block.
currentCfgMu sync.RWMutex
// currentCtx is the root context for the currently-running
// configuration, which can be accessed through this value.
// If the Config contained in this value is not nil, then
// a config is currently active/running.
currentCtx Context
// currentCfg is the currently-running configuration.
currentCfg *Config
// rawCfg is the current, generic-decoded configuration;
// we initialize it as a map with one field ("config")
// to maintain parity with the API endpoint and to avoid
// the special case of having to access/mutate the variable
// directly without traversing into it.
rawCfg = map[string]any{
rawCfg = map[string]interface{}{
rawConfigKey: nil,
}
@@ -1002,5 +818,4 @@ var (
var errSameConfig = errors.New("config is unchanged")
// ImportPath is the package import path for Caddy core.
// This identifier may be removed in the future.
const ImportPath = "github.com/caddyserver/caddy/v2"
+6 -6
View File
@@ -29,12 +29,12 @@ type Adapter struct {
}
// Adapt converts the Caddyfile config in body to Caddy JSON.
func (a Adapter) Adapt(body []byte, options map[string]any) ([]byte, []caddyconfig.Warning, error) {
func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []caddyconfig.Warning, error) {
if a.ServerType == nil {
return nil, nil, fmt.Errorf("no server type")
}
if options == nil {
options = make(map[string]any)
options = make(map[string]interface{})
}
filename, _ := options["filename"].(string)
@@ -54,7 +54,7 @@ func (a Adapter) Adapt(body []byte, options map[string]any) ([]byte, []caddyconf
// lint check: see if input was properly formatted; sometimes messy files files parse
// 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)
}
@@ -63,10 +63,10 @@ func (a Adapter) Adapt(body []byte, options map[string]any) ([]byte, []caddyconf
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.
// 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
normalizedBody := bytes.Replace(body, []byte("\r\n"), []byte("\n"), -1)
@@ -116,7 +116,7 @@ type ServerType interface {
// (e.g. CLI flags) and creates a Caddy
// config, along with any warnings or
// an error.
Setup([]ServerBlock, map[string]any) (*caddy.Config, []caddyconfig.Warning, error)
Setup([]ServerBlock, map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error)
}
// UnmarshalModule instantiates a module with the given ID and invokes
+6 -6
View File
@@ -146,15 +146,15 @@ func (d *Dispenser) NextLine() bool {
//
// Proper use of this method looks like this:
//
// for nesting := d.Nesting(); d.NextBlock(nesting); {
// }
// for nesting := d.Nesting(); d.NextBlock(nesting); {
// }
//
// However, in simple cases where it is known that the
// Dispenser is new and has not already traversed state
// by a loop over NextBlock(), this will do:
//
// for d.NextBlock(0) {
// }
// for d.NextBlock(0) {
// }
//
// As with other token parsing logic, a loop over
// NextBlock() should be contained within a loop over
@@ -217,7 +217,7 @@ func (d *Dispenser) ValRaw() string {
// ScalarVal gets value of the current token, converted to the closest
// scalar type. If there is no token loaded, it returns nil.
func (d *Dispenser) ScalarVal() any {
func (d *Dispenser) ScalarVal() interface{} {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return nil
}
@@ -412,7 +412,7 @@ func (d *Dispenser) Err(msg string) error {
}
// Errf is like Err, but for formatted error messages
func (d *Dispenser) Errf(format string, args ...any) error {
func (d *Dispenser) Errf(format string, args ...interface{}) error {
return d.WrapErr(fmt.Errorf(format, args...))
}
+1 -4
View File
@@ -153,10 +153,7 @@ func Format(input []byte) []byte {
openBraceWritten = true
nextLine()
newLines = 0
// prevent infinite nesting from ridiculous inputs (issue #4169)
if nesting < 10 {
nesting++
}
nesting++
}
switch {
+1
View File
@@ -13,6 +13,7 @@
// limitations under the License.
//go:build gofuzz
// +build gofuzz
package caddyfile
-4
View File
@@ -191,7 +191,3 @@ func Tokenize(input []byte, filename string) ([]Token, error) {
}
return tokens, nil
}
func (t Token) Quoted() bool {
return t.wasQuoted > 0
}
+1
View File
@@ -13,6 +13,7 @@
// limitations under the License.
//go:build gofuzz
// +build gofuzz
package caddyfile
+11 -23
View File
@@ -61,12 +61,20 @@ func Parse(filename string, input []byte) ([]ServerBlock, error) {
// It returns all the tokens from the input, unstructured
// and in order. It may mutate input as it expands env vars.
func allTokens(filename string, input []byte) ([]Token, error) {
return Tokenize(replaceEnvVars(input), filename)
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.
// It mutates the underlying array and returns the updated slice.
func replaceEnvVars(input []byte) []byte {
func replaceEnvVars(input []byte) ([]byte, error) {
var offset int
for {
begin := bytes.Index(input[offset:], spanOpen)
@@ -107,7 +115,7 @@ func replaceEnvVars(input []byte) []byte {
// continue at the end of the replacement
offset = begin + len(envVarBytes)
}
return input
return input, nil
}
type parser struct {
@@ -389,20 +397,6 @@ func (p *parser) doImport() error {
} else {
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
@@ -465,12 +459,6 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
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)
if err != nil {
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
+4 -18
View File
@@ -187,23 +187,6 @@ func TestParseOneAndImport(t *testing.T) {
{`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{}},
@@ -621,7 +604,10 @@ func TestEnvironmentReplacement(t *testing.T) {
expect: "}{$",
},
} {
actual := replaceEnvVars([]byte(test.input))
actual, err := replaceEnvVars([]byte(test.input))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(actual, []byte(test.expect)) {
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
-7
View File
@@ -1,7 +0,0 @@
 
+5 -5
View File
@@ -24,7 +24,7 @@ import (
// Adapter is a type which can adapt a configuration to Caddy JSON.
// It returns the results and any warnings, or an error.
type Adapter interface {
Adapt(body []byte, options map[string]any) ([]byte, []Warning, error)
Adapt(body []byte, options map[string]interface{}) ([]byte, []Warning, error)
}
// Warning represents a warning or notice related to conversion.
@@ -48,7 +48,7 @@ func (w Warning) String() string {
// are converted to warnings. This is convenient when filling config
// structs that require a json.RawMessage, without having to worry
// about errors.
func JSON(val any, warnings *[]Warning) json.RawMessage {
func JSON(val interface{}, warnings *[]Warning) json.RawMessage {
b, err := json.Marshal(val)
if err != nil {
if warnings != nil {
@@ -64,9 +64,9 @@ func JSON(val any, warnings *[]Warning) json.RawMessage {
// for encoding module values where the module name has to be described within
// the object by a certain key; for example, `"handler": "file_server"` for a
// file server HTTP handler (fieldName="handler" and fieldVal="file_server").
// The val parameter must encode into a map[string]any (i.e. it must be
// The val parameter must encode into a map[string]interface{} (i.e. it must be
// a struct or map). Any errors are converted into warnings.
func JSONModuleObject(val any, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage {
func JSONModuleObject(val interface{}, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage {
// encode to a JSON object first
enc, err := json.Marshal(val)
if err != nil {
@@ -77,7 +77,7 @@ func JSONModuleObject(val any, fieldName, fieldVal string, warnings *[]Warning)
}
// then decode the object
var tmp map[string]any
var tmp map[string]interface{}
err = json.Unmarshal(enc, &tmp)
if err != nil {
if warnings != nil {
+18 -35
View File
@@ -17,7 +17,6 @@ package httpcaddyfile
import (
"fmt"
"net"
"net/netip"
"reflect"
"sort"
"strconv"
@@ -36,12 +35,12 @@ import (
// server block that share the same address stay grouped together so the config
// isn't repeated unnecessarily. For example, this Caddyfile:
//
// example.com {
// bind 127.0.0.1
// }
// www.example.com, example.net/path, localhost:9999 {
// bind 127.0.0.1 1.2.3.4
// }
// example.com {
// bind 127.0.0.1
// }
// www.example.com, example.net/path, localhost:9999 {
// bind 127.0.0.1 1.2.3.4
// }
//
// has two server blocks to start with. But expressed in this Caddyfile are
// actually 4 listener addresses: 127.0.0.1:443, 1.2.3.4:443, 127.0.0.1:9999,
@@ -77,7 +76,7 @@ import (
// multiple addresses to the same lists of server blocks (a many:many mapping).
// (Doing this is essentially a map-reduce technique.)
func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock,
options map[string]any) (map[string][]serverBlock, error) {
options map[string]interface{}) (map[string][]serverBlock, error) {
sbmap := make(map[string][]serverBlock)
for i, sblock := range originalServerBlocks {
@@ -184,10 +183,8 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
return sbaddrs
}
// listenerAddrsForServerBlockKey essentially converts the Caddyfile
// site addresses to Caddy listener addresses for each server block.
func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string,
options map[string]any) ([]string, error) {
options map[string]interface{}) ([]string, error) {
addr, err := ParseAddress(key)
if err != nil {
return nil, fmt.Errorf("parsing key: %v", err)
@@ -219,7 +216,7 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
return nil, fmt.Errorf("[%s] scheme and port violate convention", key)
}
// the bind directive specifies hosts (and potentially network), but is optional
// the bind directive specifies hosts, but is optional
lnHosts := make([]string, 0, len(sblock.pile["bind"]))
for _, cfgVal := range sblock.pile["bind"] {
lnHosts = append(lnHosts, cfgVal.Value.([]string)...)
@@ -234,27 +231,13 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
// use a map to prevent duplication
listeners := make(map[string]struct{})
for _, lnHost := range lnHosts {
// normally we would simply append the port,
// but if lnHost is IPv6, we need to ensure it
// is enclosed in [ ]; net.JoinHostPort does
// this for us, but lnHost might also have a
// network type in front (e.g. "tcp/") leading
// to "[tcp/::1]" which causes parsing failures
// later; what we need is "tcp/[::1]", so we have
// to split the network and host, then re-combine
network, host, ok := strings.Cut(lnHost, "/")
if !ok {
host = network
network = ""
for _, host := range lnHosts {
addr, err := caddy.ParseNetworkAddress(host)
if err == nil && addr.IsUnixNetwork() {
listeners[host] = struct{}{}
} else {
listeners[host+":"+lnPort] = struct{}{}
}
host = strings.Trim(host, "[]") // IPv6
networkAddr := caddy.JoinNetworkAddress(network, host, lnPort)
addr, err := caddy.ParseNetworkAddress(networkAddr)
if err != nil {
return nil, fmt.Errorf("parsing network address: %v", err)
}
listeners[addr.String()] = struct{}{}
}
// now turn map into list
@@ -367,9 +350,9 @@ func (a Address) Normalize() Address {
// ensure host is normalized if it's an IP address
host := strings.TrimSpace(a.Host)
if ip, err := netip.ParseAddr(host); err == nil {
if ip.Is6() && !ip.Is4() && !ip.Is4In6() {
host = ip.String()
if ip := net.ParseIP(host); ip != nil {
if ipv6 := ip.To16(); ipv6 != nil && ipv6.DefaultMask() == nil {
host = ipv6.String()
}
}
@@ -13,6 +13,7 @@
// limitations under the License.
//go:build gofuzz
// +build gofuzz
package httpcaddyfile
+48 -142
View File
@@ -24,7 +24,6 @@ import (
"reflect"
"strconv"
"strings"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -49,12 +48,12 @@ func init() {
RegisterHandlerDirective("handle", parseHandle)
RegisterDirective("handle_errors", parseHandleErrors)
RegisterDirective("log", parseLog)
RegisterHandlerDirective("skip_log", parseSkipLog)
}
// parseBind parses the bind directive. Syntax:
//
// bind <addresses...>
// bind <addresses...>
//
func parseBind(h Helper) ([]ConfigValue, error) {
var lnHosts []string
for h.Next() {
@@ -65,34 +64,28 @@ func parseBind(h Helper) ([]ConfigValue, error) {
// parseTLS parses the tls directive. Syntax:
//
// tls [<email>|internal]|[<cert_file> <key_file>] {
// protocols <min> [<max>]
// ciphers <cipher_suites...>
// curves <curves...>
// client_auth {
// mode [request|require|verify_if_given|require_and_verify]
// trusted_ca_cert <base64_der>
// trusted_ca_cert_file <filename>
// trusted_leaf_cert <base64_der>
// trusted_leaf_cert_file <filename>
// }
// alpn <values...>
// load <paths...>
// ca <acme_ca_endpoint>
// ca_root <pem_file>
// key_type [ed25519|p256|p384|rsa2048|rsa4096]
// dns <provider_name> [...]
// propagation_delay <duration>
// propagation_timeout <duration>
// resolvers <dns_servers...>
// dns_ttl <duration>
// dns_challenge_override_domain <domain>
// on_demand
// eab <key_id> <mac_key>
// issuer <module_name> [...]
// get_certificate <module_name> [...]
// insecure_secrets_log <log_file>
// }
// tls [<email>|internal]|[<cert_file> <key_file>] {
// protocols <min> [<max>]
// ciphers <cipher_suites...>
// curves <curves...>
// client_auth {
// mode [request|require|verify_if_given|require_and_verify]
// trusted_ca_cert <base64_der>
// trusted_ca_cert_file <filename>
// trusted_leaf_cert <base64_der>
// trusted_leaf_cert_file <filename>
// }
// alpn <values...>
// load <paths...>
// ca <acme_ca_endpoint>
// ca_root <pem_file>
// dns <provider_name> [...]
// on_demand
// eab <key_id> <mac_key>
// issuer <module_name> [...]
// get_certificate <module_name> [...]
// }
//
func parseTLS(h Helper) ([]ConfigValue, error) {
cp := new(caddytls.ConnectionPolicy)
var fileLoader caddytls.FileLoader
@@ -370,75 +363,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
}
acmeIssuer.Challenges.DNS.Resolvers = args
case "propagation_delay":
arg := h.RemainingArgs()
if len(arg) != 1 {
return nil, h.ArgErr()
}
delayStr := arg[0]
delay, err := caddy.ParseDuration(delayStr)
if err != nil {
return nil, h.Errf("invalid propagation_delay duration %s: %v", delayStr, err)
}
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.DNS == nil {
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
}
acmeIssuer.Challenges.DNS.PropagationDelay = caddy.Duration(delay)
case "propagation_timeout":
arg := h.RemainingArgs()
if len(arg) != 1 {
return nil, h.ArgErr()
}
timeoutStr := arg[0]
var timeout time.Duration
if timeoutStr == "-1" {
timeout = time.Duration(-1)
} else {
var err error
timeout, err = caddy.ParseDuration(timeoutStr)
if err != nil {
return nil, h.Errf("invalid propagation_timeout duration %s: %v", timeoutStr, err)
}
}
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.DNS == nil {
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
}
acmeIssuer.Challenges.DNS.PropagationTimeout = caddy.Duration(timeout)
case "dns_ttl":
arg := h.RemainingArgs()
if len(arg) != 1 {
return nil, h.ArgErr()
}
ttlStr := arg[0]
ttl, err := caddy.ParseDuration(ttlStr)
if err != nil {
return nil, h.Errf("invalid dns_ttl duration %s: %v", ttlStr, err)
}
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.DNS == nil {
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
}
acmeIssuer.Challenges.DNS.TTL = caddy.Duration(ttl)
case "dns_challenge_override_domain":
arg := h.RemainingArgs()
if len(arg) != 1 {
@@ -471,12 +395,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
}
onDemand = true
case "insecure_secrets_log":
if !h.NextArg() {
return nil, h.ArgErr()
}
cp.InsecureSecretsLog = h.Val()
default:
return nil, h.Errf("unknown subdirective: %s", h.Val())
}
@@ -597,7 +515,8 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
// parseRoot parses the root directive. Syntax:
//
// root [<matcher>] <path>
// root [<matcher>] <path>
//
func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) {
var root string
for h.Next() {
@@ -621,13 +540,8 @@ func parseVars(h Helper) (caddyhttp.MiddlewareHandler, error) {
// parseRedir parses the redir directive. Syntax:
//
// redir [<matcher>] <to> [<code>]
// redir [<matcher>] <to> [<code>]
//
// <code> can be "permanent" for 301, "temporary" for 302 (default),
// a placeholder, or any number in the 3xx range or 401. The special
// code "html" can be used to redirect only browser clients (will
// respond with HTTP 200 and no Location header; redirect is performed
// with JS and a meta tag).
func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
if !h.Next() {
return nil, h.ArgErr()
@@ -644,7 +558,6 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
}
var body string
var hdr http.Header
switch code {
case "permanent":
code = "301"
@@ -665,7 +578,7 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
`
safeTo := html.EscapeString(to)
body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo)
code = "200" // don't redirect non-browser clients
code = "302"
default:
// Allow placeholders for the code
if strings.HasPrefix(code, "{") {
@@ -688,14 +601,9 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
}
}
// don't redirect non-browser clients
if code != "200" {
hdr = http.Header{"Location": []string{to}}
}
return caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(code),
Headers: hdr,
Headers: http.Header{"Location": []string{to}},
Body: body,
}, nil
}
@@ -731,20 +639,29 @@ func parseError(h Helper) (caddyhttp.MiddlewareHandler, error) {
// parseRoute parses the route directive.
func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) {
sr := new(caddyhttp.Subroute)
allResults, err := parseSegmentAsConfig(h)
if err != nil {
return nil, err
}
for _, result := range allResults {
switch result.Value.(type) {
case caddyhttp.Route, caddyhttp.Subroute:
switch handler := result.Value.(type) {
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:
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) {
@@ -766,11 +683,12 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {
// parseLog parses the log directive. Syntax:
//
// log {
// output <writer_module> ...
// format <encoder_module> ...
// level <level>
// }
// log {
// output <writer_module> ...
// format <encoder_module> ...
// level <level>
// }
//
func parseLog(h Helper) ([]ConfigValue, error) {
return parseLogHelper(h, nil)
}
@@ -802,7 +720,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
// reference the default logger. See the
// setupNewDefault function in the logging
// package for where this is configured.
globalLogName = caddy.DefaultLoggerName
globalLogName = "default"
}
// Verify this name is unused.
@@ -929,15 +847,3 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
}
return configValues, nil
}
// parseSkipLog parses the skip_log directive. Syntax:
//
// skip_log [<matcher>]
func parseSkipLog(h Helper) (caddyhttp.MiddlewareHandler, error) {
for h.Next() {
if h.NextArg() {
return nil, h.ArgErr()
}
}
return caddyhttp.VarsMiddleware{"skip_log": true}, nil
}
+25 -33
View File
@@ -42,7 +42,6 @@ var directiveOrder = []string{
"map",
"vars",
"root",
"skip_log",
"header",
"copy_response_headers", // only in reverse_proxy's handle_response
@@ -143,8 +142,8 @@ func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) {
type Helper struct {
*caddyfile.Dispenser
// State stores intermediate variables during caddyfile adaptation.
State map[string]any
options map[string]any
State map[string]interface{}
options map[string]interface{}
warnings *[]caddyconfig.Warning
matcherDefs map[string]caddy.ModuleMap
parentBlock caddyfile.ServerBlock
@@ -152,7 +151,7 @@ type Helper struct {
}
// Option gets the option keyed by name.
func (h Helper) Option(name string) any {
func (h Helper) Option(name string) interface{} {
return h.options[name]
}
@@ -176,7 +175,7 @@ func (h Helper) Caddyfiles() []string {
}
// JSON converts val into JSON. Any errors are added to warnings.
func (h Helper) JSON(val any) json.RawMessage {
func (h Helper) JSON(val interface{}) json.RawMessage {
return caddyconfig.JSON(val, h.warnings)
}
@@ -289,7 +288,7 @@ func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
return nil, err
}
return buildSubroute(allResults, h.groupCounter, true)
return buildSubroute(allResults, h.groupCounter)
}
// parseSegmentAsConfig parses the segment such that its subdirectives
@@ -376,7 +375,7 @@ type ConfigValue struct {
// The value to be used when building the config.
// Generally its type is associated with the
// name of the Class.
Value any
Value interface{}
directive string
}
@@ -407,7 +406,7 @@ func sortRoutes(routes []ConfigValue) {
return false
}
// decode the path matchers if there is just one matcher set
// decode the path matchers, if there is just one of them
var iPM, jPM caddyhttp.MatchPath
if len(iRoute.MatcherSetsRaw) == 1 {
_ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &iPM)
@@ -416,45 +415,38 @@ func sortRoutes(routes []ConfigValue) {
_ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &jPM)
}
// if there is only one path in the path matcher, sort by longer path
// (more specific) first; missing path matchers or multi-matchers are
// treated as zero-length paths
// sort by longer path (more specific) first; missing path
// matchers or multi-matchers are treated as zero-length paths
var iPathLen, jPathLen int
if len(iPM) == 1 {
if len(iPM) > 0 {
iPathLen = len(iPM[0])
}
if len(jPM) == 1 {
if len(jPM) > 0 {
jPathLen = len(jPM[0])
}
// some directives involve setting values which can overwrite
// each other, so it makes most sense to reverse the order so
// eachother, 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
// directives have a single path to match (issue #5037)
if iPathLen > 0 && jPathLen > 0 {
// sort least-specific (shortest) path first
return iPathLen < jPathLen
// if both directives have no path matcher, use whichever one
// has no matcher first.
if iPathLen == 0 && jPathLen == 0 {
return len(iRoute.MatcherSetsRaw) == 0 && len(jRoute.MatcherSetsRaw) > 0
}
// 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
// sort with the least-specific (shortest) path first
return iPathLen < jPathLen
} 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
return iPathLen > jPathLen
// if both directives have no path matcher, use whichever one
// has any kind of matcher defined first.
if iPathLen == 0 && jPathLen == 0 {
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
}
// if both directives don't have a single path to compare,
// sort whichever one has a matcher first; if both have
// a matcher, sort equally (stable sort preserves order)
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
// sort with the most-specific (longest) path first
return iPathLen > jPathLen
}
})
}
@@ -575,7 +567,7 @@ type (
// tokens from a global option. It is passed the tokens to parse and
// existing value from the previous instance of this global option
// (if any). It returns the value to associate with this global option.
UnmarshalGlobalFunc func(d *caddyfile.Dispenser, existingVal any) (any, error)
UnmarshalGlobalFunc func(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error)
)
var registeredDirectives = make(map[string]UnmarshalFunc)
+76 -135
View File
@@ -53,18 +53,27 @@ type ServerType struct {
// Setup makes a config from the tokens.
func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
options map[string]any) (*caddy.Config, []caddyconfig.Warning, error) {
options map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error) {
var warnings []caddyconfig.Warning
gc := counter{new(int)}
state := make(map[string]any)
state := make(map[string]interface{})
// load all the server blocks and associate them with a "pile" of config values
// load all the server blocks and associate them with a "pile"
// of config values; also prohibit duplicate keys because they
// can make a config confusing if more than one server block is
// chosen to handle a request - we actually will make each
// server block's route terminal so that only one will run
sbKeys := make(map[string]struct{})
originalServerBlocks := make([]serverBlock, 0, len(inputServerBlocks))
for _, sblock := range inputServerBlocks {
for i, sblock := range inputServerBlocks {
for j, k := range sblock.Keys {
if j == 0 && strings.HasPrefix(k, "@") {
return nil, warnings, fmt.Errorf("cannot define a matcher outside of a site block: '%s'", k)
}
if _, ok := sbKeys[k]; ok {
return nil, warnings, fmt.Errorf("duplicate site address not allowed: '%s' in %v (site block %d, key %d)", k, sblock.Keys, i, j)
}
sbKeys[k] = struct{}{}
}
originalServerBlocks = append(originalServerBlocks, serverBlock{
block: sblock,
@@ -91,17 +100,14 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
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(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"},
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
{regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
{regexp.MustCompile(`{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 {
@@ -193,11 +199,10 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
// now that each server is configured, make the HTTP app
httpApp := caddyhttp.App{
HTTPPort: tryInt(options["http_port"], &warnings),
HTTPSPort: tryInt(options["https_port"], &warnings),
GracePeriod: tryDuration(options["grace_period"], &warnings),
ShutdownDelay: tryDuration(options["shutdown_delay"], &warnings),
Servers: servers,
HTTPPort: tryInt(options["http_port"], &warnings),
HTTPSPort: tryInt(options["https_port"], &warnings),
GracePeriod: tryDuration(options["grace_period"], &warnings),
Servers: servers,
}
// then make the TLS app
@@ -219,11 +224,11 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
if ncl.name == "" {
return
}
if ncl.name == caddy.DefaultLoggerName {
if ncl.name == "default" {
hasDefaultLog = true
}
if _, ok := options["debug"]; ok && ncl.log.Level == "" {
ncl.log.Level = zap.DebugLevel.CapitalString()
ncl.log.Level = "DEBUG"
}
customLogs = append(customLogs, ncl)
}
@@ -240,8 +245,8 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
// configure it with any applicable options
if _, ok := options["debug"]; ok {
customLogs = append(customLogs, namedCustomLog{
name: caddy.DefaultLoggerName,
log: &caddy.CustomLog{Level: zap.DebugLevel.CapitalString()},
name: "default",
log: &caddy.CustomLog{Level: "DEBUG"},
})
}
}
@@ -286,17 +291,6 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
if adminConfig, ok := options["admin"].(*caddy.AdminConfig); ok && adminConfig != nil {
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 cfg.Logging == nil {
cfg.Logging = &caddy.Logging{
@@ -310,11 +304,11 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
// most users seem to prefer not writing access logs
// to the default log when they are directed to a
// file or have any other special customization
if ncl.name != caddy.DefaultLoggerName && len(ncl.log.Include) > 0 {
defaultLog, ok := cfg.Logging.Logs[caddy.DefaultLoggerName]
if ncl.name != "default" && len(ncl.log.Include) > 0 {
defaultLog, ok := cfg.Logging.Logs["default"]
if !ok {
defaultLog = new(caddy.CustomLog)
cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog
cfg.Logging.Logs["default"] = defaultLog
}
defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...)
}
@@ -328,14 +322,14 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
// which is expected to be the first server block if it has zero
// keys. It returns the updated list of server blocks with the
// global options block removed, and updates options accordingly.
func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options map[string]any) ([]serverBlock, error) {
func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options map[string]interface{}) ([]serverBlock, error) {
if len(serverBlocks) == 0 || len(serverBlocks[0].block.Keys) > 0 {
return serverBlocks, nil
}
for _, segment := range serverBlocks[0].block.Segments {
opt := segment.Directive()
var val any
var val interface{}
var err error
disp := caddyfile.NewDispenser(segment)
@@ -405,7 +399,7 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
// to server blocks. Each pairing is essentially a server definition.
func (st *ServerType) serversFromPairings(
pairings []sbAddrAssociation,
options map[string]any,
options map[string]interface{},
warnings *[]caddyconfig.Warning,
groupCounter counter,
) (map[string]*caddyhttp.Server, error) {
@@ -426,23 +420,6 @@ func (st *ServerType) serversFromPairings(
}
for i, p := range pairings {
// detect ambiguous site definitions: server blocks which
// have the same host bound to the same interface (listener
// address), otherwise their routes will improperly be added
// to the same server (see issue #4635)
for j, sblock1 := range p.serverBlocks {
for _, key := range sblock1.block.Keys {
for k, sblock2 := range p.serverBlocks {
if k == j {
continue
}
if sliceContains(sblock2.block.Keys, key) {
return nil, fmt.Errorf("ambiguous site definition: %s", key)
}
}
}
}
srv := &caddyhttp.Server{
Listen: p.addresses,
}
@@ -529,6 +506,15 @@ func (st *ServerType) serversFromPairings(
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
autoHTTPSWillAddConnPolicy := autoHTTPS != "off"
// if a catch-all server block (one which accepts all hostnames) exists in this pairing,
// we need to know that so that we can configure logs properly (see #3878)
var catchAllSblockExists bool
for _, sblock := range p.serverBlocks {
if len(sblock.hostsFromKeys(false)) == 0 {
catchAllSblockExists = true
}
}
// if needed, the ServerLogConfig is initialized beforehand so
// that all server blocks can populate it with data, even when not
// coming with a log directive
@@ -629,7 +615,7 @@ func (st *ServerType) serversFromPairings(
// set up each handler directive, making sure to honor directive order
dirRoutes := sblock.pile["route"]
siteSubroute, err := buildSubroute(dirRoutes, groupCounter, true)
siteSubroute, err := buildSubroute(dirRoutes, groupCounter)
if err != nil {
return nil, err
}
@@ -660,10 +646,18 @@ func (st *ServerType) serversFromPairings(
} else {
// map each host to the user's desired logger name
for _, h := range sblockLogHosts {
if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string)
// if the custom logger name is non-empty, add it to the map;
// otherwise, only map to an empty logger name if this or
// another site block on this server has a catch-all host (in
// which case only requests with mapped hostnames will be
// access-logged, so it'll be necessary to add them to the
// map even if they use default logger)
if ncl.name != "" || catchAllSblockExists {
if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string)
}
srv.Logs.LoggerNames[h] = ncl.name
}
srv.Logs.LoggerNames[h] = ncl.name
}
}
}
@@ -717,13 +711,13 @@ func (st *ServerType) serversFromPairings(
err := applyServerOptions(servers, options, warnings)
if err != nil {
return nil, fmt.Errorf("applying global server options: %v", err)
return nil, err
}
return servers, nil
}
func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock, options map[string]any) error {
func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock, options map[string]interface{}) error {
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp)
@@ -918,32 +912,11 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
return routeList
}
// No need to wrap the handlers in a subroute if this is the only server block
// and there is no matcher for it (doing so would produce unnecessarily nested
// JSON), *unless* there is a host matcher within this site block; if so, then
// we still need to wrap in a subroute because otherwise the host matcher from
// the inside of the site block would be a top-level host matcher, which is
// subject to auto-HTTPS (cert management), and using a host matcher within
// a site block is a valid, common pattern for excluding domains from cert
// management, leading to unexpected behavior; see issue #5124.
wrapInSubroute := true
if len(matcherSetsEnc) == 0 && len(p.serverBlocks) == 1 {
var hasHostMatcher bool
outer:
for _, route := range subroute.Routes {
for _, ms := range route.MatcherSetsRaw {
for matcherName := range ms {
if matcherName == "host" {
hasHostMatcher = true
break outer
}
}
}
}
wrapInSubroute = hasHostMatcher
}
if wrapInSubroute {
// no need to wrap the handlers in a subroute if this is
// the only server block and there is no matcher for it
routeList = append(routeList, subroute.Routes...)
} else {
route := caddyhttp.Route{
// the semantics of a site block in the Caddyfile dictate
// that only the first matching one is evaluated, since
@@ -961,26 +934,21 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
if len(route.MatcherSetsRaw) > 0 || len(route.HandlersRaw) > 0 {
routeList = append(routeList, route)
}
} else {
routeList = append(routeList, subroute.Routes...)
}
return routeList
}
// buildSubroute turns the config values, which are expected to be routes
// into a clean and orderly subroute that has all the routes within it.
func buildSubroute(routes []ConfigValue, groupCounter counter, needsSorting bool) (*caddyhttp.Subroute, error) {
if needsSorting {
for _, val := range routes {
if !directiveIsOrdered(val.directive) {
return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here", val.directive)
}
func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subroute, error) {
for _, val := range routes {
if !directiveIsOrdered(val.directive) {
return nil, fmt.Errorf("directive '%s' is not ordered, so it cannot be used here", val.directive)
}
sortRoutes(routes)
}
sortRoutes(routes)
subroute := new(caddyhttp.Subroute)
// some directives are mutually exclusive (only first matching
@@ -1223,7 +1191,6 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod
func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.ModuleMap) error {
for d.Next() {
// this is the "name" for "named matchers"
definitionName := d.Val()
if _, ok := matchers[definitionName]; ok {
@@ -1231,9 +1198,16 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
}
matchers[definitionName] = make(caddy.ModuleMap)
// given a matcher name and the tokens following it, parse
// the tokens as a matcher module and record it
makeMatcher := func(matcherName string, tokens []caddyfile.Token) error {
// in case there are multiple instances of the same matcher, concatenate
// their tokens (we expect that UnmarshalCaddyfile should be able to
// handle more than one segment); otherwise, we'd overwrite other
// instances of the matcher in this set
tokensByMatcherName := make(map[string][]caddyfile.Token)
for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
matcherName := d.Val()
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
}
for matcherName, tokens := range tokensByMatcherName {
mod, err := caddy.GetModule("http.matchers." + matcherName)
if err != nil {
return fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
@@ -1251,39 +1225,6 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
}
matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
return nil
}
// if the next token is quoted, we can assume it's not a matcher name
// and that it's probably an 'expression' matcher
if d.NextArg() {
if d.Token().Quoted() {
err := makeMatcher("expression", []caddyfile.Token{d.Token()})
if err != nil {
return err
}
continue
}
// if it wasn't quoted, then we need to rewind after calling
// d.NextArg() so the below properly grabs the matcher name
d.Prev()
}
// in case there are multiple instances of the same matcher, concatenate
// their tokens (we expect that UnmarshalCaddyfile should be able to
// handle more than one segment); otherwise, we'd overwrite other
// instances of the matcher in this set
tokensByMatcherName := make(map[string][]caddyfile.Token)
for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
matcherName := d.Val()
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
}
for matcherName, tokens := range tokensByMatcherName {
err := makeMatcher(matcherName, tokens)
if err != nil {
return err
}
}
}
return nil
@@ -1355,7 +1296,7 @@ func WasReplacedPlaceholderShorthand(token string) string {
// tryInt tries to convert val to an integer. If it fails,
// it downgrades the error to a warning and returns 0.
func tryInt(val any, warnings *[]caddyconfig.Warning) int {
func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int {
intVal, ok := val.(int)
if val != nil && !ok && warnings != nil {
*warnings = append(*warnings, caddyconfig.Warning{Message: "not an integer type"})
@@ -1363,7 +1304,7 @@ func tryInt(val any, warnings *[]caddyconfig.Warning) int {
return intVal
}
func tryString(val any, warnings *[]caddyconfig.Warning) string {
func tryString(val interface{}, warnings *[]caddyconfig.Warning) string {
stringVal, ok := val.(string)
if val != nil && !ok && warnings != nil {
*warnings = append(*warnings, caddyconfig.Warning{Message: "not a string type"})
@@ -1371,7 +1312,7 @@ func tryString(val any, warnings *[]caddyconfig.Warning) string {
return stringVal
}
func tryDuration(val any, warnings *[]caddyconfig.Warning) caddy.Duration {
func tryDuration(val interface{}, warnings *[]caddyconfig.Warning) caddy.Duration {
durationVal, ok := val.(caddy.Duration)
if val != nil && !ok && warnings != nil {
*warnings = append(*warnings, caddyconfig.Warning{Message: "not a duration type"})
+26 -43
View File
@@ -31,13 +31,11 @@ func init() {
RegisterGlobalOption("https_port", parseOptHTTPSPort)
RegisterGlobalOption("default_bind", parseOptStringList)
RegisterGlobalOption("grace_period", parseOptDuration)
RegisterGlobalOption("shutdown_delay", parseOptDuration)
RegisterGlobalOption("default_sni", parseOptSingleString)
RegisterGlobalOption("order", parseOptOrder)
RegisterGlobalOption("storage", parseOptStorage)
RegisterGlobalOption("storage_clean_interval", parseOptDuration)
RegisterGlobalOption("renew_interval", parseOptDuration)
RegisterGlobalOption("ocsp_interval", parseOptDuration)
RegisterGlobalOption("acme_ca", parseOptSingleString)
RegisterGlobalOption("acme_ca_root", parseOptSingleString)
RegisterGlobalOption("acme_dns", parseOptACMEDNS)
@@ -54,12 +52,11 @@ func init() {
RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
RegisterGlobalOption("log", parseLogOptions)
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
RegisterGlobalOption("persist_config", parseOptPersistConfig)
}
func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil }
func parseOptTrue(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { return true, nil }
func parseOptHTTPPort(d *caddyfile.Dispenser, _ any) (any, error) {
func parseOptHTTPPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
var httpPort int
for d.Next() {
var httpPortStr string
@@ -75,7 +72,7 @@ func parseOptHTTPPort(d *caddyfile.Dispenser, _ any) (any, error) {
return httpPort, nil
}
func parseOptHTTPSPort(d *caddyfile.Dispenser, _ any) (any, error) {
func parseOptHTTPSPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
var httpsPort int
for d.Next() {
var httpsPortStr string
@@ -91,7 +88,7 @@ func parseOptHTTPSPort(d *caddyfile.Dispenser, _ any) (any, error) {
return httpsPort, nil
}
func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
func parseOptOrder(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
newOrder := directiveOrder
for d.Next() {
@@ -167,7 +164,7 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
return newOrder, nil
}
func parseOptStorage(d *caddyfile.Dispenser, _ any) (any, error) {
func parseOptStorage(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
@@ -186,7 +183,7 @@ func parseOptStorage(d *caddyfile.Dispenser, _ any) (any, error) {
return storage, nil
}
func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) {
func parseOptDuration(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
@@ -200,7 +197,7 @@ func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) {
return caddy.Duration(dur), nil
}
func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) {
func parseOptACMEDNS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
@@ -219,7 +216,7 @@ func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) {
return prov, nil
}
func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) {
func parseOptACMEEAB(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
eab := new(acme.EAB)
for d.Next() {
if d.NextArg() {
@@ -247,7 +244,7 @@ func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) {
return eab, nil
}
func parseOptCertIssuer(d *caddyfile.Dispenser, existing any) (any, error) {
func parseOptCertIssuer(d *caddyfile.Dispenser, existing interface{}) (interface{}, error) {
var issuers []certmagic.Issuer
if existing != nil {
issuers = existing.([]certmagic.Issuer)
@@ -270,7 +267,7 @@ func parseOptCertIssuer(d *caddyfile.Dispenser, existing any) (any, error) {
return issuers, nil
}
func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) {
func parseOptSingleString(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() // consume parameter name
if !d.Next() {
return "", d.ArgErr()
@@ -282,7 +279,7 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) {
return val, nil
}
func parseOptStringList(d *caddyfile.Dispenser, _ any) (any, error) {
func parseOptStringList(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() // consume parameter name
val := d.RemainingArgs()
if len(val) == 0 {
@@ -291,7 +288,7 @@ func parseOptStringList(d *caddyfile.Dispenser, _ any) (any, error) {
return val, nil
}
func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) {
func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
adminCfg := new(caddy.AdminConfig)
for d.Next() {
if d.NextArg() {
@@ -327,7 +324,7 @@ func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) {
return adminCfg, nil
}
func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) {
func parseOptOnDemand(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
var ond *caddytls.OnDemandConfig
for d.Next() {
if d.NextArg() {
@@ -387,22 +384,7 @@ func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) {
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, _ interface{}) (interface{}, error) {
d.Next() // consume parameter name
if !d.Next() {
return "", d.ArgErr()
@@ -417,11 +399,11 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
return val, nil
}
func parseServerOptions(d *caddyfile.Dispenser, _ any) (any, error) {
func parseServerOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
return unmarshalCaddyfileServerOptions(d)
}
func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ any) (any, error) {
func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() // consume option name
var val string
if !d.AllArgs(&val) {
@@ -437,17 +419,18 @@ func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ any) (any, error) {
// parseLogOptions parses the global log option. Syntax:
//
// log [name] {
// output <writer_module> ...
// format <encoder_module> ...
// level <level>
// include <namespaces...>
// exclude <namespaces...>
// }
// log [name] {
// output <writer_module> ...
// format <encoder_module> ...
// level <level>
// include <namespaces...>
// exclude <namespaces...>
// }
//
// When the name argument is unspecified, this directive modifies the default
// logger.
func parseLogOptions(d *caddyfile.Dispenser, existingVal any) (any, error) {
//
func parseLogOptions(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) {
currentNames := make(map[string]struct{})
if existingVal != nil {
innerVals, ok := existingVal.([]ConfigValue)
@@ -482,7 +465,7 @@ func parseLogOptions(d *caddyfile.Dispenser, existingVal any) (any, error) {
return configValues, nil
}
func parseOptPreferredChains(d *caddyfile.Dispenser, _ any) (any, error) {
func parseOptPreferredChains(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next()
return caddytls.ParseCaddyfilePreferredChainsOptions(d)
}
+20 -31
View File
@@ -15,7 +15,6 @@
package httpcaddyfile
import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddypki"
@@ -27,27 +26,27 @@ func init() {
// parsePKIApp parses the global log option. Syntax:
//
// pki {
// ca [<id>] {
// name <name>
// root_cn <name>
// intermediate_cn <name>
// intermediate_lifetime <duration>
// root {
// cert <path>
// key <path>
// format <format>
// }
// intermediate {
// cert <path>
// key <path>
// format <format>
// }
// }
// }
// pki {
// ca [<id>] {
// name <name>
// root_cn <name>
// intermediate_cn <name>
// root {
// cert <path>
// key <path>
// format <format>
// }
// intermediate {
// cert <path>
// key <path>
// format <format>
// }
// }
// }
//
// When the CA ID is unspecified, 'local' is assumed.
func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) {
//
func parsePKIApp(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) {
pki := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
for d.Next() {
@@ -85,16 +84,6 @@ func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) {
}
pkiCa.IntermediateCommonName = d.Val()
case "intermediate_lifetime":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, err
}
pkiCa.IntermediateLifetime = caddy.Duration(dur)
case "root":
if pkiCa.Root == nil {
pkiCa.Root = new(caddypki.KeyPair)
@@ -171,7 +160,7 @@ func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) {
func (st ServerType) buildPKIApp(
pairings []sbAddrAssociation,
options map[string]any,
options map[string]interface{},
warnings []caddyconfig.Warning,
) (*caddypki.PKI, []caddyconfig.Warning, error) {
+24 -120
View File
@@ -33,22 +33,19 @@ type serverOptions struct {
ListenerAddress string
// These will all map 1:1 to the caddyhttp.Server struct
Name string
ListenerWrappersRaw []json.RawMessage
ReadTimeout caddy.Duration
ReadHeaderTimeout caddy.Duration
WriteTimeout caddy.Duration
IdleTimeout caddy.Duration
KeepAliveInterval caddy.Duration
MaxHeaderBytes int
Protocols []string
AllowH2C bool
ExperimentalHTTP3 bool
StrictSNIHost *bool
TrustedProxiesRaw json.RawMessage
ShouldLogCredentials bool
Metrics *caddyhttp.Metrics
}
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
serverOpts := serverOptions{}
for d.Next() {
if d.NextArg() {
@@ -59,15 +56,6 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
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":
for nesting := d.Nesting(); d.NextBlock(nesting); {
modID := "caddy.listeners." + d.Val()
@@ -135,15 +123,6 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
}
}
case "keepalive_interval":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing keepalive interval duration: %v", err)
}
serverOpts.KeepAliveInterval = caddy.Duration(dur)
case "max_header_size":
var sizeStr string
@@ -162,81 +141,22 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
}
serverOpts.ShouldLogCredentials = true
case "protocols":
protos := d.RemainingArgs()
for _, proto := range protos {
if proto != "h1" && proto != "h2" && proto != "h2c" && proto != "h3" {
return nil, d.Errf("unknown protocol '%s': expected h1, h2, h2c, or h3", proto)
}
if sliceContains(serverOpts.Protocols, proto) {
return nil, d.Errf("protocol %s specified more than once", proto)
}
serverOpts.Protocols = append(serverOpts.Protocols, proto)
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
case "strict_sni_host":
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
}
boolVal := true
if d.Val() == "insecure_off" {
boolVal = false
}
serverOpts.StrictSNIHost = &boolVal
case "trusted_proxies":
if !d.NextArg() {
return nil, d.Err("trusted_proxies expects an IP range source module name as its first argument")
}
modID := "http.ip_sources." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
source, ok := unm.(caddyhttp.IPRangeSource)
if !ok {
return nil, fmt.Errorf("module %s (%T) is not an IP range source", modID, unm)
}
jsonSource := caddyconfig.JSONModuleObject(
source,
"source",
source.(caddy.Module).CaddyModule().ID.Name(),
nil,
)
serverOpts.TrustedProxiesRaw = jsonSource
case "metrics":
if d.NextArg() {
return nil, d.ArgErr()
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
serverOpts.Metrics = new(caddyhttp.Metrics)
// TODO: DEPRECATED. (August 2022)
case "protocol":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol sub-option will be removed soon")
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "allow_h2c":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: allow_h2c will be removed soon; use protocols option instead")
if d.NextArg() {
return nil, d.ArgErr()
}
if sliceContains(serverOpts.Protocols, "h2c") {
return nil, d.Errf("protocol h2c already specified")
serverOpts.AllowH2C = true
case "experimental_http3":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.Protocols = append(serverOpts.Protocols, "h2c")
serverOpts.ExperimentalHTTP3 = true
case "strict_sni_host":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol > strict_sni_host in this position will be removed soon; move up to the servers block instead")
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
}
@@ -262,30 +182,26 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
// applyServerOptions sets the server options on the appropriate servers
func applyServerOptions(
servers map[string]*caddyhttp.Server,
options map[string]any,
options map[string]interface{},
warnings *[]caddyconfig.Warning,
) error {
// If experimental HTTP/3 is enabled, enable it on each server.
// We already know there won't be a conflict with serverOptions because
// we validated earlier that "experimental_http3" cannot be set at the same
// time as "servers"
if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
*warnings = append(*warnings, caddyconfig.Warning{Message: "the 'experimental_http3' global option is deprecated, please use the 'servers > protocol > experimental_http3' option instead"})
for _, srv := range servers {
srv.ExperimentalHTTP3 = true
}
}
serverOpts, ok := options["servers"].([]serverOptions)
if !ok {
return nil
}
// check for duplicate names, which would clobber the config
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 {
for _, server := range servers {
// find the options that apply to this server
opts := func() *serverOptions {
for _, entry := range serverOpts {
@@ -312,28 +228,16 @@ func applyServerOptions(
server.ReadHeaderTimeout = opts.ReadHeaderTimeout
server.WriteTimeout = opts.WriteTimeout
server.IdleTimeout = opts.IdleTimeout
server.KeepAliveInterval = opts.KeepAliveInterval
server.MaxHeaderBytes = opts.MaxHeaderBytes
server.Protocols = opts.Protocols
server.AllowH2C = opts.AllowH2C
server.ExperimentalHTTP3 = opts.ExperimentalHTTP3
server.StrictSNIHost = opts.StrictSNIHost
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
server.Metrics = opts.Metrics
if opts.ShouldLogCredentials {
if server.Logs == nil {
server.Logs = &caddyhttp.ServerLogConfig{}
}
server.Logs.ShouldLogCredentials = opts.ShouldLogCredentials
}
if opts.Name != "" {
nameReplacements[key] = opts.Name
}
}
// rename the servers if marked to do so
for old, new := range nameReplacements {
servers[new] = servers[old]
delete(servers, old)
}
return nil
+51 -65
View File
@@ -33,7 +33,7 @@ import (
func (st ServerType) buildTLSApp(
pairings []sbAddrAssociation,
options map[string]any,
options map[string]interface{},
warnings []caddyconfig.Warning,
) (*caddytls.TLS, []caddyconfig.Warning, error) {
@@ -44,32 +44,37 @@ func (st ServerType) buildTLSApp(
if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp)
}
autoHTTPS := "on"
if ah, ok := options["auto_https"].(string); ok {
autoHTTPS = ah
httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
if hsp, ok := options["https_port"].(int); ok {
httpsPort = strconv.Itoa(hsp)
}
// find all hosts that share a server block with a hostless
// key, so that they don't get forgotten/omitted by auto-HTTPS
// (since they won't appear in route matchers)
// count how many server blocks have a TLS-enabled key with
// no host, and find all hosts that share a server block with
// a hostless key, so that they don't get forgotten/omitted
// by auto-HTTPS (since they won't appear in route matchers)
var serverBlocksWithTLSHostlessKey int
httpsHostsSharedWithHostlessKey := make(map[string]struct{})
if autoHTTPS != "off" {
for _, pair := range pairings {
for _, sb := range pair.serverBlocks {
for _, addr := range sb.keys {
if addr.Host == "" {
// this server block has a hostless key, now
// go through and add all the hosts to the set
for _, otherAddr := range sb.keys {
if otherAddr.Original == addr.Original {
continue
}
if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
}
}
break
for _, pair := range pairings {
for _, sb := range pair.serverBlocks {
for _, addr := range sb.keys {
if addr.Host == "" {
// this address has no hostname, but if it's explicitly set
// to HTTPS, then we need to count it as being TLS-enabled
if addr.Scheme == "https" || addr.Port == httpsPort {
serverBlocksWithTLSHostlessKey++
}
// this server block has a hostless key, now
// go through and add all the hosts to the set
for _, otherAddr := range sb.keys {
if otherAddr.Original == addr.Original {
continue
}
if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
}
}
break
}
}
}
@@ -129,19 +134,6 @@ func (st ServerType) buildTLSApp(
issuers = append(issuers, issuerVal.Value.(certmagic.Issuer))
}
if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) {
// this more correctly implements an error check that was removed
// below; try it with this config:
//
// :443 {
// bind 127.0.0.1
// }
//
// :443 {
// bind ::1
// tls {
// issuer acme
// }
// }
return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers)
}
ap.Issuers = issuers
@@ -184,25 +176,29 @@ func (st ServerType) buildTLSApp(
}
}
// we used to ensure this block is allowed to create an automation policy;
// doing so was forbidden if it has a key with no host (i.e. ":443")
// first make sure this block is allowed to create an automation policy;
// doing so is forbidden if it has a key with no host (i.e. ":443")
// and if there is a different server block that also has a key with no
// host -- since a key with no host matches any host, we need its
// associated automation policy to have an empty Subjects list, i.e. no
// host filter, which is indistinguishable between the two server blocks
// because automation is not done in the context of a particular server...
// this is an example of a poor mapping from Caddyfile to JSON but that's
// the least-leaky abstraction I could figure out -- however, this check
// was preventing certain listeners, like those provided by plugins, from
// being used as desired (see the Tailscale listener plugin), so I removed
// the check: and I think since I originally wrote the check I added a new
// check above which *properly* detects this ambiguity without breaking the
// listener plugin; see the check above with a commented example config
if len(sblockHosts) == 0 && catchAllAP == nil {
// this server block has a key with no hosts, but there is not yet
// a catch-all automation policy (probably because no global options
// were set), so this one becomes it
catchAllAP = ap
// the least-leaky abstraction I could figure out
if len(sblockHosts) == 0 {
if serverBlocksWithTLSHostlessKey > 1 {
// this server block and at least one other has a key with no host,
// making the two indistinguishable; it is misleading to define such
// a policy within one server block since it actually will apply to
// others as well
return nil, warnings, fmt.Errorf("cannot make a TLS automation policy from a server block that has a host-less address when there are other TLS-enabled server block addresses lacking a host")
}
if catchAllAP == nil {
// this server block has a key with no hosts, but there is not yet
// a catch-all automation policy (probably because no global options
// were set), so this one becomes it
catchAllAP = ap
}
}
// associate our new automation policy with this server block's hosts
@@ -311,14 +307,6 @@ func (st ServerType) buildTLSApp(
tlsApp.Automation.RenewCheckInterval = renewCheckInterval
}
// set the OCSP check interval if configured
if ocspCheckInterval, ok := options["ocsp_interval"].(caddy.Duration); ok {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.OCSPCheckInterval = ocspCheckInterval
}
// set whether OCSP stapling should be disabled for manually-managed certificates
if ocspConfig, ok := options["ocsp_stapling"].(certmagic.OCSPConfig); ok {
tlsApp.DisableOCSPStapling = ocspConfig.DisableStapling
@@ -335,12 +323,10 @@ func (st ServerType) buildTLSApp(
internalAP := &caddytls.AutomationPolicy{
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
}
if autoHTTPS != "off" {
for h := range httpsHostsSharedWithHostlessKey {
al = append(al, h)
if !certmagic.SubjectQualifiesForPublicCert(h) {
internalAP.Subjects = append(internalAP.Subjects, h)
}
for h := range httpsHostsSharedWithHostlessKey {
al = append(al, h)
if !certmagic.SubjectQualifiesForPublicCert(h) {
internalAP.Subjects = append(internalAP.Subjects, h)
}
}
if len(al) > 0 {
@@ -434,7 +420,7 @@ func (st ServerType) buildTLSApp(
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) error {
func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interface{}) error {
acmeWrapper, ok := issuer.(acmeCapable)
if !ok {
return nil
@@ -481,7 +467,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
// for any other automation policies. A nil policy (and no error) will be
// returned if there are no default/global options. However, if always is
// true, a non-nil value will always be returned (unless there is an error).
func newBaseAutomationPolicy(options map[string]any, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
issuers, hasIssuers := options["cert_issuer"]
_, hasLocalCerts := options["local_certs"]
keyType, hasKeyType := options["key_type"]
+3 -35
View File
@@ -94,7 +94,7 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
}
}
resp, err := doHttpCallWithRetries(ctx, client, req)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
@@ -113,44 +113,12 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
return nil, err
}
for _, warn := range warnings {
ctx.Logger().Warn(warn.String())
ctx.Logger(hl).Warn(warn.String())
}
return result, nil
}
func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response, error) {
resp, err := client.Do(request)
if err != nil {
return nil, fmt.Errorf("problem calling http loader url: %v", err)
} else if resp.StatusCode < 200 || resp.StatusCode > 499 {
resp.Body.Close()
return nil, fmt.Errorf("bad response status code from http loader url: %v", resp.StatusCode)
}
return resp, nil
}
func doHttpCallWithRetries(ctx caddy.Context, client *http.Client, request *http.Request) (*http.Response, error) {
var resp *http.Response
var err error
const maxAttempts = 10
for i := 0; i < maxAttempts; i++ {
resp, err = attemptHttpCall(client, request)
if err != nil && i < maxAttempts-1 {
select {
case <-time.After(time.Millisecond * 500):
case <-ctx.Done():
return resp, ctx.Err()
}
} else {
break
}
}
return resp, err
}
func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
client := &http.Client{
Timeout: time.Duration(hl.Timeout),
@@ -161,7 +129,7 @@ func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
// client authentication
if hl.TLS.UseServerIdentity {
certs, err := ctx.IdentityCredentials(ctx.Logger())
certs, err := ctx.IdentityCredentials(ctx.Logger(hl))
if err != nil {
return nil, fmt.Errorf("getting server identity credentials: %v", err)
}
+5 -49
View File
@@ -58,10 +58,6 @@ func (al adminLoad) Routes() []caddy.AdminRoute {
Pattern: "/load",
Handler: caddy.AdminHandlerFunc(al.handleLoad),
},
{
Pattern: "/adapt",
Handler: caddy.AdminHandlerFunc(al.handleAdapt),
},
}
}
@@ -126,48 +122,7 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
return nil
}
// handleAdapt adapts the given Caddy config to JSON and responds with the result.
func (adminLoad) handleAdapt(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodPost {
return caddy.APIError{
HTTPStatus: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed"),
}
}
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
_, err := io.Copy(buf, r.Body)
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("reading request body: %v", err),
}
}
result, warnings, err := adaptByContentType(r.Header.Get("Content-Type"), buf.Bytes())
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: err,
}
}
out := struct {
Warnings []Warning `json:"warnings,omitempty"`
Result json.RawMessage `json:"result"`
}{
Warnings: warnings,
Result: result,
}
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(out)
}
// adaptByContentType adapts body to Caddy JSON using the adapter specified by contentType.
// adaptByContentType adapts body to Caddy JSON using the adapter specified by contenType.
// If contentType is empty or ends with "/json", the input will be returned, as a no-op.
func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, error) {
// assume JSON as the default
@@ -189,11 +144,12 @@ func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, err
}
// adapter name should be suffix of MIME type
_, adapterName, slashFound := strings.Cut(ct, "/")
if !slashFound {
slashIdx := strings.Index(ct, "/")
if slashIdx < 0 {
return nil, nil, fmt.Errorf("malformed Content-Type")
}
adapterName := ct[slashIdx+1:]
cfgAdapter := GetAdapter(adapterName)
if cfgAdapter == nil {
return nil, nil, fmt.Errorf("unrecognized config adapter '%s'", adapterName)
@@ -208,7 +164,7 @@ func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, err
}
var bufPool = sync.Pool{
New: func() any {
New: func() interface{} {
return new(bytes.Buffer)
},
}
+13 -30
View File
@@ -43,7 +43,7 @@ type Defaults struct {
// Default testing values
var Default = Defaults{
AdminPort: 2999, // different from what a real server also running on a developer's machine might be
AdminPort: 2019,
Certifcates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
TestRequestTimeout: 5 * time.Second,
LoadRequestTimeout: 5 * time.Second,
@@ -100,7 +100,7 @@ func (tc *Tester) InitServer(rawConfig string, configType string) {
tc.t.Fail()
}
if err := tc.ensureConfigRunning(rawConfig, configType); err != nil {
tc.t.Logf("failed ensuring config is running: %s", err)
tc.t.Logf("failed ensurng config is running: %s", err)
tc.t.Fail()
}
}
@@ -114,7 +114,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
return nil
}
err := validateTestPrerequisites(tc.t)
err := validateTestPrerequisites()
if err != nil {
tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err)
return nil
@@ -186,7 +186,7 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil)
}
var expected any
var expected interface{}
err := json.Unmarshal(expectedBytes, &expected)
if err != nil {
return err
@@ -196,7 +196,7 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
Timeout: Default.LoadRequestTimeout,
}
fetchConfig := func(client *http.Client) any {
fetchConfig := func(client *http.Client) interface{} {
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
return nil
@@ -206,7 +206,7 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
if err != nil {
return nil
}
var actual any
var actual interface{}
err = json.Unmarshal(actualBytes, &actual)
if err != nil {
return nil
@@ -214,24 +214,19 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
return actual
}
for retries := 10; retries > 0; retries-- {
for retries := 4; retries > 0; retries-- {
if reflect.DeepEqual(expected, fetchConfig(client)) {
return nil
}
time.Sleep(1 * time.Second)
time.Sleep(10 * time.Millisecond)
}
tc.t.Errorf("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
// designated path and Caddy sub-process is running.
func validateTestPrerequisites(t *testing.T) error {
func validateTestPrerequisites() error {
// check certificates are found
for _, certName := range Default.Certifcates {
@@ -241,27 +236,15 @@ func validateTestPrerequisites(t *testing.T) error {
}
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
os.Args = []string{"caddy", "run", "--config", f.Name(), "--adapter", "caddyfile"}
os.Args = []string{"caddy", "run"}
go func() {
caddycmd.Main()
}()
// wait for caddy to start serving the initial config
for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
time.Sleep(1 * time.Second)
for retries := 4; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
time.Sleep(10 * time.Millisecond)
}
}
@@ -388,7 +371,7 @@ func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string,
return false
}
options := make(map[string]any)
options := make(map[string]interface{})
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
if err != nil {
+1 -21
View File
@@ -11,8 +11,6 @@ func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
admin localhost:2999
skip_install_trust
http_port 9080
https_port 9443
}
@@ -27,8 +25,6 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
}
@@ -43,8 +39,6 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
}
@@ -59,9 +53,6 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
@@ -83,14 +74,7 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
]
}
}
},
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
}
}
}
}
`, "json")
@@ -101,8 +85,6 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
local_certs
@@ -126,8 +108,6 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSit
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
local_certs
@@ -1,108 +0,0 @@
{
pki {
ca internal {
name "Internal"
root_cn "Internal Root Cert"
intermediate_cn "Internal Intermediate Cert"
}
ca internal-long-lived {
name "Long-lived"
root_cn "Internal Root Cert 2"
intermediate_cn "Internal Intermediate Cert 2"
}
}
}
acme-internal.example.com {
acme_server {
ca internal
}
}
acme-long-lived.example.com {
acme_server {
ca internal-long-lived
lifetime 7d
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme-long-lived.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "internal-long-lived",
"handler": "acme_server",
"lifetime": 604800000000000
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"acme-internal.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "internal",
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"internal": {
"name": "Internal",
"root_common_name": "Internal Root Cert",
"intermediate_common_name": "Internal Intermediate Cert"
},
"internal-long-lived": {
"name": "Long-lived",
"root_common_name": "Internal Root Cert 2",
"intermediate_common_name": "Internal Intermediate Cert 2"
}
}
}
}
}
@@ -63,6 +63,32 @@ app.example.com {
]
}
]
},
{
"routes": [
{
"handle": [
{
"exclude": [
"Connection",
"Keep-Alive",
"Te",
"Trailers",
"Transfer-Encoding",
"Upgrade"
],
"handler": "copy_response_headers"
}
]
},
{
"handle": [
{
"handler": "copy_response"
}
]
}
]
}
],
"handler": "reverse_proxy",
@@ -1,90 +0,0 @@
:8881
forward_auth localhost:9000 {
uri /auth
copy_headers A>1 B C>3 {
D
E>5
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"routes": [
{
"handle": [
{
"handle_response": [
{
"match": {
"status_code": [
2
]
},
"routes": [
{
"handle": [
{
"handler": "headers",
"request": {
"set": {
"1": [
"{http.reverse_proxy.header.A}"
],
"3": [
"{http.reverse_proxy.header.C}"
],
"5": [
"{http.reverse_proxy.header.E}"
],
"B": [
"{http.reverse_proxy.header.B}"
],
"D": [
"{http.reverse_proxy.header.D}"
]
}
}
}
]
}
]
}
],
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"X-Forwarded-Method": [
"{http.request.method}"
],
"X-Forwarded-Uri": [
"{http.request.uri}"
]
}
}
},
"rewrite": {
"method": "GET",
"uri": "/auth"
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
]
}
]
}
}
}
}
}
@@ -3,7 +3,6 @@
http_port 8080
https_port 8443
grace_period 5s
shutdown_delay 10s
default_sni localhost
order root first
storage file_system {
@@ -46,7 +45,6 @@
"http_port": 8080,
"https_port": 8443,
"grace_period": 5000000000,
"shutdown_delay": 10000000000,
"servers": {
"srv0": {
"listen": [
@@ -22,7 +22,6 @@
}
storage_clean_interval 7d
renew_interval 1d
ocsp_interval 2d
key_type ed25519
}
@@ -84,7 +83,6 @@
},
"ask": "https://example.com"
},
"ocsp_interval": 172800000000000,
"renew_interval": 86400000000000,
"storage_clean_interval": 604800000000000
}
@@ -1,36 +0,0 @@
{
http_port 8080
persist_config off
admin {
origins localhost:2019 [::1]:2019 127.0.0.1:2019 192.168.10.128
}
}
:80
----------
{
"admin": {
"listen": "localhost:2019",
"origins": [
"localhost:2019",
"[::1]:2019",
"127.0.0.1:2019",
"192.168.10.128"
],
"config": {
"persist": false
}
},
"apps": {
"http": {
"http_port": 8080,
"servers": {
"srv0": {
"listen": [
":80"
]
}
}
}
}
}
@@ -1,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 {
}
}
}
}
}
@@ -3,7 +3,9 @@
timeouts {
idle 90s
}
strict_sni_host insecure_off
protocol {
strict_sni_host insecure_off
}
}
servers :80 {
timeouts {
@@ -14,7 +16,9 @@
timeouts {
idle 30s
}
strict_sni_host
protocol {
strict_sni_host
}
}
}
@@ -12,9 +12,11 @@
}
max_header_size 100MB
log_credentials
protocols h1 h2 h2c h3
strict_sni_host
trusted_proxies static private_ranges
protocol {
allow_h2c
experimental_http3
strict_sni_host
}
}
}
@@ -56,26 +58,11 @@ foo.com {
}
],
"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"
},
"logs": {
"should_log_credentials": true
},
"protocols": [
"h1",
"h2",
"h2c",
"h3"
]
"experimental_http3": true,
"allow_h2c": 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"
}
]
}
]
}
]
}
]
}
]
}
]
}
}
}
}
}
@@ -1,7 +1,5 @@
http://localhost:2020 {
log
skip_log /first-hidden*
skip_log /second-hidden*
respond 200
}
@@ -30,36 +28,6 @@ http://localhost:2020 {
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"skip_log": true
}
],
"match": [
{
"path": [
"/second-hidden*"
]
}
]
},
{
"handle": [
{
"handler": "vars",
"skip_log": true
}
],
"match": [
{
"path": [
"/first-hidden*"
]
}
]
},
{
"handle": [
{
@@ -62,9 +62,6 @@ example.com {
}
],
"logs": {
"logger_names": {
"one.example.com": ""
},
"skip_hosts": [
"three.example.com",
"two.example.com",
@@ -19,30 +19,27 @@
@matcher6 vars_regexp "{http.request.uri}" `\.([a-f0-9]{6})\.(css|js)$`
respond @matcher6 "from vars_regexp matcher without name"
@matcher7 `path('/foo*') && method('GET')`
respond @matcher7 "inline expression matcher shortcut"
@matcher8 {
@matcher7 {
header Foo bar
header Foo foobar
header Bar foo
}
respond @matcher8 "header matcher merging values of the same field"
respond @matcher7 "header matcher merging values of the same field"
@matcher9 {
@matcher8 {
query foo=bar foo=baz bar=foo
query bar=baz
}
respond @matcher9 "query matcher merging pairs with the same keys"
respond @matcher8 "query matcher merging pairs with the same keys"
@matcher10 {
@matcher9 {
header !Foo
header Bar foo
}
respond @matcher10 "header matcher with null field matcher"
respond @matcher9 "header matcher with null field matcher"
@matcher11 remote_ip private_ranges
respond @matcher11 "remote_ip matcher with private ranges"
@matcher10 remote_ip private_ranges
respond @matcher10 "remote_ip matcher with private ranges"
}
----------
{
@@ -155,19 +152,6 @@
}
]
},
{
"match": [
{
"expression": "path('/foo*') \u0026\u0026 method('GET')"
}
],
"handle": [
{
"body": "inline expression matcher shortcut",
"handler": "static_response"
}
]
},
{
"match": [
{
@@ -8,7 +8,7 @@ route {
}
not path */
}
redir @canonicalPath {http.request.orig_uri.path}/ 308
redir @canonicalPath {path}/ 308
# If the requested file does not exist, try index files
@indexFiles {
@@ -50,7 +50,7 @@ route {
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
"{http.request.uri.path}/"
]
},
"status_code": 308
@@ -74,7 +74,6 @@ route {
]
},
{
"group": "group0",
"handle": [
{
"handler": "rewrite",
@@ -130,4 +129,4 @@ route {
}
}
}
}
}
@@ -42,7 +42,7 @@
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
"{http.request.uri.path}/"
]
},
"status_code": 308
@@ -1,12 +1,7 @@
:8884
# the use of a host matcher here should cause this
# site block to be wrapped in a subroute, even though
# the site block does not have a hostname; this is
# to prevent auto-HTTPS from picking up on this host
# matcher because it is not a key on the site block
@test host example.com
php_fastcgi @test localhost:9000
@api host example.com
php_fastcgi @api localhost:9000
----------
{
"apps": {
@@ -18,6 +13,13 @@ php_fastcgi @test localhost:9000
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
@@ -25,99 +27,82 @@ php_fastcgi @test localhost:9000
{
"handle": [
{
"handler": "subroute",
"routes": [
"handler": "static_response",
"headers": {
"Location": [
"{http.request.uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
"path": [
"*/"
]
},
}
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
],
"match": [
{
"path": [
"*.php"
]
}
]
"dial": "localhost:9000"
}
]
}
],
"match": [
{
"host": [
"example.com"
"path": [
"*.php"
]
}
]
}
]
}
],
"terminal": true
]
}
]
}
@@ -43,7 +43,7 @@ php_fastcgi localhost:9000 {
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
"{http.request.uri.path}/"
]
},
"status_code": 308
@@ -46,7 +46,7 @@ php_fastcgi localhost:9000 {
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
"{http.request.uri.path}/"
]
},
"status_code": 308
@@ -1,8 +1,6 @@
:8884
reverse_proxy h2c://localhost:8080
reverse_proxy unix+h2c//run/app.sock
----------
{
"apps": {
@@ -29,21 +27,6 @@ reverse_proxy unix+h2c//run/app.sock
"dial": "localhost:8080"
}
]
},
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http",
"versions": [
"h2c",
"2"
]
},
"upstreams": [
{
"dial": "unix//run/app.sock"
}
]
}
]
}
@@ -1,64 +0,0 @@
:8884
reverse_proxy 127.0.0.1:65535 {
lb_policy first
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": "first"
},
"try_duration": 10000000000,
"try_interval": 500000000
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
@@ -1,3 +1,4 @@
https://example.com {
reverse_proxy /path https://localhost:54321 {
header_up Host {upstream_hostport}
@@ -23,12 +24,11 @@ https://example.com {
max_conns_per_host 5
keepalive_idle_conns_per_host 2
keepalive_interval 30s
tls_renegotiation freely
tls_except_ports 8181 8182
renegotiation freely
}
}
}
----------
{
"apps": {
@@ -93,10 +93,6 @@ https://example.com {
},
"response_header_timeout": 8000000000,
"tls": {
"except_ports": [
"8181",
"8182"
],
"renegotiation": "freely"
},
"versions": [
@@ -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,58 +0,0 @@
# example from issue #4667
{
auto_https off
}
https://, example.com {
tls test.crt test.key
respond "Hello World"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"handle": [
{
"body": "Hello World",
"handler": "static_response"
}
]
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": [
"cert0"
]
}
}
],
"automatic_https": {
"disable": true
}
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "test.crt",
"key": "test.key",
"tags": [
"cert0"
]
}
]
}
}
}
}
@@ -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"
}
]
}
]
}
}
}
}
+1 -11
View File
@@ -14,10 +14,8 @@ func TestRespond(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost:9080 {
@@ -37,10 +35,8 @@ func TestRedirect(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost:9080 {
@@ -72,7 +68,7 @@ func TestDuplicateHosts(t *testing.T) {
}
`,
"caddyfile",
"ambiguous site definition")
"duplicate site address not allowed")
}
func TestReadCookie(t *testing.T) {
@@ -88,11 +84,8 @@ func TestReadCookie(t *testing.T) {
tester.Client.Jar.SetCookies(localhost, []*http.Cookie{&cookie})
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost:9080 {
@@ -114,11 +107,8 @@ func TestReplIndex(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost:9080 {
-3
View File
@@ -11,11 +11,8 @@ func TestBrowse(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
file_server browse
+2 -17
View File
@@ -11,11 +11,8 @@ func TestMap(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost:9080 {
@@ -41,8 +38,6 @@ func TestMapRespondWithDefault(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
}
@@ -65,22 +60,12 @@ func TestMapRespondWithDefault(t *testing.T) {
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost unknown")
}
func TestMapAsJSON(t *testing.T) {
func TestMapAsJson(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"http_port": 9080,
"https_port": 9443,
@@ -100,7 +85,7 @@ func TestMapAsJSON(t *testing.T) {
{
"handler": "map",
"source": "{http.request.method}",
"destinations": ["{dest-name}"],
"destinations": ["dest-name"],
"defaults": ["unknown"],
"mappings": [
{
-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)")
}
+2 -73
View File
@@ -8,7 +8,6 @@ import (
"runtime"
"strings"
"testing"
"time"
"github.com/caddyserver/caddy/v2/caddytest"
)
@@ -17,19 +16,8 @@ func TestSRVReverseProxy(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
@@ -61,15 +49,7 @@ func TestSRVWithDial(t *testing.T) {
caddytest.AssertLoadError(t, `
{
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
@@ -133,19 +113,8 @@ func TestDialWithPlaceholderUnix(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
@@ -185,19 +154,8 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
@@ -279,19 +237,8 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
@@ -373,15 +320,7 @@ func TestSRVWithActiveHealthcheck(t *testing.T) {
caddytest.AssertLoadError(t, `
{
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
@@ -418,11 +357,8 @@ func TestReverseProxyHealthCheck(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:2020 {
respond "Hello, World!"
@@ -436,13 +372,12 @@ func TestReverseProxyHealthCheck(t *testing.T) {
health_uri /health
health_port 2021
health_interval 10ms
health_timeout 100ms
health_interval 2s
health_timeout 5s
}
}
`, "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
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
}
@@ -483,11 +418,8 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy {
@@ -541,11 +473,8 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy {
+237 -257
View File
@@ -11,95 +11,91 @@ func TestDefaultSNI(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from a.caddy.localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"match": [
{
"host": [
"127.0.0.1"
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"match": {
"sni": [
"127.0.0.1"
]
}
},
{
"default_sni": "*.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/caddy.localhost.crt",
"key": "/caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from a.caddy.localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"match": [
{
"host": [
"127.0.0.1"
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"match": {
"sni": [
"127.0.0.1"
]
}
},
{
"default_sni": "*.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/caddy.localhost.crt",
"key": "/caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
// act and assert
// makes a request with no sni
@@ -111,100 +107,96 @@ func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from a",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"match": [
{
"host": [
"a.caddy.localhost",
"127.0.0.1"
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost",
"match": {
"sni": [
"a.caddy.localhost",
"127.0.0.1",
""
]
}
},
{
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from a",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"match": [
{
"host": [
"a.caddy.localhost",
"127.0.0.1"
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost",
"match": {
"sni": [
"a.caddy.localhost",
"127.0.0.1",
""
]
}
},
{
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
// act and assert
// makes a request with no sni
@@ -215,72 +207,68 @@ func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"body": "hello from a.caddy.localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"body": "hello from a.caddy.localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
// act and assert
// makes a request with no sni
@@ -290,7 +278,6 @@ func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
func TestHttpOnlyOnDomainWithSNI(t *testing.T) {
caddytest.AssertAdapt(t, `
{
skip_install_trust
default_sni a.caddy.localhost
}
:80 {
@@ -326,13 +313,6 @@ func TestHttpOnlyOnDomainWithSNI(t *testing.T) {
]
}
}
},
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
}
}
}`)
+6 -12
View File
@@ -23,14 +23,10 @@ func TestH2ToH2CStream(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
@@ -127,8 +123,8 @@ func TestH2ToH2CStream(t *testing.T) {
// Disable any compression method from server.
req.Header.Set("Accept-Encoding", "identity")
resp := tester.AssertResponseCode(req, http.StatusOK)
if resp.StatusCode != http.StatusOK {
resp := tester.AssertResponseCode(req, 200)
if 200 != resp.StatusCode {
return
}
go func() {
@@ -147,6 +143,7 @@ func TestH2ToH2CStream(t *testing.T) {
if !strings.Contains(body, expectedBody) {
t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
}
return
}
func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
@@ -209,9 +206,6 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"logging": {
"logs": {
"default": {
@@ -223,7 +217,6 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
@@ -342,8 +335,8 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
fmt.Fprint(w, expectedBody)
w.Close()
}()
resp := tester.AssertResponseCode(req, http.StatusOK)
if resp.StatusCode != http.StatusOK {
resp := tester.AssertResponseCode(req, 200)
if 200 != resp.StatusCode {
return
}
@@ -358,6 +351,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
if body != expectedBody {
t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
}
return
}
func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server {
+4 -6
View File
@@ -19,13 +19,11 @@
// 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:
//
// 1. Copy this file (main.go) into a new folder
// 2. Edit the imports below to include the modules you want plugged in
// 3. Run `go mod init caddy`
// 4. Run `go install` or `go build` - you now have a custom binary!
// 1. Copy this file (main.go) into a new folder
// 2. Edit the imports below to include the modules you want plugged in
// 3. Run `go mod init caddy`
// 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:
// https://github.com/caddyserver/xcaddy
package main
import (
-120
View File
@@ -1,120 +0,0 @@
package caddycmd
import (
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "caddy",
Long: `Caddy is an extensible server platform written in Go.
At its core, Caddy merely manages configuration. Modules are plugged
in statically at compile-time to provide useful functionality. Caddy's
standard distribution includes common modules to serve HTTP, TLS,
and PKI applications, including the automation of certificates.
To run Caddy, use:
- 'caddy run' to run Caddy in the foreground (recommended).
- 'caddy start' to start Caddy in the background; only do this
if you will be keeping the terminal window open until you run
'caddy stop' to close the server.
When Caddy is started, it opens a locally-bound administrative socket
to which configuration can be POSTed via a restful HTTP API (see
https://caddyserver.com/docs/api).
Caddy's native configuration format is JSON. However, config adapters
can be used to convert other config formats to JSON when Caddy receives
its configuration. The Caddyfile is a built-in config adapter that is
popular for hand-written configurations due to its straightforward
syntax (see https://caddyserver.com/docs/caddyfile). Many third-party
adapters are available (see https://caddyserver.com/docs/config-adapters).
Use 'caddy adapt' to see how a config translates to JSON.
For convenience, the CLI can act as an HTTP client to give Caddy its
initial configuration for you. If a file named Caddyfile is in the
current working directory, it will do this automatically. Otherwise,
you can use the --config flag to specify the path to a config file.
Some special-purpose subcommands build and load a configuration file
for you directly from command line input; for example:
- caddy file-server
- caddy reverse-proxy
- caddy respond
These commands disable the administration endpoint because their
configuration is specified solely on the command line.
In general, the most common way to run Caddy is simply:
$ caddy run
Or, with a configuration file:
$ caddy run --config caddy.json
If running interactively in a terminal, running Caddy in the
background may be more convenient:
$ caddy start
...
$ caddy stop
This allows you to run other commands while Caddy stays running.
Be sure to stop Caddy before you close the terminal!
Depending on the system, Caddy may need permission to bind to low
ports. One way to do this on Linux is to use setcap:
$ sudo setcap cap_net_bind_service=+ep $(which caddy)
Remember to run that command again after replacing the binary.
See the Caddy website for tutorials, configuration structure,
syntax, and module documentation: https://caddyserver.com/docs/
Custom Caddy builds are available on the Caddy download page at:
https://caddyserver.com/download
The xcaddy command can be used to build Caddy from source with or
without additional plugins: https://github.com/caddyserver/xcaddy
Where possible, Caddy should be installed using officially-supported
package installers: https://caddyserver.com/docs/install
Instructions for running Caddy in production are also available:
https://caddyserver.com/docs/running
`,
Example: ` $ caddy run
$ caddy run --config caddy.json
$ caddy reload --config caddy.json
$ caddy stop`,
// kind of annoying to have all the help text printed out if
// caddy has an error provisioning its modules, for instance...
SilenceUsage: true,
}
const fullDocsFooter = `Full documentation is available at:
https://caddyserver.com/docs/command-line`
func init() {
rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
}
func caddyCmdToCobra(caddyCmd Command) *cobra.Command {
cmd := &cobra.Command{
Use: caddyCmd.Name,
Short: caddyCmd.Short,
Long: caddyCmd.Long,
RunE: func(cmd *cobra.Command, _ []string) error {
fls := cmd.Flags()
_, err := caddyCmd.Func(Flags{fls})
return err
},
}
cmd.Flags().AddGoFlagSet(caddyCmd.Flags)
return cmd
}
+98 -45
View File
@@ -29,6 +29,7 @@ import (
"os/exec"
"runtime"
"runtime/debug"
"sort"
"strings"
"github.com/aryann/difflib"
@@ -279,7 +280,7 @@ func cmdStop(fl Flags) (int, error) {
configFlag := fl.String("config")
configAdapterFlag := fl.String("adapter")
adminAddr, err := DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag)
adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
}
@@ -309,7 +310,7 @@ func cmdReload(fl Flags) (int, error) {
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
}
adminAddr, err := DetermineAdminAPIAddress(addrFlag, config, configFlag, configAdapterFlag)
adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
}
@@ -330,17 +331,30 @@ func cmdReload(fl Flags) (int, error) {
}
func cmdVersion(_ Flags) (int, error) {
_, full := caddy.Version()
fmt.Println(full)
fmt.Println(CaddyVersion())
return caddy.ExitCodeSuccess, nil
}
func cmdBuildInfo(_ Flags) (int, error) {
func cmdBuildInfo(fl Flags) (int, error) {
bi, ok := debug.ReadBuildInfo()
if !ok {
return caddy.ExitCodeFailedStartup, fmt.Errorf("no build information")
}
fmt.Println(bi)
fmt.Printf("go_version: %s\n", runtime.Version())
fmt.Printf("go_os: %s\n", runtime.GOOS)
fmt.Printf("go_arch: %s\n", runtime.GOARCH)
fmt.Printf("path: %s\n", bi.Path)
fmt.Printf("main: %s %s %s\n", bi.Main.Path, bi.Main.Version, bi.Main.Sum)
fmt.Println("dependencies:")
for _, goMod := range bi.Deps {
fmt.Printf("%s %s %s", goMod.Path, goMod.Version, goMod.Sum)
if goMod.Replace != nil {
fmt.Printf(" => %s %s %s", goMod.Replace.Path, goMod.Replace.Version, goMod.Replace.Sum)
}
fmt.Println()
}
return caddy.ExitCodeSuccess, nil
}
@@ -457,7 +471,7 @@ func cmdAdaptConfig(fl Flags) (int, error) {
fmt.Errorf("reading input file: %v", err)
}
opts := map[string]any{"filename": adaptCmdInputFlag}
opts := map[string]interface{}{"filename": adaptCmdInputFlag}
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
if err != nil {
@@ -506,15 +520,6 @@ func cmdAdaptConfig(fl Flags) (int, error) {
func cmdValidateConfig(fl Flags) (int, error) {
validateCmdConfigFlag := fl.String("config")
validateCmdAdapterFlag := fl.String("adapter")
runCmdLoadEnvfileFlag := fl.String("envfile")
// load all additional envs as soon as possible
if runCmdLoadEnvfileFlag != "" {
if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("loading additional environment variables: %v", err)
}
}
input, _, err := LoadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
if err != nil {
@@ -567,10 +572,7 @@ func cmdFmt(fl Flags) (int, error) {
if err := os.WriteFile(formatCmdConfigFile, output, 0600); err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
}
return caddy.ExitCodeSuccess, nil
}
if fl.Bool("diff") {
} else if fl.Bool("diff") {
diff := difflib.Diff(
strings.Split(string(input), "\n"),
strings.Split(string(output), "\n"))
@@ -588,10 +590,70 @@ func cmdFmt(fl Flags) (int, error) {
fmt.Print(string(output))
}
if warning, diff := caddyfile.FormattingDifference(formatCmdConfigFile, input); diff {
return caddy.ExitCodeFailedStartup, fmt.Errorf("%s:%d: Caddyfile input is not formatted", warning.File, warning.Line)
return caddy.ExitCodeSuccess, nil
}
func cmdHelp(fl Flags) (int, error) {
const fullDocs = `Full documentation is available at:
https://caddyserver.com/docs/command-line`
args := fl.Args()
if len(args) == 0 {
s := `Caddy is an extensible server platform.
usage:
caddy <command> [<args...>]
commands:
`
keys := make([]string, 0, len(commands))
for k := range commands {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
cmd := commands[k]
short := strings.TrimSuffix(cmd.Short, ".")
s += fmt.Sprintf(" %-15s %s\n", cmd.Name, short)
}
s += "\nUse 'caddy help <command>' for more information about a command.\n"
s += "\n" + fullDocs + "\n"
fmt.Print(s)
return caddy.ExitCodeSuccess, nil
} else if len(args) > 1 {
return caddy.ExitCodeFailedStartup, fmt.Errorf("can only give help with one command")
}
subcommand, ok := commands[args[0]]
if !ok {
return caddy.ExitCodeFailedStartup, fmt.Errorf("unknown command: %s", args[0])
}
helpText := strings.TrimSpace(subcommand.Long)
if helpText == "" {
helpText = subcommand.Short
if !strings.HasSuffix(helpText, ".") {
helpText += "."
}
}
result := fmt.Sprintf("%s\n\nusage:\n caddy %s %s\n",
helpText,
subcommand.Name,
strings.TrimSpace(subcommand.Usage),
)
if help := flagHelp(subcommand.Flags); help != "" {
result += fmt.Sprintf("\nflags:\n%s", help)
}
result += "\n" + fullDocs + "\n"
fmt.Print(result)
return caddy.ExitCodeSuccess, nil
}
@@ -670,11 +732,10 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
// DetermineAdminAPIAddress determines which admin API endpoint address should
// be used based on the inputs. By priority: if `address` is specified, then
// it is returned; if `config` is specified, then that config will be used for
// finding the admin address; if `configFile` (and `configAdapter`) are specified,
// then that config will be loaded to find the admin address; otherwise, the
// default admin listen address will be returned.
func DetermineAdminAPIAddress(address string, config []byte, configFile, configAdapter string) (string, error) {
// it is returned; if `configFile` (and `configAdapter`) are specified, then that
// config will be loaded to find the admin address; otherwise, the default
// admin listen address will be returned.
func DetermineAdminAPIAddress(address, configFile, configAdapter string) (string, error) {
// Prefer the address if specified and non-empty
if address != "" {
return address, nil
@@ -682,29 +743,21 @@ func DetermineAdminAPIAddress(address string, config []byte, configFile, configA
// Try to load the config from file if specified, with the given adapter name
if configFile != "" {
var loadedConfigFile string
var err error
// use the provided loaded config if non-empty
// otherwise, load it from the specified file/adapter
loadedConfig := config
if len(loadedConfig) == 0 {
// get the config in caddy's native format
loadedConfig, loadedConfigFile, err = LoadConfig(configFile, configAdapter)
if err != nil {
return "", err
}
if loadedConfigFile == "" {
return "", fmt.Errorf("no config file to load; either use --config flag or ensure Caddyfile exists in current directory")
}
// get the config in caddy's native format
config, loadedConfigFile, err := LoadConfig(configFile, configAdapter)
if err != nil {
return "", err
}
if loadedConfigFile == "" {
return "", fmt.Errorf("no config file to load")
}
// get the address of the admin listener from the config
if len(loadedConfig) > 0 {
// get the address of the admin listener if set
if len(config) > 0 {
var tmpStruct struct {
Admin caddy.AdminConfig `json:"admin"`
}
err := json.Unmarshal(loadedConfig, &tmpStruct)
err = json.Unmarshal(config, &tmpStruct)
if err != nil {
return "", fmt.Errorf("unmarshaling admin listener address from config: %v", err)
}
+16 -146
View File
@@ -16,14 +16,7 @@ package caddycmd
import (
"flag"
"fmt"
"os"
"regexp"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)
// Command represents a subcommand. Name, Func,
@@ -77,6 +70,13 @@ func Commands() map[string]Command {
var commands = make(map[string]Command)
func init() {
RegisterCommand(Command{
Name: "help",
Func: cmdHelp,
Usage: "<command>",
Short: "Shows help for a Caddy subcommand",
})
RegisterCommand(Command{
Name: "start",
Func: cmdStart,
@@ -137,8 +137,8 @@ The --resume flag will override the --config flag if there is a config auto-
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
changes. ⚠️ This can make unintentional config changes easier; only use this
option in a local development environment.`,
changes. ⚠️ This is dangerous in production! Only use this option in a local
development environment.`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("run", flag.ExitOnError)
fs.String("config", "", "Configuration file")
@@ -200,19 +200,6 @@ config file; otherwise the default is assumed.`,
Name: "version",
Func: cmdVersion,
Short: "Prints the version",
Long: `
Prints the version of this Caddy binary.
Version information must be embedded into the binary at compile-time in
order for Caddy to display anything useful with this command. If Caddy
is built from within a version control repository, the Go command will
embed the revision hash if available. However, if Caddy is built in the
way specified by our online documentation (or by using xcaddy), more
detailed version information is printed as given by Go modules.
For more details about the full version string, see the Go module
documentation: https://go.dev/doc/modules/version-numbers
`,
})
RegisterCommand(Command{
@@ -239,24 +226,6 @@ documentation: https://go.dev/doc/modules/version-numbers
Name: "environ",
Func: cmdEnviron,
Short: "Prints the environment",
Long: `
Prints the environment as seen by this Caddy process.
The environment includes variables set in the system. If your Caddy
configuration uses environment variables (e.g. "{env.VARIABLE}") then
this command can be useful for verifying that the variables will have
the values you expect in your config.
Note that environments may be different depending on how you run Caddy.
Environments for Caddy instances started by service managers such as
systemd are often different than the environment inherited from your
shell or terminal.
You can also print the environment the same time you use "caddy run"
by adding the "--environ" flag.
Environments may contain sensitive data.
`,
})
RegisterCommand(Command{
@@ -287,20 +256,16 @@ zero exit status will be returned.`,
RegisterCommand(Command{
Name: "validate",
Func: cmdValidateConfig,
Usage: "--config <path> [--adapter <name>] [--envfile <path>]",
Usage: "--config <path> [--adapter <name>]",
Short: "Tests whether a configuration file is valid",
Long: `
Loads and provisions the provided config, but does not start running it.
This reveals any errors with the configuration through the loading and
provisioning stages.
If --envfile is specified, an environment file with environment variables in
the KEY=VALUE format will be loaded into the Caddy process.`,
provisioning stages.`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("validate", flag.ExitOnError)
fs.String("config", "", "Input configuration file")
fs.String("adapter", "", "Name of config adapter")
fs.String("envfile", "", "Environment file to load")
return fs
}(),
})
@@ -381,111 +346,16 @@ EXPERIMENTAL: May be changed or removed.
}(),
})
RegisterCommand(Command{
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>",
Short: "Generates the manual pages for Caddy commands",
Long: `
Generates the manual pages for Caddy commands into the designated directory
tagged into section 8 (System Administration).
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.
`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("manpage", flag.ExitOnError)
fs.String("directory", "", "The output directory where the manpages are generated")
return fs
}(),
})
// source: https://github.com/spf13/cobra/blob/main/shell_completions.md
rootCmd.AddCommand(&cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: fmt.Sprintf(`To load completions:
Bash:
$ source <(%[1]s completion bash)
# To load completions for each session, execute once:
# Linux:
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
# macOS:
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
Zsh:
# If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
# You will need to start a new shell for this setup to take effect.
fish:
$ %[1]s completion fish | source
# To load completions for each session, execute once:
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
PowerShell:
PS> %[1]s completion powershell | Out-String | Invoke-Expression
# To load completions for every new session, run:
PS> %[1]s completion powershell > %[1]s.ps1
# and source this file from your PowerShell profile.
`, rootCmd.Root().Name()),
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
default:
return fmt.Errorf("unrecognized shell: %s", args[0])
}
},
})
}
// RegisterCommand registers the command cmd.
// cmd.Name must be unique and conform to the
// following format:
//
// - lowercase
// - alphanumeric and hyphen characters only
// - cannot start or end with a hyphen
// - hyphen cannot be adjacent to another hyphen
// - lowercase
// - alphanumeric and hyphen characters only
// - cannot start or end with a hyphen
// - hyphen cannot be adjacent to another hyphen
//
// This function panics if the name is already registered,
// if the name does not meet the described format, or if
@@ -508,7 +378,7 @@ func RegisterCommand(cmd Command) {
if !commandNameRegex.MatchString(cmd.Name) {
panic("invalid command name")
}
rootCmd.AddCommand(caddyCmdToCobra(cmd))
commands[cmd.Name] = cmd
}
var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)
+80 -33
View File
@@ -33,37 +33,60 @@ import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/certmagic"
"github.com/spf13/pflag"
"go.uber.org/zap"
)
func init() {
// set a fitting User-Agent for ACME requests
version, _ := caddy.Version()
cleanModVersion := strings.TrimPrefix(version, "v")
ua := "Caddy/" + cleanModVersion
if uaEnv, ok := os.LookupEnv("USERAGENT"); ok {
ua = uaEnv + " " + ua
}
certmagic.UserAgent = ua
goModule := caddy.GoModule()
cleanModVersion := strings.TrimPrefix(goModule.Version, "v")
certmagic.UserAgent = "Caddy/" + cleanModVersion
// by using Caddy, user indicates agreement to CA terms
// (very important, as Caddy is often non-interactive
// and thus ACME account creation will fail!)
// (very important, or ACME account creation will fail!)
certmagic.DefaultACME.Agreed = true
}
// Main implements the main function of the caddy command.
// Call this if Caddy is to be the main() of your program.
func Main() {
if len(os.Args) == 0 {
switch len(os.Args) {
case 0:
fmt.Printf("[FATAL] no arguments provided by OS; args[0] must be command\n")
os.Exit(caddy.ExitCodeFailedStartup)
case 1:
os.Args = append(os.Args, "help")
}
subcommandName := os.Args[1]
subcommand, ok := commands[subcommandName]
if !ok {
if strings.HasPrefix(os.Args[1], "-") {
// user probably forgot to type the subcommand
fmt.Println("[ERROR] first argument must be a subcommand; see 'caddy help'")
} else {
fmt.Printf("[ERROR] '%s' is not a recognized subcommand; see 'caddy help'\n", os.Args[1])
}
os.Exit(caddy.ExitCodeFailedStartup)
}
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
fs := subcommand.Flags
if fs == nil {
fs = flag.NewFlagSet(subcommand.Name, flag.ExitOnError)
}
err := fs.Parse(os.Args[2:])
if err != nil {
fmt.Println(err)
os.Exit(caddy.ExitCodeFailedStartup)
}
exitCode, err := subcommand.Func(Flags{fs})
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", subcommand.Name, err)
}
os.Exit(exitCode)
}
// handlePingbackConn reads from conn and ensures it matches
@@ -150,7 +173,7 @@ func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
// adapt config
if cfgAdapter != nil {
adaptedConfig, warnings, err := cfgAdapter.Adapt(config, map[string]any{
adaptedConfig, warnings, err := cfgAdapter.Adapt(config, map[string]interface{}{
"filename": configFile,
})
if err != nil {
@@ -257,7 +280,7 @@ func watchConfigFile(filename, adapterName string) {
// Flags wraps a FlagSet so that typed values
// from flags can be easily retrieved.
type Flags struct {
*pflag.FlagSet
*flag.FlagSet
}
// String returns the string representation of the
@@ -303,6 +326,22 @@ func (f Flags) Duration(name string) time.Duration {
return val
}
// flagHelp returns the help text for fs.
func flagHelp(fs *flag.FlagSet) string {
if fs == nil {
return ""
}
// temporarily redirect output
out := fs.Output()
defer fs.SetOutput(out)
buf := new(bytes.Buffer)
fs.SetOutput(buf)
fs.PrintDefaults()
return buf.String()
}
func loadEnvFromFile(envFile string) error {
file, err := os.Open(envFile)
if err != nil {
@@ -348,11 +387,11 @@ func parseEnvFile(envInput io.Reader) (map[string]string, error) {
}
// split line into key and value
before, after, isCut := strings.Cut(line, "=")
if !isCut {
fields := strings.SplitN(line, "=", 2)
if len(fields) != 2 {
return nil, fmt.Errorf("can't parse line %d; line should be in KEY=VALUE format", lineNumber)
}
key, val := before, after
key, val := fields[0], fields[1]
// sometimes keys are prefixed by "export " so file can be sourced in bash; ignore it here
key = strings.TrimPrefix(key, "export ")
@@ -369,8 +408,11 @@ func parseEnvFile(envInput io.Reader) (map[string]string, error) {
}
// remove any trailing comment after value
if commentStart, _, found := strings.Cut(val, "#"); found {
val = strings.TrimRight(commentStart, " \t")
if commentStart := strings.Index(val, "#"); commentStart > 0 {
before := val[commentStart-1]
if before == '\t' || before == ' ' {
val = strings.TrimRight(val[:commentStart], " \t")
}
}
// quoted value: support newlines
@@ -399,12 +441,11 @@ func parseEnvFile(envInput io.Reader) (map[string]string, error) {
}
func printEnvironment() {
_, version := caddy.Version()
fmt.Printf("caddy.HomeDir=%s\n", caddy.HomeDir())
fmt.Printf("caddy.AppDataDir=%s\n", caddy.AppDataDir())
fmt.Printf("caddy.AppConfigDir=%s\n", caddy.AppConfigDir())
fmt.Printf("caddy.ConfigAutosavePath=%s\n", caddy.ConfigAutosavePath)
fmt.Printf("caddy.Version=%s\n", version)
fmt.Printf("caddy.Version=%s\n", CaddyVersion())
fmt.Printf("runtime.GOOS=%s\n", runtime.GOOS)
fmt.Printf("runtime.GOARCH=%s\n", runtime.GOARCH)
fmt.Printf("runtime.Compiler=%s\n", runtime.Compiler)
@@ -421,15 +462,21 @@ func printEnvironment() {
}
}
// StringSlice is a flag.Value that enables repeated use of a string flag.
type StringSlice []string
func (ss StringSlice) String() string { return "[" + strings.Join(ss, ", ") + "]" }
func (ss *StringSlice) Set(value string) error {
*ss = append(*ss, value)
return nil
// CaddyVersion returns a detailed version string, if available.
func CaddyVersion() string {
goModule := caddy.GoModule()
ver := goModule.Version
if goModule.Sum != "" {
ver += " " + goModule.Sum
}
if goModule.Replace != nil {
ver += " => " + goModule.Replace.Path
if goModule.Replace.Version != "" {
ver += "@" + goModule.Replace.Version
}
if goModule.Replace.Sum != "" {
ver += " " + goModule.Replace.Sum
}
}
return ver
}
// Interface guard
var _ flag.Value = (*StringSlice)(nil)
+1 -1
View File
@@ -194,7 +194,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
// can use reflection but we need a non-pointer value (I'm
// not sure why), and since New() should return a pointer
// value, we need to dereference it first
iface := any(modInfo.New())
iface := interface{}(modInfo.New())
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
}
+1
View File
@@ -13,6 +13,7 @@
// limitations under the License.
//go:build !windows
// +build !windows
package caddycmd
+1 -4
View File
@@ -31,9 +31,6 @@ import (
func removeCaddyBinary(path string) error {
var sI syscall.StartupInfo
var pI syscall.ProcessInformation
argv, err := syscall.UTF16PtrFromString(filepath.Join(os.Getenv("windir"), "system32", "cmd.exe") + " /C del " + path)
if err != nil {
return err
}
argv := syscall.StringToUTF16Ptr(filepath.Join(os.Getenv("windir"), "system32", "cmd.exe") + " /C del " + path)
return syscall.CreateProcess(nil, argv, nil, nil, true, 0, nil, nil, &sI, &pI)
}
+36 -79
View File
@@ -37,10 +37,9 @@ import (
// not actually need to do this).
type Context struct {
context.Context
moduleInstances map[string][]Module
moduleInstances map[string][]interface{}
cfg *Config
cleanupFuncs []func()
ancestry []Module
}
// NewContext provides a new context derived from the given
@@ -52,7 +51,7 @@ type Context struct {
// modules which are loaded will be properly unloaded.
// See standard library context package's documentation.
func NewContext(ctx Context) (Context, context.CancelFunc) {
newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg}
newCtx := Context{moduleInstances: make(map[string][]interface{}), cfg: ctx.cfg}
c, cancel := context.WithCancel(ctx.Context)
wrappedCancel := func() {
cancel()
@@ -91,15 +90,15 @@ func (ctx *Context) OnCancel(f func()) {
// ModuleMap may be used in place of map[string]json.RawMessage. The return value's
// underlying type mirrors the input field's type:
//
// json.RawMessage => any
// []json.RawMessage => []any
// [][]json.RawMessage => [][]any
// map[string]json.RawMessage => map[string]any
// []map[string]json.RawMessage => []map[string]any
// json.RawMessage => interface{}
// []json.RawMessage => []interface{}
// [][]json.RawMessage => [][]interface{}
// map[string]json.RawMessage => map[string]interface{}
// []map[string]json.RawMessage => []map[string]interface{}
//
// The field must have a "caddy" struct tag in this format:
//
// caddy:"key1=val1 key2=val2"
// caddy:"key1=val1 key2=val2"
//
// To load modules, a "namespace" key is required. For example, to load modules
// in the "http.handlers" namespace, you'd put: `namespace=http.handlers` in the
@@ -116,20 +115,20 @@ func (ctx *Context) OnCancel(f func()) {
// meaning the key containing the module's name that is defined inline with the module
// itself. You must specify the inline key in a struct tag, along with the namespace:
//
// caddy:"namespace=http.handlers inline_key=handler"
// caddy:"namespace=http.handlers inline_key=handler"
//
// This will look for a key/value pair like `"handler": "..."` in the json.RawMessage
// in order to know the module name.
//
// To make use of the loaded module(s) (the return value), you will probably want
// to type-assert each 'any' value(s) to the types that are useful to you
// to type-assert each interface{} value(s) to the types that are useful to you
// and store them on the same struct. Storing them on the same struct makes for
// easy garbage collection when your host module is no longer needed.
//
// Loaded modules have already been provisioned and validated. Upon returning
// successfully, this method clears the json.RawMessage(s) in the field since
// the raw JSON is no longer needed, and this allows the GC to free up memory.
func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error) {
func (ctx Context) LoadModule(structPointer interface{}, fieldName string) (interface{}, error) {
val := reflect.ValueOf(structPointer).Elem().FieldByName(fieldName)
typ := val.Type()
@@ -149,7 +148,7 @@ func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error)
}
inlineModuleKey := opts["inline_key"]
var result any
var result interface{}
switch val.Kind() {
case reflect.Slice:
@@ -171,7 +170,7 @@ func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error)
if inlineModuleKey == "" {
panic("unable to determine module name without inline_key because type is not a ModuleMap")
}
var all []any
var all []interface{}
for i := 0; i < val.Len(); i++ {
val, err := ctx.loadModuleInline(inlineModuleKey, moduleNamespace, val.Index(i).Interface().(json.RawMessage))
if err != nil {
@@ -187,10 +186,10 @@ func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error)
if inlineModuleKey == "" {
panic("unable to determine module name without inline_key because type is not a ModuleMap")
}
var all [][]any
var all [][]interface{}
for i := 0; i < val.Len(); i++ {
innerVal := val.Index(i)
var allInner []any
var allInner []interface{}
for j := 0; j < innerVal.Len(); j++ {
innerInnerVal, err := ctx.loadModuleInline(inlineModuleKey, moduleNamespace, innerVal.Index(j).Interface().(json.RawMessage))
if err != nil {
@@ -205,7 +204,7 @@ func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error)
} else if isModuleMapType(typ.Elem()) {
// val is `[]map[string]json.RawMessage`
var all []map[string]any
var all []map[string]interface{}
for i := 0; i < val.Len(); i++ {
thisSet, err := ctx.loadModulesFromSomeMap(moduleNamespace, inlineModuleKey, val.Index(i))
if err != nil {
@@ -233,10 +232,10 @@ func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error)
return result, nil
}
// loadModulesFromSomeMap loads modules from val, which must be a type of map[string]any.
// loadModulesFromSomeMap loads modules from val, which must be a type of map[string]interface{}.
// Depending on inlineModuleKey, it will be interpreted as either a ModuleMap (key is the module
// name) or as a regular map (key is not the module name, and module name is defined inline).
func (ctx Context) loadModulesFromSomeMap(namespace, inlineModuleKey string, val reflect.Value) (map[string]any, error) {
func (ctx Context) loadModulesFromSomeMap(namespace, inlineModuleKey string, val reflect.Value) (map[string]interface{}, error) {
// if no inline_key is specified, then val must be a ModuleMap,
// where the key is the module name
if inlineModuleKey == "" {
@@ -254,8 +253,8 @@ func (ctx Context) loadModulesFromSomeMap(namespace, inlineModuleKey string, val
// loadModulesFromRegularMap loads modules from val, where val is a map[string]json.RawMessage.
// Map keys are NOT interpreted as module names, so module names are still expected to appear
// inline with the objects.
func (ctx Context) loadModulesFromRegularMap(namespace, inlineModuleKey string, val reflect.Value) (map[string]any, error) {
mods := make(map[string]any)
func (ctx Context) loadModulesFromRegularMap(namespace, inlineModuleKey string, val reflect.Value) (map[string]interface{}, error) {
mods := make(map[string]interface{})
iter := val.MapRange()
for iter.Next() {
k := iter.Key()
@@ -269,10 +268,10 @@ func (ctx Context) loadModulesFromRegularMap(namespace, inlineModuleKey string,
return mods, nil
}
// loadModuleMap loads modules from a ModuleMap, i.e. map[string]any, where the key is the
// loadModuleMap loads modules from a ModuleMap, i.e. map[string]interface{}, where the key is the
// module name. With a module map, module names do not need to be defined inline with their values.
func (ctx Context) loadModuleMap(namespace string, val reflect.Value) (map[string]any, error) {
all := make(map[string]any)
func (ctx Context) loadModuleMap(namespace string, val reflect.Value) (map[string]interface{}, error) {
all := make(map[string]interface{})
iter := val.MapRange()
for iter.Next() {
k := iter.Key().Interface().(string)
@@ -300,19 +299,19 @@ func (ctx Context) loadModuleMap(namespace string, val reflect.Value) (map[strin
// directly by most modules. However, this method is useful when
// dynamically loading/unloading modules in their own context,
// like from embedded scripts, etc.
func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error) {
func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{}, error) {
modulesMu.RLock()
modInfo, ok := modules[id]
mod, ok := modules[id]
modulesMu.RUnlock()
if !ok {
return nil, fmt.Errorf("unknown module: %s", id)
}
if modInfo.New == nil {
return nil, fmt.Errorf("module '%s' has no constructor", modInfo.ID)
if mod.New == nil {
return nil, fmt.Errorf("module '%s' has no constructor", mod.ID)
}
val := modInfo.New()
val := mod.New().(interface{})
// value must be a pointer for unmarshaling into concrete type, even if
// the module's concrete type is a slice or map; New() *should* return
@@ -328,7 +327,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
if len(rawMsg) > 0 {
err := strictUnmarshalJSON(rawMsg, &val)
if err != nil {
return nil, fmt.Errorf("decoding module config: %s: %v", modInfo, err)
return nil, fmt.Errorf("decoding module config: %s: %v", mod, err)
}
}
@@ -341,8 +340,6 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
return nil, fmt.Errorf("module value cannot be null")
}
ctx.ancestry = append(ctx.ancestry, val)
if prov, ok := val.(Provisioner); ok {
err := prov.Provision(ctx)
if err != nil {
@@ -354,7 +351,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2)
}
}
return nil, fmt.Errorf("provision %s: %v", modInfo, err)
return nil, fmt.Errorf("provision %s: %v", mod, err)
}
}
@@ -368,7 +365,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2)
}
}
return nil, fmt.Errorf("%s: invalid configuration: %v", modInfo, err)
return nil, fmt.Errorf("%s: invalid configuration: %v", mod, err)
}
}
@@ -378,7 +375,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
}
// loadModuleInline loads a module from a JSON raw message which decodes to
// a map[string]any, where one of the object keys is moduleNameKey
// a map[string]interface{}, where one of the object keys is moduleNameKey
// and the corresponding value is the module name (as a string) which can
// be found in the given scope. In other words, the module name is declared
// in-line with the module itself.
@@ -388,7 +385,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
// multiple instances in the map or it appears in an array (where there are
// no custom keys). In other words, the key containing the module name is
// treated special/separate from all the other keys in the object.
func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.RawMessage) (any, error) {
func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.RawMessage) (interface{}, error) {
moduleName, raw, err := getModuleNameInline(moduleNameKey, raw)
if err != nil {
return nil, err
@@ -410,7 +407,7 @@ func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.
// called during the Provision/Validate phase to reference a
// module's own host app (since the parent app module is still
// in the process of being provisioned, it is not yet ready).
func (ctx Context) App(name string) (any, error) {
func (ctx Context) App(name string) (interface{}, error) {
if app, ok := ctx.cfg.apps[name]; ok {
return app, nil
}
@@ -442,27 +439,8 @@ func (ctx Context) Storage() certmagic.Storage {
return ctx.cfg.storage
}
// Logger returns a logger that is intended for use by the most
// recent module associated with the context. Callers should not
// pass in any arguments unless they want to associate with a
// different module; it panics if more than 1 value is passed in.
//
// Originally, this method's signature was `Logger(mod Module)`,
// requiring that an instance of a Caddy module be passed in.
// However, that is no longer necessary, as the closest module
// most recently associated with the context will be automatically
// assumed. To prevent a sudden breaking change, this method's
// signature has been changed to be variadic, but we may remove
// the parameter altogether in the future. Callers should not
// pass in any argument. If there is valid need to specify a
// different module, please open an issue to discuss.
//
// PARTIALLY DEPRECATED: The Logger(module) form is deprecated and
// may be removed in the future. Do not pass in any arguments.
func (ctx Context) Logger(module ...Module) *zap.Logger {
if len(module) > 1 {
panic("more than 1 module passed in")
}
// Logger returns a logger that can be used by mod.
func (ctx Context) Logger(mod Module) *zap.Logger {
if ctx.cfg == nil {
// often the case in tests; just use a dev logger
l, err := zap.NewDevelopment()
@@ -471,26 +449,5 @@ func (ctx Context) Logger(module ...Module) *zap.Logger {
}
return l
}
mod := ctx.Module()
if len(module) > 0 {
mod = module[0]
}
return ctx.cfg.Logging.Logger(mod)
}
// Modules returns the lineage of modules that this context provisioned,
// with the most recent/current module being last in the list.
func (ctx Context) Modules() []Module {
mods := make([]Module, len(ctx.ancestry))
copy(mods, ctx.ancestry)
return mods
}
// Module returns the current module, or the most recent one
// provisioned by the context.
func (ctx Context) Module() Module {
if len(ctx.ancestry) == 0 {
return nil
}
return ctx.ancestry[len(ctx.ancestry)-1]
}
+4 -4
View File
@@ -71,13 +71,13 @@ func ExampleContext_LoadModule_array() {
},
}
// since our input is []json.RawMessage, the output will be []any
// since our input is []json.RawMessage, the output will be []interface{}
mods, err := ctx.LoadModule(myStruct, "GuestModulesRaw")
if err != nil {
// you'd want to actually handle the error here
// return fmt.Errorf("loading guest modules: %v", err)
}
for _, mod := range mods.([]any) {
for _, mod := range mods.([]interface{}) {
myStruct.guestModules = append(myStruct.guestModules, mod.(io.Writer))
}
@@ -104,13 +104,13 @@ func ExampleContext_LoadModule_map() {
},
}
// since our input is map[string]json.RawMessage, the output will be map[string]any
// since our input is map[string]json.RawMessage, the output will be map[string]interface{}
mods, err := ctx.LoadModule(myStruct, "GuestModulesRaw")
if err != nil {
// you'd want to actually handle the error here
// return fmt.Errorf("loading guest modules: %v", err)
}
for modName, mod := range mods.(map[string]any) {
for modName, mod := range mods.(map[string]interface{}) {
myStruct.guestModules[modName] = mod.(io.Writer)
}
+1
View File
@@ -13,6 +13,7 @@
// limitations under the License.
//go:build gofuzz
// +build gofuzz
package caddy
+73 -82
View File
@@ -1,141 +1,132 @@
module github.com/caddyserver/caddy/v2
go 1.18
go 1.17
require (
github.com/BurntSushi/toml v1.2.1
github.com/Masterminds/sprig/v3 v3.2.3
github.com/alecthomas/chroma/v2 v2.5.0
github.com/BurntSushi/toml v1.1.0
github.com/Masterminds/sprig/v3 v3.2.2
github.com/alecthomas/chroma v0.10.0
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
github.com/caddyserver/certmagic v0.17.2
github.com/dustin/go-humanize v1.0.1
github.com/caddyserver/certmagic v0.16.1
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
github.com/go-chi/chi v4.1.2+incompatible
github.com/google/cel-go v0.13.0
github.com/google/cel-go v0.7.3
github.com/google/uuid v1.3.0
github.com/klauspost/compress v1.15.15
github.com/klauspost/cpuid/v2 v2.2.3
github.com/mholt/acmez v1.0.4
github.com/prometheus/client_golang v1.14.0
github.com/quic-go/quic-go v0.32.0
github.com/smallstep/certificates v0.23.2
github.com/smallstep/nosql v0.5.0
github.com/smallstep/truststore v0.12.1
github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5
github.com/tailscale/tscert v0.0.0-20230124224810-c6dc1f4049b2
github.com/yuin/goldmark v1.5.4
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.39.0
go.opentelemetry.io/otel v1.13.0
github.com/klauspost/compress v1.15.4
github.com/klauspost/cpuid/v2 v2.0.12
github.com/lucas-clemente/quic-go v0.27.1
github.com/mholt/acmez v1.0.2
github.com/prometheus/client_golang v1.12.1
github.com/smallstep/certificates v0.19.0
github.com/smallstep/cli v0.18.0
github.com/smallstep/nosql v0.4.0
github.com/smallstep/truststore v0.11.0
github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2
github.com/yuin/goldmark v1.4.12
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0
go.opentelemetry.io/otel v1.4.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.0
go.opentelemetry.io/otel/sdk v1.13.0
go.uber.org/zap v1.24.0
golang.org/x/crypto v0.5.0
golang.org/x/net v0.5.0
golang.org/x/sync v0.1.0
golang.org/x/term v0.5.0
google.golang.org/genproto v0.0.0-20230202175211-008b39050e57
gopkg.in/natefinch/lumberjack.v2 v2.2.1
go.opentelemetry.io/otel/sdk v1.4.0
go.uber.org/zap v1.21.0
golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf
google.golang.org/protobuf v1.27.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-18 v0.2.0 // indirect
github.com/quic-go/qtls-go1-19 v0.2.0 // indirect
github.com/quic-go/qtls-go1-20 v0.1.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
)
require (
filippo.io/edwards25519 v1.0.0 // indirect
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/felixge/httpsnoop v1.0.2 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-kit/kit v0.10.0 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logfmt/logfmt v0.5.0 // indirect
github.com/go-logr/logr v1.2.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.13.0 // indirect
github.com/jackc/pgconn v1.10.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
github.com/jackc/pgproto3/v2 v2.2.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.12.0 // indirect
github.com/jackc/pgx/v4 v4.17.2 // indirect
github.com/jackc/pgtype v1.9.0 // indirect
github.com/jackc/pgx/v4 v4.14.0 // indirect
github.com/libdns/libdns v0.2.1 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/marten-seemann/qpack v0.2.1 // indirect
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/micromdm/scep/v2 v2.1.0 // indirect
github.com/miekg/dns v1.1.50 // indirect
github.com/miekg/dns v1.1.46 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rs/xid v1.4.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/rs/xid v1.2.1 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/slackhq/nebula v1.6.1 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/slackhq/nebula v1.5.2 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/urfave/cli v1.22.12 // indirect
github.com/urfave/cli v1.22.5 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.0 // indirect
go.opentelemetry.io/otel/metric v0.36.0 // indirect
go.opentelemetry.io/otel/trace v1.13.0 // indirect
go.opentelemetry.io/otel/internal/metric v0.27.0 // indirect
go.opentelemetry.io/otel/metric v0.27.0 // indirect
go.opentelemetry.io/otel/trace v1.4.0 // indirect
go.opentelemetry.io/proto/otlp v0.12.0 // indirect
go.step.sm/cli-utils v0.7.5 // indirect
go.step.sm/crypto v0.23.2
go.step.sm/linkedca v0.19.0 // indirect
go.step.sm/cli-utils v0.7.0 // indirect
go.step.sm/crypto v0.16.1 // indirect
go.step.sm/linkedca v0.15.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/mod v0.6.0 // indirect
golang.org/x/sys v0.5.0
golang.org/x/text v0.6.0 // indirect
golang.org/x/tools v0.2.0 // indirect
google.golang.org/grpc v1.52.3 // indirect
google.golang.org/protobuf v1.28.1 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
golang.org/x/tools v0.1.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/grpc v1.44.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
howett.net/plist v1.0.0 // indirect
)
+901 -197
View File
File diff suppressed because it is too large Load Diff
-177
View File
@@ -1,177 +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.
// TODO: Go 1.19 introduced the "unix" build tag. We have to support Go 1.18 until Go 1.20 is released.
// When Go 1.19 is our minimum, change this build tag to simply "!unix".
// (see similar change needed in listen_unix.go)
//go:build !(aix || android || darwin || dragonfly || freebsd || hurd || illumos || ios || linux || netbsd || openbsd || solaris)
package caddy
import (
"context"
"net"
"sync"
"sync/atomic"
"time"
"go.uber.org/zap"
)
func reuseUnixSocket(network, addr string) (any, error) {
return nil, nil
}
func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) {
sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
ln, err := config.Listen(ctx, network, address)
if err != nil {
return nil, err
}
return &sharedListener{Listener: ln, key: lnKey}, nil
})
if err != nil {
return nil, err
}
return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil
}
// fakeCloseListener is a private wrapper over a listener that
// is shared. The state of fakeCloseListener is not shared.
// This allows one user of a socket to "close" the listener
// while in reality the socket stays open for other users of
// the listener. In this way, servers become hot-swappable
// while the listener remains running. Listeners should be
// re-wrapped in a new fakeCloseListener each time the listener
// is reused. This type is atomic and values must not be copied.
type fakeCloseListener struct {
closed int32 // accessed atomically; belongs to this struct only
*sharedListener // embedded, so we also become a net.Listener
keepAlivePeriod time.Duration
}
type canSetKeepAlive interface {
SetKeepAlivePeriod(d time.Duration) error
SetKeepAlive(bool) error
}
func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
// if the listener is already "closed", return error
if atomic.LoadInt32(&fcl.closed) == 1 {
return nil, fakeClosedErr(fcl)
}
// call underlying accept
conn, err := fcl.sharedListener.Accept()
if err == nil {
// if 0, do nothing, Go's default is already set
// and if the connection allows setting KeepAlive, set it
if tconn, ok := conn.(canSetKeepAlive); ok && fcl.keepAlivePeriod != 0 {
if fcl.keepAlivePeriod > 0 {
err = tconn.SetKeepAlivePeriod(fcl.keepAlivePeriod)
} else { // negative
err = tconn.SetKeepAlive(false)
}
if err != nil {
Log().With(zap.String("server", fcl.sharedListener.key)).Warn("unable to set keepalive for new connection:", zap.Error(err))
}
}
return conn, nil
}
// since Accept() returned an error, it may be because our reference to
// the listener (this fakeCloseListener) may have been closed, i.e. the
// server is shutting down; in that case, we need to clear the deadline
// that we set when Close() was called, and return a non-temporary and
// non-timeout error value to the caller, masking the "true" error, so
// that server loops / goroutines won't retry, linger, and leak
if atomic.LoadInt32(&fcl.closed) == 1 {
// we dereference the sharedListener explicitly even though it's embedded
// so that it's clear in the code that side-effects are shared with other
// users of this listener, not just our own reference to it; we also don't
// do anything with the error because all we could do is log it, but we
// expliclty assign it to nothing so we don't forget it's there if needed
_ = fcl.sharedListener.clearDeadline()
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
return nil, fakeClosedErr(fcl)
}
}
return nil, err
}
// Close stops accepting new connections without closing the
// underlying listener. The underlying listener is only closed
// if the caller is the last known user of the socket.
func (fcl *fakeCloseListener) Close() error {
if atomic.CompareAndSwapInt32(&fcl.closed, 0, 1) {
// There are two ways I know of to get an Accept()
// function to return to the server loop that called
// it: close the listener, or set a deadline in the
// past. Obviously, we can't close the socket yet
// since others may be using it (hence this whole
// file). But we can set the deadline in the past,
// and this is kind of cheating, but it works, and
// it apparently even works on Windows.
_ = fcl.sharedListener.setDeadline()
_, _ = listenerPool.Delete(fcl.sharedListener.key)
}
return nil
}
// sharedListener is a wrapper over an underlying listener. The listener
// and the other fields on the struct are shared state that is synchronized,
// so sharedListener structs must never be copied (always use a pointer).
type sharedListener struct {
net.Listener
key string // uniquely identifies this listener
deadline bool // whether a deadline is currently set
deadlineMu sync.Mutex
}
func (sl *sharedListener) clearDeadline() error {
var err error
sl.deadlineMu.Lock()
if sl.deadline {
switch ln := sl.Listener.(type) {
case *net.TCPListener:
err = ln.SetDeadline(time.Time{})
}
sl.deadline = false
}
sl.deadlineMu.Unlock()
return err
}
func (sl *sharedListener) setDeadline() error {
timeInPast := time.Now().Add(-1 * time.Minute)
var err error
sl.deadlineMu.Lock()
if !sl.deadline {
switch ln := sl.Listener.(type) {
case *net.TCPListener:
err = ln.SetDeadline(timeInPast)
}
sl.deadline = true
}
sl.deadlineMu.Unlock()
return err
}
// Destruct is called by the UsagePool when the listener is
// finally not being used anymore. It closes the socket.
func (sl *sharedListener) Destruct() error {
return sl.Listener.Close()
}
-118
View File
@@ -1,118 +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.
// TODO: Go 1.19 introduced the "unix" build tag. We have to support Go 1.18 until Go 1.20 is released.
// When Go 1.19 is our minimum, remove this build tag, since "_unix" in the filename will do this.
// (see also change needed in listen.go)
//go:build aix || android || darwin || dragonfly || freebsd || hurd || illumos || ios || linux || netbsd || openbsd || solaris
package caddy
import (
"context"
"errors"
"io/fs"
"net"
"sync/atomic"
"syscall"
"go.uber.org/zap"
"golang.org/x/sys/unix"
)
// reuseUnixSocket copies and reuses the unix domain socket (UDS) if we already
// have it open; if not, unlink it so we can have it. No-op if not a unix network.
func reuseUnixSocket(network, addr string) (any, error) {
if !IsUnixNetwork(network) {
return nil, nil
}
socketKey := listenerKey(network, addr)
socket, exists := unixSockets[socketKey]
if exists {
// make copy of file descriptor
socketFile, err := socket.File() // does dup() deep down
if err != nil {
return nil, err
}
// use copied fd to make new Listener or PacketConn, then replace
// it in the map so that future copies always come from the most
// recent fd (as the previous ones will be closed, and we'd get
// "use of closed network connection" errors) -- note that we
// preserve the *pointer* to the counter (not just the value) so
// that all socket wrappers will refer to the same value
switch unixSocket := socket.(type) {
case *unixListener:
ln, err := net.FileListener(socketFile)
if err != nil {
return nil, err
}
atomic.AddInt32(unixSocket.count, 1)
unixSockets[socketKey] = &unixListener{ln.(*net.UnixListener), socketKey, unixSocket.count}
case *unixConn:
pc, err := net.FilePacketConn(socketFile)
if err != nil {
return nil, err
}
atomic.AddInt32(unixSocket.count, 1)
unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), addr, socketKey, unixSocket.count}
}
return unixSockets[socketKey], nil
}
// from what I can tell after some quick research, it's quite common for programs to
// leave their socket file behind after they close, so the typical pattern is to
// unlink it before you bind to it -- this is often crucial if the last program using
// it was killed forcefully without a chance to clean up the socket, but there is a
// race, as the comment in net.UnixListener.close() explains... oh well, I guess?
if err := syscall.Unlink(addr); err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err
}
return nil, nil
}
func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) {
// wrap any Control function set by the user so we can also add our reusePort control without clobbering theirs
oldControl := config.Control
config.Control = func(network, address string, c syscall.RawConn) error {
if oldControl != nil {
if err := oldControl(network, address, c); err != nil {
return err
}
}
return reusePort(network, address, c)
}
return config.Listen(ctx, network, address)
}
// reusePort sets SO_REUSEPORT. Ineffective for unix sockets.
func reusePort(network, address string, conn syscall.RawConn) error {
if IsUnixNetwork(network) {
return nil
}
return conn.Control(func(descriptor uintptr) {
if err := unix.SetsockoptInt(int(descriptor), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
Log().Error("setting SO_REUSEPORT",
zap.String("network", network),
zap.String("address", address),
zap.Uintptr("descriptor", descriptor),
zap.Error(err))
}
})
}
+375 -566
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -13,6 +13,7 @@
// limitations under the License.
//go:build gofuzz
// +build gofuzz
package caddy
+5 -111
View File
@@ -32,24 +32,9 @@ func TestSplitNetworkAddress(t *testing.T) {
expectErr: true,
},
{
input: "foo",
expectHost: "foo",
},
{
input: ":", // empty host & empty port
},
{
input: "::",
input: "foo",
expectErr: true,
},
{
input: "[::]",
expectHost: "::",
},
{
input: ":1234",
expectPort: "1234",
},
{
input: "foo:1234",
expectHost: "foo",
@@ -95,10 +80,10 @@ func TestSplitNetworkAddress(t *testing.T) {
} {
actualNetwork, actualHost, actualPort, err := SplitNetworkAddress(tc.input)
if tc.expectErr && err == nil {
t.Errorf("Test %d: Expected error but got %v", i, err)
t.Errorf("Test %d: Expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("Test %d: Expected no error but got %v", i, err)
t.Errorf("Test %d: Expected no error but got: %v", i, err)
}
if actualNetwork != tc.expectNetwork {
t.Errorf("Test %d: Expected network '%s' but got '%s'", i, tc.expectNetwork, actualNetwork)
@@ -184,17 +169,8 @@ func TestParseNetworkAddress(t *testing.T) {
expectErr: true,
},
{
input: ":",
expectAddr: NetworkAddress{
Network: "tcp",
},
},
{
input: "[::]",
expectAddr: NetworkAddress{
Network: "tcp",
Host: "::",
},
input: ":",
expectErr: true,
},
{
input: ":1234",
@@ -331,85 +307,3 @@ func TestJoinHostPort(t *testing.T) {
}
}
}
func TestExpand(t *testing.T) {
for i, tc := range []struct {
input NetworkAddress
expect []NetworkAddress
}{
{
input: NetworkAddress{
Network: "tcp",
Host: "localhost",
StartPort: 2000,
EndPort: 2000,
},
expect: []NetworkAddress{
{
Network: "tcp",
Host: "localhost",
StartPort: 2000,
EndPort: 2000,
},
},
},
{
input: NetworkAddress{
Network: "tcp",
Host: "localhost",
StartPort: 2000,
EndPort: 2002,
},
expect: []NetworkAddress{
{
Network: "tcp",
Host: "localhost",
StartPort: 2000,
EndPort: 2000,
},
{
Network: "tcp",
Host: "localhost",
StartPort: 2001,
EndPort: 2001,
},
{
Network: "tcp",
Host: "localhost",
StartPort: 2002,
EndPort: 2002,
},
},
},
{
input: NetworkAddress{
Network: "tcp",
Host: "localhost",
StartPort: 2000,
EndPort: 1999,
},
expect: []NetworkAddress{},
},
{
input: NetworkAddress{
Network: "unix",
Host: "/foo/bar",
StartPort: 0,
EndPort: 0,
},
expect: []NetworkAddress{
{
Network: "unix",
Host: "/foo/bar",
StartPort: 0,
EndPort: 0,
},
},
},
} {
actual := tc.input.Expand()
if !reflect.DeepEqual(actual, tc.expect) {
t.Errorf("Test %d: Expected %+v but got %+v", i, tc.expect, actual)
}
}
}
+3 -5
View File
@@ -105,7 +105,7 @@ func (logging *Logging) openLogs(ctx Context) error {
// then set up any other custom logs
for name, l := range logging.Logs {
// the default log is already set up
if name == DefaultLoggerName {
if name == "default" {
continue
}
@@ -138,7 +138,7 @@ func (logging *Logging) setupNewDefault(ctx Context) error {
// extract the user-defined default log, if any
newDefault := new(defaultCustomLog)
if userDefault, ok := logging.Logs[DefaultLoggerName]; ok {
if userDefault, ok := logging.Logs["default"]; ok {
newDefault.CustomLog = userDefault
} else {
// if none, make one with our own default settings
@@ -147,7 +147,7 @@ func (logging *Logging) setupNewDefault(ctx Context) error {
if err != nil {
return fmt.Errorf("setting up default Caddy log: %v", err)
}
logging.Logs[DefaultLoggerName] = newDefault.CustomLog
logging.Logs["default"] = newDefault.CustomLog
}
// set up this new log
@@ -702,8 +702,6 @@ var (
var writers = NewUsagePool()
const DefaultLoggerName = "default"
// Interface guards
var (
_ io.WriteCloser = (*notClosable)(nil)
+8 -8
View File
@@ -44,7 +44,7 @@ import (
// Provisioner, the Provision() method is called. 4) If the
// module is a Validator, the Validate() method is called.
// 5) The module will probably be type-asserted from
// 'any' to some other, more useful interface expected
// interface{} to some other, more useful interface expected
// by the host module. For example, HTTP handler modules are
// type-asserted as caddyhttp.MiddlewareHandler values.
// 6) When a module's containing Context is canceled, if it is
@@ -172,7 +172,7 @@ func GetModule(name string) (ModuleInfo, error) {
// GetModuleName returns a module's name (the last label of its ID)
// from an instance of its value. If the value is not a module, an
// empty string will be returned.
func GetModuleName(instance any) string {
func GetModuleName(instance interface{}) string {
var name string
if mod, ok := instance.(Module); ok {
name = mod.CaddyModule().ID.Name()
@@ -182,7 +182,7 @@ func GetModuleName(instance any) string {
// GetModuleID returns a module's ID from an instance of its value.
// If the value is not a module, an empty string will be returned.
func GetModuleID(instance any) string {
func GetModuleID(instance interface{}) string {
var id string
if mod, ok := instance.(Module); ok {
id = string(mod.CaddyModule().ID)
@@ -259,7 +259,7 @@ func Modules() []string {
// where raw must be a JSON encoding of a map. It returns that value,
// along with the result of removing that key from raw.
func getModuleNameInline(moduleNameKey string, raw json.RawMessage) (string, json.RawMessage, error) {
var tmp map[string]any
var tmp map[string]interface{}
err := json.Unmarshal(raw, &tmp)
if err != nil {
return "", nil, err
@@ -324,11 +324,11 @@ func ParseStructTag(tag string) (map[string]string, error) {
if pair == "" {
continue
}
before, after, isCut := strings.Cut(pair, "=")
if !isCut {
parts := strings.SplitN(pair, "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("missing key in '%s' (pair %d)", pair, i)
}
results[before] = after
results[parts[0]] = parts[1]
}
return results, nil
}
@@ -337,7 +337,7 @@ func ParseStructTag(tag string) (map[string]string, error) {
// if any of the fields are unrecognized. Useful when decoding
// module configurations, where you want to be more sure they're
// correct.
func strictUnmarshalJSON(data []byte, v any) error {
func strictUnmarshalJSON(data []byte, v interface{}) error {
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
return dec.Decode(v)
-390
View File
@@ -1,390 +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 caddyevents
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/google/uuid"
"go.uber.org/zap"
)
func init() {
caddy.RegisterModule(App{})
}
// App implements a global eventing system within Caddy.
// Modules can emit and subscribe to events, providing
// hooks into deep parts of the code base that aren't
// otherwise accessible. Events provide information about
// what and when things are happening, and this facility
// allows handlers to take action when events occur,
// add information to the event's metadata, and even
// control program flow in some cases.
//
// Events are propagated in a DOM-like fashion. An event
// emitted from module `a.b.c` (the "origin") will first
// invoke handlers listening to `a.b.c`, then `a.b`,
// then `a`, then those listening regardless of origin.
// If a handler returns the special error Aborted, then
// propagation immediately stops and the event is marked
// as aborted. Emitters may optionally choose to adjust
// program flow based on an abort.
//
// Modules can subscribe to events by origin and/or name.
// A handler is invoked only if it is subscribed to the
// event by name and origin. Subscriptions should be
// registered during the provisioning phase, before apps
// are started.
//
// Event handlers are fired synchronously as part of the
// regular flow of the program. This allows event handlers
// to control the flow of the program if the origin permits
// it and also allows handlers to convey new information
// back into the origin module before it continues.
// In essence, event handlers are similar to HTTP
// middleware handlers.
//
// Event bindings/subscribers are unordered; i.e.
// event handlers are invoked in an arbitrary order.
// Event handlers should not rely on the logic of other
// handlers to succeed.
//
// The entirety of this app module is EXPERIMENTAL and
// subject to change. Pay attention to release notes.
type App struct {
// Subscriptions bind handlers to one or more events
// either globally or scoped to specific modules or module
// namespaces.
Subscriptions []*Subscription `json:"subscriptions,omitempty"`
// Map of event name to map of module ID/namespace to handlers
subscriptions map[string]map[caddy.ModuleID][]Handler
logger *zap.Logger
started bool
}
// Subscription represents binding of one or more handlers to
// one or more events.
type Subscription struct {
// The name(s) of the event(s) to bind to. Default: all events.
Events []string `json:"events,omitempty"`
// The ID or namespace of the module(s) from which events
// originate to listen to for events. Default: all modules.
//
// Events propagate up, so events emitted by module "a.b.c"
// will also trigger the event for "a.b" and "a". Thus, to
// receive all events from "a.b.c" and "a.b.d", for example,
// one can subscribe to either "a.b" or all of "a" entirely.
Modules []caddy.ModuleID `json:"modules,omitempty"`
// The event handler modules. These implement the actual
// behavior to invoke when an event occurs. At least one
// handler is required.
HandlersRaw []json.RawMessage `json:"handlers,omitempty" caddy:"namespace=events.handlers inline_key=handler"`
// The decoded handlers; Go code that is subscribing to
// an event should set this field directly; HandlersRaw
// is meant for JSON configuration to fill out this field.
Handlers []Handler `json:"-"`
}
// CaddyModule returns the Caddy module information.
func (App) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "events",
New: func() caddy.Module { return new(App) },
}
}
// Provision sets up the app.
func (app *App) Provision(ctx caddy.Context) error {
app.logger = ctx.Logger()
app.subscriptions = make(map[string]map[caddy.ModuleID][]Handler)
for _, sub := range app.Subscriptions {
if sub.HandlersRaw != nil {
handlersIface, err := ctx.LoadModule(sub, "HandlersRaw")
if err != nil {
return fmt.Errorf("loading event subscriber modules: %v", err)
}
for _, h := range handlersIface.([]any) {
sub.Handlers = append(sub.Handlers, h.(Handler))
}
if len(sub.Handlers) == 0 {
// pointless to bind without any handlers
return fmt.Errorf("no handlers defined")
}
}
}
return nil
}
// Start runs the app.
func (app *App) Start() error {
for _, sub := range app.Subscriptions {
if err := app.Subscribe(sub); err != nil {
return err
}
}
app.started = true
return nil
}
// Stop gracefully shuts down the app.
func (app *App) Stop() error {
return nil
}
// Subscribe binds one or more event handlers to one or more events
// according to the subscription s. For now, subscriptions can only
// be created during the provision phase; new bindings cannot be
// created after the events app has started.
func (app *App) Subscribe(s *Subscription) error {
if app.started {
return fmt.Errorf("events already started; new subscriptions closed")
}
// handle special case of catch-alls (omission of event name or module space implies all)
if len(s.Events) == 0 {
s.Events = []string{""}
}
if len(s.Modules) == 0 {
s.Modules = []caddy.ModuleID{""}
}
for _, eventName := range s.Events {
if app.subscriptions[eventName] == nil {
app.subscriptions[eventName] = make(map[caddy.ModuleID][]Handler)
}
for _, originModule := range s.Modules {
app.subscriptions[eventName][originModule] = append(app.subscriptions[eventName][originModule], s.Handlers...)
}
}
return nil
}
// On is syntactic sugar for Subscribe() that binds a single handler
// to a single event from any module. If the eventName is empty string,
// it counts for all events.
func (app *App) On(eventName string, handler Handler) error {
return app.Subscribe(&Subscription{
Events: []string{eventName},
Handlers: []Handler{handler},
})
}
// Emit creates and dispatches an event named eventName to all relevant handlers with
// the metadata data. Events are emitted and propagated synchronously. The returned Event
// value will have any additional information from the invoked handlers.
//
// Note that the data map is not copied, for efficiency. After Emit() is called, the
// data passed in should not be changed in other goroutines.
func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) Event {
logger := app.logger.With(zap.String("name", eventName))
id, err := uuid.NewRandom()
if err != nil {
logger.Error("failed generating new event ID", zap.Error(err))
}
eventName = strings.ToLower(eventName)
e := Event{
Data: data,
id: id,
ts: time.Now(),
name: eventName,
origin: ctx.Module(),
}
logger = logger.With(
zap.String("id", e.id.String()),
zap.String("origin", e.origin.CaddyModule().String()))
// add event info to replacer, make sure it's in the context
repl, ok := ctx.Context.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
if !ok {
repl = caddy.NewReplacer()
ctx.Context = context.WithValue(ctx.Context, caddy.ReplacerCtxKey, repl)
}
repl.Map(func(key string) (any, bool) {
switch key {
case "event":
return e, true
case "event.id":
return e.id, true
case "event.name":
return e.name, true
case "event.time":
return e.ts, true
case "event.time_unix":
return e.ts.UnixMilli(), true
case "event.module":
return e.origin.CaddyModule().ID, true
case "event.data":
return e.Data, true
}
if strings.HasPrefix(key, "event.data.") {
key = strings.TrimPrefix(key, "event.data.")
if val, ok := e.Data[key]; ok {
return val, true
}
}
return nil, false
})
logger.Debug("event", zap.Any("data", e.Data))
// invoke handlers bound to the event by name and also all events; this for loop
// iterates twice at most: once for the event name, once for "" (all events)
for {
moduleID := e.origin.CaddyModule().ID
// implement propagation up the module tree (i.e. start with "a.b.c" then "a.b" then "a" then "")
for {
if app.subscriptions[eventName] == nil {
break // shortcut if event not bound at all
}
for _, handler := range app.subscriptions[eventName][moduleID] {
select {
case <-ctx.Done():
logger.Error("context canceled; event handling stopped")
return e
default:
}
if err := handler.Handle(ctx, e); err != nil {
aborted := errors.Is(err, ErrAborted)
logger.Error("handler error",
zap.Error(err),
zap.Bool("aborted", aborted))
if aborted {
e.Aborted = err
return e
}
}
}
if moduleID == "" {
break
}
lastDot := strings.LastIndex(string(moduleID), ".")
if lastDot < 0 {
moduleID = "" // include handlers bound to events regardless of module
} else {
moduleID = moduleID[:lastDot]
}
}
// include handlers listening to all events
if eventName == "" {
break
}
eventName = ""
}
return e
}
// Event represents something that has happened or is happening.
// An Event value is not synchronized, so it should be copied if
// being used in goroutines.
//
// EXPERIMENTAL: As with the rest of this package, events are
// subject to change.
type Event struct {
// If non-nil, the event has been aborted, meaning
// propagation has stopped to other handlers and
// the code should stop what it was doing. Emitters
// may choose to use this as a signal to adjust their
// code path appropriately.
Aborted error
// The data associated with the event. Usually the
// original emitter will be the only one to set or
// change these values, but the field is exported
// so handlers can have full access if needed.
// However, this map is not synchronized, so
// handlers must not use this map directly in new
// goroutines; instead, copy the map to use it in a
// goroutine.
Data map[string]any
id uuid.UUID
ts time.Time
name string
origin caddy.Module
}
// CloudEvent exports event e as a structure that, when
// serialized as JSON, is compatible with the
// CloudEvents spec.
func (e Event) CloudEvent() CloudEvent {
dataJSON, _ := json.Marshal(e.Data)
return CloudEvent{
ID: e.id.String(),
Source: e.origin.CaddyModule().String(),
SpecVersion: "1.0",
Type: e.name,
Time: e.ts,
DataContentType: "application/json",
Data: dataJSON,
}
}
// CloudEvent is a JSON-serializable structure that
// is compatible with the CloudEvents specification.
// See https://cloudevents.io.
type CloudEvent struct {
ID string `json:"id"`
Source string `json:"source"`
SpecVersion string `json:"specversion"`
Type string `json:"type"`
Time time.Time `json:"time"`
DataContentType string `json:"datacontenttype,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
// ErrAborted cancels an event.
var ErrAborted = errors.New("event aborted")
// Handler is a type that can handle events.
type Handler interface {
Handle(context.Context, Event) error
}
// Interface guards
var (
_ caddy.App = (*App)(nil)
_ caddy.Provisioner = (*App)(nil)
)
@@ -1,88 +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 eventsconfig is for configuring caddyevents.App with the
// Caddyfile. This code can't be in the caddyevents package because
// the httpcaddyfile package imports caddyhttp, which imports
// caddyevents: hence, it creates an import cycle.
package eventsconfig
import (
"encoding/json"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyevents"
)
func init() {
httpcaddyfile.RegisterGlobalOption("events", parseApp)
}
// parseApp configures the "events" global option from Caddyfile to set up the events app.
// Syntax:
//
// events {
// on <event> <handler_module...>
// }
//
// If <event> is *, then it will bind to all events.
func parseApp(d *caddyfile.Dispenser, _ any) (any, error) {
app := new(caddyevents.App)
// consume the option name
if !d.Next() {
return nil, d.ArgErr()
}
// handle the block
for d.NextBlock(0) {
switch d.Val() {
case "on":
if !d.NextArg() {
return nil, d.ArgErr()
}
eventName := d.Val()
if eventName == "*" {
eventName = ""
}
if !d.NextArg() {
return nil, d.ArgErr()
}
handlerName := d.Val()
modID := "events.handlers." + handlerName
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
app.Subscriptions = append(app.Subscriptions, &caddyevents.Subscription{
Events: []string{eventName},
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(unm, "handler", handlerName, nil),
},
})
default:
return nil, d.ArgErr()
}
}
return httpcaddyfile.App{
Name: "events",
Value: caddyconfig.JSON(app, nil),
}, nil
}
+79 -240
View File
@@ -18,15 +18,13 @@ import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"strconv"
"sync"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyevents"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/lucas-clemente/quic-go/http3"
"go.uber.org/zap"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
@@ -54,20 +52,19 @@ func init() {
// `{http.request.duration_ms}` | Same as 'duration', but in milliseconds.
// `{http.request.uuid}` | The request unique identifier
// `{http.request.header.*}` | Specific request header field
// `{http.request.host}` | The host part of the request's Host header
// `{http.request.host.labels.*}` | Request host labels (0-based from right); e.g. for foo.example.com: 0=com, 1=example, 2=foo
// `{http.request.host}` | The host part of the request's Host header
// `{http.request.hostport}` | The host and port from the request's Host header
// `{http.request.method}` | The request method
// `{http.request.orig_method}` | The request's original method
// `{http.request.orig_uri}` | The request's original URI
// `{http.request.orig_uri.path}` | The request's original path
// `{http.request.orig_uri.path.*}` | Parts of the original path, split by `/` (0-based from left)
// `{http.request.orig_uri.path.dir}` | The request's original directory
// `{http.request.orig_uri.path.file}` | The request's original filename
// `{http.request.orig_uri.path}` | The request's original path
// `{http.request.orig_uri.query}` | The request's original query string (without `?`)
// `{http.request.orig_uri}` | The request's original URI
// `{http.request.port}` | The port part of the request's Host header
// `{http.request.proto}` | The protocol of the request
// `{http.request.remote.host}` | The host (IP) part of the remote client's address
// `{http.request.remote.host}` | The host part of the remote client's address
// `{http.request.remote.port}` | The port part of the remote client's address
// `{http.request.remote}` | The address of the remote client
// `{http.request.scheme}` | The request scheme
@@ -89,17 +86,15 @@ func init() {
// `{http.request.tls.client.san.emails.*}` | SAN email addresses (index optional)
// `{http.request.tls.client.san.ips.*}` | SAN IP addresses (index optional)
// `{http.request.tls.client.san.uris.*}` | SAN URIs (index optional)
// `{http.request.uri}` | The full request URI
// `{http.request.uri.path}` | The path component of the request URI
// `{http.request.uri.path.*}` | Parts of the path, split by `/` (0-based from left)
// `{http.request.uri.path.dir}` | The directory, excluding leaf filename
// `{http.request.uri.path.file}` | The filename of the path, excluding directory
// `{http.request.uri.query}` | The query string (without `?`)
// `{http.request.uri.path}` | The path component of the request URI
// `{http.request.uri.query.*}` | Individual query string value
// `{http.request.uri.query}` | The query string (without `?`)
// `{http.request.uri}` | The full request URI
// `{http.response.header.*}` | Specific response header field
// `{http.vars.*}` | Custom variables in the HTTP handler chain
// `{http.shutting_down}` | True if the HTTP app is shutting down
// `{http.time_until_shutdown}` | Time until HTTP server shutdown, if scheduled
type App struct {
// HTTPPort specifies the port to use for HTTP (as opposed to HTTPS),
// which is used when setting up HTTP->HTTPS redirects or ACME HTTP
@@ -112,31 +107,20 @@ type App struct {
HTTPSPort int `json:"https_port,omitempty"`
// GracePeriod is how long to wait for active connections when shutting
// down the servers. During the grace period, no new connections are
// accepted, idle connections are closed, and active connections will
// be given the full length of time to become idle and close.
// Once the grace period is over, connections will be forcefully closed.
// If zero, the grace period is eternal. Default: 0.
// down the server. Once the grace period is over, connections will
// be forcefully closed.
GracePeriod caddy.Duration `json:"grace_period,omitempty"`
// ShutdownDelay is how long to wait before initiating the grace
// period. When this app is stopping (e.g. during a config reload or
// process exit), all servers will be shut down. Normally this immediately
// initiates the grace period. However, if this delay is configured, servers
// will not be shut down until the delay is over. During this time, servers
// continue to function normally and allow new connections. At the end, the
// grace period will begin. This can be useful to allow downstream load
// balancers time to move this instance out of the rotation without hiccups.
//
// When shutdown has been scheduled, placeholders {http.shutting_down} (bool)
// and {http.time_until_shutdown} (duration) may be useful for health checks.
ShutdownDelay caddy.Duration `json:"shutdown_delay,omitempty"`
Strict *StrictOptions `json:"strict,omitempty"`
// Servers is the list of servers, keyed by arbitrary names chosen
// at your discretion for your own convenience; the keys do not
// affect functionality.
Servers map[string]*Server `json:"servers,omitempty"`
servers []*http.Server
h3servers []*http3.Server
ctx caddy.Context
logger *zap.Logger
tlsApp *caddytls.TLS
@@ -145,6 +129,13 @@ type App struct {
allCertDomains []string
}
type StrictOptions struct {
Disabled bool `json:"disable,omitempty"`
LenientQueryStrings bool `json:"lenient_query_strings,omitempty"`
LenientPaths bool `json:"lenient_paths,omitempty"`
LenientHeaders bool `json:"lenient_headers,omitempty"`
}
// CaddyModule returns the Caddy module information.
func (App) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
@@ -162,12 +153,7 @@ func (app *App) Provision(ctx caddy.Context) error {
}
app.tlsApp = tlsAppIface.(*caddytls.TLS)
app.ctx = ctx
app.logger = ctx.Logger()
eventsAppIface, err := ctx.App("events")
if err != nil {
return fmt.Errorf("getting events app: %v", err)
}
app.logger = ctx.Logger(app)
repl := caddy.NewReplacer()
@@ -180,33 +166,18 @@ func (app *App) Provision(ctx caddy.Context) error {
}
// prepare each server
oldContext := ctx.Context
for srvName, srv := range app.Servers {
ctx.Context = context.WithValue(oldContext, ServerCtxKey, srv)
srv.name = srvName
srv.tlsApp = app.tlsApp
srv.events = eventsAppIface.(*caddyevents.App)
srv.ctx = ctx
srv.logger = app.logger.Named("log")
srv.errorLogger = app.logger.Named("log.error")
srv.shutdownAtMu = new(sync.RWMutex)
srv.strict = app.Strict
// only enable access logs if configured
if srv.Logs != nil {
srv.accessLogger = app.logger.Named("log.access")
}
// the Go standard library does not let us serve only HTTP/2 using
// http.Server; we would probably need to write our own server
if !srv.protocol("h1") && (srv.protocol("h2") || srv.protocol("h2c")) {
return fmt.Errorf("server %s: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName)
}
// if no protocols configured explicitly, enable all except h2c
if len(srv.Protocols) == 0 {
srv.Protocols = []string{"h1", "h2", "h3"}
}
// if not explicitly configured by the user, disallow TLS
// client auth bypass (domain fronting) which could
// otherwise be exploited by sending an unprotected SNI
@@ -218,25 +189,18 @@ func (app *App) Provision(ctx caddy.Context) error {
// based on hostname
if srv.StrictSNIHost == nil && srv.hasTLSClientAuth() {
app.logger.Warn("enabling strict SNI-Host enforcement because TLS client auth is configured",
zap.String("server_id", srvName))
zap.String("server_id", srvName),
)
trueBool := true
srv.StrictSNIHost = &trueBool
}
// set up the trusted proxies source
for srv.TrustedProxiesRaw != nil {
val, err := ctx.LoadModule(srv, "TrustedProxiesRaw")
if err != nil {
return fmt.Errorf("loading trusted proxies modules: %v", err)
}
srv.trustedProxies = val.(IPRangeSource)
}
// process each listener address
for i := range srv.Listen {
lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true)
if err != nil {
return fmt.Errorf("server %s, listener %d: %v", srvName, i, err)
return fmt.Errorf("server %s, listener %d: %v",
srvName, i, err)
}
srv.Listen[i] = lnOut
}
@@ -248,7 +212,7 @@ func (app *App) Provision(ctx caddy.Context) error {
return fmt.Errorf("loading listener wrapper modules: %v", err)
}
var hasTLSPlaceholder bool
for i, val := range vals.([]any) {
for i, val := range vals.([]interface{}) {
if _, ok := val.(*tlsPlaceholderWrapper); ok {
if i == 0 {
// putting the tls placeholder wrapper first is nonsensical because
@@ -276,7 +240,7 @@ func (app *App) Provision(ctx caddy.Context) error {
// route handler so that important security checks are done, etc.
primaryRoute := emptyHandler
if srv.Routes != nil {
err := srv.Routes.ProvisionHandlers(ctx, srv.Metrics)
err := srv.Routes.ProvisionHandlers(ctx)
if err != nil {
return fmt.Errorf("server %s: setting up route handlers: %v", srvName, err)
}
@@ -306,7 +270,7 @@ func (app *App) Provision(ctx caddy.Context) error {
srv.IdleTimeout = defaultIdleTimeout
}
}
ctx.Context = oldContext
return nil
}
@@ -344,7 +308,7 @@ func (app *App) Start() error {
}
for srvName, srv := range app.Servers {
srv.server = &http.Server{
s := &http.Server{
ReadTimeout: time.Duration(srv.ReadTimeout),
ReadHeaderTimeout: time.Duration(srv.ReadHeaderTimeout),
WriteTimeout: time.Duration(srv.WriteTimeout),
@@ -354,38 +318,12 @@ func (app *App) Start() error {
ErrorLog: serverLogger,
}
// disable HTTP/2, which we enabled by default during provisioning
if !srv.protocol("h2") {
srv.server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
for _, cp := range srv.TLSConnPolicies {
// the TLSConfig was already provisioned, so... manually remove it
for i, np := range cp.TLSConfig.NextProtos {
if np == "h2" {
cp.TLSConfig.NextProtos = append(cp.TLSConfig.NextProtos[:i], cp.TLSConfig.NextProtos[i+1:]...)
break
}
}
// remove it from the parent connection policy too, just to keep things tidy
for i, alpn := range cp.ALPN {
if alpn == "h2" {
cp.ALPN = append(cp.ALPN[:i], cp.ALPN[i+1:]...)
break
}
}
}
}
// this TLS config is used by the std lib to choose the actual TLS config for connections
// by looking through the connection policies to find the first one that matches
tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx)
srv.configureServer(srv.server)
// enable H2C if configured
if srv.protocol("h2c") {
// enable h2c if configured
if srv.AllowH2C {
h2server := &http2.Server{
IdleTimeout: time.Duration(srv.IdleTimeout),
}
srv.server.Handler = h2c.NewHandler(srv, h2server)
s.Handler = h2c.NewHandler(srv, h2server)
}
for _, lnAddr := range srv.Listen {
@@ -393,16 +331,13 @@ func (app *App) Start() error {
if err != nil {
return fmt.Errorf("%s: parsing listen address '%s': %v", srvName, lnAddr, err)
}
srv.addresses = append(srv.addresses, listenAddr)
for portOffset := uint(0); portOffset < listenAddr.PortRangeSize(); portOffset++ {
// create the listener for this socket
hostport := listenAddr.JoinHostPort(portOffset)
lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{KeepAlive: time.Duration(srv.KeepAliveInterval)})
ln, err := caddy.Listen(listenAddr.Network, hostport)
if err != nil {
return fmt.Errorf("listening on %s: %v", listenAddr.At(portOffset), err)
return fmt.Errorf("%s: listening on %s: %v", listenAddr.Network, hostport, err)
}
ln := lnAny.(net.Listener)
// wrap listener before TLS (up to the TLS placeholder wrapper)
var lnWrapperIdx int
@@ -417,33 +352,34 @@ func (app *App) Start() error {
// enable TLS if there is a policy and if this is not the HTTP port
useTLS := len(srv.TLSConnPolicies) > 0 && int(listenAddr.StartPort+portOffset) != app.httpPort()
if useTLS {
// create TLS listener - this enables and terminates TLS
// create TLS listener
tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx)
ln = tls.NewListener(ln, tlsCfg)
// enable HTTP/3 if configured
if srv.protocol("h3") {
// Can't serve HTTP/3 on the same socket as HTTP/1 and 2 because it uses
// a different transport mechanism... which is fine, but the OS doesn't
// differentiate between a SOCK_STREAM file and a SOCK_DGRAM file; they
// are still one file on the system. So even though "unixpacket" and
// "unixgram" are different network types just as "tcp" and "udp" are,
// the OS will not let us use the same file as both STREAM and DGRAM.
if len(srv.Protocols) > 1 && listenAddr.IsUnixNetwork() {
app.logger.Warn("HTTP/3 disabled because Unix can't multiplex STREAM and DGRAM on same socket",
zap.String("file", hostport))
for i := range srv.Protocols {
if srv.Protocols[i] == "h3" {
srv.Protocols = append(srv.Protocols[:i], srv.Protocols[i+1:]...)
break
}
}
} else {
app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport))
if err := srv.serveHTTP3(listenAddr.At(portOffset), tlsCfg); err != nil {
return err
}
/////////
// TODO: HTTP/3 support is experimental for now
if srv.ExperimentalHTTP3 {
app.logger.Info("enabling experimental HTTP/3 listener",
zap.String("addr", hostport),
)
h3ln, err := caddy.ListenQUIC(hostport, tlsCfg)
if err != nil {
return fmt.Errorf("getting HTTP/3 QUIC listener: %v", err)
}
h3srv := &http3.Server{
Server: &http.Server{
Addr: hostport,
Handler: srv,
TLSConfig: tlsCfg,
ErrorLog: serverLogger,
},
}
//nolint:errcheck
go h3srv.ServeListener(h3ln)
app.h3servers = append(app.h3servers, h3srv)
srv.h3server = h3srv
}
/////////
}
// finish wrapping listener where we left off before TLS
@@ -453,30 +389,24 @@ func (app *App) Start() error {
// if binding to port 0, the OS chooses a port for us;
// but the user won't know the port unless we print it
if !listenAddr.IsUnixNetwork() && listenAddr.StartPort == 0 && listenAddr.EndPort == 0 {
if listenAddr.StartPort == 0 && listenAddr.EndPort == 0 {
app.logger.Info("port 0 listener",
zap.String("input_address", lnAddr),
zap.String("actual_address", ln.Addr().String()))
zap.String("actual_address", ln.Addr().String()),
)
}
app.logger.Debug("starting server loop",
zap.String("address", ln.Addr().String()),
zap.Bool("http3", srv.ExperimentalHTTP3),
zap.Bool("tls", useTLS),
zap.Bool("http3", srv.h3server != nil))
)
srv.listeners = append(srv.listeners, ln)
// enable HTTP/1 if configured
if srv.protocol("h1") {
//nolint:errcheck
go srv.server.Serve(ln)
}
//nolint:errcheck
go s.Serve(ln)
app.servers = append(app.servers, s)
}
}
srv.logger.Info("server running",
zap.String("name", srvName),
zap.Strings("protocols", srv.Protocols))
}
// finish automatic HTTPS by finally beginning
@@ -492,117 +422,26 @@ func (app *App) Start() error {
// Stop gracefully shuts down the HTTP server.
func (app *App) Stop() error {
ctx := context.Background()
// see if any listeners in our config will be closing or if they are continuing
// hrough a reload; because if any are closing, we will enforce shutdown delay
var delay bool
scheduledTime := time.Now().Add(time.Duration(app.ShutdownDelay))
if app.ShutdownDelay > 0 {
for _, server := range app.Servers {
for _, na := range server.addresses {
for _, addr := range na.Expand() {
if caddy.ListenerUsage(addr.Network, addr.JoinHostPort(0)) < 2 {
app.logger.Debug("listener closing and shutdown delay is configured", zap.String("address", addr.String()))
server.shutdownAtMu.Lock()
server.shutdownAt = scheduledTime
server.shutdownAtMu.Unlock()
delay = true
} else {
app.logger.Debug("shutdown delay configured but listener will remain open", zap.String("address", addr.String()))
}
}
}
}
}
// honor scheduled/delayed shutdown time
if delay {
app.logger.Debug("shutdown scheduled",
zap.Duration("delay_duration", time.Duration(app.ShutdownDelay)),
zap.Time("time", scheduledTime))
time.Sleep(time.Duration(app.ShutdownDelay))
}
// enforce grace period if configured
if app.GracePeriod > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(app.GracePeriod))
defer cancel()
app.logger.Debug("servers shutting down; grace period initiated", zap.Duration("duration", time.Duration(app.GracePeriod)))
} else {
app.logger.Debug("servers shutting down with eternal grace period")
}
// goroutines aren't guaranteed to be scheduled right away,
// so we'll use one WaitGroup to wait for all the goroutines
// to start their server shutdowns, and another to wait for
// them to finish; we'll always block for them to start so
// that when we return the caller can be confident* that the
// old servers are no longer accepting new connections
// (* the scheduler might still pause them right before
// calling Shutdown(), but it's unlikely)
var startedShutdown, finishedShutdown sync.WaitGroup
// these will run in goroutines
stopServer := func(server *Server) {
defer finishedShutdown.Done()
startedShutdown.Done()
if err := server.server.Shutdown(ctx); err != nil {
app.logger.Error("server shutdown",
zap.Error(err),
zap.Strings("addresses", server.Listen))
}
}
stopH3Server := func(server *Server) {
defer finishedShutdown.Done()
startedShutdown.Done()
if server.h3server == nil {
return
}
// TODO: we have to manually close our listeners because quic-go won't
// close listeners it didn't create along with the server itself...
// see https://github.com/quic-go/quic-go/issues/3560
for _, el := range server.h3listeners {
if err := el.Close(); err != nil {
app.logger.Error("HTTP/3 listener close",
zap.Error(err),
zap.String("address", el.LocalAddr().String()))
}
}
// TODO: CloseGracefully, once implemented upstream (see https://github.com/quic-go/quic-go/issues/2103)
if err := server.h3server.Close(); err != nil {
app.logger.Error("HTTP/3 server shutdown",
zap.Error(err),
zap.Strings("addresses", server.Listen))
for _, s := range app.servers {
err := s.Shutdown(ctx)
if err != nil {
return err
}
}
for _, server := range app.Servers {
startedShutdown.Add(2)
finishedShutdown.Add(2)
go stopServer(server)
go stopH3Server(server)
for _, s := range app.h3servers {
// TODO: CloseGracefully, once implemented upstream
// (see https://github.com/lucas-clemente/quic-go/issues/2103)
err := s.Close()
if err != nil {
return err
}
}
// block until all the goroutines have been run by the scheduler;
// this means that they have likely called Shutdown() by now
startedShutdown.Wait()
// if the process is exiting, we need to block here and wait
// for the grace periods to complete, otherwise the process will
// terminate before the servers are finished shutting down; but
// we don't really need to wait for the grace period to finish
// if the process isn't exiting (but note that frequent config
// reloads with long grace periods for a sustained length of time
// may deplete resources)
if caddy.Exiting() {
finishedShutdown.Wait()
}
return nil
}
+11 -32
View File
@@ -93,9 +93,6 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// https://github.com/caddyserver/caddy/issues/3443)
redirDomains := make(map[string][]caddy.NetworkAddress)
// the log configuration for an HTTPS enabled server
var logCfg *ServerLogConfig
for srvName, srv := range app.Servers {
// as a prerequisite, provision route matchers; this is
// required for all routes on all servers, and must be
@@ -175,13 +172,6 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
continue
}
// clone the logger so we can apply it to the HTTP server
// (not sure if necessary to clone it; but probably safer)
// (we choose one log cfg arbitrarily; not sure which is best)
if srv.Logs != nil {
logCfg = srv.Logs.clone()
}
// for all the hostnames we found, filter them so we have
// a deduplicated list of names for which to obtain certs
// (only if cert management not disabled for this server)
@@ -378,29 +368,19 @@ redirServersLoop:
// we'll create a new server for all the listener addresses
// that are unused and serve the remaining redirects from it
for _, srv := range app.Servers {
// only look at servers which listen on an address which
// we want to add redirects to
if !srv.hasListenerAddress(redirServerAddr) {
continue
}
// find the index of the route after the last route with a host
// matcher, then insert the redirects there, but before any
// user-defined catch-all routes
// see https://github.com/caddyserver/caddy/issues/3212
insertIndex := srv.findLastRouteWithHostMatcher()
// add the redirects at the insert index, except for when
// we have a catch-all for HTTPS, in which case the user's
// defined catch-all should take precedence. See #4829
if len(uniqueDomainsForCerts) != 0 {
if srv.hasListenerAddress(redirServerAddr) {
// find the index of the route after the last route with a host
// matcher, then insert the redirects there, but before any
// user-defined catch-all routes
// see https://github.com/caddyserver/caddy/issues/3212
insertIndex := srv.findLastRouteWithHostMatcher()
srv.Routes = append(srv.Routes[:insertIndex], append(routes, srv.Routes[insertIndex:]...)...)
// append our catch-all route in case the user didn't define their own
srv.Routes = appendCatchAll(srv.Routes)
continue redirServersLoop
}
// append our catch-all route in case the user didn't define their own
srv.Routes = appendCatchAll(srv.Routes)
continue redirServersLoop
}
// no server with this listener address exists;
@@ -420,7 +400,6 @@ redirServersLoop:
app.Servers["remaining_auto_https_redirects"] = &Server{
Listen: redirServerAddrsList,
Routes: appendCatchAll(redirRoutes),
Logs: logCfg,
}
}
+10 -28
View File
@@ -21,12 +21,10 @@ import (
"fmt"
weakrand "math/rand"
"net/http"
"strings"
"sync"
"time"
"github.com/caddyserver/caddy/v2"
"golang.org/x/sync/singleflight"
)
func init() {
@@ -96,7 +94,10 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
// if supported, generate a fake password we can compare against if needed
if hasher, ok := hba.Hash.(Hasher); ok {
hba.fakePassword = hasher.FakeHash()
hba.fakePassword, err = hasher.Hash([]byte("antitiming"), []byte("fakesalt"))
if err != nil {
return fmt.Errorf("generating anti-timing password hash: %v", err)
}
}
repl := caddy.NewReplacer()
@@ -116,19 +117,10 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
return fmt.Errorf("account %d: username and password are required", i)
}
// TODO: Remove support for redundantly-encoded b64-encoded hashes
// Passwords starting with '$' are likely in Modular Crypt Format,
// so we don't need to base64 decode them. But historically, we
// required redundant base64, so we try to decode it otherwise.
if strings.HasPrefix(acct.Password, "$") {
acct.password = []byte(acct.Password)
} else {
acct.password, err = base64.StdEncoding.DecodeString(acct.Password)
if err != nil {
return fmt.Errorf("base64-decoding password: %v", err)
}
acct.password, err = base64.StdEncoding.DecodeString(acct.Password)
if err != nil {
return fmt.Errorf("base64-decoding password: %v", err)
}
if acct.Salt != "" {
acct.salt, err = base64.StdEncoding.DecodeString(acct.Salt)
if err != nil {
@@ -143,7 +135,6 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
if hba.HashCache != nil {
hba.HashCache.cache = make(map[string]bool)
hba.HashCache.mu = new(sync.RWMutex)
hba.HashCache.g = new(singleflight.Group)
}
return nil
@@ -192,18 +183,12 @@ func (hba HTTPBasicAuth) correctPassword(account Account, plaintextPassword []by
if ok {
return same, nil
}
// slow track: do the expensive op, then add it to the cache
// but perform it in a singleflight group so that multiple
// parallel requests using the same password don't cause a
// thundering herd problem by all performing the same hashing
// operation before the first one finishes and caches it.
v, err, _ := hba.HashCache.g.Do(cacheKey, func() (any, error) {
return compare()
})
same, err := compare()
if err != nil {
return false, err
}
same = v.(bool)
hba.HashCache.mu.Lock()
if len(hba.HashCache.cache) >= 1000 {
hba.HashCache.makeRoom() // keep cache size under control
@@ -231,7 +216,6 @@ func (hba HTTPBasicAuth) promptForCredentials(w http.ResponseWriter, err error)
// compute on every HTTP request.
type Cache struct {
mu *sync.RWMutex
g *singleflight.Group
// map of concatenated hashed password + plaintext password + salt, to result
cache map[string]bool
@@ -287,11 +271,9 @@ type Comparer interface {
// that require a salt). Hashing modules which implement
// this interface can be used with the hash-password
// subcommand as well as benefitting from anti-timing
// features. A hasher also returns a fake hash which
// can be used for timing side-channel mitigation.
// features.
type Hasher interface {
Hash(plaintext, salt []byte) ([]byte, error)
FakeHash() []byte
}
// Account contains a username, password, and salt (if applicable).
+2 -2
View File
@@ -56,13 +56,13 @@ func (Authentication) CaddyModule() caddy.ModuleInfo {
// Provision sets up a.
func (a *Authentication) Provision(ctx caddy.Context) error {
a.logger = ctx.Logger()
a.logger = ctx.Logger(a)
a.Providers = make(map[string]Authenticator)
mods, err := ctx.LoadModule(a, "ProvidersRaw")
if err != nil {
return fmt.Errorf("loading authentication providers: %v", err)
}
for modName, modIface := range mods.(map[string]any) {
for modName, modIface := range mods.(map[string]interface{}) {
a.Providers[modName] = modIface.(Authenticator)
}
return nil

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