Compare commits

..

45 Commits

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

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

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

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

* exit early
2023-02-06 11:26:07 -07:00
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
67 changed files with 1867 additions and 1236 deletions
+4 -3
View File
@@ -156,17 +156,18 @@ jobs:
short_sha=$(git rev-parse --short HEAD) short_sha=$(git rev-parse --short HEAD)
# The environment is fresh, so there's no point in keeping accepting and adding the key. # The environment is fresh, so there's no point in keeping accepting and adding the key.
rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . caddy-ci@ci-s390x.caddyserver.com:/var/tmp/"$short_sha" 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 caddy-ci@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -v ./..." ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$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=$? test_result=$?
# There's no need leaving the files around # There's no need leaving the files around
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null 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" echo "Test exit code: $test_result"
exit $test_result exit $test_result
env: env:
SSH_KEY: ${{ secrets.S390X_SSH_KEY }} SSH_KEY: ${{ secrets.S390X_SSH_KEY }}
CI_USER: ${{ secrets.CI_USER }}
goreleaser-check: goreleaser-check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+1 -1
View File
@@ -119,7 +119,7 @@ jobs:
uses: goreleaser/goreleaser-action@v2 uses: goreleaser/goreleaser-action@v2
with: with:
version: latest version: latest
args: release --rm-dist args: release --rm-dist --timeout 60m
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.vars.outputs.version_tag }} TAG: ${{ steps.vars.outputs.version_tag }}
+1
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 # This is so we can run goreleaser on tag without Git complaining of being dirty. The main.go in cmd/caddy directory
# cannot be built within that directory due to changes necessary for the build causing Git to be dirty, which # cannot be built within that directory due to changes necessary for the build causing Git to be dirty, which
# subsequently causes gorleaser to refuse running. # subsequently causes gorleaser to refuse running.
- rm -rf caddy-build caddy-dist
- mkdir -p caddy-build - mkdir -p caddy-build
- cp cmd/caddy/main.go caddy-build/main.go - cp cmd/caddy/main.go caddy-build/main.go
- /bin/sh -c 'cd ./caddy-build && go mod init caddy' - /bin/sh -c 'cd ./caddy-build && go mod init caddy'
+10 -6
View File
@@ -57,7 +57,7 @@ type AdminConfig struct {
// The address to which the admin endpoint's listener should // The address to which the admin endpoint's listener should
// bind itself. Can be any single network address that can be // bind itself. Can be any single network address that can be
// parsed by Caddy. Default: localhost:2019 // parsed by Caddy. Accepts placeholders. Default: localhost:2019
Listen string `json:"listen,omitempty"` Listen string `json:"listen,omitempty"`
// If true, CORS headers will be emitted, and requests to the // If true, CORS headers will be emitted, and requests to the
@@ -156,7 +156,7 @@ type IdentityConfig struct {
// //
// EXPERIMENTAL: Subject to change. // EXPERIMENTAL: Subject to change.
type RemoteAdmin struct { type RemoteAdmin struct {
// The address on which to start the secure listener. // The address on which to start the secure listener. Accepts placeholders.
// Default: :2021 // Default: :2021
Listen string `json:"listen,omitempty"` Listen string `json:"listen,omitempty"`
@@ -382,7 +382,7 @@ func replaceLocalAdminServer(cfg *Config) error {
handler := cfg.Admin.newAdminHandler(addr, false) 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 { if err != nil {
return err return err
} }
@@ -403,7 +403,7 @@ func replaceLocalAdminServer(cfg *Config) error {
serverMu.Lock() serverMu.Lock()
server := localAdminServer server := localAdminServer
serverMu.Unlock() 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)) adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
} }
}() }()
@@ -549,10 +549,11 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
serverMu.Unlock() serverMu.Unlock()
// start listener // start listener
ln, err := Listen(addr.Network, addr.JoinHostPort(0)) lnAny, err := addr.Listen(ctx, 0, net.ListenConfig{})
if err != nil { if err != nil {
return err return err
} }
ln := lnAny.(net.Listener)
ln = tls.NewListener(ln, tlsConfig) ln = tls.NewListener(ln, tlsConfig)
go func() { go func() {
@@ -1246,7 +1247,10 @@ func (e APIError) Error() string {
// parseAdminListenAddr extracts a singular listen address from either addr // parseAdminListenAddr extracts a singular listen address from either addr
// or defaultAddr, returning the network and the address of the listener. // or defaultAddr, returning the network and the address of the listener.
func parseAdminListenAddr(addr string, defaultAddr string) (NetworkAddress, error) { func parseAdminListenAddr(addr string, defaultAddr string) (NetworkAddress, error) {
input := addr input, err := NewReplacer().ReplaceOrErr(addr, true, true)
if err != nil {
return NetworkAddress{}, fmt.Errorf("replacing listen address: %v", err)
}
if input == "" { if input == "" {
input = defaultAddr input = defaultAddr
} }
+48 -10
View File
@@ -824,6 +824,20 @@ func InstanceID() (uuid.UUID, error) {
return uuid.ParseBytes(uuidFileBytes) 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 // Version returns the Caddy version in a simple/short form, and
// a full version string. The short form will not have spaces and // a full version string. The short form will not have spaces and
// is intended for User-Agent strings and similar, but may be // is intended for User-Agent strings and similar, but may be
@@ -833,8 +847,10 @@ func InstanceID() (uuid.UUID, error) {
// build info provided by go.mod dependencies; then it tries to // build info provided by go.mod dependencies; then it tries to
// get info from embedded VCS information, which requires having // get info from embedded VCS information, which requires having
// built Caddy from a git repository. If no version is available, // built Caddy from a git repository. If no version is available,
// this function returns "(devel)" becaise Go uses that, but for // this function returns "(devel)" because Go uses that, but for
// the simple form we change it to "unknown". // 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 // See relevant Go issues: https://github.com/golang/go/issues/29228
// and https://github.com/golang/go/issues/50603. // and https://github.com/golang/go/issues/50603.
@@ -848,13 +864,21 @@ func Version() (simple, full string) {
// bi.Main... hopefully. // bi.Main... hopefully.
var module *debug.Module var module *debug.Module
bi, ok := debug.ReadBuildInfo() bi, ok := debug.ReadBuildInfo()
if ok { if !ok {
// find the Caddy module in the dependency list if CustomVersion != "" {
for _, dep := range bi.Deps { full = CustomVersion
if dep.Path == ImportPath { simple = CustomVersion
module = dep return
break }
} full = "unknown"
simple = "unknown"
return
}
// find the Caddy module in the dependency list
for _, dep := range bi.Deps {
if dep.Path == ImportPath {
module = dep
break
} }
} }
if module != nil { if module != nil {
@@ -910,8 +934,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)" { if simple == "" || simple == "(devel)" {
simple = "unknown" if CustomVersion != "" {
simple = CustomVersion
} else {
simple = "unknown"
}
} }
return return
+1 -1
View File
@@ -735,7 +735,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
// reference the default logger. See the // reference the default logger. See the
// setupNewDefault function in the logging // setupNewDefault function in the logging
// package for where this is configured. // package for where this is configured.
globalLogName = "default" globalLogName = caddy.DefaultLoggerName
} }
// Verify this name is unused. // Verify this name is unused.
+36 -29
View File
@@ -219,7 +219,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
if ncl.name == "" { if ncl.name == "" {
return return
} }
if ncl.name == "default" { if ncl.name == caddy.DefaultLoggerName {
hasDefaultLog = true hasDefaultLog = true
} }
if _, ok := options["debug"]; ok && ncl.log.Level == "" { if _, ok := options["debug"]; ok && ncl.log.Level == "" {
@@ -240,7 +240,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
// configure it with any applicable options // configure it with any applicable options
if _, ok := options["debug"]; ok { if _, ok := options["debug"]; ok {
customLogs = append(customLogs, namedCustomLog{ customLogs = append(customLogs, namedCustomLog{
name: "default", name: caddy.DefaultLoggerName,
log: &caddy.CustomLog{Level: zap.DebugLevel.CapitalString()}, 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 // most users seem to prefer not writing access logs
// to the default log when they are directed to a // to the default log when they are directed to a
// file or have any other special customization // file or have any other special customization
if ncl.name != "default" && len(ncl.log.Include) > 0 { if ncl.name != caddy.DefaultLoggerName && len(ncl.log.Include) > 0 {
defaultLog, ok := cfg.Logging.Logs["default"] defaultLog, ok := cfg.Logging.Logs[caddy.DefaultLoggerName]
if !ok { if !ok {
defaultLog = new(caddy.CustomLog) defaultLog = new(caddy.CustomLog)
cfg.Logging.Logs["default"] = defaultLog cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog
} }
defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...) defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...)
} }
@@ -518,15 +518,6 @@ func (st *ServerType) serversFromPairings(
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
autoHTTPSWillAddConnPolicy := autoHTTPS != "off" autoHTTPSWillAddConnPolicy := autoHTTPS != "off"
// if a catch-all server block (one which accepts all hostnames) exists in this pairing,
// we need to know that so that we can configure logs properly (see #3878)
var catchAllSblockExists bool
for _, sblock := range p.serverBlocks {
if len(sblock.hostsFromKeys(false)) == 0 {
catchAllSblockExists = true
}
}
// if needed, the ServerLogConfig is initialized beforehand so // if needed, the ServerLogConfig is initialized beforehand so
// that all server blocks can populate it with data, even when not // that all server blocks can populate it with data, even when not
// coming with a log directive // coming with a log directive
@@ -658,18 +649,10 @@ func (st *ServerType) serversFromPairings(
} else { } else {
// map each host to the user's desired logger name // map each host to the user's desired logger name
for _, h := range sblockLogHosts { for _, h := range sblockLogHosts {
// if the custom logger name is non-empty, add it to the map; if srv.Logs.LoggerNames == nil {
// otherwise, only map to an empty logger name if this or srv.Logs.LoggerNames = make(map[string]string)
// another site block on this server has a catch-all host (in
// which case only requests with mapped hostnames will be
// access-logged, so it'll be necessary to add them to the
// map even if they use default logger)
if ncl.name != "" || catchAllSblockExists {
if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string)
}
srv.Logs.LoggerNames[h] = ncl.name
} }
srv.Logs.LoggerNames[h] = ncl.name
} }
} }
} }
@@ -924,11 +907,32 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
return routeList return routeList
} }
// No need to wrap the handlers in a subroute if this is the only server block
// and there is no matcher for it (doing so would produce unnecessarily nested
// JSON), *unless* there is a host matcher within this site block; if so, then
// we still need to wrap in a subroute because otherwise the host matcher from
// the inside of the site block would be a top-level host matcher, which is
// subject to auto-HTTPS (cert management), and using a host matcher within
// a site block is a valid, common pattern for excluding domains from cert
// management, leading to unexpected behavior; see issue #5124.
wrapInSubroute := true
if len(matcherSetsEnc) == 0 && len(p.serverBlocks) == 1 { if len(matcherSetsEnc) == 0 && len(p.serverBlocks) == 1 {
// no need to wrap the handlers in a subroute if this is var hasHostMatcher bool
// the only server block and there is no matcher for it outer:
routeList = append(routeList, subroute.Routes...) for _, route := range subroute.Routes {
} else { for _, ms := range route.MatcherSetsRaw {
for matcherName := range ms {
if matcherName == "host" {
hasHostMatcher = true
break outer
}
}
}
}
wrapInSubroute = hasHostMatcher
}
if wrapInSubroute {
route := caddyhttp.Route{ route := caddyhttp.Route{
// the semantics of a site block in the Caddyfile dictate // the semantics of a site block in the Caddyfile dictate
// that only the first matching one is evaluated, since // that only the first matching one is evaluated, since
@@ -946,7 +950,10 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
if len(route.MatcherSetsRaw) > 0 || len(route.HandlersRaw) > 0 { if len(route.MatcherSetsRaw) > 0 || len(route.HandlersRaw) > 0 {
routeList = append(routeList, route) routeList = append(routeList, route)
} }
} else {
routeList = append(routeList, subroute.Routes...)
} }
return routeList return routeList
} }
+1 -1
View File
@@ -180,7 +180,7 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
if d.NextArg() { if d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
} }
if d.NextBlock(0) { if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr() return nil, d.ArgErr()
} }
serverOpts.Metrics = new(caddyhttp.Metrics) serverOpts.Metrics = new(caddyhttp.Metrics)
+53 -47
View File
@@ -44,37 +44,32 @@ func (st ServerType) buildTLSApp(
if hp, ok := options["http_port"].(int); ok { if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp) httpPort = strconv.Itoa(hp)
} }
httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort) autoHTTPS := "on"
if hsp, ok := options["https_port"].(int); ok { if ah, ok := options["auto_https"].(string); ok {
httpsPort = strconv.Itoa(hsp) autoHTTPS = ah
} }
// count how many server blocks have a TLS-enabled key with // find all hosts that share a server block with a hostless
// no host, and find all hosts that share a server block with // key, so that they don't get forgotten/omitted by auto-HTTPS
// a hostless key, so that they don't get forgotten/omitted // (since they won't appear in route matchers)
// by auto-HTTPS (since they won't appear in route matchers)
var serverBlocksWithTLSHostlessKey int
httpsHostsSharedWithHostlessKey := make(map[string]struct{}) httpsHostsSharedWithHostlessKey := make(map[string]struct{})
for _, pair := range pairings { if autoHTTPS != "off" {
for _, sb := range pair.serverBlocks { for _, pair := range pairings {
for _, addr := range sb.keys { for _, sb := range pair.serverBlocks {
if addr.Host == "" { for _, addr := range sb.keys {
// this address has no hostname, but if it's explicitly set if addr.Host == "" {
// to HTTPS, then we need to count it as being TLS-enabled // this server block has a hostless key, now
if addr.Scheme == "https" || addr.Port == httpsPort { // go through and add all the hosts to the set
serverBlocksWithTLSHostlessKey++ for _, otherAddr := range sb.keys {
} if otherAddr.Original == addr.Original {
// this server block has a hostless key, now continue
// go through and add all the hosts to the set }
for _, otherAddr := range sb.keys { if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
if otherAddr.Original == addr.Original { httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
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)) issuers = append(issuers, issuerVal.Value.(certmagic.Issuer))
} }
if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) { if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) {
// this more correctly implements an error check that was removed
// below; try it with this config:
//
// :443 {
// bind 127.0.0.1
// }
//
// :443 {
// bind ::1
// tls {
// issuer acme
// }
// }
return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers) return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers)
} }
ap.Issuers = issuers ap.Issuers = issuers
@@ -176,29 +184,25 @@ func (st ServerType) buildTLSApp(
} }
} }
// first make sure this block is allowed to create an automation policy; // we used to ensure this block is allowed to create an automation policy;
// doing so is forbidden if it has a key with no host (i.e. ":443") // 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 // and if there is a different server block that also has a key with no
// host -- since a key with no host matches any host, we need its // host -- since a key with no host matches any host, we need its
// associated automation policy to have an empty Subjects list, i.e. no // associated automation policy to have an empty Subjects list, i.e. no
// host filter, which is indistinguishable between the two server blocks // host filter, which is indistinguishable between the two server blocks
// because automation is not done in the context of a particular server... // because automation is not done in the context of a particular server...
// this is an example of a poor mapping from Caddyfile to JSON but that's // this is an example of a poor mapping from Caddyfile to JSON but that's
// the least-leaky abstraction I could figure out // the least-leaky abstraction I could figure out -- however, this check
if len(sblockHosts) == 0 { // was preventing certain listeners, like those provided by plugins, from
if serverBlocksWithTLSHostlessKey > 1 { // being used as desired (see the Tailscale listener plugin), so I removed
// this server block and at least one other has a key with no host, // the check: and I think since I originally wrote the check I added a new
// making the two indistinguishable; it is misleading to define such // check above which *properly* detects this ambiguity without breaking the
// a policy within one server block since it actually will apply to // listener plugin; see the check above with a commented example config
// others as well if len(sblockHosts) == 0 && catchAllAP == nil {
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") // this server block has a key with no hosts, but there is not yet
} // a catch-all automation policy (probably because no global options
if catchAllAP == nil { // were set), so this one becomes it
// this server block has a key with no hosts, but there is not yet catchAllAP = ap
// a catch-all automation policy (probably because no global options
// were set), so this one becomes it
catchAllAP = ap
}
} }
// associate our new automation policy with this server block's hosts // associate our new automation policy with this server block's hosts
@@ -331,10 +335,12 @@ func (st ServerType) buildTLSApp(
internalAP := &caddytls.AutomationPolicy{ internalAP := &caddytls.AutomationPolicy{
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)}, IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
} }
for h := range httpsHostsSharedWithHostlessKey { if autoHTTPS != "off" {
al = append(al, h) for h := range httpsHostsSharedWithHostlessKey {
if !certmagic.SubjectQualifiesForPublicCert(h) { al = append(al, h)
internalAP.Subjects = append(internalAP.Subjects, h) if !certmagic.SubjectQualifiesForPublicCert(h) {
internalAP.Subjects = append(internalAP.Subjects, h)
}
} }
} }
if len(al) > 0 { if len(al) > 0 {
+32 -1
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 { if err != nil {
return nil, err return nil, err
} }
@@ -119,6 +119,37 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
return result, nil return result, nil
} }
func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response, error) {
resp, err := client.Do(request)
if err != nil {
return nil, fmt.Errorf("problem calling http loader url: %v", err)
} else if resp.StatusCode < 200 || resp.StatusCode > 499 {
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) { func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
client := &http.Client{ client := &http.Client{
Timeout: time.Duration(hl.Timeout), Timeout: time.Duration(hl.Timeout),
+3 -3
View File
@@ -214,7 +214,7 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
return actual return actual
} }
for retries := 4; retries > 0; retries-- { for retries := 10; retries > 0; retries-- {
if reflect.DeepEqual(expected, fetchConfig(client)) { if reflect.DeepEqual(expected, fetchConfig(client)) {
return nil return nil
} }
@@ -237,13 +237,13 @@ func validateTestPrerequisites() error {
if isCaddyAdminRunning() != nil { if isCaddyAdminRunning() != nil {
// start inprocess caddy server // start inprocess caddy server
os.Args = []string{"caddy", "run"} os.Args = []string{"caddy", "run", "--config", "./test.init.config", "--adapter", "caddyfile"}
go func() { go func() {
caddycmd.Main() caddycmd.Main()
}() }()
// wait for caddy to start serving the initial config // wait for caddy to start serving the initial config
for retries := 4; retries > 0 && isCaddyAdminRunning() != nil; retries-- { for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
} }
} }
+21 -1
View File
@@ -11,6 +11,8 @@ func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
admin localhost:2999
skip_install_trust
http_port 9080 http_port 9080
https_port 9443 https_port 9443
} }
@@ -25,6 +27,8 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
skip_install_trust
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
} }
@@ -39,6 +43,8 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
skip_install_trust
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
} }
@@ -53,6 +59,9 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
"admin": {
"listen": "localhost:2999"
},
"apps": { "apps": {
"http": { "http": {
"http_port": 9080, "http_port": 9080,
@@ -74,7 +83,14 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
] ]
} }
} }
} },
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
}
} }
} }
`, "json") `, "json")
@@ -85,6 +101,8 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
skip_install_trust
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
local_certs local_certs
@@ -108,6 +126,8 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSit
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
skip_install_trust
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
local_certs local_certs
@@ -3,9 +3,7 @@
timeouts { timeouts {
idle 90s idle 90s
} }
protocol { strict_sni_host insecure_off
strict_sni_host insecure_off
}
} }
servers :80 { servers :80 {
timeouts { timeouts {
@@ -16,9 +14,7 @@
timeouts { timeouts {
idle 30s idle 30s
} }
protocol { strict_sni_host
strict_sni_host
}
} }
} }
@@ -62,6 +62,9 @@ example.com {
} }
], ],
"logs": { "logs": {
"logger_names": {
"one.example.com": ""
},
"skip_hosts": [ "skip_hosts": [
"three.example.com", "three.example.com",
"two.example.com", "two.example.com",
@@ -8,7 +8,7 @@ route {
} }
not path */ not path */
} }
redir @canonicalPath {path}/ 308 redir @canonicalPath {http.request.orig_uri.path}/ 308
# If the requested file does not exist, try index files # If the requested file does not exist, try index files
@indexFiles { @indexFiles {
@@ -50,7 +50,7 @@ route {
"handler": "static_response", "handler": "static_response",
"headers": { "headers": {
"Location": [ "Location": [
"{http.request.uri.path}/" "{http.request.orig_uri.path}/"
] ]
}, },
"status_code": 308 "status_code": 308
@@ -42,7 +42,7 @@
"handler": "static_response", "handler": "static_response",
"headers": { "headers": {
"Location": [ "Location": [
"{http.request.uri.path}/" "{http.request.orig_uri.path}/"
] ]
}, },
"status_code": 308 "status_code": 308
@@ -1,7 +1,12 @@
:8884 :8884
@api host example.com # the use of a host matcher here should cause this
php_fastcgi @api localhost:9000 # 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": { "apps": {
@@ -13,13 +18,6 @@ php_fastcgi @api localhost:9000
], ],
"routes": [ "routes": [
{ {
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [ "handle": [
{ {
"handler": "subroute", "handler": "subroute",
@@ -27,82 +25,99 @@ php_fastcgi @api localhost:9000
{ {
"handle": [ "handle": [
{ {
"handler": "static_response", "handler": "subroute",
"headers": { "routes": [
"Location": [
"{http.request.uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{ {
"path": [ "handle": [
"*/" {
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/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": [ "match": [
{ {
"file": { "host": [
"split_path": [ "example.com"
".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"
] ]
} }
] ]
} }
] ]
} }
] ],
"terminal": true
} }
] ]
} }
@@ -43,7 +43,7 @@ php_fastcgi localhost:9000 {
"handler": "static_response", "handler": "static_response",
"headers": { "headers": {
"Location": [ "Location": [
"{http.request.uri.path}/" "{http.request.orig_uri.path}/"
] ]
}, },
"status_code": 308 "status_code": 308
@@ -46,7 +46,7 @@ php_fastcgi localhost:9000 {
"handler": "static_response", "handler": "static_response",
"headers": { "headers": {
"Location": [ "Location": [
"{http.request.uri.path}/" "{http.request.orig_uri.path}/"
] ]
}, },
"status_code": 308 "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 -4
View File
@@ -14,9 +14,10 @@ func TestRespond(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
grace_period 1 grace_period 1ns
} }
localhost:9080 { localhost:9080 {
@@ -36,9 +37,10 @@ func TestRedirect(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
grace_period 1 grace_period 1ns
} }
localhost:9080 { localhost:9080 {
@@ -86,9 +88,11 @@ func TestReadCookie(t *testing.T) {
tester.Client.Jar.SetCookies(localhost, []*http.Cookie{&cookie}) tester.Client.Jar.SetCookies(localhost, []*http.Cookie{&cookie})
tester.InitServer(` tester.InitServer(`
{ {
skip_install_trust
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
grace_period 1 grace_period 1ns
} }
localhost:9080 { localhost:9080 {
@@ -110,9 +114,11 @@ func TestReplIndex(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
skip_install_trust
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
grace_period 1 grace_period 1ns
} }
localhost:9080 { localhost:9080 {
+3 -1
View File
@@ -11,9 +11,11 @@ func TestBrowse(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
skip_install_trust
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
grace_period 1 grace_period 1ns
} }
http://localhost:9080 { http://localhost:9080 {
file_server browse file_server browse
+15 -1
View File
@@ -11,9 +11,11 @@ func TestMap(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(`{ tester.InitServer(`{
skip_install_trust
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
grace_period 1 grace_period 1ns
} }
localhost:9080 { localhost:9080 {
@@ -39,6 +41,8 @@ func TestMapRespondWithDefault(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(`{ tester.InitServer(`{
skip_install_trust
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
} }
@@ -66,7 +70,17 @@ func TestMapAsJSON(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
"admin": {
"listen": "localhost:2999"
},
"apps": { "apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": { "http": {
"http_port": 9080, "http_port": 9080,
"https_port": 9443, "https_port": 9443,
+67 -2
View File
@@ -8,6 +8,7 @@ import (
"runtime" "runtime"
"strings" "strings"
"testing" "testing"
"time"
"github.com/caddyserver/caddy/v2/caddytest" "github.com/caddyserver/caddy/v2/caddytest"
) )
@@ -16,7 +17,17 @@ func TestSRVReverseProxy(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
"admin": {
"listen": "localhost:2999"
},
"apps": { "apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": { "http": {
"grace_period": 1, "grace_period": 1,
"servers": { "servers": {
@@ -50,6 +61,13 @@ func TestSRVWithDial(t *testing.T) {
caddytest.AssertLoadError(t, ` caddytest.AssertLoadError(t, `
{ {
"apps": { "apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": { "http": {
"grace_period": 1, "grace_period": 1,
"servers": { "servers": {
@@ -115,7 +133,17 @@ func TestDialWithPlaceholderUnix(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
"admin": {
"listen": "localhost:2999"
},
"apps": { "apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": { "http": {
"grace_period": 1, "grace_period": 1,
"servers": { "servers": {
@@ -157,7 +185,17 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
"admin": {
"listen": "localhost:2999"
},
"apps": { "apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": { "http": {
"grace_period": 1, "grace_period": 1,
"servers": { "servers": {
@@ -241,7 +279,17 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
"admin": {
"listen": "localhost:2999"
},
"apps": { "apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": { "http": {
"grace_period": 1, "grace_period": 1,
"servers": { "servers": {
@@ -325,6 +373,13 @@ func TestSRVWithActiveHealthcheck(t *testing.T) {
caddytest.AssertLoadError(t, ` caddytest.AssertLoadError(t, `
{ {
"apps": { "apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": { "http": {
"grace_period": 1, "grace_period": 1,
"servers": { "servers": {
@@ -363,8 +418,11 @@ func TestReverseProxyHealthCheck(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
skip_install_trust
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
grace_period 1ns
} }
http://localhost:2020 { http://localhost:2020 {
respond "Hello, World!" respond "Hello, World!"
@@ -378,12 +436,13 @@ func TestReverseProxyHealthCheck(t *testing.T) {
health_uri /health health_uri /health
health_port 2021 health_port 2021
health_interval 2s health_interval 10ms
health_timeout 5s health_timeout 100ms
} }
} }
`, "caddyfile") `, "caddyfile")
time.Sleep(100 * time.Millisecond) // TODO: for some reason this test seems particularly flaky, getting 503 when it should be 200, unless we wait
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!") tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
} }
@@ -424,8 +483,11 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
tester.InitServer(fmt.Sprintf(` tester.InitServer(fmt.Sprintf(`
{ {
skip_install_trust
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
grace_period 1ns
} }
http://localhost:9080 { http://localhost:9080 {
reverse_proxy { reverse_proxy {
@@ -479,8 +541,11 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
tester.InitServer(fmt.Sprintf(` tester.InitServer(fmt.Sprintf(`
{ {
skip_install_trust
admin localhost:2999
http_port 9080 http_port 9080
https_port 9443 https_port 9443
grace_period 1ns
} }
http://localhost:9080 { http://localhost:9080 {
reverse_proxy { reverse_proxy {
+257 -240
View File
@@ -11,92 +11,95 @@ func TestDefaultSNI(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(`{ tester.InitServer(`{
"apps": { "admin": {
"http": { "listen": "localhost:2999"
"http_port": 9080, },
"https_port": 9443, "apps": {
"grace_period": 1, "http": {
"servers": { "http_port": 9080,
"srv0": { "https_port": 9443,
"listen": [ "grace_period": 1,
":9443" "servers": {
], "srv0": {
"routes": [ "listen": [
{ ":9443"
"handle": [ ],
{ "routes": [
"handler": "subroute", {
"routes": [ "handle": [
{ {
"handle": [ "handler": "subroute",
{ "routes": [
"body": "hello from a.caddy.localhost", {
"handler": "static_response", "handle": [
"status_code": 200 {
} "body": "hello from a.caddy.localhost",
], "handler": "static_response",
"match": [ "status_code": 200
{ }
"path": [ ],
"/version" "match": [
] {
} "path": [
] "/version"
} ]
] }
} ]
], }
"match": [ ]
{ }
"host": [ ],
"127.0.0.1" "match": [
] {
} "host": [
], "127.0.0.1"
"terminal": true ]
} }
], ],
"tls_connection_policies": [ "terminal": true
{ }
"certificate_selection": { ],
"any_tag": ["cert0"] "tls_connection_policies": [
}, {
"match": { "certificate_selection": {
"sni": [ "any_tag": ["cert0"]
"127.0.0.1" },
] "match": {
} "sni": [
}, "127.0.0.1"
{ ]
"default_sni": "*.caddy.localhost" }
} },
] {
} "default_sni": "*.caddy.localhost"
} }
}, ]
"tls": { }
"certificates": { }
"load_files": [ },
{ "tls": {
"certificate": "/caddy.localhost.crt", "certificates": {
"key": "/caddy.localhost.key", "load_files": [
"tags": [ {
"cert0" "certificate": "/caddy.localhost.crt",
] "key": "/caddy.localhost.key",
} "tags": [
] "cert0"
} ]
}, }
"pki": { ]
"certificate_authorities" : { }
"local" : { },
"install_trust": false "pki": {
} "certificate_authorities" : {
} "local" : {
} "install_trust": false
} }
} }
`, "json") }
}
}
`, "json")
// act and assert // act and assert
// makes a request with no sni // makes a request with no sni
@@ -108,97 +111,100 @@ func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
"apps": { "admin": {
"http": { "listen": "localhost:2999"
"http_port": 9080, },
"https_port": 9443, "apps": {
"grace_period": 1, "http": {
"servers": { "http_port": 9080,
"srv0": { "https_port": 9443,
"listen": [ "grace_period": 1,
":9443" "servers": {
], "srv0": {
"routes": [ "listen": [
{ ":9443"
"handle": [ ],
{ "routes": [
"handler": "subroute", {
"routes": [ "handle": [
{ {
"handle": [ "handler": "subroute",
{ "routes": [
"body": "hello from a", {
"handler": "static_response", "handle": [
"status_code": 200 {
} "body": "hello from a",
], "handler": "static_response",
"match": [ "status_code": 200
{ }
"path": [ ],
"/version" "match": [
] {
} "path": [
] "/version"
} ]
] }
} ]
], }
"match": [ ]
{ }
"host": [ ],
"a.caddy.localhost", "match": [
"127.0.0.1" {
] "host": [
} "a.caddy.localhost",
], "127.0.0.1"
"terminal": true ]
} }
], ],
"tls_connection_policies": [ "terminal": true
{ }
"certificate_selection": { ],
"any_tag": ["cert0"] "tls_connection_policies": [
}, {
"default_sni": "a.caddy.localhost", "certificate_selection": {
"match": { "any_tag": ["cert0"]
"sni": [ },
"a.caddy.localhost", "default_sni": "a.caddy.localhost",
"127.0.0.1", "match": {
"" "sni": [
] "a.caddy.localhost",
} "127.0.0.1",
}, ""
{ ]
"default_sni": "a.caddy.localhost" }
} },
] {
} "default_sni": "a.caddy.localhost"
} }
}, ]
"tls": { }
"certificates": { }
"load_files": [ },
{ "tls": {
"certificate": "/a.caddy.localhost.crt", "certificates": {
"key": "/a.caddy.localhost.key", "load_files": [
"tags": [ {
"cert0" "certificate": "/a.caddy.localhost.crt",
] "key": "/a.caddy.localhost.key",
} "tags": [
] "cert0"
} ]
}, }
"pki": { ]
"certificate_authorities" : { }
"local" : { },
"install_trust": false "pki": {
} "certificate_authorities" : {
} "local" : {
} "install_trust": false
} }
} }
`, "json") }
}
}
`, "json")
// act and assert // act and assert
// makes a request with no sni // makes a request with no sni
@@ -209,69 +215,72 @@ func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
"apps": { "admin": {
"http": { "listen": "localhost:2999"
"http_port": 9080, },
"https_port": 9443, "apps": {
"grace_period": 1, "http": {
"servers": { "http_port": 9080,
"srv0": { "https_port": 9443,
"listen": [ "grace_period": 1,
":9443" "servers": {
], "srv0": {
"routes": [ "listen": [
{ ":9443"
"handle": [ ],
{ "routes": [
"body": "hello from a.caddy.localhost", {
"handler": "static_response", "handle": [
"status_code": 200 {
} "body": "hello from a.caddy.localhost",
], "handler": "static_response",
"match": [ "status_code": 200
{ }
"path": [ ],
"/version" "match": [
] {
} "path": [
] "/version"
} ]
], }
"tls_connection_policies": [ ]
{ }
"certificate_selection": { ],
"any_tag": ["cert0"] "tls_connection_policies": [
}, {
"default_sni": "a.caddy.localhost" "certificate_selection": {
} "any_tag": ["cert0"]
] },
} "default_sni": "a.caddy.localhost"
} }
}, ]
"tls": { }
"certificates": { }
"load_files": [ },
{ "tls": {
"certificate": "/a.caddy.localhost.crt", "certificates": {
"key": "/a.caddy.localhost.key", "load_files": [
"tags": [ {
"cert0" "certificate": "/a.caddy.localhost.crt",
] "key": "/a.caddy.localhost.key",
} "tags": [
] "cert0"
} ]
}, }
"pki": { ]
"certificate_authorities" : { }
"local" : { },
"install_trust": false "pki": {
} "certificate_authorities" : {
} "local" : {
} "install_trust": false
} }
} }
`, "json") }
}
}
`, "json")
// act and assert // act and assert
// makes a request with no sni // makes a request with no sni
@@ -281,6 +290,7 @@ func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
func TestHttpOnlyOnDomainWithSNI(t *testing.T) { func TestHttpOnlyOnDomainWithSNI(t *testing.T) {
caddytest.AssertAdapt(t, ` caddytest.AssertAdapt(t, `
{ {
skip_install_trust
default_sni a.caddy.localhost default_sni a.caddy.localhost
} }
:80 { :80 {
@@ -316,6 +326,13 @@ func TestHttpOnlyOnDomainWithSNI(t *testing.T) {
] ]
} }
} }
},
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
} }
} }
}`) }`)
+6
View File
@@ -23,6 +23,9 @@ func TestH2ToH2CStream(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
"admin": {
"listen": "localhost:2999"
},
"apps": { "apps": {
"http": { "http": {
"http_port": 9080, "http_port": 9080,
@@ -206,6 +209,9 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {
"admin": {
"listen": "localhost:2999"
},
"logging": { "logging": {
"logs": { "logs": {
"default": { "default": {
+3
View File
@@ -0,0 +1,3 @@
{
admin localhost:2999
}
+1 -1
View File
@@ -448,7 +448,7 @@ func (ctx Context) Storage() certmagic.Storage {
// different module; it panics if more than 1 value is passed in. // different module; it panics if more than 1 value is passed in.
// //
// Originally, this method's signature was `Logger(mod Module)`, // Originally, this method's signature was `Logger(mod Module)`,
// requiring that an instance of a Caddy module be passsed in. // requiring that an instance of a Caddy module be passed in.
// However, that is no longer necessary, as the closest module // However, that is no longer necessary, as the closest module
// most recently associated with the context will be automatically // most recently associated with the context will be automatically
// assumed. To prevent a sudden breaking change, this method's // assumed. To prevent a sudden breaking change, this method's
+19 -19
View File
@@ -7,34 +7,34 @@ require (
github.com/Masterminds/sprig/v3 v3.2.2 github.com/Masterminds/sprig/v3 v3.2.2
github.com/alecthomas/chroma v0.10.0 github.com/alecthomas/chroma v0.10.0
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b 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/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
github.com/go-chi/chi v4.1.2+incompatible 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/google/uuid v1.3.0
github.com/klauspost/compress v1.15.9 github.com/klauspost/compress v1.15.11
github.com/klauspost/cpuid/v2 v2.1.0 github.com/klauspost/cpuid/v2 v2.1.1
github.com/lucas-clemente/quic-go v0.28.2-0.20220813150001-9957668d4301 github.com/lucas-clemente/quic-go v0.29.2
github.com/mholt/acmez v1.0.4 github.com/mholt/acmez v1.0.4
github.com/prometheus/client_golang v1.12.2 github.com/prometheus/client_golang v1.12.2
github.com/smallstep/certificates v0.21.0 github.com/smallstep/certificates v0.22.1
github.com/smallstep/cli v0.21.0 github.com/smallstep/cli v0.22.0
github.com/smallstep/nosql v0.4.0 github.com/smallstep/nosql v0.4.0
github.com/smallstep/truststore v0.12.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/spf13/pflag v1.0.5
github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2 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 github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0
go.opentelemetry.io/otel v1.9.0 go.opentelemetry.io/otel v1.9.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.0
go.opentelemetry.io/otel/sdk 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/crypto v0.0.0-20220722155217-630584e8d5aa
golang.org/x/net v0.0.0-20220812165438-1d4ff48094d1 golang.org/x/net v0.0.0-20220812165438-1d4ff48094d1
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 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/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -56,7 +56,7 @@ require (
github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // 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 v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd // 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/libdns/libdns v0.2.1 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect github.com/manifoldco/promptui v0.9.0 // indirect
github.com/marten-seemann/qpack v0.2.1 // 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-18 v0.1.3 // indirect
github.com/marten-seemann/qtls-go1-19 v0.1.0 // 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-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect github.com/mattn/go-isatty v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // 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/metric v0.31.0 // indirect
go.opentelemetry.io/otel/trace v1.9.0 // indirect go.opentelemetry.io/otel/trace v1.9.0 // indirect
go.opentelemetry.io/proto/otlp v0.12.0 // indirect go.opentelemetry.io/proto/otlp v0.12.0 // indirect
go.step.sm/cli-utils v0.7.3 // indirect go.step.sm/cli-utils v0.7.4 // indirect
go.step.sm/crypto v0.16.2 // indirect go.step.sm/crypto v0.18.0 // indirect
go.step.sm/linkedca v0.16.1 // indirect go.step.sm/linkedca v0.18.0 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.6.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/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
golang.org/x/tools v0.1.10 // indirect golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/grpc v1.46.0 // indirect google.golang.org/grpc v1.47.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // 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 package caddy
import ( import (
"fmt" "context"
"net" "net"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -12,21 +29,14 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
func ListenTimeout(network, addr string, keepAlivePeriod time.Duration) (net.Listener, error) { func reuseUnixSocket(network, addr string) (any, error) {
// check to see if plugin provides listener return nil, nil
if ln, err := getListenerFromPlugin(network, addr); err != nil || ln != nil { }
return ln, err
}
lnKey := listenerKey(network, addr)
func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) {
sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, 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 { 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 nil, err
} }
return &sharedListener{Listener: ln, key: lnKey}, nil return &sharedListener{Listener: ln, key: lnKey}, nil
@@ -34,8 +44,7 @@ func ListenTimeout(network, addr string, keepAlivePeriod time.Duration) (net.Lis
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil
return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: keepAlivePeriod}, nil
} }
// fakeCloseListener is a private wrapper over a listener that // fakeCloseListener is a private wrapper over a listener that
@@ -139,8 +148,6 @@ func (sl *sharedListener) clearDeadline() error {
switch ln := sl.Listener.(type) { switch ln := sl.Listener.(type) {
case *net.TCPListener: case *net.TCPListener:
err = ln.SetDeadline(time.Time{}) err = ln.SetDeadline(time.Time{})
case *net.UnixListener:
err = ln.SetDeadline(time.Time{})
} }
sl.deadline = false sl.deadline = false
} }
@@ -156,8 +163,6 @@ func (sl *sharedListener) setDeadline() error {
switch ln := sl.Listener.(type) { switch ln := sl.Listener.(type) {
case *net.TCPListener: case *net.TCPListener:
err = ln.SetDeadline(timeInPast) err = ln.SetDeadline(timeInPast)
case *net.UnixListener:
err = ln.SetDeadline(timeInPast)
} }
sl.deadline = true 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" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"net/netip" "net/netip"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"sync"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"time"
"github.com/lucas-clemente/quic-go" "github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/http3" "github.com/lucas-clemente/quic-go/http3"
"go.uber.org/zap" "go.uber.org/zap"
) )
// Listen is like net.Listen, except Caddy's listeners can overlap // NetworkAddress represents one or more network addresses.
// each other: multiple listeners may be created on the same socket // It contains the individual components for a parsed network
// at the same time. This is useful because during config changes, // address of the form accepted by ParseNetworkAddress().
// the new config is started while the old config is still running. type NetworkAddress struct {
// When Caddy listeners are closed, the closing logic is virtualized // Should be a network value accepted by Go's net package or
// so the underlying socket isn't actually closed until all uses of // by a plugin providing a listener for that network type.
// the socket have been finished. Always be sure to close listeners Network string
// when you are done with them, just like normal listeners.
func Listen(network, addr string) (net.Listener, error) { // The "main" part of the network address is the host, which
// a 0 timeout means Go uses its default // often takes the form of a hostname, DNS name, IP address,
return ListenTimeout(network, addr, 0) // 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 // ListenAll calls Listen() for all addresses represented by this struct, i.e. all ports in the range.
// if a plugin has registered the network name. It may return (nil, nil) if // (If the address doesn't use ports or has 1 port only, then only 1 listener will be created.)
// no plugin can provide a listener. // It returns an error if any listener failed to bind, and closes any listeners opened up to that point.
func getListenerFromPlugin(network, addr string) (net.Listener, error) { //
network = strings.TrimSpace(strings.ToLower(network)) // 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 one of the addresses has a failure, we need to close
if getListener, ok := networkTypes[network]; ok { // any that did open a socket to avoid leaking resources
Log().Debug("getting listener from plugin", zap.String("network", network)) defer func() {
return getListener(network, addr) if err == nil {
} return
}
return nil, nil for _, ln := range listeners {
} if cl, ok := ln.(io.Closer); ok {
cl.Close()
// 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)
} }
}
}()
// 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 nil, err
} }
return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil listeners = append(listeners, ln)
})
if err != nil {
return nil, err
} }
return &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}, nil return listeners, nil
} }
// ListenQUIC returns a quic.EarlyListener suitable for use in a Caddy module. // Listen is similar to net.Listen, with a few differences:
// Note that the context passed to Accept is currently ignored, so using //
// a context other than context.Background is meaningless. // Listen announces on the network address using the port calculated by adding
// This API is EXPERIMENTAL and may change. // portOffset to the start port. (For network types that do not use ports, the
func ListenQUIC(addr string, tlsConf *tls.Config, activeRequests *int64) (quic.EarlyListener, error) { // portOffset is ignored.)
lnKey := listenerKey("udp", addr) //
// 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) { // check to see if plugin provides listener
el, err := quic.ListenAddrEarly(addr, http3.ConfigureTLSConfig(tlsConf), &quic.Config{ if ln, err := getListenerFromPlugin(ctx, na.Network, na.JoinHostPort(portOffset), config); ln != nil || err != nil {
RequireAddressValidation: func(clientAddr net.Addr) bool { return ln, err
var highLoad bool }
if activeRequests != nil {
highLoad = atomic.LoadInt64(activeRequests) > 1000 // TODO: make tunable? // create (or reuse) the listener ourselves
} return na.listen(ctx, portOffset, config)
return highLoad }
},
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 { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
if ln == nil {
ctx, cancel := context.WithCancel(context.Background()) return nil, fmt.Errorf("unsupported network type: %s", na.Network)
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 the listener is "closed", return a fake closed error instead // if new listener is a unix socket, make sure we can reuse it later
if atomic.LoadInt32(&fcql.closed) == 1 && errors.Is(err, context.Canceled) { // (we do our own "unlink on close" -- not required, but more tidy)
return nil, fakeClosedErr(fcql) 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 { return ln, nil
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
} }
// IsUnixNetwork returns true if na.Network is // 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))) 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 { func (na NetworkAddress) Expand() []NetworkAddress {
size := na.PortRangeSize() size := na.PortRangeSize()
addrs := make([]NetworkAddress, size) addrs := make([]NetworkAddress, size)
for portOffset := uint(0); portOffset < size; portOffset++ { for portOffset := uint(0); portOffset < size; portOffset++ {
na2 := na addrs[portOffset] = na.At(portOffset)
na2.StartPort, na2.EndPort = na.StartPort+portOffset, na.StartPort+portOffset
addrs[portOffset] = na2
} }
return addrs 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 // PortRangeSize returns how many ports are in
// pa's port range. Port ranges are inclusive, // pa's port range. Port ranges are inclusive,
// so the size is the difference of start and // 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" 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 // ParseNetworkAddress parses addr into its individual
// components. The input string is expected to be of // components. The input string is expected to be of
// the form "network/host:port-range" where any part is // the form "network/host:port-range" where any part is
@@ -439,6 +392,208 @@ func JoinNetworkAddress(network, host, port string) string {
return a 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 // 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. // 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 // 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 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. // 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 // 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 // before old ones are unloaded, so listeners may overlap briefly if the configs
// both need the same listener. EXPERIMENTAL and subject to change. // 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{} 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 // then set up any other custom logs
for name, l := range logging.Logs { for name, l := range logging.Logs {
// the default log is already set up // the default log is already set up
if name == "default" { if name == DefaultLoggerName {
continue continue
} }
@@ -138,7 +138,7 @@ func (logging *Logging) setupNewDefault(ctx Context) error {
// extract the user-defined default log, if any // extract the user-defined default log, if any
newDefault := new(defaultCustomLog) newDefault := new(defaultCustomLog)
if userDefault, ok := logging.Logs["default"]; ok { if userDefault, ok := logging.Logs[DefaultLoggerName]; ok {
newDefault.CustomLog = userDefault newDefault.CustomLog = userDefault
} else { } else {
// if none, make one with our own default settings // if none, make one with our own default settings
@@ -147,7 +147,7 @@ func (logging *Logging) setupNewDefault(ctx Context) error {
if err != nil { if err != nil {
return fmt.Errorf("setting up default Caddy log: %v", err) 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 // set up this new log
@@ -702,6 +702,8 @@ var (
var writers = NewUsagePool() var writers = NewUsagePool()
const DefaultLoggerName = "default"
// Interface guards // Interface guards
var ( var (
_ io.WriteCloser = (*notClosable)(nil) _ io.WriteCloser = (*notClosable)(nil)
+39 -9
View File
@@ -18,6 +18,7 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"net"
"net/http" "net/http"
"strconv" "strconv"
"sync" "sync"
@@ -65,7 +66,7 @@ func init() {
// `{http.request.orig_uri}` | The request's original URI // `{http.request.orig_uri}` | The request's original URI
// `{http.request.port}` | The port part of the request's Host header // `{http.request.port}` | The port part of the request's Host header
// `{http.request.proto}` | The protocol of the request // `{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.port}` | The port part of the remote client's address
// `{http.request.remote}` | The address of the remote client // `{http.request.remote}` | The address of the remote client
// `{http.request.scheme}` | The request scheme // `{http.request.scheme}` | The request scheme
@@ -387,10 +388,11 @@ func (app *App) Start() error {
for portOffset := uint(0); portOffset < listenAddr.PortRangeSize(); portOffset++ { for portOffset := uint(0); portOffset < listenAddr.PortRangeSize(); portOffset++ {
// create the listener for this socket // create the listener for this socket
hostport := listenAddr.JoinHostPort(portOffset) 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 { 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) // wrap listener before TLS (up to the TLS placeholder wrapper)
var lnWrapperIdx int var lnWrapperIdx int
@@ -410,9 +412,26 @@ func (app *App) Start() error {
// enable HTTP/3 if configured // enable HTTP/3 if configured
if srv.protocol("h3") { if srv.protocol("h3") {
app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport)) // Can't serve HTTP/3 on the same socket as HTTP/1 and 2 because it uses
if err := srv.serveHTTP3(hostport, tlsCfg); err != nil { // a different transport mechanism... which is fine, but the OS doesn't
return err // 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
}
} }
} }
} }
@@ -424,11 +443,10 @@ func (app *App) Start() error {
// if binding to port 0, the OS chooses a port for us; // if binding to port 0, the OS chooses a port for us;
// but the user won't know the port unless we print it // 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", app.logger.Info("port 0 listener",
zap.String("input_address", lnAddr), 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", app.logger.Debug("starting server loop",
@@ -533,6 +551,18 @@ func (app *App) Stop() error {
if server.h3server == nil { if server.h3server == nil {
return 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.String("address", el.LocalAddr().String()))
}
}
// TODO: CloseGracefully, once implemented upstream (see https://github.com/lucas-clemente/quic-go/issues/2103) // TODO: CloseGracefully, once implemented upstream (see https://github.com/lucas-clemente/quic-go/issues/2103)
if err := server.h3server.Close(); err != nil { if err := server.h3server.Close(); err != nil {
app.logger.Error("HTTP/3 server shutdown", app.logger.Error("HTTP/3 server shutdown",
+1 -4
View File
@@ -241,8 +241,6 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
if !rw.wroteHeader { if !rw.wroteHeader {
if rw.statusCode != 0 { if rw.statusCode != 0 {
rw.HTTPInterfaces.WriteHeader(rw.statusCode) rw.HTTPInterfaces.WriteHeader(rw.statusCode)
} else {
rw.HTTPInterfaces.WriteHeader(http.StatusOK)
} }
rw.wroteHeader = true rw.wroteHeader = true
} }
@@ -264,10 +262,9 @@ func (rw *responseWriter) Close() error {
rw.init() rw.init()
} }
// issue #5059, don't write status code if not set explicitly.
if rw.statusCode != 0 { if rw.statusCode != 0 {
rw.HTTPInterfaces.WriteHeader(rw.statusCode) rw.HTTPInterfaces.WriteHeader(rw.statusCode)
} else {
rw.HTTPInterfaces.WriteHeader(http.StatusOK)
} }
rw.wroteHeader = true rw.wroteHeader = true
} }
+4 -3
View File
@@ -16,6 +16,7 @@ package fileserver
import ( import (
"bytes" "bytes"
"context"
_ "embed" _ "embed"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -82,7 +83,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
// calling path.Clean here prevents weird breadcrumbs when URL paths are sketchy like /%2e%2e%2f // 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 { switch {
case os.IsPermission(err): case os.IsPermission(err):
return caddyhttp.Error(http.StatusForbidden, err) return caddyhttp.Error(http.StatusForbidden, err)
@@ -136,7 +137,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
return nil 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 files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return browseTemplateContext{}, err 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 "/" // user can presumably browse "up" to parent folder if path is longer than "/"
canGoUp := len(urlPath) > 1 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. // browseApplyQueryParams applies query parameters to the listing.
+12
View File
@@ -27,6 +27,10 @@ a:visited {
color: #800080; color: #800080;
} }
a:visited:hover {
color: #b900b9;
}
header, header,
#summary { #summary {
padding-left: 5%; padding-left: 5%;
@@ -244,6 +248,14 @@ footer {
color: #62b2fd; color: #62b2fd;
} }
a:visited {
color: #c269c2;
}
a:visited:hover {
color: #d03cd0;
}
tr { tr {
border-bottom: 1px dashed rgba(255, 255, 255, 0.12); border-bottom: 1px dashed rgba(255, 255, 255, 0.12);
} }
@@ -15,6 +15,7 @@
package fileserver package fileserver
import ( import (
"context"
"io/fs" "io/fs"
"net/url" "net/url"
"os" "os"
@@ -30,13 +31,17 @@ import (
"go.uber.org/zap" "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) filesToHide := fsrv.transformHidePaths(repl)
var dirCount, fileCount int var dirCount, fileCount int
fileInfos := []fileInfo{} fileInfos := []fileInfo{}
for _, entry := range entries { for _, entry := range entries {
if err := ctx.Err(); err != nil {
break
}
name := entry.Name() name := entry.Name()
if fileHidden(name, filesToHide) { if fileHidden(name, filesToHide) {
+1
View File
@@ -57,6 +57,7 @@ respond with a file listing.`,
fs.Bool("browse", false, "Enable directory browsing") fs.Bool("browse", false, "Enable directory browsing")
fs.Bool("templates", false, "Enable template rendering") fs.Bool("templates", false, "Enable template rendering")
fs.Bool("access-log", false, "Enable the access log") fs.Bool("access-log", false, "Enable the access log")
fs.Bool("debug", false, "Enable verbose debug logs")
return fs return fs
}(), }(),
}) })
+1 -1
View File
@@ -247,7 +247,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
info, err := fs.Stat(fsrv.fileSystem, filename) info, err := fs.Stat(fsrv.fileSystem, filename)
if err != nil { if err != nil {
err = fsrv.mapDirOpenError(err, filename) 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) return fsrv.notFound(w, r, next)
} else if errors.Is(err, fs.ErrPermission) { } else if errors.Is(err, fs.ErrPermission) {
return caddyhttp.Error(http.StatusForbidden, err) 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 // parseCaddyfile sets up the handler for response headers from
// Caddyfile tokens. Syntax: // Caddyfile tokens. Syntax:
// //
// header [<matcher>] [[+|-|?]<field> [<value|regexp>] [<replacement>]] { // header [<matcher>] [[+|-|?]<field> [<value|regexp>] [<replacement>]] {
// [+]<field> [<value|regexp> [<replacement>]] // [+]<field> [<value|regexp> [<replacement>]]
// ?<field> <default_value> // ?<field> <default_value>
// -<field> // -<field>
// [defer] // [defer]
// } // }
// //
// Either a block can be opened or a single header field can be configured // 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 // 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 // parseReqHdrCaddyfile sets up the handler for request headers
// from Caddyfile tokens. Syntax: // 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) { func parseReqHdrCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
if !h.Next() { if !h.Next() {
return nil, h.ArgErr() return nil, h.ArgErr()
+4 -1
View File
@@ -332,7 +332,10 @@ func (rww *responseWriterWrapper) WriteHeader(status int) {
if rww.wroteHeader { if rww.wroteHeader {
return 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.require == nil || rww.require.Match(status, rww.ResponseWriterWrapper.Header()) {
if rww.headerOps != nil { if rww.headerOps != nil {
rww.headerOps.ApplyTo(rww.ResponseWriterWrapper.Header(), rww.replacer) rww.headerOps.ApplyTo(rww.ResponseWriterWrapper.Header(), rww.replacer)
+2 -11
View File
@@ -109,22 +109,13 @@ func (h *Handler) Validate() error {
} }
seen[input] = i 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 // ensure mappings have 1:1 output-to-destination correspondence
nOut := len(m.Outputs) nOut := len(m.Outputs)
if nOut != nDest { if nOut != nDest {
return fmt.Errorf("mapping %d has %d outputs but there are %d destinations defined", i, nOut, nDest) return fmt.Errorf("mapping %d has %d outputs but there are %d destinations defined", i, nOut, nDest)
} }
} }
return nil 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 // fall back to default if no match or if matched nil value
if len(h.Defaults) > destIdx { if len(h.Defaults) > destIdx {
return h.Defaults[destIdx], true return repl.ReplaceAll(h.Defaults[destIdx], ""), true
} }
return nil, true return nil, true
+22
View File
@@ -98,6 +98,28 @@ func TestHandler(t *testing.T) {
"output": "testing", "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 { if err := tc.handler.Provision(caddy.Context{}); err != nil {
t.Fatalf("Test %d: Provisioning handler: %v", i, err) t.Fatalf("Test %d: Provisioning handler: %v", i, err)
+3 -1
View File
@@ -156,7 +156,9 @@ type (
MatchHeaderRE map[string]*MatchRegexp MatchHeaderRE map[string]*MatchRegexp
// MatchProtocol matches requests by protocol. Recognized values are // 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 MatchProtocol string
// MatchRemoteIP matches requests by client IP (or CIDR range). // MatchRemoteIP matches requests by client IP (or CIDR range).
+32
View File
@@ -31,6 +31,7 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/netip"
"net/textproto" "net/textproto"
"net/url" "net/url"
"path" "path"
@@ -196,6 +197,37 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
return or.URL.RawQuery, true 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 // hostname labels
if strings.HasPrefix(key, reqHostLabelsReplPrefix) { if strings.HasPrefix(key, reqHostLabelsReplPrefix) {
idxStr := key[len(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) ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx) req = req.WithContext(ctx)
req.Host = "example.com:80" req.Host = "example.com:80"
req.RemoteAddr = "localhost:1234" req.RemoteAddr = "192.168.159.32:1234"
clientCert := []byte(`-----BEGIN CERTIFICATE----- clientCert := []byte(`-----BEGIN CERTIFICATE-----
MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk
@@ -61,7 +61,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
req.TLS = &tls.ConnectionState{ req.TLS = &tls.ConnectionState{
Version: tls.VersionTLS13, Version: tls.VersionTLS13,
HandshakeComplete: true, HandshakeComplete: true,
ServerName: "foo.com", ServerName: "example.com",
CipherSuite: tls.TLS_AES_256_GCM_SHA384, CipherSuite: tls.TLS_AES_256_GCM_SHA384,
PeerCertificates: []*x509.Certificate{cert}, PeerCertificates: []*x509.Certificate{cert},
NegotiatedProtocol: "h2", NegotiatedProtocol: "h2",
@@ -97,7 +97,19 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
}, },
{ {
get: "http.request.remote.host", 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", get: "http.request.remote.port",
@@ -146,7 +158,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
}, },
{ {
get: "http.request.tls.server_name", get: "http.request.tls.server_name",
expect: "foo.com", expect: "example.com",
}, },
{ {
get: "http.request.tls.version", get: "http.request.tls.version",
+2 -1
View File
@@ -170,9 +170,10 @@ func (rr *responseRecorder) WriteHeader(statusCode int) {
return return
} }
// save statusCode in case http middleware upgrading websocket // save statusCode always, in case HTTP middleware upgrades websocket
// connections by manually setting headers and writing status 101 // connections by manually setting headers and writing status 101
rr.statusCode = statusCode rr.statusCode = statusCode
// 1xx responses aren't final; just informational // 1xx responses aren't final; just informational
if statusCode < 100 || statusCode > 199 { if statusCode < 100 || statusCode > 199 {
rr.wroteHeader = true rr.wroteHeader = true
+2 -2
View File
@@ -537,9 +537,9 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if !d.NextArg() { if !d.NextArg() {
return d.ArgErr() return d.ArgErr()
} }
size, err := strconv.Atoi(d.Val()) size, err := humanize.ParseBytes(d.Val())
if err != nil { 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() { if d.NextArg() {
return d.ArgErr() return d.ArgErr()
+1 -1
View File
@@ -117,7 +117,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid upstream address %s: %v", toLoc, err) return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid upstream address %s: %v", toLoc, err)
} }
if scheme != "" && toScheme != "" { if scheme != "" && toScheme == "" {
toScheme = scheme toScheme = scheme
} }
toAddresses[i] = addr toAddresses[i] = addr
@@ -348,7 +348,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
} }
redirHandler := caddyhttp.StaticResponse{ redirHandler := caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)), 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{ redirRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet}, 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)) log.Println("c: send data length ≈", length, string(content))
conn.Close() conn.Close()
time.Sleep(1 * time.Second) time.Sleep(250 * time.Millisecond)
if bytes.Contains(content, []byte("FAILED")) { if bytes.Contains(content, []byte("FAILED")) {
globalt.Error("Server return failed message") 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 // init
fcgiParams := make(map[string]string) fcgiParams := make(map[string]string)
@@ -38,29 +38,28 @@ func init() {
// configured for most™️ auth gateways that support forward auth. The typical // configured for most™️ auth gateways that support forward auth. The typical
// config which looks something like this: // config which looks something like this:
// //
// forward_auth auth-gateway:9091 { // forward_auth auth-gateway:9091 {
// uri /authenticate?redirect=https://auth.example.com // uri /authenticate?redirect=https://auth.example.com
// copy_headers Remote-User Remote-Email // copy_headers Remote-User Remote-Email
// } // }
// //
// is equivalent to a reverse_proxy directive like this: // is equivalent to a reverse_proxy directive like this:
// //
// reverse_proxy auth-gateway:9091 { // reverse_proxy auth-gateway:9091 {
// method GET // method GET
// rewrite /authenticate?redirect=https://auth.example.com // rewrite /authenticate?redirect=https://auth.example.com
// //
// header_up X-Forwarded-Method {method} // header_up X-Forwarded-Method {method}
// header_up X-Forwarded-Uri {uri} // 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}
// }
// }
// }
// //
// @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) { func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
if !h.Next() { if !h.Next() {
return nil, h.ArgErr() 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 // need at least one handler in the routes for the response handling
// logic in reverse_proxy to not skip this entry as empty. // logic in reverse_proxy to not skip this entry as empty.
for from, to := range headersToCopy { for from, to := range headersToCopy {
handler.Request.Set[to] = []string{ handler.Request.Set.Set(to, "{http.reverse_proxy.header."+http.CanonicalHeaderKey(from)+"}")
"{http.reverse_proxy.header." + from + "}",
}
} }
goodResponseHandler.Routes = append( goodResponseHandler.Routes = append(
@@ -782,8 +782,9 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
copyHeader(h, http.Header(header)) copyHeader(h, http.Header(header))
rw.WriteHeader(code) rw.WriteHeader(code)
// Clear headers, it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses // Clear headers coming from the backend
for k := range h { // (it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses)
for k := range header {
delete(h, k) delete(h, k)
} }
@@ -976,9 +977,11 @@ func (h Handler) finalizeResponse(
} }
rw.WriteHeader(res.StatusCode) rw.WriteHeader(res.StatusCode)
logger.Debug("wrote header")
err := h.copyResponse(rw, res.Body, h.flushInterval(req, res)) err := h.copyResponse(rw, res.Body, h.flushInterval(req, res), logger)
res.Body.Close() // close now, instead of defer, to populate res.Trailer errClose := res.Body.Close() // close now, instead of defer, to populate res.Trailer
logger.Debug("closed response body from upstream", zap.Error(errClose))
if err != nil { if err != nil {
// we're streaming the response and we've already written headers, so // we're streaming the response and we've already written headers, so
// there's nothing an error handler can do to recover at this point; // there's nothing an error handler can do to recover at this point;
@@ -1013,6 +1016,8 @@ func (h Handler) finalizeResponse(
} }
} }
logger.Debug("response finalized")
return nil return nil
} }
+19 -3
View File
@@ -166,12 +166,13 @@ func (h Handler) isBidirectionalStream(req *http.Request, res *http.Response) bo
(ae == "identity" || ae == "") (ae == "identity" || ae == "")
} }
func (h Handler) copyResponse(dst io.Writer, src io.Reader, flushInterval time.Duration) error { func (h Handler) copyResponse(dst io.Writer, src io.Reader, flushInterval time.Duration, logger *zap.Logger) error {
if flushInterval != 0 { if flushInterval != 0 {
if wf, ok := dst.(writeFlusher); ok { if wf, ok := dst.(writeFlusher); ok {
mlw := &maxLatencyWriter{ mlw := &maxLatencyWriter{
dst: wf, dst: wf,
latency: flushInterval, latency: flushInterval,
logger: logger.Named("max_latency_writer"),
} }
defer mlw.stop() defer mlw.stop()
@@ -185,19 +186,22 @@ func (h Handler) copyResponse(dst io.Writer, src io.Reader, flushInterval time.D
buf := streamingBufPool.Get().(*[]byte) buf := streamingBufPool.Get().(*[]byte)
defer streamingBufPool.Put(buf) defer streamingBufPool.Put(buf)
_, err := h.copyBuffer(dst, src, *buf) _, err := h.copyBuffer(dst, src, *buf, logger)
return err return err
} }
// copyBuffer returns any write errors or non-EOF read errors, and the amount // copyBuffer returns any write errors or non-EOF read errors, and the amount
// of bytes written. // of bytes written.
func (h Handler) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) { func (h Handler) copyBuffer(dst io.Writer, src io.Reader, buf []byte, logger *zap.Logger) (int64, error) {
if len(buf) == 0 { if len(buf) == 0 {
buf = make([]byte, defaultBufferSize) buf = make([]byte, defaultBufferSize)
} }
var written int64 var written int64
for { for {
logger.Debug("waiting to read from upstream")
nr, rerr := src.Read(buf) nr, rerr := src.Read(buf)
logger := logger.With(zap.Int("read", nr))
logger.Debug("read from upstream", zap.Error(rerr))
if rerr != nil && rerr != io.EOF && rerr != context.Canceled { if rerr != nil && rerr != io.EOF && rerr != context.Canceled {
// TODO: this could be useful to know (indeed, it revealed an error in our // TODO: this could be useful to know (indeed, it revealed an error in our
// fastcgi PoC earlier; but it's this single error report here that necessitates // fastcgi PoC earlier; but it's this single error report here that necessitates
@@ -209,10 +213,15 @@ func (h Handler) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, er
h.logger.Error("reading from backend", zap.Error(rerr)) h.logger.Error("reading from backend", zap.Error(rerr))
} }
if nr > 0 { if nr > 0 {
logger.Debug("writing to downstream")
nw, werr := dst.Write(buf[:nr]) nw, werr := dst.Write(buf[:nr])
if nw > 0 { if nw > 0 {
written += int64(nw) written += int64(nw)
} }
logger.Debug("wrote to downstream",
zap.Int("written", nw),
zap.Int64("written_total", written),
zap.Error(werr))
if werr != nil { if werr != nil {
return written, werr return written, werr
} }
@@ -295,17 +304,21 @@ type maxLatencyWriter struct {
mu sync.Mutex // protects t, flushPending, and dst.Flush mu sync.Mutex // protects t, flushPending, and dst.Flush
t *time.Timer t *time.Timer
flushPending bool flushPending bool
logger *zap.Logger
} }
func (m *maxLatencyWriter) Write(p []byte) (n int, err error) { func (m *maxLatencyWriter) Write(p []byte) (n int, err error) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
n, err = m.dst.Write(p) n, err = m.dst.Write(p)
m.logger.Debug("wrote bytes", zap.Int("n", n), zap.Error(err))
if m.latency < 0 { if m.latency < 0 {
m.logger.Debug("flushing immediately")
m.dst.Flush() m.dst.Flush()
return return
} }
if m.flushPending { if m.flushPending {
m.logger.Debug("delayed flush already pending")
return return
} }
if m.t == nil { if m.t == nil {
@@ -313,6 +326,7 @@ func (m *maxLatencyWriter) Write(p []byte) (n int, err error) {
} else { } else {
m.t.Reset(m.latency) m.t.Reset(m.latency)
} }
m.logger.Debug("timer set for delayed flush", zap.Duration("duration", m.latency))
m.flushPending = true m.flushPending = true
return return
} }
@@ -321,8 +335,10 @@ func (m *maxLatencyWriter) delayedFlush() {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
if !m.flushPending { // if stop was called but AfterFunc already started this goroutine if !m.flushPending { // if stop was called but AfterFunc already started this goroutine
m.logger.Debug("delayed flush is not pending")
return return
} }
m.logger.Debug("delayed flush")
m.dst.Flush() m.dst.Flush()
m.flushPending = false m.flushPending = false
} }
@@ -4,6 +4,8 @@ import (
"bytes" "bytes"
"strings" "strings"
"testing" "testing"
"github.com/caddyserver/caddy/v2"
) )
func TestHandlerCopyResponse(t *testing.T) { func TestHandlerCopyResponse(t *testing.T) {
@@ -18,7 +20,7 @@ func TestHandlerCopyResponse(t *testing.T) {
for _, d := range testdata { for _, d := range testdata {
src := bytes.NewBuffer([]byte(d)) src := bytes.NewBuffer([]byte(d))
dst.Reset() dst.Reset()
err := h.copyResponse(dst, src, 0) err := h.copyResponse(dst, src, 0, caddy.Log())
if err != nil { if err != nil {
t.Errorf("failed with error: %v", err) t.Errorf("failed with error: %v", err)
} }
+7 -2
View File
@@ -383,8 +383,13 @@ func trimPathPrefix(escapedPath, prefix string) string {
iPrefix++ iPrefix++
} }
// found matching prefix, trim it // if we iterated through the entire prefix, we found it, so trim it
return escapedPath[iPath:] if iPath >= len(prefix) {
return escapedPath[iPath:]
}
// otherwise we did not find the prefix
return escapedPath
} }
func reverse(s string) string { func reverse(s string) string {
+10
View File
@@ -225,6 +225,16 @@ func TestRewrite(t *testing.T) {
input: newRequest(t, "GET", "/prefix/foo/bar"), input: newRequest(t, "GET", "/prefix/foo/bar"),
expect: newRequest(t, "GET", "/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"}, rule: Rewrite{StripPathPrefix: "/prefix"},
input: newRequest(t, "GET", "/prefix/foo%2Fbar"), input: newRequest(t, "GET", "/prefix/foo%2Fbar"),
+40 -18
View File
@@ -170,9 +170,10 @@ type Server struct {
errorLogger *zap.Logger errorLogger *zap.Logger
ctx caddy.Context ctx caddy.Context
server *http.Server server *http.Server
h3server *http3.Server h3server *http3.Server
addresses []caddy.NetworkAddress 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 shutdownAt time.Time
shutdownAtMu *sync.RWMutex shutdownAtMu *sync.RWMutex
@@ -193,9 +194,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&s.activeRequests, 1) atomic.AddInt64(&s.activeRequests, 1)
defer atomic.AddInt64(&s.activeRequests, -1) defer atomic.AddInt64(&s.activeRequests, -1)
err := s.h3server.SetQuicHeaders(w.Header()) if r.ProtoMajor < 3 {
if err != nil { err := s.h3server.SetQuicHeaders(w.Header())
s.logger.Error("setting HTTP/3 Alt-Svc header", zap.Error(err)) if err != nil {
s.logger.Error("setting HTTP/3 Alt-Svc header", zap.Error(err))
}
} }
} }
@@ -239,7 +242,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
repl.Set("http.response.status", wrec.Status()) 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.size", wrec.Size())
repl.Set("http.response.duration", duration) repl.Set("http.response.duration", duration)
repl.Set("http.response.duration_ms", duration.Seconds()*1e3) // multiply seconds to preserve decimal (see #4666) repl.Set("http.response.duration_ms", duration.Seconds()*1e3) // multiply seconds to preserve decimal (see #4666)
@@ -493,8 +496,25 @@ func (s *Server) findLastRouteWithHostMatcher() int {
// serveHTTP3 creates a QUIC listener, configures an HTTP/3 server if // serveHTTP3 creates a QUIC listener, configures an HTTP/3 server if
// not already done, and then uses that server to serve HTTP/3 over // not already done, and then uses that server to serve HTTP/3 over
// the listener, with Server s as the handler. // the listener, with Server s as the handler.
func (s *Server) serveHTTP3(hostport string, tlsCfg *tls.Config) error { func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error {
h3ln, err := caddy.ListenQUIC(hostport, tlsCfg, &s.activeRequests) 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 { if err != nil {
return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err) return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err)
} }
@@ -512,6 +532,8 @@ func (s *Server) serveHTTP3(hostport string, tlsCfg *tls.Config) error {
} }
} }
s.h3listeners = append(s.h3listeners, lnAny.(net.PacketConn))
//nolint:errcheck //nolint:errcheck
go s.h3server.ServeListener(h3ln) go s.h3server.ServeListener(h3ln)
@@ -615,21 +637,18 @@ func (s *Server) shouldLogRequest(r *http.Request) bool {
// logging is disabled // logging is disabled
return false 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 { for _, dh := range s.Logs.SkipHosts {
// logging for this particular host is disabled // logging for this particular host is disabled
if certmagic.MatchWildcard(r.Host, dh) { if certmagic.MatchWildcard(r.Host, dh) {
return false return false
} }
} }
if _, ok := s.Logs.LoggerNames[r.Host]; ok { // if configured, this host is not mapped and thus must not be logged
// this host is mapped to a particular logger name return !s.Logs.SkipUnmappedHosts
return true
}
if s.Logs.SkipUnmappedHosts {
// this host is not mapped and thus must not be logged
return false
}
return true
} }
// protocol returns true if the protocol proto is configured/enabled. // protocol returns true if the protocol proto is configured/enabled.
@@ -650,6 +669,9 @@ func (s *Server) protocol(proto string) bool {
// EXPERIMENTAL: Subject to change or removal. // EXPERIMENTAL: Subject to change or removal.
func (s *Server) Listeners() []net.Listener { return s.listeners } func (s *Server) Listeners() []net.Listener { return s.listeners }
// Name returns the server's name.
func (s *Server) Name() string { return s.name }
// PrepareRequest fills the request r for use in a Caddy HTTP handler chain. w and s can // 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. // 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 { func PrepareRequest(r *http.Request, repl *caddy.Replacer, w http.ResponseWriter, s *Server) *http.Request {
+41 -20
View File
@@ -45,15 +45,21 @@ func (ConsoleEncoder) CaddyModule() caddy.ModuleInfo {
// Provision sets up the encoder. // Provision sets up the encoder.
func (ce *ConsoleEncoder) Provision(_ caddy.Context) error { 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()) ce.Encoder = zapcore.NewConsoleEncoder(ce.ZapcoreEncoderConfig())
return nil return nil
} }
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: // UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
// //
// console { // console {
// <common encoder config subdirectives...> // <common encoder config subdirectives...>
// } // }
// //
// See the godoc on the LogEncoderConfig type for the syntax of // See the godoc on the LogEncoderConfig type for the syntax of
// subdirectives that are common to most/all encoders. // 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: // UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
// //
// json { // json {
// <common encoder config subdirectives...> // <common encoder config subdirectives...>
// } // }
// //
// See the godoc on the LogEncoderConfig type for the syntax of // See the godoc on the LogEncoderConfig type for the syntax of
// subdirectives that are common to most/all encoders. // subdirectives that are common to most/all encoders.
@@ -121,28 +127,37 @@ type LogEncoderConfig struct {
StacktraceKey *string `json:"stacktrace_key,omitempty"` StacktraceKey *string `json:"stacktrace_key,omitempty"`
LineEnding *string `json:"line_ending,omitempty"` LineEnding *string `json:"line_ending,omitempty"`
TimeFormat string `json:"time_format,omitempty"` TimeFormat string `json:"time_format,omitempty"`
TimeLocal bool `json:"time_local,omitempty"`
DurationFormat string `json:"duration_format,omitempty"` DurationFormat string `json:"duration_format,omitempty"`
LevelFormat string `json:"level_format,omitempty"` LevelFormat string `json:"level_format,omitempty"`
} }
// UnmarshalCaddyfile populates the struct from Caddyfile tokens. Syntax: // UnmarshalCaddyfile populates the struct from Caddyfile tokens. Syntax:
// //
// { // {
// message_key <key> // message_key <key>
// level_key <key> // level_key <key>
// time_key <key> // time_key <key>
// name_key <key> // name_key <key>
// caller_key <key> // caller_key <key>
// stacktrace_key <key> // stacktrace_key <key>
// line_ending <char> // line_ending <char>
// time_format <format> // time_format <format>
// duration_format <format> // time_local
// level_format <format> // duration_format <format>
// } // level_format <format>
// // }
func (lec *LogEncoderConfig) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { func (lec *LogEncoderConfig) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for nesting := d.Nesting(); d.NextBlock(nesting); { for nesting := d.Nesting(); d.NextBlock(nesting); {
subdir := d.Val() subdir := d.Val()
switch subdir {
case "time_local":
lec.TimeLocal = true
if d.NextArg() {
return d.ArgErr()
}
continue
}
var arg string var arg string
if !d.AllArgs(&arg) { if !d.AllArgs(&arg) {
return d.ArgErr() return d.ArgErr()
@@ -232,7 +247,13 @@ func (lec *LogEncoderConfig) ZapcoreEncoderConfig() zapcore.EncoderConfig {
timeFormat = "02/Jan/2006:15:04:05 -0700" timeFormat = "02/Jan/2006:15:04:05 -0700"
} }
timeFormatter = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) { 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 cfg.EncodeTime = timeFormatter
+61 -22
View File
@@ -23,6 +23,7 @@ import (
"net/url" "net/url"
"regexp" "regexp"
"strconv" "strconv"
"strings"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -76,7 +77,10 @@ func hash(s string) string {
} }
// HashFilter is a Caddy log field filter that // 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 { type HashFilter struct {
} }
@@ -95,7 +99,13 @@ func (f *HashFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Filter filters the input field with the replacement value. // Filter filters the input field with the replacement value.
func (f *HashFilter) Filter(in zapcore.Field) zapcore.Field { 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 return in
} }
@@ -131,7 +141,10 @@ func (f *ReplaceFilter) Filter(in zapcore.Field) zapcore.Field {
} }
// IPMaskFilter is a Caddy log field filter that // 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 { type IPMaskFilter struct {
// The IPv4 mask, as an subnet size CIDR. // The IPv4 mask, as an subnet size CIDR.
IPv4MaskRaw int `json:"ipv4_cidr,omitempty"` IPv4MaskRaw int `json:"ipv4_cidr,omitempty"`
@@ -205,27 +218,45 @@ func (m *IPMaskFilter) Provision(ctx caddy.Context) error {
// Filter filters the input field. // Filter filters the input field.
func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field { func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
host, port, err := net.SplitHostPort(in.String) if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
if err != nil { for i, s := range array {
host = in.String // assume whole thing was IP address array[i] = m.mask(s)
} }
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()
} else { } else {
in.String = net.JoinHostPort(masked.String(), port) in.String = m.mask(in.String)
} }
return in 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 type filterAction string
const ( const (
@@ -499,7 +530,10 @@ OUTER:
} }
// RegexpFilter is a Caddy log field filter that // 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 { type RegexpFilter struct {
// The regular expression pattern defining what to replace. // The regular expression pattern defining what to replace.
RawRegexp string `json:"regexp,omitempty"` 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. // Filter filters the input field with the replacement value if it matches the regexp.
func (f *RegexpFilter) Filter(in zapcore.Field) zapcore.Field { 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 return in
} }
@@ -576,7 +616,6 @@ func (f *RenameFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Filter renames the input field with the replacement name. // Filter renames the input field with the replacement name.
func (f *RenameFilter) Filter(in zapcore.Field) zapcore.Field { func (f *RenameFilter) Filter(in zapcore.Field) zapcore.Field {
in.Type = zapcore.StringType
in.Key = f.Name in.Key = f.Name
return in return in
} }
+110 -2
View File
@@ -8,6 +8,81 @@ import (
"go.uber.org/zap/zapcore" "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) { func TestQueryFilter(t *testing.T) {
f := QueryFilter{[]queryFilterAction{ f := QueryFilter{[]queryFilterAction{
{replaceAction, "foo", "REDACTED"}, {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 := RegexpFilter{RawRegexp: `secret`, Value: "REDACTED"}
f.Provision(caddy.Context{}) 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{} f := HashFilter{}
out := f.Filter(zapcore.Field{String: "foo"}) 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) 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
View File
@@ -308,6 +308,10 @@ func globalDefaultReplacements(key string) (any, bool) {
return string(filepath.Separator), true return string(filepath.Separator), true
case "system.os": case "system.os":
return runtime.GOOS, true 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": case "system.arch":
return runtime.GOARCH, true return runtime.GOARCH, true
case "time.now": case "time.now":
+5
View File
@@ -372,6 +372,7 @@ func TestReplacerNew(t *testing.T) {
} else { } else {
// test if default global replacements are added as the first provider // test if default global replacements are added as the first provider
hostname, _ := os.Hostname() hostname, _ := os.Hostname()
wd, _ := os.Getwd()
os.Setenv("CADDY_REPLACER_TEST", "envtest") os.Setenv("CADDY_REPLACER_TEST", "envtest")
defer os.Setenv("CADDY_REPLACER_TEST", "") defer os.Setenv("CADDY_REPLACER_TEST", "")
@@ -395,6 +396,10 @@ func TestReplacerNew(t *testing.T) {
variable: "system.arch", variable: "system.arch",
value: runtime.GOARCH, value: runtime.GOARCH,
}, },
{
variable: "system.wd",
value: wd,
},
{ {
variable: "env.CADDY_REPLACER_TEST", variable: "env.CADDY_REPLACER_TEST",
value: "envtest", value: "envtest",
+12 -1
View File
@@ -15,15 +15,26 @@
package caddy package caddy
import ( import (
"os"
"path/filepath"
"github.com/caddyserver/caddy/v2/notify" "github.com/caddyserver/caddy/v2/notify"
"golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc"
) )
func init() { func init() {
isService, err := svc.IsWindowsService() isService, err := svc.IsWindowsService()
if err != nil || isService { if err != nil || !isService {
return 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() { go func() {
_ = svc.Run("", runner{}) _ = svc.Run("", runner{})
}() }()