Compare commits

...

65 Commits

Author SHA1 Message Date
Matt Holt 6bad878a22 httpcaddyfile: Improve detection of indistinguishable TLS automation policies (#5120)
* httpcaddyfile: Skip some logic if auto_https off

* Try removing this check altogether...

* Refine test timeouts slightly, sigh

* caddyhttp: Assume udp for unrecognized network type

Seems like the reasonable thing to do if a plugin registers its own
network type.

* Add comment to document my lack of knowledge

* Clean up and prepare to merge

Add comments to try to explain what happened
2022-10-13 11:30:57 -06:00
Matt Holt 3e1fd2a8d4 httpcaddyfile: Wrap site block in subroute if host matcher used (#5130)
* httpcaddyfile: Wrap site block in subroute if host matcher used (fix #5124)

* Correct boolean logic (oops)
2022-10-12 09:27:08 -06:00
Abdussamet Koçak 33f60da9f2 fileserver: stop listing dir when request context is cancelled (#5131)
Prevents caddy from performing disk IO needlessly when the request is cancelled before the listing is finished.

Closes #5129
2022-10-08 12:56:35 -06:00
Kévin Dunglas b4e28af953 replacer: working directory global placeholder (#5127) 2022-10-07 05:54:41 -04:00
Francis Lavoie d46ba2e27f httpcaddyfile: Fix metrics global option parsing (#5126) 2022-10-06 19:40:08 -06:00
Cory Cooper 498f32bab9 caddyconfig: Implement retries into HTTPLoader (#5077)
* httploader: Add max_retries

* caddyconfig: dependency-free http config loading retries

* caddyconfig: support `retry_delay` in http loader

* httploader: Implement retries

* Apply suggestions from code review

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-10-05 22:34:49 -06:00
Ioannis Cherouvim ed118f2b09 Fix typo in comment (#5121) 2022-10-05 12:36:06 -06:00
Francis Lavoie 99ffe93388 logging: Fix skip_hosts with wildcards (#5102)
Fix #4859
2022-10-05 12:14:13 -06:00
Matthew Holt e07a267276 caddytest: Revise sleep durations
Attempt to reduce flakiness a bit more

Test suite needs to be rewritten.
2022-10-05 11:40:41 -06:00
Adam Weinberger e4fac1294f core: Set version manually via CustomVersion (#5072)
* Allow version to be set manually

When Caddy is built from a release tarball (as downloaded from GitHub),
`caddy version` returns an empty string. This causes confusion for
downstream packagers.

With this commit, VersionString can be set with eg.
  go build (...) -ldflags '-X (...).VersionString=v1.2.3'
Then the short form version will be "v1.2.3", and the full version
string will begin with "v1.2.3 ".

* Prefer embedded version, then CustomVersion

* Prefer "unknown" for full version over empty

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2022-10-05 10:59:57 -06:00
Matt Holt 2153a81ec8 forwardauth: Canonicalize header fields (fix #5038) (#5097) 2022-10-05 01:37:01 -04:00
Francis Lavoie ea58d51907 logging: Perform filtering on arrays of strings (where possible) (#5101)
* logging: Perform filtering on arrays of strings (where possible)

* Add test for ip_mask filter

* Oops, need to continue when it's not an IP

* Test for invalid IPs
2022-10-04 23:21:23 -06:00
Francis Lavoie 9e1d964bd6 logging: Add time_local option to use local time instead of UTC (#5108) 2022-10-05 00:23:14 -04:00
xufanglu 2be56c526c fileserver: Treat invalid file path as NotFound (#5099)
treat invalid file path as notFound so that PassThru can work
2022-10-04 21:32:40 -06:00
Francis Lavoie 01e192edc9 logging: Better console encoder defaults (#5109)
This is something that has bothered me for a while, so I figured I'd do something about it now since I'm playing in the logging code lately.

The `console` encoder doesn't actually match the defaults that zap's default logger uses. This makes it match better with the rest of the logs when using the `console` encoder alongside somekind of filter, which requires you to configure an encoder to wrap.
2022-10-04 21:18:48 -06:00
Francis Lavoie 2808de1e30 httpcaddyfile: Skip automate when auto_https off is specified (#5110) 2022-10-04 20:58:19 -06:00
Tobias Gruetzmacher 253d97c93d core: Chdir to executable location on Windows (#5115)
Since all Windows services are run from the Windows system directory,
make it easier for users by switching to our program directory right
after the start.
2022-10-04 11:04:02 -06:00
Mohammed Al Sahaf c28cd29fe7 ci: enhance the CI/CD flow (#5118) 2022-10-04 17:03:10 +03:00
Tobias Gruetzmacher da24f57dac Fix inverted logic in Windows service detection (#5106) 2022-10-02 16:56:54 -04:00
iliana etaoin b1d04f5b39 fileserver: better dark mode visited link contrast (#5105)
PR #4066 added a dark color scheme to the file_server browse template.
PR #4356 later set the links for the `:visited` pseudo-class, but did
not set anything for the dark mode, resulting in poor contrast. I
selected some new colors by feel.

This commit also adds an `a:visited:hover` for both, to go along with
the normal blue hover colors.
2022-10-01 18:14:27 -06:00
Matthew Holt fe91de67b6 go.mod: Upgrade select dependencies 2022-09-30 13:39:37 -06:00
Matthew Holt 9873ff9918 caddyhttp: Remote IP prefix placeholders
See https://github.com/mholt/caddy-ratelimit/issues/12
2022-09-30 13:29:33 -06:00
Matt Holt 5e52bbb136 map: Remove infinite recursion check (#5094)
It was not accurate. Placeholders could be used in outputs that are
defined in the same mapping as long as that placeholder does not do the
same.

A more general solution would be to detect it at run-time in the
replacer directly, but that's a bit tedious
and will require allocations I think.

A better implementation of this check could still be done, but I don't
know if it would always be accurate. Could be a "best-effort" thing?
But I've also never heard of an actual case where someone configured
infinite recursion...
2022-09-29 12:46:38 -06:00
Matthew Holt fcdbc69fab Fix comment
I apparently read the diff backwards in
2a8c458ffe
2022-09-29 12:38:36 -06:00
Matthew Holt 2a8c458ffe reverseproxy: Parse humanized byte size (fix #5095) 2022-09-29 12:37:06 -06:00
Cory Cooper 037dc23cad admin: Use replacer on listen addresses (#5071)
* admin: use replacer on listen address

* admin: consolidate replacer logic
2022-09-29 11:24:52 -06:00
Matthew Holt ab720fb768 core: Fix ListenQUIC listener key conflict
Reported on commit e3e8aabbcf

Abused this change in some bash for loops to rapidly reload config
while making requests and didn't observe any memory or resource leaks.
2022-09-29 10:32:02 -06:00
Matt Holt e2991eb019 reverseproxy: On 103 don't delete own headers (#5091)
See #5074
2022-09-29 08:19:56 -06:00
Matt Holt 897a38958c Merge pull request #5076 from caddyserver/fastcgi-redir
fastcgi: Redirect using original URI path (fix #5073) and rewrite: Only trim prefix if matched
2022-09-28 15:22:45 -06:00
Will Norris 61822f129b caddyhttp: replace placeholders in map defaults (#5081)
This updates the map directive to replace placeholders in default values
in the same way as matched values.
2022-09-28 13:38:20 -06:00
Matt Holt e3e8aabbcf core: Refactor and improve listener logic (#5089)
* core: Refactor, improve listener logic

Deprecate:
- caddy.Listen
- caddy.ListenTimeout
- caddy.ListenPacket

Prefer caddy.NetworkAddress.Listen() instead.

Change:
- caddy.ListenQUIC (hopefully to remove later)
- caddy.ListenerFunc signature (add context and ListenConfig)

- Don't emit Alt-Svc header advertising h3 over HTTP/3

- Use quic.ListenEarly instead of quic.ListenEarlyAddr; this gives us
more flexibility (e.g. possibility of HTTP/3 over UDS) but also
introduces a new issue:
https://github.com/lucas-clemente/quic-go/issues/3560#issuecomment-1258959608

- Unlink unix socket before and after use

* Appease the linter

* Keep ListenAll
2022-09-28 13:35:51 -06:00
Matthew Holt 013b510352 rewrite: Only trim prefix if matched
See #5073
2022-09-28 00:13:12 -06:00
lemmi d0556929a4 reverseproxy: fix upstream scheme handling in command (#5088)
e338648fed introduced multiple upstream
addresses. A comment notes that mixing schemes isn't supported and
therefore the first valid scheme is supposed to be used.

Fixes setting the first scheme.

fixes #5087
2022-09-27 13:03:30 -06:00
Mohammed Al Sahaf b5727b9c44 ci: fix integration tests (#5079) 2022-09-24 19:00:55 +00:00
Matthew Holt 7041970059 headers: Support repeated WriteHeader if 1xx (fix #5074) 2022-09-23 17:11:53 -06:00
Matthew Holt e747a9bb12 Fix tests 2022-09-23 16:47:59 -06:00
Matthew Holt f7c1a51efb fastcgi: Redirect using original URI path (fix #5073) 2022-09-23 14:36:38 -06:00
Mohammed Al Sahaf eead00f54a ci: extend goreleaser timeout to 1-hour (#5067) 2022-09-22 15:09:18 +00:00
Matthew Holt 9206e8a738 Tweak some comments 2022-09-21 12:59:44 -06:00
Matt Holt 1426c97da5 core: Reuse unix sockets (UDS) and don't try to serve HTTP/3 over UDS (#5063)
* core: Reuse unix sockets

* Don't serve HTTP/3 over unix sockets

This requires upstream support, if even useful

* Don't use unix build tag... yet

* Fix build tag

* Allow ErrNotExist when unlinking socket
2022-09-21 12:55:23 -06:00
WeidiDeng 44ad0cedaf encode: don't WriteHeader unless called (#5060) 2022-09-21 08:30:42 -06:00
Matthew Holt beb7dcbf2a fileserver: Reinstate --debug flag
I think it got lost during a rebase or something
2022-09-20 16:56:02 -06:00
Francis Lavoie 821a08a6e3 httpcaddyfile: Fix protocols global option parsing (#5054)
* httpcaddyfile: Fix `protocols` global option parsing

When checking for a block, the current nesting must be used, otherwise it returns the wrong thing.

* Adjust adapt test to cover the broken behaviour that is now fixed

* Fix some admin tests which suddenly run even with -short
2022-09-20 08:09:04 -06:00
Francis Lavoie e3d04ff86b caddyhttp: Skip inserting HTTP->HTTPS redir if catch-all for both exist (#5051) 2022-09-19 22:11:19 -06:00
Matt Holt da8b7fe58f caddyhttp: Honor grace period in background (#5043)
* caddyhttp: Honor grace period in background

This avoids blocking during config reloads.

* Don't quit process until servers shut down

* Make tests more likely to pass on fast CI (#5045)

* caddyhttp: Even faster shutdowns

Simultaneously shut down all HTTP servers, rather than one at a time.

In practice there usually won't be more than 1 that lingers. But this
code ensures that they all Shutdown() in their own goroutine
and then we wait for them at the end (if exiting).

We also wait for them to start up so we can be fairly confident the
shutdowns have begun; i.e. old servers no longer
accepting new connections.

* Fix comment typo

* Pull functions out of loop, for readability
2022-09-19 21:54:47 -06:00
Matthew Holt 0950ba4f0b events: Make event data exported
This could lead to bugs if handlers are not careful, but it is surely
useful. We'll see how it goes, what the feedback is like, etc.
2022-09-19 16:20:58 -06:00
WeidiDeng c7a6bc5934 caddyhttp: responseRecorder save status in all cases (#5049) 2022-09-17 18:47:53 -06:00
Matthew Holt 00beec2e34 caddyhttp: Fix write header on responseRecorder 2022-09-17 11:28:13 -06:00
Mohammed Al Sahaf b4643994d5 ci: fix the name template of singing certificate and sboms (#5046) 2022-09-17 08:54:50 -06:00
Matthew Holt e43b6d8178 core: Variadic Context.Logger(); soft deprecation
Ideally I'd just remove the parameter to caddy.Context.Logger(), but
this would break most Caddy plugins.

Instead, I'm making it variadic and marking it as partially deprecated.
In the future, I might completely remove the parameter once most
plugins have updated.
2022-09-16 16:55:36 -06:00
WeidiDeng bffc258732 caddyhttp: Support configuring Server from handler provisioning (#4933)
* configuring http.Server from handlers.

* Minor tweaks

* Run gofmt

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2022-09-16 14:48:55 -06:00
David Manouchehri 616418281b caddyhttp: Support TLS key logging for debugging (#4808)
* Add SSL key logging.

* Resolve merge conflict with master

* Add Caddyfile support; various fixes

* Also commit go.mod and go.sum, oops

* Appease linter

* Minor tweaks

* Add doc comment

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-09-16 14:05:37 -06:00
Matt Holt 74547f5bed caddyhttp: Make metrics opt-in (#5042)
* caddyhttp: Make metrics opt-in

Related to #4644

* Make configurable in Caddyfile
2022-09-16 13:32:49 -06:00
Matthew Holt 258071d857 caddytls: Debug log on implicit tailscale error (#5041) 2022-09-16 09:42:05 -06:00
Matthew Holt b6cec37893 caddyhttp: Add --debug flag to commands
file-server and reverse-proxy

This might be useful!
2022-09-15 23:10:16 -06:00
WeidiDeng 48d723c07c encode: Fix Accept-Ranges header; HEAD requests (#5039)
* fix encode handler header manipulation
also avoid implementing ReadFrom because it breaks when io.Copied to directly

* strconv.Itoa should be tried as a last resort
WriteHeader during Close
2022-09-15 16:05:08 -06:00
Matthew Holt f1f7a22674 Reject absurdly long duration strings (fix #4175) 2022-09-15 14:25:29 -06:00
Matthew Holt 49b7a25264 Fix #4169 (correct e6c58fd) 2022-09-15 14:13:58 -06:00
Matthew Holt e6c58fdc08 caddyfile: Prevent infinite nesting on fmt (fix #4175) 2022-09-15 14:12:53 -06:00
Matthew Holt 2dc747cf2d Limit unclosed placeholder tolerance (fix #4170) 2022-09-15 13:36:08 -06:00
Isaac Parker e338648fed reverseproxy: Support repeated --to flags in command (#4693)
* feat: Multiple 'to' upstreams in reverse-proxy cmd

* Repeat --to for multiple upstreams, rather than comma-separating in a single flag

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-09-15 12:35:38 -06:00
Francis Lavoie 9ad0ebc956 caddyhttp: Add 'skip_log' var to omit request from logs (#4691)
* caddyhttp: Implement `skip_log` handler

* Refactor to use vars middleware

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-09-15 10:05:36 -06:00
Michael Stapelberg a1ad20e472 httpcaddyfile: Fix bind when IPv6 is specified with network (#4950)
* fix listening on IPv6 addresses: use net.JoinHostPort

Commit 1e18afb5c8 broke my caddy setup.
This commit fixes it.

* Refactor solution; simplify, add descriptive comment

* Move network to host, not copy

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2022-09-15 08:03:24 -06:00
Matthew Holt 62b0685375 cmd: Improve error message if config missing 2022-09-14 23:24:16 -06:00
Matthew Holt 0b3161aeea cmd: Customizable user agent (close #2795) 2022-09-13 17:21:04 -06:00
100 changed files with 2544 additions and 1588 deletions
+4 -3
View File
@@ -156,17 +156,18 @@ jobs:
short_sha=$(git rev-parse --short HEAD)
# The environment is fresh, so there's no point in keeping accepting and adding the key.
rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . caddy-ci@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t caddy-ci@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -v ./..."
rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . "$CI_USER"@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$CI_USER"@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -v ./..."
test_result=$?
# There's no need leaving the files around
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null caddy-ci@ci-s390x.caddyserver.com "rm -rf /var/tmp/'$short_sha'"
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$CI_USER"@ci-s390x.caddyserver.com "rm -rf /var/tmp/'$short_sha'"
echo "Test exit code: $test_result"
exit $test_result
env:
SSH_KEY: ${{ secrets.S390X_SSH_KEY }}
CI_USER: ${{ secrets.CI_USER }}
goreleaser-check:
runs-on: ubuntu-latest
+1 -1
View File
@@ -119,7 +119,7 @@ jobs:
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --rm-dist
args: release --rm-dist --timeout 60m
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.vars.outputs.version_tag }}
+4 -4
View File
@@ -4,6 +4,7 @@ before:
# This is so we can run goreleaser on tag without Git complaining of being dirty. The main.go in cmd/caddy directory
# cannot be built within that directory due to changes necessary for the build causing Git to be dirty, which
# subsequently causes gorleaser to refuse running.
- rm -rf caddy-build caddy-dist
- mkdir -p caddy-build
- cp cmd/caddy/main.go caddy-build/main.go
- /bin/sh -c 'cd ./caddy-build && go mod init caddy'
@@ -68,14 +69,13 @@ builds:
signs:
- cmd: cosign
signature: "${artifact}.sig"
certificate: '{{ trimsuffix .Env.artifact ".tar.gz" }}.pem'
certificate: '{{ trimsuffix (trimsuffix .Env.artifact ".zip") ".tar.gz" }}.pem'
args: ["sign-blob", "--output-signature=${signature}", "--output-certificate", "${certificate}", "${artifact}"]
artifacts: all
sboms:
- artifacts: binary
# defaults to
# documents:
# - "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.sbom"
documents:
- '{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{if .Arm}}v{{ .Arm }}{{end}}.sbom'
cmd: syft
args: ["$artifact", "--file", "${document}", "--output", "cyclonedx-json"]
archives:
+10 -6
View File
@@ -57,7 +57,7 @@ type AdminConfig struct {
// The address to which the admin endpoint's listener should
// bind itself. Can be any single network address that can be
// parsed by Caddy. Default: localhost:2019
// parsed by Caddy. Accepts placeholders. Default: localhost:2019
Listen string `json:"listen,omitempty"`
// If true, CORS headers will be emitted, and requests to the
@@ -156,7 +156,7 @@ type IdentityConfig struct {
//
// EXPERIMENTAL: Subject to change.
type RemoteAdmin struct {
// The address on which to start the secure listener.
// The address on which to start the secure listener. Accepts placeholders.
// Default: :2021
Listen string `json:"listen,omitempty"`
@@ -382,7 +382,7 @@ func replaceLocalAdminServer(cfg *Config) error {
handler := cfg.Admin.newAdminHandler(addr, false)
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{})
if err != nil {
return err
}
@@ -403,7 +403,7 @@ func replaceLocalAdminServer(cfg *Config) error {
serverMu.Lock()
server := localAdminServer
serverMu.Unlock()
if err := server.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
if err := server.Serve(ln.(net.Listener)); !errors.Is(err, http.ErrServerClosed) {
adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
}
}()
@@ -549,10 +549,11 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
serverMu.Unlock()
// start listener
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
lnAny, err := addr.Listen(ctx, 0, net.ListenConfig{})
if err != nil {
return err
}
ln := lnAny.(net.Listener)
ln = tls.NewListener(ln, tlsConfig)
go func() {
@@ -1246,7 +1247,10 @@ func (e APIError) Error() string {
// parseAdminListenAddr extracts a singular listen address from either addr
// or defaultAddr, returning the network and the address of the listener.
func parseAdminListenAddr(addr string, defaultAddr string) (NetworkAddress, error) {
input := addr
input, err := NewReplacer().ReplaceOrErr(addr, true, true)
if err != nil {
return NetworkAddress{}, fmt.Errorf("replacing listen address: %v", err)
}
if input == "" {
input = defaultAddr
}
+1 -1
View File
@@ -161,7 +161,7 @@ func (fooModule) Stop() error { return nil }
func TestETags(t *testing.T) {
RegisterModule(fooModule{})
if err := Load([]byte(`{"apps": {"foo": {"strField": "abc", "intField": 0}}}`), true); err != nil {
if err := Load([]byte(`{"admin": {"listen": "localhost:2999"}, "apps": {"foo": {"strField": "abc", "intField": 0}}}`), true); err != nil {
t.Fatalf("loading: %s", err)
}
+49 -4
View File
@@ -31,6 +31,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/caddyserver/caddy/v2/notify"
@@ -676,6 +677,10 @@ func Validate(cfg *Config) error {
// Errors are logged along the way, and an appropriate exit
// code is emitted.
func exitProcess(ctx context.Context, logger *zap.Logger) {
// let the rest of the program know we're quitting
atomic.StoreInt32(exiting, 1)
// give the OS or service/process manager our 2 weeks' notice: we quit
if err := notify.Stopping(); err != nil {
Log().Error("unable to notify service manager of stopping state", zap.Error(err))
}
@@ -739,6 +744,12 @@ func exitProcess(ctx context.Context, logger *zap.Logger) {
}()
}
var exiting = new(int32) // accessed atomically
// Exiting returns true if the process is exiting.
// EXPERIMENTAL API: subject to change or removal.
func Exiting() bool { return atomic.LoadInt32(exiting) == 1 }
// Duration can be an integer or a string. An integer is
// interpreted as nanoseconds. If a string, it is a Go
// time.Duration value such as `300ms`, `1.5h`, or `2h45m`;
@@ -763,8 +774,12 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
// ParseDuration parses a duration string, adding
// support for the "d" unit meaning number of days,
// where a day is assumed to be 24h.
// 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++ {
@@ -809,6 +824,20 @@ func InstanceID() (uuid.UUID, error) {
return uuid.ParseBytes(uuidFileBytes)
}
// CustomVersion is an optional string that overrides Caddy's
// reported version. It can be helpful when downstream packagers
// need to manually set Caddy's version. If no other version
// information is available, the short form version (see
// Version()) will be set to CustomVersion, and the full version
// will include CustomVersion at the beginning.
//
// Set this variable during `go build` with `-ldflags`:
//
// -ldflags '-X github.com/caddyserver/caddy/v2.CustomVersion=v2.6.2'
//
// for example.
var CustomVersion string
// Version returns the Caddy version in a simple/short form, and
// a full version string. The short form will not have spaces and
// is intended for User-Agent strings and similar, but may be
@@ -818,8 +847,10 @@ func InstanceID() (uuid.UUID, error) {
// 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)" becaise Go uses that, but for
// the simple form we change it to "unknown".
// 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.
@@ -895,8 +926,22 @@ func Version() (simple, full string) {
}
}
if full == "" {
if CustomVersion != "" {
full = CustomVersion
} else {
full = "unknown"
}
} else if CustomVersion != "" {
full = CustomVersion + " " + full
}
if simple == "" || simple == "(devel)" {
simple = "unknown"
if CustomVersion != "" {
simple = CustomVersion
} else {
simple = "unknown"
}
}
return
+4 -1
View File
@@ -153,7 +153,10 @@ func Format(input []byte) []byte {
openBraceWritten = true
nextLine()
newLines = 0
nesting++
// prevent infinite nesting from ridiculous inputs (issue #4169)
if nesting < 10 {
nesting++
}
}
switch {
+24 -12
View File
@@ -36,12 +36,12 @@ import (
// server block that share the same address stay grouped together so the config
// isn't repeated unnecessarily. For example, this Caddyfile:
//
// example.com {
// bind 127.0.0.1
// }
// www.example.com, example.net/path, localhost:9999 {
// bind 127.0.0.1 1.2.3.4
// }
// example.com {
// bind 127.0.0.1
// }
// www.example.com, example.net/path, localhost:9999 {
// bind 127.0.0.1 1.2.3.4
// }
//
// has two server blocks to start with. But expressed in this Caddyfile are
// actually 4 listener addresses: 127.0.0.1:443, 1.2.3.4:443, 127.0.0.1:9999,
@@ -219,7 +219,7 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
return nil, fmt.Errorf("[%s] scheme and port violate convention", key)
}
// the bind directive specifies hosts, but is optional
// the bind directive specifies hosts (and potentially network), but is optional
lnHosts := make([]string, 0, len(sblock.pile["bind"]))
for _, cfgVal := range sblock.pile["bind"] {
lnHosts = append(lnHosts, cfgVal.Value.([]string)...)
@@ -234,11 +234,23 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
// use a map to prevent duplication
listeners := make(map[string]struct{})
for _, host := range lnHosts {
// host can have network + host (e.g. "tcp6/localhost") but
// will/should not have port information because this usually
// comes from the bind directive, so we append the port
addr, err := caddy.ParseNetworkAddress(host + ":" + lnPort)
for _, lnHost := range lnHosts {
// normally we would simply append the port,
// but if lnHost is IPv6, we need to ensure it
// is enclosed in [ ]; net.JoinHostPort does
// this for us, but lnHost might also have a
// network type in front (e.g. "tcp/") leading
// to "[tcp/::1]" which causes parsing failures
// later; what we need is "tcp/[::1]", so we have
// to split the network and host, then re-combine
network, host, ok := strings.Cut(lnHost, "/")
if !ok {
host = network
network = ""
}
host = strings.Trim(host, "[]") // IPv6
networkAddr := caddy.JoinNetworkAddress(network, host, lnPort)
addr, err := caddy.ParseNetworkAddress(networkAddr)
if err != nil {
return nil, fmt.Errorf("parsing network address: %v", err)
}
+49 -33
View File
@@ -48,12 +48,12 @@ func init() {
RegisterHandlerDirective("handle", parseHandle)
RegisterDirective("handle_errors", parseHandleErrors)
RegisterDirective("log", parseLog)
RegisterHandlerDirective("skip_log", parseSkipLog)
}
// parseBind parses the bind directive. Syntax:
//
// bind <addresses...>
//
// bind <addresses...>
func parseBind(h Helper) ([]ConfigValue, error) {
var lnHosts []string
for h.Next() {
@@ -64,28 +64,28 @@ func parseBind(h Helper) ([]ConfigValue, error) {
// parseTLS parses the tls directive. Syntax:
//
// tls [<email>|internal]|[<cert_file> <key_file>] {
// protocols <min> [<max>]
// ciphers <cipher_suites...>
// curves <curves...>
// client_auth {
// mode [request|require|verify_if_given|require_and_verify]
// trusted_ca_cert <base64_der>
// trusted_ca_cert_file <filename>
// trusted_leaf_cert <base64_der>
// trusted_leaf_cert_file <filename>
// }
// alpn <values...>
// load <paths...>
// ca <acme_ca_endpoint>
// ca_root <pem_file>
// dns <provider_name> [...]
// on_demand
// eab <key_id> <mac_key>
// issuer <module_name> [...]
// get_certificate <module_name> [...]
// }
//
// tls [<email>|internal]|[<cert_file> <key_file>] {
// protocols <min> [<max>]
// ciphers <cipher_suites...>
// curves <curves...>
// client_auth {
// mode [request|require|verify_if_given|require_and_verify]
// trusted_ca_cert <base64_der>
// trusted_ca_cert_file <filename>
// trusted_leaf_cert <base64_der>
// trusted_leaf_cert_file <filename>
// }
// alpn <values...>
// load <paths...>
// ca <acme_ca_endpoint>
// ca_root <pem_file>
// dns <provider_name> [...]
// on_demand
// eab <key_id> <mac_key>
// issuer <module_name> [...]
// get_certificate <module_name> [...]
// insecure_secrets_log <log_file>
// }
func parseTLS(h Helper) ([]ConfigValue, error) {
cp := new(caddytls.ConnectionPolicy)
var fileLoader caddytls.FileLoader
@@ -395,6 +395,12 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
}
onDemand = true
case "insecure_secrets_log":
if !h.NextArg() {
return nil, h.ArgErr()
}
cp.InsecureSecretsLog = h.Val()
default:
return nil, h.Errf("unknown subdirective: %s", h.Val())
}
@@ -515,8 +521,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
// parseRoot parses the root directive. Syntax:
//
// root [<matcher>] <path>
//
// root [<matcher>] <path>
func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) {
var root string
for h.Next() {
@@ -694,12 +699,11 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {
// parseLog parses the log directive. Syntax:
//
// log {
// output <writer_module> ...
// format <encoder_module> ...
// level <level>
// }
//
// log {
// output <writer_module> ...
// format <encoder_module> ...
// level <level>
// }
func parseLog(h Helper) ([]ConfigValue, error) {
return parseLogHelper(h, nil)
}
@@ -731,7 +735,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
// reference the default logger. See the
// setupNewDefault function in the logging
// package for where this is configured.
globalLogName = "default"
globalLogName = caddy.DefaultLoggerName
}
// Verify this name is unused.
@@ -858,3 +862,15 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
}
return configValues, nil
}
// parseSkipLog parses the skip_log directive. Syntax:
//
// skip_log [<matcher>]
func parseSkipLog(h Helper) (caddyhttp.MiddlewareHandler, error) {
for h.Next() {
if h.NextArg() {
return nil, h.ArgErr()
}
}
return caddyhttp.VarsMiddleware{"skip_log": true}, nil
}
+1
View File
@@ -42,6 +42,7 @@ var directiveOrder = []string{
"map",
"vars",
"root",
"skip_log",
"header",
"copy_response_headers", // only in reverse_proxy's handle_response
+38 -31
View File
@@ -219,11 +219,11 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
if ncl.name == "" {
return
}
if ncl.name == "default" {
if ncl.name == caddy.DefaultLoggerName {
hasDefaultLog = true
}
if _, ok := options["debug"]; ok && ncl.log.Level == "" {
ncl.log.Level = "DEBUG"
ncl.log.Level = zap.DebugLevel.CapitalString()
}
customLogs = append(customLogs, ncl)
}
@@ -240,8 +240,8 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
// configure it with any applicable options
if _, ok := options["debug"]; ok {
customLogs = append(customLogs, namedCustomLog{
name: "default",
log: &caddy.CustomLog{Level: "DEBUG"},
name: caddy.DefaultLoggerName,
log: &caddy.CustomLog{Level: zap.DebugLevel.CapitalString()},
})
}
}
@@ -299,11 +299,11 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
// most users seem to prefer not writing access logs
// to the default log when they are directed to a
// file or have any other special customization
if ncl.name != "default" && len(ncl.log.Include) > 0 {
defaultLog, ok := cfg.Logging.Logs["default"]
if ncl.name != caddy.DefaultLoggerName && len(ncl.log.Include) > 0 {
defaultLog, ok := cfg.Logging.Logs[caddy.DefaultLoggerName]
if !ok {
defaultLog = new(caddy.CustomLog)
cfg.Logging.Logs["default"] = defaultLog
cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog
}
defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...)
}
@@ -518,15 +518,6 @@ func (st *ServerType) serversFromPairings(
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
autoHTTPSWillAddConnPolicy := autoHTTPS != "off"
// if a catch-all server block (one which accepts all hostnames) exists in this pairing,
// we need to know that so that we can configure logs properly (see #3878)
var catchAllSblockExists bool
for _, sblock := range p.serverBlocks {
if len(sblock.hostsFromKeys(false)) == 0 {
catchAllSblockExists = true
}
}
// if needed, the ServerLogConfig is initialized beforehand so
// that all server blocks can populate it with data, even when not
// coming with a log directive
@@ -658,18 +649,10 @@ func (st *ServerType) serversFromPairings(
} else {
// map each host to the user's desired logger name
for _, h := range sblockLogHosts {
// if the custom logger name is non-empty, add it to the map;
// otherwise, only map to an empty logger name if this or
// another site block on this server has a catch-all host (in
// which case only requests with mapped hostnames will be
// access-logged, so it'll be necessary to add them to the
// map even if they use default logger)
if ncl.name != "" || catchAllSblockExists {
if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string)
}
srv.Logs.LoggerNames[h] = ncl.name
if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string)
}
srv.Logs.LoggerNames[h] = ncl.name
}
}
}
@@ -924,11 +907,32 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
return routeList
}
// No need to wrap the handlers in a subroute if this is the only server block
// and there is no matcher for it (doing so would produce unnecessarily nested
// JSON), *unless* there is a host matcher within this site block; if so, then
// we still need to wrap in a subroute because otherwise the host matcher from
// the inside of the site block would be a top-level host matcher, which is
// subject to auto-HTTPS (cert management), and using a host matcher within
// a site block is a valid, common pattern for excluding domains from cert
// management, leading to unexpected behavior; see issue #5124.
wrapInSubroute := true
if len(matcherSetsEnc) == 0 && len(p.serverBlocks) == 1 {
// no need to wrap the handlers in a subroute if this is
// the only server block and there is no matcher for it
routeList = append(routeList, subroute.Routes...)
} else {
var hasHostMatcher bool
outer:
for _, route := range subroute.Routes {
for _, ms := range route.MatcherSetsRaw {
for matcherName := range ms {
if matcherName == "host" {
hasHostMatcher = true
break outer
}
}
}
}
wrapInSubroute = hasHostMatcher
}
if wrapInSubroute {
route := caddyhttp.Route{
// the semantics of a site block in the Caddyfile dictate
// that only the first matching one is evaluated, since
@@ -946,7 +950,10 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
if len(route.MatcherSetsRaw) > 0 || len(route.HandlersRaw) > 0 {
routeList = append(routeList, route)
}
} else {
routeList = append(routeList, subroute.Routes...)
}
return routeList
}
+7 -7
View File
@@ -421,13 +421,13 @@ func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ any) (any, error) {
// parseLogOptions parses the global log option. Syntax:
//
// log [name] {
// output <writer_module> ...
// format <encoder_module> ...
// level <level>
// include <namespaces...>
// exclude <namespaces...>
// }
// log [name] {
// output <writer_module> ...
// format <encoder_module> ...
// level <level>
// include <namespaces...>
// exclude <namespaces...>
// }
//
// When the name argument is unspecified, this directive modifies the default
// logger.
+12 -1
View File
@@ -43,6 +43,7 @@ type serverOptions struct {
Protocols []string
StrictSNIHost *bool
ShouldLogCredentials bool
Metrics *caddyhttp.Metrics
}
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
@@ -161,7 +162,7 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
}
serverOpts.Protocols = append(serverOpts.Protocols, proto)
}
if d.NextBlock(0) {
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
@@ -175,6 +176,15 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
}
serverOpts.StrictSNIHost = &boolVal
case "metrics":
if d.NextArg() {
return nil, d.ArgErr()
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
serverOpts.Metrics = new(caddyhttp.Metrics)
// TODO: DEPRECATED. (August 2022)
case "protocol":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol sub-option will be removed soon")
@@ -259,6 +269,7 @@ func applyServerOptions(
server.MaxHeaderBytes = opts.MaxHeaderBytes
server.Protocols = opts.Protocols
server.StrictSNIHost = opts.StrictSNIHost
server.Metrics = opts.Metrics
if opts.ShouldLogCredentials {
if server.Logs == nil {
server.Logs = &caddyhttp.ServerLogConfig{}
+53 -47
View File
@@ -44,37 +44,32 @@ func (st ServerType) buildTLSApp(
if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp)
}
httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
if hsp, ok := options["https_port"].(int); ok {
httpsPort = strconv.Itoa(hsp)
autoHTTPS := "on"
if ah, ok := options["auto_https"].(string); ok {
autoHTTPS = ah
}
// count how many server blocks have a TLS-enabled key with
// no host, and find all hosts that share a server block with
// a hostless key, so that they don't get forgotten/omitted
// by auto-HTTPS (since they won't appear in route matchers)
var serverBlocksWithTLSHostlessKey int
// find all hosts that share a server block with a hostless
// key, so that they don't get forgotten/omitted by auto-HTTPS
// (since they won't appear in route matchers)
httpsHostsSharedWithHostlessKey := make(map[string]struct{})
for _, pair := range pairings {
for _, sb := range pair.serverBlocks {
for _, addr := range sb.keys {
if addr.Host == "" {
// this address has no hostname, but if it's explicitly set
// to HTTPS, then we need to count it as being TLS-enabled
if addr.Scheme == "https" || addr.Port == httpsPort {
serverBlocksWithTLSHostlessKey++
}
// this server block has a hostless key, now
// go through and add all the hosts to the set
for _, otherAddr := range sb.keys {
if otherAddr.Original == addr.Original {
continue
}
if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
if autoHTTPS != "off" {
for _, pair := range pairings {
for _, sb := range pair.serverBlocks {
for _, addr := range sb.keys {
if addr.Host == "" {
// this server block has a hostless key, now
// go through and add all the hosts to the set
for _, otherAddr := range sb.keys {
if otherAddr.Original == addr.Original {
continue
}
if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
}
}
break
}
break
}
}
}
@@ -134,6 +129,19 @@ func (st ServerType) buildTLSApp(
issuers = append(issuers, issuerVal.Value.(certmagic.Issuer))
}
if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) {
// this more correctly implements an error check that was removed
// below; try it with this config:
//
// :443 {
// bind 127.0.0.1
// }
//
// :443 {
// bind ::1
// tls {
// issuer acme
// }
// }
return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers)
}
ap.Issuers = issuers
@@ -176,29 +184,25 @@ func (st ServerType) buildTLSApp(
}
}
// first make sure this block is allowed to create an automation policy;
// doing so is forbidden if it has a key with no host (i.e. ":443")
// we used to ensure this block is allowed to create an automation policy;
// doing so was forbidden if it has a key with no host (i.e. ":443")
// and if there is a different server block that also has a key with no
// host -- since a key with no host matches any host, we need its
// associated automation policy to have an empty Subjects list, i.e. no
// host filter, which is indistinguishable between the two server blocks
// because automation is not done in the context of a particular server...
// this is an example of a poor mapping from Caddyfile to JSON but that's
// the least-leaky abstraction I could figure out
if len(sblockHosts) == 0 {
if serverBlocksWithTLSHostlessKey > 1 {
// this server block and at least one other has a key with no host,
// making the two indistinguishable; it is misleading to define such
// a policy within one server block since it actually will apply to
// others as well
return nil, warnings, fmt.Errorf("cannot make a TLS automation policy from a server block that has a host-less address when there are other TLS-enabled server block addresses lacking a host")
}
if catchAllAP == nil {
// this server block has a key with no hosts, but there is not yet
// a catch-all automation policy (probably because no global options
// were set), so this one becomes it
catchAllAP = ap
}
// the least-leaky abstraction I could figure out -- however, this check
// was preventing certain listeners, like those provided by plugins, from
// being used as desired (see the Tailscale listener plugin), so I removed
// the check: and I think since I originally wrote the check I added a new
// check above which *properly* detects this ambiguity without breaking the
// listener plugin; see the check above with a commented example config
if len(sblockHosts) == 0 && catchAllAP == nil {
// this server block has a key with no hosts, but there is not yet
// a catch-all automation policy (probably because no global options
// were set), so this one becomes it
catchAllAP = ap
}
// associate our new automation policy with this server block's hosts
@@ -331,10 +335,12 @@ func (st ServerType) buildTLSApp(
internalAP := &caddytls.AutomationPolicy{
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
}
for h := range httpsHostsSharedWithHostlessKey {
al = append(al, h)
if !certmagic.SubjectQualifiesForPublicCert(h) {
internalAP.Subjects = append(internalAP.Subjects, h)
if autoHTTPS != "off" {
for h := range httpsHostsSharedWithHostlessKey {
al = append(al, h)
if !certmagic.SubjectQualifiesForPublicCert(h) {
internalAP.Subjects = append(internalAP.Subjects, h)
}
}
}
if len(al) > 0 {
+34 -3
View File
@@ -94,7 +94,7 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
}
}
resp, err := client.Do(req)
resp, err := doHttpCallWithRetries(ctx, client, req)
if err != nil {
return nil, err
}
@@ -113,12 +113,43 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
return nil, err
}
for _, warn := range warnings {
ctx.Logger(hl).Warn(warn.String())
ctx.Logger().Warn(warn.String())
}
return result, nil
}
func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response, error) {
resp, err := client.Do(request)
if err != nil {
return nil, fmt.Errorf("problem calling http loader url: %v", err)
} else if resp.StatusCode < 200 || resp.StatusCode > 499 {
return nil, fmt.Errorf("bad response status code from http loader url: %v", resp.StatusCode)
}
return resp, nil
}
func doHttpCallWithRetries(ctx caddy.Context, client *http.Client, request *http.Request) (*http.Response, error) {
var resp *http.Response
var err error
const maxAttempts = 10
// attempt up to 10 times
for i := 0; i < maxAttempts; i++ {
resp, err = attemptHttpCall(client, request)
if err != nil && i < maxAttempts-1 {
// wait 500ms before reattempting, or until context is done
select {
case <-time.After(time.Millisecond * 500):
case <-ctx.Done():
return resp, ctx.Err()
}
}
}
return resp, err
}
func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
client := &http.Client{
Timeout: time.Duration(hl.Timeout),
@@ -129,7 +160,7 @@ func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
// client authentication
if hl.TLS.UseServerIdentity {
certs, err := ctx.IdentityCredentials(ctx.Logger(hl))
certs, err := ctx.IdentityCredentials(ctx.Logger())
if err != nil {
return nil, fmt.Errorf("getting server identity credentials: %v", err)
}
+4 -4
View File
@@ -43,7 +43,7 @@ type Defaults struct {
// Default testing values
var Default = Defaults{
AdminPort: 2019,
AdminPort: 2999, // different from what a real server also running on a developer's machine might be
Certifcates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
TestRequestTimeout: 5 * time.Second,
LoadRequestTimeout: 5 * time.Second,
@@ -214,7 +214,7 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
return actual
}
for retries := 4; retries > 0; retries-- {
for retries := 10; retries > 0; retries-- {
if reflect.DeepEqual(expected, fetchConfig(client)) {
return nil
}
@@ -237,13 +237,13 @@ func validateTestPrerequisites() error {
if isCaddyAdminRunning() != nil {
// start inprocess caddy server
os.Args = []string{"caddy", "run"}
os.Args = []string{"caddy", "run", "--config", "./test.init.config", "--adapter", "caddyfile"}
go func() {
caddycmd.Main()
}()
// wait for caddy to start serving the initial config
for retries := 4; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
time.Sleep(10 * time.Millisecond)
}
}
+21 -1
View File
@@ -11,6 +11,8 @@ func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
admin localhost:2999
skip_install_trust
http_port 9080
https_port 9443
}
@@ -25,6 +27,8 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
}
@@ -39,6 +43,8 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
}
@@ -53,6 +59,9 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
@@ -74,7 +83,14 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
]
}
}
}
},
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
}
}
}
`, "json")
@@ -85,6 +101,8 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
local_certs
@@ -108,6 +126,8 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSit
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
local_certs
@@ -3,9 +3,7 @@
timeouts {
idle 90s
}
protocol {
strict_sni_host insecure_off
}
strict_sni_host insecure_off
}
servers :80 {
timeouts {
@@ -16,9 +14,7 @@
timeouts {
idle 30s
}
protocol {
strict_sni_host
}
strict_sni_host
}
}
@@ -12,8 +12,8 @@
}
max_header_size 100MB
log_credentials
strict_sni_host
protocols h1 h2 h2c h3
strict_sni_host
}
}
@@ -1,5 +1,7 @@
http://localhost:2020 {
log
skip_log /first-hidden*
skip_log /second-hidden*
respond 200
}
@@ -28,6 +30,36 @@ http://localhost:2020 {
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"skip_log": true
}
],
"match": [
{
"path": [
"/second-hidden*"
]
}
]
},
{
"handle": [
{
"handler": "vars",
"skip_log": true
}
],
"match": [
{
"path": [
"/first-hidden*"
]
}
]
},
{
"handle": [
{
@@ -62,6 +62,9 @@ example.com {
}
],
"logs": {
"logger_names": {
"one.example.com": ""
},
"skip_hosts": [
"three.example.com",
"two.example.com",
@@ -8,7 +8,7 @@ route {
}
not path */
}
redir @canonicalPath {path}/ 308
redir @canonicalPath {http.request.orig_uri.path}/ 308
# If the requested file does not exist, try index files
@indexFiles {
@@ -50,7 +50,7 @@ route {
"handler": "static_response",
"headers": {
"Location": [
"{http.request.uri.path}/"
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
@@ -42,7 +42,7 @@
"handler": "static_response",
"headers": {
"Location": [
"{http.request.uri.path}/"
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
@@ -1,7 +1,12 @@
:8884
@api host example.com
php_fastcgi @api localhost:9000
# the use of a host matcher here should cause this
# site block to be wrapped in a subroute, even though
# the site block does not have a hostname; this is
# to prevent auto-HTTPS from picking up on this host
# matcher because it is not a key on the site block
@test host example.com
php_fastcgi @test localhost:9000
----------
{
"apps": {
@@ -13,13 +18,6 @@ php_fastcgi @api localhost:9000
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
@@ -27,82 +25,99 @@ php_fastcgi @api localhost:9000
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
"handler": "subroute",
"routes": [
{
"path": [
"*/"
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
],
"match": [
{
"path": [
"*.php"
]
}
]
}
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
],
"match": [
{
"path": [
"*.php"
"host": [
"example.com"
]
}
]
}
]
}
]
],
"terminal": true
}
]
}
@@ -43,7 +43,7 @@ php_fastcgi localhost:9000 {
"handler": "static_response",
"headers": {
"Location": [
"{http.request.uri.path}/"
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
@@ -46,7 +46,7 @@ php_fastcgi localhost:9000 {
"handler": "static_response",
"headers": {
"Location": [
"{http.request.uri.path}/"
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
@@ -0,0 +1,58 @@
# example from issue #4667
{
auto_https off
}
https://, example.com {
tls test.crt test.key
respond "Hello World"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"handle": [
{
"body": "Hello World",
"handler": "static_response"
}
]
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": [
"cert0"
]
}
}
],
"automatic_https": {
"disable": true
}
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "test.crt",
"key": "test.key",
"tags": [
"cert0"
]
}
]
}
}
}
}
+10
View File
@@ -14,8 +14,10 @@ func TestRespond(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost:9080 {
@@ -35,8 +37,10 @@ func TestRedirect(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost:9080 {
@@ -84,8 +88,11 @@ func TestReadCookie(t *testing.T) {
tester.Client.Jar.SetCookies(localhost, []*http.Cookie{&cookie})
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost:9080 {
@@ -107,8 +114,11 @@ func TestReplIndex(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost:9080 {
+3
View File
@@ -11,8 +11,11 @@ func TestBrowse(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
file_server browse
+15
View File
@@ -11,8 +11,11 @@ func TestMap(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost:9080 {
@@ -38,6 +41,8 @@ func TestMapRespondWithDefault(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
}
@@ -65,7 +70,17 @@ func TestMapAsJSON(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"http_port": 9080,
"https_port": 9443,
+73 -2
View File
@@ -8,6 +8,7 @@ import (
"runtime"
"strings"
"testing"
"time"
"github.com/caddyserver/caddy/v2/caddytest"
)
@@ -16,8 +17,19 @@ func TestSRVReverseProxy(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
@@ -49,7 +61,15 @@ func TestSRVWithDial(t *testing.T) {
caddytest.AssertLoadError(t, `
{
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
@@ -113,8 +133,19 @@ func TestDialWithPlaceholderUnix(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
@@ -154,8 +185,19 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
@@ -237,8 +279,19 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
@@ -320,7 +373,15 @@ func TestSRVWithActiveHealthcheck(t *testing.T) {
caddytest.AssertLoadError(t, `
{
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
@@ -357,8 +418,11 @@ func TestReverseProxyHealthCheck(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:2020 {
respond "Hello, World!"
@@ -372,12 +436,13 @@ func TestReverseProxyHealthCheck(t *testing.T) {
health_uri /health
health_port 2021
health_interval 2s
health_timeout 5s
health_interval 10ms
health_timeout 100ms
}
}
`, "caddyfile")
time.Sleep(100 * time.Millisecond) // TODO: for some reason this test seems particularly flaky, getting 503 when it should be 200, unless we wait
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
}
@@ -418,8 +483,11 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy {
@@ -473,8 +541,11 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy {
+257 -237
View File
@@ -11,91 +11,95 @@ func TestDefaultSNI(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from a.caddy.localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"match": [
{
"host": [
"127.0.0.1"
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"match": {
"sni": [
"127.0.0.1"
]
}
},
{
"default_sni": "*.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/caddy.localhost.crt",
"key": "/caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from a.caddy.localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"match": [
{
"host": [
"127.0.0.1"
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"match": {
"sni": [
"127.0.0.1"
]
}
},
{
"default_sni": "*.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/caddy.localhost.crt",
"key": "/caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
// act and assert
// makes a request with no sni
@@ -107,96 +111,100 @@ func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from a",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"match": [
{
"host": [
"a.caddy.localhost",
"127.0.0.1"
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost",
"match": {
"sni": [
"a.caddy.localhost",
"127.0.0.1",
""
]
}
},
{
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from a",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"match": [
{
"host": [
"a.caddy.localhost",
"127.0.0.1"
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost",
"match": {
"sni": [
"a.caddy.localhost",
"127.0.0.1",
""
]
}
},
{
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
// act and assert
// makes a request with no sni
@@ -207,68 +215,72 @@ func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"body": "hello from a.caddy.localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"body": "hello from a.caddy.localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
// act and assert
// makes a request with no sni
@@ -278,6 +290,7 @@ func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
func TestHttpOnlyOnDomainWithSNI(t *testing.T) {
caddytest.AssertAdapt(t, `
{
skip_install_trust
default_sni a.caddy.localhost
}
:80 {
@@ -313,6 +326,13 @@ func TestHttpOnlyOnDomainWithSNI(t *testing.T) {
]
}
}
},
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
}
}
}`)
+8
View File
@@ -23,10 +23,14 @@ func TestH2ToH2CStream(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
@@ -205,6 +209,9 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"logging": {
"logs": {
"default": {
@@ -216,6 +223,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
+3
View File
@@ -0,0 +1,3 @@
{
admin localhost:2999
}
+1 -1
View File
@@ -679,7 +679,7 @@ func DetermineAdminAPIAddress(address string, config []byte, configFile, configA
return "", err
}
if loadedConfigFile == "" {
return "", fmt.Errorf("no config file to load")
return "", fmt.Errorf("no config file to load; either use --config flag or ensure Caddyfile exists in current directory")
}
}
+7 -2
View File
@@ -41,10 +41,15 @@ func init() {
// set a fitting User-Agent for ACME requests
version, _ := caddy.Version()
cleanModVersion := strings.TrimPrefix(version, "v")
certmagic.UserAgent = "Caddy/" + cleanModVersion
ua := "Caddy/" + cleanModVersion
if uaEnv, ok := os.LookupEnv("USERAGENT"); ok {
ua = uaEnv + " " + ua
}
certmagic.UserAgent = ua
// by using Caddy, user indicates agreement to CA terms
// (very important, or ACME account creation will fail!)
// (very important, as Caddy is often non-interactive
// and thus ACME account creation will fail!)
certmagic.DefaultACME.Agreed = true
}
+25 -18
View File
@@ -442,10 +442,27 @@ func (ctx Context) Storage() certmagic.Storage {
return ctx.cfg.storage
}
// TODO: aw man, can I please change this?
// Logger returns a logger that can be used by mod.
func (ctx Context) Logger(mod Module) *zap.Logger {
// TODO: if mod is nil, use ctx.Module() instead...
// Logger returns a logger that is intended for use by the most
// recent module associated with the context. Callers should not
// pass in any arguments unless they want to associate with a
// different module; it panics if more than 1 value is passed in.
//
// Originally, this method's signature was `Logger(mod Module)`,
// requiring that an instance of a Caddy module be passed in.
// However, that is no longer necessary, as the closest module
// most recently associated with the context will be automatically
// assumed. To prevent a sudden breaking change, this method's
// signature has been changed to be variadic, but we may remove
// the parameter altogether in the future. Callers should not
// pass in any argument. If there is valid need to specify a
// different module, please open an issue to discuss.
//
// PARTIALLY DEPRECATED: The Logger(module) form is deprecated and
// may be removed in the future. Do not pass in any arguments.
func (ctx Context) Logger(module ...Module) *zap.Logger {
if len(module) > 1 {
panic("more than 1 module passed in")
}
if ctx.cfg == nil {
// often the case in tests; just use a dev logger
l, err := zap.NewDevelopment()
@@ -454,23 +471,13 @@ func (ctx Context) Logger(mod Module) *zap.Logger {
}
return l
}
mod := ctx.Module()
if len(module) > 0 {
mod = module[0]
}
return ctx.cfg.Logging.Logger(mod)
}
// TODO: use this
// // Logger returns a logger that can be used by the current module.
// func (ctx Context) Log() *zap.Logger {
// if ctx.cfg == nil {
// // often the case in tests; just use a dev logger
// l, err := zap.NewDevelopment()
// if err != nil {
// panic("config missing, unable to create dev logger: " + err.Error())
// }
// return l
// }
// return ctx.cfg.Logging.Logger(ctx.Module())
// }
// Modules returns the lineage of modules that this context provisioned,
// with the most recent/current module being last in the list.
func (ctx Context) Modules() []Module {
+19 -19
View File
@@ -7,34 +7,34 @@ require (
github.com/Masterminds/sprig/v3 v3.2.2
github.com/alecthomas/chroma v0.10.0
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
github.com/caddyserver/certmagic v0.17.1
github.com/caddyserver/certmagic v0.17.2
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
github.com/go-chi/chi v4.1.2+incompatible
github.com/google/cel-go v0.12.4
github.com/google/cel-go v0.12.5
github.com/google/uuid v1.3.0
github.com/klauspost/compress v1.15.9
github.com/klauspost/cpuid/v2 v2.1.0
github.com/lucas-clemente/quic-go v0.28.2-0.20220813150001-9957668d4301
github.com/klauspost/compress v1.15.11
github.com/klauspost/cpuid/v2 v2.1.1
github.com/lucas-clemente/quic-go v0.29.2
github.com/mholt/acmez v1.0.4
github.com/prometheus/client_golang v1.12.2
github.com/smallstep/certificates v0.21.0
github.com/smallstep/cli v0.21.0
github.com/smallstep/certificates v0.22.1
github.com/smallstep/cli v0.22.0
github.com/smallstep/nosql v0.4.0
github.com/smallstep/truststore v0.12.0
github.com/spf13/cobra v1.1.3
github.com/spf13/cobra v1.5.0
github.com/spf13/pflag v1.0.5
github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2
github.com/yuin/goldmark v1.4.13
github.com/yuin/goldmark v1.5.2
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0
go.opentelemetry.io/otel v1.9.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.0
go.opentelemetry.io/otel/sdk v1.4.0
go.uber.org/zap v1.21.0
go.uber.org/zap v1.23.0
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
golang.org/x/net v0.0.0-20220812165438-1d4ff48094d1
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21
google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -56,7 +56,7 @@ require (
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd // indirect
@@ -87,8 +87,8 @@ require (
github.com/libdns/libdns v0.2.1 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/marten-seemann/qpack v0.2.1 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.3 // indirect
github.com/marten-seemann/qtls-go1-19 v0.1.1 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
@@ -120,17 +120,17 @@ require (
go.opentelemetry.io/otel/metric v0.31.0 // indirect
go.opentelemetry.io/otel/trace v1.9.0 // indirect
go.opentelemetry.io/proto/otlp v0.12.0 // indirect
go.step.sm/cli-utils v0.7.3 // indirect
go.step.sm/crypto v0.16.2 // indirect
go.step.sm/linkedca v0.16.1 // indirect
go.step.sm/cli-utils v0.7.4 // indirect
go.step.sm/crypto v0.18.0 // indirect
go.step.sm/linkedca v0.18.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/grpc v1.46.0 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/grpc v1.47.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
+48 -375
View File
File diff suppressed because it is too large Load Diff
+25 -20
View File
@@ -1,9 +1,26 @@
//go:build !linux
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// TODO: Go 1.19 introduced the "unix" build tag. We have to support Go 1.18 until Go 1.20 is released.
// When Go 1.19 is our minimum, change this build tag to simply "!unix".
// (see similar change needed in listen_unix.go)
//go:build !(aix || android || darwin || dragonfly || freebsd || hurd || illumos || ios || linux || netbsd || openbsd || solaris)
package caddy
import (
"fmt"
"context"
"net"
"sync"
"sync/atomic"
@@ -12,21 +29,14 @@ import (
"go.uber.org/zap"
)
func ListenTimeout(network, addr string, keepAlivePeriod time.Duration) (net.Listener, error) {
// check to see if plugin provides listener
if ln, err := getListenerFromPlugin(network, addr); err != nil || ln != nil {
return ln, err
}
lnKey := listenerKey(network, addr)
func reuseUnixSocket(network, addr string) (any, error) {
return nil, nil
}
func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) {
sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
ln, err := net.Listen(network, addr)
ln, err := config.Listen(ctx, network, address)
if err != nil {
// https://github.com/caddyserver/caddy/pull/4534
if isUnixNetwork(network) && isListenBindAddressAlreadyInUseError(err) {
return nil, fmt.Errorf("%w: this can happen if Caddy was forcefully killed", err)
}
return nil, err
}
return &sharedListener{Listener: ln, key: lnKey}, nil
@@ -34,8 +44,7 @@ func ListenTimeout(network, addr string, keepAlivePeriod time.Duration) (net.Lis
if err != nil {
return nil, err
}
return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: keepAlivePeriod}, nil
return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil
}
// fakeCloseListener is a private wrapper over a listener that
@@ -139,8 +148,6 @@ func (sl *sharedListener) clearDeadline() error {
switch ln := sl.Listener.(type) {
case *net.TCPListener:
err = ln.SetDeadline(time.Time{})
case *net.UnixListener:
err = ln.SetDeadline(time.Time{})
}
sl.deadline = false
}
@@ -156,8 +163,6 @@ func (sl *sharedListener) setDeadline() error {
switch ln := sl.Listener.(type) {
case *net.TCPListener:
err = ln.SetDeadline(timeInPast)
case *net.UnixListener:
err = ln.SetDeadline(timeInPast)
}
sl.deadline = true
}
-34
View File
@@ -1,34 +0,0 @@
package caddy
import (
"context"
"net"
"syscall"
"time"
"go.uber.org/zap"
"golang.org/x/sys/unix"
)
// ListenTimeout is the same as Listen, but with a configurable keep-alive timeout duration.
func ListenTimeout(network, addr string, keepalivePeriod time.Duration) (net.Listener, error) {
// check to see if plugin provides listener
if ln, err := getListenerFromPlugin(network, addr); err != nil || ln != nil {
return ln, err
}
config := &net.ListenConfig{Control: reusePort, KeepAlive: keepalivePeriod}
return config.Listen(context.Background(), network, addr)
}
func reusePort(network, address string, conn syscall.RawConn) error {
return conn.Control(func(descriptor uintptr) {
if err := unix.SetsockoptInt(int(descriptor), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
Log().Error("setting SO_REUSEPORT",
zap.String("network", network),
zap.String("address", address),
zap.Uintptr("descriptor", descriptor),
zap.Error(err))
}
})
}
+118
View File
@@ -0,0 +1,118 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// TODO: Go 1.19 introduced the "unix" build tag. We have to support Go 1.18 until Go 1.20 is released.
// When Go 1.19 is our minimum, remove this build tag, since "_unix" in the filename will do this.
// (see also change needed in listen.go)
//go:build aix || android || darwin || dragonfly || freebsd || hurd || illumos || ios || linux || netbsd || openbsd || solaris
package caddy
import (
"context"
"errors"
"io/fs"
"net"
"sync/atomic"
"syscall"
"go.uber.org/zap"
"golang.org/x/sys/unix"
)
// reuseUnixSocket copies and reuses the unix domain socket (UDS) if we already
// have it open; if not, unlink it so we can have it. No-op if not a unix network.
func reuseUnixSocket(network, addr string) (any, error) {
if !isUnixNetwork(network) {
return nil, nil
}
socketKey := listenerKey(network, addr)
socket, exists := unixSockets[socketKey]
if exists {
// make copy of file descriptor
socketFile, err := socket.File() // does dup() deep down
if err != nil {
return nil, err
}
// use copied fd to make new Listener or PacketConn, then replace
// it in the map so that future copies always come from the most
// recent fd (as the previous ones will be closed, and we'd get
// "use of closed network connection" errors) -- note that we
// preserve the *pointer* to the counter (not just the value) so
// that all socket wrappers will refer to the same value
switch unixSocket := socket.(type) {
case *unixListener:
ln, err := net.FileListener(socketFile)
if err != nil {
return nil, err
}
atomic.AddInt32(unixSocket.count, 1)
unixSockets[socketKey] = &unixListener{ln.(*net.UnixListener), socketKey, unixSocket.count}
case *unixConn:
pc, err := net.FilePacketConn(socketFile)
if err != nil {
return nil, err
}
atomic.AddInt32(unixSocket.count, 1)
unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), addr, socketKey, unixSocket.count}
}
return unixSockets[socketKey], nil
}
// from what I can tell after some quick research, it's quite common for programs to
// leave their socket file behind after they close, so the typical pattern is to
// unlink it before you bind to it -- this is often crucial if the last program using
// it was killed forcefully without a chance to clean up the socket, but there is a
// race, as the comment in net.UnixListener.close() explains... oh well, I guess?
if err := syscall.Unlink(addr); err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err
}
return nil, nil
}
func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) {
// wrap any Control function set by the user so we can also add our reusePort control without clobbering theirs
oldControl := config.Control
config.Control = func(network, address string, c syscall.RawConn) error {
if oldControl != nil {
if err := oldControl(network, address, c); err != nil {
return err
}
}
return reusePort(network, address, c)
}
return config.Listen(ctx, network, address)
}
// reusePort sets SO_REUSEPORT. Ineffective for unix sockets.
func reusePort(network, address string, conn syscall.RawConn) error {
if isUnixNetwork(network) {
return nil
}
return conn.Control(func(descriptor uintptr) {
if err := unix.SetsockoptInt(int(descriptor), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
Log().Error("setting SO_REUSEPORT",
zap.String("network", network),
zap.String("address", address),
zap.Uintptr("descriptor", descriptor),
zap.Error(err))
}
})
}
+428 -207
View File
@@ -19,230 +19,187 @@ import (
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/netip"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/http3"
"go.uber.org/zap"
)
// Listen is like net.Listen, except Caddy's listeners can overlap
// each other: multiple listeners may be created on the same socket
// at the same time. This is useful because during config changes,
// the new config is started while the old config is still running.
// When Caddy listeners are closed, the closing logic is virtualized
// so the underlying socket isn't actually closed until all uses of
// the socket have been finished. Always be sure to close listeners
// when you are done with them, just like normal listeners.
func Listen(network, addr string) (net.Listener, error) {
// a 0 timeout means Go uses its default
return ListenTimeout(network, addr, 0)
// NetworkAddress represents one or more network addresses.
// It contains the individual components for a parsed network
// address of the form accepted by ParseNetworkAddress().
type NetworkAddress struct {
// Should be a network value accepted by Go's net package or
// by a plugin providing a listener for that network type.
Network string
// The "main" part of the network address is the host, which
// often takes the form of a hostname, DNS name, IP address,
// or socket path.
Host string
// For addresses that contain a port, ranges are given by
// [StartPort, EndPort]; i.e. for a single port, StartPort
// and EndPort are the same. For no port, they are 0.
StartPort uint
EndPort uint
}
// getListenerFromPlugin returns a listener on the given network and address
// if a plugin has registered the network name. It may return (nil, nil) if
// no plugin can provide a listener.
func getListenerFromPlugin(network, addr string) (net.Listener, error) {
network = strings.TrimSpace(strings.ToLower(network))
// ListenAll calls Listen() for all addresses represented by this struct, i.e. all ports in the range.
// (If the address doesn't use ports or has 1 port only, then only 1 listener will be created.)
// It returns an error if any listener failed to bind, and closes any listeners opened up to that point.
//
// TODO: Experimental API: subject to change or removal.
func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig) ([]any, error) {
var listeners []any
var err error
// get listener from plugin if network type is registered
if getListener, ok := networkTypes[network]; ok {
Log().Debug("getting listener from plugin", zap.String("network", network))
return getListener(network, addr)
}
return nil, nil
}
// ListenPacket returns a net.PacketConn suitable for use in a Caddy module.
// It is like Listen except for PacketConns.
// Always be sure to close the PacketConn when you are done.
func ListenPacket(network, addr string) (net.PacketConn, error) {
lnKey := listenerKey(network, addr)
sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
pc, err := net.ListenPacket(network, addr)
if err != nil {
// https://github.com/caddyserver/caddy/pull/4534
if isUnixNetwork(network) && isListenBindAddressAlreadyInUseError(err) {
return nil, fmt.Errorf("%w: this can happen if Caddy was forcefully killed", err)
// if one of the addresses has a failure, we need to close
// any that did open a socket to avoid leaking resources
defer func() {
if err == nil {
return
}
for _, ln := range listeners {
if cl, ok := ln.(io.Closer); ok {
cl.Close()
}
}
}()
// an address can contain a port range, which represents multiple addresses;
// some addresses don't use ports at all and have a port range size of 1;
// whatever the case, iterate each address represented and bind a socket
for portOffset := uint(0); portOffset < na.PortRangeSize(); portOffset++ {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
// create (or reuse) the listener ourselves
var ln any
ln, err = na.Listen(ctx, portOffset, config)
if err != nil {
return nil, err
}
return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil
})
if err != nil {
return nil, err
listeners = append(listeners, ln)
}
return &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}, nil
return listeners, nil
}
// ListenQUIC returns a quic.EarlyListener suitable for use in a Caddy module.
// Note that the context passed to Accept is currently ignored, so using
// a context other than context.Background is meaningless.
// This API is EXPERIMENTAL and may change.
func ListenQUIC(addr string, tlsConf *tls.Config, activeRequests *int64) (quic.EarlyListener, error) {
lnKey := listenerKey("udp", addr)
// Listen is similar to net.Listen, with a few differences:
//
// Listen announces on the network address using the port calculated by adding
// portOffset to the start port. (For network types that do not use ports, the
// portOffset is ignored.)
//
// The provided ListenConfig is used to create the listener. Its Control function,
// if set, may be wrapped by an internally-used Control function. The provided
// context may be used to cancel long operations early. The context is not used
// to close the listener after it has been created.
//
// Caddy's listeners can overlap each other: multiple listeners may be created on
// the same socket at the same time. This is useful because during config changes,
// the new config is started while the old config is still running. How this is
// accomplished varies by platform and network type. For example, on Unix, SO_REUSEPORT
// is set except on Unix sockets, for which the file descriptor is duplicated and
// reused; on Windows, the close logic is virtualized using timeouts. Like normal
// listeners, be sure to Close() them when you are done.
//
// This method returns any type, as the implementations of listeners for various
// network types are not interchangeable. The type of listener returned is switched
// on the network type. Stream-based networks ("tcp", "unix", "unixpacket", etc.)
// return a net.Listener; datagram-based networks ("udp", "unixgram", etc.) return
// a net.PacketConn; and so forth. The actual concrete types are not guaranteed to
// be standard, exported types (wrapping is necessary to provide graceful reloads).
//
// Unix sockets will be unlinked before being created, to ensure we can bind to
// it even if the previous program using it exited uncleanly; it will also be
// unlinked upon a graceful exit (or when a new config does not use that socket).
//
// TODO: Experimental API: subject to change or removal.
func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
if na.IsUnixNetwork() {
unixSocketsMu.Lock()
defer unixSocketsMu.Unlock()
}
sharedEl, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
el, err := quic.ListenAddrEarly(addr, http3.ConfigureTLSConfig(tlsConf), &quic.Config{
RequireAddressValidation: func(clientAddr net.Addr) bool {
var highLoad bool
if activeRequests != nil {
highLoad = atomic.LoadInt64(activeRequests) > 1000 // TODO: make tunable?
}
return highLoad
},
// check to see if plugin provides listener
if ln, err := getListenerFromPlugin(ctx, na.Network, na.JoinHostPort(portOffset), config); ln != nil || err != nil {
return ln, err
}
// create (or reuse) the listener ourselves
return na.listen(ctx, portOffset, config)
}
func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
var ln any
var err error
address := na.JoinHostPort(portOffset)
// if this is a unix socket, see if we already have it open
if socket, err := reuseUnixSocket(na.Network, address); socket != nil || err != nil {
return socket, err
}
lnKey := listenerKey(na.Network, address)
switch na.Network {
case "tcp", "tcp4", "tcp6", "unix", "unixpacket":
ln, err = listenTCPOrUnix(ctx, lnKey, na.Network, address, config)
case "unixgram":
ln, err = config.ListenPacket(ctx, na.Network, address)
case "udp", "udp4", "udp6":
sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
pc, err := config.ListenPacket(ctx, na.Network, address)
if err != nil {
return nil, err
}
return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil
})
if err != nil {
return nil, err
}
return &sharedQuicListener{EarlyListener: el, key: lnKey}, nil
})
ln = &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}
}
if strings.HasPrefix(na.Network, "ip") {
ln, err = config.ListenPacket(ctx, na.Network, address)
}
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
return &fakeCloseQuicListener{
sharedQuicListener: sharedEl.(*sharedQuicListener),
context: ctx,
contextCancel: cancel,
}, nil
}
// ListenerUsage returns the current usage count of the given listener address.
func ListenerUsage(network, addr string) int {
count, _ := listenerPool.References(listenerKey(network, addr))
return count
}
func listenerKey(network, addr string) string {
return network + "/" + addr
}
type fakeCloseQuicListener struct {
closed int32 // accessed atomically; belongs to this struct only
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
context context.Context
contextCancel context.CancelFunc
}
// Currently Accept ignores the passed context, however a situation where
// someone would need a hotswappable QUIC-only (not http3, since it uses context.Background here)
// server on which Accept would be called with non-empty contexts
// (mind that the default net listeners' Accept doesn't take a context argument)
// sounds way too rare for us to sacrifice efficiency here.
func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (quic.EarlyConnection, error) {
conn, err := fcql.sharedQuicListener.Accept(fcql.context)
if err == nil {
return conn, nil
if ln == nil {
return nil, fmt.Errorf("unsupported network type: %s", na.Network)
}
// if the listener is "closed", return a fake closed error instead
if atomic.LoadInt32(&fcql.closed) == 1 && errors.Is(err, context.Canceled) {
return nil, fakeClosedErr(fcql)
// if new listener is a unix socket, make sure we can reuse it later
// (we do our own "unlink on close" -- not required, but more tidy)
one := int32(1)
switch unix := ln.(type) {
case *net.UnixListener:
unix.SetUnlinkOnClose(false)
ln = &unixListener{unix, lnKey, &one}
unixSockets[lnKey] = ln.(*unixListener)
case *net.UnixConn:
ln = &unixConn{unix, address, lnKey, &one}
unixSockets[lnKey] = ln.(*unixConn)
}
return nil, err
}
func (fcql *fakeCloseQuicListener) Close() error {
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
fcql.contextCancel()
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
}
return nil
}
// fakeClosedErr returns an error value that is not temporary
// nor a timeout, suitable for making the caller think the
// listener is actually closed
func fakeClosedErr(l interface{ Addr() net.Addr }) error {
return &net.OpError{
Op: "accept",
Net: l.Addr().Network(),
Addr: l.Addr(),
Err: errFakeClosed,
}
}
// ErrFakeClosed is the underlying error value returned by
// fakeCloseListener.Accept() after Close() has been called,
// indicating that it is pretending to be closed so that the
// server using it can terminate, while the underlying
// socket is actually left open.
var errFakeClosed = fmt.Errorf("listener 'closed' 😉")
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns.
type fakeClosePacketConn struct {
closed int32 // accessed atomically; belongs to this struct only
*sharedPacketConn // embedded, so we also become a net.PacketConn
}
func (fcpc *fakeClosePacketConn) Close() error {
if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
}
return nil
}
// Supports QUIC implementation: https://github.com/caddyserver/caddy/issues/3998
func (fcpc fakeClosePacketConn) SetReadBuffer(bytes int) error {
if conn, ok := fcpc.PacketConn.(interface{ SetReadBuffer(int) error }); ok {
return conn.SetReadBuffer(bytes)
}
return fmt.Errorf("SetReadBuffer() not implemented for %T", fcpc.PacketConn)
}
// Supports QUIC implementation: https://github.com/caddyserver/caddy/issues/3998
func (fcpc fakeClosePacketConn) SyscallConn() (syscall.RawConn, error) {
if conn, ok := fcpc.PacketConn.(interface {
SyscallConn() (syscall.RawConn, error)
}); ok {
return conn.SyscallConn()
}
return nil, fmt.Errorf("SyscallConn() not implemented for %T", fcpc.PacketConn)
}
// sharedQuicListener is like sharedListener, but for quic.EarlyListeners.
type sharedQuicListener struct {
quic.EarlyListener
key string
}
// Destruct closes the underlying QUIC listener.
func (sql *sharedQuicListener) Destruct() error {
return sql.EarlyListener.Close()
}
// sharedPacketConn is like sharedListener, but for net.PacketConns.
type sharedPacketConn struct {
net.PacketConn
key string
}
// Destruct closes the underlying socket.
func (spc *sharedPacketConn) Destruct() error {
return spc.PacketConn.Close()
}
// NetworkAddress contains the individual components
// for a parsed network address of the form accepted
// by ParseNetworkAddress(). Network should be a
// network value accepted by Go's net package. Port
// ranges are given by [StartPort, EndPort].
type NetworkAddress struct {
Network string
Host string
StartPort uint
EndPort uint
return ln, nil
}
// IsUnixNetwork returns true if na.Network is
@@ -260,17 +217,27 @@ func (na NetworkAddress) JoinHostPort(offset uint) string {
return net.JoinHostPort(na.Host, strconv.Itoa(int(na.StartPort+offset)))
}
// Expand returns one NetworkAddress for each port in the port range.
//
// This is EXPERIMENTAL and subject to change or removal.
func (na NetworkAddress) Expand() []NetworkAddress {
size := na.PortRangeSize()
addrs := make([]NetworkAddress, size)
for portOffset := uint(0); portOffset < size; portOffset++ {
na2 := na
na2.StartPort, na2.EndPort = na.StartPort+portOffset, na.StartPort+portOffset
addrs[portOffset] = na2
addrs[portOffset] = na.At(portOffset)
}
return addrs
}
// At returns a NetworkAddress with a port range of just 1
// at the given port offset; i.e. a NetworkAddress that
// represents precisely 1 address only.
func (na NetworkAddress) At(portOffset uint) NetworkAddress {
na2 := na
na2.StartPort, na2.EndPort = na.StartPort+portOffset, na.StartPort+portOffset
return na2
}
// PortRangeSize returns how many ports are in
// pa's port range. Port ranges are inclusive,
// so the size is the difference of start and
@@ -326,20 +293,6 @@ func isUnixNetwork(netw string) bool {
return netw == "unix" || netw == "unixgram" || netw == "unixpacket"
}
func isListenBindAddressAlreadyInUseError(err error) bool {
switch networkOperationError := err.(type) {
case *net.OpError:
switch syscallError := networkOperationError.Err.(type) {
case *os.SyscallError:
if syscallError.Syscall == "bind" {
return true
}
}
}
return false
}
// ParseNetworkAddress parses addr into its individual
// components. The input string is expected to be of
// the form "network/host:port-range" where any part is
@@ -439,6 +392,208 @@ func JoinNetworkAddress(network, host, port string) string {
return a
}
// DEPRECATED: Use NetworkAddress.Listen instead. This function will likely be changed or removed in the future.
func Listen(network, addr string) (net.Listener, error) {
// a 0 timeout means Go uses its default
return ListenTimeout(network, addr, 0)
}
// DEPRECATED: Use NetworkAddress.Listen instead. This function will likely be changed or removed in the future.
func ListenTimeout(network, addr string, keepalivePeriod time.Duration) (net.Listener, error) {
netAddr, err := ParseNetworkAddress(JoinNetworkAddress(network, addr, ""))
if err != nil {
return nil, err
}
ln, err := netAddr.Listen(context.TODO(), 0, net.ListenConfig{KeepAlive: keepalivePeriod})
if err != nil {
return nil, err
}
return ln.(net.Listener), nil
}
// DEPRECATED: Use NetworkAddress.Listen instead. This function will likely be changed or removed in the future.
func ListenPacket(network, addr string) (net.PacketConn, error) {
netAddr, err := ParseNetworkAddress(JoinNetworkAddress(network, addr, ""))
if err != nil {
return nil, err
}
ln, err := netAddr.Listen(context.TODO(), 0, net.ListenConfig{})
if err != nil {
return nil, err
}
return ln.(net.PacketConn), nil
}
// ListenQUIC returns a quic.EarlyListener suitable for use in a Caddy module.
// The network will be transformed into a QUIC-compatible type (if unix, then
// unixgram will be used; otherwise, udp will be used).
//
// NOTE: This API is EXPERIMENTAL and may be changed or removed.
//
// TODO: See if we can find a more elegant solution closer to the new NetworkAddress.Listen API.
func ListenQUIC(ln net.PacketConn, tlsConf *tls.Config, activeRequests *int64) (quic.EarlyListener, error) {
lnKey := listenerKey("quic+"+ln.LocalAddr().Network(), ln.LocalAddr().String())
sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
earlyLn, err := quic.ListenEarly(ln, http3.ConfigureTLSConfig(tlsConf), &quic.Config{
RequireAddressValidation: func(clientAddr net.Addr) bool {
var highLoad bool
if activeRequests != nil {
highLoad = atomic.LoadInt64(activeRequests) > 1000 // TODO: make tunable?
}
return highLoad
},
})
if err != nil {
return nil, err
}
return &sharedQuicListener{EarlyListener: earlyLn, key: lnKey}, nil
})
if err != nil {
return nil, err
}
// TODO: to serve QUIC over a unix socket, currently we need to hold onto
// the underlying net.PacketConn (which we wrap as unixConn to keep count
// of closes) because closing the quic.EarlyListener doesn't actually close
// the underlying PacketConn, but we need to for unix sockets since we dup
// the file descriptor and thus need to close the original; track issue:
// https://github.com/lucas-clemente/quic-go/issues/3560#issuecomment-1258959608
var unix *unixConn
if uc, ok := ln.(*unixConn); ok {
unix = uc
}
ctx, cancel := context.WithCancel(context.Background())
return &fakeCloseQuicListener{
sharedQuicListener: sharedEarlyListener.(*sharedQuicListener),
uc: unix,
context: ctx,
contextCancel: cancel,
}, nil
}
// ListenerUsage returns the current usage count of the given listener address.
func ListenerUsage(network, addr string) int {
count, _ := listenerPool.References(listenerKey(network, addr))
return count
}
// sharedQuicListener is like sharedListener, but for quic.EarlyListeners.
type sharedQuicListener struct {
quic.EarlyListener
key string
}
// Destruct closes the underlying QUIC listener.
func (sql *sharedQuicListener) Destruct() error {
return sql.EarlyListener.Close()
}
// sharedPacketConn is like sharedListener, but for net.PacketConns.
type sharedPacketConn struct {
net.PacketConn
key string
}
// Destruct closes the underlying socket.
func (spc *sharedPacketConn) Destruct() error {
return spc.PacketConn.Close()
}
// fakeClosedErr returns an error value that is not temporary
// nor a timeout, suitable for making the caller think the
// listener is actually closed
func fakeClosedErr(l interface{ Addr() net.Addr }) error {
return &net.OpError{
Op: "accept",
Net: l.Addr().Network(),
Addr: l.Addr(),
Err: errFakeClosed,
}
}
// errFakeClosed is the underlying error value returned by
// fakeCloseListener.Accept() after Close() has been called,
// indicating that it is pretending to be closed so that the
// server using it can terminate, while the underlying
// socket is actually left open.
var errFakeClosed = fmt.Errorf("listener 'closed' 😉")
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns.
type fakeClosePacketConn struct {
closed int32 // accessed atomically; belongs to this struct only
*sharedPacketConn // embedded, so we also become a net.PacketConn
}
func (fcpc *fakeClosePacketConn) Close() error {
if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
}
return nil
}
// Supports QUIC implementation: https://github.com/caddyserver/caddy/issues/3998
func (fcpc fakeClosePacketConn) SetReadBuffer(bytes int) error {
if conn, ok := fcpc.PacketConn.(interface{ SetReadBuffer(int) error }); ok {
return conn.SetReadBuffer(bytes)
}
return fmt.Errorf("SetReadBuffer() not implemented for %T", fcpc.PacketConn)
}
// Supports QUIC implementation: https://github.com/caddyserver/caddy/issues/3998
func (fcpc fakeClosePacketConn) SyscallConn() (syscall.RawConn, error) {
if conn, ok := fcpc.PacketConn.(interface {
SyscallConn() (syscall.RawConn, error)
}); ok {
return conn.SyscallConn()
}
return nil, fmt.Errorf("SyscallConn() not implemented for %T", fcpc.PacketConn)
}
type fakeCloseQuicListener struct {
closed int32 // accessed atomically; belongs to this struct only
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
uc *unixConn // underlying unix socket, if UDS
context context.Context
contextCancel context.CancelFunc
}
// Currently Accept ignores the passed context, however a situation where
// someone would need a hotswappable QUIC-only (not http3, since it uses context.Background here)
// server on which Accept would be called with non-empty contexts
// (mind that the default net listeners' Accept doesn't take a context argument)
// sounds way too rare for us to sacrifice efficiency here.
func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (quic.EarlyConnection, error) {
conn, err := fcql.sharedQuicListener.Accept(fcql.context)
if err == nil {
return conn, nil
}
// if the listener is "closed", return a fake closed error instead
if atomic.LoadInt32(&fcql.closed) == 1 && errors.Is(err, context.Canceled) {
return nil, fakeClosedErr(fcql)
}
return nil, err
}
func (fcql *fakeCloseQuicListener) Close() error {
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
fcql.contextCancel()
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
if fcql.uc != nil {
// unix sockets need to be closed ourselves because we dup() the file
// descriptor when we reuse them, so this avoids a resource leak
fcql.uc.Close()
}
}
return nil
}
// RegisterNetwork registers a network type with Caddy so that if a listener is
// created for that network type, getListener will be invoked to get the listener.
// This should be called during init() and will panic if the network type is standard
@@ -460,11 +615,77 @@ func RegisterNetwork(network string, getListener ListenerFunc) {
networkTypes[network] = getListener
}
type unixListener struct {
*net.UnixListener
mapKey string
count *int32 // accessed atomically
}
func (uln *unixListener) Close() error {
newCount := atomic.AddInt32(uln.count, -1)
if newCount == 0 {
defer func() {
addr := uln.Addr().String()
unixSocketsMu.Lock()
delete(unixSockets, uln.mapKey)
unixSocketsMu.Unlock()
_ = syscall.Unlink(addr)
}()
}
return uln.UnixListener.Close()
}
type unixConn struct {
*net.UnixConn
filename string
mapKey string
count *int32 // accessed atomically
}
func (uc *unixConn) Close() error {
newCount := atomic.AddInt32(uc.count, -1)
if newCount == 0 {
defer func() {
unixSocketsMu.Lock()
delete(unixSockets, uc.mapKey)
unixSocketsMu.Unlock()
_ = syscall.Unlink(uc.filename)
}()
}
return uc.UnixConn.Close()
}
// unixSockets keeps track of the currently-active unix sockets
// so we can transfer their FDs gracefully during reloads.
var (
unixSockets = make(map[string]interface {
File() (*os.File, error)
})
unixSocketsMu sync.Mutex
)
// getListenerFromPlugin returns a listener on the given network and address
// if a plugin has registered the network name. It may return (nil, nil) if
// no plugin can provide a listener.
func getListenerFromPlugin(ctx context.Context, network, addr string, config net.ListenConfig) (any, error) {
// get listener from plugin if network type is registered
if getListener, ok := networkTypes[network]; ok {
Log().Debug("getting listener from plugin", zap.String("network", network))
return getListener(ctx, network, addr, config)
}
return nil, nil
}
func listenerKey(network, addr string) string {
return network + "/" + addr
}
// ListenerFunc is a function that can return a listener given a network and address.
// The listeners must be capable of overlapping: with Caddy, new configs are loaded
// before old ones are unloaded, so listeners may overlap briefly if the configs
// both need the same listener. EXPERIMENTAL and subject to change.
type ListenerFunc func(network, addr string) (net.Listener, error)
type ListenerFunc func(ctx context.Context, network, addr string, cfg net.ListenConfig) (any, error)
var networkTypes = map[string]ListenerFunc{}
+5 -3
View File
@@ -105,7 +105,7 @@ func (logging *Logging) openLogs(ctx Context) error {
// then set up any other custom logs
for name, l := range logging.Logs {
// the default log is already set up
if name == "default" {
if name == DefaultLoggerName {
continue
}
@@ -138,7 +138,7 @@ func (logging *Logging) setupNewDefault(ctx Context) error {
// extract the user-defined default log, if any
newDefault := new(defaultCustomLog)
if userDefault, ok := logging.Logs["default"]; ok {
if userDefault, ok := logging.Logs[DefaultLoggerName]; ok {
newDefault.CustomLog = userDefault
} else {
// if none, make one with our own default settings
@@ -147,7 +147,7 @@ func (logging *Logging) setupNewDefault(ctx Context) error {
if err != nil {
return fmt.Errorf("setting up default Caddy log: %v", err)
}
logging.Logs["default"] = newDefault.CustomLog
logging.Logs[DefaultLoggerName] = newDefault.CustomLog
}
// set up this new log
@@ -702,6 +702,8 @@ var (
var writers = NewUsagePool()
const DefaultLoggerName = "default"
// Interface guards
var (
_ io.WriteCloser = (*notClosable)(nil)
+29 -12
View File
@@ -119,7 +119,7 @@ func (App) CaddyModule() caddy.ModuleInfo {
// Provision sets up the app.
func (app *App) Provision(ctx caddy.Context) error {
app.logger = ctx.Logger(app)
app.logger = ctx.Logger()
app.subscriptions = make(map[string]map[caddy.ModuleID][]Handler)
for _, sub := range app.Subscriptions {
@@ -201,6 +201,9 @@ func (app *App) On(eventName string, handler Handler) error {
// Emit creates and dispatches an event named eventName to all relevant handlers with
// the metadata data. Events are emitted and propagated synchronously. The returned Event
// value will have any additional information from the invoked handlers.
//
// Note that the data map is not copied, for efficiency. After Emit() is called, the
// data passed in should not be changed in other goroutines.
func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) Event {
logger := app.logger.With(zap.String("name", eventName))
@@ -212,11 +215,11 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
eventName = strings.ToLower(eventName)
e := Event{
Data: data,
id: id,
ts: time.Now(),
name: eventName,
origin: ctx.Module(),
data: data,
}
logger = logger.With(
@@ -244,12 +247,12 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
case "event.module":
return e.origin.CaddyModule().ID, true
case "event.data":
return e.data, true
return e.Data, true
}
if strings.HasPrefix(key, "event.data.") {
key = strings.TrimPrefix(key, "event.data.")
if val, ok := data[key]; ok {
if val, ok := e.Data[key]; ok {
return val, true
}
}
@@ -257,7 +260,7 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
return nil, false
})
logger.Debug("event", zap.Any("data", e.data))
logger.Debug("event", zap.Any("data", e.Data))
// invoke handlers bound to the event by name and also all events; this for loop
// iterates twice at most: once for the event name, once for "" (all events)
@@ -314,26 +317,40 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
}
// Event represents something that has happened or is happening.
// An Event value is not synchronized, so it should be copied if
// being used in goroutines.
//
// EXPERIMENTAL: As with the rest of this package, events are
// subject to change.
type Event struct {
id uuid.UUID
ts time.Time
name string
origin caddy.Module
data map[string]any
// If non-nil, the event has been aborted, meaning
// propagation has stopped to other handlers and
// the code should stop what it was doing. Emitters
// may choose to use this as a signal to adjust their
// code path appropriately.
Aborted error
// The data associated with the event. Usually the
// original emitter will be the only one to set or
// change these values, but the field is exported
// so handlers can have full access if needed.
// However, this map is not synchronized, so
// handlers must not use this map directly in new
// goroutines; instead, copy the map to use it in a
// goroutine.
Data map[string]any
id uuid.UUID
ts time.Time
name string
origin caddy.Module
}
// CloudEvent exports event e as a structure that, when
// serialized as JSON, is compatible with the
// CloudEvents spec.
func (e Event) CloudEvent() CloudEvent {
dataJSON, _ := json.Marshal(e.data)
dataJSON, _ := json.Marshal(e.Data)
return CloudEvent{
ID: e.id.String(),
Source: e.origin.CaddyModule().String(),
+92 -19
View File
@@ -18,6 +18,7 @@ import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"strconv"
"sync"
@@ -65,7 +66,7 @@ func init() {
// `{http.request.orig_uri}` | The request's original URI
// `{http.request.port}` | The port part of the request's Host header
// `{http.request.proto}` | The protocol of the request
// `{http.request.remote.host}` | The host part of the remote client's address
// `{http.request.remote.host}` | The host (IP) part of the remote client's address
// `{http.request.remote.port}` | The port part of the remote client's address
// `{http.request.remote}` | The address of the remote client
// `{http.request.scheme}` | The request scheme
@@ -160,7 +161,7 @@ func (app *App) Provision(ctx caddy.Context) error {
}
app.tlsApp = tlsAppIface.(*caddytls.TLS)
app.ctx = ctx
app.logger = ctx.Logger(app)
app.logger = ctx.Logger()
eventsAppIface, err := ctx.App("events")
if err != nil {
@@ -178,7 +179,9 @@ func (app *App) Provision(ctx caddy.Context) error {
}
// prepare each server
oldContext := ctx.Context
for srvName, srv := range app.Servers {
ctx.Context = context.WithValue(oldContext, ServerCtxKey, srv)
srv.name = srvName
srv.tlsApp = app.tlsApp
srv.events = eventsAppIface.(*caddyevents.App)
@@ -263,7 +266,7 @@ func (app *App) Provision(ctx caddy.Context) error {
// route handler so that important security checks are done, etc.
primaryRoute := emptyHandler
if srv.Routes != nil {
err := srv.Routes.ProvisionHandlers(ctx)
err := srv.Routes.ProvisionHandlers(ctx, srv.Metrics)
if err != nil {
return fmt.Errorf("server %s: setting up route handlers: %v", srvName, err)
}
@@ -293,7 +296,7 @@ func (app *App) Provision(ctx caddy.Context) error {
srv.IdleTimeout = defaultIdleTimeout
}
}
ctx.Context = oldContext
return nil
}
@@ -365,6 +368,7 @@ func (app *App) Start() error {
// this TLS config is used by the std lib to choose the actual TLS config for connections
// by looking through the connection policies to find the first one that matches
tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx)
srv.configureServer(srv.server)
// enable H2C if configured
if srv.protocol("h2c") {
@@ -384,10 +388,11 @@ func (app *App) Start() error {
for portOffset := uint(0); portOffset < listenAddr.PortRangeSize(); portOffset++ {
// create the listener for this socket
hostport := listenAddr.JoinHostPort(portOffset)
ln, err := caddy.ListenTimeout(listenAddr.Network, hostport, time.Duration(srv.KeepAliveInterval))
lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{KeepAlive: time.Duration(srv.KeepAliveInterval)})
if err != nil {
return fmt.Errorf("%s: listening on %s: %v", listenAddr.Network, hostport, err)
return fmt.Errorf("listening on %s: %v", listenAddr.At(portOffset), err)
}
ln := lnAny.(net.Listener)
// wrap listener before TLS (up to the TLS placeholder wrapper)
var lnWrapperIdx int
@@ -407,9 +412,26 @@ func (app *App) Start() error {
// enable HTTP/3 if configured
if srv.protocol("h3") {
app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport))
if err := srv.serveHTTP3(hostport, tlsCfg); err != nil {
return err
// Can't serve HTTP/3 on the same socket as HTTP/1 and 2 because it uses
// a different transport mechanism... which is fine, but the OS doesn't
// differentiate between a SOCK_STREAM file and a SOCK_DGRAM file; they
// are still one file on the system. So even though "unixpacket" and
// "unixgram" are different network types just as "tcp" and "udp" are,
// the OS will not let us use the same file as both STREAM and DGRAM.
if len(srv.Protocols) > 1 && listenAddr.IsUnixNetwork() {
app.logger.Warn("HTTP/3 disabled because Unix can't multiplex STREAM and DGRAM on same socket",
zap.String("file", hostport))
for i := range srv.Protocols {
if srv.Protocols[i] == "h3" {
srv.Protocols = append(srv.Protocols[:i], srv.Protocols[i+1:]...)
break
}
}
} else {
app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport))
if err := srv.serveHTTP3(listenAddr.At(portOffset), tlsCfg); err != nil {
return err
}
}
}
}
@@ -421,11 +443,10 @@ func (app *App) Start() error {
// if binding to port 0, the OS chooses a port for us;
// but the user won't know the port unless we print it
if listenAddr.StartPort == 0 && listenAddr.EndPort == 0 {
if !listenAddr.IsUnixNetwork() && listenAddr.StartPort == 0 && listenAddr.EndPort == 0 {
app.logger.Info("port 0 listener",
zap.String("input_address", lnAddr),
zap.String("actual_address", ln.Addr().String()),
)
zap.String("actual_address", ln.Addr().String()))
}
app.logger.Debug("starting server loop",
@@ -502,22 +523,74 @@ func (app *App) Stop() error {
app.logger.Debug("servers shutting down with eternal grace period")
}
// shut down servers
for _, server := range app.Servers {
// goroutines aren't guaranteed to be scheduled right away,
// so we'll use one WaitGroup to wait for all the goroutines
// to start their server shutdowns, and another to wait for
// them to finish; we'll always block for them to start so
// that when we return the caller can be confident* that the
// old servers are no longer accepting new connections
// (* the scheduler might still pause them right before
// calling Shutdown(), but it's unlikely)
var startedShutdown, finishedShutdown sync.WaitGroup
// these will run in goroutines
stopServer := func(server *Server) {
defer finishedShutdown.Done()
startedShutdown.Done()
if err := server.server.Shutdown(ctx); err != nil {
app.logger.Error("server shutdown",
zap.Error(err),
zap.Strings("addresses", server.Listen))
}
}
stopH3Server := func(server *Server) {
defer finishedShutdown.Done()
startedShutdown.Done()
if server.h3server != nil {
// TODO: CloseGracefully, once implemented upstream (see https://github.com/lucas-clemente/quic-go/issues/2103)
if err := server.h3server.Close(); err != nil {
app.logger.Error("HTTP/3 server shutdown",
if server.h3server == nil {
return
}
// TODO: we have to manually close our listeners because quic-go won't
// close listeners it didn't create along with the server itself...
// see https://github.com/lucas-clemente/quic-go/issues/3560
for _, el := range server.h3listeners {
if err := el.Close(); err != nil {
app.logger.Error("HTTP/3 listener close",
zap.Error(err),
zap.Strings("addresses", server.Listen))
zap.String("address", el.LocalAddr().String()))
}
}
// TODO: CloseGracefully, once implemented upstream (see https://github.com/lucas-clemente/quic-go/issues/2103)
if err := server.h3server.Close(); err != nil {
app.logger.Error("HTTP/3 server shutdown",
zap.Error(err),
zap.Strings("addresses", server.Listen))
}
}
for _, server := range app.Servers {
startedShutdown.Add(2)
finishedShutdown.Add(2)
go stopServer(server)
go stopH3Server(server)
}
// block until all the goroutines have been run by the scheduler;
// this means that they have likely called Shutdown() by now
startedShutdown.Wait()
// if the process is exiting, we need to block here and wait
// for the grace periods to complete, otherwise the process will
// terminate before the servers are finished shutting down; but
// we don't really need to wait for the grace period to finish
// if the process isn't exiting (but note that frequent config
// reloads with long grace periods for a sustained length of time
// may deplete resources)
if caddy.Exiting() {
finishedShutdown.Wait()
}
return nil
+22 -12
View File
@@ -378,19 +378,29 @@ redirServersLoop:
// we'll create a new server for all the listener addresses
// that are unused and serve the remaining redirects from it
for _, srv := range app.Servers {
if srv.hasListenerAddress(redirServerAddr) {
// find the index of the route after the last route with a host
// matcher, then insert the redirects there, but before any
// user-defined catch-all routes
// see https://github.com/caddyserver/caddy/issues/3212
insertIndex := srv.findLastRouteWithHostMatcher()
srv.Routes = append(srv.Routes[:insertIndex], append(routes, srv.Routes[insertIndex:]...)...)
// append our catch-all route in case the user didn't define their own
srv.Routes = appendCatchAll(srv.Routes)
continue redirServersLoop
// only look at servers which listen on an address which
// we want to add redirects to
if !srv.hasListenerAddress(redirServerAddr) {
continue
}
// find the index of the route after the last route with a host
// matcher, then insert the redirects there, but before any
// user-defined catch-all routes
// see https://github.com/caddyserver/caddy/issues/3212
insertIndex := srv.findLastRouteWithHostMatcher()
// add the redirects at the insert index, except for when
// we have a catch-all for HTTPS, in which case the user's
// defined catch-all should take precedence. See #4829
if len(uniqueDomainsForCerts) != 0 {
srv.Routes = append(srv.Routes[:insertIndex], append(routes, srv.Routes[insertIndex:]...)...)
}
// append our catch-all route in case the user didn't define their own
srv.Routes = appendCatchAll(srv.Routes)
continue redirServersLoop
}
// no server with this listener address exists;
+1 -1
View File
@@ -56,7 +56,7 @@ func (Authentication) CaddyModule() caddy.ModuleInfo {
// Provision sets up a.
func (a *Authentication) Provision(ctx caddy.Context) error {
a.logger = ctx.Logger(a)
a.logger = ctx.Logger()
a.Providers = make(map[string]Authenticator)
mods, err := ctx.LoadModule(a, "ProvidersRaw")
if err != nil {
+1 -1
View File
@@ -92,7 +92,7 @@ func (ScryptHash) CaddyModule() caddy.ModuleInfo {
// Provision sets up s.
func (s *ScryptHash) Provision(ctx caddy.Context) error {
s.SetDefaults()
ctx.Logger(s).Warn("use of 'scrypt' is deprecated, please use 'bcrypt' instead")
ctx.Logger().Warn("use of 'scrypt' is deprecated, please use 'bcrypt' instead")
return nil
}
+8 -8
View File
@@ -90,7 +90,7 @@ func (m *MatchExpression) UnmarshalJSON(data []byte) error {
// Provision sets ups m.
func (m *MatchExpression) Provision(ctx caddy.Context) error {
m.log = ctx.Logger(m)
m.log = ctx.Logger()
// replace placeholders with a function call - this is just some
// light (and possibly naïve) syntactic sugar
@@ -294,13 +294,13 @@ type CELLibraryProducer interface {
// CELMatcherImpl creates a new cel.Library based on the following pieces of
// data:
//
// - macroName: the function name to be used within CEL. This will be a macro
// and not a function proper.
// - funcName: the function overload name generated by the CEL macro used to
// represent the matcher.
// - matcherDataTypes: the argument types to the macro.
// - fac: a matcherFactory implementation which converts from CEL constant
// values to a Matcher instance.
// - macroName: the function name to be used within CEL. This will be a macro
// and not a function proper.
// - funcName: the function overload name generated by the CEL macro used to
// represent the matcher.
// - matcherDataTypes: the argument types to the macro.
// - fac: a matcherFactory implementation which converts from CEL constant
// values to a Matcher instance.
//
// Note, macro names and function names must not collide with other macros or
// functions exposed within CEL expressions, or an error will be produced
+49 -76
View File
@@ -20,7 +20,6 @@
package encode
import (
"bytes"
"fmt"
"io"
"math"
@@ -160,13 +159,12 @@ func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter
// initResponseWriter initializes the responseWriter instance
// allocated in openResponseWriter, enabling mid-stack inlining.
func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
// The allocation of ResponseWriterWrapper might be optimized as well.
rw.ResponseWriterWrapper = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW}
if httpInterfaces, ok := wrappedRW.(caddyhttp.HTTPInterfaces); ok {
rw.HTTPInterfaces = httpInterfaces
} else {
rw.HTTPInterfaces = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW}
}
rw.encodingName = encodingName
rw.buf = buf
rw.config = enc
return rw
@@ -176,10 +174,9 @@ func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, w
// using the encoding represented by encodingName and
// configured by config.
type responseWriter struct {
*caddyhttp.ResponseWriterWrapper
caddyhttp.HTTPInterfaces
encodingName string
w Encoder
buf *bytes.Buffer
config *Encode
statusCode int
wroteHeader bool
@@ -206,28 +203,33 @@ func (rw *responseWriter) Flush() {
// to rw.Write (see bug in #4314)
return
}
rw.ResponseWriterWrapper.Flush()
rw.HTTPInterfaces.Flush()
}
// Write writes to the response. If the response qualifies,
// it is encoded using the encoder, which is initialized
// if not done so already.
func (rw *responseWriter) Write(p []byte) (int, error) {
var n, written int
var err error
// ignore zero data writes, probably head request
if len(p) == 0 {
return 0, nil
}
if rw.buf != nil && rw.config.MinLength > 0 {
written = rw.buf.Len()
_, err := rw.buf.Write(p)
if err != nil {
return 0, err
// sniff content-type and determine content-length
if !rw.wroteHeader && rw.config.MinLength > 0 {
var gtMinLength bool
if len(p) > rw.config.MinLength {
gtMinLength = true
} else if cl, err := strconv.Atoi(rw.Header().Get("Content-Length")); err == nil && cl > rw.config.MinLength {
gtMinLength = true
}
if gtMinLength {
if rw.Header().Get("Content-Type") == "" {
rw.Header().Set("Content-Type", http.DetectContentType(p))
}
rw.init()
}
rw.init()
p = rw.buf.Bytes()
defer func() {
bufPool.Put(rw.buf)
rw.buf = nil
}()
}
// before we write to the response, we need to make
@@ -236,63 +238,41 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
// and if so, that means we haven't written the
// header OR the default status code will be written
// by the standard library
if rw.statusCode > 0 {
rw.ResponseWriter.WriteHeader(rw.statusCode)
rw.statusCode = 0
if !rw.wroteHeader {
if rw.statusCode != 0 {
rw.HTTPInterfaces.WriteHeader(rw.statusCode)
}
rw.wroteHeader = true
}
switch {
case rw.w != nil:
n, err = rw.w.Write(p)
default:
n, err = rw.ResponseWriter.Write(p)
if rw.w != nil {
return rw.w.Write(p)
} else {
return rw.HTTPInterfaces.Write(p)
}
n -= written
if n < 0 {
n = 0
}
return n, err
}
// Close writes any remaining buffered response and
// deallocates any active resources.
func (rw *responseWriter) Close() error {
var err error
// only attempt to write the remaining buffered response
// if there are any bytes left to write; otherwise, if
// the handler above us returned an error without writing
// anything, we'd write to the response when we instead
// should simply let the error propagate back down; this
// is why the check for rw.buf.Len() > 0 is crucial
if rw.buf != nil && rw.buf.Len() > 0 {
rw.init()
p := rw.buf.Bytes()
defer func() {
bufPool.Put(rw.buf)
rw.buf = nil
}()
switch {
case rw.w != nil:
_, err = rw.w.Write(p)
default:
_, err = rw.ResponseWriter.Write(p)
// didn't write, probably head request
if !rw.wroteHeader {
cl, err := strconv.Atoi(rw.Header().Get("Content-Length"))
if err == nil && cl > rw.config.MinLength {
rw.init()
}
// issue #5059, don't write status code if not set explicitly.
if rw.statusCode != 0 {
rw.HTTPInterfaces.WriteHeader(rw.statusCode)
}
} else if rw.statusCode != 0 {
// it is possible that a body was not written, and
// a header was not even written yet, even though
// we are closing; ensure the proper status code is
// written exactly once, or we risk breaking requests
// that rely on If-None-Match, for example
rw.ResponseWriter.WriteHeader(rw.statusCode)
rw.statusCode = 0
rw.wroteHeader = true
}
var err error
if rw.w != nil {
err2 := rw.w.Close()
if err2 != nil && err == nil {
err = err2
}
err = rw.w.Close()
rw.w.Reset(nil)
rw.config.writerPools[rw.encodingName].Put(rw.w)
rw.w = nil
}
@@ -302,16 +282,15 @@ func (rw *responseWriter) Close() error {
// init should be called before we write a response, if rw.buf has contents.
func (rw *responseWriter) init() {
if rw.Header().Get("Content-Encoding") == "" &&
rw.buf.Len() >= rw.config.MinLength &&
rw.config.Match(rw) {
rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
rw.w.Reset(rw.ResponseWriter)
rw.w.Reset(rw.HTTPInterfaces)
rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975
rw.Header().Set("Content-Encoding", rw.encodingName)
rw.Header().Add("Vary", "Accept-Encoding")
rw.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-encoded content
}
rw.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-encoded content
}
// AcceptedEncodings returns the list of encodings that the
@@ -417,12 +396,6 @@ type Precompressed interface {
Suffix() string
}
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
// defaultMinLength is the minimum length at which to compress content.
const defaultMinLength = 512
+4 -3
View File
@@ -16,6 +16,7 @@ package fileserver
import (
"bytes"
"context"
_ "embed"
"encoding/json"
"fmt"
@@ -82,7 +83,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
// calling path.Clean here prevents weird breadcrumbs when URL paths are sketchy like /%2e%2e%2f
listing, err := fsrv.loadDirectoryContents(dir.(fs.ReadDirFile), root, path.Clean(r.URL.Path), repl)
listing, err := fsrv.loadDirectoryContents(r.Context(), dir.(fs.ReadDirFile), root, path.Clean(r.URL.Path), repl)
switch {
case os.IsPermission(err):
return caddyhttp.Error(http.StatusForbidden, err)
@@ -136,7 +137,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
return nil
}
func (fsrv *FileServer) loadDirectoryContents(dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (browseTemplateContext, error) {
func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (browseTemplateContext, error) {
files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable
if err != nil && err != io.EOF {
return browseTemplateContext{}, err
@@ -145,7 +146,7 @@ func (fsrv *FileServer) loadDirectoryContents(dir fs.ReadDirFile, root, urlPath
// user can presumably browse "up" to parent folder if path is longer than "/"
canGoUp := len(urlPath) > 1
return fsrv.directoryListing(files, canGoUp, root, urlPath, repl), nil
return fsrv.directoryListing(ctx, files, canGoUp, root, urlPath, repl), nil
}
// browseApplyQueryParams applies query parameters to the listing.
+12
View File
@@ -27,6 +27,10 @@ a:visited {
color: #800080;
}
a:visited:hover {
color: #b900b9;
}
header,
#summary {
padding-left: 5%;
@@ -244,6 +248,14 @@ footer {
color: #62b2fd;
}
a:visited {
color: #c269c2;
}
a:visited:hover {
color: #d03cd0;
}
tr {
border-bottom: 1px dashed rgba(255, 255, 255, 0.12);
}
@@ -15,6 +15,7 @@
package fileserver
import (
"context"
"io/fs"
"net/url"
"os"
@@ -30,13 +31,17 @@ import (
"go.uber.org/zap"
)
func (fsrv *FileServer) directoryListing(entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) browseTemplateContext {
func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) browseTemplateContext {
filesToHide := fsrv.transformHidePaths(repl)
var dirCount, fileCount int
fileInfos := []fileInfo{}
for _, entry := range entries {
if err := ctx.Err(); err != nil {
break
}
name := entry.Name()
if fileHidden(name, filesToHide) {
+11
View File
@@ -27,6 +27,7 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
caddytpl "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
"github.com/caddyserver/certmagic"
"go.uber.org/zap"
)
func init() {
@@ -56,6 +57,7 @@ respond with a file listing.`,
fs.Bool("browse", false, "Enable directory browsing")
fs.Bool("templates", false, "Enable template rendering")
fs.Bool("access-log", false, "Enable the access log")
fs.Bool("debug", false, "Enable verbose debug logs")
return fs
}(),
})
@@ -70,6 +72,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
browse := fs.Bool("browse")
templates := fs.Bool("templates")
accessLog := fs.Bool("access-log")
debug := fs.Bool("debug")
var handlers []json.RawMessage
@@ -130,6 +133,14 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
},
}
if debug {
cfg.Logging = &caddy.Logging{
Logs: map[string]*caddy.CustomLog{
"default": {Level: zap.DebugLevel.CapitalString()},
},
}
}
err := caddy.Run(cfg)
if err != nil {
return caddy.ExitCodeFailedStartup, err
+1 -1
View File
@@ -256,7 +256,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
// Provision sets up m's defaults.
func (m *MatchFile) Provision(ctx caddy.Context) error {
m.logger = ctx.Logger(m)
m.logger = ctx.Logger()
// establish the file system to use
if len(m.FileSystemRaw) > 0 {
+2 -2
View File
@@ -167,7 +167,7 @@ func (FileServer) CaddyModule() caddy.ModuleInfo {
// Provision sets up the static files responder.
func (fsrv *FileServer) Provision(ctx caddy.Context) error {
fsrv.logger = ctx.Logger(fsrv)
fsrv.logger = ctx.Logger()
// establish which file system (possibly a virtual one) we'll be using
if len(fsrv.FileSystemRaw) > 0 {
@@ -247,7 +247,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
info, err := fs.Stat(fsrv.fileSystem, filename)
if err != nil {
err = fsrv.mapDirOpenError(err, filename)
if errors.Is(err, fs.ErrNotExist) {
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrInvalid) {
return fsrv.notFound(w, r, next)
} else if errors.Is(err, fs.ErrPermission) {
return caddyhttp.Error(http.StatusForbidden, err)
+7 -8
View File
@@ -32,12 +32,12 @@ func init() {
// parseCaddyfile sets up the handler for response headers from
// Caddyfile tokens. Syntax:
//
// header [<matcher>] [[+|-|?]<field> [<value|regexp>] [<replacement>]] {
// [+]<field> [<value|regexp> [<replacement>]]
// ?<field> <default_value>
// -<field>
// [defer]
// }
// header [<matcher>] [[+|-|?]<field> [<value|regexp>] [<replacement>]] {
// [+]<field> [<value|regexp> [<replacement>]]
// ?<field> <default_value>
// -<field>
// [defer]
// }
//
// Either a block can be opened or a single header field can be configured
// in the first line, but not both in the same directive. Header operations
@@ -148,8 +148,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
// parseReqHdrCaddyfile sets up the handler for request headers
// from Caddyfile tokens. Syntax:
//
// request_header [<matcher>] [[+|-]<field> [<value|regexp>] [<replacement>]]
//
// request_header [<matcher>] [[+|-]<field> [<value|regexp>] [<replacement>]]
func parseReqHdrCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
if !h.Next() {
return nil, h.ArgErr()
+4 -1
View File
@@ -332,7 +332,10 @@ func (rww *responseWriterWrapper) WriteHeader(status int) {
if rww.wroteHeader {
return
}
rww.wroteHeader = true
// 1xx responses aren't final; just informational
if status < 100 || status > 199 {
rww.wroteHeader = true
}
if rww.require == nil || rww.require.Match(status, rww.ResponseWriterWrapper.Header()) {
if rww.headerOps != nil {
rww.headerOps.ApplyTo(rww.ResponseWriterWrapper.Header(), rww.replacer)
+144
View File
@@ -0,0 +1,144 @@
// 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 caddyhttp
import (
"errors"
"net"
"net/http"
"strings"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// ServerLogConfig describes a server's logging configuration. If
// enabled without customization, all requests to this server are
// logged to the default logger; logger destinations may be
// customized per-request-host.
type ServerLogConfig struct {
// The default logger name for all logs emitted by this server for
// hostnames that are not in the LoggerNames (logger_names) map.
DefaultLoggerName string `json:"default_logger_name,omitempty"`
// LoggerNames maps request hostnames to a custom logger name.
// For example, a mapping of "example.com" to "example" would
// cause access logs from requests with a Host of example.com
// to be emitted by a logger named "http.log.access.example".
LoggerNames map[string]string `json:"logger_names,omitempty"`
// By default, all requests to this server will be logged if
// access logging is enabled. This field lists the request
// hosts for which access logging should be disabled.
SkipHosts []string `json:"skip_hosts,omitempty"`
// If true, requests to any host not appearing in the
// LoggerNames (logger_names) map will not be logged.
SkipUnmappedHosts bool `json:"skip_unmapped_hosts,omitempty"`
// If true, credentials that are otherwise omitted, will be logged.
// The definition of credentials is defined by https://fetch.spec.whatwg.org/#credentials,
// and this includes some request and response headers, i.e `Cookie`,
// `Set-Cookie`, `Authorization`, and `Proxy-Authorization`.
ShouldLogCredentials bool `json:"should_log_credentials,omitempty"`
}
// wrapLogger wraps logger in a logger named according to user preferences for the given host.
func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, host string) *zap.Logger {
if loggerName := slc.getLoggerName(host); loggerName != "" {
return logger.Named(loggerName)
}
return logger
}
func (slc ServerLogConfig) getLoggerName(host string) string {
tryHost := func(key string) (string, bool) {
// first try exact match
if loggerName, ok := slc.LoggerNames[key]; ok {
return loggerName, ok
}
// strip port and try again (i.e. Host header of "example.com:1234" should
// match "example.com" if there is no "example.com:1234" in the map)
hostOnly, _, err := net.SplitHostPort(key)
if err != nil {
return "", false
}
loggerName, ok := slc.LoggerNames[hostOnly]
return loggerName, ok
}
// try the exact hostname first
if loggerName, ok := tryHost(host); ok {
return loggerName
}
// try matching wildcard domains if other non-specific loggers exist
labels := strings.Split(host, ".")
for i := range labels {
if labels[i] == "" {
continue
}
labels[i] = "*"
wildcardHost := strings.Join(labels, ".")
if loggerName, ok := tryHost(wildcardHost); ok {
return loggerName
}
}
return slc.DefaultLoggerName
}
func (slc *ServerLogConfig) clone() *ServerLogConfig {
clone := &ServerLogConfig{
DefaultLoggerName: slc.DefaultLoggerName,
LoggerNames: make(map[string]string),
SkipHosts: append([]string{}, slc.SkipHosts...),
SkipUnmappedHosts: slc.SkipUnmappedHosts,
ShouldLogCredentials: slc.ShouldLogCredentials,
}
for k, v := range slc.LoggerNames {
clone.LoggerNames[k] = v
}
return clone
}
// errLogValues inspects err and returns the status code
// to use, the error log message, and any extra fields.
// If err is a HandlerError, the returned values will
// have richer information.
func errLogValues(err error) (status int, msg string, fields []zapcore.Field) {
var handlerErr HandlerError
if errors.As(err, &handlerErr) {
status = handlerErr.StatusCode
if handlerErr.Err == nil {
msg = err.Error()
} else {
msg = handlerErr.Err.Error()
}
fields = []zapcore.Field{
zap.Int("status", handlerErr.StatusCode),
zap.String("err_id", handlerErr.ID),
zap.String("err_trace", handlerErr.Trace),
}
return
}
status = http.StatusInternalServerError
msg = err.Error()
return
}
// Variable name used to indicate that this request
// should be omitted from the access logs
const SkipLogVar = "skip_log"
+2 -11
View File
@@ -109,22 +109,13 @@ func (h *Handler) Validate() error {
}
seen[input] = i
// prevent infinite recursion
for _, out := range m.Outputs {
for _, dest := range h.Destinations {
if strings.Contains(caddy.ToString(out), dest) ||
strings.Contains(m.Input, dest) {
return fmt.Errorf("mapping %d requires value of {%s} to define value of {%s}: infinite recursion", i, dest, dest)
}
}
}
// ensure mappings have 1:1 output-to-destination correspondence
nOut := len(m.Outputs)
if nOut != nDest {
return fmt.Errorf("mapping %d has %d outputs but there are %d destinations defined", i, nOut, nDest)
}
}
return nil
}
@@ -169,7 +160,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhtt
// fall back to default if no match or if matched nil value
if len(h.Defaults) > destIdx {
return h.Defaults[destIdx], true
return repl.ReplaceAll(h.Defaults[destIdx], ""), true
}
return nil, true
+22
View File
@@ -98,6 +98,28 @@ func TestHandler(t *testing.T) {
"output": "testing",
},
},
{
reqURI: "/foo",
handler: Handler{
Source: "{http.request.uri.path}",
Destinations: []string{"{output}"},
Defaults: []string{"default"},
},
expect: map[string]any{
"output": "default",
},
},
{
reqURI: "/foo",
handler: Handler{
Source: "{http.request.uri.path}",
Destinations: []string{"{output}"},
Defaults: []string{"{testvar}"},
},
expect: map[string]any{
"output": "testing",
},
},
} {
if err := tc.handler.Provision(caddy.Context{}); err != nil {
t.Fatalf("Test %d: Provisioning handler: %v", i, err)
+4 -2
View File
@@ -156,7 +156,9 @@ type (
MatchHeaderRE map[string]*MatchRegexp
// MatchProtocol matches requests by protocol. Recognized values are
// "http", "https", and "grpc".
// "http", "https", and "grpc" for broad protocol matches, or specific
// HTTP versions can be specified like so: "http/1", "http/1.1",
// "http/2", "http/3", or minimum versions: "http/2+", etc.
MatchProtocol string
// MatchRemoteIP matches requests by client IP (or CIDR range).
@@ -1326,7 +1328,7 @@ func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
// Provision parses m's IP ranges, either from IP or CIDR expressions.
func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
m.logger = ctx.Logger(m)
m.logger = ctx.Logger()
for _, str := range m.Ranges {
// Exclude the zone_id from the IP
if strings.Contains(str, "%") {
+4
View File
@@ -11,6 +11,10 @@ import (
"github.com/prometheus/client_golang/prometheus/promauto"
)
// Metrics configures metrics observations.
// EXPERIMENTAL and subject to change or removal.
type Metrics struct{}
var httpMetrics = struct {
init sync.Once
requestInFlight *prometheus.GaugeVec
+1 -1
View File
@@ -61,7 +61,7 @@ func (Handler) CaddyModule() caddy.ModuleInfo {
// Provision sets up h.
func (h *Handler) Provision(ctx caddy.Context) error {
h.logger = ctx.Logger(h)
h.logger = ctx.Logger()
if h.Headers != nil {
err := h.Headers.Provision(ctx)
if err != nil {
+32
View File
@@ -31,6 +31,7 @@ import (
"io"
"net"
"net/http"
"net/netip"
"net/textproto"
"net/url"
"path"
@@ -196,6 +197,37 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
return or.URL.RawQuery, true
}
// remote IP range/prefix (e.g. keep top 24 bits of 1.2.3.4 => "1.2.3.0/24")
// syntax: "/V4,V6" where V4 = IPv4 bits, and V6 = IPv6 bits; if no comma, then same bit length used for both
// (EXPERIMENTAL)
if strings.HasPrefix(key, "http.request.remote.host/") {
host, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
host = req.RemoteAddr // assume no port, I guess?
}
addr, err := netip.ParseAddr(host)
if err != nil {
return host, true // not an IP address
}
// extract the bits from the end of the placeholder (start after "/") then split on ","
bitsBoth := key[strings.Index(key, "/")+1:]
ipv4BitsStr, ipv6BitsStr, cutOK := strings.Cut(bitsBoth, ",")
bitsStr := ipv4BitsStr
if addr.Is6() && cutOK {
bitsStr = ipv6BitsStr
}
// convert to integer then compute prefix
bits, err := strconv.Atoi(bitsStr)
if err != nil {
return "", true
}
prefix, err := addr.Prefix(bits)
if err != nil {
return "", true
}
return prefix.String(), true
}
// hostname labels
if strings.HasPrefix(key, reqHostLabelsReplPrefix) {
idxStr := key[len(reqHostLabelsReplPrefix):]
+16 -4
View File
@@ -32,7 +32,7 @@ func TestHTTPVarReplacement(t *testing.T) {
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
req.Host = "example.com:80"
req.RemoteAddr = "localhost:1234"
req.RemoteAddr = "192.168.159.32:1234"
clientCert := []byte(`-----BEGIN CERTIFICATE-----
MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk
@@ -61,7 +61,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
req.TLS = &tls.ConnectionState{
Version: tls.VersionTLS13,
HandshakeComplete: true,
ServerName: "foo.com",
ServerName: "example.com",
CipherSuite: tls.TLS_AES_256_GCM_SHA384,
PeerCertificates: []*x509.Certificate{cert},
NegotiatedProtocol: "h2",
@@ -97,7 +97,19 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
},
{
get: "http.request.remote.host",
expect: "localhost",
expect: "192.168.159.32",
},
{
get: "http.request.remote.host/24",
expect: "192.168.159.0/24",
},
{
get: "http.request.remote.host/24,32",
expect: "192.168.159.0/24",
},
{
get: "http.request.remote.host/999",
expect: "",
},
{
get: "http.request.remote.port",
@@ -146,7 +158,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
},
{
get: "http.request.tls.server_name",
expect: "foo.com",
expect: "example.com",
},
{
get: "http.request.tls.version",
+5 -2
View File
@@ -170,9 +170,12 @@ func (rr *responseRecorder) WriteHeader(statusCode int) {
return
}
// save statusCode always, in case HTTP middleware upgrades websocket
// connections by manually setting headers and writing status 101
rr.statusCode = statusCode
// 1xx responses aren't final; just informational
if statusCode < 100 || statusCode > 199 {
rr.statusCode = statusCode
rr.wroteHeader = true
// decide whether we should buffer the response
@@ -185,7 +188,7 @@ func (rr *responseRecorder) WriteHeader(statusCode int) {
// if informational or not buffered, immediately write header
if rr.stream || (100 <= statusCode && statusCode <= 199) {
rr.ResponseWriterWrapper.WriteHeader(rr.statusCode)
rr.ResponseWriterWrapper.WriteHeader(statusCode)
}
}
+2 -2
View File
@@ -537,9 +537,9 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if !d.NextArg() {
return d.ArgErr()
}
size, err := strconv.Atoi(d.Val())
size, err := humanize.ParseBytes(d.Val())
if err != nil {
return d.Errf("invalid size (bytes): %s", d.Val())
return d.Errf("invalid byte size '%s': %v", d.Val(), err)
}
if d.NextArg() {
return d.ArgErr()
+43 -8
View File
@@ -28,6 +28,7 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"go.uber.org/zap"
)
func init() {
@@ -41,6 +42,7 @@ A simple but production-ready reverse proxy. Useful for quick deployments,
demos, and development.
Simply shuttles HTTP(S) traffic from the --from address to the --to address.
Multiple --to addresses may be specified by repeating the flag.
Unless otherwise specified in the addresses, the --from address will be
assumed to be HTTPS if a hostname is given, and the --to address will be
@@ -57,10 +59,11 @@ default, all incoming headers are passed through unmodified.)
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("reverse-proxy", flag.ExitOnError)
fs.String("from", "localhost", "Address on which to receive traffic")
fs.String("to", "", "Upstream address to which traffic should be sent")
fs.Var(&reverseProxyCmdTo, "to", "Upstream address(es) to which traffic should be sent")
fs.Bool("change-host-header", false, "Set upstream Host header to address of upstream")
fs.Bool("insecure", false, "Disable TLS verification (WARNING: DISABLES SECURITY BY NOT VERIFYING SSL CERTIFICATES!)")
fs.Bool("internal-certs", false, "Use internal CA for issuing certs")
fs.Bool("debug", false, "Enable verbose debug logs")
return fs
}(),
})
@@ -70,15 +73,15 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
caddy.TrapSignals()
from := fs.String("from")
to := fs.String("to")
changeHost := fs.Bool("change-host-header")
insecure := fs.Bool("insecure")
internalCerts := fs.Bool("internal-certs")
debug := fs.Bool("debug")
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
if to == "" {
if len(reverseProxyCmdTo) == 0 {
return caddy.ExitCodeFailedStartup, fmt.Errorf("--to is required")
}
@@ -106,9 +109,18 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
}
// set up the upstream address; assume missing information from given parts
toAddr, toScheme, err := parseUpstreamDialAddress(to)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid upstream address %s: %v", to, err)
// mixing schemes isn't supported, so use first defined (if available)
toAddresses := make([]string, len(reverseProxyCmdTo))
var toScheme string
for i, toLoc := range reverseProxyCmdTo {
addr, scheme, err := parseUpstreamDialAddress(toLoc)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid upstream address %s: %v", toLoc, err)
}
if scheme != "" && toScheme == "" {
toScheme = scheme
}
toAddresses[i] = addr
}
// proceed to build the handler and server
@@ -120,9 +132,16 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
}
}
upstreamPool := UpstreamPool{}
for _, toAddr := range toAddresses {
upstreamPool = append(upstreamPool, &Upstream{
Dial: toAddr,
})
}
handler := Handler{
TransportRaw: caddyconfig.JSONModuleObject(ht, "protocol", "http", nil),
Upstreams: UpstreamPool{{Dial: toAddr}},
Upstreams: upstreamPool,
}
if changeHost {
@@ -182,12 +201,28 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
AppsRaw: appsRaw,
}
if debug {
cfg.Logging = &caddy.Logging{
Logs: map[string]*caddy.CustomLog{
"default": {Level: zap.DebugLevel.CapitalString()},
},
}
}
err = caddy.Run(cfg)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
fmt.Printf("Caddy proxying %s -> %s\n", fromAddr.String(), toAddr)
for _, to := range toAddresses {
fmt.Printf("Caddy proxying %s -> %s\n", fromAddr.String(), to)
}
if len(toAddresses) > 1 {
fmt.Println("Load balancing policy: random")
}
select {}
}
// reverseProxyCmdTo holds the parsed values from repeated use of the --to flag.
var reverseProxyCmdTo caddycmd.StringSlice
@@ -348,7 +348,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
}
redirHandler := caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
Headers: http.Header{"Location": []string{"{http.request.uri.path}/"}},
Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}},
}
redirRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
@@ -171,7 +171,7 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
log.Println("c: send data length ≈", length, string(content))
conn.Close()
time.Sleep(1 * time.Second)
time.Sleep(250 * time.Millisecond)
if bytes.Contains(content, []byte("FAILED")) {
globalt.Error("Server return failed message")
@@ -230,7 +230,7 @@ func DisabledTest(t *testing.T) {
}
}()
time.Sleep(1 * time.Second)
time.Sleep(250 * time.Millisecond)
// init
fcgiParams := make(map[string]string)
@@ -94,7 +94,7 @@ func (Transport) CaddyModule() caddy.ModuleInfo {
// Provision sets up t.
func (t *Transport) Provision(ctx caddy.Context) error {
t.logger = ctx.Logger(t)
t.logger = ctx.Logger()
if t.Root == "" {
t.Root = "{http.vars.root}"
@@ -38,29 +38,28 @@ func init() {
// configured for most™️ auth gateways that support forward auth. The typical
// config which looks something like this:
//
// forward_auth auth-gateway:9091 {
// uri /authenticate?redirect=https://auth.example.com
// copy_headers Remote-User Remote-Email
// }
// forward_auth auth-gateway:9091 {
// uri /authenticate?redirect=https://auth.example.com
// copy_headers Remote-User Remote-Email
// }
//
// is equivalent to a reverse_proxy directive like this:
//
// reverse_proxy auth-gateway:9091 {
// method GET
// rewrite /authenticate?redirect=https://auth.example.com
// reverse_proxy auth-gateway:9091 {
// method GET
// rewrite /authenticate?redirect=https://auth.example.com
//
// header_up X-Forwarded-Method {method}
// header_up X-Forwarded-Uri {uri}
//
// @good status 2xx
// handle_response @good {
// request_header {
// Remote-User {http.reverse_proxy.header.Remote-User}
// Remote-Email {http.reverse_proxy.header.Remote-Email}
// }
// }
// }
// header_up X-Forwarded-Method {method}
// header_up X-Forwarded-Uri {uri}
//
// @good status 2xx
// handle_response @good {
// request_header {
// Remote-User {http.reverse_proxy.header.Remote-User}
// Remote-Email {http.reverse_proxy.header.Remote-Email}
// }
// }
// }
func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
if !h.Next() {
return nil, h.ArgErr()
@@ -196,9 +195,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
// need at least one handler in the routes for the response handling
// logic in reverse_proxy to not skip this entry as empty.
for from, to := range headersToCopy {
handler.Request.Set[to] = []string{
"{http.reverse_proxy.header." + from + "}",
}
handler.Request.Set.Set(to, "{http.reverse_proxy.header."+http.CanonicalHeaderKey(from)+"}")
}
goodResponseHandler.Routes = append(
@@ -195,7 +195,7 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
TCPConn: tcpConn,
readTimeout: time.Duration(h.ReadTimeout),
writeTimeout: time.Duration(h.WriteTimeout),
logger: caddyCtx.Logger(h),
logger: caddyCtx.Logger(),
}
}
@@ -217,7 +217,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
}
h.events = eventAppIface.(*caddyevents.App)
h.ctx = ctx
h.logger = ctx.Logger(h)
h.logger = ctx.Logger()
h.connections = make(map[io.ReadWriteCloser]openConnection)
h.connectionsMu = new(sync.Mutex)
@@ -782,8 +782,9 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
copyHeader(h, http.Header(header))
rw.WriteHeader(code)
// Clear headers, it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses
for k := range h {
// Clear headers coming from the backend
// (it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses)
for k := range header {
delete(h, k)
}
+2 -2
View File
@@ -76,7 +76,7 @@ func (SRVUpstreams) CaddyModule() caddy.ModuleInfo {
}
func (su *SRVUpstreams) Provision(ctx caddy.Context) error {
su.logger = ctx.Logger(su)
su.logger = ctx.Logger()
if su.Refresh == 0 {
su.Refresh = caddy.Duration(time.Minute)
}
@@ -383,7 +383,7 @@ func (MultiUpstreams) CaddyModule() caddy.ModuleInfo {
}
func (mu *MultiUpstreams) Provision(ctx caddy.Context) error {
mu.logger = ctx.Logger(mu)
mu.logger = ctx.Logger()
if mu.SourcesRaw != nil {
mod, err := ctx.LoadModule(mu, "SourcesRaw")
+8 -3
View File
@@ -101,7 +101,7 @@ func (Rewrite) CaddyModule() caddy.ModuleInfo {
// Provision sets up rewr.
func (rewr *Rewrite) Provision(ctx caddy.Context) error {
rewr.logger = ctx.Logger(rewr)
rewr.logger = ctx.Logger()
for i, rep := range rewr.PathRegexp {
if rep.Find == "" {
@@ -383,8 +383,13 @@ func trimPathPrefix(escapedPath, prefix string) string {
iPrefix++
}
// found matching prefix, trim it
return escapedPath[iPath:]
// if we iterated through the entire prefix, we found it, so trim it
if iPath >= len(prefix) {
return escapedPath[iPath:]
}
// otherwise we did not find the prefix
return escapedPath
}
func reverse(s string) string {
+10
View File
@@ -225,6 +225,16 @@ func TestRewrite(t *testing.T) {
input: newRequest(t, "GET", "/prefix/foo/bar"),
expect: newRequest(t, "GET", "/foo/bar"),
},
{
rule: Rewrite{StripPathPrefix: "/prefix"},
input: newRequest(t, "GET", "/prefix"),
expect: newRequest(t, "GET", ""),
},
{
rule: Rewrite{StripPathPrefix: "/prefix"},
input: newRequest(t, "GET", "/"),
expect: newRequest(t, "GET", "/"),
},
{
rule: Rewrite{StripPathPrefix: "/prefix"},
input: newRequest(t, "GET", "/prefix/foo%2Fbar"),
+10 -7
View File
@@ -130,7 +130,7 @@ func (routes RouteList) Provision(ctx caddy.Context) error {
if err != nil {
return err
}
return routes.ProvisionHandlers(ctx)
return routes.ProvisionHandlers(ctx, nil)
}
// ProvisionMatchers sets up all the matchers by loading the
@@ -156,7 +156,7 @@ func (routes RouteList) ProvisionMatchers(ctx caddy.Context) error {
// handler modules. Only call this method directly if you need
// to set up matchers and handlers separately without having
// to provision a second time; otherwise use Provision instead.
func (routes RouteList) ProvisionHandlers(ctx caddy.Context) error {
func (routes RouteList) ProvisionHandlers(ctx caddy.Context, metrics *Metrics) error {
for i := range routes {
handlersIface, err := ctx.LoadModule(&routes[i], "HandlersRaw")
if err != nil {
@@ -168,7 +168,7 @@ func (routes RouteList) ProvisionHandlers(ctx caddy.Context) error {
// pre-compile the middleware handler chain
for _, midhandler := range routes[i].Handlers {
routes[i].middleware = append(routes[i].middleware, wrapMiddleware(ctx, midhandler))
routes[i].middleware = append(routes[i].middleware, wrapMiddleware(ctx, midhandler, metrics))
}
}
return nil
@@ -270,9 +270,12 @@ func wrapRoute(route Route) Middleware {
// we need to pull this particular MiddlewareHandler
// pointer into its own stack frame to preserve it so it
// won't be overwritten in future loop iterations.
func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler) Middleware {
// wrap the middleware with metrics instrumentation
metricsHandler := newMetricsInstrumentedHandler(caddy.GetModuleName(mh), mh)
func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler, metrics *Metrics) Middleware {
handlerToUse := mh
if metrics != nil {
// wrap the middleware with metrics instrumentation
handlerToUse = newMetricsInstrumentedHandler(caddy.GetModuleName(mh), mh)
}
return func(next Handler) Handler {
// copy the next handler (it's an interface, so it's
@@ -284,7 +287,7 @@ func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler) Middleware {
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
// TODO: This is where request tracing could be implemented
// TODO: see what the std lib gives us in terms of stack tracing too
return metricsHandler.ServeHTTP(w, r, nextCopy)
return handlerToUse.ServeHTTP(w, r, nextCopy)
})
}
}
+96 -134
View File
@@ -18,7 +18,6 @@ import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
@@ -153,6 +152,10 @@ type Server struct {
// Default: `[h1 h2 h3]`
Protocols []string `json:"protocols,omitempty"`
// If set, metrics observations will be enabled.
// This setting is EXPERIMENTAL and subject to change.
Metrics *Metrics `json:"metrics,omitempty"`
name string
primaryHandlerChain Handler
@@ -167,12 +170,18 @@ type Server struct {
errorLogger *zap.Logger
ctx caddy.Context
server *http.Server
h3server *http3.Server
addresses []caddy.NetworkAddress
server *http.Server
h3server *http3.Server
h3listeners []net.PacketConn // TODO: we have to hold these because quic-go won't close listeners it didn't create
addresses []caddy.NetworkAddress
shutdownAt time.Time
shutdownAtMu *sync.RWMutex
// registered callback functions
connStateFuncs []func(net.Conn, http.ConnState)
connContextFuncs []func(ctx context.Context, c net.Conn) context.Context
onShutdownFuncs []func()
}
// ServeHTTP is the entry point for all HTTP requests.
@@ -185,9 +194,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&s.activeRequests, 1)
defer atomic.AddInt64(&s.activeRequests, -1)
err := s.h3server.SetQuicHeaders(w.Header())
if err != nil {
s.logger.Error("setting HTTP/3 Alt-Svc header", zap.Error(err))
if r.ProtoMajor < 3 {
err := s.h3server.SetQuicHeaders(w.Header())
if err != nil {
s.logger.Error("setting HTTP/3 Alt-Svc header", zap.Error(err))
}
}
}
@@ -226,7 +237,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
accLog := s.accessLogger.With(loggableReq)
defer func() {
repl.Set("http.response.status", wrec.Status())
// this request may be flagged as omitted from the logs
if skipLog, ok := GetVar(r.Context(), SkipLogVar).(bool); ok && skipLog {
return
}
repl.Set("http.response.status", wrec.Status()) // will be 0 if no response is written by us (Go will write 200 to client)
repl.Set("http.response.size", wrec.Size())
repl.Set("http.response.duration", duration)
repl.Set("http.response.duration_ms", duration.Seconds()*1e3) // multiply seconds to preserve decimal (see #4666)
@@ -480,8 +496,25 @@ func (s *Server) findLastRouteWithHostMatcher() int {
// serveHTTP3 creates a QUIC listener, configures an HTTP/3 server if
// not already done, and then uses that server to serve HTTP/3 over
// the listener, with Server s as the handler.
func (s *Server) serveHTTP3(hostport string, tlsCfg *tls.Config) error {
h3ln, err := caddy.ListenQUIC(hostport, tlsCfg, &s.activeRequests)
func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error {
switch addr.Network {
case "unix":
addr.Network = "unixgram"
case "tcp4":
addr.Network = "udp4"
case "tcp6":
addr.Network = "udp6"
default:
addr.Network = "udp" // TODO: Maybe a better default is to not enable HTTP/3 if we do not know the network?
}
lnAny, err := addr.Listen(s.ctx, 0, net.ListenConfig{})
if err != nil {
return err
}
ln := lnAny.(net.PacketConn)
h3ln, err := caddy.ListenQUIC(ln, tlsCfg, &s.activeRequests)
if err != nil {
return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err)
}
@@ -499,12 +532,59 @@ func (s *Server) serveHTTP3(hostport string, tlsCfg *tls.Config) error {
}
}
s.h3listeners = append(s.h3listeners, lnAny.(net.PacketConn))
//nolint:errcheck
go s.h3server.ServeListener(h3ln)
return nil
}
// configureServer applies/binds the registered callback functions to the server.
func (s *Server) configureServer(server *http.Server) {
for _, f := range s.connStateFuncs {
if server.ConnState != nil {
baseConnStateFunc := server.ConnState
server.ConnState = func(conn net.Conn, state http.ConnState) {
baseConnStateFunc(conn, state)
f(conn, state)
}
} else {
server.ConnState = f
}
}
for _, f := range s.connContextFuncs {
if server.ConnContext != nil {
baseConnContextFunc := server.ConnContext
server.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
return f(baseConnContextFunc(ctx, c), c)
}
} else {
server.ConnContext = f
}
}
for _, f := range s.onShutdownFuncs {
server.RegisterOnShutdown(f)
}
}
// RegisterConnState registers f to be invoked on s.ConnState.
func (s *Server) RegisterConnState(f func(net.Conn, http.ConnState)) {
s.connStateFuncs = append(s.connStateFuncs, f)
}
// RegisterConnContext registers f to be invoked as part of s.ConnContext.
func (s *Server) RegisterConnContext(f func(ctx context.Context, c net.Conn) context.Context) {
s.connContextFuncs = append(s.connContextFuncs, f)
}
// RegisterOnShutdown registers f to be invoked on server shutdown.
func (s *Server) RegisterOnShutdown(f func()) {
s.onShutdownFuncs = append(s.onShutdownFuncs, f)
}
// HTTPErrorConfig determines how to handle errors
// from the HTTP handlers.
type HTTPErrorConfig struct {
@@ -557,21 +637,18 @@ func (s *Server) shouldLogRequest(r *http.Request) bool {
// logging is disabled
return false
}
if _, ok := s.Logs.LoggerNames[r.Host]; ok {
// this host is mapped to a particular logger name
return true
}
for _, dh := range s.Logs.SkipHosts {
// logging for this particular host is disabled
if certmagic.MatchWildcard(r.Host, dh) {
return false
}
}
if _, ok := s.Logs.LoggerNames[r.Host]; ok {
// this host is mapped to a particular logger name
return true
}
if s.Logs.SkipUnmappedHosts {
// this host is not mapped and thus must not be logged
return false
}
return true
// if configured, this host is not mapped and thus must not be logged
return !s.Logs.SkipUnmappedHosts
}
// protocol returns true if the protocol proto is configured/enabled.
@@ -592,96 +669,6 @@ func (s *Server) protocol(proto string) bool {
// EXPERIMENTAL: Subject to change or removal.
func (s *Server) Listeners() []net.Listener { return s.listeners }
// ServerLogConfig describes a server's logging configuration. If
// enabled without customization, all requests to this server are
// logged to the default logger; logger destinations may be
// customized per-request-host.
type ServerLogConfig struct {
// The default logger name for all logs emitted by this server for
// hostnames that are not in the LoggerNames (logger_names) map.
DefaultLoggerName string `json:"default_logger_name,omitempty"`
// LoggerNames maps request hostnames to a custom logger name.
// For example, a mapping of "example.com" to "example" would
// cause access logs from requests with a Host of example.com
// to be emitted by a logger named "http.log.access.example".
LoggerNames map[string]string `json:"logger_names,omitempty"`
// By default, all requests to this server will be logged if
// access logging is enabled. This field lists the request
// hosts for which access logging should be disabled.
SkipHosts []string `json:"skip_hosts,omitempty"`
// If true, requests to any host not appearing in the
// LoggerNames (logger_names) map will not be logged.
SkipUnmappedHosts bool `json:"skip_unmapped_hosts,omitempty"`
// If true, credentials that are otherwise omitted, will be logged.
// The definition of credentials is defined by https://fetch.spec.whatwg.org/#credentials,
// and this includes some request and response headers, i.e `Cookie`,
// `Set-Cookie`, `Authorization`, and `Proxy-Authorization`.
ShouldLogCredentials bool `json:"should_log_credentials,omitempty"`
}
// wrapLogger wraps logger in a logger named according to user preferences for the given host.
func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, host string) *zap.Logger {
if loggerName := slc.getLoggerName(host); loggerName != "" {
return logger.Named(loggerName)
}
return logger
}
func (slc ServerLogConfig) getLoggerName(host string) string {
tryHost := func(key string) (string, bool) {
// first try exact match
if loggerName, ok := slc.LoggerNames[key]; ok {
return loggerName, ok
}
// strip port and try again (i.e. Host header of "example.com:1234" should
// match "example.com" if there is no "example.com:1234" in the map)
hostOnly, _, err := net.SplitHostPort(key)
if err != nil {
return "", false
}
loggerName, ok := slc.LoggerNames[hostOnly]
return loggerName, ok
}
// try the exact hostname first
if loggerName, ok := tryHost(host); ok {
return loggerName
}
// try matching wildcard domains if other non-specific loggers exist
labels := strings.Split(host, ".")
for i := range labels {
if labels[i] == "" {
continue
}
labels[i] = "*"
wildcardHost := strings.Join(labels, ".")
if loggerName, ok := tryHost(wildcardHost); ok {
return loggerName
}
}
return slc.DefaultLoggerName
}
func (slc *ServerLogConfig) clone() *ServerLogConfig {
clone := &ServerLogConfig{
DefaultLoggerName: slc.DefaultLoggerName,
LoggerNames: make(map[string]string),
SkipHosts: append([]string{}, slc.SkipHosts...),
SkipUnmappedHosts: slc.SkipUnmappedHosts,
ShouldLogCredentials: slc.ShouldLogCredentials,
}
for k, v := range slc.LoggerNames {
clone.LoggerNames[k] = v
}
return clone
}
// PrepareRequest fills the request r for use in a Caddy HTTP handler chain. w and s can
// be nil, but the handlers will lose response placeholders and access to the server.
func PrepareRequest(r *http.Request, repl *caddy.Replacer, w http.ResponseWriter, s *Server) *http.Request {
@@ -701,31 +688,6 @@ func PrepareRequest(r *http.Request, repl *caddy.Replacer, w http.ResponseWriter
return r
}
// errLogValues inspects err and returns the status code
// to use, the error log message, and any extra fields.
// If err is a HandlerError, the returned values will
// have richer information.
func errLogValues(err error) (status int, msg string, fields []zapcore.Field) {
var handlerErr HandlerError
if errors.As(err, &handlerErr) {
status = handlerErr.StatusCode
if handlerErr.Err == nil {
msg = err.Error()
} else {
msg = handlerErr.Err.Error()
}
fields = []zapcore.Field{
zap.Int("status", handlerErr.StatusCode),
zap.String("err_id", handlerErr.ID),
zap.String("err_trace", handlerErr.Trace),
}
return
}
status = http.StatusInternalServerError
msg = err.Error()
return
}
// originalRequest returns a partial, shallow copy of
// req, including: req.Method, deep copy of req.URL
// (into the urlCopy parameter, which should be on the
+2 -1
View File
@@ -31,6 +31,7 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
"go.uber.org/zap"
)
func init() {
@@ -403,7 +404,7 @@ func cmdRespond(fl caddycmd.Flags) (int, error) {
if debug {
cfg.Logging = &caddy.Logging{
Logs: map[string]*caddy.CustomLog{
"default": {Level: "DEBUG"},
"default": {Level: zap.DebugLevel.CapitalString()},
},
}
}
+4 -5
View File
@@ -42,7 +42,7 @@ func (Tracing) CaddyModule() caddy.ModuleInfo {
// Provision implements caddy.Provisioner.
func (ot *Tracing) Provision(ctx caddy.Context) error {
ot.logger = ctx.Logger(ot)
ot.logger = ctx.Logger()
var err error
ot.otel, err = newOpenTelemetryWrapper(ctx, ot.SpanName)
@@ -66,10 +66,9 @@ func (ot *Tracing) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
//
// tracing {
// [span <span_name>]
// }
//
// tracing {
// [span <span_name>]
// }
func (ot *Tracing) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
setParameter := func(d *caddyfile.Dispenser, val *string) error {
if d.NextArg() {
+1 -1
View File
@@ -87,7 +87,7 @@ func (Handler) CaddyModule() caddy.ModuleInfo {
// Provision sets up the ACME server handler.
func (ash *Handler) Provision(ctx caddy.Context) error {
ash.logger = ctx.Logger(ash)
ash.logger = ctx.Logger()
// set some defaults
if ash.CA == "" {
ash.CA = caddypki.DefaultCAID
+1 -1
View File
@@ -47,7 +47,7 @@ func (adminAPI) CaddyModule() caddy.ModuleInfo {
// Provision sets up the adminAPI module.
func (a *adminAPI) Provision(ctx caddy.Context) error {
a.ctx = ctx
a.log = ctx.Logger(a)
a.log = ctx.Logger(a) // TODO: passing in 'a' is a hack until the admin API is officially extensible (see #5032)
// First check if the PKI app was configured, because
// a.ctx.App() has the side effect of instantiating
+1 -1
View File
@@ -54,7 +54,7 @@ func (PKI) CaddyModule() caddy.ModuleInfo {
// Provision sets up the configuration for the PKI app.
func (p *PKI) Provision(ctx caddy.Context) error {
p.ctx = ctx
p.log = ctx.Logger(p)
p.log = ctx.Logger()
for caID, ca := range p.CAs {
err := ca.Provision(ctx, caID, p.log)
+1 -1
View File
@@ -103,7 +103,7 @@ func (ACMEIssuer) CaddyModule() caddy.ModuleInfo {
// Provision sets up iss.
func (iss *ACMEIssuer) Provision(ctx caddy.Context) error {
iss.logger = ctx.Logger(iss)
iss.logger = ctx.Logger()
repl := caddy.NewReplacer()
+6 -6
View File
@@ -43,7 +43,7 @@ func (Tailscale) CaddyModule() caddy.ModuleInfo {
}
func (ts *Tailscale) Provision(ctx caddy.Context) error {
ts.logger = ctx.Logger(ts)
ts.logger = ctx.Logger()
return nil
}
@@ -66,7 +66,9 @@ func (ts Tailscale) canHazCertificate(ctx context.Context, hello *tls.ClientHell
status, err := tscert.GetStatus(ctx)
if err != nil {
if ts.Optional {
return false, nil // ignore error if we don't expect/require it to work anyway
// ignore error if we don't expect/require it to work anyway, but log it for debugging
ts.logger.Debug("error getting tailscale status", zap.Error(err), zap.String("server_name", hello.ServerName))
return false, nil
}
return false, err
}
@@ -80,8 +82,7 @@ func (ts Tailscale) canHazCertificate(ctx context.Context, hello *tls.ClientHell
// UnmarshalCaddyfile deserializes Caddyfile tokens into ts.
//
// ... tailscale
//
// ... tailscale
func (Tailscale) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
if d.NextArg() {
@@ -178,8 +179,7 @@ func (hcg HTTPCertGetter) GetCertificate(ctx context.Context, hello *tls.ClientH
// UnmarshalCaddyfile deserializes Caddyfile tokens into ts.
//
// ... http <url>
//
// ... http <url>
func (hcg *HTTPCertGetter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
if !d.NextArg() {
+45 -1
View File
@@ -20,11 +20,14 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/mholt/acmez"
"go.uber.org/zap"
)
func init() {
@@ -156,6 +159,16 @@ type ConnectionPolicy struct {
// is no policy configured for the empty SNI value.
DefaultSNI string `json:"default_sni,omitempty"`
// Also known as "SSLKEYLOGFILE", TLS secrets will be written to
// this file in NSS key log format which can then be parsed by
// Wireshark and other tools. This is INSECURE as it allows other
// programs or tools to decrypt TLS connections. However, this
// capability can be useful for debugging and troubleshooting.
// **ENABLING THIS LOG COMPROMISES SECURITY!**
//
// This feature is EXPERIMENTAL and subject to change or removal.
InsecureSecretsLog string `json:"insecure_secrets_log,omitempty"`
// TLSConfig is the fully-formed, standard lib TLS config
// used to serve TLS connections. Provision all
// ConnectionPolicies to populate this. It is exported only
@@ -280,6 +293,30 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
}
}
if p.InsecureSecretsLog != "" {
filename, err := caddy.NewReplacer().ReplaceOrErr(p.InsecureSecretsLog, true, true)
if err != nil {
return err
}
filename, err = filepath.Abs(filename)
if err != nil {
return err
}
logFile, _, err := secretsLogPool.LoadOrNew(filename, func() (caddy.Destructor, error) {
w, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
return destructableWriter{w}, err
})
if err != nil {
return err
}
ctx.OnCancel(func() { _, _ = secretsLogPool.Delete(filename) })
cfg.KeyLogWriter = logFile.(io.Writer)
tlsApp.logger.Warn("TLS SECURITY COMPROMISED: secrets logging is enabled!",
zap.String("log_filename", filename))
}
setDefaultTLSParams(cfg)
p.TLSConfig = cfg
@@ -297,7 +334,8 @@ func (p ConnectionPolicy) SettingsEmpty() bool {
p.ProtocolMin == "" &&
p.ProtocolMax == "" &&
p.ClientAuthentication == nil &&
p.DefaultSNI == ""
p.DefaultSNI == "" &&
p.InsecureSecretsLog == ""
}
// ClientAuthentication configures TLS client auth.
@@ -542,3 +580,9 @@ type ClientCertificateVerifier interface {
}
var defaultALPN = []string{"h2", "http/1.1"}
type destructableWriter struct{ *os.File }
func (d destructableWriter) Destruct() error { return d.Close() }
var secretsLogPool = caddy.NewUsagePool()
+6 -7
View File
@@ -65,7 +65,7 @@ func (InternalIssuer) CaddyModule() caddy.ModuleInfo {
// Provision sets up the issuer.
func (iss *InternalIssuer) Provision(ctx caddy.Context) error {
iss.logger = ctx.Logger(iss)
iss.logger = ctx.Logger()
// set some defaults
if iss.CA == "" {
@@ -148,12 +148,11 @@ func (iss InternalIssuer) Issue(ctx context.Context, csr *x509.CertificateReques
// UnmarshalCaddyfile deserializes Caddyfile tokens into iss.
//
// ... internal {
// ca <name>
// lifetime <duration>
// sign_with_root
// }
//
// ... internal {
// ca <name>
// lifetime <duration>
// sign_with_root
// }
func (iss *InternalIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
for d.NextBlock(0) {
+1 -1
View File
@@ -81,7 +81,7 @@ func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo {
// Provision parses m's IP ranges, either from IP or CIDR expressions.
func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
m.logger = ctx.Logger(m)
m.logger = ctx.Logger()
for _, str := range m.Ranges {
cidrs, err := m.parseIPRange(str)
if err != nil {
+1 -1
View File
@@ -94,7 +94,7 @@ func (t *TLS) Provision(ctx caddy.Context) error {
}
t.events = eventsAppIface.(*caddyevents.App)
t.ctx = ctx
t.logger = ctx.Logger(t)
t.logger = ctx.Logger()
repl := caddy.NewReplacer()
// set up a new certificate cache; this (re)loads all certificates
+1 -1
View File
@@ -66,7 +66,7 @@ func (*ZeroSSLIssuer) CaddyModule() caddy.ModuleInfo {
// Provision sets up iss.
func (iss *ZeroSSLIssuer) Provision(ctx caddy.Context) error {
iss.logger = ctx.Logger(iss)
iss.logger = ctx.Logger()
if iss.ACMEIssuer == nil {
iss.ACMEIssuer = new(ACMEIssuer)
}
+41 -20
View File
@@ -45,15 +45,21 @@ func (ConsoleEncoder) CaddyModule() caddy.ModuleInfo {
// Provision sets up the encoder.
func (ce *ConsoleEncoder) Provision(_ caddy.Context) error {
if ce.LevelFormat == "" {
ce.LevelFormat = "color"
}
if ce.TimeFormat == "" {
ce.TimeFormat = "wall_milli"
}
ce.Encoder = zapcore.NewConsoleEncoder(ce.ZapcoreEncoderConfig())
return nil
}
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
//
// console {
// <common encoder config subdirectives...>
// }
// console {
// <common encoder config subdirectives...>
// }
//
// See the godoc on the LogEncoderConfig type for the syntax of
// subdirectives that are common to most/all encoders.
@@ -92,9 +98,9 @@ func (je *JSONEncoder) Provision(_ caddy.Context) error {
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
//
// json {
// <common encoder config subdirectives...>
// }
// json {
// <common encoder config subdirectives...>
// }
//
// See the godoc on the LogEncoderConfig type for the syntax of
// subdirectives that are common to most/all encoders.
@@ -121,28 +127,37 @@ type LogEncoderConfig struct {
StacktraceKey *string `json:"stacktrace_key,omitempty"`
LineEnding *string `json:"line_ending,omitempty"`
TimeFormat string `json:"time_format,omitempty"`
TimeLocal bool `json:"time_local,omitempty"`
DurationFormat string `json:"duration_format,omitempty"`
LevelFormat string `json:"level_format,omitempty"`
}
// UnmarshalCaddyfile populates the struct from Caddyfile tokens. Syntax:
//
// {
// message_key <key>
// level_key <key>
// time_key <key>
// name_key <key>
// caller_key <key>
// stacktrace_key <key>
// line_ending <char>
// time_format <format>
// duration_format <format>
// level_format <format>
// }
//
// {
// message_key <key>
// level_key <key>
// time_key <key>
// name_key <key>
// caller_key <key>
// stacktrace_key <key>
// line_ending <char>
// time_format <format>
// time_local
// duration_format <format>
// level_format <format>
// }
func (lec *LogEncoderConfig) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for nesting := d.Nesting(); d.NextBlock(nesting); {
subdir := d.Val()
switch subdir {
case "time_local":
lec.TimeLocal = true
if d.NextArg() {
return d.ArgErr()
}
continue
}
var arg string
if !d.AllArgs(&arg) {
return d.ArgErr()
@@ -232,7 +247,13 @@ func (lec *LogEncoderConfig) ZapcoreEncoderConfig() zapcore.EncoderConfig {
timeFormat = "02/Jan/2006:15:04:05 -0700"
}
timeFormatter = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(ts.UTC().Format(timeFormat))
var time time.Time
if lec.TimeLocal {
time = ts.Local()
} else {
time = ts.UTC()
}
encoder.AppendString(time.Format(timeFormat))
}
}
cfg.EncodeTime = timeFormatter
+61 -22
View File
@@ -23,6 +23,7 @@ import (
"net/url"
"regexp"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -76,7 +77,10 @@ func hash(s string) string {
}
// HashFilter is a Caddy log field filter that
// replaces the field with the initial 4 bytes of the SHA-256 hash of the content.
// replaces the field with the initial 4 bytes
// of the SHA-256 hash of the content. Operates
// on string fields, or on arrays of strings
// where each string is hashed.
type HashFilter struct {
}
@@ -95,7 +99,13 @@ func (f *HashFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Filter filters the input field with the replacement value.
func (f *HashFilter) Filter(in zapcore.Field) zapcore.Field {
in.String = hash(in.String)
if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
for i, s := range array {
array[i] = hash(s)
}
} else {
in.String = hash(in.String)
}
return in
}
@@ -131,7 +141,10 @@ func (f *ReplaceFilter) Filter(in zapcore.Field) zapcore.Field {
}
// IPMaskFilter is a Caddy log field filter that
// masks IP addresses.
// masks IP addresses in a string, or in an array
// of strings. The string may be a comma separated
// list of IP addresses, where all of the values
// will be masked.
type IPMaskFilter struct {
// The IPv4 mask, as an subnet size CIDR.
IPv4MaskRaw int `json:"ipv4_cidr,omitempty"`
@@ -205,27 +218,45 @@ func (m *IPMaskFilter) Provision(ctx caddy.Context) error {
// Filter filters the input field.
func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
host, port, err := net.SplitHostPort(in.String)
if err != nil {
host = in.String // assume whole thing was IP address
}
ipAddr := net.ParseIP(host)
if ipAddr == nil {
return in
}
mask := m.v4Mask
if ipAddr.To4() == nil {
mask = m.v6Mask
}
masked := ipAddr.Mask(mask)
if port == "" {
in.String = masked.String()
if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
for i, s := range array {
array[i] = m.mask(s)
}
} else {
in.String = net.JoinHostPort(masked.String(), port)
in.String = m.mask(in.String)
}
return in
}
func (m IPMaskFilter) mask(s string) string {
output := ""
for _, value := range strings.Split(s, ",") {
value = strings.TrimSpace(value)
host, port, err := net.SplitHostPort(value)
if err != nil {
host = value // assume whole thing was IP address
}
ipAddr := net.ParseIP(host)
if ipAddr == nil {
output += value + ", "
continue
}
mask := m.v4Mask
if ipAddr.To4() == nil {
mask = m.v6Mask
}
masked := ipAddr.Mask(mask)
if port == "" {
output += masked.String() + ", "
continue
}
output += net.JoinHostPort(masked.String(), port) + ", "
}
return strings.TrimSuffix(output, ", ")
}
type filterAction string
const (
@@ -499,7 +530,10 @@ OUTER:
}
// RegexpFilter is a Caddy log field filter that
// replaces the field matching the provided regexp with the indicated string.
// replaces the field matching the provided regexp
// with the indicated string. If the field is an
// array of strings, each of them will have the
// regexp replacement applied.
type RegexpFilter struct {
// The regular expression pattern defining what to replace.
RawRegexp string `json:"regexp,omitempty"`
@@ -545,7 +579,13 @@ func (m *RegexpFilter) Provision(ctx caddy.Context) error {
// Filter filters the input field with the replacement value if it matches the regexp.
func (f *RegexpFilter) Filter(in zapcore.Field) zapcore.Field {
in.String = f.regexp.ReplaceAllString(in.String, f.Value)
if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
for i, s := range array {
array[i] = f.regexp.ReplaceAllString(s, f.Value)
}
} else {
in.String = f.regexp.ReplaceAllString(in.String, f.Value)
}
return in
}
@@ -576,7 +616,6 @@ func (f *RenameFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Filter renames the input field with the replacement name.
func (f *RenameFilter) Filter(in zapcore.Field) zapcore.Field {
in.Type = zapcore.StringType
in.Key = f.Name
return in
}
+110 -2
View File
@@ -8,6 +8,81 @@ import (
"go.uber.org/zap/zapcore"
)
func TestIPMaskSingleValue(t *testing.T) {
f := IPMaskFilter{IPv4MaskRaw: 16, IPv6MaskRaw: 32}
f.Provision(caddy.Context{})
out := f.Filter(zapcore.Field{String: "255.255.255.255"})
if out.String != "255.255.0.0" {
t.Fatalf("field has not been filtered: %s", out.String)
}
out = f.Filter(zapcore.Field{String: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"})
if out.String != "ffff:ffff::" {
t.Fatalf("field has not been filtered: %s", out.String)
}
out = f.Filter(zapcore.Field{String: "not-an-ip"})
if out.String != "not-an-ip" {
t.Fatalf("field has been filtered: %s", out.String)
}
}
func TestIPMaskCommaValue(t *testing.T) {
f := IPMaskFilter{IPv4MaskRaw: 16, IPv6MaskRaw: 32}
f.Provision(caddy.Context{})
out := f.Filter(zapcore.Field{String: "255.255.255.255, 244.244.244.244"})
if out.String != "255.255.0.0, 244.244.0.0" {
t.Fatalf("field has not been filtered: %s", out.String)
}
out = f.Filter(zapcore.Field{String: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff, ff00:ffff:ffff:ffff:ffff:ffff:ffff:ffff"})
if out.String != "ffff:ffff::, ff00:ffff::" {
t.Fatalf("field has not been filtered: %s", out.String)
}
out = f.Filter(zapcore.Field{String: "not-an-ip, 255.255.255.255"})
if out.String != "not-an-ip, 255.255.0.0" {
t.Fatalf("field has not been filtered: %s", out.String)
}
}
func TestIPMaskMultiValue(t *testing.T) {
f := IPMaskFilter{IPv4MaskRaw: 16, IPv6MaskRaw: 32}
f.Provision(caddy.Context{})
out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{
"255.255.255.255",
"244.244.244.244",
}})
arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
if !ok {
t.Fatalf("field is wrong type: %T", out.Integer)
}
if arr[0] != "255.255.0.0" {
t.Fatalf("field entry 0 has not been filtered: %s", arr[0])
}
if arr[1] != "244.244.0.0" {
t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
}
out = f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{
"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
"ff00:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
}})
arr, ok = out.Interface.(caddyhttp.LoggableStringArray)
if !ok {
t.Fatalf("field is wrong type: %T", out.Integer)
}
if arr[0] != "ffff:ffff::" {
t.Fatalf("field entry 0 has not been filtered: %s", arr[0])
}
if arr[1] != "ff00:ffff::" {
t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
}
}
func TestQueryFilter(t *testing.T) {
f := QueryFilter{[]queryFilterAction{
{replaceAction, "foo", "REDACTED"},
@@ -78,7 +153,7 @@ func TestValidateCookieFilter(t *testing.T) {
}
}
func TestRegexpFilter(t *testing.T) {
func TestRegexpFilterSingleValue(t *testing.T) {
f := RegexpFilter{RawRegexp: `secret`, Value: "REDACTED"}
f.Provision(caddy.Context{})
@@ -88,7 +163,24 @@ func TestRegexpFilter(t *testing.T) {
}
}
func TestHashFilter(t *testing.T) {
func TestRegexpFilterMultiValue(t *testing.T) {
f := RegexpFilter{RawRegexp: `secret`, Value: "REDACTED"}
f.Provision(caddy.Context{})
out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{"foo-secret-bar", "bar-secret-foo"}})
arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
if !ok {
t.Fatalf("field is wrong type: %T", out.Integer)
}
if arr[0] != "foo-REDACTED-bar" {
t.Fatalf("field entry 0 has not been filtered: %s", arr[0])
}
if arr[1] != "bar-REDACTED-foo" {
t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
}
}
func TestHashFilterSingleValue(t *testing.T) {
f := HashFilter{}
out := f.Filter(zapcore.Field{String: "foo"})
@@ -96,3 +188,19 @@ func TestHashFilter(t *testing.T) {
t.Fatalf("field has not been filtered: %s", out.String)
}
}
func TestHashFilterMultiValue(t *testing.T) {
f := HashFilter{}
out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{"foo", "bar"}})
arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
if !ok {
t.Fatalf("field is wrong type: %T", out.Integer)
}
if arr[0] != "2c26b46b" {
t.Fatalf("field entry 0 has not been filtered: %s", arr[0])
}
if arr[1] != "fcde2b2e" {
t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
}
}
+4 -5
View File
@@ -62,7 +62,7 @@ func (l *zapLogger) Println(v ...any) {
// Provision sets up m.
func (m *Metrics) Provision(ctx caddy.Context) error {
log := ctx.Logger(m)
log := ctx.Logger()
m.metricsHandler = createMetricsHandler(&zapLogger{log}, !m.DisableOpenMetrics)
return nil
}
@@ -75,10 +75,9 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
//
// metrics [<matcher>] {
// disable_openmetrics
// }
//
// metrics [<matcher>] {
// disable_openmetrics
// }
func (m *Metrics) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
args := d.RemainingArgs()
+16 -1
View File
@@ -144,9 +144,11 @@ func (r *Replacer) replace(input, empty string,
// iterate the input to find each placeholder
var lastWriteCursor int
// fail fast if too many placeholders are unclosed
var unclosedCount int
scan:
for i := 0; i < len(input); i++ {
// check for escaped braces
if i > 0 && input[i-1] == phEscape && (input[i] == phClose || input[i] == phOpen) {
sb.WriteString(input[lastWriteCursor : i-1])
@@ -158,9 +160,17 @@ scan:
continue
}
// our iterator is now on an unescaped open brace (start of placeholder)
// too many unclosed placeholders in absolutely ridiculous input can be extremely slow (issue #4170)
if unclosedCount > 100 {
return "", fmt.Errorf("too many unclosed placeholders")
}
// find the end of the placeholder
end := strings.Index(input[i:], string(phClose)) + i
if end < i {
unclosedCount++
continue
}
@@ -168,6 +178,7 @@ scan:
for end > 0 && end < len(input)-1 && input[end-1] == phEscape {
nextEnd := strings.Index(input[end+1:], string(phClose))
if nextEnd < 0 {
unclosedCount++
continue scan
}
end += nextEnd + 1
@@ -297,6 +308,10 @@ func globalDefaultReplacements(key string) (any, bool) {
return string(filepath.Separator), true
case "system.os":
return runtime.GOOS, true
case "system.wd":
// OK if there is an error; just return empty string
wd, _ := os.Getwd()
return wd, true
case "system.arch":
return runtime.GOARCH, true
case "time.now":
+5
View File
@@ -372,6 +372,7 @@ func TestReplacerNew(t *testing.T) {
} else {
// test if default global replacements are added as the first provider
hostname, _ := os.Hostname()
wd, _ := os.Getwd()
os.Setenv("CADDY_REPLACER_TEST", "envtest")
defer os.Setenv("CADDY_REPLACER_TEST", "")
@@ -395,6 +396,10 @@ func TestReplacerNew(t *testing.T) {
variable: "system.arch",
value: runtime.GOARCH,
},
{
variable: "system.wd",
value: wd,
},
{
variable: "env.CADDY_REPLACER_TEST",
value: "envtest",
+12 -1
View File
@@ -15,15 +15,26 @@
package caddy
import (
"os"
"path/filepath"
"github.com/caddyserver/caddy/v2/notify"
"golang.org/x/sys/windows/svc"
)
func init() {
isService, err := svc.IsWindowsService()
if err != nil || isService {
if err != nil || !isService {
return
}
// Windows services always start in the system32 directory, try to
// switch into the directory where the caddy executable is.
execPath, err := os.Executable()
if err == nil {
_ = os.Chdir(filepath.Dir(execPath))
}
go func() {
_ = svc.Run("", runner{})
}()