caddy-2.11.2/ 0000775 0000000 0000000 00000000000 15173660016 0012666 5 ustar 00root root 0000000 0000000 caddy-2.11.2/.editorconfig 0000664 0000000 0000000 00000000140 15173660016 0015336 0 ustar 00root root 0000000 0000000 [*]
end_of_line = lf
[caddytest/integration/caddyfile_adapt/*.caddyfiletest]
indent_style = tab caddy-2.11.2/.gitattributes 0000664 0000000 0000000 00000000020 15173660016 0015551 0 ustar 00root root 0000000 0000000 *.go text eol=lf caddy-2.11.2/.gitignore 0000664 0000000 0000000 00000000526 15173660016 0014661 0 ustar 00root root 0000000 0000000 _gitignore/
*.log
Caddyfile
Caddyfile.*
!caddyfile/
!caddyfile.go
# artifacts from pprof tooling
*.prof
*.test
# build artifacts and helpers
cmd/caddy/caddy
cmd/caddy/caddy.exe
cmd/caddy/tmp/*.exe
cmd/caddy/.env
# mac specific
.DS_Store
# go modules
vendor
# goreleaser artifacts
dist
caddy-build
caddy-dist
# IDE files
.idea/
.vscode/
caddy-2.11.2/.golangci.yml 0000664 0000000 0000000 00000006075 15173660016 0015262 0 ustar 00root root 0000000 0000000 version: "2"
run:
issues-exit-code: 1
tests: false
build-tags:
- nobadger
- nomysql
- nopgx
output:
formats:
text:
path: stdout
print-linter-name: true
print-issued-lines: true
linters:
default: none
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- errcheck
- errname
- exhaustive
- gosec
- govet
- importas
- ineffassign
- misspell
- modernize
- prealloc
- promlinter
- sloglint
- sqlclosecheck
- staticcheck
- testableexamples
- testifylint
- tparallel
- unconvert
- unused
- wastedassign
- whitespace
- zerologlint
settings:
staticcheck:
checks: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-QF1006", "-QF1008"] # default, and exclude 1 more undesired check
errcheck:
exclude-functions:
- fmt.*
- (go.uber.org/zap/zapcore.ObjectEncoder).AddObject
- (go.uber.org/zap/zapcore.ObjectEncoder).AddArray
exhaustive:
ignore-enum-types: reflect.Kind|svc.Cmd
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- gosec
text: G115 # TODO: Either we should fix the issues or nuke the linter if it's bad
- linters:
- gosec
text: G107 # we aren't calling unknown URL
- linters:
- gosec
text: G203 # as a web server that's expected to handle any template, this is totally in the hands of the user.
- linters:
- gosec
text: G204 # we're shelling out to known commands, not relying on user-defined input.
- linters:
- gosec
# the choice of weakrand is deliberate, hence the named import "weakrand"
path: modules/caddyhttp/reverseproxy/selectionpolicies.go
text: G404
- linters:
- gosec
path: modules/caddyhttp/reverseproxy/streaming.go
text: G404
- linters:
- dupl
path: modules/logging/filters.go
- linters:
- dupl
path: modules/caddyhttp/matchers.go
- linters:
- dupl
path: modules/caddyhttp/vars.go
- linters:
- errcheck
path: _test\.go
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
settings:
gci:
sections:
- standard # Standard section: captures all standard packages.
- default # Default section: contains all imports that could not be matched to another section type.
- prefix(github.com/caddyserver/caddy/v2/cmd) # ensure that this is always at the top and always has a line break.
- prefix(github.com/caddyserver/caddy) # Custom section: groups all imports with the specified Prefix.
custom-order: true
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
caddy-2.11.2/.goreleaser.yml 0000664 0000000 0000000 00000013550 15173660016 0015623 0 ustar 00root root 0000000 0000000 version: 2
before:
hooks:
# The build is done in this particular way to build Caddy in a designated directory named in .gitignore.
# 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 vendor
# vendor Caddy deps
- go mod vendor
- mkdir -p caddy-build
- cp cmd/caddy/main.go caddy-build/main.go
- /bin/sh -c 'cd ./caddy-build && go mod init caddy'
# prepare syso files for windows embedding
- /bin/sh -c 'for a in amd64 arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a xcaddy build {{.Env.TAG}}; done'
- /bin/sh -c 'mv /tmp/buildenv_*/*.syso caddy-build'
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
- go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod
# as of Go 1.16, `go` commands no longer automatically change go.{mod,sum}. We now have to explicitly
# run `go mod tidy`. The `/bin/sh -c '...'` is because goreleaser can't find cd in PATH without shell invocation.
- /bin/sh -c 'cd ./caddy-build && go mod tidy'
# vendor the deps of the prepared to-build module
- /bin/sh -c 'cd ./caddy-build && go mod vendor'
- git clone --depth 1 https://github.com/caddyserver/dist caddy-dist
- 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:
- CGO_ENABLED=0
- GO111MODULE=on
dir: ./caddy-build
binary: caddy
goos:
- darwin
- linux
- windows
- freebsd
goarch:
- amd64
- arm
- arm64
- s390x
- ppc64le
- riscv64
goarm:
- "5"
- "6"
- "7"
ignore:
- goos: darwin
goarch: arm
- goos: darwin
goarch: ppc64le
- goos: darwin
goarch: s390x
- goos: darwin
goarch: riscv64
- goos: windows
goarch: ppc64le
- goos: windows
goarch: s390x
- goos: windows
goarch: riscv64
- goos: windows
goarch: arm
- goos: freebsd
goarch: ppc64le
- goos: freebsd
goarch: s390x
- goos: freebsd
goarch: riscv64
- goos: freebsd
goarch: arm
goarm: "5"
flags:
- -trimpath
- -mod=readonly
ldflags:
- -s -w
tags:
- nobadger
- nomysql
- nopgx
signs:
- cmd: cosign
signature: "${artifact}.sig"
certificate: '{{ trimsuffix (trimsuffix .Env.artifact ".zip") ".tar.gz" }}.pem'
args: ["sign-blob", "--yes", "--output-signature=${signature}", "--output-certificate", "${certificate}", "${artifact}"]
artifacts: all
sboms:
- artifacts: binary
documents:
- >-
{{ .ProjectName }}_
{{- .Version }}_
{{- if eq .Os "darwin" }}mac{{ else }}{{ .Os }}{{ end }}_
{{- .Arch }}
{{- with .Arm }}v{{ . }}{{ end }}
{{- with .Mips }}_{{ . }}{{ end }}
{{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}.sbom
cmd: syft
args: ["$artifact", "--file", "${document}", "--output", "cyclonedx-json"]
archives:
- id: default
format_overrides:
- goos: windows
formats: 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 }}
# package the 'caddy-build' directory into a tarball,
# allowing users to build the exact same set of files as ours.
- id: source
meta: true
name_template: "{{ .ProjectName }}_{{ .Version }}_buildable-artifact"
files:
- src: LICENSE
dst: ./LICENSE
- src: README.md
dst: ./README.md
- src: AUTHORS
dst: ./AUTHORS
- src: ./caddy-build
dst: ./
source:
enabled: true
name_template: '{{ .ProjectName }}_{{ .Version }}_src'
format: 'tar.gz'
# Additional files/template/globs you want to add to the source archive.
#
# Default: empty.
files:
- vendor
checksum:
algorithm: sha512
nfpms:
- id: default
package_name: caddy
vendor: Dyanim
homepage: https://caddyserver.com
maintainer: Matthew Holt
description: |
Caddy - Powerful, enterprise-ready, open source web server with automatic HTTPS written in Go
license: Apache 2.0
formats:
- deb
# - rpm
bindir: /usr/bin
contents:
- src: ./caddy-dist/init/caddy.service
dst: /lib/systemd/system/caddy.service
- src: ./caddy-dist/init/caddy-api.service
dst: /lib/systemd/system/caddy-api.service
- src: ./caddy-dist/welcome/index.html
dst: /usr/share/caddy/index.html
- src: ./caddy-dist/scripts/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
provides:
- httpd
release:
github:
owner: caddyserver
name: caddy
draft: true
prerelease: auto
changelog:
sort: asc
filters:
exclude:
- '^chore:'
- '^ci:'
- '^docs?:'
- '^readme:'
- '^tests?:'
- '^\w+\s+' # a hack to remove commit messages without colons thus don't correspond to a package
caddy-2.11.2/.pre-commit-config.yaml 0000664 0000000 0000000 00000000741 15173660016 0017151 0 ustar 00root root 0000000 0000000 repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.16.3
hooks:
- id: gitleaks
- repo: https://github.com/golangci/golangci-lint
rev: v1.52.2
hooks:
- id: golangci-lint-config-verify
- id: golangci-lint
- id: golangci-lint-fmt
- repo: https://github.com/jumanjihouse/pre-commit-hooks
rev: 3.0.0
hooks:
- id: shellcheck
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
caddy-2.11.2/AUTHORS 0000664 0000000 0000000 00000000617 15173660016 0013742 0 ustar 00root root 0000000 0000000 # This is the official list of Caddy Authors for copyright purposes.
# Authors may be either individual people or legal entities.
#
# Not all individual contributors are authors. For the full list of
# contributors, refer to the project's page on GitHub or the repo's
# commit history.
Matthew Holt
Light Code Labs
Ardan Labs
caddy-2.11.2/LICENSE 0000664 0000000 0000000 00000026136 15173660016 0013703 0 ustar 00root root 0000000 0000000
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
caddy-2.11.2/README.md 0000664 0000000 0000000 00000030664 15173660016 0014156 0 ustar 00root root 0000000 0000000
a project
Every site on HTTPS
Caddy is an extensible server platform that uses TLS by default.
Special thanks to:
### [Warp, built for coding with multiple AI agents](https://go.warp.dev/caddy)
[Available for MacOS, Linux, & Windows](https://go.warp.dev/caddy)
### Menu
- [Features](#features)
- [Install](#install)
- [Build from source](#build-from-source)
- [For development](#for-development)
- [With version information and/or plugins](#with-version-information-andor-plugins)
- [Quick start](#quick-start)
- [Overview](#overview)
- [Full documentation](#full-documentation)
- [Getting help](#getting-help)
- [About](#about)
## [Features](https://caddyserver.com/features)
- **Easy configuration** with the [Caddyfile](https://caddyserver.com/docs/caddyfile)
- **Powerful configuration** with its [native JSON config](https://caddyserver.com/docs/json/)
- **Dynamic configuration** with the [JSON API](https://caddyserver.com/docs/api)
- [**Config adapters**](https://caddyserver.com/docs/config-adapters) if you don't like JSON
- **Automatic HTTPS** by default
- [ZeroSSL](https://zerossl.com) and [Let's Encrypt](https://letsencrypt.org) for public names
- Fully-managed local CA for internal names & IPs
- Can coordinate with other Caddy instances in a cluster
- Multi-issuer fallback
- Encrypted ClientHello (ECH) support
- **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** all supported by default
- **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/features)
## 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.
See [our online documentation](https://caddyserver.com/docs/install) for other install instructions.
## Build from source
Requirements:
- [Go 1.25.0 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
$ git clone "https://github.com/caddyserver/caddy.git"
$ cd caddy/cmd/caddy/
$ go build
```
When you run Caddy, it may try to bind to low ports unless otherwise specified in your config. If your OS requires elevated privileges for this, you will need to give your new binary permission to do so. On Linux, this can be done easily with: `sudo setcap cap_net_bind_service=+ep ./caddy`
If you prefer to use `go run` which only creates temporary binaries, you can still do this with the included `setcap.sh` like so:
```bash
$ go run -exec ./setcap.sh main.go
```
If you don't want to type your password for `setcap`, use `sudo visudo` to edit your sudoers file and allow your user account to run that command without a password, for example:
```
username ALL=(ALL:ALL) NOPASSWD: /usr/sbin/setcap
```
replacing `username` with your actual username. Please be careful and only do this if you know what you are doing! We are only qualified to document how to use Caddy, not Go tooling or your computer, and we are providing these instructions for convenience only; please learn how to use your own computer at your own risk and make any needful adjustments.
Then you can run the tests in all modules or a specific one:
```bash
$ go test ./...
$ go test ./modules/caddyhttp/tracing/
```
### With version information and/or plugins
Using [our builder tool, `xcaddy`](https://github.com/caddyserver/xcaddy)...
```bash
$ xcaddy build
```
...the following steps are automated:
1. Create a new folder: `mkdir caddy`
2. Change into it: `cd caddy`
3. Copy [Caddy's main.go](https://github.com/caddyserver/caddy/blob/master/cmd/caddy/main.go) into the empty folder. Add imports for any custom plugins you want to add.
4. Initialize a Go module: `go mod init caddy`
5. (Optional) Pin Caddy version: `go get github.com/caddyserver/caddy/v2@version` replacing `version` with a git tag, commit, or branch name.
6. (Optional) Add plugins by adding their import: `_ "import/path/here"`
7. Compile: `go build -tags=nobadger,nomysql,nopgx`
## Quick start
The [Caddy website](https://caddyserver.com/docs/) has documentation that includes tutorials, quick-start guides, reference, and more.
**We recommend that all users -- regardless of experience level -- do our [Getting Started](https://caddyserver.com/docs/getting-started) guide to become familiar with using Caddy.**
If you've only got a minute, [the website has several quick-start tutorials](https://caddyserver.com/docs/quick-starts) to choose from! However, after finishing a quick-start tutorial, please read more documentation to understand how the software works. 🙂
## Overview
Caddy is most often used as an HTTPS server, but it is suitable for any long-running Go program. First and foremost, it is a platform to run Go applications. Caddy "apps" are just Go programs that are implemented as Caddy modules. Two apps -- `tls` and `http` -- ship standard with Caddy.
Caddy apps instantly benefit from [automated documentation](https://caddyserver.com/docs/json/), graceful on-line [config changes via API](https://caddyserver.com/docs/api), and unification with other Caddy apps.
Although [JSON](https://caddyserver.com/docs/json/) is Caddy's native config language, Caddy can accept input from [config adapters](https://caddyserver.com/docs/config-adapters) which can essentially convert any config format of your choice into JSON: Caddyfile, JSON 5, YAML, TOML, NGINX config, and more.
The primary way to configure Caddy is through [its API](https://caddyserver.com/docs/api), but if you prefer config files, the [command-line interface](https://caddyserver.com/docs/command-line) supports those too.
Caddy exposes an unprecedented level of control compared to any web server in existence. In Caddy, you are usually setting the actual values of the initialized types in memory that power everything from your HTTP handlers and TLS handshakes to your storage medium. Caddy is also ridiculously extensible, with a powerful plugin system that makes vast improvements over other web servers.
To wield the power of this design, you need to know how the config document is structured. Please see [our documentation site](https://caddyserver.com/docs/) for details about [Caddy's config structure](https://caddyserver.com/docs/json/).
Nearly all of Caddy's configuration is contained in a single config document, rather than being scattered across CLI flags and env variables and a configuration file as with other web servers. This makes managing your server config more straightforward and reduces hidden variables/factors.
## Full documentation
Our website has complete documentation:
**https://caddyserver.com/docs/**
The docs are also open source. You can contribute to them here: https://github.com/caddyserver/website
## Getting help
- We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com) 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!
- 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!
Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only for bug reports and feature requests, i.e. actionable development items (support questions will usually be referred to the forums).
## About
Matthew Holt began developing Caddy in 2014 while studying computer science at Brigham Young University. (The name "Caddy" was chosen because this software helps with the tedious, mundane tasks of serving the Web, and is also a single place for multiple things to be organized together.) It soon became the first web server to use HTTPS automatically and by default, and now has hundreds of contributors and has served trillions of HTTPS requests.
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH.
- _Project on X: [@caddyserver](https://x.com/caddyserver)_
- _Author on X: [@mholt6](https://x.com/mholt6)_
Caddy is a project of [ZeroSSL](https://zerossl.com), an HID Global 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.
caddy-2.11.2/admin.go 0000664 0000000 0000000 00000136374 15173660016 0014323 0 ustar 00root root 0000000 0000000 // 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 caddy
import (
"bytes"
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"expvar"
"fmt"
"hash"
"io"
"net"
"net/http"
"net/http/pprof"
"net/url"
"os"
"path"
"regexp"
"slices"
"strconv"
"strings"
"sync"
"time"
"github.com/caddyserver/certmagic"
"github.com/cespare/xxhash/v2"
"github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// testCertMagicStorageOverride is a package-level test hook. Tests may set
// this variable to provide a temporary certmagic.Storage so that cert
// management in tests does not hit the real default storage on disk.
// This must NOT be set in production code.
var testCertMagicStorageOverride certmagic.Storage
func init() {
// The hard-coded default `DefaultAdminListen` can be overridden
// 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 {
// If true, the admin endpoint will be completely disabled.
// Note that this makes any runtime changes to the config
// impossible, since the interface to do so is through the
// admin endpoint.
Disabled bool `json:"disabled,omitempty"`
// 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.
//
// Remember: When changing this value through a config reload,
// be sure to use the `--address` CLI flag to specify the current
// admin address if the currently-running admin endpoint is not
// the default address.
Listen string `json:"listen,omitempty"`
// If true, CORS headers will be emitted, and requests to the
// API will be rejected if their `Host` and `Origin` headers
// do not match the expected value(s). Use `origins` to
// customize which origins/hosts are allowed. If `origins` is
// not set, the listen address is the only value allowed by
// default. Enforced only on local (plaintext) endpoint.
EnforceOrigin bool `json:"enforce_origin,omitempty"`
// The list of allowed origins/hosts for API requests. Only needed
// if accessing the admin endpoint from a host different from the
// socket's network interface or if `enforce_origin` is true. If not
// set, the listener address will be the default value. If set but
// empty, no origins will be allowed. Enforced only on local
// (plaintext) endpoint.
Origins []string `json:"origins,omitempty"`
// Options pertaining to configuration management.
Config *ConfigSettings `json:"config,omitempty"`
// Options that establish this server's identity. Identity refers to
// credentials which can be used to uniquely identify and authenticate
// this server instance. This is required if remote administration is
// enabled (but does not require remote administration to be enabled).
// Default: no identity management.
Identity *IdentityConfig `json:"identity,omitempty"`
// Options pertaining to remote administration. By default, remote
// administration is disabled. If enabled, identity management must
// also be configured, as that is how the endpoint is secured.
// See the neighboring "identity" object.
//
// EXPERIMENTAL: This feature is subject to change.
Remote *RemoteAdmin `json:"remote,omitempty"`
// Holds onto the routers so that we can later provision them
// if they require provisioning.
routers []AdminRouter
}
// ConfigSettings configures the management of configuration.
type ConfigSettings struct {
// Whether to keep a copy of the active config on disk. Default is true.
// Note that "pulled" dynamic configs (using the neighboring "load" module)
// are not persisted; only configs that are pushed to Caddy get persisted.
Persist *bool `json:"persist,omitempty"`
// Loads a new configuration. This is helpful if your configs are
// managed elsewhere and you want Caddy to pull its config dynamically
// when it starts. The pulled config completely replaces the current
// one, just like any other config load. It is an error if a pulled
// config is configured to pull another config without a load_delay,
// as this creates a tight loop.
//
// EXPERIMENTAL: Subject to change.
LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"`
// The duration after which to load config. If set, config will be pulled
// from the config loader after this duration. A delay is required if a
// dynamically-loaded config is configured to load yet another config. To
// load configs on a regular interval, ensure this value is set the same
// on all loaded configs; it can also be variable if needed, and to stop
// the loop, simply remove dynamic config loading from the next-loaded
// config.
//
// EXPERIMENTAL: Subject to change.
LoadDelay Duration `json:"load_delay,omitempty"`
}
// IdentityConfig configures management of this server's identity. An identity
// consists of credentials that uniquely verify this instance; for example,
// TLS certificates (public + private key pairs).
type IdentityConfig struct {
// List of names or IP addresses which refer to this server.
// Certificates will be obtained for these identifiers so
// secure TLS connections can be made using them.
Identifiers []string `json:"identifiers,omitempty"`
// Issuers that can provide this admin endpoint its identity
// certificate(s). Default: ACME issuers configured for
// ZeroSSL and Let's Encrypt. Be sure to change this if you
// require credentials for private identifiers.
IssuersRaw []json.RawMessage `json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module"`
issuers []certmagic.Issuer
}
// RemoteAdmin enables and configures remote administration. If enabled,
// a secure listener enforcing mutual TLS authentication will be started
// on a different port from the standard plaintext admin server.
//
// This endpoint is secured using identity management, which must be
// configured separately (because identity management does not depend
// on remote administration). See the admin/identity config struct.
//
// EXPERIMENTAL: Subject to change.
type RemoteAdmin struct {
// The address on which to start the secure listener. Accepts placeholders.
// Default: :2021
Listen string `json:"listen,omitempty"`
// List of access controls for this secure admin endpoint.
// This configures TLS mutual authentication (i.e. authorized
// client certificates), but also application-layer permissions
// like which paths and methods each identity is authorized for.
AccessControl []*AdminAccess `json:"access_control,omitempty"`
}
// AdminAccess specifies what permissions an identity or group
// of identities are granted.
type AdminAccess struct {
// Base64-encoded DER certificates containing public keys to accept.
// (The contents of PEM certificate blocks are base64-encoded DER.)
// Any of these public keys can appear in any part of a verified chain.
PublicKeys []string `json:"public_keys,omitempty"`
// Limits what the associated identities are allowed to do.
// If unspecified, all permissions are granted.
Permissions []AdminPermissions `json:"permissions,omitempty"`
publicKeys []crypto.PublicKey
}
// AdminPermissions specifies what kinds of requests are allowed
// to be made to the admin endpoint.
type AdminPermissions struct {
// The API paths allowed. Paths are simple prefix matches.
// Any subpath of the specified paths will be allowed.
Paths []string `json:"paths,omitempty"`
// The HTTP methods allowed for the given paths.
Methods []string `json:"methods,omitempty"`
}
// newAdminHandler reads admin's config and returns an http.Handler suitable
// for use in an admin endpoint server, which will be listening on listenAddr.
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Context) adminHandler {
muxWrap := adminHandler{mux: http.NewServeMux()}
// secure the local or remote endpoint respectively
if remote {
muxWrap.remoteControl = admin.Remote
} else {
// see comment in allowedOrigins() as to why we disable the host check for unix/fd networks
muxWrap.enforceHost = !addr.isWildcardInterface() && !addr.IsUnixNetwork() && !addr.IsFdNetwork()
muxWrap.allowedOrigins = admin.allowedOrigins(addr)
muxWrap.enforceOrigin = admin.EnforceOrigin
}
addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
labels := prometheus.Labels{"path": pattern, "handler": handlerLabel}
h = instrumentHandlerCounter(
adminMetrics.requestCount.MustCurryWith(labels),
h,
)
muxWrap.mux.Handle(pattern, h)
}
// addRoute just calls muxWrap.mux.Handle after
// wrapping the handler with error handling
addRoute := func(pattern string, handlerLabel string, h AdminHandler) {
wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := h.ServeHTTP(w, r)
if err != nil {
labels := prometheus.Labels{
"path": pattern,
"handler": handlerLabel,
"method": strings.ToUpper(r.Method),
}
adminMetrics.requestErrors.With(labels).Inc()
}
muxWrap.handleError(w, r, err)
})
addRouteWithMetrics(pattern, handlerLabel, wrapper)
}
const handlerLabel = "admin"
// register standard config control endpoints
addRoute("/"+rawConfigKey+"/", handlerLabel, AdminHandlerFunc(handleConfig))
addRoute("/id/", handlerLabel, AdminHandlerFunc(handleConfigID))
addRoute("/stop", handlerLabel, AdminHandlerFunc(handleStop))
// register debugging endpoints
addRouteWithMetrics("/debug/pprof/", handlerLabel, http.HandlerFunc(pprof.Index))
addRouteWithMetrics("/debug/pprof/cmdline", handlerLabel, http.HandlerFunc(pprof.Cmdline))
addRouteWithMetrics("/debug/pprof/profile", handlerLabel, http.HandlerFunc(pprof.Profile))
addRouteWithMetrics("/debug/pprof/symbol", handlerLabel, http.HandlerFunc(pprof.Symbol))
addRouteWithMetrics("/debug/pprof/trace", handlerLabel, http.HandlerFunc(pprof.Trace))
addRouteWithMetrics("/debug/vars", handlerLabel, expvar.Handler())
// register third-party module endpoints
for _, m := range GetModules("admin.api") {
router := m.New().(AdminRouter)
for _, route := range router.Routes() {
addRoute(route.Pattern, handlerLabel, route.Handler)
}
admin.routers = append(admin.routers, router)
}
return muxWrap
}
// provisionAdminRouters provisions all the router modules
// in the admin.api namespace that need provisioning.
func (admin *AdminConfig) provisionAdminRouters(ctx Context) error {
for _, router := range admin.routers {
provisioner, ok := router.(Provisioner)
if !ok {
continue
}
err := provisioner.Provision(ctx)
if err != nil {
return err
}
}
// We no longer need the routers once provisioned, allow for GC
admin.routers = nil
return nil
}
// allowedOrigins returns a list of origins that are allowed.
// If admin.Origins is nil (null), the provided listen address
// will be used as the default origin. If admin.Origins is
// empty, no origins will be allowed, effectively bricking the
// endpoint for non-unix-socket endpoints, but whatever.
func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
uniqueOrigins := make(map[string]struct{})
for _, o := range admin.Origins {
uniqueOrigins[o] = struct{}{}
}
// RFC 2616, Section 14.26:
// "A client MUST include a Host header field in all HTTP/1.1 request
// messages. If the requested URI does not include an Internet host
// name for the service being requested, then the Host header field MUST
// be given with an empty value."
//
// UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6.
// Understandable, but frustrating. See:
// https://github.com/golang/go/issues/60374
// See also the discussion here:
// https://github.com/golang/go/issues/61431
//
// We can no longer conform to RFC 2616 Section 14.26 from either Go or curl
// in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a
// bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin
// security checks, the infosec community assures me that it is secure to do
// so, because:
//
// 1) Browsers do not allow access to unix sockets
// 2) DNS is irrelevant to unix sockets
//
// If either of those two statements ever fail to hold true, it is not the
// fault of Caddy.
//
// Thus, we do not fill out allowed origins and do not enforce Host
// requirements for unix sockets. Enforcing it leads to confusion and
// frustration, when UDS have their own permissions from the OS.
// Enforcing host requirements here is effectively security theater,
// and a false sense of security.
//
// See also the discussion in #6832.
if admin.Origins == nil && !addr.IsUnixNetwork() && !addr.IsFdNetwork() {
if addr.isLoopback() {
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{}
} else {
uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
}
}
allowed := make([]*url.URL, 0, len(uniqueOrigins))
for originStr := range uniqueOrigins {
var origin *url.URL
if strings.Contains(originStr, "://") {
var err error
origin, err = url.Parse(originStr)
if err != nil {
continue
}
origin.Path = ""
origin.RawPath = ""
origin.Fragment = ""
origin.RawFragment = ""
origin.RawQuery = ""
} else {
origin = &url.URL{Host: originStr}
}
allowed = append(allowed, origin)
}
return allowed
}
// replaceLocalAdminServer replaces the running local admin server
// according to the relevant configuration in cfg. If no configuration
// for the admin endpoint exists in cfg, a default one is used, so
// that there is always an admin server (unless it is explicitly
// configured to be disabled).
// Critically note that some elements and functionality of the context
// may not be ready, e.g. storage. Tread carefully.
func replaceLocalAdminServer(cfg *Config, ctx Context) error {
// always* be sure to close down the old admin endpoint
// 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 {
go func(oldAdminServer *http.Server) {
err := stopAdminServer(oldAdminServer)
if err != nil {
Log().Named("admin").Error("stopping current admin endpoint", zap.Error(err))
}
}(oldAdminServer)
}
}()
// set a default if admin wasn't otherwise configured
if cfg.Admin == nil {
cfg.Admin = &AdminConfig{
Listen: DefaultAdminListen,
}
}
// if new admin endpoint is to be disabled, we're done
if cfg.Admin.Disabled {
Log().Named("admin").Warn("admin endpoint disabled")
return nil
}
// extract a singular listener address
addr, err := parseAdminListenAddr(cfg.Admin.Listen, DefaultAdminListen)
if err != nil {
return err
}
handler := cfg.Admin.newAdminHandler(addr, false, ctx)
// run the provisioners for loaded modules to make sure local
// state is properly re-initialized in the new admin server
err = cfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return err
}
ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{})
if err != nil {
return err
}
serverMu.Lock()
localAdminServer = &http.Server{
Addr: addr.String(), // for logging purposes only
Handler: handler,
ReadTimeout: 10 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1024 * 64,
}
serverMu.Unlock()
adminLogger := Log().Named("admin")
go func() {
serverMu.Lock()
server := localAdminServer
serverMu.Unlock()
if err := server.Serve(ln.(net.Listener)); !errors.Is(err, http.ErrServerClosed) {
adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
}
}()
adminLogger.Info("admin endpoint started",
zap.String("address", addr.String()),
zap.Bool("enforce_origin", cfg.Admin.EnforceOrigin),
zap.Array("origins", loggableURLArray(handler.allowedOrigins)))
if !handler.enforceHost {
adminLogger.Warn("admin endpoint on open interface; host checking disabled",
zap.String("address", addr.String()))
}
return nil
}
// manageIdentity sets up automated identity management for this server.
func manageIdentity(ctx Context, cfg *Config) error {
if cfg == nil || cfg.Admin == nil || cfg.Admin.Identity == nil {
return nil
}
// set default issuers; this is pretty hacky because we can't
// import the caddytls package -- but it works
if cfg.Admin.Identity.IssuersRaw == nil {
cfg.Admin.Identity.IssuersRaw = []json.RawMessage{
json.RawMessage(`{"module": "acme"}`),
}
}
// load and provision issuer modules
if cfg.Admin.Identity.IssuersRaw != nil {
val, err := ctx.LoadModule(cfg.Admin.Identity, "IssuersRaw")
if err != nil {
return fmt.Errorf("loading identity issuer modules: %s", err)
}
for _, issVal := range val.([]any) {
cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer))
}
}
// we'll make a new cache when we make the CertMagic config, so stop any previous cache
if identityCertCache != nil {
identityCertCache.Stop()
}
logger := Log().Named("admin.identity")
cmCfg := cfg.Admin.Identity.certmagicConfig(logger, true)
// issuers have circular dependencies with the configs because,
// as explained in the caddytls package, they need access to the
// correct storage and cache to solve ACME challenges
for _, issuer := range cfg.Admin.Identity.issuers {
// avoid import cycle with caddytls package, so manually duplicate the interface here, yuck
if annoying, ok := issuer.(interface{ SetConfig(cfg *certmagic.Config) }); ok {
annoying.SetConfig(cmCfg)
}
}
// obtain and renew server identity certificate(s)
return cmCfg.ManageAsync(ctx, cfg.Admin.Identity.Identifiers)
}
// replaceRemoteAdminServer replaces the running remote admin server
// according to the relevant configuration in cfg. It stops any previous
// remote admin server and only starts a new one if configured.
func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
if cfg == nil {
return nil
}
remoteLogger := Log().Named("admin.remote")
oldAdminServer := remoteAdminServer
defer func() {
if oldAdminServer != nil {
go func(oldAdminServer *http.Server) {
err := stopAdminServer(oldAdminServer)
if err != nil {
Log().Named("admin").Error("stopping current secure admin endpoint", zap.Error(err))
}
}(oldAdminServer)
}
}()
if cfg.Admin == nil || cfg.Admin.Remote == nil {
return nil
}
addr, err := parseAdminListenAddr(cfg.Admin.Remote.Listen, DefaultRemoteAdminListen)
if err != nil {
return err
}
// make the HTTP handler but disable Host/Origin enforcement
// because we are using TLS authentication instead
handler := cfg.Admin.newAdminHandler(addr, true, ctx)
// run the provisioners for loaded modules to make sure local
// state is properly re-initialized in the new admin server
err = cfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return err
}
// create client certificate pool for TLS mutual auth, and extract public keys
// so that we can enforce access controls at the application layer
clientCertPool := x509.NewCertPool()
for i, accessControl := range cfg.Admin.Remote.AccessControl {
for j, certBase64 := range accessControl.PublicKeys {
cert, err := decodeBase64DERCert(certBase64)
if err != nil {
return fmt.Errorf("access control %d public key %d: parsing base64 certificate DER: %v", i, j, err)
}
accessControl.publicKeys = append(accessControl.publicKeys, cert.PublicKey)
clientCertPool.AddCert(cert)
}
}
// create TLS config that will enforce mutual authentication
if identityCertCache == nil {
return fmt.Errorf("cannot enable remote admin without a certificate cache; configure identity management to initialize a certificate cache")
}
cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger, false)
tlsConfig := cmCfg.TLSConfig()
tlsConfig.NextProtos = nil // this server does not solve ACME challenges
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
tlsConfig.ClientCAs = clientCertPool
// convert logger to stdlib so it can be used by HTTP server
serverLogger, err := zap.NewStdLogAt(remoteLogger, zap.DebugLevel)
if err != nil {
return err
}
serverMu.Lock()
// create secure HTTP server
remoteAdminServer = &http.Server{
Addr: addr.String(), // for logging purposes only
Handler: handler,
TLSConfig: tlsConfig,
ReadTimeout: 10 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1024 * 64,
ErrorLog: serverLogger,
}
serverMu.Unlock()
// start listener
lnAny, err := addr.Listen(ctx, 0, net.ListenConfig{})
if err != nil {
return err
}
ln := lnAny.(net.Listener)
ln = tls.NewListener(ln, tlsConfig)
go func() {
serverMu.Lock()
server := remoteAdminServer
serverMu.Unlock()
if err := server.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
remoteLogger.Error("admin remote server shutdown for unknown reason", zap.Error(err))
}
}()
remoteLogger.Info("secure admin remote control endpoint started",
zap.String("address", addr.String()))
return nil
}
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)
}
// Choose storage: prefer the package-level test override when present,
// otherwise use the configured DefaultStorage. Tests may set an override
// to divert storage into a temporary location. Otherwise, in production
// we use the DefaultStorage since we don't want to act as part of a
// cluster; this storage is for the server's local identity only.
var storage certmagic.Storage
if testCertMagicStorageOverride != nil {
storage = testCertMagicStorageOverride
} else {
storage = DefaultStorage
}
template := certmagic.Config{
Storage: storage,
Logger: logger,
Issuers: ident.issuers,
}
if makeCache {
identityCertCache = certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
return cmCfg, nil
},
Logger: logger.Named("cache"),
})
}
cmCfg = certmagic.New(identityCertCache, template)
return cmCfg
}
// IdentityCredentials returns this instance's configured, managed identity credentials
// that can be used in TLS client authentication.
func (ctx Context) IdentityCredentials(logger *zap.Logger) ([]tls.Certificate, error) {
if ctx.cfg == nil || ctx.cfg.Admin == nil || ctx.cfg.Admin.Identity == nil {
return nil, fmt.Errorf("no server identity configured")
}
ident := ctx.cfg.Admin.Identity
if len(ident.Identifiers) == 0 {
return nil, fmt.Errorf("no identifiers configured")
}
if logger == nil {
logger = Log()
}
magic := ident.certmagicConfig(logger, false)
return magic.ClientCredentials(ctx, ident.Identifiers)
}
// enforceAccessControls enforces application-layer access controls for r based on remote.
// It expects that the TLS server has already established at least one verified chain of
// trust, and then looks for a matching, authorized public key that is allowed to access
// the defined path(s) using the defined method(s).
func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
for _, chain := range r.TLS.VerifiedChains {
for _, peerCert := range chain {
for _, adminAccess := range remote.AccessControl {
for _, allowedKey := range adminAccess.publicKeys {
// see if we found a matching public key; the TLS server already verified the chain
// so we know the client possesses the associated private key; this handy interface
// doesn't appear to be defined anywhere in the std lib, but was implemented here:
// https://github.com/golang/go/commit/b5f2c0f50297fa5cd14af668ddd7fd923626cf8c
comparer, ok := peerCert.PublicKey.(interface{ Equal(crypto.PublicKey) bool })
if !ok || !comparer.Equal(allowedKey) {
continue
}
// key recognized; make sure its HTTP request is permitted
for _, accessPerm := range adminAccess.Permissions {
// verify method
methodFound := accessPerm.Methods == nil || slices.Contains(accessPerm.Methods, r.Method)
if !methodFound {
return APIError{
HTTPStatus: http.StatusForbidden,
Message: "not authorized to use this method",
}
}
// verify path
pathFound := accessPerm.Paths == nil
for _, allowedPath := range accessPerm.Paths {
if strings.HasPrefix(r.URL.Path, allowedPath) {
pathFound = true
break
}
}
if !pathFound {
return APIError{
HTTPStatus: http.StatusForbidden,
Message: "not authorized to access this path",
}
}
}
// public key authorized, method and path allowed
return nil
}
}
}
}
// in theory, this should never happen; with an unverified chain, the TLS server
// should not accept the connection in the first place, and the acceptable cert
// pool is configured using the same list of public keys we verify against
return APIError{
HTTPStatus: http.StatusUnauthorized,
Message: "client identity not authorized",
}
}
func stopAdminServer(srv *http.Server) error {
if srv == nil {
return fmt.Errorf("no admin server")
}
timeout := 10 * time.Second
ctx, cancel := context.WithTimeoutCause(context.Background(), timeout, fmt.Errorf("stopping admin server: %ds timeout", int(timeout.Seconds())))
defer cancel()
err := srv.Shutdown(ctx)
if err != nil {
if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) {
err = cause
}
return fmt.Errorf("shutting down admin server: %v", err)
}
Log().Named("admin").Info("stopped previous server", zap.String("address", srv.Addr))
return nil
}
// AdminRouter is a type which can return routes for the admin API.
type AdminRouter interface {
Routes() []AdminRoute
}
// AdminRoute represents a route for the admin endpoint.
type AdminRoute struct {
Pattern string
Handler AdminHandler
}
type adminHandler struct {
mux *http.ServeMux
// security for local/plaintext endpoint
enforceOrigin bool
enforceHost bool
allowedOrigins []*url.URL
// security for remote/encrypted endpoint
remoteControl *RemoteAdmin
}
// ServeHTTP is the external entry point for API requests.
// It will only be called once per request.
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ip, port, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
ip = r.RemoteAddr
port = ""
}
log := Log().Named("admin.api").With(
zap.String("method", r.Method),
zap.String("host", r.Host),
zap.String("uri", r.RequestURI),
zap.String("remote_ip", ip),
zap.String("remote_port", port),
zap.Reflect("headers", r.Header),
)
if r.TLS != nil {
log = log.With(
zap.Bool("secure", true),
zap.Int("verified_chains", len(r.TLS.VerifiedChains)),
)
}
if r.RequestURI == "/metrics" {
log.Debug("received request")
} else {
log.Info("received request")
}
h.serveHTTP(w, r)
}
// serveHTTP is the internal entry point for API requests. It may
// be called more than once per request, for example if a request
// is rewritten (i.e. internal redirect).
func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
if h.remoteControl != nil {
// enforce access controls on secure endpoint
if err := h.remoteControl.enforceAccessControls(r); err != nil {
h.handleError(w, r, err)
return
}
}
// common mitigations in browser contexts
if strings.Contains(r.Header.Get("Upgrade"), "websocket") {
// I've never been able demonstrate a vulnerability myself, but apparently
// WebSocket connections originating from browsers aren't subject to CORS
// restrictions, so we'll just be on the safe side
h.handleError(w, r, APIError{
HTTPStatus: http.StatusBadRequest,
Err: errors.New("websocket connections aren't allowed"),
Message: "WebSocket connections aren't allowed.",
})
return
}
if strings.Contains(r.Header.Get("Sec-Fetch-Mode"), "no-cors") {
// turns out web pages can just disable the same-origin policy (!???!?)
// but at least browsers let us know that's the case, holy heck
h.handleError(w, r, APIError{
HTTPStatus: http.StatusBadRequest,
Err: errors.New("client attempted to make request by disabling same-origin policy using no-cors mode"),
Message: "Disabling same-origin restrictions is not allowed.",
})
return
}
if r.Header.Get("Origin") == "null" {
// bug in Firefox in certain cross-origin situations (yikes?)
// (not strictly a security vuln on its own, but it's red flaggy,
// since it seems to manifest in cross-origin contexts)
h.handleError(w, r, APIError{
HTTPStatus: http.StatusBadRequest,
Err: errors.New("invalid origin 'null'"),
Message: "Buggy browser is sending null Origin header.",
})
}
if h.enforceHost {
// DNS rebinding mitigation
err := h.checkHost(r)
if err != nil {
h.handleError(w, r, err)
return
}
}
_, hasOriginHeader := r.Header["Origin"]
_, hasSecHeader := r.Header["Sec-Fetch-Mode"]
if h.enforceOrigin || hasOriginHeader || hasSecHeader {
// cross-site mitigation
origin, err := h.checkOrigin(r)
if err != nil {
h.handleError(w, r, err)
return
}
if r.Method == http.MethodOptions {
w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Cache-Control")
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
w.Header().Set("Access-Control-Allow-Origin", origin)
}
h.mux.ServeHTTP(w, r)
}
func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err error) {
if err == nil {
return
}
if err == errInternalRedir {
h.serveHTTP(w, r)
return
}
apiErr, ok := err.(APIError)
if !ok {
apiErr = APIError{
HTTPStatus: http.StatusInternalServerError,
Err: err,
}
}
if apiErr.HTTPStatus == 0 {
apiErr.HTTPStatus = http.StatusInternalServerError
}
if apiErr.Message == "" && apiErr.Err != nil {
apiErr.Message = apiErr.Err.Error()
}
Log().Named("admin.api").Error("request error",
zap.Error(err),
zap.Int("status_code", apiErr.HTTPStatus),
)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(apiErr.HTTPStatus)
encErr := json.NewEncoder(w).Encode(apiErr)
if encErr != nil {
Log().Named("admin.api").Error("failed to encode error response", zap.Error(encErr))
}
}
// checkHost returns a handler that wraps next such that
// it will only be called if the request's Host header matches
// a trustworthy/expected value. This helps to mitigate DNS
// rebinding attacks.
func (h adminHandler) checkHost(r *http.Request) error {
allowed := slices.ContainsFunc(h.allowedOrigins, func(u *url.URL) bool {
return r.Host == u.Host
})
if !allowed {
return APIError{
HTTPStatus: http.StatusForbidden,
Err: fmt.Errorf("host not allowed: %s", r.Host),
}
}
return nil
}
// checkOrigin ensures that the Origin header, if
// set, matches the intended target; prevents arbitrary
// sites from issuing requests to our listener. It
// returns the origin that was obtained from r.
func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
originStr, origin := h.getOrigin(r)
if origin == nil {
return "", APIError{
HTTPStatus: http.StatusForbidden,
Err: fmt.Errorf("required Origin header is missing or invalid"),
}
}
if !h.originAllowed(origin) {
return "", APIError{
HTTPStatus: http.StatusForbidden,
Err: fmt.Errorf("client is not allowed to access from origin '%s'", originStr),
}
}
return origin.String(), nil
}
func (h adminHandler) getOrigin(r *http.Request) (string, *url.URL) {
origin := r.Header.Get("Origin")
if origin == "" {
origin = r.Header.Get("Referer")
}
originURL, err := url.Parse(origin)
if err != nil {
return origin, nil
}
originURL.Path = ""
originURL.RawPath = ""
originURL.Fragment = ""
originURL.RawFragment = ""
originURL.RawQuery = ""
return origin, originURL
}
func (h adminHandler) originAllowed(origin *url.URL) bool {
for _, allowedOrigin := range h.allowedOrigins {
if allowedOrigin.Scheme != "" && origin.Scheme != allowedOrigin.Scheme {
continue
}
if origin.Host == allowedOrigin.Host {
return true
}
}
return false
}
// etagHasher returns the hasher we used on the config to both
// produce and verify ETags.
func etagHasher() hash.Hash { return xxhash.New() }
// 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))
}
// This buffer pool is used to keep buffers for
// reading the config file during eTag header generation
var bufferPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func handleConfig(w http.ResponseWriter, r *http.Request) error {
switch r.Method {
case http.MethodGet:
w.Header().Set("Content-Type", "application/json")
hash := etagHasher()
// Read the config into a buffer instead of writing directly to
// the response writer, as we want to set the ETag as the header,
// not the trailer.
buf := bufferPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufferPool.Put(buf)
configWriter := io.MultiWriter(buf, hash)
err := readConfig(r.URL.Path, configWriter)
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))
_, err = w.Write(buf.Bytes())
if err != nil {
return APIError{HTTPStatus: http.StatusInternalServerError, Err: err}
}
return nil
case http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete:
// DELETE does not use a body, but the others do
var body []byte
if r.Method != http.MethodDelete {
if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
}
}
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
_, err := io.Copy(buf, r.Body)
if err != nil {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("reading request body: %v", err),
}
}
body = buf.Bytes()
}
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
err := changeConfig(r.Method, r.URL.Path, body, r.Header.Get("If-Match"), forceReload)
if err != nil && !errors.Is(err, errSameConfig) {
return err
}
// If this request changed the config, clear the last
// config info we have stored, if it is different from
// the original source.
ClearLastConfigIfDifferent(
r.Header.Get("Caddy-Config-Source-File"),
r.Header.Get("Caddy-Config-Source-Adapter"))
default:
return APIError{
HTTPStatus: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method %s not allowed", r.Method),
}
}
return nil
}
func handleConfigID(w http.ResponseWriter, r *http.Request) error {
idPath := r.URL.Path
parts := strings.Split(idPath, "/")
if len(parts) < 3 || parts[2] == "" {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("request path is missing object ID"),
}
}
if parts[0] != "" || parts[1] != "id" {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed object path"),
}
}
id := parts[2]
// map the ID to the expanded path
rawCfgMu.RLock()
expanded, ok := rawCfgIndex[id]
rawCfgMu.RUnlock()
if !ok {
return APIError{
HTTPStatus: http.StatusNotFound,
Err: fmt.Errorf("unknown object ID '%s'", id),
}
}
// piece the full URL path back together
parts = append([]string{expanded}, parts[3:]...)
r.URL.Path = path.Join(parts...)
return errInternalRedir
}
func handleStop(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodPost {
return APIError{
HTTPStatus: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed"),
}
}
exitProcess(context.Background(), Log().Named("admin.api"))
return nil
}
// unsyncedConfigAccess traverses into the current config and performs
// 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
// 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
// if there is a request body, decode it into the
// variable that will be set in the config according
// to method and path
if len(body) > 0 {
err = json.Unmarshal(body, &val)
if err != nil {
if jsonErr, ok := err.(*json.SyntaxError); ok {
return fmt.Errorf("decoding request body: %w, at offset %d", jsonErr, jsonErr.Offset)
}
return fmt.Errorf("decoding request body: %w", err)
}
}
enc := json.NewEncoder(out)
cleanPath := strings.Trim(path, "/")
if cleanPath == "" {
return fmt.Errorf("no traversable path")
}
parts := strings.Split(cleanPath, "/")
if len(parts) == 0 {
return fmt.Errorf("path missing")
}
// A path that ends with "..." implies:
// 1) the part before it is an array
// 2) the payload is an array
// and means that the user wants to expand the elements
// in the payload array and append each one into the
// destination array, like so:
// array = append(array, elems...)
// This special case is handled below.
ellipses := parts[len(parts)-1] == "..."
if ellipses {
parts = parts[:len(parts)-1]
}
var ptr any = rawCfg
traverseLoop:
for i, part := range parts {
switch v := ptr.(type) {
case map[string]any:
// 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 {
var idx int
if method != http.MethodPost {
idxStr := parts[len(parts)-1]
idx, err = strconv.Atoi(idxStr)
if err != nil {
return fmt.Errorf("[%s] invalid array index '%s': %v",
path, idxStr, err)
}
if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) {
return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
}
}
switch method {
case http.MethodGet:
err = enc.Encode(arr[idx])
if err != nil {
return fmt.Errorf("encoding config: %v", err)
}
case http.MethodPost:
if ellipses {
valArray, ok := val.([]any)
if !ok {
return fmt.Errorf("final element is not an array")
}
v[part] = append(arr, valArray...)
} else {
v[part] = append(arr, val)
}
case http.MethodPut:
// avoid creation of new slice and a second copy (see
// https://github.com/golang/go/wiki/SliceTricks#insert)
arr = append(arr, nil)
copy(arr[idx+1:], arr[idx:])
arr[idx] = val
v[part] = arr
case http.MethodPatch:
arr[idx] = val
case http.MethodDelete:
v[part] = append(arr[:idx], arr[idx+1:]...)
default:
return fmt.Errorf("unrecognized method %s", method)
}
break traverseLoop
}
if i == len(parts)-1 {
switch method {
case http.MethodGet:
err = enc.Encode(v[part])
if err != nil {
return fmt.Errorf("encoding config: %v", err)
}
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 ellipses {
valArray, ok := val.([]any)
if !ok {
return fmt.Errorf("final element is not an array")
}
v[part] = append(arr, valArray...)
} else {
v[part] = append(arr, val)
}
} else {
v[part] = val
}
case http.MethodPut:
if _, ok := v[part]; ok {
return APIError{
HTTPStatus: http.StatusConflict,
Err: fmt.Errorf("[%s] key already exists: %s", path, part),
}
}
v[part] = val
case http.MethodPatch:
if _, ok := v[part]; !ok {
return APIError{
HTTPStatus: http.StatusNotFound,
Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
}
}
v[part] = val
case http.MethodDelete:
if _, ok := v[part]; !ok {
return APIError{
HTTPStatus: http.StatusNotFound,
Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
}
}
delete(v, part)
default:
return fmt.Errorf("unrecognized method %s", method)
}
} else {
// if we are "PUTting" a new resource, the key(s) in its path
// 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)
}
ptr = v[part]
}
case []any:
partInt, err := strconv.Atoi(part)
if err != nil {
return fmt.Errorf("[/%s] invalid array index '%s': %v",
strings.Join(parts[:i+1], "/"), part, err)
}
if partInt < 0 || partInt >= len(v) {
return fmt.Errorf("[/%s] array index out of bounds: %s",
strings.Join(parts[:i+1], "/"), part)
}
ptr = v[partInt]
default:
return fmt.Errorf("invalid traversal path at: %s", strings.Join(parts[:i+1], "/"))
}
}
return nil
}
// 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
// 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 {
return idRegexp.ReplaceAllFunc(rawJSON, func(in []byte) []byte {
// matches with a comma on both sides (when "@id" property is
// not the first or last in the object) need to keep exactly
// one comma for correct JSON syntax
comma := []byte{','}
if bytes.HasPrefix(in, comma) && bytes.HasSuffix(in, comma) {
return comma
}
return []byte{}
})
}
// AdminHandler is like http.Handler except ServeHTTP may return an error.
//
// If any handler encounters an error, it should be returned for proper
// handling.
type AdminHandler interface {
ServeHTTP(http.ResponseWriter, *http.Request) error
}
// AdminHandlerFunc is a convenience type like http.HandlerFunc.
type AdminHandlerFunc func(http.ResponseWriter, *http.Request) error
// ServeHTTP implements the Handler interface.
func (f AdminHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
return f(w, r)
}
// APIError is a structured error that every API
// handler should return for consistency in logging
// and client responses. If Message is unset, then
// Err.Error() will be serialized in its place.
type APIError struct {
HTTPStatus int `json:"-"`
Err error `json:"-"`
Message string `json:"error"`
}
func (e APIError) Error() string {
if e.Err != nil {
return e.Err.Error()
}
return e.Message
}
// 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)
}
if input == "" {
input = defaultAddr
}
listenAddr, err := ParseNetworkAddress(input)
if err != nil {
return NetworkAddress{}, fmt.Errorf("parsing listener address: %v", err)
}
if listenAddr.PortRangeSize() != 1 {
return NetworkAddress{}, fmt.Errorf("must be exactly one listener address; cannot listen on: %s", listenAddr)
}
return listenAddr, nil
}
// decodeBase64DERCert base64-decodes, then DER-decodes, certStr.
func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
derBytes, err := base64.StdEncoding.DecodeString(certStr)
if err != nil {
return nil, err
}
return x509.ParseCertificate(derBytes)
}
type loggableURLArray []*url.URL
func (ua loggableURLArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
if ua == nil {
return nil
}
for _, u := range ua {
enc.AppendString(u.String())
}
return nil
}
var (
// DefaultAdminListen is the address for the local admin
// listener, if none is specified at startup.
DefaultAdminListen = "localhost:2019"
// DefaultRemoteAdminListen is the address for the remote
// (TLS-authenticated) admin listener, if enabled and not
// specified otherwise.
DefaultRemoteAdminListen = ":2021"
)
// PIDFile writes a pidfile to the file at filename. It
// will get deleted before the process gracefully exits.
func PIDFile(filename string) error {
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
err := os.WriteFile(filename, pid, 0o600)
if err != nil {
return err
}
pidfile = filename
return nil
}
// idRegexp is used to match ID fields and their associated values
// in the config. It also matches adjacent commas so that syntax
// can be preserved no matter where in the object the field appears.
// It supports string and most numeric values.
var idRegexp = regexp.MustCompile(`(?m),?\s*"` + idKey + `"\s*:\s*(-?[0-9]+(\.[0-9]+)?|(?U)".*")\s*,?`)
// pidfile is the name of the pidfile, if any.
var pidfile string
// errInternalRedir indicates an internal redirect
// and is useful when admin API handlers rewrite
// the request; in that case, authentication and
// authorization needs to happen again for the
// rewritten request.
var errInternalRedir = fmt.Errorf("internal redirect; re-authorization required")
const (
rawConfigKey = "config"
idKey = "@id"
)
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
// keep a reference to admin endpoint singletons while they're active
var (
serverMu sync.Mutex
localAdminServer, remoteAdminServer *http.Server
identityCertCache *certmagic.Cache
)
caddy-2.11.2/admin_test.go 0000664 0000000 0000000 00000060020 15173660016 0015342 0 ustar 00root root 0000000 0000000 // 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 caddy
import (
"context"
"crypto/x509"
"encoding/json"
"fmt"
"maps"
"net/http"
"net/http/httptest"
"os"
"reflect"
"sync"
"testing"
"time"
"github.com/caddyserver/certmagic"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
var testCfg = []byte(`{
"apps": {
"http": {
"servers": {
"myserver": {
"listen": ["tcp/localhost:8080-8084"],
"read_timeout": "30s"
},
"yourserver": {
"listen": ["127.0.0.1:5000"],
"read_header_timeout": "15s"
}
}
}
}
}
`)
func TestUnsyncedConfigAccess(t *testing.T) {
// each test is performed in sequence, so
// each change builds on the previous ones;
// the config is not reset between tests
for i, tc := range []struct {
method string
path string // rawConfigKey will be prepended
payload string
expect string // JSON representation of what the whole config is expected to be after the request
shouldErr bool
}{
{
method: "POST",
path: "",
payload: `{"foo": "bar", "list": ["a", "b", "c"]}`, // starting value
expect: `{"foo": "bar", "list": ["a", "b", "c"]}`,
},
{
method: "POST",
path: "/foo",
payload: `"jet"`,
expect: `{"foo": "jet", "list": ["a", "b", "c"]}`,
},
{
method: "POST",
path: "/bar",
payload: `{"aa": "bb", "qq": "zz"}`,
expect: `{"foo": "jet", "bar": {"aa": "bb", "qq": "zz"}, "list": ["a", "b", "c"]}`,
},
{
method: "DELETE",
path: "/bar/qq",
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
},
{
method: "DELETE",
path: "/bar/qq",
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
shouldErr: true,
},
{
method: "POST",
path: "/list",
payload: `"e"`,
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
},
{
method: "PUT",
path: "/list/3",
payload: `"d"`,
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e"]}`,
},
{
method: "DELETE",
path: "/list/3",
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
},
{
method: "PATCH",
path: "/list/3",
payload: `"d"`,
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d"]}`,
},
{
method: "POST",
path: "/list/...",
payload: `["e", "f", "g"]`,
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e", "f", "g"]}`,
},
} {
err := unsyncedConfigAccess(tc.method, rawConfigKey+tc.path, []byte(tc.payload), nil)
if tc.shouldErr && err == nil {
t.Fatalf("Test %d: Expected error return value, but got: %v", i, err)
}
if !tc.shouldErr && err != nil {
t.Fatalf("Test %d: Should not have had error return value, but got: %v", i, err)
}
// decode the expected config so we can do a convenient DeepEqual
var expectedDecoded any
err = json.Unmarshal([]byte(tc.expect), &expectedDecoded)
if err != nil {
t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err)
}
// make sure the resulting config is as we expect it
if !reflect.DeepEqual(rawCfg[rawConfigKey], expectedDecoded) {
t.Fatalf("Test %d:\nExpected:\n\t%#v\nActual:\n\t%#v",
i, expectedDecoded, rawCfg[rawConfigKey])
}
}
}
// TestLoadConcurrent exercises Load under concurrent conditions
// and is most useful under test with `-race` enabled.
func TestLoadConcurrent(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Go(func() {
_ = Load(testCfg, true)
})
}
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 b.Loop() {
Load(testCfg, true)
}
}
func TestAdminHandlerErrorHandling(t *testing.T) {
initAdminMetrics()
handler := adminHandler{
mux: http.NewServeMux(),
}
handler.mux.Handle("/error", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := fmt.Errorf("test error")
handler.handleError(w, r, err)
}))
req := httptest.NewRequest(http.MethodGet, "/error", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code == http.StatusOK {
t.Error("expected error response, got success")
}
var apiErr APIError
if err := json.NewDecoder(rr.Body).Decode(&apiErr); err != nil {
t.Fatalf("decoding response: %v", err)
}
if apiErr.Message != "test error" {
t.Errorf("expected error message 'test error', got '%s'", apiErr.Message)
}
}
func initAdminMetrics() {
if adminMetrics.requestErrors != nil {
prometheus.Unregister(adminMetrics.requestErrors)
}
if adminMetrics.requestCount != nil {
prometheus.Unregister(adminMetrics.requestCount)
}
adminMetrics.requestErrors = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "caddy",
Subsystem: "admin_http",
Name: "request_errors_total",
Help: "Number of errors that occurred handling admin endpoint requests",
}, []string{"handler", "path", "method"})
adminMetrics.requestCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "caddy",
Subsystem: "admin_http",
Name: "requests_total",
Help: "Count of requests to the admin endpoint",
}, []string{"handler", "path", "code", "method"}) // Added code and method labels
prometheus.MustRegister(adminMetrics.requestErrors)
prometheus.MustRegister(adminMetrics.requestCount)
}
func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
initAdminMetrics()
cfg := &Config{
Admin: &AdminConfig{
Listen: "localhost:2019",
},
}
// Build the admin handler directly (no listener active)
addr, err := ParseNetworkAddress("localhost:2019")
if err != nil {
t.Fatalf("Failed to parse address: %v", err)
}
handler := cfg.Admin.newAdminHandler(addr, false, Context{})
tests := []struct {
name string
path string
method string
expectedStatus int
}{
{
name: "stop endpoint wrong method",
path: "/stop",
method: http.MethodGet,
expectedStatus: http.StatusMethodNotAllowed,
},
{
name: "config endpoint wrong content-type",
path: "/config/",
method: http.MethodPost,
expectedStatus: http.StatusBadRequest,
},
{
name: "config ID missing ID",
path: "/id/",
method: http.MethodGet,
expectedStatus: http.StatusBadRequest,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := httptest.NewRequest(test.method, fmt.Sprintf("http://localhost:2019%s", test.path), nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != test.expectedStatus {
t.Errorf("expected status %d but got %d", test.expectedStatus, rr.Code)
}
metricValue := testGetMetricValue(map[string]string{
"path": test.path,
"handler": "admin",
"method": test.method,
})
if metricValue != 1 {
t.Errorf("expected error metric to be incremented once, got %v", metricValue)
}
})
}
}
func testGetMetricValue(labels map[string]string) float64 {
promLabels := prometheus.Labels{}
maps.Copy(promLabels, labels)
metric, err := adminMetrics.requestErrors.GetMetricWith(promLabels)
if err != nil {
return 0
}
pb := &dto.Metric{}
metric.Write(pb)
return pb.GetCounter().GetValue()
}
type mockRouter struct {
routes []AdminRoute
}
func (m mockRouter) Routes() []AdminRoute {
return m.routes
}
type mockModule struct {
mockRouter
}
func (m *mockModule) CaddyModule() ModuleInfo {
return ModuleInfo{
ID: "admin.api.mock",
New: func() Module {
mm := &mockModule{
mockRouter: mockRouter{
routes: m.routes,
},
}
return mm
},
}
}
func TestNewAdminHandlerRouterRegistration(t *testing.T) {
originalModules := make(map[string]ModuleInfo)
maps.Copy(originalModules, modules)
defer func() {
modules = originalModules
}()
mockRoute := AdminRoute{
Pattern: "/mock",
Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
w.WriteHeader(http.StatusOK)
return nil
}),
}
mock := &mockModule{
mockRouter: mockRouter{
routes: []AdminRoute{mockRoute},
},
}
RegisterModule(mock)
addr, err := ParseNetworkAddress("localhost:2019")
if err != nil {
t.Fatalf("Failed to parse address: %v", err)
}
admin := &AdminConfig{
EnforceOrigin: false,
}
handler := admin.newAdminHandler(addr, false, Context{})
req := httptest.NewRequest("GET", "/mock", nil)
req.Host = "localhost:2019"
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
t.Logf("Response body: %s", rr.Body.String())
}
if len(admin.routers) != 1 {
t.Errorf("Expected 1 router to be stored, got %d", len(admin.routers))
}
}
type mockProvisionableRouter struct {
mockRouter
provisionErr error
provisioned bool
}
func (m *mockProvisionableRouter) Provision(Context) error {
m.provisioned = true
return m.provisionErr
}
type mockProvisionableModule struct {
*mockProvisionableRouter
}
func (m *mockProvisionableModule) CaddyModule() ModuleInfo {
return ModuleInfo{
ID: "admin.api.mock_provision",
New: func() Module {
mm := &mockProvisionableModule{
mockProvisionableRouter: &mockProvisionableRouter{
mockRouter: m.mockRouter,
provisionErr: m.provisionErr,
},
}
return mm
},
}
}
func TestAdminRouterProvisioning(t *testing.T) {
tests := []struct {
name string
provisionErr error
wantErr bool
routersAfter int // expected number of routers after provisioning
}{
{
name: "successful provisioning",
provisionErr: nil,
wantErr: false,
routersAfter: 0,
},
{
name: "provisioning error",
provisionErr: fmt.Errorf("provision failed"),
wantErr: true,
routersAfter: 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
originalModules := make(map[string]ModuleInfo)
maps.Copy(originalModules, modules)
defer func() {
modules = originalModules
}()
mockRoute := AdminRoute{
Pattern: "/mock",
Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}),
}
// Create provisionable module
mock := &mockProvisionableModule{
mockProvisionableRouter: &mockProvisionableRouter{
mockRouter: mockRouter{
routes: []AdminRoute{mockRoute},
},
provisionErr: test.provisionErr,
},
}
RegisterModule(mock)
admin := &AdminConfig{}
addr, err := ParseNetworkAddress("localhost:2019")
if err != nil {
t.Fatalf("Failed to parse address: %v", err)
}
_ = admin.newAdminHandler(addr, false, Context{})
err = admin.provisionAdminRouters(Context{})
if test.wantErr {
if err == nil {
t.Error("Expected error but got nil")
}
} else {
if err != nil {
t.Errorf("Expected no error but got: %v", err)
}
}
if len(admin.routers) != test.routersAfter {
t.Errorf("Expected %d routers after provisioning, got %d", test.routersAfter, len(admin.routers))
}
})
}
}
func TestAllowedOriginsUnixSocket(t *testing.T) {
// see comment in allowedOrigins() as to why we do not fill out allowed origins for UDS
tests := []struct {
name string
addr NetworkAddress
origins []string
expectOrigins []string
}{
{
name: "unix socket with default origins",
addr: NetworkAddress{
Network: "unix",
Host: "/tmp/caddy.sock",
},
origins: nil, // default origins
expectOrigins: []string{},
},
{
name: "unix socket with custom origins",
addr: NetworkAddress{
Network: "unix",
Host: "/tmp/caddy.sock",
},
origins: []string{"example.com"},
expectOrigins: []string{
"example.com",
},
},
{
name: "tcp socket on localhost gets all loopback addresses",
addr: NetworkAddress{
Network: "tcp",
Host: "localhost",
StartPort: 2019,
EndPort: 2019,
},
origins: nil,
expectOrigins: []string{
"localhost:2019",
"[::1]:2019",
"127.0.0.1:2019",
},
},
}
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
admin := AdminConfig{
Origins: test.origins,
}
got := admin.allowedOrigins(test.addr)
var gotOrigins []string
for _, u := range got {
gotOrigins = append(gotOrigins, u.Host)
}
if len(gotOrigins) != len(test.expectOrigins) {
t.Errorf("%d: Expected %d origins but got %d", i, len(test.expectOrigins), len(gotOrigins))
return
}
expectMap := make(map[string]struct{})
for _, origin := range test.expectOrigins {
expectMap[origin] = struct{}{}
}
gotMap := make(map[string]struct{})
for _, origin := range gotOrigins {
gotMap[origin] = struct{}{}
}
if !reflect.DeepEqual(expectMap, gotMap) {
t.Errorf("%d: Origins mismatch.\nExpected: %v\nGot: %v", i, test.expectOrigins, gotOrigins)
}
})
}
}
func TestReplaceRemoteAdminServer(t *testing.T) {
const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw
MTAwMDAwMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEA4O4S6BSoYcoxvRqI+h7yPOjF6KjntjzVVm9M+uHK4lzX
F1L3pSxJ2nDD4wZEV3FJ5yFOHVFqkG2vXG3BIczOlYG7UeNmKbQnKc5kZj3HGUrS
VGEktA4OJbeZhhWP15gcXN5eDM2eH3g9BFXVX6AURxLiUXzhNBUEZuj/OEyH9yEF
/qPCE+EjzVvWxvBXwgz/io4r4yok/Vq/bxJ6FlV6R7DX5oJSXyO0VEHZPi9DIyNU
kK3F/r4U1sWiJGWOs8i3YQWZ2ejh1C0aLFZpPcCGGgMNpoF31gyYP6ZuPDUyCXsE
g36UUw1JHNtIXYcLhnXuqj4A8TybTDpgXLqvwA9DBQIDAQABo1MwUTAdBgNVHQ4E
FgQUc13z30pFC63rr/HGKOE7E82vjXwwHwYDVR0jBBgwFoAUc13z30pFC63rr/HG
KOE7E82vjXwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAHO3j
oeiUXXJ7xD4P8Wj5t9d+E8lE1Xv1Dk3Z+EdG5+dan+RcToE42JJp9zB7FIh5Qz8g
W77LAjqh5oyqz3A2VJcyVgfE3uJP1R1mJM7JfGHf84QH4TZF2Q1RZY4SZs0VQ6+q
5wSlIZ4NXDy4Q4XkIJBGS61wT8IzYFXYBpx4PCP1Qj0PIE4sevEGwjsBIgxK307o
BxF8AWe6N6e4YZmQLGjQ+SeH0iwZb6vpkHyAY8Kj2hvK+cq2P7vU3VGi0t3r1F8L
IvrXHCvO2BMNJ/1UK1M4YNX8LYJqQhg9hEsIROe1OE/m3VhxIYMJI+qZXk9yHfgJ
vq+SH04xKhtFudVBAQ==`
tests := []struct {
name string
cfg *Config
wantErr bool
}{
{
name: "nil config",
cfg: nil,
wantErr: false,
},
{
name: "nil admin config",
cfg: &Config{
Admin: nil,
},
wantErr: false,
},
{
name: "nil remote config",
cfg: &Config{
Admin: &AdminConfig{},
},
wantErr: false,
},
{
name: "invalid listen address",
cfg: &Config{
Admin: &AdminConfig{
Remote: &RemoteAdmin{
Listen: "invalid:address",
},
},
},
wantErr: true,
},
{
name: "valid config",
cfg: &Config{
Admin: &AdminConfig{
Identity: &IdentityConfig{},
Remote: &RemoteAdmin{
Listen: "localhost:2021",
AccessControl: []*AdminAccess{
{
PublicKeys: []string{testCert},
Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}},
},
},
},
},
},
wantErr: false,
},
{
name: "invalid certificate",
cfg: &Config{
Admin: &AdminConfig{
Identity: &IdentityConfig{},
Remote: &RemoteAdmin{
Listen: "localhost:2021",
AccessControl: []*AdminAccess{
{
PublicKeys: []string{"invalid-cert-data"},
Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}},
},
},
},
},
},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := Context{
Context: context.Background(),
cfg: test.cfg,
}
if test.cfg != nil {
test.cfg.storage = &certmagic.FileStorage{Path: t.TempDir()}
}
if test.cfg != nil && test.cfg.Admin != nil && test.cfg.Admin.Identity != nil {
identityCertCache = certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
return &certmagic.Config{}, nil
},
})
}
err := replaceRemoteAdminServer(ctx, test.cfg)
if test.wantErr {
if err == nil {
t.Error("Expected error but got nil")
}
} else {
if err != nil {
t.Errorf("Expected no error but got: %v", err)
}
}
// Clean up
if remoteAdminServer != nil {
_ = stopAdminServer(remoteAdminServer)
}
})
}
}
type mockIssuer struct {
configSet *certmagic.Config
}
func (m *mockIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) {
return &certmagic.IssuedCertificate{
Certificate: []byte(csr.Raw),
}, nil
}
func (m *mockIssuer) SetConfig(cfg *certmagic.Config) {
m.configSet = cfg
}
func (m *mockIssuer) IssuerKey() string {
return "mock"
}
type mockIssuerModule struct {
*mockIssuer
}
func (m *mockIssuerModule) CaddyModule() ModuleInfo {
return ModuleInfo{
ID: "tls.issuance.acme",
New: func() Module {
return &mockIssuerModule{mockIssuer: new(mockIssuer)}
},
}
}
func TestManageIdentity(t *testing.T) {
originalModules := make(map[string]ModuleInfo)
maps.Copy(originalModules, modules)
defer func() {
modules = originalModules
}()
RegisterModule(&mockIssuerModule{})
certPEM := []byte(`-----BEGIN CERTIFICATE-----
MIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw
WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp
bC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3lcub2pUwkjC
5GJQA2ZZfJJi6d1QHhEmkX9VxKYGp6gagZuRqJWy9TXP6++1ZzQQxqZLD0TkuxZ9
8i9Nz00000CCBjCCAQQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGgG
CCsGAQUFBwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29t
L0dJQUcyLmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5j
b20vb2NzcDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/
BAIwADAfBgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHREEEDAO
ggxtYWlsLmdvb2dsZTANBgkqhkiG9w0BAQUFAAOCAQEAMP6IWgNGZE8wP9TjFjSZ
3mmW3A1eIr0CuPwNZ2LJ5ZD1i70ojzcj4I9IdP5yPg9CAEV4hNASbM1LzfC7GmJE
tPzW5tRmpKVWZGRgTgZI8Hp/xZXMwLh9ZmXV4kESFAGj5G5FNvJyUV7R5Eh+7OZX
7G4jJ4ZGJh+5jzN9HdJJHQHGYNIYOzC7+HH9UMwCjX9vhQ4RjwFZJThS2Yb+y7pb
9yxTJZoXC6J0H5JpnZb7kZEJ+Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END CERTIFICATE-----`)
keyPEM := []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
...
-----END PRIVATE KEY-----`)
tmpDir, err := os.MkdirTemp("", "TestManageIdentity-")
if err != nil {
t.Fatal(err)
}
testStorage := certmagic.FileStorage{Path: tmpDir}
// Clean up the temp dir after the test finishes. Ensure any background
// certificate maintenance is stopped first to avoid RemoveAll races.
t.Cleanup(func() {
if identityCertCache != nil {
identityCertCache.Stop()
identityCertCache = nil
}
// Give goroutines a moment to exit and release file handles.
time.Sleep(50 * time.Millisecond)
_ = os.RemoveAll(tmpDir)
})
err = testStorage.Store(context.Background(), "localhost/localhost.crt", certPEM)
if err != nil {
t.Fatal(err)
}
err = testStorage.Store(context.Background(), "localhost/localhost.key", keyPEM)
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
cfg *Config
wantErr bool
checkState func(*testing.T, *Config)
}{
{
name: "nil config",
cfg: nil,
},
{
name: "nil admin config",
cfg: &Config{
Admin: nil,
},
},
{
name: "nil identity config",
cfg: &Config{
Admin: &AdminConfig{},
},
},
{
name: "default issuer when none specified",
cfg: &Config{
Admin: &AdminConfig{
Identity: &IdentityConfig{
Identifiers: []string{"localhost"},
},
},
storage: &testStorage,
},
checkState: func(t *testing.T, cfg *Config) {
if len(cfg.Admin.Identity.issuers) == 0 {
t.Error("Expected at least 1 issuer to be configured")
return
}
if _, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule); !ok {
t.Error("Expected mock issuer to be configured")
}
},
},
{
name: "custom issuer",
cfg: &Config{
Admin: &AdminConfig{
Identity: &IdentityConfig{
Identifiers: []string{"localhost"},
IssuersRaw: []json.RawMessage{
json.RawMessage(`{"module": "acme"}`),
},
},
},
storage: &testStorage,
},
checkState: func(t *testing.T, cfg *Config) {
if len(cfg.Admin.Identity.issuers) != 1 {
t.Fatalf("Expected 1 issuer, got %d", len(cfg.Admin.Identity.issuers))
}
mockIss, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule)
if !ok {
t.Fatal("Expected mock issuer")
}
if mockIss.configSet == nil {
t.Error("Issuer config was not set")
}
},
},
{
name: "invalid issuer module",
cfg: &Config{
Admin: &AdminConfig{
Identity: &IdentityConfig{
Identifiers: []string{"localhost"},
IssuersRaw: []json.RawMessage{
json.RawMessage(`{"module": "doesnt_exist"}`),
},
},
},
},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if identityCertCache != nil {
// Reset the cert cache before each test
identityCertCache.Stop()
identityCertCache = nil
}
// Ensure any cache started by manageIdentity is stopped at the end
defer func() {
if identityCertCache != nil {
identityCertCache.Stop()
identityCertCache = nil
}
}()
ctx := Context{
Context: context.Background(),
cfg: test.cfg,
moduleInstances: make(map[string][]Module),
}
// If this test provided a FileStorage, set the package-level
// testCertMagicStorageOverride so certmagicConfig will use it.
if test.cfg != nil && test.cfg.storage != nil {
testCertMagicStorageOverride = test.cfg.storage
defer func() { testCertMagicStorageOverride = nil }()
}
err := manageIdentity(ctx, test.cfg)
if test.wantErr {
if err == nil {
t.Error("Expected error but got nil")
}
return
}
if err != nil {
t.Fatalf("Expected no error but got: %v", err)
}
if test.checkState != nil {
test.checkState(t, test.cfg)
}
})
}
}
caddy-2.11.2/caddy.go 0000664 0000000 0000000 00000121204 15173660016 0014301 0 ustar 00root root 0000000 0000000 // 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 caddy
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"os"
"path"
"path/filepath"
"runtime/debug"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/caddyserver/certmagic"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2/internal/filesystems"
"github.com/caddyserver/caddy/v2/notify"
)
// Config is the top (or beginning) of the Caddy configuration structure.
// Caddy config is expressed natively as a JSON document. If you prefer
// not to work with JSON directly, there are [many config adapters](/docs/config-adapters)
// available that can convert various inputs into Caddy JSON.
//
// Many parts of this config are extensible through the use of Caddy modules.
// Fields which have a json.RawMessage type and which appear as dots (•••) in
// the online docs can be fulfilled by modules in a certain module
// namespace. The docs show which modules can be used in a given place.
//
// Whenever a module is used, its name must be given either inline as part of
// the module, or as the key to the module's value. The docs will make it clear
// which to use.
//
// Generally, all config settings are optional, as it is Caddy convention to
// have good, documented default values. If a parameter is required, the docs
// should say so.
//
// Go programs which are directly building a Config struct value should take
// care to populate the JSON-encodable fields of the struct (i.e. the fields
// with `json` struct tags) if employing the module lifecycle (e.g. Provision
// method calls).
type Config struct {
Admin *AdminConfig `json:"admin,omitempty"`
Logging *Logging `json:"logging,omitempty"`
// StorageRaw is a storage module that defines how/where Caddy
// stores assets (such as TLS certificates). The default storage
// module is `caddy.storage.file_system` (the local file system),
// and the default path
// [depends on the OS and environment](/docs/conventions#data-directory).
StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
// AppsRaw are the apps that Caddy will load and run. The
// app module name is the key, and the app's config is the
// associated value.
AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
apps map[string]App
// failedApps is a map of apps that failed to provision with their underlying error.
failedApps map[string]error
storage certmagic.Storage
eventEmitter eventEmitter
cancelFunc context.CancelCauseFunc
// fileSystems is a dict of fileSystems that will later be loaded from and added to.
fileSystems FileSystems
}
// App is a thing that Caddy runs.
type App interface {
Start() error
Stop() error
}
// Run runs the given config, replacing any existing config.
func Run(cfg *Config) error {
cfgJSON, err := json.Marshal(cfg)
if err != nil {
return err
}
return Load(cfgJSON, true)
}
// Load loads the given config JSON and runs it only
// 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))
}
// 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))
}
}()
err = changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, "", forceReload)
if errors.Is(err, errSameConfig) {
err = nil // not really an error
}
return err
}
// changeConfig changes the current config (rawCfg) according to the
// method, traversed via the given path, and uses the given input as
// the new value (if applicable; i.e. "DELETE" doesn't have an input).
// If the resulting config is the same as the previous, no reload will
// occur unless forceReload is true. If the config is unchanged and not
// forcefully reloaded, then errConfigUnchanged is returned. This function
// is safe for concurrent use.
// The ifMatchHeader can optionally be given a string of the format:
//
// ""
//
// where is the absolute path in the config and is the expected hash of
// the config at that path. If the hash in the ifMatchHeader doesn't match
// the hash of the config, then an APIError with status 412 will be returned.
func changeConfig(method, path string, input []byte, ifMatchHeader string, forceReload bool) error {
switch method {
case http.MethodGet,
http.MethodHead,
http.MethodOptions,
http.MethodConnect,
http.MethodTrace:
return fmt.Errorf("method not allowed")
}
rawCfgMu.Lock()
defer rawCfgMu.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 \"\""),
}
}
// get the current hash of the config
// at the given path
hash := etagHasher()
err := unsyncedConfigAccess(http.MethodGet, parts[0], nil, hash)
if err != nil {
return err
}
if hex.EncodeToString(hash.Sum(nil)) != parts[1] {
return APIError{
HTTPStatus: http.StatusPreconditionFailed,
Err: fmt.Errorf("If-Match header did not match current config hash"),
}
}
}
err := unsyncedConfigAccess(method, path, input, nil)
if err != nil {
return err
}
// the mutation is complete, so encode the entire config as JSON
newCfg, err := json.Marshal(rawCfg[rawConfigKey])
if err != nil {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("encoding new config: %v", err),
}
}
// if nothing changed, no need to do a whole reload unless the client forces it
if !forceReload && bytes.Equal(rawCfgJSON, newCfg) {
Log().Info("config is unchanged")
return errSameConfig
}
// find any IDs in this config and index them
idx := make(map[string]string)
err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
if err != nil {
if len(rawCfgJSON) > 0 {
var oldCfg any
err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
if err2 != nil {
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
}
rawCfg[rawConfigKey] = oldCfg
} else {
rawCfg[rawConfigKey] = nil
}
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("indexing config: %v", err),
}
}
// load this new config; if it fails, we need to revert to
// our old representation of caddy's actual config
err = unsyncedDecodeAndRun(newCfg, true)
if err != nil {
if len(rawCfgJSON) > 0 {
// restore old config state to keep it consistent
// 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
err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
if err2 != nil {
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
}
rawCfg[rawConfigKey] = oldCfg
} else {
rawCfg[rawConfigKey] = nil
}
return fmt.Errorf("loading new config: %v", err)
}
// success, so update our stored copy of the encoded
// config to keep it consistent with what caddy is now
// running (storing an encoded copy is not strictly
// necessary, but avoids an extra json.Marshal for
// each config change)
rawCfgJSON = newCfg
rawCfgIndex = idx
return nil
}
// readConfig traverses the current config to path
// and writes its JSON encoding to out.
func readConfig(path string, out io.Writer) error {
rawCfgMu.RLock()
defer rawCfgMu.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 {
switch val := ptr.(type) {
case map[string]any:
for k, v := range val {
if k == idKey {
var idStr string
switch idVal := v.(type) {
case string:
idStr = idVal
case float64: // all JSON numbers decode as float64
idStr = fmt.Sprintf("%v", idVal)
default:
return fmt.Errorf("%s: %s field must be a string or number", configPath, idKey)
}
if existingPath, ok := index[idStr]; ok {
return fmt.Errorf("duplicate ID '%s' found at %s and %s", idStr, existingPath, configPath)
}
index[idStr] = configPath
continue
}
// traverse this object property recursively
err := indexConfigObjects(val[k], path.Join(configPath, k), index)
if err != nil {
return err
}
}
case []any:
// traverse each element of the array recursively
for i := range val {
err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index)
if err != nil {
return err
}
}
}
return nil
}
// unsyncedDecodeAndRun removes any meta fields (like @id tags)
// from cfgJSON, decodes the result into a *Config, and runs
// 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 rawCfgMu 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 {
// remove any @id fields from the JSON, which would cause
// loading to break since the field wouldn't be recognized
strippedCfgJSON := RemoveMetaFields(cfgJSON)
var newCfg *Config
err := StrictUnmarshalJSON(strippedCfgJSON, &newCfg)
if err != nil {
return err
}
// prevent recursive config loads; that is a user error, and
// although frequent config loads should be safe, we cannot
// guarantee that in the presence of third party plugins, nor
// do we want this error to go unnoticed (we assume it was a
// pulled config if we're not allowed to persist it)
if !allowPersist &&
newCfg != nil &&
newCfg.Admin != nil &&
newCfg.Admin.Config != nil &&
newCfg.Admin.Config.LoadRaw != nil &&
newCfg.Admin.Config.LoadDelay <= 0 {
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_delay")
}
// run the new config and start all its apps
ctx, err := run(newCfg, true)
if err != nil {
return err
}
// swap old context (including its config) with the new one
currentCtxMu.Lock()
oldCtx := currentCtx
currentCtx = ctx
currentCtxMu.Unlock()
// Stop, Cleanup each old app
unsyncedStop(oldCtx)
// autosave a non-nil config, if not disabled
if allowPersist &&
newCfg != nil &&
(newCfg.Admin == nil ||
newCfg.Admin.Config == nil ||
newCfg.Admin.Config.Persist == nil ||
*newCfg.Admin.Config.Persist) {
dir := filepath.Dir(ConfigAutosavePath)
err := os.MkdirAll(dir, 0o700)
if err != nil {
Log().Error("unable to create folder for config autosave",
zap.String("dir", dir),
zap.Error(err))
} else {
err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0o600)
if err == nil {
Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath))
} else {
Log().Error("unable to autosave config",
zap.String("file", ConfigAutosavePath),
zap.Error(err))
}
}
}
return nil
}
// run runs newCfg and starts all its apps if
// start is true. If any errors happen, cleanup
// is performed if any modules were provisioned;
// apps that were started already will be stopped,
// so this function should not leak resources if
// an error is returned. However, if no error is
// returned and start == false, you should cancel
// the config if you are not going to start it,
// so that each provisioned module will be
// cleaned up.
//
// 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) {
ctx, err := provisionContext(newCfg, start)
if err != nil {
globalMetrics.configSuccess.Set(0)
return ctx, err
}
if !start {
return ctx, nil
}
defer func() {
// if newCfg fails to start completely, clean up the already provisioned modules
// partially copied from provisionContext
if err != nil {
globalMetrics.configSuccess.Set(0)
ctx.cfg.cancelFunc(fmt.Errorf("configuration start error: %w", err))
if currentCtx.cfg != nil {
certmagic.Default.Storage = currentCtx.cfg.storage
}
}
}()
// Provision any admin routers which may need to access
// some of the other apps at runtime
err = ctx.cfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return ctx, err
}
// Start
err = func() error {
started := make([]string, 0, len(ctx.cfg.apps))
for name, a := range ctx.cfg.apps {
err := a.Start()
if err != nil {
// an app failed to start, so we need to stop
// all other apps that were already started
for _, otherAppName := range started {
err2 := ctx.cfg.apps[otherAppName].Stop()
if err2 != nil {
err = fmt.Errorf("%v; additionally, aborting app %s: %v",
err, otherAppName, err2)
}
}
return fmt.Errorf("%s app module: start: %v", name, err)
}
started = append(started, name)
}
return nil
}()
if err != nil {
return ctx, err
}
globalMetrics.configSuccess.Set(1)
globalMetrics.configSuccessTime.SetToCurrentTime()
// TODO: This event is experimental and subject to change.
ctx.emitEvent("started", nil)
// now that the user's config is running, finish setting up anything else,
// such as remote admin endpoint, config loader, etc.
err = finishSettingUp(ctx, ctx.cfg)
return ctx, err
}
// provisionContext creates a new context from the given configuration and provisions
// storage and apps.
// If `newCfg` is nil a new empty configuration will be created.
// If `replaceAdminServer` is true any currently active admin server will be replaced
// with a new admin server based on the provided configuration.
func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) {
// because we will need to roll back any state
// modifications if this function errors, we
// keep a single error value and scope all
// sub-operations to their own functions to
// ensure this error value does not get
// overridden or missed when it should have
// been set by a short assignment
var err error
if newCfg == nil {
newCfg = new(Config)
}
// create a context within which to load
// modules - essentially our new config's
// execution environment; be sure that
// cleanup occurs when we return if there
// was an error; if no error, it will get
// cleaned up on next config cycle
ctx, cancelCause := NewContextWithCause(Context{Context: context.Background(), cfg: newCfg})
defer func() {
if err != nil {
globalMetrics.configSuccess.Set(0)
// if there were any errors during startup,
// we should cancel the new context we created
// since the associated config won't be used;
// this will cause all modules that were newly
// provisioned to clean themselves up
cancelCause(fmt.Errorf("configuration error: %w", err))
// also undo any other state changes we made
if currentCtx.cfg != nil {
certmagic.Default.Storage = currentCtx.cfg.storage
}
}
}()
newCfg.cancelFunc = cancelCause // clean up later
// set up logging before anything bad happens
if newCfg.Logging == nil {
newCfg.Logging = new(Logging)
}
err = newCfg.Logging.openLogs(ctx)
if err != nil {
return ctx, err
}
// create the new filesystem map
newCfg.fileSystems = &filesystems.FileSystemMap{}
// prepare the new config for use
newCfg.apps = make(map[string]App)
newCfg.failedApps = make(map[string]error)
// set up global storage and make it CertMagic's default storage, too
err = func() error {
if newCfg.StorageRaw != nil {
val, err := ctx.LoadModule(newCfg, "StorageRaw")
if err != nil {
return fmt.Errorf("loading storage module: %v", err)
}
stor, err := val.(StorageConverter).CertMagicStorage()
if err != nil {
return fmt.Errorf("creating storage value: %v", err)
}
newCfg.storage = stor
}
if newCfg.storage == nil {
newCfg.storage = DefaultStorage
}
certmagic.Default.Storage = newCfg.storage
return nil
}()
if err != nil {
return ctx, err
}
// start the admin endpoint (and stop any prior one)
if replaceAdminServer {
err = replaceLocalAdminServer(newCfg, ctx)
if err != nil {
return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
}
}
// Load and Provision each app and their submodules
err = func() error {
for appName := range newCfg.AppsRaw {
if _, err := ctx.App(appName); err != nil {
return err
}
}
return nil
}()
return ctx, err
}
// ProvisionContext creates a new context from the configuration and provisions storage
// and app modules.
// The function is intended for testing and advanced use cases only, typically `Run` should be
// use to ensure a fully functional caddy instance.
// EXPERIMENTAL: While this is public the interface and implementation details of this function may change.
func ProvisionContext(newCfg *Config) (Context, error) {
return provisionContext(newCfg, false)
}
// finishSettingUp should be run after all apps have successfully started.
func finishSettingUp(ctx Context, cfg *Config) error {
// establish this server's identity (only after apps are loaded
// so that cert management of this endpoint doesn't prevent user's
// servers from starting which likely also use HTTP/HTTPS ports;
// but before remote management which may depend on these creds)
err := manageIdentity(ctx, cfg)
if err != nil {
return fmt.Errorf("provisioning remote admin endpoint: %v", err)
}
// replace any remote admin endpoint
err = replaceRemoteAdminServer(ctx, cfg)
if err != nil {
return fmt.Errorf("provisioning remote admin endpoint: %v", err)
}
// if dynamic config is requested, set that up and run it
if cfg != nil && cfg.Admin != nil && cfg.Admin.Config != nil && cfg.Admin.Config.LoadRaw != nil {
val, err := ctx.LoadModule(cfg.Admin.Config, "LoadRaw")
if err != nil {
return fmt.Errorf("loading config loader module: %s", err)
}
logger := Log().Named("config_loader").With(
zap.String("module", val.(Module).CaddyModule().ID.Name()),
zap.Int("load_delay", int(cfg.Admin.Config.LoadDelay)))
runLoadedConfig := func(config []byte) error {
logger.Info("applying dynamically-loaded config")
err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, "", false)
if errors.Is(err, errSameConfig) {
return err
}
if err != nil {
logger.Error("failed to run dynamically-loaded config", zap.Error(err))
return err
}
logger.Info("successfully applied dynamically-loaded config")
return nil
}
if cfg.Admin.Config.LoadDelay > 0 {
go func() {
// the loop is here to iterate ONLY if there is an error, a no-op config load,
// or an unchanged config; in which case we simply wait the delay and try again
for {
timer := time.NewTimer(time.Duration(cfg.Admin.Config.LoadDelay))
select {
case <-timer.C:
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
if err != nil {
logger.Error("failed loading dynamic config; will retry", zap.Error(err))
continue
}
if loadedConfig == nil {
logger.Info("dynamically-loaded config was nil; will retry")
continue
}
err = runLoadedConfig(loadedConfig)
if errors.Is(err, errSameConfig) {
logger.Info("dynamically-loaded config was unchanged; will retry")
continue
}
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
logger.Info("stopping dynamic config loading")
}
break
}
}()
} else {
// if no LoadDelay is provided, will load config synchronously
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
if err != nil {
return fmt.Errorf("loading dynamic config from %T: %v", val, err)
}
// do this in a goroutine so current config can finish being loaded; otherwise deadlock
go func() { _ = runLoadedConfig(loadedConfig) }()
}
}
return nil
}
// ConfigLoader is a type that can load a Caddy config. If
// the return value is non-nil, it must be valid Caddy JSON;
// if nil or with non-nil error, it is considered to be a
// no-op load and may be retried later.
type ConfigLoader interface {
LoadConfig(Context) ([]byte, error)
}
// Stop stops running the current configuration.
// It is the antithesis of Run(). This function
// will log any errors that occur during the
// stopping of individual apps and continue to
// stop the others. Stop should only be called
// if not replacing with a new config.
func Stop() error {
currentCtxMu.RLock()
ctx := currentCtx
currentCtxMu.RUnlock()
rawCfgMu.Lock()
unsyncedStop(ctx)
currentCtxMu.Lock()
currentCtx = Context{}
currentCtxMu.Unlock()
rawCfgJSON = nil
rawCfgIndex = nil
rawCfg[rawConfigKey] = nil
rawCfgMu.Unlock()
return nil
}
// unsyncedStop stops ctx from running, but has
// no locking around ctx. It is a no-op if ctx has a
// nil cfg. If any app returns an error when stopping,
// it is logged and the function continues stopping
// the next app. This function assumes all apps in
// ctx were successfully started first.
//
// A lock on rawCfgMu is required, even though this
// function does not access rawCfg, that lock
// synchronizes the stop/start of apps.
func unsyncedStop(ctx Context) {
if ctx.cfg == nil {
return
}
// TODO: This event is experimental and subject to change.
ctx.emitEvent("stopping", nil)
// stop each app
for name, a := range ctx.cfg.apps {
err := a.Stop()
if err != nil {
log.Printf("[ERROR] stop %s: %v", name, err)
}
}
// clean up all modules
ctx.cfg.cancelFunc(fmt.Errorf("stopping apps"))
}
// Validate loads, provisions, and validates
// cfg, but does not start running it.
func Validate(cfg *Config) error {
_, err := run(cfg, false)
if err == nil {
cfg.cancelFunc(fmt.Errorf("validation complete")) // call Cleanup on all modules
}
return err
}
// exitProcess exits the process as gracefully as possible,
// but it always exits, even if there are errors doing so.
// It stops all apps, cleans up external locks, removes any
// PID file, and shuts down admin endpoint(s) in a goroutine.
// 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; only do it once
if !atomic.CompareAndSwapInt32(exiting, 0, 1) {
return
}
// 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()
}
logger.Warn("exiting; byeee!! 👋")
exitCode := ExitCodeSuccess
lastContext := ActiveContext()
// stop all apps
if err := Stop(); err != nil {
logger.Error("failed to stop apps", zap.Error(err))
exitCode = ExitCodeFailedQuit
}
// clean up certmagic locks
certmagic.CleanUpOwnLocks(ctx, logger)
// remove pidfile
if pidfile != "" {
err := os.Remove(pidfile)
if err != nil {
logger.Error("cleaning up PID file:",
zap.String("pidfile", pidfile),
zap.Error(err))
exitCode = ExitCodeFailedQuit
}
}
// execute any process-exit callbacks
for _, exitFunc := range lastContext.exitFuncs {
exitFunc(ctx)
}
exitFuncsMu.Lock()
for _, exitFunc := range exitFuncs {
exitFunc(ctx)
}
exitFuncsMu.Unlock()
// shut down admin endpoint(s) in goroutines so that
// if this function was called from an admin handler,
// it has a chance to return gracefully
// use goroutine so that we can finish responding to API request
go func() {
defer func() {
logger = logger.With(zap.Int("exit_code", exitCode))
if exitCode == ExitCodeSuccess {
logger.Info("shutdown complete")
} else {
logger.Error("unclean shutdown")
}
os.Exit(exitCode)
}()
if remoteAdminServer != nil {
err := stopAdminServer(remoteAdminServer)
if err != nil {
exitCode = ExitCodeFailedQuit
logger.Error("failed to stop remote admin server gracefully", zap.Error(err))
}
}
if localAdminServer != nil {
err := stopAdminServer(localAdminServer)
if err != nil {
exitCode = ExitCodeFailedQuit
logger.Error("failed to stop local admin server gracefully", zap.Error(err))
}
}
}()
}
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 }
// OnExit registers a callback to invoke during process exit.
// This registration is PROCESS-GLOBAL, meaning that each
// function should only be registered once forever, NOT once
// per config load (etc).
//
// EXPERIMENTAL API: subject to change or removal.
func OnExit(f func(context.Context)) {
exitFuncsMu.Lock()
exitFuncs = append(exitFuncs, f)
exitFuncsMu.Unlock()
}
var (
exitFuncs []func(context.Context)
exitFuncsMu sync.Mutex
)
// 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`;
// valid units are `ns`, `us`/`µs`, `ms`, `s`, `m`, `h`, and `d`.
type Duration time.Duration
// UnmarshalJSON satisfies json.Unmarshaler.
func (d *Duration) UnmarshalJSON(b []byte) error {
if len(b) == 0 {
return io.EOF
}
var dur time.Duration
var err error
if b[0] == byte('"') && b[len(b)-1] == byte('"') {
dur, err = ParseDuration(strings.Trim(string(b), `"`))
} else {
err = json.Unmarshal(b, &dur)
}
*d = Duration(dur)
return err
}
// 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.
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++ {
ch := s[i]
if ch == 'd' {
daysStr := s[numStart:i]
days, err := strconv.ParseFloat(daysStr, 64)
if err != nil {
return 0, err
}
hours := days * 24.0
hoursStr := strconv.FormatFloat(hours, 'f', -1, 64)
s = s[:numStart] + hoursStr + "h" + s[i+1:]
i--
continue
}
if !inNumber {
numStart = i
}
inNumber = (ch >= '0' && ch <= '9') || ch == '.' || ch == '-' || ch == '+'
}
return time.ParseDuration(s)
}
// InstanceID returns the UUID for this instance, and generates one if it
// does not already exist. The UUID is stored in the local data directory,
// regardless of storage configuration, since each instance is intended to
// have its own unique ID.
func InstanceID() (uuid.UUID, error) {
appDataDir := AppDataDir()
uuidFilePath := filepath.Join(appDataDir, "instance.uuid")
uuidFileBytes, err := os.ReadFile(uuidFilePath)
if errors.Is(err, fs.ErrNotExist) {
uuid, err := uuid.NewRandom()
if err != nil {
return uuid, err
}
err = os.MkdirAll(appDataDir, 0o700)
if err != nil {
return uuid, err
}
err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0o600)
return uuid, err
} else if err != nil {
return [16]byte{}, err
}
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
// CustomBinaryName is an optional string that overrides the root
// command name from the default of "caddy". This is useful for
// downstream projects that embed Caddy but use a different binary
// name. Shell completions and help text will use this name instead
// of "caddy".
//
// Set this variable during `go build` with `-ldflags`:
//
// -ldflags '-X github.com/caddyserver/caddy/v2.CustomBinaryName=my_custom_caddy'
//
// for example.
var CustomBinaryName string
// CustomLongDescription is an optional string that overrides the
// long description of the root Cobra command. This is useful for
// downstream projects that embed Caddy but want different help
// output.
//
// Set this variable in an init() function of a package that is
// imported by your main:
//
// func init() {
// caddy.CustomLongDescription = "My custom server based on Caddy..."
// }
//
// for example.
var CustomLongDescription 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 simple, full
}
full = "unknown"
simple = "unknown"
return simple, full
}
// 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 simple, full
}
// Event represents something that has happened or is happening.
// An Event value is not synchronized, so it should be copied if
// being used in goroutines.
//
// EXPERIMENTAL: Events are subject to change.
type Event struct {
// If non-nil, the event has been aborted, meaning
// propagation has stopped to other handlers and
// the code should stop what it was doing. Emitters
// may choose to use this as a signal to adjust their
// code path appropriately.
Aborted error
// The data associated with the event. Usually the
// original emitter will be the only one to set or
// change these values, but the field is exported
// so handlers can have full access if needed.
// However, this map is not synchronized, so
// handlers must not use this map directly in new
// goroutines; instead, copy the map to use it in a
// goroutine. Data may be nil.
Data map[string]any
id uuid.UUID
ts time.Time
name string
origin Module
}
// NewEvent creates a new event, but does not emit the event. To emit an
// event, call Emit() on the current instance of the caddyevents app instead.
//
// EXPERIMENTAL: Subject to change.
func NewEvent(ctx Context, name string, data map[string]any) (Event, error) {
id, err := uuid.NewRandom()
if err != nil {
return Event{}, fmt.Errorf("generating new event ID: %v", err)
}
name = strings.ToLower(name)
return Event{
Data: data,
id: id,
ts: time.Now(),
name: name,
origin: ctx.Module(),
}, nil
}
func (e Event) ID() uuid.UUID { return e.id }
func (e Event) Timestamp() time.Time { return e.ts }
func (e Event) Name() string { return e.name }
func (e Event) Origin() Module { return e.origin } // Returns the module that originated the event. May be nil, usually if caddy core emits the event.
// CloudEvent exports event e as a structure that, when
// serialized as JSON, is compatible with the
// CloudEvents spec.
func (e Event) CloudEvent() CloudEvent {
dataJSON, _ := json.Marshal(e.Data)
var source string
if e.Origin() == nil {
source = "caddy"
} else {
source = string(e.Origin().CaddyModule().ID)
}
return CloudEvent{
ID: e.id.String(),
Source: source,
SpecVersion: "1.0",
Type: e.name,
Time: e.ts,
DataContentType: "application/json",
Data: dataJSON,
}
}
// CloudEvent is a JSON-serializable structure that
// is compatible with the CloudEvents specification.
// See https://cloudevents.io.
// EXPERIMENTAL: Subject to change.
type CloudEvent struct {
ID string `json:"id"`
Source string `json:"source"`
SpecVersion string `json:"specversion"`
Type string `json:"type"`
Time time.Time `json:"time"`
DataContentType string `json:"datacontenttype,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
// ErrEventAborted cancels an event.
var ErrEventAborted = errors.New("event aborted")
// ActiveContext returns the currently-active context.
// This function is experimental and might be changed
// or removed in the future.
func ActiveContext() Context {
currentCtxMu.RLock()
defer currentCtxMu.RUnlock()
return currentCtx
}
// CtxKey is a value type for use with context.WithValue.
type CtxKey string
// This group of variables pertains to the current configuration.
var (
// 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
currentCtxMu sync.RWMutex
// 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{
rawConfigKey: nil,
}
// rawCfgJSON is the JSON-encoded form of rawCfg. Keeping
// this around avoids an extra Marshal call during changes.
rawCfgJSON []byte
// rawCfgIndex is the map of user-assigned ID to expanded
// path, for converting /id/ paths to /config/ paths.
rawCfgIndex map[string]string
// rawCfgMu protects all the rawCfg fields and also
// essentially synchronizes config changes/reloads.
rawCfgMu sync.RWMutex
)
// lastConfigFile and lastConfigAdapter remember the source config
// file and adapter used when Caddy was started via the CLI "run" command.
// These are consulted by the SIGUSR1 handler to attempt reloading from
// the same source. They are intentionally not set for other entrypoints
// such as "caddy start" or subcommands like file-server.
var (
lastConfigMu sync.RWMutex
lastConfigFile string
lastConfigAdapter string
)
// reloadFromSourceFunc is the type of stored callback
// which is called when we receive a SIGUSR1 signal.
type reloadFromSourceFunc func(file, adapter string) error
// reloadFromSourceCallback is the stored callback
// which is called when we receive a SIGUSR1 signal.
var reloadFromSourceCallback reloadFromSourceFunc
// errReloadFromSourceUnavailable is returned when no reload-from-source callback is set.
var errReloadFromSourceUnavailable = errors.New("reload from source unavailable in this process") //nolint:unused
// SetLastConfig records the given source file and adapter as the
// last-known external configuration source. Intended to be called
// only when starting via "caddy run --config --adapter ".
func SetLastConfig(file, adapter string, fn reloadFromSourceFunc) {
lastConfigMu.Lock()
lastConfigFile = file
lastConfigAdapter = adapter
reloadFromSourceCallback = fn
lastConfigMu.Unlock()
}
// ClearLastConfigIfDifferent clears the recorded last-config if the provided
// source file/adapter do not match the recorded last-config. If both srcFile
// and srcAdapter are empty, the last-config is cleared.
func ClearLastConfigIfDifferent(srcFile, srcAdapter string) {
if (srcFile != "" || srcAdapter != "") && lastConfigMatches(srcFile, srcAdapter) {
return
}
SetLastConfig("", "", nil)
}
// getLastConfig returns the last-known config file and adapter.
func getLastConfig() (file, adapter string, fn reloadFromSourceFunc) {
lastConfigMu.RLock()
f, a, cb := lastConfigFile, lastConfigAdapter, reloadFromSourceCallback
lastConfigMu.RUnlock()
return f, a, cb
}
// lastConfigMatches returns true if the provided source file and/or adapter
// matches the recorded last-config. Matching rules (in priority order):
// 1. If srcAdapter is provided and differs from the recorded adapter, no match.
// 2. If srcFile exactly equals the recorded file, match.
// 3. If both sides can be made absolute and equal, match.
// 4. If basenames are equal, match.
func lastConfigMatches(srcFile, srcAdapter string) bool {
lf, la, _ := getLastConfig()
// If adapter is provided, it must match.
if srcAdapter != "" && srcAdapter != la {
return false
}
// Quick equality check.
if srcFile == lf {
return true
}
// Try absolute path comparison.
sAbs, sErr := filepath.Abs(srcFile)
lAbs, lErr := filepath.Abs(lf)
if sErr == nil && lErr == nil && sAbs == lAbs {
return true
}
// Final fallback: basename equality.
if filepath.Base(srcFile) == filepath.Base(lf) {
return true
}
return false
}
// errSameConfig is returned if the new config is the same
// as the old one. This isn't usually an actual, actionable
// error; it's mostly a sentinel value.
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"
caddy-2.11.2/caddy_test.go 0000664 0000000 0000000 00000004226 15173660016 0015344 0 ustar 00root root 0000000 0000000 // 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 caddy
import (
"context"
"testing"
"time"
)
func TestParseDuration(t *testing.T) {
const day = 24 * time.Hour
for i, tc := range []struct {
input string
expect time.Duration
}{
{
input: "3h",
expect: 3 * time.Hour,
},
{
input: "1d",
expect: day,
},
{
input: "1d30m",
expect: day + 30*time.Minute,
},
{
input: "1m2d",
expect: time.Minute + day*2,
},
{
input: "1m2d30s",
expect: time.Minute + day*2 + 30*time.Second,
},
{
input: "1d2d",
expect: 3 * day,
},
{
input: "1.5d",
expect: time.Duration(1.5 * float64(day)),
},
{
input: "4m1.25d",
expect: 4*time.Minute + time.Duration(1.25*float64(day)),
},
{
input: "-1.25d12h",
expect: time.Duration(-1.25*float64(day)) - 12*time.Hour,
},
} {
actual, err := ParseDuration(tc.input)
if err != nil {
t.Errorf("Test %d ('%s'): Got error: %v", i, tc.input, err)
continue
}
if actual != tc.expect {
t.Errorf("Test %d ('%s'): Expected=%s Actual=%s", i, tc.input, tc.expect, actual)
}
}
}
func TestEvent_CloudEvent_NilOrigin(t *testing.T) {
ctx, _ := NewContext(Context{Context: context.Background()}) // module will be nil by default
event, err := NewEvent(ctx, "started", nil)
if err != nil {
t.Fatalf("NewEvent() error = %v", err)
}
// This should not panic
ce := event.CloudEvent()
if ce.Source != "caddy" {
t.Errorf("Expected CloudEvent Source to be 'caddy', got '%s'", ce.Source)
}
if ce.Type != "started" {
t.Errorf("Expected CloudEvent Type to be 'started', got '%s'", ce.Type)
}
}
caddy-2.11.2/caddyconfig/ 0000775 0000000 0000000 00000000000 15173660016 0015140 5 ustar 00root root 0000000 0000000 caddy-2.11.2/caddyconfig/caddyfile/ 0000775 0000000 0000000 00000000000 15173660016 0017064 5 ustar 00root root 0000000 0000000 caddy-2.11.2/caddyconfig/caddyfile/adapter.go 0000664 0000000 0000000 00000011365 15173660016 0021041 0 ustar 00root root 0000000 0000000 // Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"bytes"
"encoding/json"
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
)
// Adapter adapts Caddyfile to Caddy JSON.
type Adapter struct {
ServerType ServerType
}
// Adapt converts the Caddyfile config in body to Caddy JSON.
func (a Adapter) Adapt(body []byte, options map[string]any) ([]byte, []caddyconfig.Warning, error) {
if a.ServerType == nil {
return nil, nil, fmt.Errorf("no server type")
}
if options == nil {
options = make(map[string]any)
}
filename, _ := options["filename"].(string)
if filename == "" {
filename = "Caddyfile"
}
serverBlocks, err := Parse(filename, body)
if err != nil {
return nil, nil, err
}
cfg, warnings, err := a.ServerType.Setup(serverBlocks, options)
if err != nil {
return nil, warnings, err
}
// lint check: see if input was properly formatted; sometimes messy files parse
// successfully but result in logical errors (the Caddyfile is a bad format, I'm sorry)
if warning, different := FormattingDifference(filename, body); different {
warnings = append(warnings, warning)
}
result, err := json.Marshal(cfg)
return result, warnings, err
}
// 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) {
// replace windows-style newlines to normalize comparison
normalizedBody := bytes.ReplaceAll(body, []byte("\r\n"), []byte("\n"))
formatted := Format(normalizedBody)
if bytes.Equal(formatted, normalizedBody) {
return caddyconfig.Warning{}, false
}
// find where the difference is
line := 1
for i, ch := range normalizedBody {
if i >= len(formatted) || ch != formatted[i] {
break
}
if ch == '\n' {
line++
}
}
return caddyconfig.Warning{
File: filename,
Line: line,
Message: "Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies",
}, true
}
// Unmarshaler is a type that can unmarshal Caddyfile tokens to
// set itself up for a JSON encoding. The goal of an unmarshaler
// is not to set itself up for actual use, but to set itself up for
// being marshaled into JSON. Caddyfile-unmarshaled values will not
// be used directly; they will be encoded as JSON and then used from
// that. Implementations _may_ be able to support multiple segments
// (instances of their directive or batch of tokens); typically this
// means wrapping parsing logic in a loop: `for d.Next() { ... }`.
// More commonly, only a single segment is supported, so a simple
// `d.Next()` at the start should be used to consume the module
// identifier token (directive name, etc).
type Unmarshaler interface {
UnmarshalCaddyfile(d *Dispenser) error
}
// ServerType is a type that can evaluate a Caddyfile and set up a caddy config.
type ServerType interface {
// Setup takes the server blocks which contain tokens,
// as well as options (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)
}
// UnmarshalModule instantiates a module with the given ID and invokes
// UnmarshalCaddyfile on the new value using the immediate next segment
// of d as input. In other words, d's next token should be the first
// token of the module's Caddyfile input.
//
// This function is used when the next segment of Caddyfile tokens
// belongs to another Caddy module. The returned value is often
// type-asserted to the module's associated type for practical use
// when setting up a config.
func UnmarshalModule(d *Dispenser, moduleID string) (Unmarshaler, error) {
mod, err := caddy.GetModule(moduleID)
if err != nil {
return nil, d.Errf("getting module named '%s': %v", moduleID, err)
}
inst := mod.New()
unm, ok := inst.(Unmarshaler)
if !ok {
return nil, d.Errf("module %s is not a Caddyfile unmarshaler; is %T", mod.ID, inst)
}
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
if err != nil {
return nil, err
}
return unm, nil
}
// Interface guard
var _ caddyconfig.Adapter = (*Adapter)(nil)
caddy-2.11.2/caddyconfig/caddyfile/dispenser.go 0000664 0000000 0000000 00000036424 15173660016 0021420 0 ustar 00root root 0000000 0000000 // Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"errors"
"fmt"
"io"
"log"
"strconv"
"strings"
)
// Dispenser is a type that dispenses tokens, similarly to a lexer,
// except that it can do so with some notion of structure. An empty
// Dispenser is invalid; call NewDispenser to make a proper instance.
type Dispenser struct {
tokens []Token
cursor int
nesting int
// A map of arbitrary context data that can be used
// to pass through some information to unmarshalers.
context map[string]any
}
// NewDispenser returns a Dispenser filled with the given tokens.
func NewDispenser(tokens []Token) *Dispenser {
return &Dispenser{
tokens: tokens,
cursor: -1,
}
}
// NewTestDispenser parses input into tokens and creates a new
// Dispenser for test purposes only; any errors are fatal.
func NewTestDispenser(input string) *Dispenser {
tokens, err := allTokens("Testfile", []byte(input))
if err != nil && err != io.EOF {
log.Fatalf("getting all tokens from input: %v", err)
}
return NewDispenser(tokens)
}
// Next loads the next token. Returns true if a token
// was loaded; false otherwise. If false, all tokens
// have been consumed.
func (d *Dispenser) Next() bool {
if d.cursor < len(d.tokens)-1 {
d.cursor++
return true
}
return false
}
// Prev moves to the previous token. It does the inverse
// of Next(), except this function may decrement the cursor
// to -1 so that the next call to Next() points to the
// first token; this allows dispensing to "start over". This
// method returns true if the cursor ends up pointing to a
// valid token.
func (d *Dispenser) Prev() bool {
if d.cursor > -1 {
d.cursor--
return d.cursor > -1
}
return false
}
// NextArg loads the next token if it is on the same
// line and if it is not a block opening (open curly
// brace). Returns true if an argument token was
// loaded; false otherwise. If false, all tokens on
// the line have been consumed except for potentially
// a block opening. It handles imported tokens
// correctly.
func (d *Dispenser) NextArg() bool {
if !d.nextOnSameLine() {
return false
}
if d.Val() == "{" {
// roll back; a block opening is not an argument
d.cursor--
return false
}
return true
}
// nextOnSameLine advances the cursor if the next
// token is on the same line of the same file.
func (d *Dispenser) nextOnSameLine() bool {
if d.cursor < 0 {
d.cursor++
return true
}
if d.cursor >= len(d.tokens)-1 {
return false
}
curr := d.tokens[d.cursor]
next := d.tokens[d.cursor+1]
if !isNextOnNewLine(curr, next) {
d.cursor++
return true
}
return false
}
// NextLine loads the next token only if it is not on the same
// line as the current token, and returns true if a token was
// loaded; false otherwise. If false, there is not another token
// or it is on the same line. It handles imported tokens correctly.
func (d *Dispenser) NextLine() bool {
if d.cursor < 0 {
d.cursor++
return true
}
if d.cursor >= len(d.tokens)-1 {
return false
}
curr := d.tokens[d.cursor]
next := d.tokens[d.cursor+1]
if isNextOnNewLine(curr, next) {
d.cursor++
return true
}
return false
}
// NextBlock can be used as the condition of a for loop
// to load the next token as long as it opens a block or
// is already in a block nested more than initialNestingLevel.
// In other words, a loop over NextBlock() will iterate
// all tokens in the block assuming the next token is an
// open curly brace, until the matching closing brace.
// The open and closing brace tokens for the outer-most
// block will be consumed internally and omitted from
// the iteration.
//
// Proper use of this method looks like this:
//
// 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) {
// }
//
// As with other token parsing logic, a loop over
// NextBlock() should be contained within a loop over
// Next(), as it is usually prudent to skip the initial
// token.
func (d *Dispenser) NextBlock(initialNestingLevel int) bool {
if d.nesting > initialNestingLevel {
if !d.Next() {
return false // should be EOF error
}
if d.Val() == "}" && !d.nextOnSameLine() {
d.nesting--
} else if d.Val() == "{" && !d.nextOnSameLine() {
d.nesting++
}
return d.nesting > initialNestingLevel
}
if !d.nextOnSameLine() { // block must open on same line
return false
}
if d.Val() != "{" {
d.cursor-- // roll back if not opening brace
return false
}
d.Next() // consume open curly brace
if d.Val() == "}" {
return false // open and then closed right away
}
d.nesting++
return true
}
// Nesting returns the current nesting level. Necessary
// if using NextBlock()
func (d *Dispenser) Nesting() int {
return d.nesting
}
// Val gets the text of the current token. If there is no token
// loaded, it returns empty string.
func (d *Dispenser) Val() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return ""
}
return d.tokens[d.cursor].Text
}
// ValRaw gets the raw text of the current token (including quotes).
// If the token was a heredoc, then the delimiter is not included,
// because that is not relevant to any unmarshaling logic at this time.
// If there is no token loaded, it returns empty string.
func (d *Dispenser) ValRaw() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return ""
}
quote := d.tokens[d.cursor].wasQuoted
if quote > 0 && quote != '<' {
// string literal
return string(quote) + d.tokens[d.cursor].Text + string(quote)
}
return d.tokens[d.cursor].Text
}
// ScalarVal gets value of the current token, converted to the closest
// scalar type. If there is no token loaded, it returns nil.
func (d *Dispenser) ScalarVal() any {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return nil
}
quote := d.tokens[d.cursor].wasQuoted
text := d.tokens[d.cursor].Text
if quote > 0 {
return text // string literal
}
if num, err := strconv.Atoi(text); err == nil {
return num
}
if num, err := strconv.ParseFloat(text, 64); err == nil {
return num
}
if bool, err := strconv.ParseBool(text); err == nil {
return bool
}
return text
}
// Line gets the line number of the current token.
// If there is no token loaded, it returns 0.
func (d *Dispenser) Line() int {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return 0
}
return d.tokens[d.cursor].Line
}
// File gets the filename where the current token originated.
func (d *Dispenser) File() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return ""
}
return d.tokens[d.cursor].File
}
// Args is a convenience function that loads the next arguments
// (tokens on the same line) into an arbitrary number of strings
// pointed to in targets. If there are not enough argument tokens
// available to fill targets, false is returned and the remaining
// targets are left unchanged. If all the targets are filled,
// then true is returned.
func (d *Dispenser) Args(targets ...*string) bool {
for i := range targets {
if !d.NextArg() {
return false
}
*targets[i] = d.Val()
}
return true
}
// AllArgs is like Args, but if there are more argument tokens
// available than there are targets, false is returned. The
// number of available argument tokens must match the number of
// targets exactly to return true.
func (d *Dispenser) AllArgs(targets ...*string) bool {
if !d.Args(targets...) {
return false
}
if d.NextArg() {
d.Prev()
return false
}
return true
}
// CountRemainingArgs counts the amount of remaining arguments
// (tokens on the same line) without consuming the tokens.
func (d *Dispenser) CountRemainingArgs() int {
count := 0
for d.NextArg() {
count++
}
for i := 0; i < count; i++ {
d.Prev()
}
return count
}
// RemainingArgs loads any more arguments (tokens on the same line)
// into a slice of strings and returns them. Open curly brace tokens
// also indicate the end of arguments, and the curly brace is not
// included in the return value nor is it loaded.
func (d *Dispenser) RemainingArgs() []string {
var args []string
for d.NextArg() {
args = append(args, d.Val())
}
return args
}
// RemainingArgsRaw loads any more arguments (tokens on the same line,
// retaining quotes) into a slice of strings and returns them.
// Open curly brace tokens also indicate the end of arguments,
// and the curly brace is not included in the return value nor is it loaded.
func (d *Dispenser) RemainingArgsRaw() []string {
var args []string
for d.NextArg() {
args = append(args, d.ValRaw())
}
return args
}
// RemainingArgsAsTokens loads any more arguments (tokens on the same line)
// into a slice of Token-structs and returns them. Open curly brace tokens
// also indicate the end of arguments, and the curly brace is not included
// in the return value nor is it loaded.
func (d *Dispenser) RemainingArgsAsTokens() []Token {
var args []Token
for d.NextArg() {
args = append(args, d.Token())
}
return args
}
// NewFromNextSegment returns a new dispenser with a copy of
// the tokens from the current token until the end of the
// "directive" whether that be to the end of the line or
// the end of a block that starts at the end of the line;
// in other words, until the end of the segment.
func (d *Dispenser) NewFromNextSegment() *Dispenser {
return NewDispenser(d.NextSegment())
}
// NextSegment returns a copy of the tokens from the current
// token until the end of the line or block that starts at
// the end of the line.
func (d *Dispenser) NextSegment() Segment {
tkns := Segment{d.Token()}
for d.NextArg() {
tkns = append(tkns, d.Token())
}
var openedBlock bool
for nesting := d.Nesting(); d.NextBlock(nesting); {
if !openedBlock {
// because NextBlock() consumes the initial open
// curly brace, we rewind here to append it, since
// our case is special in that we want the new
// dispenser to have all the tokens including
// surrounding curly braces
d.Prev()
tkns = append(tkns, d.Token())
d.Next()
openedBlock = true
}
tkns = append(tkns, d.Token())
}
if openedBlock {
// include closing brace
tkns = append(tkns, d.Token())
// do not consume the closing curly brace; the
// next iteration of the enclosing loop will
// call Next() and consume it
}
return tkns
}
// Token returns the current token.
func (d *Dispenser) Token() Token {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return Token{}
}
return d.tokens[d.cursor]
}
// Reset sets d's cursor to the beginning, as
// if this was a new and unused dispenser.
func (d *Dispenser) Reset() {
d.cursor = -1
d.nesting = 0
}
// ArgErr returns an argument error, meaning that another
// argument was expected but not found. In other words,
// a line break or open curly brace was encountered instead of
// an argument.
func (d *Dispenser) ArgErr() error {
if d.Val() == "{" {
return d.Err("unexpected token '{', expecting argument")
}
return d.Errf("wrong argument count or unexpected line ending after '%s'", d.Val())
}
// SyntaxErr creates a generic syntax error which explains what was
// found and what was expected.
func (d *Dispenser) SyntaxErr(expected string) error {
msg := fmt.Sprintf("syntax error: unexpected token '%s', expecting '%s', at %s:%d import chain: ['%s']", d.Val(), expected, d.File(), d.Line(), strings.Join(d.Token().imports, "','"))
return errors.New(msg)
}
// EOFErr returns an error indicating that the dispenser reached
// the end of the input when searching for the next token.
func (d *Dispenser) EOFErr() error {
return d.Errf("unexpected EOF")
}
// Err generates a custom parse-time error with a message of msg.
func (d *Dispenser) Err(msg string) error {
return d.WrapErr(errors.New(msg))
}
// Errf is like Err, but for formatted error messages
func (d *Dispenser) Errf(format string, args ...any) error {
return d.WrapErr(fmt.Errorf(format, args...))
}
// WrapErr takes an existing error and adds the Caddyfile file and line number.
func (d *Dispenser) WrapErr(err error) error {
if len(d.Token().imports) > 0 {
return fmt.Errorf("%w, at %s:%d import chain ['%s']", err, d.File(), d.Line(), strings.Join(d.Token().imports, "','"))
}
return fmt.Errorf("%w, at %s:%d", err, d.File(), d.Line())
}
// Delete deletes the current token and returns the updated slice
// of tokens. The cursor is not advanced to the next token.
// Because deletion modifies the underlying slice, this method
// should only be called if you have access to the original slice
// of tokens and/or are using the slice of tokens outside this
// Dispenser instance. If you do not re-assign the slice with the
// return value of this method, inconsistencies in the token
// array will become apparent (or worse, hide from you like they
// did me for 3 and a half freaking hours late one night).
func (d *Dispenser) Delete() []Token {
if d.cursor >= 0 && d.cursor <= len(d.tokens)-1 {
d.tokens = append(d.tokens[:d.cursor], d.tokens[d.cursor+1:]...)
d.cursor--
}
return d.tokens
}
// DeleteN is the same as Delete, but can delete many tokens at once.
// If there aren't N tokens available to delete, none are deleted.
func (d *Dispenser) DeleteN(amount int) []Token {
if amount > 0 && d.cursor >= (amount-1) && d.cursor <= len(d.tokens)-1 {
d.tokens = append(d.tokens[:d.cursor-(amount-1)], d.tokens[d.cursor+1:]...)
d.cursor -= amount
}
return d.tokens
}
// SetContext sets a key-value pair in the context map.
func (d *Dispenser) SetContext(key string, value any) {
if d.context == nil {
d.context = make(map[string]any)
}
d.context[key] = value
}
// GetContext gets the value of a key in the context map.
func (d *Dispenser) GetContext(key string) any {
if d.context == nil {
return nil
}
return d.context[key]
}
// GetContextString gets the value of a key in the context map
// as a string, or an empty string if the key does not exist.
func (d *Dispenser) GetContextString(key string) string {
if d.context == nil {
return ""
}
if val, ok := d.context[key].(string); ok {
return val
}
return ""
}
// isNewLine determines whether the current token is on a different
// line (higher line number) than the previous token. It handles imported
// tokens correctly. If there isn't a previous token, it returns true.
func (d *Dispenser) isNewLine() bool {
if d.cursor < 1 {
return true
}
if d.cursor > len(d.tokens)-1 {
return false
}
prev := d.tokens[d.cursor-1]
curr := d.tokens[d.cursor]
return isNextOnNewLine(prev, curr)
}
// isNextOnNewLine determines whether the current token is on a different
// line (higher line number) than the next token. It handles imported
// tokens correctly. If there isn't a next token, it returns true.
func (d *Dispenser) isNextOnNewLine() bool {
if d.cursor < 0 {
return false
}
if d.cursor >= len(d.tokens)-1 {
return true
}
curr := d.tokens[d.cursor]
next := d.tokens[d.cursor+1]
return isNextOnNewLine(curr, next)
}
const MatcherNameCtxKey = "matcher_name"
caddy-2.11.2/caddyconfig/caddyfile/dispenser_test.go 0000664 0000000 0000000 00000024033 15173660016 0022450 0 ustar 00root root 0000000 0000000 // Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"errors"
"reflect"
"strings"
"testing"
)
func TestDispenser_Val_Next(t *testing.T) {
input := `host:port
dir1 arg1
dir2 arg2 arg3
dir3`
d := NewTestDispenser(input)
if val := d.Val(); val != "" {
t.Fatalf("Val(): Should return empty string when no token loaded; got '%s'", val)
}
assertNext := func(shouldLoad bool, expectedCursor int, expectedVal string) {
if loaded := d.Next(); loaded != shouldLoad {
t.Errorf("Next(): Expected %v but got %v instead (val '%s')", shouldLoad, loaded, d.Val())
}
if d.cursor != expectedCursor {
t.Errorf("Expected cursor to be %d, but was %d", expectedCursor, d.cursor)
}
if d.nesting != 0 {
t.Errorf("Nesting should be 0, was %d instead", d.nesting)
}
if val := d.Val(); val != expectedVal {
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
}
}
assertNext(true, 0, "host:port")
assertNext(true, 1, "dir1")
assertNext(true, 2, "arg1")
assertNext(true, 3, "dir2")
assertNext(true, 4, "arg2")
assertNext(true, 5, "arg3")
assertNext(true, 6, "dir3")
// Note: This next test simply asserts existing behavior.
// If desired, we may wish to empty the token value after
// reading past the EOF. Open an issue if you want this change.
assertNext(false, 6, "dir3")
}
func TestDispenser_NextArg(t *testing.T) {
input := `dir1 arg1
dir2 arg2 arg3
dir3`
d := NewTestDispenser(input)
assertNext := func(shouldLoad bool, expectedVal string, expectedCursor int) {
if d.Next() != shouldLoad {
t.Errorf("Next(): Should load token but got false instead (val: '%s')", d.Val())
}
if d.cursor != expectedCursor {
t.Errorf("Next(): Expected cursor to be at %d, but it was %d", expectedCursor, d.cursor)
}
if val := d.Val(); val != expectedVal {
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
}
}
assertNextArg := func(expectedVal string, loadAnother bool, expectedCursor int) {
if !d.NextArg() {
t.Error("NextArg(): Should load next argument but got false instead")
}
if d.cursor != expectedCursor {
t.Errorf("NextArg(): Expected cursor to be at %d, but it was %d", expectedCursor, d.cursor)
}
if val := d.Val(); val != expectedVal {
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
}
if !loadAnother {
if d.NextArg() {
t.Fatalf("NextArg(): Should NOT load another argument, but got true instead (val: '%s')", d.Val())
}
if d.cursor != expectedCursor {
t.Errorf("NextArg(): Expected cursor to remain at %d, but it was %d", expectedCursor, d.cursor)
}
}
}
assertNext(true, "dir1", 0)
assertNextArg("arg1", false, 1)
assertNext(true, "dir2", 2)
assertNextArg("arg2", true, 3)
assertNextArg("arg3", false, 4)
assertNext(true, "dir3", 5)
assertNext(false, "dir3", 5)
}
func TestDispenser_NextLine(t *testing.T) {
input := `host:port
dir1 arg1
dir2 arg2 arg3`
d := NewTestDispenser(input)
assertNextLine := func(shouldLoad bool, expectedVal string, expectedCursor int) {
if d.NextLine() != shouldLoad {
t.Errorf("NextLine(): Should load token but got false instead (val: '%s')", d.Val())
}
if d.cursor != expectedCursor {
t.Errorf("NextLine(): Expected cursor to be %d, instead was %d", expectedCursor, d.cursor)
}
if val := d.Val(); val != expectedVal {
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
}
}
assertNextLine(true, "host:port", 0)
assertNextLine(true, "dir1", 1)
assertNextLine(false, "dir1", 1)
d.Next() // arg1
assertNextLine(true, "dir2", 3)
assertNextLine(false, "dir2", 3)
d.Next() // arg2
assertNextLine(false, "arg2", 4)
d.Next() // arg3
assertNextLine(false, "arg3", 5)
}
func TestDispenser_NextBlock(t *testing.T) {
input := `foobar1 {
sub1 arg1
sub2
}
foobar2 {
}`
d := NewTestDispenser(input)
assertNextBlock := func(shouldLoad bool, expectedCursor, expectedNesting int) {
if loaded := d.NextBlock(0); loaded != shouldLoad {
t.Errorf("NextBlock(): Should return %v but got %v", shouldLoad, loaded)
}
if d.cursor != expectedCursor {
t.Errorf("NextBlock(): Expected cursor to be %d, was %d", expectedCursor, d.cursor)
}
if d.nesting != expectedNesting {
t.Errorf("NextBlock(): Nesting should be %d, not %d", expectedNesting, d.nesting)
}
}
assertNextBlock(false, -1, 0)
d.Next() // foobar1
assertNextBlock(true, 2, 1)
assertNextBlock(true, 3, 1)
assertNextBlock(true, 4, 1)
assertNextBlock(false, 5, 0)
d.Next() // foobar2
assertNextBlock(false, 8, 0) // empty block is as if it didn't exist
}
func TestDispenser_Args(t *testing.T) {
var s1, s2, s3 string
input := `dir1 arg1 arg2 arg3
dir2 arg4 arg5
dir3 arg6 arg7
dir4`
d := NewTestDispenser(input)
d.Next() // dir1
// As many strings as arguments
if all := d.Args(&s1, &s2, &s3); !all {
t.Error("Args(): Expected true, got false")
}
if s1 != "arg1" {
t.Errorf("Args(): Expected s1 to be 'arg1', got '%s'", s1)
}
if s2 != "arg2" {
t.Errorf("Args(): Expected s2 to be 'arg2', got '%s'", s2)
}
if s3 != "arg3" {
t.Errorf("Args(): Expected s3 to be 'arg3', got '%s'", s3)
}
d.Next() // dir2
// More strings than arguments
if all := d.Args(&s1, &s2, &s3); all {
t.Error("Args(): Expected false, got true")
}
if s1 != "arg4" {
t.Errorf("Args(): Expected s1 to be 'arg4', got '%s'", s1)
}
if s2 != "arg5" {
t.Errorf("Args(): Expected s2 to be 'arg5', got '%s'", s2)
}
if s3 != "arg3" {
t.Errorf("Args(): Expected s3 to be unchanged ('arg3'), instead got '%s'", s3)
}
// (quick cursor check just for kicks and giggles)
if d.cursor != 6 {
t.Errorf("Cursor should be 6, but is %d", d.cursor)
}
d.Next() // dir3
// More arguments than strings
if all := d.Args(&s1); !all {
t.Error("Args(): Expected true, got false")
}
if s1 != "arg6" {
t.Errorf("Args(): Expected s1 to be 'arg6', got '%s'", s1)
}
d.Next() // dir4
// No arguments or strings
if all := d.Args(); !all {
t.Error("Args(): Expected true, got false")
}
// No arguments but at least one string
if all := d.Args(&s1); all {
t.Error("Args(): Expected false, got true")
}
}
func TestDispenser_RemainingArgs(t *testing.T) {
input := `dir1 arg1 arg2 arg3
dir2 arg4 arg5
dir3 arg6 { arg7
dir4`
d := NewTestDispenser(input)
d.Next() // dir1
args := d.RemainingArgs()
if expected := []string{"arg1", "arg2", "arg3"}; !reflect.DeepEqual(args, expected) {
t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args)
}
d.Next() // dir2
args = d.RemainingArgs()
if expected := []string{"arg4", "arg5"}; !reflect.DeepEqual(args, expected) {
t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args)
}
d.Next() // dir3
args = d.RemainingArgs()
if expected := []string{"arg6"}; !reflect.DeepEqual(args, expected) {
t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args)
}
d.Next() // {
d.Next() // arg7
d.Next() // dir4
args = d.RemainingArgs()
if len(args) != 0 {
t.Errorf("RemainingArgs(): Expected %v, got %v", []string{}, args)
}
}
func TestDispenser_RemainingArgsAsTokens(t *testing.T) {
input := `dir1 arg1 arg2 arg3
dir2 arg4 arg5
dir3 arg6 { arg7
dir4`
d := NewTestDispenser(input)
d.Next() // dir1
args := d.RemainingArgsAsTokens()
tokenTexts := make([]string, 0, len(args))
for _, arg := range args {
tokenTexts = append(tokenTexts, arg.Text)
}
if expected := []string{"arg1", "arg2", "arg3"}; !reflect.DeepEqual(tokenTexts, expected) {
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
}
d.Next() // dir2
args = d.RemainingArgsAsTokens()
tokenTexts = tokenTexts[:0]
for _, arg := range args {
tokenTexts = append(tokenTexts, arg.Text)
}
if expected := []string{"arg4", "arg5"}; !reflect.DeepEqual(tokenTexts, expected) {
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
}
d.Next() // dir3
args = d.RemainingArgsAsTokens()
tokenTexts = tokenTexts[:0]
for _, arg := range args {
tokenTexts = append(tokenTexts, arg.Text)
}
if expected := []string{"arg6"}; !reflect.DeepEqual(tokenTexts, expected) {
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
}
d.Next() // {
d.Next() // arg7
d.Next() // dir4
args = d.RemainingArgsAsTokens()
tokenTexts = tokenTexts[:0]
for _, arg := range args {
tokenTexts = append(tokenTexts, arg.Text)
}
if len(args) != 0 {
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", []string{}, tokenTexts)
}
}
func TestDispenser_ArgErr_Err(t *testing.T) {
input := `dir1 {
}
dir2 arg1 arg2`
d := NewTestDispenser(input)
d.cursor = 1 // {
if err := d.ArgErr(); err == nil || !strings.Contains(err.Error(), "{") {
t.Errorf("ArgErr(): Expected an error message with { in it, but got '%v'", err)
}
d.cursor = 5 // arg2
if err := d.ArgErr(); err == nil || !strings.Contains(err.Error(), "arg2") {
t.Errorf("ArgErr(): Expected an error message with 'arg2' in it; got '%v'", err)
}
err := d.Err("foobar")
if err == nil {
t.Fatalf("Err(): Expected an error, got nil")
}
if !strings.Contains(err.Error(), "Testfile:3") {
t.Errorf("Expected error message with filename:line in it; got '%v'", err)
}
if !strings.Contains(err.Error(), "foobar") {
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
}
ErrBarIsFull := errors.New("bar is full")
bookingError := d.Errf("unable to reserve: %w", ErrBarIsFull)
if !errors.Is(bookingError, ErrBarIsFull) {
t.Errorf("Errf(): should be able to unwrap the error chain")
}
}
caddy-2.11.2/caddyconfig/caddyfile/formatter.go 0000664 0000000 0000000 00000016322 15173660016 0021422 0 ustar 00root root 0000000 0000000 // Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"bytes"
"io"
"slices"
"strings"
"unicode"
)
// Format formats the input Caddyfile to a standard, nice-looking
// appearance. It works by reading each rune of the input and taking
// control over all the bracing and whitespace that is written; otherwise,
// words, comments, placeholders, and escaped characters are all treated
// literally and written as they appear in the input.
func Format(input []byte) []byte {
input = bytes.TrimSpace(input)
out := new(bytes.Buffer)
rdr := bytes.NewReader(input)
type heredocState int
const (
heredocClosed heredocState = 0
heredocOpening heredocState = 1
heredocOpened heredocState = 2
)
var (
last rune // the last character that was written to the result
space = true // whether current/previous character was whitespace (beginning of input counts as space)
beginningOfLine = true // whether we are at beginning of line
openBrace bool // whether current word/token is or started with open curly brace
openBraceWritten bool // if openBrace, whether that brace was written or not
openBraceSpace bool // whether there was a non-newline space before open brace
newLines int // count of newlines consumed
comment bool // whether we're in a comment
quotes string // encountered quotes ('', '`', '"', '"`', '`"')
escaped bool // whether current char is escaped
heredoc heredocState // whether we're in a heredoc
heredocEscaped bool // whether heredoc is escaped
heredocMarker []rune
heredocClosingMarker []rune
nesting int // indentation level
)
write := func(ch rune) {
out.WriteRune(ch)
last = ch
}
indent := func() {
for tabs := nesting; tabs > 0; tabs-- {
write('\t')
}
}
nextLine := func() {
write('\n')
beginningOfLine = true
}
for {
ch, _, err := rdr.ReadRune()
if err != nil {
if err == io.EOF {
break
}
panic(err)
}
// detect whether we have the start of a heredoc
if quotes == "" && (heredoc == heredocClosed && !heredocEscaped) &&
space && last == '<' && ch == '<' {
write(ch)
heredoc = heredocOpening
space = false
continue
}
if heredoc == heredocOpening {
if ch == '\n' {
if len(heredocMarker) > 0 && heredocMarkerRegexp.MatchString(string(heredocMarker)) {
heredoc = heredocOpened
} else {
heredocMarker = nil
heredoc = heredocClosed
nextLine()
continue
}
write(ch)
continue
}
if unicode.IsSpace(ch) {
// a space means it's just a regular token and not a heredoc
heredocMarker = nil
heredoc = heredocClosed
} else {
heredocMarker = append(heredocMarker, ch)
write(ch)
continue
}
}
// if we're in a heredoc, all characters are read&write as-is
if heredoc == heredocOpened {
heredocClosingMarker = append(heredocClosingMarker, ch)
if len(heredocClosingMarker) > len(heredocMarker)+1 { // We assert that the heredocClosingMarker is followed by a unicode.Space
heredocClosingMarker = heredocClosingMarker[1:]
}
// check if we're done
if unicode.IsSpace(ch) && slices.Equal(heredocClosingMarker[:len(heredocClosingMarker)-1], heredocMarker) {
heredocMarker = nil
heredocClosingMarker = nil
heredoc = heredocClosed
} else {
write(ch)
if ch == '\n' {
heredocClosingMarker = heredocClosingMarker[:0]
}
continue
}
}
if last == '<' && space {
space = false
}
if comment {
if ch == '\n' {
comment = false
space = true
nextLine()
continue
} else {
write(ch)
continue
}
}
if !escaped && ch == '\\' {
if space {
write(' ')
space = false
}
write(ch)
escaped = true
continue
}
if escaped {
if ch == '<' {
heredocEscaped = true
}
write(ch)
escaped = false
continue
}
if ch == '`' {
switch quotes {
case "\"`":
quotes = "\""
case "`":
quotes = ""
case "\"":
quotes = "\"`"
default:
quotes = "`"
}
}
if quotes == "\"" {
if ch == '"' {
quotes = ""
}
write(ch)
continue
}
if ch == '"' {
switch quotes {
case "":
if space {
quotes = "\""
}
case "`\"":
quotes = "`"
case "\"`":
quotes = ""
}
}
if strings.Contains(quotes, "`") {
if ch == '`' && space && !beginningOfLine {
write(' ')
}
write(ch)
space = false
continue
}
if unicode.IsSpace(ch) {
space = true
heredocEscaped = false
if ch == '\n' {
newLines++
}
continue
}
spacePrior := space
space = false
//////////////////////////////////////////////////////////
// I find it helpful to think of the formatting loop in two
// main sections; by the time we reach this point, we
// know we are in a "regular" part of the file: we know
// the character is not a space, not in a literal segment
// like a comment or quoted, it's not escaped, etc.
//////////////////////////////////////////////////////////
if ch == '#' {
comment = true
}
if openBrace && spacePrior && !openBraceWritten {
if nesting == 0 && last == '}' {
nextLine()
nextLine()
}
openBrace = false
if beginningOfLine {
indent()
} else if !openBraceSpace || !unicode.IsSpace(last) {
write(' ')
}
write('{')
openBraceWritten = true
nextLine()
newLines = 0
// prevent infinite nesting from ridiculous inputs (issue #4169)
if nesting < 10 {
nesting++
}
}
switch {
case ch == '{':
openBrace = true
openBraceSpace = spacePrior && !beginningOfLine
if openBraceSpace && newLines == 0 {
write(' ')
}
openBraceWritten = false
if quotes == "`" {
write('{')
openBraceWritten = true
continue
}
continue
case ch == '}' && (spacePrior || !openBrace):
if quotes == "`" {
write('}')
continue
}
if last != '\n' {
nextLine()
}
if nesting > 0 {
nesting--
}
indent()
write('}')
newLines = 0
continue
}
if newLines > 2 {
newLines = 2
}
for i := 0; i < newLines; i++ {
nextLine()
}
newLines = 0
if beginningOfLine {
indent()
}
if nesting == 0 && last == '}' && beginningOfLine {
nextLine()
nextLine()
}
if !beginningOfLine && spacePrior {
write(' ')
}
if openBrace && !openBraceWritten {
write('{')
openBraceWritten = true
}
if spacePrior && ch == '<' {
space = true
}
write(ch)
beginningOfLine = false
}
// the Caddyfile does not need any leading or trailing spaces, but...
trimmedResult := bytes.TrimSpace(out.Bytes())
// ...Caddyfiles should, however, end with a newline because
// newlines are significant to the syntax of the file
return append(trimmedResult, '\n')
}
caddy-2.11.2/caddyconfig/caddyfile/formatter_fuzz.go 0000664 0000000 0000000 00000001444 15173660016 0022477 0 ustar 00root root 0000000 0000000 // 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.
//go:build gofuzz
package caddyfile
import "bytes"
func FuzzFormat(input []byte) int {
formatted := Format(input)
if bytes.Equal(formatted, Format(formatted)) {
return 1
}
return 0
}
caddy-2.11.2/caddyconfig/caddyfile/formatter_test.go 0000664 0000000 0000000 00000015124 15173660016 0022460 0 ustar 00root root 0000000 0000000 // Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"strings"
"testing"
)
func TestFormatter(t *testing.T) {
for i, tc := range []struct {
description string
input string
expect string
}{
{
description: "very simple",
input: `abc def
g hi jkl
mn`,
expect: `abc def
g hi jkl
mn`,
},
{
description: "basic indentation, line breaks, and nesting",
input: ` a
b
c {
d
}
e { f
}
g {
h {
i
}
}
j { k {
l
}
}
m {
n { o
}
p { q r
s }
}
{
{ t
u
v
w
}
}`,
expect: `a
b
c {
d
}
e {
f
}
g {
h {
i
}
}
j {
k {
l
}
}
m {
n {
o
}
p {
q r
s
}
}
{
{
t
u
v
w
}
}`,
},
{
description: "block spacing",
input: `a{
b
}
c{ d
}`,
expect: `a {
b
}
c {
d
}`,
},
{
description: "advanced spacing",
input: `abc {
def
}ghi{
jkl mno
pqr}`,
expect: `abc {
def
}
ghi {
jkl mno
pqr
}`,
},
{
description: "env var placeholders",
input: `{$A}
b {
{$C}
}
d { {$E}
}
{ {$F}
}
`,
expect: `{$A}
b {
{$C}
}
d {
{$E}
}
{
{$F}
}`,
},
{
description: "env var placeholders with port",
input: `:{$PORT}`,
expect: `:{$PORT}`,
},
{
description: "comments",
input: `#a "\n"
#b {
c
}
d {
e#f
# g
}
h { # i
}`,
expect: `#a "\n"
#b {
c
}
d {
e#f
# g
}
h {
# i
}`,
},
{
description: "quotes and escaping",
input: `"a \"b\" "#c
d
e {
"f"
}
g { "h"
}
i {
"foo
bar"
}
j {
"\"k\" l m"
}`,
expect: `"a \"b\" "#c
d
e {
"f"
}
g {
"h"
}
i {
"foo
bar"
}
j {
"\"k\" l m"
}`,
},
{
description: "bad nesting (too many open)",
input: `a
{
{
}`,
expect: `a {
{
}
`,
},
{
description: "bad nesting (too many close)",
input: `a
{
{
}}}`,
expect: `a {
{
}
}
}
`,
},
{
description: "json",
input: `foo
bar "{\"key\":34}"
`,
expect: `foo
bar "{\"key\":34}"`,
},
{
description: "escaping after spaces",
input: `foo \"literal\"`,
expect: `foo \"literal\"`,
},
{
description: "simple placeholders as standalone tokens",
input: `foo {bar}`,
expect: `foo {bar}`,
},
{
description: "simple placeholders within tokens",
input: `foo{bar} foo{bar}baz`,
expect: `foo{bar} foo{bar}baz`,
},
{
description: "placeholders and malformed braces",
input: `foo{bar} foo{ bar}baz`,
expect: `foo{bar} foo {
bar
}
baz`,
},
{
description: "hash within string is not a comment",
input: `redir / /some/#/path`,
expect: `redir / /some/#/path`,
},
{
description: "brace does not fold into comment above",
input: `# comment
{
foo
}`,
expect: `# comment
{
foo
}`,
},
{
description: "matthewpi/vscode-caddyfile-support#13",
input: `{
email {$ACMEEMAIL}
#debug
}
block {
}
`,
expect: `{
email {$ACMEEMAIL}
#debug
}
block {
}
`,
},
{
description: "matthewpi/vscode-caddyfile-support#13 - bad formatting",
input: `{
email {$ACMEEMAIL}
#debug
}
block {
}
`,
expect: `{
email {$ACMEEMAIL}
#debug
}
block {
}
`,
},
{
description: "keep heredoc as-is",
input: `block {
heredoc <