Compare commits

...

213 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
Francis Lavoie 821a08a6e3 httpcaddyfile: Fix protocols global option parsing (#5054)
* httpcaddyfile: Fix `protocols` global option parsing

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

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

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

This avoids blocking during config reloads.

* Don't quit process until servers shut down

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

* caddyhttp: Even faster shutdowns

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

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

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

* Fix comment typo

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

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

* Minor tweaks

* Run gofmt

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

* Resolve merge conflict with master

* Add Caddyfile support; various fixes

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

* Appease linter

* Minor tweaks

* Add doc comment

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

Related to #4644

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

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

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

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

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

* Refactor to use vars middleware

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

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

* Refactor solution; simplify, add descriptive comment

* Move network to host, not copy

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2022-09-15 08:03:24 -06:00
Matthew Holt 62b0685375 cmd: Improve error message if config missing 2022-09-14 23:24:16 -06:00
Matthew Holt 0b3161aeea cmd: Customizable user agent (close #2795) 2022-09-13 17:21:04 -06:00
Matthew Holt 754fe4f7b4 httpcaddyfile: Fix sorting of repeated directives
Fixes #5037
2022-09-13 13:43:21 -06:00
Matthew Holt 20d487be57 caddyhttp: Very minor optimization to path matcher
If * is in the matcher it will always match so we can just put it first.
2022-09-13 11:26:10 -06:00
Francis Lavoie 61c75f74de caddyhttp: Explicitly disallow multiple regexp matchers (#5030)
* caddyhttp: Explicitly disallow multiple regexp matchers

Fix #5028

Since the matchers would overwrite eachother, we should error out to tell the user their config doesn't make sense.

* Update modules/caddyhttp/matchers.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-09-13 11:18:37 -06:00
Matthew Holt d35f618b10 caddytls: Error if placeholder is empty in 'ask'
Fixes #5036
2022-09-13 08:59:03 -06:00
Mohammed Al Sahaf 9fe4f93bc7 supplychain: publish signing cert, sbom, and signatures of sbom (#5027) 2022-09-12 22:59:53 +00:00
Matthew Holt c5df7bb6bd go.mod: Update truststore 2022-09-10 21:44:35 -06:00
Matthew Holt 076a8b8095 Very minor tweaks 2022-09-08 13:10:40 -06:00
Matthew Holt 50748e19c3 core: Check error on ListenQUIC 2022-09-08 12:36:31 -06:00
Matthew Holt c19f207237 fileserver: Ignore EOF when browsing empty dir
Thanks to @WeidiDeng for reporting this
2022-09-07 21:14:11 -06:00
fleandro dd9813c65b caddyhttp: ensure ResponseWriterWrapper and ResponseRecorder use ReadFrom if the underlying response writer implements it. (#5022)
Doing so allows for splice/sendfile optimizations when available.
Fixes #4731

Co-authored-by: flga <flga@users.noreply.github.com>
Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2022-09-07 21:13:35 +01:00
Matthew Holt 1c9c8f6a13 cmd: Enhance some help text 2022-09-06 14:19:58 -06:00
Francis Lavoie 8cc8f9fddd httpcaddyfile: Add a couple more placeholder shortcuts (#5015)
This adds:
- `{file.*}` -> `{http.request.uri.path.file.*}`
- `{file_match.*}` -> `{http.matchers.file.*}`

This is a follow-up to #4993 which introduces the new URI file placeholders, and a shortcut for using `file` matcher output.

For example, where the `try_files` directive is a shortcut for this:

```
@try_files file <files...>
rewrite @try_files {http.matchers.file.relative}
```

It could instead be:
```
@try_files file <files...>
rewrite @try_files {file_match.relative}
```
2022-09-05 21:41:48 -06:00
Dave Henderson 8f6a88e2b0 Merge pull request #5018 from hairyhenderson/allow-fs.FS-for-virtual-filesystems
Drop requirement for filesystems to implement fs.StatFS
2022-09-05 20:10:48 -04:00
Dave Henderson fded2644f8 Drop requirement for filesystems to implement fs.StatFS
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
2022-09-05 19:25:34 -04:00
Mohammed Al Sahaf 487217519c ci: grant the release workflow the write permission to contents (#5017) 2022-09-05 21:35:47 +00:00
Mohammed Al Sahaf 0499d9c1c4 ci: add id-token permission and update the signing command (#5016) 2022-09-05 20:57:27 +00:00
Matthew Holt 5dfa08174a go.mod: Upgrade CertMagic (v0.17.1) 2022-09-05 13:55:48 -06:00
Matt Holt d5ea43fb4b fileserver: Support glob expansion in file matcher (#4993)
* fileserver: Support glob expansion in file matcher

* Fix tests

* Fix bugs and tests

* Attempt Windows fix, sigh

* debug Windows, WIP

* Continue debugging Windows

* Another attempt at Windows

* Plz Windows

* Cmon...

* Clean up, hope I didn't break anything
2022-09-05 13:53:41 -06:00
Matt Holt ca4fae64d9 caddyhttp: Support respond with HTTP 103 Early Hints (#5006)
* caddyhttp: Support sending HTTP 103 Early Hints

This adds support for early hints in the static_response handler.

* caddyhttp: Don't record 1xx responses
2022-09-05 13:50:44 -06:00
Matthew Holt ad69503aef Remove unnecessary error check 2022-09-05 13:42:59 -06:00
Francis Lavoie 6e3063b15a caddyauth: Speed up basicauth provision, deprecate scrypt (#4720)
* caddyauth: Speed up basicauth provisioning, precalculate fake password

* Deprecate scrypt, allow using decoded bcrypt hashes

* Add TODO note

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

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-09-05 13:32:58 -06:00
Mohammed Al Sahaf d6b3c7d262 ci: generate SBOM and sign artifacts using cosign (#4910)
* ci: sign artifacts using cosign

* include SBOM
2022-09-03 03:37:10 +03:00
Matt Holt 66476d8c8f reverseproxy: Close hijacked conns on reload/quit (#4895)
* reverseproxy: Close hijacked conns on reload/quit

We also send a Close control message to both ends of
WebSocket connections. I have tested this many times in
my dev environment with consistent success, although
the variety of scenarios was limited.

* Oops... actually call Close() this time

* CloseMessage --> closeMessage

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Use httpguts, duh

* Use map instead of sync.Map

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2022-09-02 17:01:55 -06:00
Matt Holt d3c3fa10bd core: Refactor listeners; use SO_REUSEPORT on Unix (#4705)
* core: Refactor listeners; use SO_REUSEPORT on Unix

Just an experiment for now

* Fix lint by logging error

* TCP Keepalive configuration (#4865)

* initial attempt at TCP Keepalive configuration

* core: implement tcp-keepalive for linux

* move canSetKeepAlive interface

* Godoc for keepalive server parameter

* handle return values

* log keepalive errors

* Clean up after bad merge

* Merge in pluggable network types

From 1edc1a45e3

* Slight refactor, fix from recent merge conflict

Co-authored-by: Karmanyaah Malhotra <karmanyaah.gh@malhotra.cc>
2022-09-02 16:59:11 -06:00
WeidiDeng 83b26975bd fastcgi: Optimize FastCGI transport (#4978)
* break up code and use lazy reading and pool bufio.Writer

* close underlying connection when operation failed

* allocate bufWriter and streamWriter only once

* refactor record writing

* rebase from master

* handle err

* Fix type assertion

Also reduce some duplication

* Refactor client and clientCloser for logging

Should reduce allocations

* Minor cosmetic adjustments; apply Apache license

* Appease the linter

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2022-09-02 16:57:55 -06:00
Matthew Holt 005c5a6382 Minor style adjustments for HTTP redir logging 2022-09-02 13:04:31 -06:00
Matthew Holt 6c0d0511ba Update readme 2022-09-02 10:26:31 -06:00
Matthew Holt 5c7ae5e505 Minor fix of error log 2022-09-02 10:19:51 -06:00
Matthew Holt 59286d2c7e notify: Don't send ready after error (fix #5003)
Also simplify the notify package quite a bit.
Also move stop notification into better place.
Add ability to send status or error.
2022-09-02 09:24:05 -06:00
Avdhut 66959d9f18 templates: Document httpError function (#4972)
* added the httpError function into the document

* Update templates.go

* Update templates.go

* Fix gofmt

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-09-01 22:07:52 -06:00
fleandro f2a7e7c966 fastcgi: allow users to log stderr output (#4967) (#5004)
Co-authored-by: flga <flga@users.noreply.github.com>
2022-09-02 00:02:48 -04:00
Matthew Holt ec2a5762b0 cmd: Don't print long help text on error 2022-09-01 21:43:23 -06:00
Matthew Holt e77992dd99 Fix failing test 2022-09-01 21:43:23 -06:00
Mohammed Al Sahaf aefd821ae0 dist: deb package manpages and bash completion scripts (#5007) 2022-09-01 23:39:18 -04:00
Francis Lavoie d062fb4020 caddyhttp: Copy logger config to HTTP server during AutoHTTPS (#4990) 2022-09-01 23:31:54 -04:00
Matthew Holt 73d4a8ba02 map: Coerce val to string, fix #4987
Also prevent infinite recursion, and enforce placeholder syntax.
2022-09-01 21:15:44 -06:00
Francis Lavoie 7d5108d132 httpcaddyfile: Add shortcut for expression matchers (#4976) 2022-09-01 23:12:37 -04:00
Matthew Holt 7c35bfa57c caddyhttp: Accept placeholders in vars matcher key
Until now, the vars matcher has unintentionally lacked parity with the
map directive: the destination placeholders of the map directive would
be expressed as placeholders, i.e. {foo}. But the vars matcher would
not use { }: vars foo value

This looked weird, and was confusing, since it implied that the key
could be dynamic, which doesn't seem helpful here.

I think this is a proper bug fix, since we're not used to accessing
placeholders literally without { } in the Caddyfile.
2022-09-01 16:49:18 -06:00
Matt Holt 1edc1a45e3 core: Plugins can register listener networks (#5002)
* core: Plugins can register listener networks

This can be useful for custom listeners.

This feature/API is experimental and may change!

* caddyhttp: Expose server listeners
2022-09-01 16:30:03 -06:00
Matthew Holt cb849bd664 caddyhttp: Disable draft versions of QUIC
See comment in #4996
2022-08-31 18:49:34 -06:00
Matthew Holt 3cd7437b3d events: Tune logging and context cancellation 2022-08-31 18:48:46 -06:00
Francis Lavoie d4d8bbcfc6 events: Implement event system (#4912)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-08-31 15:01:30 -06:00
Francis Lavoie 68d8ac9802 httpcaddyfile: Add {cookie.*} placeholder shortcut (#5001) 2022-08-31 10:18:29 -06:00
Matt Holt 2d5a30b908 caddyhttp: Set Content-Type for static response (#4999) 2022-08-31 09:43:46 -06:00
Matthew Holt 687a4b9e81 cmd: Enhance CLI docs 2022-08-30 19:15:52 -06:00
Mohammed Al Sahaf d605ebe75a cmd: add completion command (#4994)
* cmd: add completion command

* error check
2022-08-30 23:24:05 +00:00
Mohammed Al Sahaf 258bc82b69 cmd: Migrate to spf13/cobra, remove single-dash arg support (#4565)
* cmd: migrate to spf13/cobra

* add `manpage` command

* limit Caddy tagline to root `help` only

* hard-code the manpage section to 8
2022-08-30 22:38:38 +00:00
Matthew Holt 8cb3cf540c Minor cleanup, resolve a couple lint warnings 2022-08-29 12:31:56 -06:00
Abirdcfly e1801fdb19 Remove duplicate words in comments (#4986) 2022-08-27 14:39:26 -06:00
Dávid Szabó 0c57facc67 reverseproxy: Add upstreams healthy metrics (#4935) 2022-08-27 12:30:23 -06:00
WeidiDeng 4c282e86da admin: Don't stop old server if new one fails (#4964)
Fixes #4954

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2022-08-25 22:17:52 -06:00
Matthew Holt 5fb5b81439 reverseproxy: Multiple dynamic upstreams
This allows users to, for example, get upstreams from multiple SRV
endpoints in order (such as primary and secondary clusters).

Also, gofmt went to town on the comments, sigh
2022-08-25 21:42:48 -06:00
Matthew Holt 2cc5d38229 Fix comment indentation 2022-08-25 13:28:58 -06:00
Simon Legner 66596f2d74 zstd: fix typo in comment (#4985) 2022-08-25 12:00:05 +03:00
Ben Burkert b540f195b1 httpcaddyfile: Add ocsp_interval global option (#4980) 2022-08-24 11:22:56 -06:00
Matthew Holt 3aabbc49a2 caddytls: Log error if ask request fails
Errors returned from the DecisionFunc (whether to get a cert on-demand)
are used as a signal whether to allow a cert or not; *any* error
will forbid cert issuance.

We bubble up the error all the way to the caller, but that caller is the
Go standard library which might gobble it up.
Now we explicitly log connection errors so sysadmins can
ensure their ask endpoints are working.

Thanks to our sponsor AppCove for reporting this!
2022-08-23 22:28:15 -06:00
Matt Holt bbc923d66b ci: Increase linter timeout (#4981) 2022-08-23 14:26:19 -06:00
jedy e289ba6187 templates: cap of slice should not be smaller than length (#4975) 2022-08-23 08:26:02 -06:00
Francis Lavoie a22c08a638 caddyhttp: Fix for nil handlerErr.Err (#4977) 2022-08-23 08:17:46 -06:00
Francis Lavoie 72541f1cb8 caddyhttp: Set http.error.message to the HandlerError message (#4971) 2022-08-22 23:31:07 -06:00
Matthew Holt fe5f5dfd6a go.mod: Upgrade CertMagic to v0.16.3 2022-08-18 10:56:27 -06:00
WilczyńskiT c7772588bd core: Change net.IP to netip.Addr; use netip.Prefix (#4966)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-08-17 16:10:57 -06:00
Matthew Holt a944de4ab7 Clean up metrics test code
No need to use != for booleans
2022-08-16 10:03:19 -06:00
Matt Holt a479943acd caddyhttp: Smarter path matching and rewriting (#4948)
Co-authored-by: RussellLuo <luopeng.he@gmail.com>
2022-08-16 08:48:57 -06:00
Abdussamet Koçak dc62d468e9 fileserver: reset buffer before using it (#4962) (#4963) 2022-08-15 22:31:45 -06:00
Matt Holt c79c08627d caddyhttp: Enable HTTP/3 by default (#4707) 2022-08-15 12:01:58 -06:00
Francis Lavoie e2a5e2293a reverseproxy: Add unix+h2c Caddyfile network shortcut (#4953) 2022-08-12 17:09:18 -04:00
Matt Holt f5dce84a70 reverseproxy: Ignore context cancel in stream mode (#4952) 2022-08-12 13:15:41 -06:00
Francis Lavoie 922d9f5c25 reverseproxy: Fix H2C dialer using new stdlib DialTLSContext (#4951) 2022-08-12 13:11:13 -06:00
Matthew Holt 91ab0e6066 httpcaddyfile: redir with "html" emits 200, no Location (fix #4940)
The intent of "html" is to redirect browser clients only, or those which can evaluate JS and/or meta tags. So return HTTP 200 and no Location header. See #4940.
2022-08-09 11:12:09 -06:00
Kévin Dunglas 085df25c7e reverseproxy: Support 1xx status codes (HTTP early hints) (#4882) 2022-08-09 10:53:24 -06:00
Francis Lavoie fe61209df2 logging: Fix cookie filter (#4943) 2022-08-08 19:11:02 -06:00
lewandowski-stripe 7f6a328b47 go.mod: Upgrade OpenTelemetry dependencies (#4937) 2022-08-08 15:04:18 -06:00
Matthew Holt 7ab61f46f0 fileserver: Better fix for Etag of compressed files 2022-08-08 13:09:57 -06:00
Matthew Holt 8c72f34357 fileserver: Generate Etag from sidecar file
Don't use the primary/uncompressed file for Etag when serving sidecars.

This was just overlooked initially.
2022-08-08 12:50:06 -06:00
Matthew Holt b9618b8b98 Improve docs for ZeroSSL issuer 2022-08-08 12:50:06 -06:00
Chirag Maheshwari d26559316f Replace strings.Index with strings.Cut (#4932) 2022-08-06 22:03:37 -06:00
WilczyńskiT 2642bd72b7 Replace strings.Index usages with strings.Cut (#4930) 2022-08-04 11:17:35 -06:00
Matt Holt 17ae5acaba cmd: Use newly-available version information (#4931) 2022-08-04 11:16:59 -06:00
Matt Holt 1960a0dc11 httpserver: Configurable shutdown delay (#4906) 2022-08-03 11:04:51 -06:00
Matthew Holt 63c7720e84 go.mod: Upgrade CertMagic and acmez 2022-08-02 15:35:19 -06:00
Francis Lavoie 141872ed80 chore: Bump up to Go 1.19, minimum 1.18 (#4925) 2022-08-02 16:39:09 -04:00
Matthew Holt db1aa5b5bc Oops (sigh)
Forgot to remove this redundant line
2022-08-01 13:40:09 -06:00
Matt Holt f783290f40 caddyhttp: Implement caddy respond command (#4870) 2022-08-01 13:36:22 -06:00
Matthew Holt ebd6abcbd5 fileserver: Support virtual file system in Caddyfile 2022-07-31 21:41:26 -06:00
Matt Holt 6668271661 fileserver: Support virtual file systems (#4909)
* fileserver: Support virtual file systems (close #3720)

This change replaces the hard-coded use of os.Open() and os.Stat() with
the use of the new (Go 1.16) io/fs APIs, enabling virtual file systems.
It introduces a new module namespace, caddy.fs, for such file systems.

Also improve documentation for the file server. I realized it was one of
the first modules written for Caddy 2, and the docs hadn't really been
updated since!

* Virtualize FS for file matcher; minor tweaks

* Fix tests and rename dirFS -> osFS

(Since we do not use a root directory, it is dynamic.)
2022-07-30 13:07:44 -06:00
Matthew Holt 07ed3e7c30 Minor docs clarification
Related to #4565
2022-07-29 16:56:02 -06:00
WingLim 1e0cdc54f8 core: Windows service integration (#4790)
Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2022-07-29 14:06:54 -06:00
Francis Lavoie 2f43aa0629 chore: Add .gitattributes to force *.go to LF (#4919)
* chore: Add .gitattributes to force *.go to LF

* What if I remove this flag
2022-07-29 08:46:45 -04:00
Matthew Holt 56c139f003 Fix compilation on Windows 2022-07-28 15:44:36 -06:00
Matthew Holt 35a81d7c5b Ignore linter warnings
Use of non-cryptographic random numbers in the load balancing
is intentional.
2022-07-28 15:40:23 -06:00
Matthew Holt 2e70d1d3bf Fix deprecation notice by using UTF16PtrFromString 2022-07-28 15:24:08 -06:00
Francis Lavoie ff2ba6de8a caddyhttp: Clear out matcher error immediately after grabbing it (#4916)
Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2022-07-28 15:19:48 -06:00
Matthew Holt 4fced0b6e1 Finish fixing lint errors from ea8df6ff
Follows up #4915
2022-07-28 15:16:36 -06:00
Matthew Holt 1bdd451913 caddytls: Remove PreferServerCipherSuites
It has been deprecated by Go
2022-07-28 14:50:51 -06:00
Matthew Holt ea8df6ff11 caddyhttp: Use new CEL APIs (fix #4915)
Hahaha this is the ultimate "I have no idea what I'm doing" commit but it
compiles and the tests pass and I declare victory!

... probably broke something, should be tested more.

It is nice that the protobuf dependency becomes indirect now.
2022-07-28 14:50:28 -06:00
Y.Horie c833e3b249 ci: Run golangci-lint on multiple os(#4875) (#4913) 2022-07-27 09:27:18 -04:00
Matthew Holt 7991cd1250 go.mod: Upgrade dependencies 2022-07-26 11:07:20 -06:00
Matthew Holt 1e18afb5c8 httpcaddyfile: Detect ambiguous site definitions (fix #4635)
Previously, our "duplicate key in server block" logic was flawed because
it did not account for the site's bind address. We defer this check to
when the listener addresses have been assigned, but before we commit
a server block to its listener.

Also refined how network address parsing and joining works, which was
necessary for a less convoluted fix.
2022-07-25 17:28:20 -06:00
Matthew Holt 0bebea0d4c caddyhttp: Log shutdown errors, don't return (fix #4908) 2022-07-25 10:39:59 -06:00
Matt Holt a379fa4c6c reverseproxy: Implement read & write timeouts for HTTP transport (#4905) 2022-07-23 22:38:41 -06:00
Francis Lavoie abad9bc256 cmd: Fix reload with stdin (#4900) 2022-07-20 18:14:33 -06:00
Matthew Holt 8bdee04651 caddyhttp: Enhance comment 2022-07-16 23:33:49 -06:00
Francis Lavoie 7d1f7771c9 reverseproxy: Implement retry count, alternative to try_duration (#4756)
* reverseproxy: Implement retry count, alternative to try_duration

* Add Caddyfile support for `retry_match`

* Refactor to deduplicate matcher parsing logic

* Fix lint
2022-07-13 14:15:00 -06:00
Matthew Holt 04a14ee37a caddyhttp: Make query matcher more efficient
Only parse query string once
2022-07-13 12:20:00 -06:00
Matthew Holt c2bbe42fc3 reverseproxy: Export SetScheme() again
Turns out the NTLM transport uses it. Oops.
2022-07-13 08:52:30 -06:00
jhwz ad3a83fb91 admin: expect quoted ETags (#4879)
* expect quoted etags

* admin: Minor refactor of etag facilities

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2022-07-12 12:23:55 -06:00
Francis Lavoie 53c4d788d4 headers: Only replace known placeholders (#4880) 2022-07-12 12:16:03 -06:00
Matthew Holt d6bc9e0b5c reverseproxy: Err 503 if all upstreams unavailable 2022-07-08 13:01:32 -06:00
Francis Lavoie 54d1923ccb reverseproxy: Adjust new TLS Caddyfile directive names (#4872) 2022-07-08 13:04:22 -04:00
Matthew Holt c0f76e9ed4 fileserver: Use safe redirects in file browser 2022-07-07 14:10:19 -06:00
jhwz f259ed52bb admin: support ETag on config endpoints (#4579)
* admin: support ETags

* support etags

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-07-06 13:50:07 -06:00
Francis Lavoie 8bac134f26 go.mod: Bump up quic-go to v0.28.0, fixes for BC breaks (#4867) 2022-07-06 12:14:32 -06:00
Matt Holt 412dcc07d3 caddytls: Reuse issuer between PreCheck and Issue (#4866)
This enables EAB reuse for ZeroSSLIssuer (which is now supported by ZeroSSL).
2022-07-05 18:12:25 -06:00
Matt Holt 660c59b6f3 admin: Implement /adapt endpoint (close #4465) (#4846) 2022-06-29 00:43:57 -04:00
Francis Lavoie 58e05cab15 forwardauth: Fix case when copy_headers is omitted (#4856)
See https://caddy.community/t/using-forward-auth-and-writing-my-own-authenticator-in-php/16410, apparently it didn't work when `copy_headers` wasn't used. This is because we were skipping adding a handler to the routes in the "good response handler", but this causes the logic in `reverseproxy.go` to ignore the response handler since it's empty. Instead, we can just always put in the `header` handler, even with an empty `Set` operation, it's just a no-op, but it fixes that condition in the proxy code.
2022-06-28 19:23:30 -06:00
Tristan Swadell 10f85558ea Expose several Caddy HTTP Matchers to the CEL Matcher (#4715)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2022-06-22 18:53:46 -04:00
Francis Lavoie 98468af8b6 reverseproxy: Fix double headers in response handlers (#4847) 2022-06-22 15:10:14 -04:00
Francis Lavoie 25f10511e7 reverseproxy: Fix panic when TLS is not configured (#4848)
* reverseproxy: Fix panic when TLS is not configured

* Refactor and simplify setScheme

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2022-06-22 15:01:57 -04:00
Kiss Károly Pál b6e96fa3c5 reverseproxy: Skip TLS for certain configured ports (#4843)
* Make reverse proxy TLS server name replaceable for SNI upstreams.

* Reverted previous TLS server name replacement, and implemented thread safe version.

* Move TLS servername replacement into it's own function

* Moved SNI servername replacement into httptransport.

* Solve issue when dynamic upstreams use wrong protocol upstream.

* Revert previous commit.

Old commit was: Solve issue when dynamic upstreams use wrong protocol upstream.
Id: 3c9806ccb6

* Added SkipTLSPorts option to http transport.

* Fix typo in test config file.

* Rename config option as suggested by Matt

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

* Update code to match renamed config option.

* Fix typo in config option name.

* Fix another typo that I missed.

* Tests not completing because of apparent wrong ordering of options.

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-06-20 11:51:42 -06:00
Matthew Holt 56013934a4 go.mod: Update some dependencies 2022-06-20 10:50:50 -06:00
Francis Lavoie 0b6f764356 forwardauth: Support renaming copied headers, block support (#4783) 2022-06-16 14:28:11 -06:00
Matthew Holt 050d6e0aeb Add comment about xcaddy to main 2022-06-15 15:20:59 -06:00
Matt Holt 0bcd02d5f6 headers: Support wildcards for delete ops (close #4830) (#4831) 2022-06-15 09:57:43 -06:00
Kiss Károly Pál c82fe91104 reverseproxy: Dynamic ServerName for TLS upstreams (#4836)
* Make reverse proxy TLS server name replaceable for SNI upstreams.

* Reverted previous TLS server name replacement, and implemented thread safe version.

* Move TLS servername replacement into it's own function

* Moved SNI servername replacement into httptransport.

* Solve issue when dynamic upstreams use wrong protocol upstream.

* Revert previous commit.

Old commit was: Solve issue when dynamic upstreams use wrong protocol upstream.
Id: 3c9806ccb6

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-06-14 21:53:05 -06:00
Matthew Holt f9b42c3772 reverseproxy: Make TLS renegotiation optional 2022-06-14 09:05:25 -06:00
Yaacov Akiba Slama aaf6794b31 reverseproxy: Add renegotiation param in TLS client (#4784)
* Add renegotiation option in reverseproxy tls client

* Update modules/caddyhttp/reverseproxy/httptransport.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-06-10 09:33:35 -06:00
Matthew Holt 1498132ea3 caddyhttp: Log error from CEL evaluation (fix #4832) 2022-06-08 16:42:24 -06:00
Francis Lavoie 7f9b1f43c9 reverseproxy: Correct the tls_server_name docs (#4827)
* reverseproxy: Correct the `tls_server_name` docs

* Update modules/caddyhttp/reverseproxy/httptransport.go

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

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-06-06 12:37:09 -06:00
Matt Holt 5e729c1e85 reverseproxy: HTTP 504 for upstream timeouts (#4824)
Closes #4823
2022-06-03 14:13:47 -06:00
Gr33nbl00d 0a14f97e49 caddytls: Make peer certificate verification pluggable (#4389)
* caddytls: Adding ClientCertValidator for custom client cert validations

* caddytls: Cleanups for ClientCertValidator changes

caddytls: Cleanups for ClientCertValidator changes

* Update modules/caddytls/connpolicy.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Update modules/caddytls/connpolicy.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Update modules/caddytls/connpolicy.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Update modules/caddytls/connpolicy.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Update modules/caddytls/connpolicy.go

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

* Update modules/caddytls/connpolicy.go

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

* Unexported field Validators, corrected renaming of LeafVerificationValidator to LeafCertClientAuth

* admin: Write proper status on invalid requests (#4569) (fix #4561)

* Apply suggestions from code review

* Register module; fix compilation

* Add log for deprecation notice

Co-authored-by: Roettges Florian <roettges.florian@scheidt-bachmann.de>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Alok Naushad <alokme123@gmail.com>
2022-06-02 14:25:07 -06:00
Matthew Holt 9864b138fb reverseproxy: api: Remove misleading 'healthy' value
In v2.5.0, upstream health was fixed such that whether an upstream is
considered healthy or not is mostly up to each individual handler's
config. Since "healthy" is an opinion, it is not a global value.

I unintentionally left in the "healthy" field in the API endpoint for
checking upstreams, and it is now misleading (see #4792).

However, num_requests and fails remains, so health can be determined by
the API client, rather than having it be opaquely (and unhelpfully)
determined for the client.

If we do restore this value later on, it'd need to be replicated once
per reverse_proxy handler according to their individual configs.
2022-06-02 12:32:23 -06:00
Matthew Holt 3d18bc56b9 go.mod: Update go-yaml to v3 2022-06-01 15:15:20 -06:00
Matthew Holt 886ba84baa Fix #4822 and fix #4779
The fix for 4822 is the change at the top of the file, and
4779's fix is toward the bottom of the file.
2022-06-01 15:12:57 -06:00
Alexander M a9267791c4 reverseproxy: Add --internal-certs CLI flag #3589 (#4817)
added flag --internal-certs
when set, for non-local domains the internal CA will be used for cert generation
2022-05-29 14:33:01 -06:00
Francis Lavoie ef0aaca0d6 ci: Fix build caching on Windows (#4811)
* ci: Fix build caching on Windows

I was getting tired of Windows being slow as molasses in our CI jobs, so I went to look at our trusty source of github actions + golang information, and found a somewhat recent commit that actually fixed it. See https://github.com/mvdan/github-actions-golang/commit/4b754729baa709da219a5889c459010d4eda1888

I'll do a 2nd empty commit to re-trigger CI shortly to confirm that it actually fixes it.

* Retrigger CI
2022-05-25 11:56:39 -06:00
Aleks 6891f7f421 templates: Add humanize function (#4767)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2022-05-24 19:47:08 -04:00
Kévin Dunglas 499ad6d182 core: Micro-optim in run() (#4810) 2022-05-24 13:52:50 -06:00
Matthew Holt 8e6bc36084 go.mod: Upgrade some dependencies 2022-05-24 12:44:16 -06:00
Francis Lavoie 58970cae92 httpcaddyfile: Add {err.*} placeholder shortcut (#4798) 2022-05-24 10:06:46 -06:00
David Larlet 9e760e2e0c templates: Documentation consistency (#4796) 2022-05-17 18:56:40 -04:00
世界 4b4e99bdb2 chore: Bump quic-go to v0.27.0 (#4782) 2022-05-12 01:25:17 -04:00
Matt Holt 57d27c1b58 reverseproxy: Support http1.1>h2c (close #4777) (#4778) 2022-05-10 17:25:58 -04:00
Matthew Holt 693e9b5283 rewrite: Handle fragment before query (fix #4775) 2022-05-09 11:09:42 -06:00
Francis Lavoie b687d7b967 httpcaddyfile: Support multiple values for default_bind (#4774)
* httpcaddyfile: Support multiple values for `default_bind`

* Fix ordering of server blocks
2022-05-08 21:32:10 -04:00
183 changed files with 10546 additions and 4308 deletions
+1
View File
@@ -0,0 +1 @@
*.go text eol=lf
+19 -10
View File
@@ -19,16 +19,16 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ] os: [ ubuntu-latest, macos-latest, windows-latest ]
go: [ '1.17', '1.18' ] go: [ '1.18', '1.19' ]
include: include:
# Set the minimum Go patch version for the given Go minor # Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }} # Usable via ${{ matrix.GO_SEMVER }}
- go: '1.17'
GO_SEMVER: '~1.17.9'
- go: '1.18' - go: '1.18'
GO_SEMVER: '~1.18.1' GO_SEMVER: '~1.18.4'
- go: '1.19'
GO_SEMVER: '~1.19.0'
# Set some variables per OS, usable via ${{ matrix.VAR }} # Set some variables per OS, usable via ${{ matrix.VAR }}
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing # CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
@@ -78,12 +78,20 @@ jobs:
printf "Git version: $(git version)\n\n" printf "Git version: $(git version)\n\n"
# Calculate the short SHA1 hash of the git commit # Calculate the short SHA1 hash of the git commit
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)" echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
echo "::set-output name=go_cache::$(go env GOCACHE)"
- name: Cache the build cache - name: Cache the build cache
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ${{ steps.vars.outputs.go_cache }} # In order:
# * Module download cache
# * Build cache (Linux)
# * Build cache (Mac)
# * Build cache (Windows)
path: |
~/go/pkg/mod
~/.cache/go-build
~/Library/Caches/go-build
~\AppData\Local\go-build
key: ${{ runner.os }}-${{ matrix.go }}-go-ci-${{ hashFiles('**/go.sum') }} key: ${{ runner.os }}-${{ matrix.go }}-go-ci-${{ hashFiles('**/go.sum') }}
restore-keys: | restore-keys: |
${{ runner.os }}-${{ matrix.go }}-go-ci ${{ runner.os }}-${{ matrix.go }}-go-ci
@@ -148,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
+9 -5
View File
@@ -16,13 +16,13 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd'] goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
go: [ '1.18' ] go: [ '1.19' ]
include: include:
# Set the minimum Go patch version for the given Go minor # Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }} # Usable via ${{ matrix.GO_SEMVER }}
- go: '1.18' - go: '1.19'
GO_SEMVER: '~1.18.1' GO_SEMVER: '~1.19.0'
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
@@ -42,12 +42,16 @@ jobs:
go env go env
printf "\n\nSystem environment:\n\n" printf "\n\nSystem environment:\n\n"
env env
echo "::set-output name=go_cache::$(go env GOCACHE)"
- name: Cache the build cache - name: Cache the build cache
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ${{ steps.vars.outputs.go_cache }} # In order:
# * Module download cache
# * Build cache (Linux)
path: |
~/go/pkg/mod
~/.cache/go-build
key: cross-build-go${{ matrix.go }}-${{ matrix.goos }}-${{ hashFiles('**/go.sum') }} key: cross-build-go${{ matrix.go }}-${{ matrix.goos }}-${{ hashFiles('**/go.sum') }}
restore-keys: | restore-keys: |
cross-build-go${{ matrix.go }}-${{ matrix.goos }} cross-build-go${{ matrix.go }}-${{ matrix.goos }}
+8 -3
View File
@@ -14,17 +14,22 @@ jobs:
# From https://github.com/golangci/golangci-lint-action # From https://github.com/golangci/golangci-lint-action
golangci: golangci:
name: lint name: lint
runs-on: ubuntu-latest strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
with: with:
go-version: '~1.17.9' go-version: '~1.18.4'
check-latest: true check-latest: true
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3
with: with:
version: v1.44 version: v1.47
# Windows times out frequently after about 5m50s if we don't set a longer timeout.
args: --timeout 10m
# Optional: show only new issues if it's a pull request. The default value is `false`. # Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true # only-new-issues: true
+26 -7
View File
@@ -11,15 +11,22 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ ubuntu-latest ] os: [ ubuntu-latest ]
go: [ '1.18' ] go: [ '1.19' ]
include: include:
# Set the minimum Go patch version for the given Go minor # Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }} # Usable via ${{ matrix.GO_SEMVER }}
- go: '1.18' - go: '1.19'
GO_SEMVER: '~1.18.1' GO_SEMVER: '~1.19.0'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
# https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings
permissions:
id-token: write
# https://docs.github.com/en/rest/overview/permissions-required-for-github-apps#permission-on-contents
# "Releases" is part of `contents`, so it needs the `write`
contents: write
steps: steps:
- name: Install Go - name: Install Go
@@ -56,7 +63,6 @@ jobs:
env env
echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\//}" echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\//}"
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)" echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
echo "::set-output name=go_cache::$(go env GOCACHE)"
# Add "pip install" CLI tools to PATH # Add "pip install" CLI tools to PATH
echo ~/.local/bin >> $GITHUB_PATH echo ~/.local/bin >> $GITHUB_PATH
@@ -91,20 +97,33 @@ jobs:
- name: Cache the build cache - name: Cache the build cache
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ${{ steps.vars.outputs.go_cache }} # In order:
# * Module download cache
# * Build cache (Linux)
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }} key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }}
restore-keys: | restore-keys: |
${{ runner.os }}-go${{ matrix.go }}-release ${{ runner.os }}-go${{ matrix.go }}-release
- name: Install Cosign
uses: sigstore/cosign-installer@main
- name: Cosign version
run: cosign version
- name: Install Syft
uses: anchore/sbom-action/download-syft@main
- name: Syft version
run: syft version
# GoReleaser will take care of publishing those artifacts into the release # GoReleaser will take care of publishing those artifacts into the release
- name: Run GoReleaser - name: Run GoReleaser
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 }}
COSIGN_EXPERIMENTAL: 1
# Only publish on non-special tags (e.g. non-beta) # Only publish on non-special tags (e.g. non-beta)
# We will continue to push to Gemfury for the foreseeable future, although # We will continue to push to Gemfury for the foreseeable future, although
+22 -2
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'
@@ -14,7 +15,11 @@ before:
# run `go mod tidy`. The `/bin/sh -c '...'` is because goreleaser can't find cd in PATH without shell invocation. # run `go mod tidy`. The `/bin/sh -c '...'` is because goreleaser can't find cd in PATH without shell invocation.
- /bin/sh -c 'cd ./caddy-build && go mod tidy' - /bin/sh -c 'cd ./caddy-build && go mod tidy'
- git clone --depth 1 https://github.com/caddyserver/dist caddy-dist - git clone --depth 1 https://github.com/caddyserver/dist caddy-dist
- mkdir -p caddy-dist/man
- go mod download - go mod download
- go run cmd/caddy/main.go manpage --directory ./caddy-dist/man
- gzip -r ./caddy-dist/man/
- /bin/sh -c 'go run cmd/caddy/main.go completion bash > ./caddy-dist/scripts/bash-completion'
builds: builds:
- env: - env:
@@ -58,9 +63,21 @@ builds:
goarm: "5" goarm: "5"
flags: flags:
- -trimpath - -trimpath
- -mod=readonly
ldflags: ldflags:
- -s -w - -s -w
signs:
- cmd: cosign
signature: "${artifact}.sig"
certificate: '{{ trimsuffix (trimsuffix .Env.artifact ".zip") ".tar.gz" }}.pem'
args: ["sign-blob", "--output-signature=${signature}", "--output-certificate", "${certificate}", "${artifact}"]
artifacts: all
sboms:
- artifacts: binary
documents:
- '{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{if .Arm}}v{{ .Arm }}{{end}}.sbom'
cmd: syft
args: ["$artifact", "--file", "${document}", "--output", "cyclonedx-json"]
archives: archives:
- format_overrides: - format_overrides:
- goos: windows - goos: windows
@@ -96,13 +113,16 @@ nfpms:
- src: ./caddy-dist/welcome/index.html - src: ./caddy-dist/welcome/index.html
dst: /usr/share/caddy/index.html dst: /usr/share/caddy/index.html
- src: ./caddy-dist/scripts/completions/bash-completion - src: ./caddy-dist/scripts/bash-completion
dst: /etc/bash_completion.d/caddy dst: /etc/bash_completion.d/caddy
- src: ./caddy-dist/config/Caddyfile - src: ./caddy-dist/config/Caddyfile
dst: /etc/caddy/Caddyfile dst: /etc/caddy/Caddyfile
type: config type: config
- src: ./caddy-dist/man/*
dst: /usr/share/man/man8/
scripts: scripts:
postinstall: ./caddy-dist/scripts/postinstall.sh postinstall: ./caddy-dist/scripts/postinstall.sh
preremove: ./caddy-dist/scripts/preremove.sh preremove: ./caddy-dist/scripts/preremove.sh
+8 -8
View File
@@ -57,25 +57,25 @@
- Multi-issuer fallback - Multi-issuer fallback
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues - **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
- **Production-ready** after serving trillions of requests and managing millions of TLS certificates - **Production-ready** after serving trillions of requests and managing millions of TLS certificates
- **Scales to tens of thousands of sites** ... and probably more - **Scales to hundreds of thousands of sites** as proven in production
- **HTTP/1.1, HTTP/2, and experimental HTTP/3** support - **HTTP/1.1, HTTP/2, and HTTP/3** supported all by default
- **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat - **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat
- **Runs anywhere** with **no external dependencies** (not even libc) - **Runs anywhere** with **no external dependencies** (not even libc)
- Written in Go, a language with higher **memory safety guarantees** than other servers - Written in Go, a language with higher **memory safety guarantees** than other servers
- Actually **fun to use** - Actually **fun to use**
- So, so much more to [discover](https://caddyserver.com/v2) - So much more to [discover](https://caddyserver.com/v2)
## Install ## Install
The simplest, cross-platform way is to download from [GitHub Releases](https://github.com/caddyserver/caddy/releases) and place the executable file in your PATH. The simplest, cross-platform way to get started is to download Caddy from [GitHub Releases](https://github.com/caddyserver/caddy/releases) and place the executable file in your PATH.
For other install options, see https://caddyserver.com/docs/install. See [our online documentation](https://caddyserver.com/docs/install) for other install instructions.
## Build from source ## Build from source
Requirements: Requirements:
- [Go 1.17 or newer](https://golang.org/dl/) - [Go 1.18 or newer](https://golang.org/dl/)
### For development ### For development
@@ -164,9 +164,9 @@ The docs are also open source. You can contribute to them here: https://github.c
## Getting help ## Getting help
- We **strongly recommend** that all professionals or companies using Caddy get a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed. - We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed.
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way! If Caddy is benefitting your company, please consider a sponsorship! This not only helps fund full-time work to ensure the longevity of the project, it's also a great look for your company to your customers and potential customers! - A [sponsorship](https://github.com/sponsors/mholt) goes a long way! We can offer private help to sponsors. If Caddy is benefitting your company, please consider a sponsorship. This not only helps fund full-time work to ensure the longevity of the project, it provides your company the resources, support, and discounts you need; along with being a great look for your company to your customers and potential customers!
- Individuals can exchange help for free on our community forum at https://caddy.community. Remember that people give help out of their spare time and good will. The best way to get help is to give it first! - Individuals can exchange help for free on our community forum at https://caddy.community. Remember that people give help out of their spare time and good will. The best way to get help is to give it first!
+53 -30
View File
@@ -25,6 +25,8 @@ import (
"errors" "errors"
"expvar" "expvar"
"fmt" "fmt"
"hash"
"hash/fnv"
"io" "io"
"net" "net"
"net/http" "net/http"
@@ -38,7 +40,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/caddyserver/caddy/v2/notify"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap" "go.uber.org/zap"
@@ -56,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
@@ -155,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"`
@@ -338,17 +339,19 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
// that there is always an admin server (unless it is explicitly // that there is always an admin server (unless it is explicitly
// configured to be disabled). // configured to be disabled).
func replaceLocalAdminServer(cfg *Config) error { func replaceLocalAdminServer(cfg *Config) error {
// always be sure to close down the old admin endpoint // always* be sure to close down the old admin endpoint
// as gracefully as possible, even if the new one is // as gracefully as possible, even if the new one is
// disabled -- careful to use reference to the current // disabled -- careful to use reference to the current
// (old) admin endpoint since it will be different // (old) admin endpoint since it will be different
// when the function returns // when the function returns
// (* except if the new one fails to start)
oldAdminServer := localAdminServer oldAdminServer := localAdminServer
var err error
defer func() { defer func() {
// do the shutdown asynchronously so that any // do the shutdown asynchronously so that any
// current API request gets a response; this // current API request gets a response; this
// goroutine may last a few seconds // goroutine may last a few seconds
if oldAdminServer != nil { if oldAdminServer != nil && err == nil {
go func(oldAdminServer *http.Server) { go func(oldAdminServer *http.Server) {
err := stopAdminServer(oldAdminServer) err := stopAdminServer(oldAdminServer)
if err != nil { if err != nil {
@@ -379,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
} }
@@ -400,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))
} }
}() }()
@@ -439,7 +442,7 @@ func manageIdentity(ctx Context, cfg *Config) error {
if err != nil { if err != nil {
return fmt.Errorf("loading identity issuer modules: %s", err) return fmt.Errorf("loading identity issuer modules: %s", err)
} }
for _, issVal := range val.([]interface{}) { for _, issVal := range val.([]any) {
cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer)) cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer))
} }
} }
@@ -546,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() {
@@ -894,16 +898,36 @@ func (h adminHandler) originAllowed(origin *url.URL) bool {
return false return false
} }
// etagHasher returns a the hasher we used on the config to both
// produce and verify ETags.
func etagHasher() hash.Hash32 { return fnv.New32a() }
// makeEtag returns an Etag header value (including quotes) for
// the given config path and hash of contents at that path.
func makeEtag(path string, hash hash.Hash) string {
return fmt.Sprintf(`"%s %x"`, path, hash.Sum(nil))
}
func handleConfig(w http.ResponseWriter, r *http.Request) error { func handleConfig(w http.ResponseWriter, r *http.Request) error {
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
// Set the ETag as a trailer header.
// The alternative is to write the config to a buffer, and
// then hash that.
w.Header().Set("Trailer", "ETag")
err := readConfig(r.URL.Path, w) hash := etagHasher()
configWriter := io.MultiWriter(w, hash)
err := readConfig(r.URL.Path, configWriter)
if err != nil { if err != nil {
return APIError{HTTPStatus: http.StatusBadRequest, Err: err} return APIError{HTTPStatus: http.StatusBadRequest, Err: err}
} }
// we could consider setting up a sync.Pool for the summed
// hashes to reduce GC pressure.
w.Header().Set("Etag", makeEtag(r.URL.Path, hash))
return nil return nil
case http.MethodPost, case http.MethodPost,
@@ -937,7 +961,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
forceReload := r.Header.Get("Cache-Control") == "must-revalidate" forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
err := changeConfig(r.Method, r.URL.Path, body, forceReload) err := changeConfig(r.Method, r.URL.Path, body, r.Header.Get("If-Match"), forceReload)
if err != nil && !errors.Is(err, errSameConfig) { if err != nil && !errors.Is(err, errSameConfig) {
return err return err
} }
@@ -971,9 +995,9 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
id := parts[2] id := parts[2]
// map the ID to the expanded path // map the ID to the expanded path
currentCfgMu.RLock() currentCtxMu.RLock()
expanded, ok := rawCfgIndex[id] expanded, ok := rawCfgIndex[id]
defer currentCfgMu.RUnlock() defer currentCtxMu.RUnlock()
if !ok { if !ok {
return APIError{ return APIError{
HTTPStatus: http.StatusNotFound, HTTPStatus: http.StatusNotFound,
@@ -996,10 +1020,6 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
} }
} }
if err := notify.NotifyStopping(); err != nil {
Log().Error("unable to notify stopping to service manager", zap.Error(err))
}
exitProcess(context.Background(), Log().Named("admin.api")) exitProcess(context.Background(), Log().Named("admin.api"))
return nil return nil
} }
@@ -1008,11 +1028,11 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
// the operation at path according to method, using body and out as // the operation at path according to method, using body and out as
// needed. This is a low-level, unsynchronized function; most callers // needed. This is a low-level, unsynchronized function; most callers
// will want to use changeConfig or readConfig instead. This requires a // will want to use changeConfig or readConfig instead. This requires a
// read or write lock on currentCfgMu, depending on method (GET needs // read or write lock on currentCtxMu, depending on method (GET needs
// only a read lock; all others need a write lock). // only a read lock; all others need a write lock).
func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error { func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error {
var err error var err error
var val interface{} var val any
// if there is a request body, decode it into the // if there is a request body, decode it into the
// variable that will be set in the config according // variable that will be set in the config according
@@ -1049,16 +1069,16 @@ func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error
parts = parts[:len(parts)-1] parts = parts[:len(parts)-1]
} }
var ptr interface{} = rawCfg var ptr any = rawCfg
traverseLoop: traverseLoop:
for i, part := range parts { for i, part := range parts {
switch v := ptr.(type) { switch v := ptr.(type) {
case map[string]interface{}: case map[string]any:
// if the next part enters a slice, and the slice is our destination, // if the next part enters a slice, and the slice is our destination,
// handle it specially (because appending to the slice copies the slice // handle it specially (because appending to the slice copies the slice
// header, which does not replace the original one like we want) // header, which does not replace the original one like we want)
if arr, ok := v[part].([]interface{}); ok && i == len(parts)-2 { if arr, ok := v[part].([]any); ok && i == len(parts)-2 {
var idx int var idx int
if method != http.MethodPost { if method != http.MethodPost {
idxStr := parts[len(parts)-1] idxStr := parts[len(parts)-1]
@@ -1080,7 +1100,7 @@ traverseLoop:
} }
case http.MethodPost: case http.MethodPost:
if ellipses { if ellipses {
valArray, ok := val.([]interface{}) valArray, ok := val.([]any)
if !ok { if !ok {
return fmt.Errorf("final element is not an array") return fmt.Errorf("final element is not an array")
} }
@@ -1115,9 +1135,9 @@ traverseLoop:
case http.MethodPost: case http.MethodPost:
// if the part is an existing list, POST appends to // if the part is an existing list, POST appends to
// it, otherwise it just sets or creates the value // it, otherwise it just sets or creates the value
if arr, ok := v[part].([]interface{}); ok { if arr, ok := v[part].([]any); ok {
if ellipses { if ellipses {
valArray, ok := val.([]interface{}) valArray, ok := val.([]any)
if !ok { if !ok {
return fmt.Errorf("final element is not an array") return fmt.Errorf("final element is not an array")
} }
@@ -1148,12 +1168,12 @@ traverseLoop:
// might not exist yet; that's OK but we need to make them as // might not exist yet; that's OK but we need to make them as
// we go, while we still have a pointer from the level above // we go, while we still have a pointer from the level above
if v[part] == nil && method == http.MethodPut { if v[part] == nil && method == http.MethodPut {
v[part] = make(map[string]interface{}) v[part] = make(map[string]any)
} }
ptr = v[part] ptr = v[part]
} }
case []interface{}: case []any:
partInt, err := strconv.Atoi(part) partInt, err := strconv.Atoi(part)
if err != nil { if err != nil {
return fmt.Errorf("[/%s] invalid array index '%s': %v", return fmt.Errorf("[/%s] invalid array index '%s': %v",
@@ -1175,7 +1195,7 @@ traverseLoop:
// RemoveMetaFields removes meta fields like "@id" from a JSON message // RemoveMetaFields removes meta fields like "@id" from a JSON message
// by using a simple regular expression. (An alternate way to do this // by using a simple regular expression. (An alternate way to do this
// would be to delete them from the raw, map[string]interface{} // would be to delete them from the raw, map[string]any
// representation as they are indexed, then iterate the index we made // representation as they are indexed, then iterate the index we made
// and add them back after encoding as JSON, but this is simpler.) // and add them back after encoding as JSON, but this is simpler.)
func RemoveMetaFields(rawJSON []byte) []byte { func RemoveMetaFields(rawJSON []byte) []byte {
@@ -1227,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
} }
@@ -1307,7 +1330,7 @@ const (
) )
var bufPool = sync.Pool{ var bufPool = sync.Pool{
New: func() interface{} { New: func() any {
return new(bytes.Buffer) return new(bytes.Buffer)
}, },
} }
+51 -2
View File
@@ -16,6 +16,8 @@ package caddy
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http"
"reflect" "reflect"
"sync" "sync"
"testing" "testing"
@@ -113,7 +115,7 @@ func TestUnsyncedConfigAccess(t *testing.T) {
} }
// decode the expected config so we can do a convenient DeepEqual // decode the expected config so we can do a convenient DeepEqual
var expectedDecoded interface{} var expectedDecoded any
err = json.Unmarshal([]byte(tc.expect), &expectedDecoded) err = json.Unmarshal([]byte(tc.expect), &expectedDecoded)
if err != nil { if err != nil {
t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err) t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err)
@@ -139,10 +141,57 @@ func TestLoadConcurrent(t *testing.T) {
wg.Done() wg.Done()
}() }()
} }
wg.Wait() wg.Wait()
} }
type fooModule struct {
IntField int
StrField string
}
func (fooModule) CaddyModule() ModuleInfo {
return ModuleInfo{
ID: "foo",
New: func() Module { return new(fooModule) },
}
}
func (fooModule) Start() error { return nil }
func (fooModule) Stop() error { return nil }
func TestETags(t *testing.T) {
RegisterModule(fooModule{})
if err := Load([]byte(`{"admin": {"listen": "localhost:2999"}, "apps": {"foo": {"strField": "abc", "intField": 0}}}`), true); err != nil {
t.Fatalf("loading: %s", err)
}
const key = "/" + rawConfigKey + "/apps/foo"
// try update the config with the wrong etag
err := changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}}`), fmt.Sprintf(`"/%s not_an_etag"`, rawConfigKey), false)
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
t.Fatalf("expected precondition failed; got %v", err)
}
// get the etag
hash := etagHasher()
if err := readConfig(key, hash); err != nil {
t.Fatalf("reading: %s", err)
}
// do the same update with the correct key
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}`), makeEtag(key, hash), false)
if err != nil {
t.Fatalf("expected update to work; got %v", err)
}
// now try another update. The hash should no longer match and we should get precondition failed
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 2}`), makeEtag(key, hash), false)
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
t.Fatalf("expected precondition failed; got %v", err)
}
}
func BenchmarkLoad(b *testing.B) { func BenchmarkLoad(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
Load(testCfg, true) Load(testCfg, true)
+259 -74
View File
@@ -17,6 +17,7 @@ package caddy
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -30,6 +31,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/caddyserver/caddy/v2/notify" "github.com/caddyserver/caddy/v2/notify"
@@ -101,20 +103,32 @@ func Run(cfg *Config) error {
// if it is different from the current config or // if it is different from the current config or
// forceReload is true. // forceReload is true.
func Load(cfgJSON []byte, forceReload bool) error { func Load(cfgJSON []byte, forceReload bool) error {
if err := notify.NotifyReloading(); err != nil { if err := notify.Reloading(); err != nil {
Log().Error("unable to notify reloading to service manager", zap.Error(err)) Log().Error("unable to notify service manager of reloading state", zap.Error(err))
} }
// after reload, notify system of success or, if
// failure, update with status (error message)
var err error
defer func() { defer func() {
if err := notify.NotifyReadiness(); err != nil { if err != nil {
Log().Error("unable to notify readiness to service manager", zap.Error(err)) if notifyErr := notify.Error(err, 0); notifyErr != nil {
Log().Error("unable to notify to service manager of reload error",
zap.Error(notifyErr),
zap.String("reload_err", err.Error()))
}
return
}
if err := notify.Ready(); err != nil {
Log().Error("unable to notify to service manager of ready state", zap.Error(err))
} }
}() }()
err := changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload) err = changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, "", forceReload)
if errors.Is(err, errSameConfig) { if errors.Is(err, errSameConfig) {
err = nil // not really an error err = nil // not really an error
} }
return err return err
} }
@@ -125,7 +139,14 @@ func Load(cfgJSON []byte, forceReload bool) error {
// occur unless forceReload is true. If the config is unchanged and not // occur unless forceReload is true. If the config is unchanged and not
// forcefully reloaded, then errConfigUnchanged This function is safe for // forcefully reloaded, then errConfigUnchanged This function is safe for
// concurrent use. // concurrent use.
func changeConfig(method, path string, input []byte, forceReload bool) error { // The ifMatchHeader can optionally be given a string of the format:
//
// "<path> <hash>"
//
// where <path> is the absolute path in the config and <hash> is the expected hash of
// the config at that path. If the hash in the ifMatchHeader doesn't match
// the hash of the config, then an APIError with status 412 will be returned.
func changeConfig(method, path string, input []byte, ifMatchHeader string, forceReload bool) error {
switch method { switch method {
case http.MethodGet, case http.MethodGet,
http.MethodHead, http.MethodHead,
@@ -135,8 +156,42 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
return fmt.Errorf("method not allowed") return fmt.Errorf("method not allowed")
} }
currentCfgMu.Lock() currentCtxMu.Lock()
defer currentCfgMu.Unlock() defer currentCtxMu.Unlock()
if ifMatchHeader != "" {
// expect the first and last character to be quotes
if len(ifMatchHeader) < 2 || ifMatchHeader[0] != '"' || ifMatchHeader[len(ifMatchHeader)-1] != '"' {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed If-Match header; expect quoted string"),
}
}
// read out the parts
parts := strings.Fields(ifMatchHeader[1 : len(ifMatchHeader)-1])
if len(parts) != 2 {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed If-Match header; expect format \"<path> <hash>\""),
}
}
// get the current hash of the config
// at the given path
hash := etagHasher()
err := unsyncedConfigAccess(http.MethodGet, parts[0], nil, hash)
if err != nil {
return err
}
if hex.EncodeToString(hash.Sum(nil)) != parts[1] {
return APIError{
HTTPStatus: http.StatusPreconditionFailed,
Err: fmt.Errorf("If-Match header did not match current config hash"),
}
}
}
err := unsyncedConfigAccess(method, path, input, nil) err := unsyncedConfigAccess(method, path, input, nil)
if err != nil { if err != nil {
@@ -177,7 +232,7 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
// with what caddy is still running; we need to // with what caddy is still running; we need to
// unmarshal it again because it's likely that // unmarshal it again because it's likely that
// pointers deep in our rawCfg map were modified // pointers deep in our rawCfg map were modified
var oldCfg interface{} var oldCfg any
err2 := json.Unmarshal(rawCfgJSON, &oldCfg) err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
if err2 != nil { if err2 != nil {
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2) err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
@@ -202,18 +257,18 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
// readConfig traverses the current config to path // readConfig traverses the current config to path
// and writes its JSON encoding to out. // and writes its JSON encoding to out.
func readConfig(path string, out io.Writer) error { func readConfig(path string, out io.Writer) error {
currentCfgMu.RLock() currentCtxMu.RLock()
defer currentCfgMu.RUnlock() defer currentCtxMu.RUnlock()
return unsyncedConfigAccess(http.MethodGet, path, nil, out) return unsyncedConfigAccess(http.MethodGet, path, nil, out)
} }
// indexConfigObjects recursively searches ptr for object fields named // indexConfigObjects recursively searches ptr for object fields named
// "@id" and maps that ID value to the full configPath in the index. // "@id" and maps that ID value to the full configPath in the index.
// This function is NOT safe for concurrent access; obtain a write lock // This function is NOT safe for concurrent access; obtain a write lock
// on currentCfgMu. // on currentCtxMu.
func indexConfigObjects(ptr interface{}, configPath string, index map[string]string) error { func indexConfigObjects(ptr any, configPath string, index map[string]string) error {
switch val := ptr.(type) { switch val := ptr.(type) {
case map[string]interface{}: case map[string]any:
for k, v := range val { for k, v := range val {
if k == idKey { if k == idKey {
switch idVal := v.(type) { switch idVal := v.(type) {
@@ -232,7 +287,7 @@ func indexConfigObjects(ptr interface{}, configPath string, index map[string]str
return err return err
} }
} }
case []interface{}: case []any:
// traverse each element of the array recursively // traverse each element of the array recursively
for i := range val { for i := range val {
err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index) err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index)
@@ -250,7 +305,7 @@ func indexConfigObjects(ptr interface{}, configPath string, index map[string]str
// it as the new config, replacing any other current config. // it as the new config, replacing any other current config.
// It does NOT update the raw config state, as this is a // It does NOT update the raw config state, as this is a
// lower-level function; most callers will want to use Load // lower-level function; most callers will want to use Load
// instead. A write lock on currentCfgMu is required! If // instead. A write lock on currentCtxMu is required! If
// allowPersist is false, it will not be persisted to disk, // allowPersist is false, it will not be persisted to disk,
// even if it is configured to. // even if it is configured to.
func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error { func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
@@ -279,17 +334,17 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
} }
// run the new config and start all its apps // run the new config and start all its apps
err = run(newCfg, true) ctx, err := run(newCfg, true)
if err != nil { if err != nil {
return err return err
} }
// swap old config with the new one // swap old context (including its config) with the new one
oldCfg := currentCfg oldCtx := currentCtx
currentCfg = newCfg currentCtx = ctx
// Stop, Cleanup each old app // Stop, Cleanup each old app
unsyncedStop(oldCfg) unsyncedStop(oldCtx)
// autosave a non-nil config, if not disabled // autosave a non-nil config, if not disabled
if allowPersist && if allowPersist &&
@@ -333,7 +388,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
// This is a low-level function; most callers // This is a low-level function; most callers
// will want to use Run instead, which also // will want to use Run instead, which also
// updates the config's raw state. // updates the config's raw state.
func run(newCfg *Config, start bool) error { func run(newCfg *Config, start bool) (Context, error) {
// because we will need to roll back any state // because we will need to roll back any state
// modifications if this function errors, we // modifications if this function errors, we
// keep a single error value and scope all // keep a single error value and scope all
@@ -364,8 +419,8 @@ func run(newCfg *Config, start bool) error {
cancel() cancel()
// also undo any other state changes we made // also undo any other state changes we made
if currentCfg != nil { if currentCtx.cfg != nil {
certmagic.Default.Storage = currentCfg.storage certmagic.Default.Storage = currentCtx.cfg.storage
} }
} }
}() }()
@@ -377,14 +432,14 @@ func run(newCfg *Config, start bool) error {
} }
err = newCfg.Logging.openLogs(ctx) err = newCfg.Logging.openLogs(ctx)
if err != nil { if err != nil {
return err return ctx, err
} }
// start the admin endpoint (and stop any prior one) // start the admin endpoint (and stop any prior one)
if start { if start {
err = replaceLocalAdminServer(newCfg) err = replaceLocalAdminServer(newCfg)
if err != nil { if err != nil {
return fmt.Errorf("starting caddy administration endpoint: %v", err) return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
} }
} }
@@ -413,7 +468,7 @@ func run(newCfg *Config, start bool) error {
return nil return nil
}() }()
if err != nil { if err != nil {
return err return ctx, err
} }
// Load and Provision each app and their submodules // Load and Provision each app and their submodules
@@ -426,23 +481,23 @@ func run(newCfg *Config, start bool) error {
return nil return nil
}() }()
if err != nil { if err != nil {
return err return ctx, err
} }
if !start { if !start {
return nil return ctx, nil
} }
// Provision any admin routers which may need to access // Provision any admin routers which may need to access
// some of the other apps at runtime // some of the other apps at runtime
err = newCfg.Admin.provisionAdminRouters(ctx) err = newCfg.Admin.provisionAdminRouters(ctx)
if err != nil { if err != nil {
return err return ctx, err
} }
// Start // Start
err = func() error { err = func() error {
var started []string started := make([]string, 0, len(newCfg.apps))
for name, a := range newCfg.apps { for name, a := range newCfg.apps {
err := a.Start() err := a.Start()
if err != nil { if err != nil {
@@ -462,12 +517,12 @@ func run(newCfg *Config, start bool) error {
return nil return nil
}() }()
if err != nil { if err != nil {
return err return ctx, err
} }
// now that the user's config is running, finish setting up anything else, // now that the user's config is running, finish setting up anything else,
// such as remote admin endpoint, config loader, etc. // such as remote admin endpoint, config loader, etc.
return finishSettingUp(ctx, newCfg) return ctx, finishSettingUp(ctx, newCfg)
} }
// finishSettingUp should be run after all apps have successfully started. // finishSettingUp should be run after all apps have successfully started.
@@ -500,7 +555,7 @@ func finishSettingUp(ctx Context, cfg *Config) error {
runLoadedConfig := func(config []byte) error { runLoadedConfig := func(config []byte) error {
logger.Info("applying dynamically-loaded config") logger.Info("applying dynamically-loaded config")
err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, false) err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, "", false)
if errors.Is(err, errSameConfig) { if errors.Is(err, errSameConfig) {
return err return err
} }
@@ -572,10 +627,10 @@ type ConfigLoader interface {
// stop the others. Stop should only be called // stop the others. Stop should only be called
// if not replacing with a new config. // if not replacing with a new config.
func Stop() error { func Stop() error {
currentCfgMu.Lock() currentCtxMu.Lock()
defer currentCfgMu.Unlock() defer currentCtxMu.Unlock()
unsyncedStop(currentCfg) unsyncedStop(currentCtx)
currentCfg = nil currentCtx = Context{}
rawCfgJSON = nil rawCfgJSON = nil
rawCfgIndex = nil rawCfgIndex = nil
rawCfg[rawConfigKey] = nil rawCfg[rawConfigKey] = nil
@@ -588,13 +643,13 @@ func Stop() error {
// it is logged and the function continues stopping // it is logged and the function continues stopping
// the next app. This function assumes all apps in // the next app. This function assumes all apps in
// cfg were successfully started first. // cfg were successfully started first.
func unsyncedStop(cfg *Config) { func unsyncedStop(ctx Context) {
if cfg == nil { if ctx.cfg == nil {
return return
} }
// stop each app // stop each app
for name, a := range cfg.apps { for name, a := range ctx.cfg.apps {
err := a.Stop() err := a.Stop()
if err != nil { if err != nil {
log.Printf("[ERROR] stop %s: %v", name, err) log.Printf("[ERROR] stop %s: %v", name, err)
@@ -602,13 +657,13 @@ func unsyncedStop(cfg *Config) {
} }
// clean up all modules // clean up all modules
cfg.cancelFunc() ctx.cfg.cancelFunc()
} }
// Validate loads, provisions, and validates // Validate loads, provisions, and validates
// cfg, but does not start running it. // cfg, but does not start running it.
func Validate(cfg *Config) error { func Validate(cfg *Config) error {
err := run(cfg, false) _, err := run(cfg, false)
if err == nil { if err == nil {
cfg.cancelFunc() // call Cleanup on all modules cfg.cancelFunc() // call Cleanup on all modules
} }
@@ -622,6 +677,14 @@ func Validate(cfg *Config) error {
// Errors are logged along the way, and an appropriate exit // Errors are logged along the way, and an appropriate exit
// code is emitted. // code is emitted.
func exitProcess(ctx context.Context, logger *zap.Logger) { func exitProcess(ctx context.Context, logger *zap.Logger) {
// let the rest of the program know we're quitting
atomic.StoreInt32(exiting, 1)
// give the OS or service/process manager our 2 weeks' notice: we quit
if err := notify.Stopping(); err != nil {
Log().Error("unable to notify service manager of stopping state", zap.Error(err))
}
if logger == nil { if logger == nil {
logger = Log() logger = Log()
} }
@@ -681,6 +744,12 @@ func exitProcess(ctx context.Context, logger *zap.Logger) {
}() }()
} }
var exiting = new(int32) // accessed atomically
// Exiting returns true if the process is exiting.
// EXPERIMENTAL API: subject to change or removal.
func Exiting() bool { return atomic.LoadInt32(exiting) == 1 }
// Duration can be an integer or a string. An integer is // Duration can be an integer or a string. An integer is
// interpreted as nanoseconds. If a string, it is a Go // interpreted as nanoseconds. If a string, it is a Go
// time.Duration value such as `300ms`, `1.5h`, or `2h45m`; // time.Duration value such as `300ms`, `1.5h`, or `2h45m`;
@@ -705,8 +774,12 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
// ParseDuration parses a duration string, adding // ParseDuration parses a duration string, adding
// support for the "d" unit meaning number of days, // support for the "d" unit meaning number of days,
// where a day is assumed to be 24h. // where a day is assumed to be 24h. The maximum
// input string length is 1024.
func ParseDuration(s string) (time.Duration, error) { func ParseDuration(s string) (time.Duration, error) {
if len(s) > 1024 {
return 0, fmt.Errorf("parsing duration: input string too long")
}
var inNumber bool var inNumber bool
var numStart int var numStart int
for i := 0; i < len(s); i++ { for i := 0; i < len(s); i++ {
@@ -751,36 +824,144 @@ func InstanceID() (uuid.UUID, error) {
return uuid.ParseBytes(uuidFileBytes) return uuid.ParseBytes(uuidFileBytes)
} }
// GoModule returns the build info of this Caddy // CustomVersion is an optional string that overrides Caddy's
// build from debug.BuildInfo (requires Go modules). // reported version. It can be helpful when downstream packagers
// If no version information is available, a non-nil // need to manually set Caddy's version. If no other version
// value will still be returned, but with an // information is available, the short form version (see
// unknown version. // Version()) will be set to CustomVersion, and the full version
func GoModule() *debug.Module { // will include CustomVersion at the beginning.
var mod debug.Module //
return goModule(&mod) // Set this variable during `go build` with `-ldflags`:
} //
// -ldflags '-X github.com/caddyserver/caddy/v2.CustomVersion=v2.6.2'
//
// for example.
var CustomVersion string
// goModule holds the actual implementation of GoModule. // Version returns the Caddy version in a simple/short form, and
// Allocating debug.Module in GoModule() and passing a // a full version string. The short form will not have spaces and
// reference to goModule enables mid-stack inlining. // is intended for User-Agent strings and similar, but may be
func goModule(mod *debug.Module) *debug.Module { // omitting valuable information. Note that Caddy must be compiled
mod.Version = "unknown" // in a special way to properly embed complete version information.
// First this function tries to get the version from the embedded
// build info provided by go.mod dependencies; then it tries to
// get info from embedded VCS information, which requires having
// built Caddy from a git repository. If no version is available,
// this function returns "(devel)" because Go uses that, but for
// the simple form we change it to "unknown". If still no version
// is available (e.g. no VCS repo), then it will use CustomVersion;
// CustomVersion is always prepended to the full version string.
//
// See relevant Go issues: https://github.com/golang/go/issues/29228
// and https://github.com/golang/go/issues/50603.
//
// This function is experimental and subject to change or removal.
func Version() (simple, full string) {
// the currently-recommended way to build Caddy involves
// building it as a dependency so we can extract version
// information from go.mod tooling; once the upstream
// Go issues are fixed, we should just be able to use
// bi.Main... hopefully.
var module *debug.Module
bi, ok := debug.ReadBuildInfo() bi, ok := debug.ReadBuildInfo()
if ok { if !ok {
mod.Path = bi.Main.Path if CustomVersion != "" {
// The recommended way to build Caddy involves full = CustomVersion
// creating a separate main module, which simple = CustomVersion
// TODO: track related Go issue: https://github.com/golang/go/issues/29228 return
// once that issue is fixed, we should just be able to use bi.Main... hopefully. }
for _, dep := range bi.Deps { full = "unknown"
if dep.Path == ImportPath { simple = "unknown"
return dep return
}
// find the Caddy module in the dependency list
for _, dep := range bi.Deps {
if dep.Path == ImportPath {
module = dep
break
}
}
if module != nil {
simple, full = module.Version, module.Version
if module.Sum != "" {
full += " " + module.Sum
}
if module.Replace != nil {
full += " => " + module.Replace.Path
if module.Replace.Version != "" {
simple = module.Replace.Version + "_custom"
full += "@" + module.Replace.Version
}
if module.Replace.Sum != "" {
full += " " + module.Replace.Sum
} }
} }
return &bi.Main
} }
return mod
if full == "" {
var vcsRevision string
var vcsTime time.Time
var vcsModified bool
for _, setting := range bi.Settings {
switch setting.Key {
case "vcs.revision":
vcsRevision = setting.Value
case "vcs.time":
vcsTime, _ = time.Parse(time.RFC3339, setting.Value)
case "vcs.modified":
vcsModified, _ = strconv.ParseBool(setting.Value)
}
}
if vcsRevision != "" {
var modified string
if vcsModified {
modified = "+modified"
}
full = fmt.Sprintf("%s%s (%s)", vcsRevision, modified, vcsTime.Format(time.RFC822))
simple = vcsRevision
// use short checksum for simple, if hex-only
if _, err := hex.DecodeString(simple); err == nil {
simple = simple[:8]
}
// append date to simple since it can be convenient
// to know the commit date as part of the version
if !vcsTime.IsZero() {
simple += "-" + vcsTime.Format("20060102")
}
}
}
if full == "" {
if CustomVersion != "" {
full = CustomVersion
} else {
full = "unknown"
}
} else if CustomVersion != "" {
full = CustomVersion + " " + full
}
if simple == "" || simple == "(devel)" {
if CustomVersion != "" {
simple = CustomVersion
} else {
simple = "unknown"
}
}
return
}
// ActiveContext returns the currently-active context.
// This function is experimental and might be changed
// or removed in the future.
func ActiveContext() Context {
currentCtxMu.RLock()
defer currentCtxMu.RUnlock()
return currentCtx
} }
// CtxKey is a value type for use with context.WithValue. // CtxKey is a value type for use with context.WithValue.
@@ -788,18 +969,21 @@ type CtxKey string
// This group of variables pertains to the current configuration. // This group of variables pertains to the current configuration.
var ( var (
// currentCfgMu protects everything in this var block. // currentCtxMu protects everything in this var block.
currentCfgMu sync.RWMutex currentCtxMu sync.RWMutex
// currentCfg is the currently-running configuration. // currentCtx is the root context for the currently-running
currentCfg *Config // configuration, which can be accessed through this value.
// If the Config contained in this value is not nil, then
// a config is currently active/running.
currentCtx Context
// rawCfg is the current, generic-decoded configuration; // rawCfg is the current, generic-decoded configuration;
// we initialize it as a map with one field ("config") // we initialize it as a map with one field ("config")
// to maintain parity with the API endpoint and to avoid // to maintain parity with the API endpoint and to avoid
// the special case of having to access/mutate the variable // the special case of having to access/mutate the variable
// directly without traversing into it. // directly without traversing into it.
rawCfg = map[string]interface{}{ rawCfg = map[string]any{
rawConfigKey: nil, rawConfigKey: nil,
} }
@@ -818,4 +1002,5 @@ var (
var errSameConfig = errors.New("config is unchanged") var errSameConfig = errors.New("config is unchanged")
// ImportPath is the package import path for Caddy core. // ImportPath is the package import path for Caddy core.
// This identifier may be removed in the future.
const ImportPath = "github.com/caddyserver/caddy/v2" const ImportPath = "github.com/caddyserver/caddy/v2"
+3 -3
View File
@@ -29,12 +29,12 @@ type Adapter struct {
} }
// Adapt converts the Caddyfile config in body to Caddy JSON. // Adapt converts the Caddyfile config in body to Caddy JSON.
func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []caddyconfig.Warning, error) { func (a Adapter) Adapt(body []byte, options map[string]any) ([]byte, []caddyconfig.Warning, error) {
if a.ServerType == nil { if a.ServerType == nil {
return nil, nil, fmt.Errorf("no server type") return nil, nil, fmt.Errorf("no server type")
} }
if options == nil { if options == nil {
options = make(map[string]interface{}) options = make(map[string]any)
} }
filename, _ := options["filename"].(string) filename, _ := options["filename"].(string)
@@ -116,7 +116,7 @@ type ServerType interface {
// (e.g. CLI flags) and creates a Caddy // (e.g. CLI flags) and creates a Caddy
// config, along with any warnings or // config, along with any warnings or
// an error. // an error.
Setup([]ServerBlock, map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error) Setup([]ServerBlock, map[string]any) (*caddy.Config, []caddyconfig.Warning, error)
} }
// UnmarshalModule instantiates a module with the given ID and invokes // UnmarshalModule instantiates a module with the given ID and invokes
+6 -6
View File
@@ -146,15 +146,15 @@ func (d *Dispenser) NextLine() bool {
// //
// Proper use of this method looks like this: // Proper use of this method looks like this:
// //
// for nesting := d.Nesting(); d.NextBlock(nesting); { // for nesting := d.Nesting(); d.NextBlock(nesting); {
// } // }
// //
// However, in simple cases where it is known that the // However, in simple cases where it is known that the
// Dispenser is new and has not already traversed state // Dispenser is new and has not already traversed state
// by a loop over NextBlock(), this will do: // by a loop over NextBlock(), this will do:
// //
// for d.NextBlock(0) { // for d.NextBlock(0) {
// } // }
// //
// As with other token parsing logic, a loop over // As with other token parsing logic, a loop over
// NextBlock() should be contained within a loop over // NextBlock() should be contained within a loop over
@@ -217,7 +217,7 @@ func (d *Dispenser) ValRaw() string {
// ScalarVal gets value of the current token, converted to the closest // ScalarVal gets value of the current token, converted to the closest
// scalar type. If there is no token loaded, it returns nil. // scalar type. If there is no token loaded, it returns nil.
func (d *Dispenser) ScalarVal() interface{} { func (d *Dispenser) ScalarVal() any {
if d.cursor < 0 || d.cursor >= len(d.tokens) { if d.cursor < 0 || d.cursor >= len(d.tokens) {
return nil return nil
} }
@@ -412,7 +412,7 @@ func (d *Dispenser) Err(msg string) error {
} }
// Errf is like Err, but for formatted error messages // Errf is like Err, but for formatted error messages
func (d *Dispenser) Errf(format string, args ...interface{}) error { func (d *Dispenser) Errf(format string, args ...any) error {
return d.WrapErr(fmt.Errorf(format, args...)) return d.WrapErr(fmt.Errorf(format, args...))
} }
+4 -1
View File
@@ -153,7 +153,10 @@ func Format(input []byte) []byte {
openBraceWritten = true openBraceWritten = true
nextLine() nextLine()
newLines = 0 newLines = 0
nesting++ // prevent infinite nesting from ridiculous inputs (issue #4169)
if nesting < 10 {
nesting++
}
} }
switch { switch {
-1
View File
@@ -13,7 +13,6 @@
// limitations under the License. // limitations under the License.
//go:build gofuzz //go:build gofuzz
// +build gofuzz
package caddyfile package caddyfile
+4
View File
@@ -191,3 +191,7 @@ func Tokenize(input []byte, filename string) ([]Token, error) {
} }
return tokens, nil return tokens, nil
} }
func (t Token) Quoted() bool {
return t.wasQuoted > 0
}
-1
View File
@@ -13,7 +13,6 @@
// limitations under the License. // limitations under the License.
//go:build gofuzz //go:build gofuzz
// +build gofuzz
package caddyfile package caddyfile
+5 -5
View File
@@ -24,7 +24,7 @@ import (
// Adapter is a type which can adapt a configuration to Caddy JSON. // Adapter is a type which can adapt a configuration to Caddy JSON.
// It returns the results and any warnings, or an error. // It returns the results and any warnings, or an error.
type Adapter interface { type Adapter interface {
Adapt(body []byte, options map[string]interface{}) ([]byte, []Warning, error) Adapt(body []byte, options map[string]any) ([]byte, []Warning, error)
} }
// Warning represents a warning or notice related to conversion. // Warning represents a warning or notice related to conversion.
@@ -48,7 +48,7 @@ func (w Warning) String() string {
// are converted to warnings. This is convenient when filling config // are converted to warnings. This is convenient when filling config
// structs that require a json.RawMessage, without having to worry // structs that require a json.RawMessage, without having to worry
// about errors. // about errors.
func JSON(val interface{}, warnings *[]Warning) json.RawMessage { func JSON(val any, warnings *[]Warning) json.RawMessage {
b, err := json.Marshal(val) b, err := json.Marshal(val)
if err != nil { if err != nil {
if warnings != nil { if warnings != nil {
@@ -64,9 +64,9 @@ func JSON(val interface{}, warnings *[]Warning) json.RawMessage {
// for encoding module values where the module name has to be described within // for encoding module values where the module name has to be described within
// the object by a certain key; for example, `"handler": "file_server"` for a // the object by a certain key; for example, `"handler": "file_server"` for a
// file server HTTP handler (fieldName="handler" and fieldVal="file_server"). // file server HTTP handler (fieldName="handler" and fieldVal="file_server").
// The val parameter must encode into a map[string]interface{} (i.e. it must be // The val parameter must encode into a map[string]any (i.e. it must be
// a struct or map). Any errors are converted into warnings. // a struct or map). Any errors are converted into warnings.
func JSONModuleObject(val interface{}, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage { func JSONModuleObject(val any, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage {
// encode to a JSON object first // encode to a JSON object first
enc, err := json.Marshal(val) enc, err := json.Marshal(val)
if err != nil { if err != nil {
@@ -77,7 +77,7 @@ func JSONModuleObject(val interface{}, fieldName, fieldVal string, warnings *[]W
} }
// then decode the object // then decode the object
var tmp map[string]interface{} var tmp map[string]any
err = json.Unmarshal(enc, &tmp) err = json.Unmarshal(enc, &tmp)
if err != nil { if err != nil {
if warnings != nil { if warnings != nil {
+49 -22
View File
@@ -17,6 +17,7 @@ package httpcaddyfile
import ( import (
"fmt" "fmt"
"net" "net"
"net/netip"
"reflect" "reflect"
"sort" "sort"
"strconv" "strconv"
@@ -35,12 +36,12 @@ import (
// server block that share the same address stay grouped together so the config // server block that share the same address stay grouped together so the config
// isn't repeated unnecessarily. For example, this Caddyfile: // isn't repeated unnecessarily. For example, this Caddyfile:
// //
// example.com { // example.com {
// bind 127.0.0.1 // bind 127.0.0.1
// } // }
// www.example.com, example.net/path, localhost:9999 { // www.example.com, example.net/path, localhost:9999 {
// bind 127.0.0.1 1.2.3.4 // bind 127.0.0.1 1.2.3.4
// } // }
// //
// has two server blocks to start with. But expressed in this Caddyfile are // has two server blocks to start with. But expressed in this Caddyfile are
// actually 4 listener addresses: 127.0.0.1:443, 1.2.3.4:443, 127.0.0.1:9999, // actually 4 listener addresses: 127.0.0.1:443, 1.2.3.4:443, 127.0.0.1:9999,
@@ -76,7 +77,7 @@ import (
// multiple addresses to the same lists of server blocks (a many:many mapping). // multiple addresses to the same lists of server blocks (a many:many mapping).
// (Doing this is essentially a map-reduce technique.) // (Doing this is essentially a map-reduce technique.)
func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock, func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock,
options map[string]interface{}) (map[string][]serverBlock, error) { options map[string]any) (map[string][]serverBlock, error) {
sbmap := make(map[string][]serverBlock) sbmap := make(map[string][]serverBlock)
for i, sblock := range originalServerBlocks { for i, sblock := range originalServerBlocks {
@@ -102,12 +103,20 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBloc
} }
} }
// make a slice of the map keys so we can iterate in sorted order
addrs := make([]string, 0, len(addrToKeys))
for k := range addrToKeys {
addrs = append(addrs, k)
}
sort.Strings(addrs)
// now that we know which addresses serve which keys of this // now that we know which addresses serve which keys of this
// server block, we iterate that mapping and create a list of // server block, we iterate that mapping and create a list of
// new server blocks for each address where the keys of the // new server blocks for each address where the keys of the
// server block are only the ones which use the address; but // server block are only the ones which use the address; but
// the contents (tokens) are of course the same // the contents (tokens) are of course the same
for addr, keys := range addrToKeys { for _, addr := range addrs {
keys := addrToKeys[addr]
// parse keys so that we only have to do it once // parse keys so that we only have to do it once
parsedKeys := make([]Address, 0, len(keys)) parsedKeys := make([]Address, 0, len(keys))
for _, key := range keys { for _, key := range keys {
@@ -161,6 +170,7 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
delete(addrToServerBlocks, otherAddr) delete(addrToServerBlocks, otherAddr)
} }
} }
sort.Strings(a.addresses)
sbaddrs = append(sbaddrs, a) sbaddrs = append(sbaddrs, a)
} }
@@ -174,8 +184,10 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
return sbaddrs return sbaddrs
} }
// listenerAddrsForServerBlockKey essentially converts the Caddyfile
// site addresses to Caddy listener addresses for each server block.
func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string, func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string,
options map[string]interface{}) ([]string, error) { options map[string]any) ([]string, error) {
addr, err := ParseAddress(key) addr, err := ParseAddress(key)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing key: %v", err) return nil, fmt.Errorf("parsing key: %v", err)
@@ -207,14 +219,14 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
return nil, fmt.Errorf("[%s] scheme and port violate convention", key) return nil, fmt.Errorf("[%s] scheme and port violate convention", key)
} }
// the bind directive specifies hosts, but is optional // the bind directive specifies hosts (and potentially network), but is optional
lnHosts := make([]string, 0, len(sblock.pile)) lnHosts := make([]string, 0, len(sblock.pile["bind"]))
for _, cfgVal := range sblock.pile["bind"] { for _, cfgVal := range sblock.pile["bind"] {
lnHosts = append(lnHosts, cfgVal.Value.([]string)...) lnHosts = append(lnHosts, cfgVal.Value.([]string)...)
} }
if len(lnHosts) == 0 { if len(lnHosts) == 0 {
if defaultBind, ok := options["default_bind"].(string); ok { if defaultBind, ok := options["default_bind"].([]string); ok {
lnHosts = []string{defaultBind} lnHosts = defaultBind
} else { } else {
lnHosts = []string{""} lnHosts = []string{""}
} }
@@ -222,13 +234,27 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
// use a map to prevent duplication // use a map to prevent duplication
listeners := make(map[string]struct{}) listeners := make(map[string]struct{})
for _, host := range lnHosts { for _, lnHost := range lnHosts {
addr, err := caddy.ParseNetworkAddress(host) // normally we would simply append the port,
if err == nil && addr.IsUnixNetwork() { // but if lnHost is IPv6, we need to ensure it
listeners[host] = struct{}{} // is enclosed in [ ]; net.JoinHostPort does
} else { // this for us, but lnHost might also have a
listeners[host+":"+lnPort] = struct{}{} // network type in front (e.g. "tcp/") leading
// to "[tcp/::1]" which causes parsing failures
// later; what we need is "tcp/[::1]", so we have
// to split the network and host, then re-combine
network, host, ok := strings.Cut(lnHost, "/")
if !ok {
host = network
network = ""
} }
host = strings.Trim(host, "[]") // IPv6
networkAddr := caddy.JoinNetworkAddress(network, host, lnPort)
addr, err := caddy.ParseNetworkAddress(networkAddr)
if err != nil {
return nil, fmt.Errorf("parsing network address: %v", err)
}
listeners[addr.String()] = struct{}{}
} }
// now turn map into list // now turn map into list
@@ -236,6 +262,7 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
for lnStr := range listeners { for lnStr := range listeners {
listenersList = append(listenersList, lnStr) listenersList = append(listenersList, lnStr)
} }
sort.Strings(listenersList)
return listenersList, nil return listenersList, nil
} }
@@ -340,9 +367,9 @@ func (a Address) Normalize() Address {
// ensure host is normalized if it's an IP address // ensure host is normalized if it's an IP address
host := strings.TrimSpace(a.Host) host := strings.TrimSpace(a.Host)
if ip := net.ParseIP(host); ip != nil { if ip, err := netip.ParseAddr(host); err == nil {
if ipv6 := ip.To16(); ipv6 != nil && ipv6.DefaultMask() == nil { if ip.Is6() && !ip.Is4() && !ip.Is4In6() {
host = ipv6.String() host = ip.String()
} }
} }
@@ -13,7 +13,6 @@
// limitations under the License. // limitations under the License.
//go:build gofuzz //go:build gofuzz
// +build gofuzz
package httpcaddyfile package httpcaddyfile
+63 -36
View File
@@ -48,12 +48,12 @@ func init() {
RegisterHandlerDirective("handle", parseHandle) RegisterHandlerDirective("handle", parseHandle)
RegisterDirective("handle_errors", parseHandleErrors) RegisterDirective("handle_errors", parseHandleErrors)
RegisterDirective("log", parseLog) RegisterDirective("log", parseLog)
RegisterHandlerDirective("skip_log", parseSkipLog)
} }
// parseBind parses the bind directive. Syntax: // parseBind parses the bind directive. Syntax:
// //
// bind <addresses...> // bind <addresses...>
//
func parseBind(h Helper) ([]ConfigValue, error) { func parseBind(h Helper) ([]ConfigValue, error) {
var lnHosts []string var lnHosts []string
for h.Next() { for h.Next() {
@@ -64,28 +64,28 @@ func parseBind(h Helper) ([]ConfigValue, error) {
// parseTLS parses the tls directive. Syntax: // parseTLS parses the tls directive. Syntax:
// //
// tls [<email>|internal]|[<cert_file> <key_file>] { // tls [<email>|internal]|[<cert_file> <key_file>] {
// protocols <min> [<max>] // protocols <min> [<max>]
// ciphers <cipher_suites...> // ciphers <cipher_suites...>
// curves <curves...> // curves <curves...>
// client_auth { // client_auth {
// mode [request|require|verify_if_given|require_and_verify] // mode [request|require|verify_if_given|require_and_verify]
// trusted_ca_cert <base64_der> // trusted_ca_cert <base64_der>
// trusted_ca_cert_file <filename> // trusted_ca_cert_file <filename>
// trusted_leaf_cert <base64_der> // trusted_leaf_cert <base64_der>
// trusted_leaf_cert_file <filename> // trusted_leaf_cert_file <filename>
// } // }
// alpn <values...> // alpn <values...>
// load <paths...> // load <paths...>
// ca <acme_ca_endpoint> // ca <acme_ca_endpoint>
// ca_root <pem_file> // ca_root <pem_file>
// dns <provider_name> [...] // dns <provider_name> [...]
// on_demand // on_demand
// eab <key_id> <mac_key> // eab <key_id> <mac_key>
// issuer <module_name> [...] // issuer <module_name> [...]
// get_certificate <module_name> [...] // get_certificate <module_name> [...]
// } // insecure_secrets_log <log_file>
// // }
func parseTLS(h Helper) ([]ConfigValue, error) { func parseTLS(h Helper) ([]ConfigValue, error) {
cp := new(caddytls.ConnectionPolicy) cp := new(caddytls.ConnectionPolicy)
var fileLoader caddytls.FileLoader var fileLoader caddytls.FileLoader
@@ -395,6 +395,12 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
} }
onDemand = true onDemand = true
case "insecure_secrets_log":
if !h.NextArg() {
return nil, h.ArgErr()
}
cp.InsecureSecretsLog = h.Val()
default: default:
return nil, h.Errf("unknown subdirective: %s", h.Val()) return nil, h.Errf("unknown subdirective: %s", h.Val())
} }
@@ -515,8 +521,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
// parseRoot parses the root directive. Syntax: // parseRoot parses the root directive. Syntax:
// //
// root [<matcher>] <path> // root [<matcher>] <path>
//
func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) { func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) {
var root string var root string
for h.Next() { for h.Next() {
@@ -540,8 +545,13 @@ func parseVars(h Helper) (caddyhttp.MiddlewareHandler, error) {
// parseRedir parses the redir directive. Syntax: // parseRedir parses the redir directive. Syntax:
// //
// redir [<matcher>] <to> [<code>] // redir [<matcher>] <to> [<code>]
// //
// <code> can be "permanent" for 301, "temporary" for 302 (default),
// a placeholder, or any number in the 3xx range or 401. The special
// code "html" can be used to redirect only browser clients (will
// respond with HTTP 200 and no Location header; redirect is performed
// with JS and a meta tag).
func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) { func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
if !h.Next() { if !h.Next() {
return nil, h.ArgErr() return nil, h.ArgErr()
@@ -558,6 +568,7 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
} }
var body string var body string
var hdr http.Header
switch code { switch code {
case "permanent": case "permanent":
code = "301" code = "301"
@@ -578,7 +589,7 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
` `
safeTo := html.EscapeString(to) safeTo := html.EscapeString(to)
body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo) body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo)
code = "302" code = "200" // don't redirect non-browser clients
default: default:
// Allow placeholders for the code // Allow placeholders for the code
if strings.HasPrefix(code, "{") { if strings.HasPrefix(code, "{") {
@@ -601,9 +612,14 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
} }
} }
// don't redirect non-browser clients
if code != "200" {
hdr = http.Header{"Location": []string{to}}
}
return caddyhttp.StaticResponse{ return caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(code), StatusCode: caddyhttp.WeakString(code),
Headers: http.Header{"Location": []string{to}}, Headers: hdr,
Body: body, Body: body,
}, nil }, nil
} }
@@ -683,12 +699,11 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {
// parseLog parses the log directive. Syntax: // parseLog parses the log directive. Syntax:
// //
// log { // log {
// output <writer_module> ... // output <writer_module> ...
// format <encoder_module> ... // format <encoder_module> ...
// level <level> // level <level>
// } // }
//
func parseLog(h Helper) ([]ConfigValue, error) { func parseLog(h Helper) ([]ConfigValue, error) {
return parseLogHelper(h, nil) return parseLogHelper(h, nil)
} }
@@ -720,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.
@@ -847,3 +862,15 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
} }
return configValues, nil return configValues, nil
} }
// parseSkipLog parses the skip_log directive. Syntax:
//
// skip_log [<matcher>]
func parseSkipLog(h Helper) (caddyhttp.MiddlewareHandler, error) {
for h.Next() {
if h.NextArg() {
return nil, h.ArgErr()
}
}
return caddyhttp.VarsMiddleware{"skip_log": true}, nil
}
+32 -24
View File
@@ -42,6 +42,7 @@ var directiveOrder = []string{
"map", "map",
"vars", "vars",
"root", "root",
"skip_log",
"header", "header",
"copy_response_headers", // only in reverse_proxy's handle_response "copy_response_headers", // only in reverse_proxy's handle_response
@@ -142,8 +143,8 @@ func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) {
type Helper struct { type Helper struct {
*caddyfile.Dispenser *caddyfile.Dispenser
// State stores intermediate variables during caddyfile adaptation. // State stores intermediate variables during caddyfile adaptation.
State map[string]interface{} State map[string]any
options map[string]interface{} options map[string]any
warnings *[]caddyconfig.Warning warnings *[]caddyconfig.Warning
matcherDefs map[string]caddy.ModuleMap matcherDefs map[string]caddy.ModuleMap
parentBlock caddyfile.ServerBlock parentBlock caddyfile.ServerBlock
@@ -151,7 +152,7 @@ type Helper struct {
} }
// Option gets the option keyed by name. // Option gets the option keyed by name.
func (h Helper) Option(name string) interface{} { func (h Helper) Option(name string) any {
return h.options[name] return h.options[name]
} }
@@ -175,7 +176,7 @@ func (h Helper) Caddyfiles() []string {
} }
// JSON converts val into JSON. Any errors are added to warnings. // JSON converts val into JSON. Any errors are added to warnings.
func (h Helper) JSON(val interface{}) json.RawMessage { func (h Helper) JSON(val any) json.RawMessage {
return caddyconfig.JSON(val, h.warnings) return caddyconfig.JSON(val, h.warnings)
} }
@@ -375,7 +376,7 @@ type ConfigValue struct {
// The value to be used when building the config. // The value to be used when building the config.
// Generally its type is associated with the // Generally its type is associated with the
// name of the Class. // name of the Class.
Value interface{} Value any
directive string directive string
} }
@@ -406,7 +407,7 @@ func sortRoutes(routes []ConfigValue) {
return false return false
} }
// decode the path matchers, if there is just one of them // decode the path matchers if there is just one matcher set
var iPM, jPM caddyhttp.MatchPath var iPM, jPM caddyhttp.MatchPath
if len(iRoute.MatcherSetsRaw) == 1 { if len(iRoute.MatcherSetsRaw) == 1 {
_ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &iPM) _ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &iPM)
@@ -415,38 +416,45 @@ func sortRoutes(routes []ConfigValue) {
_ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &jPM) _ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &jPM)
} }
// sort by longer path (more specific) first; missing path // if there is only one path in the path matcher, sort by longer path
// matchers or multi-matchers are treated as zero-length paths // (more specific) first; missing path matchers or multi-matchers are
// treated as zero-length paths
var iPathLen, jPathLen int var iPathLen, jPathLen int
if len(iPM) > 0 { if len(iPM) == 1 {
iPathLen = len(iPM[0]) iPathLen = len(iPM[0])
} }
if len(jPM) > 0 { if len(jPM) == 1 {
jPathLen = len(jPM[0]) jPathLen = len(jPM[0])
} }
// some directives involve setting values which can overwrite // some directives involve setting values which can overwrite
// eachother, so it makes most sense to reverse the order so // each other, so it makes most sense to reverse the order so
// that the lease specific matcher is first; everything else // that the lease specific matcher is first; everything else
// has most-specific matcher first // has most-specific matcher first
if iDir == "vars" { if iDir == "vars" {
// if both directives have no path matcher, use whichever one // we can only confidently compare path lengths if both
// has no matcher first. // directives have a single path to match (issue #5037)
if iPathLen == 0 && jPathLen == 0 { if iPathLen > 0 && jPathLen > 0 {
return len(iRoute.MatcherSetsRaw) == 0 && len(jRoute.MatcherSetsRaw) > 0 // sort least-specific (shortest) path first
return iPathLen < jPathLen
} }
// sort with the least-specific (shortest) path first // if both directives don't have a single path to compare,
return iPathLen < jPathLen // sort whichever one has no matcher first; if both have
// no matcher, sort equally (stable sort preserves order)
return len(iRoute.MatcherSetsRaw) == 0 && len(jRoute.MatcherSetsRaw) > 0
} else { } else {
// if both directives have no path matcher, use whichever one // we can only confidently compare path lengths if both
// has any kind of matcher defined first. // directives have a single path to match (issue #5037)
if iPathLen == 0 && jPathLen == 0 { if iPathLen > 0 && jPathLen > 0 {
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0 // sort most-specific (longest) path first
return iPathLen > jPathLen
} }
// sort with the most-specific (longest) path first // if both directives don't have a single path to compare,
return iPathLen > jPathLen // sort whichever one has a matcher first; if both have
// a matcher, sort equally (stable sort preserves order)
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
} }
}) })
} }
@@ -567,7 +575,7 @@ type (
// tokens from a global option. It is passed the tokens to parse and // tokens from a global option. It is passed the tokens to parse and
// existing value from the previous instance of this global option // existing value from the previous instance of this global option
// (if any). It returns the value to associate with this global option. // (if any). It returns the value to associate with this global option.
UnmarshalGlobalFunc func(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) UnmarshalGlobalFunc func(d *caddyfile.Dispenser, existingVal any) (any, error)
) )
var registeredDirectives = make(map[string]UnmarshalFunc) var registeredDirectives = make(map[string]UnmarshalFunc)
+115 -68
View File
@@ -53,27 +53,18 @@ type ServerType struct {
// Setup makes a config from the tokens. // Setup makes a config from the tokens.
func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock, func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
options map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error) { options map[string]any) (*caddy.Config, []caddyconfig.Warning, error) {
var warnings []caddyconfig.Warning var warnings []caddyconfig.Warning
gc := counter{new(int)} gc := counter{new(int)}
state := make(map[string]interface{}) state := make(map[string]any)
// load all the server blocks and associate them with a "pile" // load all the server blocks and associate them with a "pile" of config values
// of config values; also prohibit duplicate keys because they
// can make a config confusing if more than one server block is
// chosen to handle a request - we actually will make each
// server block's route terminal so that only one will run
sbKeys := make(map[string]struct{})
originalServerBlocks := make([]serverBlock, 0, len(inputServerBlocks)) originalServerBlocks := make([]serverBlock, 0, len(inputServerBlocks))
for i, sblock := range inputServerBlocks { for _, sblock := range inputServerBlocks {
for j, k := range sblock.Keys { for j, k := range sblock.Keys {
if j == 0 && strings.HasPrefix(k, "@") { if j == 0 && strings.HasPrefix(k, "@") {
return nil, warnings, fmt.Errorf("cannot define a matcher outside of a site block: '%s'", k) return nil, warnings, fmt.Errorf("cannot define a matcher outside of a site block: '%s'", k)
} }
if _, ok := sbKeys[k]; ok {
return nil, warnings, fmt.Errorf("duplicate site address not allowed: '%s' in %v (site block %d, key %d)", k, sblock.Keys, i, j)
}
sbKeys[k] = struct{}{}
} }
originalServerBlocks = append(originalServerBlocks, serverBlock{ originalServerBlocks = append(originalServerBlocks, serverBlock{
block: sblock, block: sblock,
@@ -100,13 +91,17 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
search *regexp.Regexp search *regexp.Regexp
replace string replace string
}{ }{
{regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"},
{regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"},
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"}, {regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
{regexp.MustCompile(`{cookie\.([\w-]*)}`), "{http.request.cookie.$1}"},
{regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"},
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"}, {regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
{regexp.MustCompile(`{file\.([\w-]*)}`), "{http.request.uri.path.file.$1}"},
{regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"},
{regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"}, {regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
{regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"}, {regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
{regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"}, {regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
{regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"},
{regexp.MustCompile(`{file_match\.([\w-]*)}`), "{http.matchers.file.$1}"},
} }
for _, sb := range originalServerBlocks { for _, sb := range originalServerBlocks {
@@ -198,10 +193,11 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
// now that each server is configured, make the HTTP app // now that each server is configured, make the HTTP app
httpApp := caddyhttp.App{ httpApp := caddyhttp.App{
HTTPPort: tryInt(options["http_port"], &warnings), HTTPPort: tryInt(options["http_port"], &warnings),
HTTPSPort: tryInt(options["https_port"], &warnings), HTTPSPort: tryInt(options["https_port"], &warnings),
GracePeriod: tryDuration(options["grace_period"], &warnings), GracePeriod: tryDuration(options["grace_period"], &warnings),
Servers: servers, ShutdownDelay: tryDuration(options["shutdown_delay"], &warnings),
Servers: servers,
} }
// then make the TLS app // then make the TLS app
@@ -223,11 +219,11 @@ 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 == "" {
ncl.log.Level = "DEBUG" ncl.log.Level = zap.DebugLevel.CapitalString()
} }
customLogs = append(customLogs, ncl) customLogs = append(customLogs, ncl)
} }
@@ -244,8 +240,8 @@ 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: "DEBUG"}, log: &caddy.CustomLog{Level: zap.DebugLevel.CapitalString()},
}) })
} }
} }
@@ -303,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...)
} }
@@ -321,14 +317,14 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
// which is expected to be the first server block if it has zero // which is expected to be the first server block if it has zero
// keys. It returns the updated list of server blocks with the // keys. It returns the updated list of server blocks with the
// global options block removed, and updates options accordingly. // global options block removed, and updates options accordingly.
func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options map[string]interface{}) ([]serverBlock, error) { func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options map[string]any) ([]serverBlock, error) {
if len(serverBlocks) == 0 || len(serverBlocks[0].block.Keys) > 0 { if len(serverBlocks) == 0 || len(serverBlocks[0].block.Keys) > 0 {
return serverBlocks, nil return serverBlocks, nil
} }
for _, segment := range serverBlocks[0].block.Segments { for _, segment := range serverBlocks[0].block.Segments {
opt := segment.Directive() opt := segment.Directive()
var val interface{} var val any
var err error var err error
disp := caddyfile.NewDispenser(segment) disp := caddyfile.NewDispenser(segment)
@@ -398,7 +394,7 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
// to server blocks. Each pairing is essentially a server definition. // to server blocks. Each pairing is essentially a server definition.
func (st *ServerType) serversFromPairings( func (st *ServerType) serversFromPairings(
pairings []sbAddrAssociation, pairings []sbAddrAssociation,
options map[string]interface{}, options map[string]any,
warnings *[]caddyconfig.Warning, warnings *[]caddyconfig.Warning,
groupCounter counter, groupCounter counter,
) (map[string]*caddyhttp.Server, error) { ) (map[string]*caddyhttp.Server, error) {
@@ -419,6 +415,23 @@ func (st *ServerType) serversFromPairings(
} }
for i, p := range pairings { for i, p := range pairings {
// detect ambiguous site definitions: server blocks which
// have the same host bound to the same interface (listener
// address), otherwise their routes will improperly be added
// to the same server (see issue #4635)
for j, sblock1 := range p.serverBlocks {
for _, key := range sblock1.block.Keys {
for k, sblock2 := range p.serverBlocks {
if k == j {
continue
}
if sliceContains(sblock2.block.Keys, key) {
return nil, fmt.Errorf("ambiguous site definition: %s", key)
}
}
}
}
srv := &caddyhttp.Server{ srv := &caddyhttp.Server{
Listen: p.addresses, Listen: p.addresses,
} }
@@ -505,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
@@ -645,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
} }
} }
} }
@@ -716,7 +712,7 @@ func (st *ServerType) serversFromPairings(
return servers, nil return servers, nil
} }
func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock, options map[string]interface{}) error { func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock, options map[string]any) error {
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort) httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
if hp, ok := options["http_port"].(int); ok { if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp) httpPort = strconv.Itoa(hp)
@@ -911,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
@@ -933,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
} }
@@ -942,7 +962,7 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subroute, error) { func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subroute, error) {
for _, val := range routes { for _, val := range routes {
if !directiveIsOrdered(val.directive) { if !directiveIsOrdered(val.directive) {
return nil, fmt.Errorf("directive '%s' is not ordered, so it cannot be used here", val.directive) return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here", val.directive)
} }
} }
@@ -1190,6 +1210,7 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod
func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.ModuleMap) error { func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.ModuleMap) error {
for d.Next() { for d.Next() {
// this is the "name" for "named matchers"
definitionName := d.Val() definitionName := d.Val()
if _, ok := matchers[definitionName]; ok { if _, ok := matchers[definitionName]; ok {
@@ -1197,16 +1218,9 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
} }
matchers[definitionName] = make(caddy.ModuleMap) matchers[definitionName] = make(caddy.ModuleMap)
// in case there are multiple instances of the same matcher, concatenate // given a matcher name and the tokens following it, parse
// their tokens (we expect that UnmarshalCaddyfile should be able to // the tokens as a matcher module and record it
// handle more than one segment); otherwise, we'd overwrite other makeMatcher := func(matcherName string, tokens []caddyfile.Token) error {
// instances of the matcher in this set
tokensByMatcherName := make(map[string][]caddyfile.Token)
for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
matcherName := d.Val()
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
}
for matcherName, tokens := range tokensByMatcherName {
mod, err := caddy.GetModule("http.matchers." + matcherName) mod, err := caddy.GetModule("http.matchers." + matcherName)
if err != nil { if err != nil {
return fmt.Errorf("getting matcher module '%s': %v", matcherName, err) return fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
@@ -1224,6 +1238,39 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
} }
matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil) matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
return nil
}
// if the next token is quoted, we can assume it's not a matcher name
// and that it's probably an 'expression' matcher
if d.NextArg() {
if d.Token().Quoted() {
err := makeMatcher("expression", []caddyfile.Token{d.Token()})
if err != nil {
return err
}
continue
}
// if it wasn't quoted, then we need to rewind after calling
// d.NextArg() so the below properly grabs the matcher name
d.Prev()
}
// in case there are multiple instances of the same matcher, concatenate
// their tokens (we expect that UnmarshalCaddyfile should be able to
// handle more than one segment); otherwise, we'd overwrite other
// instances of the matcher in this set
tokensByMatcherName := make(map[string][]caddyfile.Token)
for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
matcherName := d.Val()
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
}
for matcherName, tokens := range tokensByMatcherName {
err := makeMatcher(matcherName, tokens)
if err != nil {
return err
}
} }
} }
return nil return nil
@@ -1295,7 +1342,7 @@ func WasReplacedPlaceholderShorthand(token string) string {
// tryInt tries to convert val to an integer. If it fails, // tryInt tries to convert val to an integer. If it fails,
// it downgrades the error to a warning and returns 0. // it downgrades the error to a warning and returns 0.
func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int { func tryInt(val any, warnings *[]caddyconfig.Warning) int {
intVal, ok := val.(int) intVal, ok := val.(int)
if val != nil && !ok && warnings != nil { if val != nil && !ok && warnings != nil {
*warnings = append(*warnings, caddyconfig.Warning{Message: "not an integer type"}) *warnings = append(*warnings, caddyconfig.Warning{Message: "not an integer type"})
@@ -1303,7 +1350,7 @@ func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int {
return intVal return intVal
} }
func tryString(val interface{}, warnings *[]caddyconfig.Warning) string { func tryString(val any, warnings *[]caddyconfig.Warning) string {
stringVal, ok := val.(string) stringVal, ok := val.(string)
if val != nil && !ok && warnings != nil { if val != nil && !ok && warnings != nil {
*warnings = append(*warnings, caddyconfig.Warning{Message: "not a string type"}) *warnings = append(*warnings, caddyconfig.Warning{Message: "not a string type"})
@@ -1311,7 +1358,7 @@ func tryString(val interface{}, warnings *[]caddyconfig.Warning) string {
return stringVal return stringVal
} }
func tryDuration(val interface{}, warnings *[]caddyconfig.Warning) caddy.Duration { func tryDuration(val any, warnings *[]caddyconfig.Warning) caddy.Duration {
durationVal, ok := val.(caddy.Duration) durationVal, ok := val.(caddy.Duration)
if val != nil && !ok && warnings != nil { if val != nil && !ok && warnings != nil {
*warnings = append(*warnings, caddyconfig.Warning{Message: "not a duration type"}) *warnings = append(*warnings, caddyconfig.Warning{Message: "not a duration type"})
+36 -26
View File
@@ -29,13 +29,15 @@ func init() {
RegisterGlobalOption("debug", parseOptTrue) RegisterGlobalOption("debug", parseOptTrue)
RegisterGlobalOption("http_port", parseOptHTTPPort) RegisterGlobalOption("http_port", parseOptHTTPPort)
RegisterGlobalOption("https_port", parseOptHTTPSPort) RegisterGlobalOption("https_port", parseOptHTTPSPort)
RegisterGlobalOption("default_bind", parseOptSingleString) RegisterGlobalOption("default_bind", parseOptStringList)
RegisterGlobalOption("grace_period", parseOptDuration) RegisterGlobalOption("grace_period", parseOptDuration)
RegisterGlobalOption("shutdown_delay", parseOptDuration)
RegisterGlobalOption("default_sni", parseOptSingleString) RegisterGlobalOption("default_sni", parseOptSingleString)
RegisterGlobalOption("order", parseOptOrder) RegisterGlobalOption("order", parseOptOrder)
RegisterGlobalOption("storage", parseOptStorage) RegisterGlobalOption("storage", parseOptStorage)
RegisterGlobalOption("storage_clean_interval", parseOptDuration) RegisterGlobalOption("storage_clean_interval", parseOptDuration)
RegisterGlobalOption("renew_interval", parseOptDuration) RegisterGlobalOption("renew_interval", parseOptDuration)
RegisterGlobalOption("ocsp_interval", parseOptDuration)
RegisterGlobalOption("acme_ca", parseOptSingleString) RegisterGlobalOption("acme_ca", parseOptSingleString)
RegisterGlobalOption("acme_ca_root", parseOptSingleString) RegisterGlobalOption("acme_ca_root", parseOptSingleString)
RegisterGlobalOption("acme_dns", parseOptACMEDNS) RegisterGlobalOption("acme_dns", parseOptACMEDNS)
@@ -54,9 +56,9 @@ func init() {
RegisterGlobalOption("preferred_chains", parseOptPreferredChains) RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
} }
func parseOptTrue(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { return true, nil } func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil }
func parseOptHTTPPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { func parseOptHTTPPort(d *caddyfile.Dispenser, _ any) (any, error) {
var httpPort int var httpPort int
for d.Next() { for d.Next() {
var httpPortStr string var httpPortStr string
@@ -72,7 +74,7 @@ func parseOptHTTPPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error
return httpPort, nil return httpPort, nil
} }
func parseOptHTTPSPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { func parseOptHTTPSPort(d *caddyfile.Dispenser, _ any) (any, error) {
var httpsPort int var httpsPort int
for d.Next() { for d.Next() {
var httpsPortStr string var httpsPortStr string
@@ -88,7 +90,7 @@ func parseOptHTTPSPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, erro
return httpsPort, nil return httpsPort, nil
} }
func parseOptOrder(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
newOrder := directiveOrder newOrder := directiveOrder
for d.Next() { for d.Next() {
@@ -164,7 +166,7 @@ func parseOptOrder(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
return newOrder, nil return newOrder, nil
} }
func parseOptStorage(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { func parseOptStorage(d *caddyfile.Dispenser, _ any) (any, error) {
if !d.Next() { // consume option name if !d.Next() { // consume option name
return nil, d.ArgErr() return nil, d.ArgErr()
} }
@@ -183,7 +185,7 @@ func parseOptStorage(d *caddyfile.Dispenser, _ interface{}) (interface{}, error)
return storage, nil return storage, nil
} }
func parseOptDuration(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) {
if !d.Next() { // consume option name if !d.Next() { // consume option name
return nil, d.ArgErr() return nil, d.ArgErr()
} }
@@ -197,7 +199,7 @@ func parseOptDuration(d *caddyfile.Dispenser, _ interface{}) (interface{}, error
return caddy.Duration(dur), nil return caddy.Duration(dur), nil
} }
func parseOptACMEDNS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) {
if !d.Next() { // consume option name if !d.Next() { // consume option name
return nil, d.ArgErr() return nil, d.ArgErr()
} }
@@ -216,7 +218,7 @@ func parseOptACMEDNS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error)
return prov, nil return prov, nil
} }
func parseOptACMEEAB(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) {
eab := new(acme.EAB) eab := new(acme.EAB)
for d.Next() { for d.Next() {
if d.NextArg() { if d.NextArg() {
@@ -244,7 +246,7 @@ func parseOptACMEEAB(d *caddyfile.Dispenser, _ interface{}) (interface{}, error)
return eab, nil return eab, nil
} }
func parseOptCertIssuer(d *caddyfile.Dispenser, existing interface{}) (interface{}, error) { func parseOptCertIssuer(d *caddyfile.Dispenser, existing any) (any, error) {
var issuers []certmagic.Issuer var issuers []certmagic.Issuer
if existing != nil { if existing != nil {
issuers = existing.([]certmagic.Issuer) issuers = existing.([]certmagic.Issuer)
@@ -267,7 +269,7 @@ func parseOptCertIssuer(d *caddyfile.Dispenser, existing interface{}) (interface
return issuers, nil return issuers, nil
} }
func parseOptSingleString(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume parameter name d.Next() // consume parameter name
if !d.Next() { if !d.Next() {
return "", d.ArgErr() return "", d.ArgErr()
@@ -279,7 +281,16 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ interface{}) (interface{}, e
return val, nil return val, nil
} }
func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { func parseOptStringList(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume parameter name
val := d.RemainingArgs()
if len(val) == 0 {
return "", d.ArgErr()
}
return val, nil
}
func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) {
adminCfg := new(caddy.AdminConfig) adminCfg := new(caddy.AdminConfig)
for d.Next() { for d.Next() {
if d.NextArg() { if d.NextArg() {
@@ -315,7 +326,7 @@ func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
return adminCfg, nil return adminCfg, nil
} }
func parseOptOnDemand(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) {
var ond *caddytls.OnDemandConfig var ond *caddytls.OnDemandConfig
for d.Next() { for d.Next() {
if d.NextArg() { if d.NextArg() {
@@ -375,7 +386,7 @@ func parseOptOnDemand(d *caddyfile.Dispenser, _ interface{}) (interface{}, error
return ond, nil return ond, nil
} }
func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume parameter name d.Next() // consume parameter name
if !d.Next() { if !d.Next() {
return "", d.ArgErr() return "", d.ArgErr()
@@ -390,11 +401,11 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ interface{}) (interface{}, erro
return val, nil return val, nil
} }
func parseServerOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { func parseServerOptions(d *caddyfile.Dispenser, _ any) (any, error) {
return unmarshalCaddyfileServerOptions(d) return unmarshalCaddyfileServerOptions(d)
} }
func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name d.Next() // consume option name
var val string var val string
if !d.AllArgs(&val) { if !d.AllArgs(&val) {
@@ -410,18 +421,17 @@ func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ interface{}) (interface{
// parseLogOptions parses the global log option. Syntax: // parseLogOptions parses the global log option. Syntax:
// //
// log [name] { // log [name] {
// output <writer_module> ... // output <writer_module> ...
// format <encoder_module> ... // format <encoder_module> ...
// level <level> // level <level>
// include <namespaces...> // include <namespaces...>
// exclude <namespaces...> // exclude <namespaces...>
// } // }
// //
// When the name argument is unspecified, this directive modifies the default // When the name argument is unspecified, this directive modifies the default
// logger. // logger.
// func parseLogOptions(d *caddyfile.Dispenser, existingVal any) (any, error) {
func parseLogOptions(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) {
currentNames := make(map[string]struct{}) currentNames := make(map[string]struct{})
if existingVal != nil { if existingVal != nil {
innerVals, ok := existingVal.([]ConfigValue) innerVals, ok := existingVal.([]ConfigValue)
@@ -456,7 +466,7 @@ func parseLogOptions(d *caddyfile.Dispenser, existingVal interface{}) (interface
return configValues, nil return configValues, nil
} }
func parseOptPreferredChains(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { func parseOptPreferredChains(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() d.Next()
return caddytls.ParseCaddyfilePreferredChainsOptions(d) return caddytls.ParseCaddyfilePreferredChainsOptions(d)
} }
+2 -3
View File
@@ -45,8 +45,7 @@ func init() {
// } // }
// //
// When the CA ID is unspecified, 'local' is assumed. // When the CA ID is unspecified, 'local' is assumed.
// func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) {
func parsePKIApp(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) {
pki := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)} pki := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
for d.Next() { for d.Next() {
@@ -160,7 +159,7 @@ func parsePKIApp(d *caddyfile.Dispenser, existingVal interface{}) (interface{},
func (st ServerType) buildPKIApp( func (st ServerType) buildPKIApp(
pairings []sbAddrAssociation, pairings []sbAddrAssociation,
options map[string]interface{}, options map[string]any,
warnings []caddyconfig.Warning, warnings []caddyconfig.Warning,
) (*caddypki.PKI, []caddyconfig.Warning, error) { ) (*caddypki.PKI, []caddyconfig.Warning, error) {
+61 -23
View File
@@ -38,14 +38,15 @@ type serverOptions struct {
ReadHeaderTimeout caddy.Duration ReadHeaderTimeout caddy.Duration
WriteTimeout caddy.Duration WriteTimeout caddy.Duration
IdleTimeout caddy.Duration IdleTimeout caddy.Duration
KeepAliveInterval caddy.Duration
MaxHeaderBytes int MaxHeaderBytes int
AllowH2C bool Protocols []string
ExperimentalHTTP3 bool
StrictSNIHost *bool StrictSNIHost *bool
ShouldLogCredentials bool ShouldLogCredentials bool
Metrics *caddyhttp.Metrics
} }
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) { func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
serverOpts := serverOptions{} serverOpts := serverOptions{}
for d.Next() { for d.Next() {
if d.NextArg() { if d.NextArg() {
@@ -123,6 +124,15 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val()) return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
} }
} }
case "keepalive_interval":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing keepalive interval duration: %v", err)
}
serverOpts.KeepAliveInterval = caddy.Duration(dur)
case "max_header_size": case "max_header_size":
var sizeStr string var sizeStr string
@@ -141,22 +151,60 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error
} }
serverOpts.ShouldLogCredentials = true serverOpts.ShouldLogCredentials = true
case "protocols":
protos := d.RemainingArgs()
for _, proto := range protos {
if proto != "h1" && proto != "h2" && proto != "h2c" && proto != "h3" {
return nil, d.Errf("unknown protocol '%s': expected h1, h2, h2c, or h3", proto)
}
if sliceContains(serverOpts.Protocols, proto) {
return nil, d.Errf("protocol %s specified more than once", proto)
}
serverOpts.Protocols = append(serverOpts.Protocols, proto)
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
case "strict_sni_host":
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
}
boolVal := true
if d.Val() == "insecure_off" {
boolVal = false
}
serverOpts.StrictSNIHost = &boolVal
case "metrics":
if d.NextArg() {
return nil, d.ArgErr()
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
serverOpts.Metrics = new(caddyhttp.Metrics)
// TODO: DEPRECATED. (August 2022)
case "protocol": case "protocol":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol sub-option will be removed soon")
for nesting := d.Nesting(); d.NextBlock(nesting); { for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() { switch d.Val() {
case "allow_h2c": case "allow_h2c":
if d.NextArg() { caddy.Log().Named("caddyfile").Warn("DEPRECATED: allow_h2c will be removed soon; use protocols option instead")
return nil, d.ArgErr()
}
serverOpts.AllowH2C = true
case "experimental_http3":
if d.NextArg() { if d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
} }
serverOpts.ExperimentalHTTP3 = true if sliceContains(serverOpts.Protocols, "h2c") {
return nil, d.Errf("protocol h2c already specified")
}
serverOpts.Protocols = append(serverOpts.Protocols, "h2c")
case "strict_sni_host": case "strict_sni_host":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol > strict_sni_host in this position will be removed soon; move up to the servers block instead")
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" { if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val()) return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
} }
@@ -182,20 +230,9 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error
// applyServerOptions sets the server options on the appropriate servers // applyServerOptions sets the server options on the appropriate servers
func applyServerOptions( func applyServerOptions(
servers map[string]*caddyhttp.Server, servers map[string]*caddyhttp.Server,
options map[string]interface{}, options map[string]any,
warnings *[]caddyconfig.Warning, warnings *[]caddyconfig.Warning,
) error { ) error {
// If experimental HTTP/3 is enabled, enable it on each server.
// We already know there won't be a conflict with serverOptions because
// we validated earlier that "experimental_http3" cannot be set at the same
// time as "servers"
if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
*warnings = append(*warnings, caddyconfig.Warning{Message: "the 'experimental_http3' global option is deprecated, please use the 'servers > protocol > experimental_http3' option instead"})
for _, srv := range servers {
srv.ExperimentalHTTP3 = true
}
}
serverOpts, ok := options["servers"].([]serverOptions) serverOpts, ok := options["servers"].([]serverOptions)
if !ok { if !ok {
return nil return nil
@@ -228,10 +265,11 @@ func applyServerOptions(
server.ReadHeaderTimeout = opts.ReadHeaderTimeout server.ReadHeaderTimeout = opts.ReadHeaderTimeout
server.WriteTimeout = opts.WriteTimeout server.WriteTimeout = opts.WriteTimeout
server.IdleTimeout = opts.IdleTimeout server.IdleTimeout = opts.IdleTimeout
server.KeepAliveInterval = opts.KeepAliveInterval
server.MaxHeaderBytes = opts.MaxHeaderBytes server.MaxHeaderBytes = opts.MaxHeaderBytes
server.AllowH2C = opts.AllowH2C server.Protocols = opts.Protocols
server.ExperimentalHTTP3 = opts.ExperimentalHTTP3
server.StrictSNIHost = opts.StrictSNIHost server.StrictSNIHost = opts.StrictSNIHost
server.Metrics = opts.Metrics
if opts.ShouldLogCredentials { if opts.ShouldLogCredentials {
if server.Logs == nil { if server.Logs == nil {
server.Logs = &caddyhttp.ServerLogConfig{} server.Logs = &caddyhttp.ServerLogConfig{}
+64 -51
View File
@@ -33,7 +33,7 @@ import (
func (st ServerType) buildTLSApp( func (st ServerType) buildTLSApp(
pairings []sbAddrAssociation, pairings []sbAddrAssociation,
options map[string]interface{}, options map[string]any,
warnings []caddyconfig.Warning, warnings []caddyconfig.Warning,
) (*caddytls.TLS, []caddyconfig.Warning, error) { ) (*caddytls.TLS, []caddyconfig.Warning, error) {
@@ -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
@@ -307,6 +311,14 @@ func (st ServerType) buildTLSApp(
tlsApp.Automation.RenewCheckInterval = renewCheckInterval tlsApp.Automation.RenewCheckInterval = renewCheckInterval
} }
// set the OCSP check interval if configured
if ocspCheckInterval, ok := options["ocsp_interval"].(caddy.Duration); ok {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.OCSPCheckInterval = ocspCheckInterval
}
// set whether OCSP stapling should be disabled for manually-managed certificates // set whether OCSP stapling should be disabled for manually-managed certificates
if ocspConfig, ok := options["ocsp_stapling"].(certmagic.OCSPConfig); ok { if ocspConfig, ok := options["ocsp_stapling"].(certmagic.OCSPConfig); ok {
tlsApp.DisableOCSPStapling = ocspConfig.DisableStapling tlsApp.DisableOCSPStapling = ocspConfig.DisableStapling
@@ -323,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 {
@@ -350,7 +364,6 @@ func (st ServerType) buildTLSApp(
globalPreferredChains := options["preferred_chains"] globalPreferredChains := options["preferred_chains"]
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil
if hasGlobalACMEDefaults { if hasGlobalACMEDefaults {
// for _, ap := range tlsApp.Automation.Policies {
for i := 0; i < len(tlsApp.Automation.Policies); i++ { for i := 0; i < len(tlsApp.Automation.Policies); i++ {
ap := tlsApp.Automation.Policies[i] ap := tlsApp.Automation.Policies[i]
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) { if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
@@ -421,7 +434,7 @@ func (st ServerType) buildTLSApp(
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer } type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interface{}) error { func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) error {
acmeWrapper, ok := issuer.(acmeCapable) acmeWrapper, ok := issuer.(acmeCapable)
if !ok { if !ok {
return nil return nil
@@ -468,7 +481,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf
// for any other automation policies. A nil policy (and no error) will be // for any other automation policies. A nil policy (and no error) will be
// returned if there are no default/global options. However, if always is // returned if there are no default/global options. However, if always is
// true, a non-nil value will always be returned (unless there is an error). // true, a non-nil value will always be returned (unless there is an error).
func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) { func newBaseAutomationPolicy(options map[string]any, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
issuers, hasIssuers := options["cert_issuer"] issuers, hasIssuers := options["cert_issuer"]
_, hasLocalCerts := options["local_certs"] _, hasLocalCerts := options["local_certs"]
keyType, hasKeyType := options["key_type"] keyType, hasKeyType := options["key_type"]
+34 -3
View File
@@ -94,7 +94,7 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
} }
} }
resp, err := client.Do(req) resp, err := doHttpCallWithRetries(ctx, client, req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -113,12 +113,43 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
return nil, err return nil, err
} }
for _, warn := range warnings { for _, warn := range warnings {
ctx.Logger(hl).Warn(warn.String()) ctx.Logger().Warn(warn.String())
} }
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),
@@ -129,7 +160,7 @@ func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
// client authentication // client authentication
if hl.TLS.UseServerIdentity { if hl.TLS.UseServerIdentity {
certs, err := ctx.IdentityCredentials(ctx.Logger(hl)) certs, err := ctx.IdentityCredentials(ctx.Logger())
if err != nil { if err != nil {
return nil, fmt.Errorf("getting server identity credentials: %v", err) return nil, fmt.Errorf("getting server identity credentials: %v", err)
} }
+49 -5
View File
@@ -58,6 +58,10 @@ func (al adminLoad) Routes() []caddy.AdminRoute {
Pattern: "/load", Pattern: "/load",
Handler: caddy.AdminHandlerFunc(al.handleLoad), Handler: caddy.AdminHandlerFunc(al.handleLoad),
}, },
{
Pattern: "/adapt",
Handler: caddy.AdminHandlerFunc(al.handleAdapt),
},
} }
} }
@@ -122,7 +126,48 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
return nil return nil
} }
// adaptByContentType adapts body to Caddy JSON using the adapter specified by contenType. // handleAdapt adapts the given Caddy config to JSON and responds with the result.
func (adminLoad) handleAdapt(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodPost {
return caddy.APIError{
HTTPStatus: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed"),
}
}
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
_, err := io.Copy(buf, r.Body)
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("reading request body: %v", err),
}
}
result, warnings, err := adaptByContentType(r.Header.Get("Content-Type"), buf.Bytes())
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: err,
}
}
out := struct {
Warnings []Warning `json:"warnings,omitempty"`
Result json.RawMessage `json:"result"`
}{
Warnings: warnings,
Result: result,
}
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(out)
}
// adaptByContentType adapts body to Caddy JSON using the adapter specified by contentType.
// If contentType is empty or ends with "/json", the input will be returned, as a no-op. // If contentType is empty or ends with "/json", the input will be returned, as a no-op.
func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, error) { func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, error) {
// assume JSON as the default // assume JSON as the default
@@ -144,12 +189,11 @@ func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, err
} }
// adapter name should be suffix of MIME type // adapter name should be suffix of MIME type
slashIdx := strings.Index(ct, "/") _, adapterName, slashFound := strings.Cut(ct, "/")
if slashIdx < 0 { if !slashFound {
return nil, nil, fmt.Errorf("malformed Content-Type") return nil, nil, fmt.Errorf("malformed Content-Type")
} }
adapterName := ct[slashIdx+1:]
cfgAdapter := GetAdapter(adapterName) cfgAdapter := GetAdapter(adapterName)
if cfgAdapter == nil { if cfgAdapter == nil {
return nil, nil, fmt.Errorf("unrecognized config adapter '%s'", adapterName) return nil, nil, fmt.Errorf("unrecognized config adapter '%s'", adapterName)
@@ -164,7 +208,7 @@ func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, err
} }
var bufPool = sync.Pool{ var bufPool = sync.Pool{
New: func() interface{} { New: func() any {
return new(bytes.Buffer) return new(bytes.Buffer)
}, },
} }
+9 -9
View File
@@ -43,7 +43,7 @@ type Defaults struct {
// Default testing values // Default testing values
var Default = Defaults{ var Default = Defaults{
AdminPort: 2019, AdminPort: 2999, // different from what a real server also running on a developer's machine might be
Certifcates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"}, Certifcates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
TestRequestTimeout: 5 * time.Second, TestRequestTimeout: 5 * time.Second,
LoadRequestTimeout: 5 * time.Second, LoadRequestTimeout: 5 * time.Second,
@@ -100,7 +100,7 @@ func (tc *Tester) InitServer(rawConfig string, configType string) {
tc.t.Fail() tc.t.Fail()
} }
if err := tc.ensureConfigRunning(rawConfig, configType); err != nil { if err := tc.ensureConfigRunning(rawConfig, configType); err != nil {
tc.t.Logf("failed ensurng config is running: %s", err) tc.t.Logf("failed ensuring config is running: %s", err)
tc.t.Fail() tc.t.Fail()
} }
} }
@@ -186,7 +186,7 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil) expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil)
} }
var expected interface{} var expected any
err := json.Unmarshal(expectedBytes, &expected) err := json.Unmarshal(expectedBytes, &expected)
if err != nil { if err != nil {
return err return err
@@ -196,7 +196,7 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
Timeout: Default.LoadRequestTimeout, Timeout: Default.LoadRequestTimeout,
} }
fetchConfig := func(client *http.Client) interface{} { fetchConfig := func(client *http.Client) any {
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil { if err != nil {
return nil return nil
@@ -206,7 +206,7 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
if err != nil { if err != nil {
return nil return nil
} }
var actual interface{} var actual any
err = json.Unmarshal(actualBytes, &actual) err = json.Unmarshal(actualBytes, &actual)
if err != nil { if err != nil {
return nil return nil
@@ -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)
} }
} }
@@ -371,7 +371,7 @@ func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string,
return false return false
} }
options := make(map[string]interface{}) options := make(map[string]any)
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options) result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
if err != nil { if err != nil {
+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
@@ -63,32 +63,6 @@ app.example.com {
] ]
} }
] ]
},
{
"routes": [
{
"handle": [
{
"exclude": [
"Connection",
"Keep-Alive",
"Te",
"Trailers",
"Transfer-Encoding",
"Upgrade"
],
"handler": "copy_response_headers"
}
]
},
{
"handle": [
{
"handler": "copy_response"
}
]
}
]
} }
], ],
"handler": "reverse_proxy", "handler": "reverse_proxy",
@@ -0,0 +1,90 @@
:8881
forward_auth localhost:9000 {
uri /auth
copy_headers A>1 B C>3 {
D
E>5
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"routes": [
{
"handle": [
{
"handle_response": [
{
"match": {
"status_code": [
2
]
},
"routes": [
{
"handle": [
{
"handler": "headers",
"request": {
"set": {
"1": [
"{http.reverse_proxy.header.A}"
],
"3": [
"{http.reverse_proxy.header.C}"
],
"5": [
"{http.reverse_proxy.header.E}"
],
"B": [
"{http.reverse_proxy.header.B}"
],
"D": [
"{http.reverse_proxy.header.D}"
]
}
}
}
]
}
]
}
],
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"X-Forwarded-Method": [
"{http.request.method}"
],
"X-Forwarded-Uri": [
"{http.request.uri}"
]
}
}
},
"rewrite": {
"method": "GET",
"uri": "/auth"
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
]
}
]
}
}
}
}
}
@@ -3,6 +3,7 @@
http_port 8080 http_port 8080
https_port 8443 https_port 8443
grace_period 5s grace_period 5s
shutdown_delay 10s
default_sni localhost default_sni localhost
order root first order root first
storage file_system { storage file_system {
@@ -45,6 +46,7 @@
"http_port": 8080, "http_port": 8080,
"https_port": 8443, "https_port": 8443,
"grace_period": 5000000000, "grace_period": 5000000000,
"shutdown_delay": 10000000000,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
@@ -22,6 +22,7 @@
} }
storage_clean_interval 7d storage_clean_interval 7d
renew_interval 1d renew_interval 1d
ocsp_interval 2d
key_type ed25519 key_type ed25519
} }
@@ -83,6 +84,7 @@
}, },
"ask": "https://example.com" "ask": "https://example.com"
}, },
"ocsp_interval": 172800000000000,
"renew_interval": 86400000000000, "renew_interval": 86400000000000,
"storage_clean_interval": 604800000000000 "storage_clean_interval": 604800000000000
} }
@@ -1,5 +1,5 @@
{ {
default_bind tcp4/0.0.0.0 default_bind tcp4/0.0.0.0 tcp6/[::]
} }
example.com { example.com {
@@ -14,7 +14,8 @@ example.org:12345 {
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
"tcp4/0.0.0.0:12345" "tcp4/0.0.0.0:12345",
"tcp6/[::]:12345"
], ],
"routes": [ "routes": [
{ {
@@ -31,7 +32,8 @@ example.org:12345 {
}, },
"srv1": { "srv1": {
"listen": [ "listen": [
"tcp4/0.0.0.0:443" "tcp4/0.0.0.0:443",
"tcp6/[::]:443"
], ],
"routes": [ "routes": [
{ {
@@ -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
}
} }
} }
@@ -12,11 +12,8 @@
} }
max_header_size 100MB max_header_size 100MB
log_credentials log_credentials
protocol { protocols h1 h2 h2c h3
allow_h2c strict_sni_host
experimental_http3
strict_sni_host
}
} }
} }
@@ -61,8 +58,12 @@ foo.com {
"logs": { "logs": {
"should_log_credentials": true "should_log_credentials": true
}, },
"experimental_http3": true, "protocols": [
"allow_h2c": true "h1",
"h2",
"h2c",
"h3"
]
} }
} }
} }
@@ -1,5 +1,7 @@
http://localhost:2020 { http://localhost:2020 {
log log
skip_log /first-hidden*
skip_log /second-hidden*
respond 200 respond 200
} }
@@ -28,6 +30,36 @@ http://localhost:2020 {
{ {
"handler": "subroute", "handler": "subroute",
"routes": [ "routes": [
{
"handle": [
{
"handler": "vars",
"skip_log": true
}
],
"match": [
{
"path": [
"/second-hidden*"
]
}
]
},
{
"handle": [
{
"handler": "vars",
"skip_log": true
}
],
"match": [
{
"path": [
"/first-hidden*"
]
}
]
},
{ {
"handle": [ "handle": [
{ {
@@ -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",
@@ -19,27 +19,30 @@
@matcher6 vars_regexp "{http.request.uri}" `\.([a-f0-9]{6})\.(css|js)$` @matcher6 vars_regexp "{http.request.uri}" `\.([a-f0-9]{6})\.(css|js)$`
respond @matcher6 "from vars_regexp matcher without name" respond @matcher6 "from vars_regexp matcher without name"
@matcher7 { @matcher7 `path('/foo*') && method('GET')`
respond @matcher7 "inline expression matcher shortcut"
@matcher8 {
header Foo bar header Foo bar
header Foo foobar header Foo foobar
header Bar foo header Bar foo
} }
respond @matcher7 "header matcher merging values of the same field" respond @matcher8 "header matcher merging values of the same field"
@matcher8 { @matcher9 {
query foo=bar foo=baz bar=foo query foo=bar foo=baz bar=foo
query bar=baz query bar=baz
} }
respond @matcher8 "query matcher merging pairs with the same keys" respond @matcher9 "query matcher merging pairs with the same keys"
@matcher9 { @matcher10 {
header !Foo header !Foo
header Bar foo header Bar foo
} }
respond @matcher9 "header matcher with null field matcher" respond @matcher10 "header matcher with null field matcher"
@matcher10 remote_ip private_ranges @matcher11 remote_ip private_ranges
respond @matcher10 "remote_ip matcher with private ranges" respond @matcher11 "remote_ip matcher with private ranges"
} }
---------- ----------
{ {
@@ -152,6 +155,19 @@
} }
] ]
}, },
{
"match": [
{
"expression": "path('/foo*') \u0026\u0026 method('GET')"
}
],
"handle": [
{
"body": "inline expression matcher shortcut",
"handler": "static_response"
}
]
},
{ {
"match": [ "match": [
{ {
@@ -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
@@ -1,6 +1,8 @@
:8884 :8884
reverse_proxy h2c://localhost:8080 reverse_proxy h2c://localhost:8080
reverse_proxy unix+h2c//run/app.sock
---------- ----------
{ {
"apps": { "apps": {
@@ -27,6 +29,21 @@ reverse_proxy h2c://localhost:8080
"dial": "localhost:8080" "dial": "localhost:8080"
} }
] ]
},
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http",
"versions": [
"h2c",
"2"
]
},
"upstreams": [
{
"dial": "unix//run/app.sock"
}
]
} }
] ]
} }
@@ -0,0 +1,64 @@
:8884
reverse_proxy 127.0.0.1:65535 {
lb_policy first
lb_retries 5
lb_try_duration 10s
lb_try_interval 500ms
lb_retry_match {
path /foo*
method POST
}
lb_retry_match path /bar*
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"load_balancing": {
"retries": 5,
"retry_match": [
{
"method": [
"POST"
],
"path": [
"/foo*"
]
},
{
"path": [
"/bar*"
]
}
],
"selection_policy": {
"policy": "first"
},
"try_duration": 10000000000,
"try_interval": 500000000
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
@@ -24,6 +24,9 @@ https://example.com {
max_conns_per_host 5 max_conns_per_host 5
keepalive_idle_conns_per_host 2 keepalive_idle_conns_per_host 2
keepalive_interval 30s keepalive_interval 30s
tls_renegotiation freely
tls_except_ports 8181 8182
} }
} }
} }
@@ -91,7 +94,13 @@ https://example.com {
] ]
}, },
"response_header_timeout": 8000000000, "response_header_timeout": 8000000000,
"tls": {}, "tls": {
"except_ports": [
"8181",
"8182"
],
"renegotiation": "freely"
},
"versions": [ "versions": [
"h2c", "h2c",
"2" "2"
@@ -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"
]
}
]
}
}
}
}
+11 -1
View File
@@ -14,8 +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 1ns
} }
localhost:9080 { localhost:9080 {
@@ -35,8 +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 1ns
} }
localhost:9080 { localhost:9080 {
@@ -68,7 +72,7 @@ func TestDuplicateHosts(t *testing.T) {
} }
`, `,
"caddyfile", "caddyfile",
"duplicate site address not allowed") "ambiguous site definition")
} }
func TestReadCookie(t *testing.T) { func TestReadCookie(t *testing.T) {
@@ -84,8 +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 1ns
} }
localhost:9080 { localhost:9080 {
@@ -107,8 +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 1ns
} }
localhost:9080 { localhost:9080 {
+3
View File
@@ -11,8 +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 1ns
} }
http://localhost:9080 { http://localhost:9080 {
file_server browse file_server browse
+17 -2
View File
@@ -11,8 +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 1ns
} }
localhost:9080 { localhost:9080 {
@@ -38,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
} }
@@ -60,12 +65,22 @@ func TestMapRespondWithDefault(t *testing.T) {
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost unknown") tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost unknown")
} }
func TestMapAsJson(t *testing.T) { func TestMapAsJSON(t *testing.T) {
// arrange // arrange
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,
@@ -85,7 +100,7 @@ func TestMapAsJson(t *testing.T) {
{ {
"handler": "map", "handler": "map",
"source": "{http.request.method}", "source": "{http.request.method}",
"destinations": ["dest-name"], "destinations": ["{dest-name}"],
"defaults": ["unknown"], "defaults": ["unknown"],
"mappings": [ "mappings": [
{ {
+73 -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,8 +17,19 @@ 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,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
@@ -49,7 +61,15 @@ 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,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
@@ -113,8 +133,19 @@ 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,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
@@ -154,8 +185,19 @@ 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,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
@@ -237,8 +279,19 @@ 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,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
@@ -320,7 +373,15 @@ 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,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
@@ -357,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!"
@@ -372,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!")
} }
@@ -418,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 {
@@ -473,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 -237
View File
@@ -11,91 +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": {
"servers": { "http": {
"srv0": { "http_port": 9080,
"listen": [ "https_port": 9443,
":9443" "grace_period": 1,
], "servers": {
"routes": [ "srv0": {
{ "listen": [
"handle": [ ":9443"
{ ],
"handler": "subroute", "routes": [
"routes": [ {
{ "handle": [
"handle": [ {
{ "handler": "subroute",
"body": "hello from a.caddy.localhost", "routes": [
"handler": "static_response", {
"status_code": 200 "handle": [
} {
], "body": "hello from a.caddy.localhost",
"match": [ "handler": "static_response",
{ "status_code": 200
"path": [ }
"/version" ],
] "match": [
} {
] "path": [
} "/version"
] ]
} }
], ]
"match": [ }
{ ]
"host": [ }
"127.0.0.1" ],
] "match": [
} {
], "host": [
"terminal": true "127.0.0.1"
} ]
], }
"tls_connection_policies": [ ],
{ "terminal": true
"certificate_selection": { }
"any_tag": ["cert0"] ],
}, "tls_connection_policies": [
"match": { {
"sni": [ "certificate_selection": {
"127.0.0.1" "any_tag": ["cert0"]
] },
} "match": {
}, "sni": [
{ "127.0.0.1"
"default_sni": "*.caddy.localhost" ]
} }
] },
} {
} "default_sni": "*.caddy.localhost"
}, }
"tls": { ]
"certificates": { }
"load_files": [ }
{ },
"certificate": "/caddy.localhost.crt", "tls": {
"key": "/caddy.localhost.key", "certificates": {
"tags": [ "load_files": [
"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
@@ -107,96 +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": {
"servers": { "http": {
"srv0": { "http_port": 9080,
"listen": [ "https_port": 9443,
":9443" "grace_period": 1,
], "servers": {
"routes": [ "srv0": {
{ "listen": [
"handle": [ ":9443"
{ ],
"handler": "subroute", "routes": [
"routes": [ {
{ "handle": [
"handle": [ {
{ "handler": "subroute",
"body": "hello from a", "routes": [
"handler": "static_response", {
"status_code": 200 "handle": [
} {
], "body": "hello from a",
"match": [ "handler": "static_response",
{ "status_code": 200
"path": [ }
"/version" ],
] "match": [
} {
] "path": [
} "/version"
] ]
} }
], ]
"match": [ }
{ ]
"host": [ }
"a.caddy.localhost", ],
"127.0.0.1" "match": [
] {
} "host": [
], "a.caddy.localhost",
"terminal": true "127.0.0.1"
} ]
], }
"tls_connection_policies": [ ],
{ "terminal": true
"certificate_selection": { }
"any_tag": ["cert0"] ],
}, "tls_connection_policies": [
"default_sni": "a.caddy.localhost", {
"match": { "certificate_selection": {
"sni": [ "any_tag": ["cert0"]
"a.caddy.localhost", },
"127.0.0.1", "default_sni": "a.caddy.localhost",
"" "match": {
] "sni": [
} "a.caddy.localhost",
}, "127.0.0.1",
{ ""
"default_sni": "a.caddy.localhost" ]
} }
] },
} {
} "default_sni": "a.caddy.localhost"
}, }
"tls": { ]
"certificates": { }
"load_files": [ }
{ },
"certificate": "/a.caddy.localhost.crt", "tls": {
"key": "/a.caddy.localhost.key", "certificates": {
"tags": [ "load_files": [
"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
@@ -207,68 +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": {
"servers": { "http": {
"srv0": { "http_port": 9080,
"listen": [ "https_port": 9443,
":9443" "grace_period": 1,
], "servers": {
"routes": [ "srv0": {
{ "listen": [
"handle": [ ":9443"
{ ],
"body": "hello from a.caddy.localhost", "routes": [
"handler": "static_response", {
"status_code": 200 "handle": [
} {
], "body": "hello from a.caddy.localhost",
"match": [ "handler": "static_response",
{ "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": [ }
{ },
"certificate": "/a.caddy.localhost.crt", "tls": {
"key": "/a.caddy.localhost.key", "certificates": {
"tags": [ "load_files": [
"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
@@ -278,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 {
@@ -313,6 +326,13 @@ func TestHttpOnlyOnDomainWithSNI(t *testing.T) {
] ]
} }
} }
},
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
} }
} }
}`) }`)
+12 -6
View File
@@ -23,10 +23,14 @@ 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,
"https_port": 9443, "https_port": 9443,
"grace_period": 1,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
@@ -123,8 +127,8 @@ func TestH2ToH2CStream(t *testing.T) {
// Disable any compression method from server. // Disable any compression method from server.
req.Header.Set("Accept-Encoding", "identity") req.Header.Set("Accept-Encoding", "identity")
resp := tester.AssertResponseCode(req, 200) resp := tester.AssertResponseCode(req, http.StatusOK)
if 200 != resp.StatusCode { if resp.StatusCode != http.StatusOK {
return return
} }
go func() { go func() {
@@ -143,7 +147,6 @@ func TestH2ToH2CStream(t *testing.T) {
if !strings.Contains(body, expectedBody) { if !strings.Contains(body, expectedBody) {
t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body) t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
} }
return
} }
func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server { func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
@@ -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": {
@@ -217,6 +223,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
"http": { "http": {
"http_port": 9080, "http_port": 9080,
"https_port": 9443, "https_port": 9443,
"grace_period": 1,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
@@ -335,8 +342,8 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
fmt.Fprint(w, expectedBody) fmt.Fprint(w, expectedBody)
w.Close() w.Close()
}() }()
resp := tester.AssertResponseCode(req, 200) resp := tester.AssertResponseCode(req, http.StatusOK)
if 200 != resp.StatusCode { if resp.StatusCode != http.StatusOK {
return return
} }
@@ -351,7 +358,6 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
if body != expectedBody { if body != expectedBody {
t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body) t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
} }
return
} }
func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server { func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server {
+3
View File
@@ -0,0 +1,3 @@
{
admin localhost:2999
}
+2
View File
@@ -24,6 +24,8 @@
// 3. Run `go mod init caddy` // 3. Run `go mod init caddy`
// 4. Run `go install` or `go build` - you now have a custom binary! // 4. Run `go install` or `go build` - you now have a custom binary!
// //
// Or you can use xcaddy which does it all for you as a command:
// https://github.com/caddyserver/xcaddy
package main package main
import ( import (
+120
View File
@@ -0,0 +1,120 @@
package caddycmd
import (
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "caddy",
Long: `Caddy is an extensible server platform written in Go.
At its core, Caddy merely manages configuration. Modules are plugged
in statically at compile-time to provide useful functionality. Caddy's
standard distribution includes common modules to serve HTTP, TLS,
and PKI applications, including the automation of certificates.
To run Caddy, use:
- 'caddy run' to run Caddy in the foreground (recommended).
- 'caddy start' to start Caddy in the background; only do this
if you will be keeping the terminal window open until you run
'caddy stop' to close the server.
When Caddy is started, it opens a locally-bound administrative socket
to which configuration can be POSTed via a restful HTTP API (see
https://caddyserver.com/docs/api).
Caddy's native configuration format is JSON. However, config adapters
can be used to convert other config formats to JSON when Caddy receives
its configuration. The Caddyfile is a built-in config adapter that is
popular for hand-written configurations due to its straightforward
syntax (see https://caddyserver.com/docs/caddyfile). Many third-party
adapters are available (see https://caddyserver.com/docs/config-adapters).
Use 'caddy adapt' to see how a config translates to JSON.
For convenience, the CLI can act as an HTTP client to give Caddy its
initial configuration for you. If a file named Caddyfile is in the
current working directory, it will do this automatically. Otherwise,
you can use the --config flag to specify the path to a config file.
Some special-purpose subcommands build and load a configuration file
for you directly from command line input; for example:
- caddy file-server
- caddy reverse-proxy
- caddy respond
These commands disable the administration endpoint because their
configuration is specified solely on the command line.
In general, the most common way to run Caddy is simply:
$ caddy run
Or, with a configuration file:
$ caddy run --config caddy.json
If running interactively in a terminal, running Caddy in the
background may be more convenient:
$ caddy start
...
$ caddy stop
This allows you to run other commands while Caddy stays running.
Be sure to stop Caddy before you close the terminal!
Depending on the system, Caddy may need permission to bind to low
ports. One way to do this on Linux is to use setcap:
$ sudo setcap cap_net_bind_service=+ep $(which caddy)
Remember to run that command again after replacing the binary.
See the Caddy website for tutorials, configuration structure,
syntax, and module documentation: https://caddyserver.com/docs/
Custom Caddy builds are available on the Caddy download page at:
https://caddyserver.com/download
The xcaddy command can be used to build Caddy from source with or
without additional plugins: https://github.com/caddyserver/xcaddy
Where possible, Caddy should be installed using officially-supported
package installers: https://caddyserver.com/docs/install
Instructions for running Caddy in production are also available:
https://caddyserver.com/docs/running
`,
Example: ` $ caddy run
$ caddy run --config caddy.json
$ caddy reload --config caddy.json
$ caddy stop`,
// kind of annoying to have all the help text printed out if
// caddy has an error provisioning its modules, for instance...
SilenceUsage: true,
}
const fullDocsFooter = `Full documentation is available at:
https://caddyserver.com/docs/command-line`
func init() {
rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter)
}
func caddyCmdToCoral(caddyCmd Command) *cobra.Command {
cmd := &cobra.Command{
Use: caddyCmd.Name,
Short: caddyCmd.Short,
Long: caddyCmd.Long,
RunE: func(cmd *cobra.Command, _ []string) error {
fls := cmd.Flags()
_, err := caddyCmd.Func(Flags{fls})
return err
},
}
cmd.Flags().AddGoFlagSet(caddyCmd.Flags)
return cmd
}
+30 -99
View File
@@ -29,7 +29,6 @@ import (
"os/exec" "os/exec"
"runtime" "runtime"
"runtime/debug" "runtime/debug"
"sort"
"strings" "strings"
"github.com/aryann/difflib" "github.com/aryann/difflib"
@@ -280,7 +279,7 @@ func cmdStop(fl Flags) (int, error) {
configFlag := fl.String("config") configFlag := fl.String("config")
configAdapterFlag := fl.String("adapter") configAdapterFlag := fl.String("adapter")
adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag) adminAddr, err := DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
} }
@@ -310,7 +309,7 @@ func cmdReload(fl Flags) (int, error) {
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load") return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
} }
adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag) adminAddr, err := DetermineAdminAPIAddress(addrFlag, config, configFlag, configAdapterFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
} }
@@ -331,30 +330,17 @@ func cmdReload(fl Flags) (int, error) {
} }
func cmdVersion(_ Flags) (int, error) { func cmdVersion(_ Flags) (int, error) {
fmt.Println(CaddyVersion()) _, full := caddy.Version()
fmt.Println(full)
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
func cmdBuildInfo(fl Flags) (int, error) { func cmdBuildInfo(_ Flags) (int, error) {
bi, ok := debug.ReadBuildInfo() bi, ok := debug.ReadBuildInfo()
if !ok { if !ok {
return caddy.ExitCodeFailedStartup, fmt.Errorf("no build information") return caddy.ExitCodeFailedStartup, fmt.Errorf("no build information")
} }
fmt.Println(bi)
fmt.Printf("go_version: %s\n", runtime.Version())
fmt.Printf("go_os: %s\n", runtime.GOOS)
fmt.Printf("go_arch: %s\n", runtime.GOARCH)
fmt.Printf("path: %s\n", bi.Path)
fmt.Printf("main: %s %s %s\n", bi.Main.Path, bi.Main.Version, bi.Main.Sum)
fmt.Println("dependencies:")
for _, goMod := range bi.Deps {
fmt.Printf("%s %s %s", goMod.Path, goMod.Version, goMod.Sum)
if goMod.Replace != nil {
fmt.Printf(" => %s %s %s", goMod.Replace.Path, goMod.Replace.Version, goMod.Replace.Sum)
}
fmt.Println()
}
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
@@ -471,7 +457,7 @@ func cmdAdaptConfig(fl Flags) (int, error) {
fmt.Errorf("reading input file: %v", err) fmt.Errorf("reading input file: %v", err)
} }
opts := map[string]interface{}{"filename": adaptCmdInputFlag} opts := map[string]any{"filename": adaptCmdInputFlag}
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts) adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
if err != nil { if err != nil {
@@ -593,70 +579,6 @@ func cmdFmt(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
func cmdHelp(fl Flags) (int, error) {
const fullDocs = `Full documentation is available at:
https://caddyserver.com/docs/command-line`
args := fl.Args()
if len(args) == 0 {
s := `Caddy is an extensible server platform.
usage:
caddy <command> [<args...>]
commands:
`
keys := make([]string, 0, len(commands))
for k := range commands {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
cmd := commands[k]
short := strings.TrimSuffix(cmd.Short, ".")
s += fmt.Sprintf(" %-15s %s\n", cmd.Name, short)
}
s += "\nUse 'caddy help <command>' for more information about a command.\n"
s += "\n" + fullDocs + "\n"
fmt.Print(s)
return caddy.ExitCodeSuccess, nil
} else if len(args) > 1 {
return caddy.ExitCodeFailedStartup, fmt.Errorf("can only give help with one command")
}
subcommand, ok := commands[args[0]]
if !ok {
return caddy.ExitCodeFailedStartup, fmt.Errorf("unknown command: %s", args[0])
}
helpText := strings.TrimSpace(subcommand.Long)
if helpText == "" {
helpText = subcommand.Short
if !strings.HasSuffix(helpText, ".") {
helpText += "."
}
}
result := fmt.Sprintf("%s\n\nusage:\n caddy %s %s\n",
helpText,
subcommand.Name,
strings.TrimSpace(subcommand.Usage),
)
if help := flagHelp(subcommand.Flags); help != "" {
result += fmt.Sprintf("\nflags:\n%s", help)
}
result += "\n" + fullDocs + "\n"
fmt.Print(result)
return caddy.ExitCodeSuccess, nil
}
// AdminAPIRequest makes an API request according to the CLI flags given, // AdminAPIRequest makes an API request according to the CLI flags given,
// with the given HTTP method and request URI. If body is non-nil, it will // with the given HTTP method and request URI. If body is non-nil, it will
// be assumed to be Content-Type application/json. The caller should close // be assumed to be Content-Type application/json. The caller should close
@@ -732,10 +654,11 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
// DetermineAdminAPIAddress determines which admin API endpoint address should // DetermineAdminAPIAddress determines which admin API endpoint address should
// be used based on the inputs. By priority: if `address` is specified, then // be used based on the inputs. By priority: if `address` is specified, then
// it is returned; if `configFile` (and `configAdapter`) are specified, then that // it is returned; if `config` is specified, then that config will be used for
// config will be loaded to find the admin address; otherwise, the default // finding the admin address; if `configFile` (and `configAdapter`) are specified,
// admin listen address will be returned. // then that config will be loaded to find the admin address; otherwise, the
func DetermineAdminAPIAddress(address, configFile, configAdapter string) (string, error) { // default admin listen address will be returned.
func DetermineAdminAPIAddress(address string, config []byte, configFile, configAdapter string) (string, error) {
// Prefer the address if specified and non-empty // Prefer the address if specified and non-empty
if address != "" { if address != "" {
return address, nil return address, nil
@@ -743,21 +666,29 @@ func DetermineAdminAPIAddress(address, configFile, configAdapter string) (string
// Try to load the config from file if specified, with the given adapter name // Try to load the config from file if specified, with the given adapter name
if configFile != "" { if configFile != "" {
// get the config in caddy's native format var loadedConfigFile string
config, loadedConfigFile, err := LoadConfig(configFile, configAdapter) var err error
if err != nil {
return "", err // use the provided loaded config if non-empty
} // otherwise, load it from the specified file/adapter
if loadedConfigFile == "" { loadedConfig := config
return "", fmt.Errorf("no config file to load") if len(loadedConfig) == 0 {
// get the config in caddy's native format
loadedConfig, loadedConfigFile, err = LoadConfig(configFile, configAdapter)
if err != nil {
return "", err
}
if loadedConfigFile == "" {
return "", fmt.Errorf("no config file to load; either use --config flag or ensure Caddyfile exists in current directory")
}
} }
// get the address of the admin listener if set // get the address of the admin listener from the config
if len(config) > 0 { if len(loadedConfig) > 0 {
var tmpStruct struct { var tmpStruct struct {
Admin caddy.AdminConfig `json:"admin"` Admin caddy.AdminConfig `json:"admin"`
} }
err = json.Unmarshal(config, &tmpStruct) err := json.Unmarshal(loadedConfig, &tmpStruct)
if err != nil { if err != nil {
return "", fmt.Errorf("unmarshaling admin listener address from config: %v", err) return "", fmt.Errorf("unmarshaling admin listener address from config: %v", err)
} }
+140 -14
View File
@@ -16,7 +16,14 @@ package caddycmd
import ( import (
"flag" "flag"
"fmt"
"os"
"regexp" "regexp"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
) )
// Command represents a subcommand. Name, Func, // Command represents a subcommand. Name, Func,
@@ -70,13 +77,6 @@ func Commands() map[string]Command {
var commands = make(map[string]Command) var commands = make(map[string]Command)
func init() { func init() {
RegisterCommand(Command{
Name: "help",
Func: cmdHelp,
Usage: "<command>",
Short: "Shows help for a Caddy subcommand",
})
RegisterCommand(Command{ RegisterCommand(Command{
Name: "start", Name: "start",
Func: cmdStart, Func: cmdStart,
@@ -137,8 +137,8 @@ The --resume flag will override the --config flag if there is a config auto-
save file. It is not an error if --resume is used and no autosave file exists. save file. It is not an error if --resume is used and no autosave file exists.
If --watch is specified, the config file will be loaded automatically after If --watch is specified, the config file will be loaded automatically after
changes. ⚠️ This is dangerous in production! Only use this option in a local changes. ⚠️ This can make unintentional config changes easier; only use this
development environment.`, option in a local development environment.`,
Flags: func() *flag.FlagSet { Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("run", flag.ExitOnError) fs := flag.NewFlagSet("run", flag.ExitOnError)
fs.String("config", "", "Configuration file") fs.String("config", "", "Configuration file")
@@ -200,6 +200,19 @@ config file; otherwise the default is assumed.`,
Name: "version", Name: "version",
Func: cmdVersion, Func: cmdVersion,
Short: "Prints the version", Short: "Prints the version",
Long: `
Prints the version of this Caddy binary.
Version information must be embedded into the binary at compile-time in
order for Caddy to display anything useful with this command. If Caddy
is built from within a version control repository, the Go command will
embed the revision hash if available. However, if Caddy is built in the
way specified by our online documentation (or by using xcaddy), more
detailed version information is printed as given by Go modules.
For more details about the full version string, see the Go module
documentation: https://go.dev/doc/modules/version-numbers
`,
}) })
RegisterCommand(Command{ RegisterCommand(Command{
@@ -226,6 +239,24 @@ config file; otherwise the default is assumed.`,
Name: "environ", Name: "environ",
Func: cmdEnviron, Func: cmdEnviron,
Short: "Prints the environment", Short: "Prints the environment",
Long: `
Prints the environment as seen by this Caddy process.
The environment includes variables set in the system. If your Caddy
configuration uses environment variables (e.g. "{env.VARIABLE}") then
this command can be useful for verifying that the variables will have
the values you expect in your config.
Note that environments may be different depending on how you run Caddy.
Environments for Caddy instances started by service managers such as
systemd are often different than the environment inherited from your
shell or terminal.
You can also print the environment the same time you use "caddy run"
by adding the "--environ" flag.
Environments may contain sensitive data.
`,
}) })
RegisterCommand(Command{ RegisterCommand(Command{
@@ -346,16 +377,111 @@ EXPERIMENTAL: May be changed or removed.
}(), }(),
}) })
RegisterCommand(Command{
Name: "manpage",
Func: func(fl Flags) (int, error) {
dir := strings.TrimSpace(fl.String("directory"))
if dir == "" {
return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
}
if err := os.MkdirAll(dir, 0755); err != nil {
return caddy.ExitCodeFailedQuit, err
}
if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
Title: "Caddy",
Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
}, dir); err != nil {
return caddy.ExitCodeFailedQuit, err
}
return caddy.ExitCodeSuccess, nil
},
Usage: "--directory <path>",
Short: "Generates the manual pages for Caddy commands",
Long: `
Generates the manual pages for Caddy commands into the designated directory
tagged into section 8 (System Administration).
The manual page files are generated into the directory specified by the
argument of --directory. If the directory does not exist, it will be created.
`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("manpage", flag.ExitOnError)
fs.String("directory", "", "The output directory where the manpages are generated")
return fs
}(),
})
// source: https://github.com/spf13/cobra/blob/main/shell_completions.md
rootCmd.AddCommand(&cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: fmt.Sprintf(`To load completions:
Bash:
$ source <(%[1]s completion bash)
# To load completions for each session, execute once:
# Linux:
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
# macOS:
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
Zsh:
# If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
# You will need to start a new shell for this setup to take effect.
fish:
$ %[1]s completion fish | source
# To load completions for each session, execute once:
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
PowerShell:
PS> %[1]s completion powershell | Out-String | Invoke-Expression
# To load completions for every new session, run:
PS> %[1]s completion powershell > %[1]s.ps1
# and source this file from your PowerShell profile.
`, rootCmd.Root().Name()),
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.ExactValidArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
default:
return fmt.Errorf("unrecognized shell: %s", args[0])
}
},
})
} }
// RegisterCommand registers the command cmd. // RegisterCommand registers the command cmd.
// cmd.Name must be unique and conform to the // cmd.Name must be unique and conform to the
// following format: // following format:
// //
// - lowercase // - lowercase
// - alphanumeric and hyphen characters only // - alphanumeric and hyphen characters only
// - cannot start or end with a hyphen // - cannot start or end with a hyphen
// - hyphen cannot be adjacent to another hyphen // - hyphen cannot be adjacent to another hyphen
// //
// This function panics if the name is already registered, // This function panics if the name is already registered,
// if the name does not meet the described format, or if // if the name does not meet the described format, or if
@@ -378,7 +504,7 @@ func RegisterCommand(cmd Command) {
if !commandNameRegex.MatchString(cmd.Name) { if !commandNameRegex.MatchString(cmd.Name) {
panic("invalid command name") panic("invalid command name")
} }
commands[cmd.Name] = cmd rootCmd.AddCommand(caddyCmdToCoral(cmd))
} }
var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`) var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)
+33 -80
View File
@@ -33,60 +33,37 @@ import (
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/spf13/pflag"
"go.uber.org/zap" "go.uber.org/zap"
) )
func init() { func init() {
// set a fitting User-Agent for ACME requests // set a fitting User-Agent for ACME requests
goModule := caddy.GoModule() version, _ := caddy.Version()
cleanModVersion := strings.TrimPrefix(goModule.Version, "v") cleanModVersion := strings.TrimPrefix(version, "v")
certmagic.UserAgent = "Caddy/" + cleanModVersion ua := "Caddy/" + cleanModVersion
if uaEnv, ok := os.LookupEnv("USERAGENT"); ok {
ua = uaEnv + " " + ua
}
certmagic.UserAgent = ua
// by using Caddy, user indicates agreement to CA terms // by using Caddy, user indicates agreement to CA terms
// (very important, or ACME account creation will fail!) // (very important, as Caddy is often non-interactive
// and thus ACME account creation will fail!)
certmagic.DefaultACME.Agreed = true certmagic.DefaultACME.Agreed = true
} }
// Main implements the main function of the caddy command. // Main implements the main function of the caddy command.
// Call this if Caddy is to be the main() of your program. // Call this if Caddy is to be the main() of your program.
func Main() { func Main() {
switch len(os.Args) { if len(os.Args) == 0 {
case 0:
fmt.Printf("[FATAL] no arguments provided by OS; args[0] must be command\n") fmt.Printf("[FATAL] no arguments provided by OS; args[0] must be command\n")
os.Exit(caddy.ExitCodeFailedStartup) os.Exit(caddy.ExitCodeFailedStartup)
case 1:
os.Args = append(os.Args, "help")
} }
subcommandName := os.Args[1] if err := rootCmd.Execute(); err != nil {
subcommand, ok := commands[subcommandName] os.Exit(1)
if !ok {
if strings.HasPrefix(os.Args[1], "-") {
// user probably forgot to type the subcommand
fmt.Println("[ERROR] first argument must be a subcommand; see 'caddy help'")
} else {
fmt.Printf("[ERROR] '%s' is not a recognized subcommand; see 'caddy help'\n", os.Args[1])
}
os.Exit(caddy.ExitCodeFailedStartup)
} }
fs := subcommand.Flags
if fs == nil {
fs = flag.NewFlagSet(subcommand.Name, flag.ExitOnError)
}
err := fs.Parse(os.Args[2:])
if err != nil {
fmt.Println(err)
os.Exit(caddy.ExitCodeFailedStartup)
}
exitCode, err := subcommand.Func(Flags{fs})
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", subcommand.Name, err)
}
os.Exit(exitCode)
} }
// handlePingbackConn reads from conn and ensures it matches // handlePingbackConn reads from conn and ensures it matches
@@ -173,7 +150,7 @@ func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
// adapt config // adapt config
if cfgAdapter != nil { if cfgAdapter != nil {
adaptedConfig, warnings, err := cfgAdapter.Adapt(config, map[string]interface{}{ adaptedConfig, warnings, err := cfgAdapter.Adapt(config, map[string]any{
"filename": configFile, "filename": configFile,
}) })
if err != nil { if err != nil {
@@ -280,7 +257,7 @@ func watchConfigFile(filename, adapterName string) {
// Flags wraps a FlagSet so that typed values // Flags wraps a FlagSet so that typed values
// from flags can be easily retrieved. // from flags can be easily retrieved.
type Flags struct { type Flags struct {
*flag.FlagSet *pflag.FlagSet
} }
// String returns the string representation of the // String returns the string representation of the
@@ -326,22 +303,6 @@ func (f Flags) Duration(name string) time.Duration {
return val return val
} }
// flagHelp returns the help text for fs.
func flagHelp(fs *flag.FlagSet) string {
if fs == nil {
return ""
}
// temporarily redirect output
out := fs.Output()
defer fs.SetOutput(out)
buf := new(bytes.Buffer)
fs.SetOutput(buf)
fs.PrintDefaults()
return buf.String()
}
func loadEnvFromFile(envFile string) error { func loadEnvFromFile(envFile string) error {
file, err := os.Open(envFile) file, err := os.Open(envFile)
if err != nil { if err != nil {
@@ -387,11 +348,11 @@ func parseEnvFile(envInput io.Reader) (map[string]string, error) {
} }
// split line into key and value // split line into key and value
fields := strings.SplitN(line, "=", 2) before, after, isCut := strings.Cut(line, "=")
if len(fields) != 2 { if !isCut {
return nil, fmt.Errorf("can't parse line %d; line should be in KEY=VALUE format", lineNumber) return nil, fmt.Errorf("can't parse line %d; line should be in KEY=VALUE format", lineNumber)
} }
key, val := fields[0], fields[1] key, val := before, after
// sometimes keys are prefixed by "export " so file can be sourced in bash; ignore it here // sometimes keys are prefixed by "export " so file can be sourced in bash; ignore it here
key = strings.TrimPrefix(key, "export ") key = strings.TrimPrefix(key, "export ")
@@ -408,11 +369,8 @@ func parseEnvFile(envInput io.Reader) (map[string]string, error) {
} }
// remove any trailing comment after value // remove any trailing comment after value
if commentStart := strings.Index(val, "#"); commentStart > 0 { if commentStart, _, found := strings.Cut(val, "#"); found {
before := val[commentStart-1] val = strings.TrimRight(commentStart, " \t")
if before == '\t' || before == ' ' {
val = strings.TrimRight(val[:commentStart], " \t")
}
} }
// quoted value: support newlines // quoted value: support newlines
@@ -441,11 +399,12 @@ func parseEnvFile(envInput io.Reader) (map[string]string, error) {
} }
func printEnvironment() { func printEnvironment() {
_, version := caddy.Version()
fmt.Printf("caddy.HomeDir=%s\n", caddy.HomeDir()) fmt.Printf("caddy.HomeDir=%s\n", caddy.HomeDir())
fmt.Printf("caddy.AppDataDir=%s\n", caddy.AppDataDir()) fmt.Printf("caddy.AppDataDir=%s\n", caddy.AppDataDir())
fmt.Printf("caddy.AppConfigDir=%s\n", caddy.AppConfigDir()) fmt.Printf("caddy.AppConfigDir=%s\n", caddy.AppConfigDir())
fmt.Printf("caddy.ConfigAutosavePath=%s\n", caddy.ConfigAutosavePath) fmt.Printf("caddy.ConfigAutosavePath=%s\n", caddy.ConfigAutosavePath)
fmt.Printf("caddy.Version=%s\n", CaddyVersion()) fmt.Printf("caddy.Version=%s\n", version)
fmt.Printf("runtime.GOOS=%s\n", runtime.GOOS) fmt.Printf("runtime.GOOS=%s\n", runtime.GOOS)
fmt.Printf("runtime.GOARCH=%s\n", runtime.GOARCH) fmt.Printf("runtime.GOARCH=%s\n", runtime.GOARCH)
fmt.Printf("runtime.Compiler=%s\n", runtime.Compiler) fmt.Printf("runtime.Compiler=%s\n", runtime.Compiler)
@@ -462,21 +421,15 @@ func printEnvironment() {
} }
} }
// CaddyVersion returns a detailed version string, if available. // StringSlice is a flag.Value that enables repeated use of a string flag.
func CaddyVersion() string { type StringSlice []string
goModule := caddy.GoModule()
ver := goModule.Version func (ss StringSlice) String() string { return "[" + strings.Join(ss, ", ") + "]" }
if goModule.Sum != "" {
ver += " " + goModule.Sum func (ss *StringSlice) Set(value string) error {
} *ss = append(*ss, value)
if goModule.Replace != nil { return nil
ver += " => " + goModule.Replace.Path
if goModule.Replace.Version != "" {
ver += "@" + goModule.Replace.Version
}
if goModule.Replace.Sum != "" {
ver += " " + goModule.Replace.Sum
}
}
return ver
} }
// Interface guard
var _ flag.Value = (*StringSlice)(nil)
+1 -1
View File
@@ -194,7 +194,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
// can use reflection but we need a non-pointer value (I'm // can use reflection but we need a non-pointer value (I'm
// not sure why), and since New() should return a pointer // not sure why), and since New() should return a pointer
// value, we need to dereference it first // value, we need to dereference it first
iface := interface{}(modInfo.New()) iface := any(modInfo.New())
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr { if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface() iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
} }
-1
View File
@@ -13,7 +13,6 @@
// limitations under the License. // limitations under the License.
//go:build !windows //go:build !windows
// +build !windows
package caddycmd package caddycmd
+4 -1
View File
@@ -31,6 +31,9 @@ import (
func removeCaddyBinary(path string) error { func removeCaddyBinary(path string) error {
var sI syscall.StartupInfo var sI syscall.StartupInfo
var pI syscall.ProcessInformation var pI syscall.ProcessInformation
argv := syscall.StringToUTF16Ptr(filepath.Join(os.Getenv("windir"), "system32", "cmd.exe") + " /C del " + path) argv, err := syscall.UTF16PtrFromString(filepath.Join(os.Getenv("windir"), "system32", "cmd.exe") + " /C del " + path)
if err != nil {
return err
}
return syscall.CreateProcess(nil, argv, nil, nil, true, 0, nil, nil, &sI, &pI) return syscall.CreateProcess(nil, argv, nil, nil, true, 0, nil, nil, &sI, &pI)
} }
+79 -36
View File
@@ -37,9 +37,10 @@ import (
// not actually need to do this). // not actually need to do this).
type Context struct { type Context struct {
context.Context context.Context
moduleInstances map[string][]interface{} moduleInstances map[string][]Module
cfg *Config cfg *Config
cleanupFuncs []func() cleanupFuncs []func()
ancestry []Module
} }
// NewContext provides a new context derived from the given // NewContext provides a new context derived from the given
@@ -51,7 +52,7 @@ type Context struct {
// modules which are loaded will be properly unloaded. // modules which are loaded will be properly unloaded.
// See standard library context package's documentation. // See standard library context package's documentation.
func NewContext(ctx Context) (Context, context.CancelFunc) { func NewContext(ctx Context) (Context, context.CancelFunc) {
newCtx := Context{moduleInstances: make(map[string][]interface{}), cfg: ctx.cfg} newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg}
c, cancel := context.WithCancel(ctx.Context) c, cancel := context.WithCancel(ctx.Context)
wrappedCancel := func() { wrappedCancel := func() {
cancel() cancel()
@@ -90,15 +91,15 @@ func (ctx *Context) OnCancel(f func()) {
// ModuleMap may be used in place of map[string]json.RawMessage. The return value's // ModuleMap may be used in place of map[string]json.RawMessage. The return value's
// underlying type mirrors the input field's type: // underlying type mirrors the input field's type:
// //
// json.RawMessage => interface{} // json.RawMessage => any
// []json.RawMessage => []interface{} // []json.RawMessage => []any
// [][]json.RawMessage => [][]interface{} // [][]json.RawMessage => [][]any
// map[string]json.RawMessage => map[string]interface{} // map[string]json.RawMessage => map[string]any
// []map[string]json.RawMessage => []map[string]interface{} // []map[string]json.RawMessage => []map[string]any
// //
// The field must have a "caddy" struct tag in this format: // The field must have a "caddy" struct tag in this format:
// //
// caddy:"key1=val1 key2=val2" // caddy:"key1=val1 key2=val2"
// //
// To load modules, a "namespace" key is required. For example, to load modules // To load modules, a "namespace" key is required. For example, to load modules
// in the "http.handlers" namespace, you'd put: `namespace=http.handlers` in the // in the "http.handlers" namespace, you'd put: `namespace=http.handlers` in the
@@ -115,20 +116,20 @@ func (ctx *Context) OnCancel(f func()) {
// meaning the key containing the module's name that is defined inline with the module // meaning the key containing the module's name that is defined inline with the module
// itself. You must specify the inline key in a struct tag, along with the namespace: // itself. You must specify the inline key in a struct tag, along with the namespace:
// //
// caddy:"namespace=http.handlers inline_key=handler" // caddy:"namespace=http.handlers inline_key=handler"
// //
// This will look for a key/value pair like `"handler": "..."` in the json.RawMessage // This will look for a key/value pair like `"handler": "..."` in the json.RawMessage
// in order to know the module name. // in order to know the module name.
// //
// To make use of the loaded module(s) (the return value), you will probably want // To make use of the loaded module(s) (the return value), you will probably want
// to type-assert each interface{} value(s) to the types that are useful to you // to type-assert each 'any' value(s) to the types that are useful to you
// and store them on the same struct. Storing them on the same struct makes for // and store them on the same struct. Storing them on the same struct makes for
// easy garbage collection when your host module is no longer needed. // easy garbage collection when your host module is no longer needed.
// //
// Loaded modules have already been provisioned and validated. Upon returning // Loaded modules have already been provisioned and validated. Upon returning
// successfully, this method clears the json.RawMessage(s) in the field since // successfully, this method clears the json.RawMessage(s) in the field since
// the raw JSON is no longer needed, and this allows the GC to free up memory. // the raw JSON is no longer needed, and this allows the GC to free up memory.
func (ctx Context) LoadModule(structPointer interface{}, fieldName string) (interface{}, error) { func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error) {
val := reflect.ValueOf(structPointer).Elem().FieldByName(fieldName) val := reflect.ValueOf(structPointer).Elem().FieldByName(fieldName)
typ := val.Type() typ := val.Type()
@@ -148,7 +149,7 @@ func (ctx Context) LoadModule(structPointer interface{}, fieldName string) (inte
} }
inlineModuleKey := opts["inline_key"] inlineModuleKey := opts["inline_key"]
var result interface{} var result any
switch val.Kind() { switch val.Kind() {
case reflect.Slice: case reflect.Slice:
@@ -170,7 +171,7 @@ func (ctx Context) LoadModule(structPointer interface{}, fieldName string) (inte
if inlineModuleKey == "" { if inlineModuleKey == "" {
panic("unable to determine module name without inline_key because type is not a ModuleMap") panic("unable to determine module name without inline_key because type is not a ModuleMap")
} }
var all []interface{} var all []any
for i := 0; i < val.Len(); i++ { for i := 0; i < val.Len(); i++ {
val, err := ctx.loadModuleInline(inlineModuleKey, moduleNamespace, val.Index(i).Interface().(json.RawMessage)) val, err := ctx.loadModuleInline(inlineModuleKey, moduleNamespace, val.Index(i).Interface().(json.RawMessage))
if err != nil { if err != nil {
@@ -186,10 +187,10 @@ func (ctx Context) LoadModule(structPointer interface{}, fieldName string) (inte
if inlineModuleKey == "" { if inlineModuleKey == "" {
panic("unable to determine module name without inline_key because type is not a ModuleMap") panic("unable to determine module name without inline_key because type is not a ModuleMap")
} }
var all [][]interface{} var all [][]any
for i := 0; i < val.Len(); i++ { for i := 0; i < val.Len(); i++ {
innerVal := val.Index(i) innerVal := val.Index(i)
var allInner []interface{} var allInner []any
for j := 0; j < innerVal.Len(); j++ { for j := 0; j < innerVal.Len(); j++ {
innerInnerVal, err := ctx.loadModuleInline(inlineModuleKey, moduleNamespace, innerVal.Index(j).Interface().(json.RawMessage)) innerInnerVal, err := ctx.loadModuleInline(inlineModuleKey, moduleNamespace, innerVal.Index(j).Interface().(json.RawMessage))
if err != nil { if err != nil {
@@ -204,7 +205,7 @@ func (ctx Context) LoadModule(structPointer interface{}, fieldName string) (inte
} else if isModuleMapType(typ.Elem()) { } else if isModuleMapType(typ.Elem()) {
// val is `[]map[string]json.RawMessage` // val is `[]map[string]json.RawMessage`
var all []map[string]interface{} var all []map[string]any
for i := 0; i < val.Len(); i++ { for i := 0; i < val.Len(); i++ {
thisSet, err := ctx.loadModulesFromSomeMap(moduleNamespace, inlineModuleKey, val.Index(i)) thisSet, err := ctx.loadModulesFromSomeMap(moduleNamespace, inlineModuleKey, val.Index(i))
if err != nil { if err != nil {
@@ -232,10 +233,10 @@ func (ctx Context) LoadModule(structPointer interface{}, fieldName string) (inte
return result, nil return result, nil
} }
// loadModulesFromSomeMap loads modules from val, which must be a type of map[string]interface{}. // loadModulesFromSomeMap loads modules from val, which must be a type of map[string]any.
// Depending on inlineModuleKey, it will be interpreted as either a ModuleMap (key is the module // Depending on inlineModuleKey, it will be interpreted as either a ModuleMap (key is the module
// name) or as a regular map (key is not the module name, and module name is defined inline). // name) or as a regular map (key is not the module name, and module name is defined inline).
func (ctx Context) loadModulesFromSomeMap(namespace, inlineModuleKey string, val reflect.Value) (map[string]interface{}, error) { func (ctx Context) loadModulesFromSomeMap(namespace, inlineModuleKey string, val reflect.Value) (map[string]any, error) {
// if no inline_key is specified, then val must be a ModuleMap, // if no inline_key is specified, then val must be a ModuleMap,
// where the key is the module name // where the key is the module name
if inlineModuleKey == "" { if inlineModuleKey == "" {
@@ -253,8 +254,8 @@ func (ctx Context) loadModulesFromSomeMap(namespace, inlineModuleKey string, val
// loadModulesFromRegularMap loads modules from val, where val is a map[string]json.RawMessage. // loadModulesFromRegularMap loads modules from val, where val is a map[string]json.RawMessage.
// Map keys are NOT interpreted as module names, so module names are still expected to appear // Map keys are NOT interpreted as module names, so module names are still expected to appear
// inline with the objects. // inline with the objects.
func (ctx Context) loadModulesFromRegularMap(namespace, inlineModuleKey string, val reflect.Value) (map[string]interface{}, error) { func (ctx Context) loadModulesFromRegularMap(namespace, inlineModuleKey string, val reflect.Value) (map[string]any, error) {
mods := make(map[string]interface{}) mods := make(map[string]any)
iter := val.MapRange() iter := val.MapRange()
for iter.Next() { for iter.Next() {
k := iter.Key() k := iter.Key()
@@ -268,10 +269,10 @@ func (ctx Context) loadModulesFromRegularMap(namespace, inlineModuleKey string,
return mods, nil return mods, nil
} }
// loadModuleMap loads modules from a ModuleMap, i.e. map[string]interface{}, where the key is the // loadModuleMap loads modules from a ModuleMap, i.e. map[string]any, where the key is the
// module name. With a module map, module names do not need to be defined inline with their values. // module name. With a module map, module names do not need to be defined inline with their values.
func (ctx Context) loadModuleMap(namespace string, val reflect.Value) (map[string]interface{}, error) { func (ctx Context) loadModuleMap(namespace string, val reflect.Value) (map[string]any, error) {
all := make(map[string]interface{}) all := make(map[string]any)
iter := val.MapRange() iter := val.MapRange()
for iter.Next() { for iter.Next() {
k := iter.Key().Interface().(string) k := iter.Key().Interface().(string)
@@ -299,19 +300,19 @@ func (ctx Context) loadModuleMap(namespace string, val reflect.Value) (map[strin
// directly by most modules. However, this method is useful when // directly by most modules. However, this method is useful when
// dynamically loading/unloading modules in their own context, // dynamically loading/unloading modules in their own context,
// like from embedded scripts, etc. // like from embedded scripts, etc.
func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{}, error) { func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error) {
modulesMu.RLock() modulesMu.RLock()
mod, ok := modules[id] modInfo, ok := modules[id]
modulesMu.RUnlock() modulesMu.RUnlock()
if !ok { if !ok {
return nil, fmt.Errorf("unknown module: %s", id) return nil, fmt.Errorf("unknown module: %s", id)
} }
if mod.New == nil { if modInfo.New == nil {
return nil, fmt.Errorf("module '%s' has no constructor", mod.ID) return nil, fmt.Errorf("module '%s' has no constructor", modInfo.ID)
} }
val := mod.New().(interface{}) val := modInfo.New()
// value must be a pointer for unmarshaling into concrete type, even if // value must be a pointer for unmarshaling into concrete type, even if
// the module's concrete type is a slice or map; New() *should* return // the module's concrete type is a slice or map; New() *should* return
@@ -327,7 +328,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{
if len(rawMsg) > 0 { if len(rawMsg) > 0 {
err := strictUnmarshalJSON(rawMsg, &val) err := strictUnmarshalJSON(rawMsg, &val)
if err != nil { if err != nil {
return nil, fmt.Errorf("decoding module config: %s: %v", mod, err) return nil, fmt.Errorf("decoding module config: %s: %v", modInfo, err)
} }
} }
@@ -340,6 +341,8 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{
return nil, fmt.Errorf("module value cannot be null") return nil, fmt.Errorf("module value cannot be null")
} }
ctx.ancestry = append(ctx.ancestry, val)
if prov, ok := val.(Provisioner); ok { if prov, ok := val.(Provisioner); ok {
err := prov.Provision(ctx) err := prov.Provision(ctx)
if err != nil { if err != nil {
@@ -351,7 +354,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{
err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2) err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2)
} }
} }
return nil, fmt.Errorf("provision %s: %v", mod, err) return nil, fmt.Errorf("provision %s: %v", modInfo, err)
} }
} }
@@ -365,7 +368,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{
err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2) err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2)
} }
} }
return nil, fmt.Errorf("%s: invalid configuration: %v", mod, err) return nil, fmt.Errorf("%s: invalid configuration: %v", modInfo, err)
} }
} }
@@ -375,7 +378,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{
} }
// loadModuleInline loads a module from a JSON raw message which decodes to // loadModuleInline loads a module from a JSON raw message which decodes to
// a map[string]interface{}, where one of the object keys is moduleNameKey // a map[string]any, where one of the object keys is moduleNameKey
// and the corresponding value is the module name (as a string) which can // and the corresponding value is the module name (as a string) which can
// be found in the given scope. In other words, the module name is declared // be found in the given scope. In other words, the module name is declared
// in-line with the module itself. // in-line with the module itself.
@@ -385,7 +388,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{
// multiple instances in the map or it appears in an array (where there are // multiple instances in the map or it appears in an array (where there are
// no custom keys). In other words, the key containing the module name is // no custom keys). In other words, the key containing the module name is
// treated special/separate from all the other keys in the object. // treated special/separate from all the other keys in the object.
func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.RawMessage) (interface{}, error) { func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.RawMessage) (any, error) {
moduleName, raw, err := getModuleNameInline(moduleNameKey, raw) moduleName, raw, err := getModuleNameInline(moduleNameKey, raw)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -407,7 +410,7 @@ func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.
// called during the Provision/Validate phase to reference a // called during the Provision/Validate phase to reference a
// module's own host app (since the parent app module is still // module's own host app (since the parent app module is still
// in the process of being provisioned, it is not yet ready). // in the process of being provisioned, it is not yet ready).
func (ctx Context) App(name string) (interface{}, error) { func (ctx Context) App(name string) (any, error) {
if app, ok := ctx.cfg.apps[name]; ok { if app, ok := ctx.cfg.apps[name]; ok {
return app, nil return app, nil
} }
@@ -439,8 +442,27 @@ func (ctx Context) Storage() certmagic.Storage {
return ctx.cfg.storage return ctx.cfg.storage
} }
// Logger returns a logger that can be used by mod. // Logger returns a logger that is intended for use by the most
func (ctx Context) Logger(mod Module) *zap.Logger { // recent module associated with the context. Callers should not
// pass in any arguments unless they want to associate with a
// different module; it panics if more than 1 value is passed in.
//
// Originally, this method's signature was `Logger(mod Module)`,
// requiring that an instance of a Caddy module be passed in.
// However, that is no longer necessary, as the closest module
// most recently associated with the context will be automatically
// assumed. To prevent a sudden breaking change, this method's
// signature has been changed to be variadic, but we may remove
// the parameter altogether in the future. Callers should not
// pass in any argument. If there is valid need to specify a
// different module, please open an issue to discuss.
//
// PARTIALLY DEPRECATED: The Logger(module) form is deprecated and
// may be removed in the future. Do not pass in any arguments.
func (ctx Context) Logger(module ...Module) *zap.Logger {
if len(module) > 1 {
panic("more than 1 module passed in")
}
if ctx.cfg == nil { if ctx.cfg == nil {
// often the case in tests; just use a dev logger // often the case in tests; just use a dev logger
l, err := zap.NewDevelopment() l, err := zap.NewDevelopment()
@@ -449,5 +471,26 @@ func (ctx Context) Logger(mod Module) *zap.Logger {
} }
return l return l
} }
mod := ctx.Module()
if len(module) > 0 {
mod = module[0]
}
return ctx.cfg.Logging.Logger(mod) return ctx.cfg.Logging.Logger(mod)
} }
// Modules returns the lineage of modules that this context provisioned,
// with the most recent/current module being last in the list.
func (ctx Context) Modules() []Module {
mods := make([]Module, len(ctx.ancestry))
copy(mods, ctx.ancestry)
return mods
}
// Module returns the current module, or the most recent one
// provisioned by the context.
func (ctx Context) Module() Module {
if len(ctx.ancestry) == 0 {
return nil
}
return ctx.ancestry[len(ctx.ancestry)-1]
}
+4 -4
View File
@@ -71,13 +71,13 @@ func ExampleContext_LoadModule_array() {
}, },
} }
// since our input is []json.RawMessage, the output will be []interface{} // since our input is []json.RawMessage, the output will be []any
mods, err := ctx.LoadModule(myStruct, "GuestModulesRaw") mods, err := ctx.LoadModule(myStruct, "GuestModulesRaw")
if err != nil { if err != nil {
// you'd want to actually handle the error here // you'd want to actually handle the error here
// return fmt.Errorf("loading guest modules: %v", err) // return fmt.Errorf("loading guest modules: %v", err)
} }
for _, mod := range mods.([]interface{}) { for _, mod := range mods.([]any) {
myStruct.guestModules = append(myStruct.guestModules, mod.(io.Writer)) myStruct.guestModules = append(myStruct.guestModules, mod.(io.Writer))
} }
@@ -104,13 +104,13 @@ func ExampleContext_LoadModule_map() {
}, },
} }
// since our input is map[string]json.RawMessage, the output will be map[string]interface{} // since our input is map[string]json.RawMessage, the output will be map[string]any
mods, err := ctx.LoadModule(myStruct, "GuestModulesRaw") mods, err := ctx.LoadModule(myStruct, "GuestModulesRaw")
if err != nil { if err != nil {
// you'd want to actually handle the error here // you'd want to actually handle the error here
// return fmt.Errorf("loading guest modules: %v", err) // return fmt.Errorf("loading guest modules: %v", err)
} }
for modName, mod := range mods.(map[string]interface{}) { for modName, mod := range mods.(map[string]any) {
myStruct.guestModules[modName] = mod.(io.Writer) myStruct.guestModules[modName] = mod.(io.Writer)
} }
-1
View File
@@ -13,7 +13,6 @@
// limitations under the License. // limitations under the License.
//go:build gofuzz //go:build gofuzz
// +build gofuzz
package caddy package caddy
+49 -43
View File
@@ -1,41 +1,48 @@
module github.com/caddyserver/caddy/v2 module github.com/caddyserver/caddy/v2
go 1.17 go 1.18
require ( require (
github.com/BurntSushi/toml v1.0.0 github.com/BurntSushi/toml v1.2.0
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.16.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.7.3 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.0 github.com/klauspost/compress v1.15.11
github.com/klauspost/cpuid/v2 v2.0.11 github.com/klauspost/cpuid/v2 v2.1.1
github.com/lucas-clemente/quic-go v0.26.0 github.com/lucas-clemente/quic-go v0.29.2
github.com/mholt/acmez v1.0.2 github.com/mholt/acmez v1.0.4
github.com/prometheus/client_golang v1.12.1 github.com/prometheus/client_golang v1.12.2
github.com/smallstep/certificates v0.19.0 github.com/smallstep/certificates v0.22.1
github.com/smallstep/cli v0.18.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.11.0 github.com/smallstep/truststore v0.12.0
github.com/tailscale/tscert v0.0.0-20220125204807-4509a5fbaf74 github.com/spf13/cobra v1.5.0
github.com/yuin/goldmark v1.4.8 github.com/spf13/pflag v1.0.5
github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2
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.29.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0
go.opentelemetry.io/otel v1.4.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-20220210151621-f4118a5b28e2 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd 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-20220222213610-43724f9ea8cf google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad
google.golang.org/protobuf v1.27.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/golang/mock v1.6.0 // indirect
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
) )
require ( require (
@@ -43,24 +50,23 @@ require (
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f // indirect github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.2 // indirect github.com/cenkalti/backoff/v4 v4.1.2 // indirect
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/cheekybits/genny v1.0.0 // 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.0 // 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
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/felixge/httpsnoop v1.0.2 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-kit/kit v0.10.0 // indirect github.com/go-kit/kit v0.10.0 // indirect
github.com/go-logfmt/logfmt v0.5.0 // indirect github.com/go-logfmt/logfmt v0.5.0 // indirect
github.com/go-logr/logr v1.2.2 // indirect github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
@@ -69,6 +75,7 @@ require (
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.10.1 // indirect github.com/jackc/pgconn v1.10.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgio v1.0.0 // indirect
@@ -80,15 +87,14 @@ 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-16 v0.1.5 // indirect github.com/marten-seemann/qtls-go1-18 v0.1.3 // indirect
github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect github.com/marten-seemann/qtls-go1-19 v0.1.1 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-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
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/micromdm/scep/v2 v2.1.0 // indirect github.com/micromdm/scep/v2 v2.1.0 // indirect
github.com/miekg/dns v1.1.46 // indirect github.com/miekg/dns v1.1.50 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect
@@ -99,7 +105,7 @@ require (
github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect github.com/prometheus/procfs v0.7.3 // indirect
github.com/rs/xid v1.2.1 // indirect github.com/rs/xid v1.2.1 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect github.com/sirupsen/logrus v1.8.1 // indirect
@@ -111,21 +117,21 @@ require (
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.0 // indirect go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.0 // indirect
go.opentelemetry.io/otel/internal/metric v0.27.0 // indirect go.opentelemetry.io/otel/metric v0.31.0 // indirect
go.opentelemetry.io/otel/metric v0.27.0 // indirect go.opentelemetry.io/otel/trace v1.9.0 // indirect
go.opentelemetry.io/otel/trace v1.4.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.0 // indirect go.step.sm/cli-utils v0.7.4 // indirect
go.step.sm/crypto v0.16.1 // indirect go.step.sm/crypto v0.18.0 // indirect
go.step.sm/linkedca v0.15.0 // 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.4.2 // indirect golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect 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.7 // 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.44.0 // indirect google.golang.org/grpc v1.47.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/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
howett.net/plist v1.0.0 // indirect howett.net/plist v1.0.0 // indirect
+101 -832
View File
File diff suppressed because it is too large Load Diff
+177
View File
@@ -0,0 +1,177 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// TODO: Go 1.19 introduced the "unix" build tag. We have to support Go 1.18 until Go 1.20 is released.
// When Go 1.19 is our minimum, change this build tag to simply "!unix".
// (see similar change needed in listen_unix.go)
//go:build !(aix || android || darwin || dragonfly || freebsd || hurd || illumos || ios || linux || netbsd || openbsd || solaris)
package caddy
import (
"context"
"net"
"sync"
"sync/atomic"
"time"
"go.uber.org/zap"
)
func reuseUnixSocket(network, addr string) (any, error) {
return nil, nil
}
func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) {
sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
ln, err := config.Listen(ctx, network, address)
if err != nil {
return nil, err
}
return &sharedListener{Listener: ln, key: lnKey}, nil
})
if err != nil {
return nil, err
}
return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil
}
// fakeCloseListener is a private wrapper over a listener that
// is shared. The state of fakeCloseListener is not shared.
// This allows one user of a socket to "close" the listener
// while in reality the socket stays open for other users of
// the listener. In this way, servers become hot-swappable
// while the listener remains running. Listeners should be
// re-wrapped in a new fakeCloseListener each time the listener
// is reused. This type is atomic and values must not be copied.
type fakeCloseListener struct {
closed int32 // accessed atomically; belongs to this struct only
*sharedListener // embedded, so we also become a net.Listener
keepAlivePeriod time.Duration
}
type canSetKeepAlive interface {
SetKeepAlivePeriod(d time.Duration) error
SetKeepAlive(bool) error
}
func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
// if the listener is already "closed", return error
if atomic.LoadInt32(&fcl.closed) == 1 {
return nil, fakeClosedErr(fcl)
}
// call underlying accept
conn, err := fcl.sharedListener.Accept()
if err == nil {
// if 0, do nothing, Go's default is already set
// and if the connection allows setting KeepAlive, set it
if tconn, ok := conn.(canSetKeepAlive); ok && fcl.keepAlivePeriod != 0 {
if fcl.keepAlivePeriod > 0 {
err = tconn.SetKeepAlivePeriod(fcl.keepAlivePeriod)
} else { // negative
err = tconn.SetKeepAlive(false)
}
if err != nil {
Log().With(zap.String("server", fcl.sharedListener.key)).Warn("unable to set keepalive for new connection:", zap.Error(err))
}
}
return conn, nil
}
// since Accept() returned an error, it may be because our reference to
// the listener (this fakeCloseListener) may have been closed, i.e. the
// server is shutting down; in that case, we need to clear the deadline
// that we set when Close() was called, and return a non-temporary and
// non-timeout error value to the caller, masking the "true" error, so
// that server loops / goroutines won't retry, linger, and leak
if atomic.LoadInt32(&fcl.closed) == 1 {
// we dereference the sharedListener explicitly even though it's embedded
// so that it's clear in the code that side-effects are shared with other
// users of this listener, not just our own reference to it; we also don't
// do anything with the error because all we could do is log it, but we
// expliclty assign it to nothing so we don't forget it's there if needed
_ = fcl.sharedListener.clearDeadline()
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
return nil, fakeClosedErr(fcl)
}
}
return nil, err
}
// Close stops accepting new connections without closing the
// underlying listener. The underlying listener is only closed
// if the caller is the last known user of the socket.
func (fcl *fakeCloseListener) Close() error {
if atomic.CompareAndSwapInt32(&fcl.closed, 0, 1) {
// There are two ways I know of to get an Accept()
// function to return to the server loop that called
// it: close the listener, or set a deadline in the
// past. Obviously, we can't close the socket yet
// since others may be using it (hence this whole
// file). But we can set the deadline in the past,
// and this is kind of cheating, but it works, and
// it apparently even works on Windows.
_ = fcl.sharedListener.setDeadline()
_, _ = listenerPool.Delete(fcl.sharedListener.key)
}
return nil
}
// sharedListener is a wrapper over an underlying listener. The listener
// and the other fields on the struct are shared state that is synchronized,
// so sharedListener structs must never be copied (always use a pointer).
type sharedListener struct {
net.Listener
key string // uniquely identifies this listener
deadline bool // whether a deadline is currently set
deadlineMu sync.Mutex
}
func (sl *sharedListener) clearDeadline() error {
var err error
sl.deadlineMu.Lock()
if sl.deadline {
switch ln := sl.Listener.(type) {
case *net.TCPListener:
err = ln.SetDeadline(time.Time{})
}
sl.deadline = false
}
sl.deadlineMu.Unlock()
return err
}
func (sl *sharedListener) setDeadline() error {
timeInPast := time.Now().Add(-1 * time.Minute)
var err error
sl.deadlineMu.Lock()
if !sl.deadline {
switch ln := sl.Listener.(type) {
case *net.TCPListener:
err = ln.SetDeadline(timeInPast)
}
sl.deadline = true
}
sl.deadlineMu.Unlock()
return err
}
// Destruct is called by the UsagePool when the listener is
// finally not being used anymore. It closes the socket.
func (sl *sharedListener) Destruct() error {
return sl.Listener.Close()
}
+118
View File
@@ -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))
}
})
}
+553 -363
View File
@@ -19,7 +19,9 @@ import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"net/netip"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@@ -30,179 +32,477 @@ import (
"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"
) )
// 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) {
lnKey := network + "/" + addr
sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { // The "main" part of the network address is the host, which
ln, err := net.Listen(network, addr) // often takes the form of a hostname, DNS name, IP address,
if err != nil { // or socket path.
// https://github.com/caddyserver/caddy/pull/4534 Host string
if isUnixNetwork(network) && isListenBindAddressAlreadyInUseError(err) {
return nil, fmt.Errorf("%w: this can happen if Caddy was forcefully killed", err)
}
return nil, err
}
return &sharedListener{Listener: ln, key: lnKey}, nil
})
if err != nil {
return nil, err
}
return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener)}, nil // 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
} }
// ListenPacket returns a net.PacketConn suitable for use in a Caddy module. // ListenAll calls Listen() for all addresses represented by this struct, i.e. all ports in the range.
// It is like Listen except for PacketConns. // (If the address doesn't use ports or has 1 port only, then only 1 listener will be created.)
// Always be sure to close the PacketConn when you are done. // It returns an error if any listener failed to bind, and closes any listeners opened up to that point.
func ListenPacket(network, addr string) (net.PacketConn, error) { //
lnKey := network + "/" + addr // 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
sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { // if one of the addresses has a failure, we need to close
pc, err := net.ListenPacket(network, addr) // any that did open a socket to avoid leaking resources
if err != nil { defer func() {
// https://github.com/caddyserver/caddy/pull/4534 if err == nil {
if isUnixNetwork(network) && isListenBindAddressAlreadyInUseError(err) { return
return nil, fmt.Errorf("%w: this can happen if Caddy was forcefully killed", err) }
for _, ln := range listeners {
if cl, ok := ln.(io.Closer); ok {
cl.Close()
} }
}
}()
// an address can contain a port range, which represents multiple addresses;
// some addresses don't use ports at all and have a port range size of 1;
// whatever the case, iterate each address represented and bind a socket
for portOffset := uint(0); portOffset < na.PortRangeSize(); portOffset++ {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
// create (or reuse) the listener ourselves
var ln any
ln, err = na.Listen(ctx, portOffset, config)
if err != nil {
return nil, err return nil, err
} }
return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil listeners = append(listeners, ln)
}) }
return listeners, nil
}
// Listen is similar to net.Listen, with a few differences:
//
// Listen announces on the network address using the port calculated by adding
// portOffset to the start port. (For network types that do not use ports, the
// portOffset is ignored.)
//
// The provided ListenConfig is used to create the listener. Its Control function,
// if set, may be wrapped by an internally-used Control function. The provided
// context may be used to cancel long operations early. The context is not used
// to close the listener after it has been created.
//
// Caddy's listeners can overlap each other: multiple listeners may be created on
// the same socket at the same time. This is useful because during config changes,
// the new config is started while the old config is still running. How this is
// accomplished varies by platform and network type. For example, on Unix, SO_REUSEPORT
// is set except on Unix sockets, for which the file descriptor is duplicated and
// reused; on Windows, the close logic is virtualized using timeouts. Like normal
// listeners, be sure to Close() them when you are done.
//
// This method returns any type, as the implementations of listeners for various
// network types are not interchangeable. The type of listener returned is switched
// on the network type. Stream-based networks ("tcp", "unix", "unixpacket", etc.)
// return a net.Listener; datagram-based networks ("udp", "unixgram", etc.) return
// a net.PacketConn; and so forth. The actual concrete types are not guaranteed to
// be standard, exported types (wrapping is necessary to provide graceful reloads).
//
// Unix sockets will be unlinked before being created, to ensure we can bind to
// it even if the previous program using it exited uncleanly; it will also be
// unlinked upon a graceful exit (or when a new config does not use that socket).
//
// TODO: Experimental API: subject to change or removal.
func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
if na.IsUnixNetwork() {
unixSocketsMu.Lock()
defer unixSocketsMu.Unlock()
}
// check to see if plugin provides listener
if ln, err := getListenerFromPlugin(ctx, na.Network, na.JoinHostPort(portOffset), config); ln != nil || err != nil {
return ln, err
}
// create (or reuse) the listener ourselves
return na.listen(ctx, portOffset, config)
}
func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
var ln any
var err error
address := na.JoinHostPort(portOffset)
// if this is a unix socket, see if we already have it open
if socket, err := reuseUnixSocket(na.Network, address); socket != nil || err != nil {
return socket, err
}
lnKey := listenerKey(na.Network, address)
switch na.Network {
case "tcp", "tcp4", "tcp6", "unix", "unixpacket":
ln, err = listenTCPOrUnix(ctx, lnKey, na.Network, address, config)
case "unixgram":
ln, err = config.ListenPacket(ctx, na.Network, address)
case "udp", "udp4", "udp6":
sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
pc, err := config.ListenPacket(ctx, na.Network, address)
if err != nil {
return nil, err
}
return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil
})
if err != nil {
return nil, err
}
ln = &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}
}
if strings.HasPrefix(na.Network, "ip") {
ln, err = config.ListenPacket(ctx, na.Network, address)
}
if err != nil {
return nil, err
}
if ln == nil {
return nil, fmt.Errorf("unsupported network type: %s", na.Network)
}
// if new listener is a unix socket, make sure we can reuse it later
// (we do our own "unlink on close" -- not required, but more tidy)
one := int32(1)
switch unix := ln.(type) {
case *net.UnixListener:
unix.SetUnlinkOnClose(false)
ln = &unixListener{unix, lnKey, &one}
unixSockets[lnKey] = ln.(*unixListener)
case *net.UnixConn:
ln = &unixConn{unix, address, lnKey, &one}
unixSockets[lnKey] = ln.(*unixConn)
}
return ln, nil
}
// IsUnixNetwork returns true if na.Network is
// unix, unixgram, or unixpacket.
func (na NetworkAddress) IsUnixNetwork() bool {
return isUnixNetwork(na.Network)
}
// JoinHostPort is like net.JoinHostPort, but where the port
// is StartPort + offset.
func (na NetworkAddress) JoinHostPort(offset uint) string {
if na.IsUnixNetwork() {
return na.Host
}
return net.JoinHostPort(na.Host, strconv.Itoa(int(na.StartPort+offset)))
}
// Expand returns one NetworkAddress for each port in the port range.
//
// This is EXPERIMENTAL and subject to change or removal.
func (na NetworkAddress) Expand() []NetworkAddress {
size := na.PortRangeSize()
addrs := make([]NetworkAddress, size)
for portOffset := uint(0); portOffset < size; portOffset++ {
addrs[portOffset] = na.At(portOffset)
}
return addrs
}
// At returns a NetworkAddress with a port range of just 1
// at the given port offset; i.e. a NetworkAddress that
// represents precisely 1 address only.
func (na NetworkAddress) At(portOffset uint) NetworkAddress {
na2 := na
na2.StartPort, na2.EndPort = na.StartPort+portOffset, na.StartPort+portOffset
return na2
}
// PortRangeSize returns how many ports are in
// pa's port range. Port ranges are inclusive,
// so the size is the difference of start and
// end ports plus one.
func (na NetworkAddress) PortRangeSize() uint {
if na.EndPort < na.StartPort {
return 0
}
return (na.EndPort - na.StartPort) + 1
}
func (na NetworkAddress) isLoopback() bool {
if na.IsUnixNetwork() {
return true
}
if na.Host == "localhost" {
return true
}
if ip, err := netip.ParseAddr(na.Host); err == nil {
return ip.IsLoopback()
}
return false
}
func (na NetworkAddress) isWildcardInterface() bool {
if na.Host == "" {
return true
}
if ip, err := netip.ParseAddr(na.Host); err == nil {
return ip.IsUnspecified()
}
return false
}
func (na NetworkAddress) port() string {
if na.StartPort == na.EndPort {
return strconv.FormatUint(uint64(na.StartPort), 10)
}
return fmt.Sprintf("%d-%d", na.StartPort, na.EndPort)
}
// String reconstructs the address string for human display.
// The output can be parsed by ParseNetworkAddress(). If the
// address is a unix socket, any non-zero port will be dropped.
func (na NetworkAddress) String() string {
if na.Network == "tcp" && (na.Host != "" || na.port() != "") {
na.Network = "" // omit default network value for brevity
}
return JoinNetworkAddress(na.Network, na.Host, na.port())
}
func isUnixNetwork(netw string) bool {
return netw == "unix" || netw == "unixgram" || netw == "unixpacket"
}
// ParseNetworkAddress parses addr into its individual
// components. The input string is expected to be of
// the form "network/host:port-range" where any part is
// optional. The default network, if unspecified, is tcp.
// Port ranges are inclusive.
//
// Network addresses are distinct from URLs and do not
// use URL syntax.
func ParseNetworkAddress(addr string) (NetworkAddress, error) {
var host, port string
network, host, port, err := SplitNetworkAddress(addr)
if err != nil {
return NetworkAddress{}, err
}
if network == "" {
network = "tcp"
}
if isUnixNetwork(network) {
return NetworkAddress{
Network: network,
Host: host,
}, nil
}
var start, end uint64
if port != "" {
before, after, found := strings.Cut(port, "-")
if !found {
after = before
}
start, err = strconv.ParseUint(before, 10, 16)
if err != nil {
return NetworkAddress{}, fmt.Errorf("invalid start port: %v", err)
}
end, err = strconv.ParseUint(after, 10, 16)
if err != nil {
return NetworkAddress{}, fmt.Errorf("invalid end port: %v", err)
}
if end < start {
return NetworkAddress{}, fmt.Errorf("end port must not be less than start port")
}
if (end - start) > maxPortSpan {
return NetworkAddress{}, fmt.Errorf("port range exceeds %d ports", maxPortSpan)
}
}
return NetworkAddress{
Network: network,
Host: host,
StartPort: uint(start),
EndPort: uint(end),
}, nil
}
// SplitNetworkAddress splits a into its network, host, and port components.
// Note that port may be a port range (:X-Y), or omitted for unix sockets.
func SplitNetworkAddress(a string) (network, host, port string, err error) {
beforeSlash, afterSlash, slashFound := strings.Cut(a, "/")
if slashFound {
network = strings.ToLower(strings.TrimSpace(beforeSlash))
a = afterSlash
}
if isUnixNetwork(network) {
host = a
return
}
host, port, err = net.SplitHostPort(a)
if err == nil || a == "" {
return
}
// in general, if there was an error, it was likely "missing port",
// so try adding a bogus port to take advantage of standard library's
// robust parser, then strip the artificial port before returning
// (don't overwrite original error though; might still be relevant)
var err2 error
host, port, err2 = net.SplitHostPort(a + ":0")
if err2 == nil {
err = nil
port = ""
}
return
}
// JoinNetworkAddress combines network, host, and port into a single
// address string of the form accepted by ParseNetworkAddress(). For
// unix sockets, the network should be "unix" (or "unixgram" or
// "unixpacket") and the path to the socket should be given as the
// host parameter.
func JoinNetworkAddress(network, host, port string) string {
var a string
if network != "" {
a = network + "/"
}
if (host != "" && port == "") || isUnixNetwork(network) {
a += host
} else if port != "" {
a += net.JoinHostPort(host, port)
}
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 { if err != nil {
return nil, err return nil, err
} }
return &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}, nil 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. // ListenQUIC returns a quic.EarlyListener suitable for use in a Caddy module.
// Note that the context passed to Accept is currently ignored, so using // The network will be transformed into a QUIC-compatible type (if unix, then
// a context other than context.Background is meaningless. // unixgram will be used; otherwise, udp will be used).
func ListenQUIC(addr string, tlsConf *tls.Config) (quic.EarlyListener, error) { //
lnKey := "quic/" + addr // 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())
sharedEl, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
el, err := quic.ListenAddrEarly(addr, http3.ConfigureTLSConfig(tlsConf), &quic.Config{}) 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 { if err != nil {
return nil, err return nil, err
} }
return &sharedQuicListener{EarlyListener: el, key: lnKey}, nil 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()) ctx, cancel := context.WithCancel(context.Background())
return &fakeCloseQuicListener{ return &fakeCloseQuicListener{
sharedQuicListener: sharedEl.(*sharedQuicListener), sharedQuicListener: sharedEarlyListener.(*sharedQuicListener),
context: ctx, contextCancel: cancel, uc: unix,
}, err context: ctx,
contextCancel: cancel,
}, nil
} }
// fakeCloseListener is a private wrapper over a listener that // ListenerUsage returns the current usage count of the given listener address.
// is shared. The state of fakeCloseListener is not shared. func ListenerUsage(network, addr string) int {
// This allows one user of a socket to "close" the listener count, _ := listenerPool.References(listenerKey(network, addr))
// while in reality the socket stays open for other users of return count
// the listener. In this way, servers become hot-swappable
// while the listener remains running. Listeners should be
// re-wrapped in a new fakeCloseListener each time the listener
// is reused. This type is atomic and values must not be copied.
type fakeCloseListener struct {
closed int32 // accessed atomically; belongs to this struct only
*sharedListener // embedded, so we also become a net.Listener
} }
func (fcl *fakeCloseListener) Accept() (net.Conn, error) { // sharedQuicListener is like sharedListener, but for quic.EarlyListeners.
// if the listener is already "closed", return error type sharedQuicListener struct {
if atomic.LoadInt32(&fcl.closed) == 1 { quic.EarlyListener
return nil, fakeClosedErr(fcl) key string
}
// call underlying accept
conn, err := fcl.sharedListener.Accept()
if err == nil {
return conn, nil
}
// since Accept() returned an error, it may be because our reference to
// the listener (this fakeCloseListener) may have been closed, i.e. the
// server is shutting down; in that case, we need to clear the deadline
// that we set when Close() was called, and return a non-temporary and
// non-timeout error value to the caller, masking the "true" error, so
// that server loops / goroutines won't retry, linger, and leak
if atomic.LoadInt32(&fcl.closed) == 1 {
// we dereference the sharedListener explicitly even though it's embedded
// so that it's clear in the code that side-effects are shared with other
// users of this listener, not just our own reference to it; we also don't
// do anything with the error because all we could do is log it, but we
// expliclty assign it to nothing so we don't forget it's there if needed
_ = fcl.sharedListener.clearDeadline()
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
return nil, fakeClosedErr(fcl)
}
}
return nil, err
} }
// Close stops accepting new connections without closing the // Destruct closes the underlying QUIC listener.
// underlying listener. The underlying listener is only closed func (sql *sharedQuicListener) Destruct() error {
// if the caller is the last known user of the socket. return sql.EarlyListener.Close()
func (fcl *fakeCloseListener) Close() error {
if atomic.CompareAndSwapInt32(&fcl.closed, 0, 1) {
// There are two ways I know of to get an Accept()
// function to return to the server loop that called
// it: close the listener, or set a deadline in the
// past. Obviously, we can't close the socket yet
// since others may be using it (hence this whole
// file). But we can set the deadline in the past,
// and this is kind of cheating, but it works, and
// it apparently even works on Windows.
_ = fcl.sharedListener.setDeadline()
_, _ = listenerPool.Delete(fcl.sharedListener.key)
}
return nil
} }
type fakeCloseQuicListener struct { // sharedPacketConn is like sharedListener, but for net.PacketConns.
closed int32 // accessed atomically; belongs to this struct only type sharedPacketConn struct {
*sharedQuicListener // embedded, so we also become a quic.EarlyListener net.PacketConn
context context.Context key string
contextCancel context.CancelFunc
} }
// Currently Accept ignores the passed context, however a situation where // Destruct closes the underlying socket.
// someone would need a hotswappable QUIC-only (not http3, since it uses context.Background here) func (spc *sharedPacketConn) Destruct() error {
// server on which Accept would be called with non-empty contexts return spc.PacketConn.Close()
// (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.EarlySession, 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)
}
return nil
} }
// fakeClosedErr returns an error value that is not temporary // fakeClosedErr returns an error value that is not temporary
@@ -217,7 +517,7 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error {
} }
} }
// ErrFakeClosed is the underlying error value returned by // errFakeClosed is the underlying error value returned by
// fakeCloseListener.Accept() after Close() has been called, // fakeCloseListener.Accept() after Close() has been called,
// indicating that it is pretending to be closed so that the // indicating that it is pretending to be closed so that the
// server using it can terminate, while the underlying // server using it can terminate, while the underlying
@@ -255,250 +555,140 @@ func (fcpc fakeClosePacketConn) SyscallConn() (syscall.RawConn, error) {
return nil, fmt.Errorf("SyscallConn() not implemented for %T", fcpc.PacketConn) return nil, fmt.Errorf("SyscallConn() not implemented for %T", fcpc.PacketConn)
} }
// sharedListener is a wrapper over an underlying listener. The listener type fakeCloseQuicListener struct {
// and the other fields on the struct are shared state that is synchronized, closed int32 // accessed atomically; belongs to this struct only
// so sharedListener structs must never be copied (always use a pointer). *sharedQuicListener // embedded, so we also become a quic.EarlyListener
type sharedListener struct { uc *unixConn // underlying unix socket, if UDS
net.Listener context context.Context
key string // uniquely identifies this listener contextCancel context.CancelFunc
deadline bool // whether a deadline is currently set
deadlineMu sync.Mutex
} }
func (sl *sharedListener) clearDeadline() error { // Currently Accept ignores the passed context, however a situation where
var err error // someone would need a hotswappable QUIC-only (not http3, since it uses context.Background here)
sl.deadlineMu.Lock() // server on which Accept would be called with non-empty contexts
if sl.deadline { // (mind that the default net listeners' Accept doesn't take a context argument)
switch ln := sl.Listener.(type) { // sounds way too rare for us to sacrifice efficiency here.
case *net.TCPListener: func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (quic.EarlyConnection, error) {
err = ln.SetDeadline(time.Time{}) conn, err := fcql.sharedQuicListener.Accept(fcql.context)
case *net.UnixListener: if err == nil {
err = ln.SetDeadline(time.Time{}) return conn, nil
}
sl.deadline = false
} }
sl.deadlineMu.Unlock()
return err
}
func (sl *sharedListener) setDeadline() error { // if the listener is "closed", return a fake closed error instead
timeInPast := time.Now().Add(-1 * time.Minute) if atomic.LoadInt32(&fcql.closed) == 1 && errors.Is(err, context.Canceled) {
var err error return nil, fakeClosedErr(fcql)
sl.deadlineMu.Lock()
if !sl.deadline {
switch ln := sl.Listener.(type) {
case *net.TCPListener:
err = ln.SetDeadline(timeInPast)
case *net.UnixListener:
err = ln.SetDeadline(timeInPast)
}
sl.deadline = true
} }
sl.deadlineMu.Unlock() return nil, err
return err
} }
// Destruct is called by the UsagePool when the listener is func (fcql *fakeCloseQuicListener) Close() error {
// finally not being used anymore. It closes the socket. if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
func (sl *sharedListener) Destruct() error { fcql.contextCancel()
return sl.Listener.Close() _, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
} if fcql.uc != nil {
// unix sockets need to be closed ourselves because we dup() the file
// sharedQuicListener is like sharedListener, but for quic.EarlyListeners. // descriptor when we reuse them, so this avoids a resource leak
type sharedQuicListener struct { fcql.uc.Close()
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
// unix, unixgram, or unixpacket.
func (na NetworkAddress) IsUnixNetwork() bool {
return isUnixNetwork(na.Network)
}
// JoinHostPort is like net.JoinHostPort, but where the port
// is StartPort + offset.
func (na NetworkAddress) JoinHostPort(offset uint) string {
if na.IsUnixNetwork() {
return na.Host
}
return net.JoinHostPort(na.Host, strconv.Itoa(int(na.StartPort+offset)))
}
// PortRangeSize returns how many ports are in
// pa's port range. Port ranges are inclusive,
// so the size is the difference of start and
// end ports plus one.
func (na NetworkAddress) PortRangeSize() uint {
return (na.EndPort - na.StartPort) + 1
}
func (na NetworkAddress) isLoopback() bool {
if na.IsUnixNetwork() {
return true
}
if na.Host == "localhost" {
return true
}
if ip := net.ParseIP(na.Host); ip != nil {
return ip.IsLoopback()
}
return false
}
func (na NetworkAddress) isWildcardInterface() bool {
if na.Host == "" {
return true
}
if ip := net.ParseIP(na.Host); ip != nil {
return ip.IsUnspecified()
}
return false
}
func (na NetworkAddress) port() string {
if na.StartPort == na.EndPort {
return strconv.FormatUint(uint64(na.StartPort), 10)
}
return fmt.Sprintf("%d-%d", na.StartPort, na.EndPort)
}
// String reconstructs the address string to the form expected
// by ParseNetworkAddress(). If the address is a unix socket,
// any non-zero port will be dropped.
func (na NetworkAddress) String() string {
return JoinNetworkAddress(na.Network, na.Host, na.port())
}
func isUnixNetwork(netw string) bool {
return netw == "unix" || netw == "unixgram" || netw == "unixpacket"
}
func isListenBindAddressAlreadyInUseError(err error) bool {
switch networkOperationError := err.(type) {
case *net.OpError:
switch syscallError := networkOperationError.Err.(type) {
case *os.SyscallError:
if syscallError.Syscall == "bind" {
return true
}
} }
} }
return nil
return false
} }
// ParseNetworkAddress parses addr into its individual // RegisterNetwork registers a network type with Caddy so that if a listener is
// components. The input string is expected to be of // created for that network type, getListener will be invoked to get the listener.
// the form "network/host:port-range" where any part is // This should be called during init() and will panic if the network type is standard
// optional. The default network, if unspecified, is tcp. // or reserved, or if it is already registered. EXPERIMENTAL and subject to change.
// Port ranges are inclusive. func RegisterNetwork(network string, getListener ListenerFunc) {
// network = strings.TrimSpace(strings.ToLower(network))
// Network addresses are distinct from URLs and do not
// use URL syntax. if network == "tcp" || network == "tcp4" || network == "tcp6" ||
func ParseNetworkAddress(addr string) (NetworkAddress, error) { network == "udp" || network == "udp4" || network == "udp6" ||
var host, port string network == "unix" || network == "unixpacket" || network == "unixgram" ||
network, host, port, err := SplitNetworkAddress(addr) strings.HasPrefix("ip:", network) || strings.HasPrefix("ip4:", network) || strings.HasPrefix("ip6:", network) {
if network == "" { panic("network type " + network + " is reserved")
network = "tcp"
} }
if err != nil {
return NetworkAddress{}, err if _, ok := networkTypes[strings.ToLower(network)]; ok {
panic("network type " + network + " is already registered")
} }
if isUnixNetwork(network) {
return NetworkAddress{ networkTypes[network] = getListener
Network: network,
Host: host,
}, nil
}
ports := strings.SplitN(port, "-", 2)
if len(ports) == 1 {
ports = append(ports, ports[0])
}
var start, end uint64
start, err = strconv.ParseUint(ports[0], 10, 16)
if err != nil {
return NetworkAddress{}, fmt.Errorf("invalid start port: %v", err)
}
end, err = strconv.ParseUint(ports[1], 10, 16)
if err != nil {
return NetworkAddress{}, fmt.Errorf("invalid end port: %v", err)
}
if end < start {
return NetworkAddress{}, fmt.Errorf("end port must not be less than start port")
}
if (end - start) > maxPortSpan {
return NetworkAddress{}, fmt.Errorf("port range exceeds %d ports", maxPortSpan)
}
return NetworkAddress{
Network: network,
Host: host,
StartPort: uint(start),
EndPort: uint(end),
}, nil
} }
// SplitNetworkAddress splits a into its network, host, and port components. type unixListener struct {
// Note that port may be a port range (:X-Y), or omitted for unix sockets. *net.UnixListener
func SplitNetworkAddress(a string) (network, host, port string, err error) { mapKey string
if idx := strings.Index(a, "/"); idx >= 0 { count *int32 // accessed atomically
network = strings.ToLower(strings.TrimSpace(a[:idx]))
a = a[idx+1:]
}
if isUnixNetwork(network) {
host = a
return
}
host, port, err = net.SplitHostPort(a)
return
} }
// JoinNetworkAddress combines network, host, and port into a single func (uln *unixListener) Close() error {
// address string of the form accepted by ParseNetworkAddress(). For newCount := atomic.AddInt32(uln.count, -1)
// unix sockets, the network should be "unix" (or "unixgram" or if newCount == 0 {
// "unixpacket") and the path to the socket should be given as the defer func() {
// host parameter. addr := uln.Addr().String()
func JoinNetworkAddress(network, host, port string) string { unixSocketsMu.Lock()
var a string delete(unixSockets, uln.mapKey)
if network != "" { unixSocketsMu.Unlock()
a = network + "/" _ = syscall.Unlink(addr)
}()
} }
if (host != "" && port == "") || isUnixNetwork(network) { return uln.UnixListener.Close()
a += host
} else if port != "" {
a += net.JoinHostPort(host, port)
}
return a
} }
type unixConn struct {
*net.UnixConn
filename string
mapKey string
count *int32 // accessed atomically
}
func (uc *unixConn) Close() error {
newCount := atomic.AddInt32(uc.count, -1)
if newCount == 0 {
defer func() {
unixSocketsMu.Lock()
delete(unixSockets, uc.mapKey)
unixSocketsMu.Unlock()
_ = syscall.Unlink(uc.filename)
}()
}
return uc.UnixConn.Close()
}
// unixSockets keeps track of the currently-active unix sockets
// so we can transfer their FDs gracefully during reloads.
var (
unixSockets = make(map[string]interface {
File() (*os.File, error)
})
unixSocketsMu sync.Mutex
)
// getListenerFromPlugin returns a listener on the given network and address
// if a plugin has registered the network name. It may return (nil, nil) if
// no plugin can provide a listener.
func getListenerFromPlugin(ctx context.Context, network, addr string, config net.ListenConfig) (any, error) {
// get listener from plugin if network type is registered
if getListener, ok := networkTypes[network]; ok {
Log().Debug("getting listener from plugin", zap.String("network", network))
return getListener(ctx, network, addr, config)
}
return nil, nil
}
func listenerKey(network, addr string) string {
return network + "/" + addr
}
// ListenerFunc is a function that can return a listener given a network and address.
// The listeners must be capable of overlapping: with Caddy, new configs are loaded
// before old ones are unloaded, so listeners may overlap briefly if the configs
// both need the same listener. EXPERIMENTAL and subject to change.
type ListenerFunc func(ctx context.Context, network, addr string, cfg net.ListenConfig) (any, error)
var networkTypes = map[string]ListenerFunc{}
// ListenerWrapper is a type that wraps a listener // ListenerWrapper is a type that wraps a listener
// so it can modify the input listener's methods. // so it can modify the input listener's methods.
// Modules that implement this interface are found // Modules that implement this interface are found
-1
View File
@@ -13,7 +13,6 @@
// limitations under the License. // limitations under the License.
//go:build gofuzz //go:build gofuzz
// +build gofuzz
package caddy package caddy
+111 -5
View File
@@ -32,9 +32,24 @@ func TestSplitNetworkAddress(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
input: "foo", input: "foo",
expectHost: "foo",
},
{
input: ":", // empty host & empty port
},
{
input: "::",
expectErr: true, expectErr: true,
}, },
{
input: "[::]",
expectHost: "::",
},
{
input: ":1234",
expectPort: "1234",
},
{ {
input: "foo:1234", input: "foo:1234",
expectHost: "foo", expectHost: "foo",
@@ -80,10 +95,10 @@ func TestSplitNetworkAddress(t *testing.T) {
} { } {
actualNetwork, actualHost, actualPort, err := SplitNetworkAddress(tc.input) actualNetwork, actualHost, actualPort, err := SplitNetworkAddress(tc.input)
if tc.expectErr && err == nil { if tc.expectErr && err == nil {
t.Errorf("Test %d: Expected error but got: %v", i, err) t.Errorf("Test %d: Expected error but got %v", i, err)
} }
if !tc.expectErr && err != nil { if !tc.expectErr && err != nil {
t.Errorf("Test %d: Expected no error but got: %v", i, err) t.Errorf("Test %d: Expected no error but got %v", i, err)
} }
if actualNetwork != tc.expectNetwork { if actualNetwork != tc.expectNetwork {
t.Errorf("Test %d: Expected network '%s' but got '%s'", i, tc.expectNetwork, actualNetwork) t.Errorf("Test %d: Expected network '%s' but got '%s'", i, tc.expectNetwork, actualNetwork)
@@ -169,8 +184,17 @@ func TestParseNetworkAddress(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
input: ":", input: ":",
expectErr: true, expectAddr: NetworkAddress{
Network: "tcp",
},
},
{
input: "[::]",
expectAddr: NetworkAddress{
Network: "tcp",
Host: "::",
},
}, },
{ {
input: ":1234", input: ":1234",
@@ -307,3 +331,85 @@ func TestJoinHostPort(t *testing.T) {
} }
} }
} }
func TestExpand(t *testing.T) {
for i, tc := range []struct {
input NetworkAddress
expect []NetworkAddress
}{
{
input: NetworkAddress{
Network: "tcp",
Host: "localhost",
StartPort: 2000,
EndPort: 2000,
},
expect: []NetworkAddress{
{
Network: "tcp",
Host: "localhost",
StartPort: 2000,
EndPort: 2000,
},
},
},
{
input: NetworkAddress{
Network: "tcp",
Host: "localhost",
StartPort: 2000,
EndPort: 2002,
},
expect: []NetworkAddress{
{
Network: "tcp",
Host: "localhost",
StartPort: 2000,
EndPort: 2000,
},
{
Network: "tcp",
Host: "localhost",
StartPort: 2001,
EndPort: 2001,
},
{
Network: "tcp",
Host: "localhost",
StartPort: 2002,
EndPort: 2002,
},
},
},
{
input: NetworkAddress{
Network: "tcp",
Host: "localhost",
StartPort: 2000,
EndPort: 1999,
},
expect: []NetworkAddress{},
},
{
input: NetworkAddress{
Network: "unix",
Host: "/foo/bar",
StartPort: 0,
EndPort: 0,
},
expect: []NetworkAddress{
{
Network: "unix",
Host: "/foo/bar",
StartPort: 0,
EndPort: 0,
},
},
},
} {
actual := tc.input.Expand()
if !reflect.DeepEqual(actual, tc.expect) {
t.Errorf("Test %d: Expected %+v but got %+v", i, tc.expect, actual)
}
}
}
+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)
+8 -8
View File
@@ -44,7 +44,7 @@ import (
// Provisioner, the Provision() method is called. 4) If the // Provisioner, the Provision() method is called. 4) If the
// module is a Validator, the Validate() method is called. // module is a Validator, the Validate() method is called.
// 5) The module will probably be type-asserted from // 5) The module will probably be type-asserted from
// interface{} to some other, more useful interface expected // 'any' to some other, more useful interface expected
// by the host module. For example, HTTP handler modules are // by the host module. For example, HTTP handler modules are
// type-asserted as caddyhttp.MiddlewareHandler values. // type-asserted as caddyhttp.MiddlewareHandler values.
// 6) When a module's containing Context is canceled, if it is // 6) When a module's containing Context is canceled, if it is
@@ -172,7 +172,7 @@ func GetModule(name string) (ModuleInfo, error) {
// GetModuleName returns a module's name (the last label of its ID) // GetModuleName returns a module's name (the last label of its ID)
// from an instance of its value. If the value is not a module, an // from an instance of its value. If the value is not a module, an
// empty string will be returned. // empty string will be returned.
func GetModuleName(instance interface{}) string { func GetModuleName(instance any) string {
var name string var name string
if mod, ok := instance.(Module); ok { if mod, ok := instance.(Module); ok {
name = mod.CaddyModule().ID.Name() name = mod.CaddyModule().ID.Name()
@@ -182,7 +182,7 @@ func GetModuleName(instance interface{}) string {
// GetModuleID returns a module's ID from an instance of its value. // GetModuleID returns a module's ID from an instance of its value.
// If the value is not a module, an empty string will be returned. // If the value is not a module, an empty string will be returned.
func GetModuleID(instance interface{}) string { func GetModuleID(instance any) string {
var id string var id string
if mod, ok := instance.(Module); ok { if mod, ok := instance.(Module); ok {
id = string(mod.CaddyModule().ID) id = string(mod.CaddyModule().ID)
@@ -259,7 +259,7 @@ func Modules() []string {
// where raw must be a JSON encoding of a map. It returns that value, // where raw must be a JSON encoding of a map. It returns that value,
// along with the result of removing that key from raw. // along with the result of removing that key from raw.
func getModuleNameInline(moduleNameKey string, raw json.RawMessage) (string, json.RawMessage, error) { func getModuleNameInline(moduleNameKey string, raw json.RawMessage) (string, json.RawMessage, error) {
var tmp map[string]interface{} var tmp map[string]any
err := json.Unmarshal(raw, &tmp) err := json.Unmarshal(raw, &tmp)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
@@ -324,11 +324,11 @@ func ParseStructTag(tag string) (map[string]string, error) {
if pair == "" { if pair == "" {
continue continue
} }
parts := strings.SplitN(pair, "=", 2) before, after, isCut := strings.Cut(pair, "=")
if len(parts) != 2 { if !isCut {
return nil, fmt.Errorf("missing key in '%s' (pair %d)", pair, i) return nil, fmt.Errorf("missing key in '%s' (pair %d)", pair, i)
} }
results[parts[0]] = parts[1] results[before] = after
} }
return results, nil return results, nil
} }
@@ -337,7 +337,7 @@ func ParseStructTag(tag string) (map[string]string, error) {
// if any of the fields are unrecognized. Useful when decoding // if any of the fields are unrecognized. Useful when decoding
// module configurations, where you want to be more sure they're // module configurations, where you want to be more sure they're
// correct. // correct.
func strictUnmarshalJSON(data []byte, v interface{}) error { func strictUnmarshalJSON(data []byte, v any) error {
dec := json.NewDecoder(bytes.NewReader(data)) dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields() dec.DisallowUnknownFields()
return dec.Decode(v) return dec.Decode(v)
+390
View File
@@ -0,0 +1,390 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyevents
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/google/uuid"
"go.uber.org/zap"
)
func init() {
caddy.RegisterModule(App{})
}
// App implements a global eventing system within Caddy.
// Modules can emit and subscribe to events, providing
// hooks into deep parts of the code base that aren't
// otherwise accessible. Events provide information about
// what and when things are happening, and this facility
// allows handlers to take action when events occur,
// add information to the event's metadata, and even
// control program flow in some cases.
//
// Events are propagated in a DOM-like fashion. An event
// emitted from module `a.b.c` (the "origin") will first
// invoke handlers listening to `a.b.c`, then `a.b`,
// then `a`, then those listening regardless of origin.
// If a handler returns the special error Aborted, then
// propagation immediately stops and the event is marked
// as aborted. Emitters may optionally choose to adjust
// program flow based on an abort.
//
// Modules can subscribe to events by origin and/or name.
// A handler is invoked only if it is subscribed to the
// event by name and origin. Subscriptions should be
// registered during the provisioning phase, before apps
// are started.
//
// Event handlers are fired synchronously as part of the
// regular flow of the program. This allows event handlers
// to control the flow of the program if the origin permits
// it and also allows handlers to convey new information
// back into the origin module before it continues.
// In essence, event handlers are similar to HTTP
// middleware handlers.
//
// Event bindings/subscribers are unordered; i.e.
// event handlers are invoked in an arbitrary order.
// Event handlers should not rely on the logic of other
// handlers to succeed.
//
// The entirety of this app module is EXPERIMENTAL and
// subject to change. Pay attention to release notes.
type App struct {
// Subscriptions bind handlers to one or more events
// either globally or scoped to specific modules or module
// namespaces.
Subscriptions []*Subscription `json:"subscriptions,omitempty"`
// Map of event name to map of module ID/namespace to handlers
subscriptions map[string]map[caddy.ModuleID][]Handler
logger *zap.Logger
started bool
}
// Subscription represents binding of one or more handlers to
// one or more events.
type Subscription struct {
// The name(s) of the event(s) to bind to. Default: all events.
Events []string `json:"events,omitempty"`
// The ID or namespace of the module(s) from which events
// originate to listen to for events. Default: all modules.
//
// Events propagate up, so events emitted by module "a.b.c"
// will also trigger the event for "a.b" and "a". Thus, to
// receive all events from "a.b.c" and "a.b.d", for example,
// one can subscribe to either "a.b" or all of "a" entirely.
Modules []caddy.ModuleID `json:"modules,omitempty"`
// The event handler modules. These implement the actual
// behavior to invoke when an event occurs. At least one
// handler is required.
HandlersRaw []json.RawMessage `json:"handlers,omitempty" caddy:"namespace=events.handlers inline_key=handler"`
// The decoded handlers; Go code that is subscribing to
// an event should set this field directly; HandlersRaw
// is meant for JSON configuration to fill out this field.
Handlers []Handler `json:"-"`
}
// CaddyModule returns the Caddy module information.
func (App) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "events",
New: func() caddy.Module { return new(App) },
}
}
// Provision sets up the app.
func (app *App) Provision(ctx caddy.Context) error {
app.logger = ctx.Logger()
app.subscriptions = make(map[string]map[caddy.ModuleID][]Handler)
for _, sub := range app.Subscriptions {
if sub.HandlersRaw != nil {
handlersIface, err := ctx.LoadModule(sub, "HandlersRaw")
if err != nil {
return fmt.Errorf("loading event subscriber modules: %v", err)
}
for _, h := range handlersIface.([]any) {
sub.Handlers = append(sub.Handlers, h.(Handler))
}
if len(sub.Handlers) == 0 {
// pointless to bind without any handlers
return fmt.Errorf("no handlers defined")
}
}
}
return nil
}
// Start runs the app.
func (app *App) Start() error {
for _, sub := range app.Subscriptions {
if err := app.Subscribe(sub); err != nil {
return err
}
}
app.started = true
return nil
}
// Stop gracefully shuts down the app.
func (app *App) Stop() error {
return nil
}
// Subscribe binds one or more event handlers to one or more events
// according to the subscription s. For now, subscriptions can only
// be created during the provision phase; new bindings cannot be
// created after the events app has started.
func (app *App) Subscribe(s *Subscription) error {
if app.started {
return fmt.Errorf("events already started; new subscriptions closed")
}
// handle special case of catch-alls (omission of event name or module space implies all)
if len(s.Events) == 0 {
s.Events = []string{""}
}
if len(s.Modules) == 0 {
s.Modules = []caddy.ModuleID{""}
}
for _, eventName := range s.Events {
if app.subscriptions[eventName] == nil {
app.subscriptions[eventName] = make(map[caddy.ModuleID][]Handler)
}
for _, originModule := range s.Modules {
app.subscriptions[eventName][originModule] = append(app.subscriptions[eventName][originModule], s.Handlers...)
}
}
return nil
}
// On is syntactic sugar for Subscribe() that binds a single handler
// to a single event from any module. If the eventName is empty string,
// it counts for all events.
func (app *App) On(eventName string, handler Handler) error {
return app.Subscribe(&Subscription{
Events: []string{eventName},
Handlers: []Handler{handler},
})
}
// Emit creates and dispatches an event named eventName to all relevant handlers with
// the metadata data. Events are emitted and propagated synchronously. The returned Event
// value will have any additional information from the invoked handlers.
//
// Note that the data map is not copied, for efficiency. After Emit() is called, the
// data passed in should not be changed in other goroutines.
func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) Event {
logger := app.logger.With(zap.String("name", eventName))
id, err := uuid.NewRandom()
if err != nil {
logger.Error("failed generating new event ID", zap.Error(err))
}
eventName = strings.ToLower(eventName)
e := Event{
Data: data,
id: id,
ts: time.Now(),
name: eventName,
origin: ctx.Module(),
}
logger = logger.With(
zap.String("id", e.id.String()),
zap.String("origin", e.origin.CaddyModule().String()))
// add event info to replacer, make sure it's in the context
repl, ok := ctx.Context.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
if !ok {
repl = caddy.NewReplacer()
ctx.Context = context.WithValue(ctx.Context, caddy.ReplacerCtxKey, repl)
}
repl.Map(func(key string) (any, bool) {
switch key {
case "event":
return e, true
case "event.id":
return e.id, true
case "event.name":
return e.name, true
case "event.time":
return e.ts, true
case "event.time_unix":
return e.ts.UnixMilli(), true
case "event.module":
return e.origin.CaddyModule().ID, true
case "event.data":
return e.Data, true
}
if strings.HasPrefix(key, "event.data.") {
key = strings.TrimPrefix(key, "event.data.")
if val, ok := e.Data[key]; ok {
return val, true
}
}
return nil, false
})
logger.Debug("event", zap.Any("data", e.Data))
// invoke handlers bound to the event by name and also all events; this for loop
// iterates twice at most: once for the event name, once for "" (all events)
for {
moduleID := e.origin.CaddyModule().ID
// implement propagation up the module tree (i.e. start with "a.b.c" then "a.b" then "a" then "")
for {
if app.subscriptions[eventName] == nil {
break // shortcut if event not bound at all
}
for _, handler := range app.subscriptions[eventName][moduleID] {
select {
case <-ctx.Done():
logger.Error("context canceled; event handling stopped")
return e
default:
}
if err := handler.Handle(ctx, e); err != nil {
aborted := errors.Is(err, ErrAborted)
logger.Error("handler error",
zap.Error(err),
zap.Bool("aborted", aborted))
if aborted {
e.Aborted = err
return e
}
}
}
if moduleID == "" {
break
}
lastDot := strings.LastIndex(string(moduleID), ".")
if lastDot < 0 {
moduleID = "" // include handlers bound to events regardless of module
} else {
moduleID = moduleID[:lastDot]
}
}
// include handlers listening to all events
if eventName == "" {
break
}
eventName = ""
}
return e
}
// Event represents something that has happened or is happening.
// An Event value is not synchronized, so it should be copied if
// being used in goroutines.
//
// EXPERIMENTAL: As with the rest of this package, events are
// subject to change.
type Event struct {
// If non-nil, the event has been aborted, meaning
// propagation has stopped to other handlers and
// the code should stop what it was doing. Emitters
// may choose to use this as a signal to adjust their
// code path appropriately.
Aborted error
// The data associated with the event. Usually the
// original emitter will be the only one to set or
// change these values, but the field is exported
// so handlers can have full access if needed.
// However, this map is not synchronized, so
// handlers must not use this map directly in new
// goroutines; instead, copy the map to use it in a
// goroutine.
Data map[string]any
id uuid.UUID
ts time.Time
name string
origin caddy.Module
}
// CloudEvent exports event e as a structure that, when
// serialized as JSON, is compatible with the
// CloudEvents spec.
func (e Event) CloudEvent() CloudEvent {
dataJSON, _ := json.Marshal(e.Data)
return CloudEvent{
ID: e.id.String(),
Source: e.origin.CaddyModule().String(),
SpecVersion: "1.0",
Type: e.name,
Time: e.ts,
DataContentType: "application/json",
Data: dataJSON,
}
}
// CloudEvent is a JSON-serializable structure that
// is compatible with the CloudEvents specification.
// See https://cloudevents.io.
type CloudEvent struct {
ID string `json:"id"`
Source string `json:"source"`
SpecVersion string `json:"specversion"`
Type string `json:"type"`
Time time.Time `json:"time"`
DataContentType string `json:"datacontenttype,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
// ErrAborted cancels an event.
var ErrAborted = errors.New("event aborted")
// Handler is a type that can handle events.
type Handler interface {
Handle(context.Context, Event) error
}
// Interface guards
var (
_ caddy.App = (*App)(nil)
_ caddy.Provisioner = (*App)(nil)
)
@@ -0,0 +1,88 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package eventsconfig is for configuring caddyevents.App with the
// Caddyfile. This code can't be in the caddyevents package because
// the httpcaddyfile package imports caddyhttp, which imports
// caddyevents: hence, it creates an import cycle.
package eventsconfig
import (
"encoding/json"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyevents"
)
func init() {
httpcaddyfile.RegisterGlobalOption("events", parseApp)
}
// parseApp configures the "events" global option from Caddyfile to set up the events app.
// Syntax:
//
// events {
// on <event> <handler_module...>
// }
//
// If <event> is *, then it will bind to all events.
func parseApp(d *caddyfile.Dispenser, _ any) (any, error) {
app := new(caddyevents.App)
// consume the option name
if !d.Next() {
return nil, d.ArgErr()
}
// handle the block
for d.NextBlock(0) {
switch d.Val() {
case "on":
if !d.NextArg() {
return nil, d.ArgErr()
}
eventName := d.Val()
if eventName == "*" {
eventName = ""
}
if !d.NextArg() {
return nil, d.ArgErr()
}
handlerName := d.Val()
modID := "events.handlers." + handlerName
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
app.Subscriptions = append(app.Subscriptions, &caddyevents.Subscription{
Events: []string{eventName},
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(unm, "handler", handlerName, nil),
},
})
default:
return nil, d.ArgErr()
}
}
return httpcaddyfile.App{
Name: "events",
Value: caddyconfig.JSON(app, nil),
}, nil
}
+225 -64
View File
@@ -18,13 +18,15 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"net"
"net/http" "net/http"
"strconv" "strconv"
"sync"
"time" "time"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyevents"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/lucas-clemente/quic-go/http3"
"go.uber.org/zap" "go.uber.org/zap"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"golang.org/x/net/http2/h2c" "golang.org/x/net/http2/h2c"
@@ -64,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
@@ -95,6 +97,8 @@ func init() {
// `{http.request.uri}` | The full request URI // `{http.request.uri}` | The full request URI
// `{http.response.header.*}` | Specific response header field // `{http.response.header.*}` | Specific response header field
// `{http.vars.*}` | Custom variables in the HTTP handler chain // `{http.vars.*}` | Custom variables in the HTTP handler chain
// `{http.shutting_down}` | True if the HTTP app is shutting down
// `{http.time_until_shutdown}` | Time until HTTP server shutdown, if scheduled
type App struct { type App struct {
// HTTPPort specifies the port to use for HTTP (as opposed to HTTPS), // HTTPPort specifies the port to use for HTTP (as opposed to HTTPS),
// which is used when setting up HTTP->HTTPS redirects or ACME HTTP // which is used when setting up HTTP->HTTPS redirects or ACME HTTP
@@ -107,18 +111,31 @@ type App struct {
HTTPSPort int `json:"https_port,omitempty"` HTTPSPort int `json:"https_port,omitempty"`
// GracePeriod is how long to wait for active connections when shutting // GracePeriod is how long to wait for active connections when shutting
// down the server. Once the grace period is over, connections will // down the servers. During the grace period, no new connections are
// be forcefully closed. // accepted, idle connections are closed, and active connections will
// be given the full length of time to become idle and close.
// Once the grace period is over, connections will be forcefully closed.
// If zero, the grace period is eternal. Default: 0.
GracePeriod caddy.Duration `json:"grace_period,omitempty"` GracePeriod caddy.Duration `json:"grace_period,omitempty"`
// ShutdownDelay is how long to wait before initiating the grace
// period. When this app is stopping (e.g. during a config reload or
// process exit), all servers will be shut down. Normally this immediately
// initiates the grace period. However, if this delay is configured, servers
// will not be shut down until the delay is over. During this time, servers
// continue to function normally and allow new connections. At the end, the
// grace period will begin. This can be useful to allow downstream load
// balancers time to move this instance out of the rotation without hiccups.
//
// When shutdown has been scheduled, placeholders {http.shutting_down} (bool)
// and {http.time_until_shutdown} (duration) may be useful for health checks.
ShutdownDelay caddy.Duration `json:"shutdown_delay,omitempty"`
// Servers is the list of servers, keyed by arbitrary names chosen // Servers is the list of servers, keyed by arbitrary names chosen
// at your discretion for your own convenience; the keys do not // at your discretion for your own convenience; the keys do not
// affect functionality. // affect functionality.
Servers map[string]*Server `json:"servers,omitempty"` Servers map[string]*Server `json:"servers,omitempty"`
servers []*http.Server
h3servers []*http3.Server
ctx caddy.Context ctx caddy.Context
logger *zap.Logger logger *zap.Logger
tlsApp *caddytls.TLS tlsApp *caddytls.TLS
@@ -144,7 +161,12 @@ func (app *App) Provision(ctx caddy.Context) error {
} }
app.tlsApp = tlsAppIface.(*caddytls.TLS) app.tlsApp = tlsAppIface.(*caddytls.TLS)
app.ctx = ctx app.ctx = ctx
app.logger = ctx.Logger(app) app.logger = ctx.Logger()
eventsAppIface, err := ctx.App("events")
if err != nil {
return fmt.Errorf("getting events app: %v", err)
}
repl := caddy.NewReplacer() repl := caddy.NewReplacer()
@@ -157,17 +179,33 @@ func (app *App) Provision(ctx caddy.Context) error {
} }
// prepare each server // prepare each server
oldContext := ctx.Context
for srvName, srv := range app.Servers { for srvName, srv := range app.Servers {
ctx.Context = context.WithValue(oldContext, ServerCtxKey, srv)
srv.name = srvName srv.name = srvName
srv.tlsApp = app.tlsApp srv.tlsApp = app.tlsApp
srv.events = eventsAppIface.(*caddyevents.App)
srv.ctx = ctx
srv.logger = app.logger.Named("log") srv.logger = app.logger.Named("log")
srv.errorLogger = app.logger.Named("log.error") srv.errorLogger = app.logger.Named("log.error")
srv.shutdownAtMu = new(sync.RWMutex)
// only enable access logs if configured // only enable access logs if configured
if srv.Logs != nil { if srv.Logs != nil {
srv.accessLogger = app.logger.Named("log.access") srv.accessLogger = app.logger.Named("log.access")
} }
// the Go standard library does not let us serve only HTTP/2 using
// http.Server; we would probably need to write our own server
if !srv.protocol("h1") && (srv.protocol("h2") || srv.protocol("h2c")) {
return fmt.Errorf("server %s: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName)
}
// if no protocols configured explicitly, enable all except h2c
if len(srv.Protocols) == 0 {
srv.Protocols = []string{"h1", "h2", "h3"}
}
// if not explicitly configured by the user, disallow TLS // if not explicitly configured by the user, disallow TLS
// client auth bypass (domain fronting) which could // client auth bypass (domain fronting) which could
// otherwise be exploited by sending an unprotected SNI // otherwise be exploited by sending an unprotected SNI
@@ -179,8 +217,7 @@ func (app *App) Provision(ctx caddy.Context) error {
// based on hostname // based on hostname
if srv.StrictSNIHost == nil && srv.hasTLSClientAuth() { if srv.StrictSNIHost == nil && srv.hasTLSClientAuth() {
app.logger.Warn("enabling strict SNI-Host enforcement because TLS client auth is configured", app.logger.Warn("enabling strict SNI-Host enforcement because TLS client auth is configured",
zap.String("server_id", srvName), zap.String("server_id", srvName))
)
trueBool := true trueBool := true
srv.StrictSNIHost = &trueBool srv.StrictSNIHost = &trueBool
} }
@@ -189,8 +226,7 @@ func (app *App) Provision(ctx caddy.Context) error {
for i := range srv.Listen { for i := range srv.Listen {
lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true) lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true)
if err != nil { if err != nil {
return fmt.Errorf("server %s, listener %d: %v", return fmt.Errorf("server %s, listener %d: %v", srvName, i, err)
srvName, i, err)
} }
srv.Listen[i] = lnOut srv.Listen[i] = lnOut
} }
@@ -202,7 +238,7 @@ func (app *App) Provision(ctx caddy.Context) error {
return fmt.Errorf("loading listener wrapper modules: %v", err) return fmt.Errorf("loading listener wrapper modules: %v", err)
} }
var hasTLSPlaceholder bool var hasTLSPlaceholder bool
for i, val := range vals.([]interface{}) { for i, val := range vals.([]any) {
if _, ok := val.(*tlsPlaceholderWrapper); ok { if _, ok := val.(*tlsPlaceholderWrapper); ok {
if i == 0 { if i == 0 {
// putting the tls placeholder wrapper first is nonsensical because // putting the tls placeholder wrapper first is nonsensical because
@@ -230,7 +266,7 @@ func (app *App) Provision(ctx caddy.Context) error {
// route handler so that important security checks are done, etc. // route handler so that important security checks are done, etc.
primaryRoute := emptyHandler primaryRoute := emptyHandler
if srv.Routes != nil { if srv.Routes != nil {
err := srv.Routes.ProvisionHandlers(ctx) err := srv.Routes.ProvisionHandlers(ctx, srv.Metrics)
if err != nil { if err != nil {
return fmt.Errorf("server %s: setting up route handlers: %v", srvName, err) return fmt.Errorf("server %s: setting up route handlers: %v", srvName, err)
} }
@@ -260,7 +296,7 @@ func (app *App) Provision(ctx caddy.Context) error {
srv.IdleTimeout = defaultIdleTimeout srv.IdleTimeout = defaultIdleTimeout
} }
} }
ctx.Context = oldContext
return nil return nil
} }
@@ -298,7 +334,7 @@ func (app *App) Start() error {
} }
for srvName, srv := range app.Servers { for srvName, srv := range app.Servers {
s := &http.Server{ srv.server = &http.Server{
ReadTimeout: time.Duration(srv.ReadTimeout), ReadTimeout: time.Duration(srv.ReadTimeout),
ReadHeaderTimeout: time.Duration(srv.ReadHeaderTimeout), ReadHeaderTimeout: time.Duration(srv.ReadHeaderTimeout),
WriteTimeout: time.Duration(srv.WriteTimeout), WriteTimeout: time.Duration(srv.WriteTimeout),
@@ -308,12 +344,38 @@ func (app *App) Start() error {
ErrorLog: serverLogger, ErrorLog: serverLogger,
} }
// enable h2c if configured // disable HTTP/2, which we enabled by default during provisioning
if srv.AllowH2C { if !srv.protocol("h2") {
srv.server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
for _, cp := range srv.TLSConnPolicies {
// the TLSConfig was already provisioned, so... manually remove it
for i, np := range cp.TLSConfig.NextProtos {
if np == "h2" {
cp.TLSConfig.NextProtos = append(cp.TLSConfig.NextProtos[:i], cp.TLSConfig.NextProtos[i+1:]...)
break
}
}
// remove it from the parent connection policy too, just to keep things tidy
for i, alpn := range cp.ALPN {
if alpn == "h2" {
cp.ALPN = append(cp.ALPN[:i], cp.ALPN[i+1:]...)
break
}
}
}
}
// this TLS config is used by the std lib to choose the actual TLS config for connections
// by looking through the connection policies to find the first one that matches
tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx)
srv.configureServer(srv.server)
// enable H2C if configured
if srv.protocol("h2c") {
h2server := &http2.Server{ h2server := &http2.Server{
IdleTimeout: time.Duration(srv.IdleTimeout), IdleTimeout: time.Duration(srv.IdleTimeout),
} }
s.Handler = h2c.NewHandler(srv, h2server) srv.server.Handler = h2c.NewHandler(srv, h2server)
} }
for _, lnAddr := range srv.Listen { for _, lnAddr := range srv.Listen {
@@ -321,13 +383,16 @@ func (app *App) Start() error {
if err != nil { if err != nil {
return fmt.Errorf("%s: parsing listen address '%s': %v", srvName, lnAddr, err) return fmt.Errorf("%s: parsing listen address '%s': %v", srvName, lnAddr, err)
} }
srv.addresses = append(srv.addresses, listenAddr)
for portOffset := uint(0); portOffset < listenAddr.PortRangeSize(); portOffset++ { 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.Listen(listenAddr.Network, hostport) 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
@@ -342,34 +407,33 @@ func (app *App) Start() error {
// enable TLS if there is a policy and if this is not the HTTP port // enable TLS if there is a policy and if this is not the HTTP port
useTLS := len(srv.TLSConnPolicies) > 0 && int(listenAddr.StartPort+portOffset) != app.httpPort() useTLS := len(srv.TLSConnPolicies) > 0 && int(listenAddr.StartPort+portOffset) != app.httpPort()
if useTLS { if useTLS {
// create TLS listener // create TLS listener - this enables and terminates TLS
tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx)
ln = tls.NewListener(ln, tlsCfg) ln = tls.NewListener(ln, tlsCfg)
///////// // enable HTTP/3 if configured
// TODO: HTTP/3 support is experimental for now if srv.protocol("h3") {
if srv.ExperimentalHTTP3 { // Can't serve HTTP/3 on the same socket as HTTP/1 and 2 because it uses
app.logger.Info("enabling experimental HTTP/3 listener", // a different transport mechanism... which is fine, but the OS doesn't
zap.String("addr", hostport), // differentiate between a SOCK_STREAM file and a SOCK_DGRAM file; they
) // are still one file on the system. So even though "unixpacket" and
h3ln, err := caddy.ListenQUIC(hostport, tlsCfg) // "unixgram" are different network types just as "tcp" and "udp" are,
if err != nil { // the OS will not let us use the same file as both STREAM and DGRAM.
return fmt.Errorf("getting HTTP/3 QUIC listener: %v", err) 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
}
} }
h3srv := &http3.Server{
Server: &http.Server{
Addr: hostport,
Handler: srv,
TLSConfig: tlsCfg,
ErrorLog: serverLogger,
},
}
//nolint:errcheck
go h3srv.ServeListener(h3ln)
app.h3servers = append(app.h3servers, h3srv)
srv.h3server = h3srv
} }
/////////
} }
// finish wrapping listener where we left off before TLS // finish wrapping listener where we left off before TLS
@@ -379,24 +443,30 @@ 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",
zap.String("address", ln.Addr().String()), zap.String("address", ln.Addr().String()),
zap.Bool("http3", srv.ExperimentalHTTP3),
zap.Bool("tls", useTLS), zap.Bool("tls", useTLS),
) zap.Bool("http3", srv.h3server != nil))
//nolint:errcheck srv.listeners = append(srv.listeners, ln)
go s.Serve(ln)
app.servers = append(app.servers, s) // enable HTTP/1 if configured
if srv.protocol("h1") {
//nolint:errcheck
go srv.server.Serve(ln)
}
} }
} }
srv.logger.Info("server running",
zap.String("name", srvName),
zap.Strings("protocols", srv.Protocols))
} }
// finish automatic HTTPS by finally beginning // finish automatic HTTPS by finally beginning
@@ -412,26 +482,117 @@ func (app *App) Start() error {
// Stop gracefully shuts down the HTTP server. // Stop gracefully shuts down the HTTP server.
func (app *App) Stop() error { func (app *App) Stop() error {
ctx := context.Background() ctx := context.Background()
// see if any listeners in our config will be closing or if they are continuing
// hrough a reload; because if any are closing, we will enforce shutdown delay
var delay bool
scheduledTime := time.Now().Add(time.Duration(app.ShutdownDelay))
if app.ShutdownDelay > 0 {
for _, server := range app.Servers {
for _, na := range server.addresses {
for _, addr := range na.Expand() {
if caddy.ListenerUsage(addr.Network, addr.JoinHostPort(0)) < 2 {
app.logger.Debug("listener closing and shutdown delay is configured", zap.String("address", addr.String()))
server.shutdownAtMu.Lock()
server.shutdownAt = scheduledTime
server.shutdownAtMu.Unlock()
delay = true
} else {
app.logger.Debug("shutdown delay configured but listener will remain open", zap.String("address", addr.String()))
}
}
}
}
}
// honor scheduled/delayed shutdown time
if delay {
app.logger.Debug("shutdown scheduled",
zap.Duration("delay_duration", time.Duration(app.ShutdownDelay)),
zap.Time("time", scheduledTime))
time.Sleep(time.Duration(app.ShutdownDelay))
}
// enforce grace period if configured
if app.GracePeriod > 0 { if app.GracePeriod > 0 {
var cancel context.CancelFunc var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(app.GracePeriod)) ctx, cancel = context.WithTimeout(ctx, time.Duration(app.GracePeriod))
defer cancel() defer cancel()
app.logger.Debug("servers shutting down; grace period initiated", zap.Duration("duration", time.Duration(app.GracePeriod)))
} else {
app.logger.Debug("servers shutting down with eternal grace period")
} }
for _, s := range app.servers {
err := s.Shutdown(ctx) // goroutines aren't guaranteed to be scheduled right away,
if err != nil { // so we'll use one WaitGroup to wait for all the goroutines
return err // to start their server shutdowns, and another to wait for
// them to finish; we'll always block for them to start so
// that when we return the caller can be confident* that the
// old servers are no longer accepting new connections
// (* the scheduler might still pause them right before
// calling Shutdown(), but it's unlikely)
var startedShutdown, finishedShutdown sync.WaitGroup
// these will run in goroutines
stopServer := func(server *Server) {
defer finishedShutdown.Done()
startedShutdown.Done()
if err := server.server.Shutdown(ctx); err != nil {
app.logger.Error("server shutdown",
zap.Error(err),
zap.Strings("addresses", server.Listen))
}
}
stopH3Server := func(server *Server) {
defer finishedShutdown.Done()
startedShutdown.Done()
if server.h3server == nil {
return
}
// TODO: we have to manually close our listeners because quic-go won't
// close listeners it didn't create along with the server itself...
// see https://github.com/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)
if err := server.h3server.Close(); err != nil {
app.logger.Error("HTTP/3 server shutdown",
zap.Error(err),
zap.Strings("addresses", server.Listen))
} }
} }
for _, s := range app.h3servers { for _, server := range app.Servers {
// TODO: CloseGracefully, once implemented upstream startedShutdown.Add(2)
// (see https://github.com/lucas-clemente/quic-go/issues/2103) finishedShutdown.Add(2)
err := s.Close() go stopServer(server)
if err != nil { go stopH3Server(server)
return err
}
} }
// block until all the goroutines have been run by the scheduler;
// this means that they have likely called Shutdown() by now
startedShutdown.Wait()
// if the process is exiting, we need to block here and wait
// for the grace periods to complete, otherwise the process will
// terminate before the servers are finished shutting down; but
// we don't really need to wait for the grace period to finish
// if the process isn't exiting (but note that frequent config
// reloads with long grace periods for a sustained length of time
// may deplete resources)
if caddy.Exiting() {
finishedShutdown.Wait()
}
return nil return nil
} }
+53 -20
View File
@@ -93,6 +93,9 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// https://github.com/caddyserver/caddy/issues/3443) // https://github.com/caddyserver/caddy/issues/3443)
redirDomains := make(map[string][]caddy.NetworkAddress) redirDomains := make(map[string][]caddy.NetworkAddress)
// the log configuration for an HTTPS enabled server
var logCfg *ServerLogConfig
for srvName, srv := range app.Servers { for srvName, srv := range app.Servers {
// as a prerequisite, provision route matchers; this is // as a prerequisite, provision route matchers; this is
// required for all routes on all servers, and must be // required for all routes on all servers, and must be
@@ -152,9 +155,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
return fmt.Errorf("%s: route %d, matcher set %d, matcher %d, host matcher %d: %v", return fmt.Errorf("%s: route %d, matcher set %d, matcher %d, host matcher %d: %v",
srvName, routeIdx, matcherSetIdx, matcherIdx, hostMatcherIdx, err) srvName, routeIdx, matcherSetIdx, matcherIdx, hostMatcherIdx, err)
} }
// only include domain if it's not explicitly skipped and it's not a Tailscale domain if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) {
// (the implicit Tailscale manager module will get those certs at run-time)
if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) && !isTailscaleDomain(d) {
serverDomainSet[d] = struct{}{} serverDomainSet[d] = struct{}{}
} }
} }
@@ -174,6 +175,13 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
continue continue
} }
// clone the logger so we can apply it to the HTTP server
// (not sure if necessary to clone it; but probably safer)
// (we choose one log cfg arbitrarily; not sure which is best)
if srv.Logs != nil {
logCfg = srv.Logs.clone()
}
// for all the hostnames we found, filter them so we have // for all the hostnames we found, filter them so we have
// a deduplicated list of names for which to obtain certs // a deduplicated list of names for which to obtain certs
// (only if cert management not disabled for this server) // (only if cert management not disabled for this server)
@@ -181,6 +189,11 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
app.logger.Warn("skipping automated certificate management for server because it is disabled", zap.String("server_name", srvName)) app.logger.Warn("skipping automated certificate management for server because it is disabled", zap.String("server_name", srvName))
} else { } else {
for d := range serverDomainSet { for d := range serverDomainSet {
// the implicit Tailscale manager module will get its own certs at run-time
if isTailscaleDomain(d) {
continue
}
if certmagic.SubjectQualifiesForCert(d) && if certmagic.SubjectQualifiesForCert(d) &&
!srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) { !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) {
// if a certificate for this name is already loaded, // if a certificate for this name is already loaded,
@@ -365,19 +378,29 @@ redirServersLoop:
// we'll create a new server for all the listener addresses // we'll create a new server for all the listener addresses
// that are unused and serve the remaining redirects from it // that are unused and serve the remaining redirects from it
for _, srv := range app.Servers { for _, srv := range app.Servers {
if srv.hasListenerAddress(redirServerAddr) { // only look at servers which listen on an address which
// find the index of the route after the last route with a host // we want to add redirects to
// matcher, then insert the redirects there, but before any if !srv.hasListenerAddress(redirServerAddr) {
// user-defined catch-all routes continue
// see https://github.com/caddyserver/caddy/issues/3212
insertIndex := srv.findLastRouteWithHostMatcher()
srv.Routes = append(srv.Routes[:insertIndex], append(routes, srv.Routes[insertIndex:]...)...)
// append our catch-all route in case the user didn't define their own
srv.Routes = appendCatchAll(srv.Routes)
continue redirServersLoop
} }
// find the index of the route after the last route with a host
// matcher, then insert the redirects there, but before any
// user-defined catch-all routes
// see https://github.com/caddyserver/caddy/issues/3212
insertIndex := srv.findLastRouteWithHostMatcher()
// add the redirects at the insert index, except for when
// we have a catch-all for HTTPS, in which case the user's
// defined catch-all should take precedence. See #4829
if len(uniqueDomainsForCerts) != 0 {
srv.Routes = append(srv.Routes[:insertIndex], append(routes, srv.Routes[insertIndex:]...)...)
}
// append our catch-all route in case the user didn't define their own
srv.Routes = appendCatchAll(srv.Routes)
continue redirServersLoop
} }
// no server with this listener address exists; // no server with this listener address exists;
@@ -397,6 +420,7 @@ redirServersLoop:
app.Servers["remaining_auto_https_redirects"] = &Server{ app.Servers["remaining_auto_https_redirects"] = &Server{
Listen: redirServerAddrsList, Listen: redirServerAddrsList,
Routes: appendCatchAll(redirRoutes), Routes: appendCatchAll(redirRoutes),
Logs: logCfg,
} }
} }
@@ -436,7 +460,7 @@ func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
} }
} }
// createAutomationPolicy ensures that automated certificates for this // createAutomationPolicies ensures that automated certificates for this
// app are managed properly. This adds up to two automation policies: // app are managed properly. This adds up to two automation policies:
// one for the public names, and one for the internal names. If a catch-all // one for the public names, and one for the internal names. If a catch-all
// automation policy exists, it will be shallow-copied and used as the // automation policy exists, it will be shallow-copied and used as the
@@ -485,6 +509,12 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri
return err return err
} }
ap.Managers = []certmagic.Manager{ts} ap.Managers = []certmagic.Manager{ts}
// must reprovision the automation policy so that the underlying
// CertMagic config knows about the updated Managers
if err := ap.Provision(app.tlsApp); err != nil {
return fmt.Errorf("re-provisioning automation policy: %v", err)
}
} }
// while we're here, is this the catch-all/base policy? // while we're here, is this the catch-all/base policy?
@@ -495,14 +525,17 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri
} }
if basePolicy == nil { if basePolicy == nil {
// no base policy found, we will make one! (with implicit Tailscale integration) // no base policy found; we will make one
basePolicy = new(caddytls.AutomationPolicy)
}
if basePolicy.Managers == nil {
// add implicit Tailscale integration, for harmless convenience
ts, err := implicitTailscale(ctx) ts, err := implicitTailscale(ctx)
if err != nil { if err != nil {
return err return err
} }
basePolicy = &caddytls.AutomationPolicy{ basePolicy.Managers = []certmagic.Manager{ts}
Managers: []certmagic.Manager{ts},
}
} }
// if the basePolicy has an existing ACMEIssuer (particularly to // if the basePolicy has an existing ACMEIssuer (particularly to
+17 -8
View File
@@ -21,6 +21,7 @@ import (
"fmt" "fmt"
weakrand "math/rand" weakrand "math/rand"
"net/http" "net/http"
"strings"
"sync" "sync"
"time" "time"
@@ -94,10 +95,7 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
// if supported, generate a fake password we can compare against if needed // if supported, generate a fake password we can compare against if needed
if hasher, ok := hba.Hash.(Hasher); ok { if hasher, ok := hba.Hash.(Hasher); ok {
hba.fakePassword, err = hasher.Hash([]byte("antitiming"), []byte("fakesalt")) hba.fakePassword = hasher.FakeHash()
if err != nil {
return fmt.Errorf("generating anti-timing password hash: %v", err)
}
} }
repl := caddy.NewReplacer() repl := caddy.NewReplacer()
@@ -117,10 +115,19 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
return fmt.Errorf("account %d: username and password are required", i) return fmt.Errorf("account %d: username and password are required", i)
} }
acct.password, err = base64.StdEncoding.DecodeString(acct.Password) // TODO: Remove support for redundantly-encoded b64-encoded hashes
if err != nil { // Passwords starting with '$' are likely in Modular Crypt Format,
return fmt.Errorf("base64-decoding password: %v", err) // so we don't need to base64 decode them. But historically, we
// required redundant base64, so we try to decode it otherwise.
if strings.HasPrefix(acct.Password, "$") {
acct.password = []byte(acct.Password)
} else {
acct.password, err = base64.StdEncoding.DecodeString(acct.Password)
if err != nil {
return fmt.Errorf("base64-decoding password: %v", err)
}
} }
if acct.Salt != "" { if acct.Salt != "" {
acct.salt, err = base64.StdEncoding.DecodeString(acct.Salt) acct.salt, err = base64.StdEncoding.DecodeString(acct.Salt)
if err != nil { if err != nil {
@@ -271,9 +278,11 @@ type Comparer interface {
// that require a salt). Hashing modules which implement // that require a salt). Hashing modules which implement
// this interface can be used with the hash-password // this interface can be used with the hash-password
// subcommand as well as benefitting from anti-timing // subcommand as well as benefitting from anti-timing
// features. // features. A hasher also returns a fake hash which
// can be used for timing side-channel mitigation.
type Hasher interface { type Hasher interface {
Hash(plaintext, salt []byte) ([]byte, error) Hash(plaintext, salt []byte) ([]byte, error)
FakeHash() []byte
} }
// Account contains a username, password, and salt (if applicable). // Account contains a username, password, and salt (if applicable).
+2 -2
View File
@@ -56,13 +56,13 @@ func (Authentication) CaddyModule() caddy.ModuleInfo {
// Provision sets up a. // Provision sets up a.
func (a *Authentication) Provision(ctx caddy.Context) error { func (a *Authentication) Provision(ctx caddy.Context) error {
a.logger = ctx.Logger(a) a.logger = ctx.Logger()
a.Providers = make(map[string]Authenticator) a.Providers = make(map[string]Authenticator)
mods, err := ctx.LoadModule(a, "ProvidersRaw") mods, err := ctx.LoadModule(a, "ProvidersRaw")
if err != nil { if err != nil {
return fmt.Errorf("loading authentication providers: %v", err) return fmt.Errorf("loading authentication providers: %v", err)
} }
for modName, modIface := range mods.(map[string]interface{}) { for modName, modIface := range mods.(map[string]any) {
a.Providers[modName] = modIface.(Authenticator) a.Providers[modName] = modIface.(Authenticator)
} }
return nil return nil
+7 -4
View File
@@ -42,11 +42,13 @@ hash is written to stdout as a base64 string.
Caddy is attached to a controlling tty, the plaintext will Caddy is attached to a controlling tty, the plaintext will
not be echoed. not be echoed.
--algorithm may be bcrypt or scrypt. If script, the default --algorithm may be bcrypt or scrypt. If scrypt, the default
parameters are used. parameters are used.
Use the --salt flag for algorithms which require a salt to Use the --salt flag for algorithms which require a salt to
be provided (scrypt). be provided (scrypt).
Note that scrypt is deprecated. Please use 'bcrypt' instead.
`, `,
Flags: func() *flag.FlagSet { Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("hash-password", flag.ExitOnError) fs := flag.NewFlagSet("hash-password", flag.ExitOnError)
@@ -112,13 +114,16 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
} }
var hash []byte var hash []byte
var hashString string
switch algorithm { switch algorithm {
case "bcrypt": case "bcrypt":
hash, err = BcryptHash{}.Hash(plaintext, nil) hash, err = BcryptHash{}.Hash(plaintext, nil)
hashString = string(hash)
case "scrypt": case "scrypt":
def := ScryptHash{} def := ScryptHash{}
def.SetDefaults() def.SetDefaults()
hash, err = def.Hash(plaintext, salt) hash, err = def.Hash(plaintext, salt)
hashString = base64.StdEncoding.EncodeToString(hash)
default: default:
return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm) return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm)
} }
@@ -126,9 +131,7 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} }
hashBase64 := base64.StdEncoding.EncodeToString(hash) fmt.Println(hashString)
fmt.Println(hashBase64)
return 0, nil return 0, nil
} }
+20 -1
View File
@@ -16,6 +16,7 @@ package caddyauth
import ( import (
"crypto/subtle" "crypto/subtle"
"encoding/base64"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@@ -55,7 +56,16 @@ func (BcryptHash) Hash(plaintext, _ []byte) ([]byte, error) {
return bcrypt.GenerateFromPassword(plaintext, 14) return bcrypt.GenerateFromPassword(plaintext, 14)
} }
// FakeHash returns a fake hash.
func (BcryptHash) FakeHash() []byte {
// hashed with the following command:
// caddy hash-password --plaintext "antitiming" --algorithm "bcrypt"
return []byte("$2a$14$X3ulqf/iGxnf1k6oMZ.RZeJUoqI9PX2PM4rS5lkIKJXduLGXGPrt6")
}
// ScryptHash implements the scrypt KDF as a hash. // ScryptHash implements the scrypt KDF as a hash.
//
// DEPRECATED, please use 'bcrypt' instead.
type ScryptHash struct { type ScryptHash struct {
// scrypt's N parameter. If unset or 0, a safe default is used. // scrypt's N parameter. If unset or 0, a safe default is used.
N int `json:"N,omitempty"` N int `json:"N,omitempty"`
@@ -80,8 +90,9 @@ func (ScryptHash) CaddyModule() caddy.ModuleInfo {
} }
// Provision sets up s. // Provision sets up s.
func (s *ScryptHash) Provision(_ caddy.Context) error { func (s *ScryptHash) Provision(ctx caddy.Context) error {
s.SetDefaults() s.SetDefaults()
ctx.Logger().Warn("use of 'scrypt' is deprecated, please use 'bcrypt' instead")
return nil return nil
} }
@@ -123,6 +134,14 @@ func (s ScryptHash) Hash(plaintext, salt []byte) ([]byte, error) {
return scrypt.Key(plaintext, salt, s.N, s.R, s.P, s.KeyLength) return scrypt.Key(plaintext, salt, s.N, s.R, s.P, s.KeyLength)
} }
// FakeHash returns a fake hash.
func (ScryptHash) FakeHash() []byte {
// hashed with the following command:
// caddy hash-password --plaintext "antitiming" --salt "fakesalt" --algorithm "scrypt"
bytes, _ := base64.StdEncoding.DecodeString("kFbjiVemlwK/ZS0tS6/UQqEDeaNMigyCs48KEsGUse8=")
return bytes
}
func hashesMatch(pwdHash1, pwdHash2 []byte) bool { func hashesMatch(pwdHash1, pwdHash2 []byte) bool {
return subtle.ConstantTimeCompare(pwdHash1, pwdHash2) == 1 return subtle.ConstantTimeCompare(pwdHash1, pwdHash2) == 1
} }
+35
View File
@@ -20,6 +20,7 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@@ -244,6 +245,40 @@ func SanitizedPathJoin(root, reqPath string) string {
return path return path
} }
// CleanPath cleans path p according to path.Clean(), but only
// merges repeated slashes if collapseSlashes is true, and always
// preserves trailing slashes.
func CleanPath(p string, collapseSlashes bool) string {
if collapseSlashes {
return cleanPath(p)
}
// insert an invalid/impossible URI character into each two consecutive
// slashes to expand empty path segments; then clean the path as usual,
// and then remove the remaining temporary characters.
const tmpCh = 0xff
var sb strings.Builder
for i, ch := range p {
if ch == '/' && i > 0 && p[i-1] == '/' {
sb.WriteByte(tmpCh)
}
sb.WriteRune(ch)
}
halfCleaned := cleanPath(sb.String())
halfCleaned = strings.ReplaceAll(halfCleaned, string([]byte{tmpCh}), "")
return halfCleaned
}
// cleanPath does path.Clean(p) but preserves any trailing slash.
func cleanPath(p string) string {
cleaned := path.Clean(p)
if cleaned != "/" && strings.HasSuffix(p, "/") {
cleaned = cleaned + "/"
}
return cleaned
}
// tlsPlaceholderWrapper is a no-op listener wrapper that marks // tlsPlaceholderWrapper is a no-op listener wrapper that marks
// where the TLS listener should be in a chain of listener wrappers. // where the TLS listener should be in a chain of listener wrappers.
// It should only be used if another listener wrapper must be placed // It should only be used if another listener wrapper must be placed
+57
View File
@@ -92,3 +92,60 @@ func TestSanitizedPathJoin(t *testing.T) {
} }
} }
} }
func TestCleanPath(t *testing.T) {
for i, tc := range []struct {
input string
mergeSlashes bool
expect string
}{
{
input: "/foo",
expect: "/foo",
},
{
input: "/foo/",
expect: "/foo/",
},
{
input: "//foo",
expect: "//foo",
},
{
input: "//foo",
mergeSlashes: true,
expect: "/foo",
},
{
input: "/foo//bar/",
mergeSlashes: true,
expect: "/foo/bar/",
},
{
input: "/foo/./.././bar",
expect: "/bar",
},
{
input: "/foo//./..//./bar",
expect: "/foo//bar",
},
{
input: "/foo///./..//./bar",
expect: "/foo///bar",
},
{
input: "/foo///./..//.",
expect: "/foo//",
},
{
input: "/foo//./bar",
expect: "/foo//bar",
},
} {
actual := CleanPath(tc.input, tc.mergeSlashes)
if actual != tc.expect {
t.Errorf("Test %d [input='%s' mergeSlashes=%t]: Got '%s', expected '%s'",
i, tc.input, tc.mergeSlashes, actual, tc.expect)
}
}
}
+450 -39
View File
@@ -17,6 +17,7 @@ package caddyhttp
import ( import (
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"reflect" "reflect"
@@ -27,14 +28,17 @@ import (
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls" "github.com/google/cel-go/common"
"github.com/google/cel-go/common/operators"
"github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits" "github.com/google/cel-go/common/types/traits"
"github.com/google/cel-go/ext" "github.com/google/cel-go/ext"
"github.com/google/cel-go/interpreter"
"github.com/google/cel-go/interpreter/functions" "github.com/google/cel-go/interpreter/functions"
"github.com/google/cel-go/parser"
"go.uber.org/zap"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"google.golang.org/protobuf/proto"
) )
func init() { func init() {
@@ -62,6 +66,8 @@ type MatchExpression struct {
expandedExpr string expandedExpr string
prg cel.Program prg cel.Program
ta ref.TypeAdapter ta ref.TypeAdapter
log *zap.Logger
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
@@ -83,7 +89,9 @@ func (m *MatchExpression) UnmarshalJSON(data []byte) error {
} }
// Provision sets ups m. // Provision sets ups m.
func (m *MatchExpression) Provision(_ caddy.Context) error { func (m *MatchExpression) Provision(ctx caddy.Context) error {
m.log = ctx.Logger()
// replace placeholders with a function call - this is just some // replace placeholders with a function call - this is just some
// light (and possibly naïve) syntactic sugar // light (and possibly naïve) syntactic sugar
m.expandedExpr = placeholderRegexp.ReplaceAllString(m.Expr, placeholderExpansion) m.expandedExpr = placeholderRegexp.ReplaceAllString(m.Expr, placeholderExpansion)
@@ -91,17 +99,40 @@ func (m *MatchExpression) Provision(_ caddy.Context) error {
// our type adapter expands CEL's standard type support // our type adapter expands CEL's standard type support
m.ta = celTypeAdapter{} m.ta = celTypeAdapter{}
// initialize the CEL libraries from the Matcher implementations which
// have been configured to support CEL.
matcherLibProducers := []CELLibraryProducer{}
for _, info := range caddy.GetModules("http.matchers") {
p, ok := info.New().(CELLibraryProducer)
if ok {
matcherLibProducers = append(matcherLibProducers, p)
}
}
// Assemble the compilation and program options from the different library
// producers into a single cel.Library implementation.
matcherEnvOpts := []cel.EnvOption{}
matcherProgramOpts := []cel.ProgramOption{}
for _, producer := range matcherLibProducers {
l, err := producer.CELLibrary(ctx)
if err != nil {
return fmt.Errorf("error initializing CEL library for %T: %v", producer, err)
}
matcherEnvOpts = append(matcherEnvOpts, l.CompileOptions()...)
matcherProgramOpts = append(matcherProgramOpts, l.ProgramOptions()...)
}
matcherLib := cel.Lib(NewMatcherCELLibrary(matcherEnvOpts, matcherProgramOpts))
// create the CEL environment // create the CEL environment
env, err := cel.NewEnv( env, err := cel.NewEnv(
cel.Declarations( cel.Function(placeholderFuncName, cel.SingletonBinaryImpl(m.caddyPlaceholderFunc), cel.Overload(
decls.NewVar("request", httpRequestObjectType), placeholderFuncName+"_httpRequest_string",
decls.NewFunction(placeholderFuncName, []*cel.Type{httpRequestObjectType, cel.StringType},
decls.NewOverload(placeholderFuncName+"_httpRequest_string", cel.AnyType,
[]*exprpb.Type{httpRequestObjectType, decls.String}, )),
decls.Any)), cel.Variable("request", httpRequestObjectType),
),
cel.CustomTypeAdapter(m.ta), cel.CustomTypeAdapter(m.ta),
ext.Strings(), ext.Strings(),
matcherLib,
) )
if err != nil { if err != nil {
return fmt.Errorf("setting up CEL environment: %v", err) return fmt.Errorf("setting up CEL environment: %v", err)
@@ -109,26 +140,18 @@ func (m *MatchExpression) Provision(_ caddy.Context) error {
// parse and type-check the expression // parse and type-check the expression
checked, issues := env.Compile(m.expandedExpr) checked, issues := env.Compile(m.expandedExpr)
if issues != nil && issues.Err() != nil { if issues.Err() != nil {
return fmt.Errorf("compiling CEL program: %s", issues.Err()) return fmt.Errorf("compiling CEL program: %s", issues.Err())
} }
// request matching is a boolean operation, so we don't really know // request matching is a boolean operation, so we don't really know
// what to do if the expression returns a non-boolean type // what to do if the expression returns a non-boolean type
if !proto.Equal(checked.ResultType(), decls.Bool) { if checked.OutputType() != cel.BoolType {
return fmt.Errorf("CEL request matcher expects return type of bool, not %s", checked.ResultType()) return fmt.Errorf("CEL request matcher expects return type of bool, not %s", checked.OutputType())
} }
// compile the "program" // compile the "program"
m.prg, err = env.Program(checked, m.prg, err = env.Program(checked, cel.EvalOptions(cel.OptOptimize))
cel.Functions(
&functions.Overload{
Operator: placeholderFuncName,
Binary: m.caddyPlaceholderFunc,
},
),
)
if err != nil { if err != nil {
return fmt.Errorf("compiling CEL program: %s", err) return fmt.Errorf("compiling CEL program: %s", err)
} }
@@ -137,14 +160,17 @@ func (m *MatchExpression) Provision(_ caddy.Context) error {
// Match returns true if r matches m. // Match returns true if r matches m.
func (m MatchExpression) Match(r *http.Request) bool { func (m MatchExpression) Match(r *http.Request) bool {
out, _, _ := m.prg.Eval(map[string]interface{}{ celReq := celHTTPRequest{r}
"request": celHTTPRequest{r}, out, _, err := m.prg.Eval(celReq)
}) if err != nil {
m.log.Error("evaluating expression", zap.Error(err))
SetVar(r.Context(), MatcherErrorVarKey, err)
return false
}
if outBool, ok := out.Value().(bool); ok { if outBool, ok := out.Value().(bool); ok {
return outBool return outBool
} }
return false return false
} }
// UnmarshalCaddyfile implements caddyfile.Unmarshaler. // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
@@ -166,13 +192,15 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val {
if !ok { if !ok {
return types.NewErr( return types.NewErr(
"invalid request of type '%v' to "+placeholderFuncName+"(request, placeholderVarName)", "invalid request of type '%v' to "+placeholderFuncName+"(request, placeholderVarName)",
lhs.Type()) lhs.Type(),
)
} }
phStr, ok := rhs.(types.String) phStr, ok := rhs.(types.String)
if !ok { if !ok {
return types.NewErr( return types.NewErr(
"invalid placeholder variable name of type '%v' to "+placeholderFuncName+"(request, placeholderVarName)", "invalid placeholder variable name of type '%v' to "+placeholderFuncName+"(request, placeholderVarName)",
rhs.Type()) rhs.Type(),
)
} }
repl := celReq.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl := celReq.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
@@ -184,11 +212,24 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val {
// httpRequestCELType is the type representation of a native HTTP request. // httpRequestCELType is the type representation of a native HTTP request.
var httpRequestCELType = types.NewTypeValue("http.Request", traits.ReceiverType) var httpRequestCELType = types.NewTypeValue("http.Request", traits.ReceiverType)
// cellHTTPRequest wraps an http.Request with // celHTTPRequest wraps an http.Request with ref.Val interface methods.
// methods to satisfy the ref.Val interface. //
// This type also implements the interpreter.Activation interface which
// drops allocation costs for CEL expression evaluations by roughly half.
type celHTTPRequest struct{ *http.Request } type celHTTPRequest struct{ *http.Request }
func (cr celHTTPRequest) ConvertToNative(typeDesc reflect.Type) (interface{}, error) { func (cr celHTTPRequest) ResolveName(name string) (any, bool) {
if name == "request" {
return cr, true
}
return nil, false
}
func (cr celHTTPRequest) Parent() interpreter.Activation {
return nil
}
func (cr celHTTPRequest) ConvertToNative(typeDesc reflect.Type) (any, error) {
return cr.Request, nil return cr.Request, nil
} }
func (celHTTPRequest) ConvertToType(typeVal ref.Type) ref.Val { func (celHTTPRequest) ConvertToType(typeVal ref.Type) ref.Val {
@@ -200,8 +241,8 @@ func (cr celHTTPRequest) Equal(other ref.Val) ref.Val {
} }
return types.ValOrErr(other, "%v is not comparable type", other) return types.ValOrErr(other, "%v is not comparable type", other)
} }
func (celHTTPRequest) Type() ref.Type { return httpRequestCELType } func (celHTTPRequest) Type() ref.Type { return httpRequestCELType }
func (cr celHTTPRequest) Value() interface{} { return cr } func (cr celHTTPRequest) Value() any { return cr }
var pkixNameCELType = types.NewTypeValue("pkix.Name", traits.ReceiverType) var pkixNameCELType = types.NewTypeValue("pkix.Name", traits.ReceiverType)
@@ -209,7 +250,7 @@ var pkixNameCELType = types.NewTypeValue("pkix.Name", traits.ReceiverType)
// methods to satisfy the ref.Val interface. // methods to satisfy the ref.Val interface.
type celPkixName struct{ *pkix.Name } type celPkixName struct{ *pkix.Name }
func (pn celPkixName) ConvertToNative(typeDesc reflect.Type) (interface{}, error) { func (pn celPkixName) ConvertToNative(typeDesc reflect.Type) (any, error) {
return pn.Name, nil return pn.Name, nil
} }
func (celPkixName) ConvertToType(typeVal ref.Type) ref.Val { func (celPkixName) ConvertToType(typeVal ref.Type) ref.Val {
@@ -221,13 +262,13 @@ func (pn celPkixName) Equal(other ref.Val) ref.Val {
} }
return types.ValOrErr(other, "%v is not comparable type", other) return types.ValOrErr(other, "%v is not comparable type", other)
} }
func (celPkixName) Type() ref.Type { return pkixNameCELType } func (celPkixName) Type() ref.Type { return pkixNameCELType }
func (pn celPkixName) Value() interface{} { return pn } func (pn celPkixName) Value() any { return pn }
// celTypeAdapter can adapt our custom types to a CEL value. // celTypeAdapter can adapt our custom types to a CEL value.
type celTypeAdapter struct{} type celTypeAdapter struct{}
func (celTypeAdapter) NativeToValue(value interface{}) ref.Val { func (celTypeAdapter) NativeToValue(value any) ref.Val {
switch v := value.(type) { switch v := value.(type) {
case celHTTPRequest: case celHTTPRequest:
return v return v
@@ -241,15 +282,385 @@ func (celTypeAdapter) NativeToValue(value interface{}) ref.Val {
return types.DefaultTypeAdapter.NativeToValue(value) return types.DefaultTypeAdapter.NativeToValue(value)
} }
// CELLibraryProducer provide CEL libraries that expose a Matcher
// implementation as a first class function within the CEL expression
// matcher.
type CELLibraryProducer interface {
// CELLibrary creates a cel.Library which makes it possible to use the
// target object within CEL expression matchers.
CELLibrary(caddy.Context) (cel.Library, error)
}
// CELMatcherImpl creates a new cel.Library based on the following pieces of
// data:
//
// - macroName: the function name to be used within CEL. This will be a macro
// and not a function proper.
// - funcName: the function overload name generated by the CEL macro used to
// represent the matcher.
// - matcherDataTypes: the argument types to the macro.
// - fac: a matcherFactory implementation which converts from CEL constant
// values to a Matcher instance.
//
// Note, macro names and function names must not collide with other macros or
// functions exposed within CEL expressions, or an error will be produced
// during the expression matcher plan time.
//
// The existing CELMatcherImpl support methods are configured to support a
// limited set of function signatures. For strong type validation you may need
// to provide a custom macro which does a more detailed analysis of the CEL
// literal provided to the macro as an argument.
func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fac CELMatcherFactory) (cel.Library, error) {
requestType := cel.ObjectType("http.Request")
var macro parser.Macro
switch len(matcherDataTypes) {
case 1:
matcherDataType := matcherDataTypes[0]
switch matcherDataType.String() {
case "list(string)":
macro = parser.NewGlobalVarArgMacro(macroName, celMatcherStringListMacroExpander(funcName))
case cel.StringType.String():
macro = parser.NewGlobalMacro(macroName, 1, celMatcherStringMacroExpander(funcName))
case CELTypeJSON.String():
macro = parser.NewGlobalMacro(macroName, 1, celMatcherJSONMacroExpander(funcName))
default:
return nil, fmt.Errorf("unsupported matcher data type: %s", matcherDataType)
}
case 2:
if matcherDataTypes[0] == cel.StringType && matcherDataTypes[1] == cel.StringType {
macro = parser.NewGlobalMacro(macroName, 2, celMatcherStringListMacroExpander(funcName))
matcherDataTypes = []*cel.Type{cel.ListType(cel.StringType)}
} else {
return nil, fmt.Errorf("unsupported matcher data type: %s, %s", matcherDataTypes[0], matcherDataTypes[1])
}
case 3:
if matcherDataTypes[0] == cel.StringType && matcherDataTypes[1] == cel.StringType && matcherDataTypes[2] == cel.StringType {
macro = parser.NewGlobalMacro(macroName, 3, celMatcherStringListMacroExpander(funcName))
matcherDataTypes = []*cel.Type{cel.ListType(cel.StringType)}
} else {
return nil, fmt.Errorf("unsupported matcher data type: %s, %s, %s", matcherDataTypes[0], matcherDataTypes[1], matcherDataTypes[2])
}
}
envOptions := []cel.EnvOption{
cel.Macros(macro),
cel.Function(funcName,
cel.Overload(funcName, append([]*cel.Type{requestType}, matcherDataTypes...), cel.BoolType),
cel.SingletonBinaryImpl(CELMatcherRuntimeFunction(funcName, fac))),
}
programOptions := []cel.ProgramOption{
cel.CustomDecorator(CELMatcherDecorator(funcName, fac)),
}
return NewMatcherCELLibrary(envOptions, programOptions), nil
}
// CELMatcherFactory converts a constant CEL value into a RequestMatcher.
type CELMatcherFactory func(data ref.Val) (RequestMatcher, error)
// matcherCELLibrary is a simplistic configurable cel.Library implementation.
type matcherCELLibary struct {
envOptions []cel.EnvOption
programOptions []cel.ProgramOption
}
// NewMatcherCELLibrary creates a matcherLibrary from option setes.
func NewMatcherCELLibrary(envOptions []cel.EnvOption, programOptions []cel.ProgramOption) cel.Library {
return &matcherCELLibary{
envOptions: envOptions,
programOptions: programOptions,
}
}
func (lib *matcherCELLibary) CompileOptions() []cel.EnvOption {
return lib.envOptions
}
func (lib *matcherCELLibary) ProgramOptions() []cel.ProgramOption {
return lib.programOptions
}
// CELMatcherDecorator matches a call overload generated by a CEL macro
// that takes a single argument, and optimizes the implementation to precompile
// the matcher and return a function that references the precompiled and
// provisioned matcher.
func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.InterpretableDecorator {
return func(i interpreter.Interpretable) (interpreter.Interpretable, error) {
call, ok := i.(interpreter.InterpretableCall)
if !ok {
return i, nil
}
if call.OverloadID() != funcName {
return i, nil
}
callArgs := call.Args()
reqAttr, ok := callArgs[0].(interpreter.InterpretableAttribute)
if !ok {
return nil, errors.New("missing 'request' argument")
}
nsAttr, ok := reqAttr.Attr().(interpreter.NamespacedAttribute)
if !ok {
return nil, errors.New("missing 'request' argument")
}
varNames := nsAttr.CandidateVariableNames()
if len(varNames) != 1 || len(varNames) == 1 && varNames[0] != "request" {
return nil, errors.New("missing 'request' argument")
}
matcherData, ok := callArgs[1].(interpreter.InterpretableConst)
if !ok {
// If the matcher arguments are not constant, then this means
// they contain a Caddy placeholder reference and the evaluation
// and matcher provisioning should be handled at dynamically.
return i, nil
}
matcher, err := fac(matcherData.Value())
if err != nil {
return nil, err
}
return interpreter.NewCall(
i.ID(), funcName, funcName+"_opt",
[]interpreter.Interpretable{reqAttr},
func(args ...ref.Val) ref.Val {
// The request value, guaranteed to be of type celHTTPRequest
celReq := args[0]
// If needed this call could be changed to convert the value
// to a *http.Request using CEL's ConvertToNative method.
httpReq := celReq.Value().(celHTTPRequest)
return types.Bool(matcher.Match(httpReq.Request))
},
), nil
}
}
// CELMatcherRuntimeFunction creates a function binding for when the input to the matcher
// is dynamically resolved rather than a set of static constant values.
func CELMatcherRuntimeFunction(funcName string, fac CELMatcherFactory) functions.BinaryOp {
return func(celReq, matcherData ref.Val) ref.Val {
matcher, err := fac(matcherData)
if err != nil {
return types.NewErr(err.Error())
}
httpReq := celReq.Value().(celHTTPRequest)
return types.Bool(matcher.Match(httpReq.Request))
}
}
// celMatcherStringListMacroExpander validates that the macro is called
// with a variable number of string arguments (at least one).
//
// The arguments are collected into a single list argument the following
// function call returned: <funcName>(request, [args])
func celMatcherStringListMacroExpander(funcName string) parser.MacroExpander {
return func(eh parser.ExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) {
matchArgs := []*exprpb.Expr{}
if len(args) == 0 {
return nil, &common.Error{
Message: "matcher requires at least one argument",
}
}
for _, arg := range args {
if isCELStringExpr(arg) {
matchArgs = append(matchArgs, arg)
} else {
return nil, &common.Error{
Location: eh.OffsetLocation(arg.GetId()),
Message: "matcher arguments must be string constants",
}
}
}
return eh.GlobalCall(funcName, eh.Ident("request"), eh.NewList(matchArgs...)), nil
}
}
// celMatcherStringMacroExpander validates that the macro is called a single
// string argument.
//
// The following function call is returned: <funcName>(request, arg)
func celMatcherStringMacroExpander(funcName string) parser.MacroExpander {
return func(eh parser.ExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) {
if len(args) != 1 {
return nil, &common.Error{
Message: "matcher requires one argument",
}
}
if isCELStringExpr(args[0]) {
return eh.GlobalCall(funcName, eh.Ident("request"), args[0]), nil
}
return nil, &common.Error{
Location: eh.OffsetLocation(args[0].GetId()),
Message: "matcher argument must be a string literal",
}
}
}
// celMatcherStringMacroExpander validates that the macro is called a single
// map literal argument.
//
// The following function call is returned: <funcName>(request, arg)
func celMatcherJSONMacroExpander(funcName string) parser.MacroExpander {
return func(eh parser.ExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) {
if len(args) != 1 {
return nil, &common.Error{
Message: "matcher requires a map literal argument",
}
}
arg := args[0]
switch arg.GetExprKind().(type) {
case *exprpb.Expr_StructExpr:
structExpr := arg.GetStructExpr()
if structExpr.GetMessageName() != "" {
return nil, &common.Error{
Location: eh.OffsetLocation(arg.GetId()),
Message: fmt.Sprintf(
"matcher input must be a map literal, not a %s",
structExpr.GetMessageName(),
),
}
}
for _, entry := range structExpr.GetEntries() {
isStringPlaceholder := isCELStringExpr(entry.GetMapKey())
if !isStringPlaceholder {
return nil, &common.Error{
Location: eh.OffsetLocation(entry.GetId()),
Message: "matcher map keys must be string literals",
}
}
isStringListPlaceholder := isCELStringExpr(entry.GetValue()) ||
isCELStringListLiteral(entry.GetValue())
if !isStringListPlaceholder {
return nil, &common.Error{
Location: eh.OffsetLocation(entry.GetValue().GetId()),
Message: "matcher map values must be string or list literals",
}
}
}
return eh.GlobalCall(funcName, eh.Ident("request"), arg), nil
}
return nil, &common.Error{
Location: eh.OffsetLocation(arg.GetId()),
Message: "matcher requires a map literal argument",
}
}
}
// CELValueToMapStrList converts a CEL value to a map[string][]string
//
// Earlier validation stages should guarantee that the value has this type
// at compile time, and that the runtime value type is map[string]any.
// The reason for the slight difference in value type is that CEL allows for
// map literals containing heterogeneous values, in this case string and list
// of string.
func CELValueToMapStrList(data ref.Val) (map[string][]string, error) {
mapStrType := reflect.TypeOf(map[string]any{})
mapStrRaw, err := data.ConvertToNative(mapStrType)
if err != nil {
return nil, err
}
mapStrIface := mapStrRaw.(map[string]any)
mapStrListStr := make(map[string][]string, len(mapStrIface))
for k, v := range mapStrIface {
switch val := v.(type) {
case string:
mapStrListStr[k] = []string{val}
case types.String:
mapStrListStr[k] = []string{string(val)}
case []string:
mapStrListStr[k] = val
case []ref.Val:
convVals := make([]string, len(val))
for i, elem := range val {
strVal, ok := elem.(types.String)
if !ok {
return nil, fmt.Errorf("unsupported value type in header match: %T", val)
}
convVals[i] = string(strVal)
}
mapStrListStr[k] = convVals
default:
return nil, fmt.Errorf("unsupported value type in header match: %T", val)
}
}
return mapStrListStr, nil
}
// isCELStringExpr indicates whether the expression is a supported string expression
func isCELStringExpr(e *exprpb.Expr) bool {
return isCELStringLiteral(e) || isCELCaddyPlaceholderCall(e) || isCELConcatCall(e)
}
// isCELStringLiteral returns whether the expression is a CEL string literal.
func isCELStringLiteral(e *exprpb.Expr) bool {
switch e.GetExprKind().(type) {
case *exprpb.Expr_ConstExpr:
constant := e.GetConstExpr()
switch constant.GetConstantKind().(type) {
case *exprpb.Constant_StringValue:
return true
}
}
return false
}
// isCELCaddyPlaceholderCall returns whether the expression is a caddy placeholder call.
func isCELCaddyPlaceholderCall(e *exprpb.Expr) bool {
switch e.GetExprKind().(type) {
case *exprpb.Expr_CallExpr:
call := e.GetCallExpr()
if call.GetFunction() == "caddyPlaceholder" {
return true
}
}
return false
}
// isCELConcatCall tests whether the expression is a concat function (+) with string, placeholder, or
// other concat call arguments.
func isCELConcatCall(e *exprpb.Expr) bool {
switch e.GetExprKind().(type) {
case *exprpb.Expr_CallExpr:
call := e.GetCallExpr()
if call.GetTarget() != nil {
return false
}
if call.GetFunction() != operators.Add {
return false
}
for _, arg := range call.GetArgs() {
if !isCELStringExpr(arg) {
return false
}
}
return true
}
return false
}
// isCELStringListLiteral returns whether the expression resolves to a list literal
// containing only string constants or a placeholder call.
func isCELStringListLiteral(e *exprpb.Expr) bool {
switch e.GetExprKind().(type) {
case *exprpb.Expr_ListExpr:
list := e.GetListExpr()
for _, elem := range list.GetElements() {
if !isCELStringExpr(elem) {
return false
}
}
return true
}
return false
}
// Variables used for replacing Caddy placeholders in CEL // Variables used for replacing Caddy placeholders in CEL
// expressions with a proper CEL function call; this is // expressions with a proper CEL function call; this is
// just for syntactic sugar. // just for syntactic sugar.
var ( var (
placeholderRegexp = regexp.MustCompile(`{([\w.-]+)}`) placeholderRegexp = regexp.MustCompile(`{([a-zA-Z][\w.-]+)}`)
placeholderExpansion = `caddyPlaceholder(request, "${1}")` placeholderExpansion = `caddyPlaceholder(request, "${1}")`
CELTypeJSON = cel.MapType(cel.StringType, cel.DynType)
) )
var httpRequestObjectType = decls.NewObjectType("http.Request") var httpRequestObjectType = cel.ObjectType("http.Request")
// The name of the CEL function which accesses Replacer values. // The name of the CEL function which accesses Replacer values.
const placeholderFuncName = "caddyPlaceholder" const placeholderFuncName = "caddyPlaceholder"
+450 -68
View File
@@ -19,12 +19,462 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
) )
var (
clientCert = []byte(`-----BEGIN CERTIFICATE-----
MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk
eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG
A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF
z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+
fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ
BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A
AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+
eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH
9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g=
-----END CERTIFICATE-----`)
matcherTests = []struct {
name string
expression *MatchExpression
urlTarget string
httpMethod string
httpHeader *http.Header
wantErr bool
wantResult bool
clientCertificate []byte
}{
{
name: "boolean matches succeed for placeholder http.request.tls.client.subject",
expression: &MatchExpression{
Expr: "{http.request.tls.client.subject} == 'CN=client.localdomain'",
},
clientCertificate: clientCert,
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "header matches (MatchHeader)",
expression: &MatchExpression{
Expr: `header({'Field': 'foo'})`,
},
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
wantResult: true,
},
{
name: "header error (MatchHeader)",
expression: &MatchExpression{
Expr: `header('foo')`,
},
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
wantErr: true,
},
{
name: "header_regexp matches (MatchHeaderRE)",
expression: &MatchExpression{
Expr: `header_regexp('Field', 'fo{2}')`,
},
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
wantResult: true,
},
{
name: "header_regexp matches with name (MatchHeaderRE)",
expression: &MatchExpression{
Expr: `header_regexp('foo', 'Field', 'fo{2}')`,
},
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
wantResult: true,
},
{
name: "header_regexp does not match (MatchHeaderRE)",
expression: &MatchExpression{
Expr: `header_regexp('foo', 'Nope', 'fo{2}')`,
},
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
wantResult: false,
},
{
name: "header_regexp error (MatchHeaderRE)",
expression: &MatchExpression{
Expr: `header_regexp('foo')`,
},
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
wantErr: true,
},
{
name: "host matches localhost (MatchHost)",
expression: &MatchExpression{
Expr: `host('localhost')`,
},
urlTarget: "http://localhost",
wantResult: true,
},
{
name: "host matches (MatchHost)",
expression: &MatchExpression{
Expr: `host('*.example.com')`,
},
urlTarget: "https://foo.example.com",
wantResult: true,
},
{
name: "host does not match (MatchHost)",
expression: &MatchExpression{
Expr: `host('example.net', '*.example.com')`,
},
urlTarget: "https://foo.example.org",
wantResult: false,
},
{
name: "host error (MatchHost)",
expression: &MatchExpression{
Expr: `host(80)`,
},
urlTarget: "http://localhost:80",
wantErr: true,
},
{
name: "method does not match (MatchMethod)",
expression: &MatchExpression{
Expr: `method('PUT')`,
},
urlTarget: "https://foo.example.com",
httpMethod: "GET",
wantResult: false,
},
{
name: "method matches (MatchMethod)",
expression: &MatchExpression{
Expr: `method('DELETE', 'PUT', 'POST')`,
},
urlTarget: "https://foo.example.com",
httpMethod: "PUT",
wantResult: true,
},
{
name: "method error not enough arguments (MatchMethod)",
expression: &MatchExpression{
Expr: `method()`,
},
urlTarget: "https://foo.example.com",
httpMethod: "PUT",
wantErr: true,
},
{
name: "path matches substring (MatchPath)",
expression: &MatchExpression{
Expr: `path('*substring*')`,
},
urlTarget: "https://example.com/foo/substring/bar.txt",
wantResult: true,
},
{
name: "path does not match (MatchPath)",
expression: &MatchExpression{
Expr: `path('/foo')`,
},
urlTarget: "https://example.com/foo/bar",
wantResult: false,
},
{
name: "path matches end url fragment (MatchPath)",
expression: &MatchExpression{
Expr: `path('/foo')`,
},
urlTarget: "https://example.com/FOO",
wantResult: true,
},
{
name: "path matches end fragment with substring prefix (MatchPath)",
expression: &MatchExpression{
Expr: `path('/foo*')`,
},
urlTarget: "https://example.com/FOOOOO",
wantResult: true,
},
{
name: "path matches one of multiple (MatchPath)",
expression: &MatchExpression{
Expr: `path('/foo', '/foo/*', '/bar', '/bar/*', '/baz', '/baz*')`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "path_regexp with empty regex matches empty path (MatchPathRE)",
expression: &MatchExpression{
Expr: `path_regexp('')`,
},
urlTarget: "https://example.com/",
wantResult: true,
},
{
name: "path_regexp with slash regex matches empty path (MatchPathRE)",
expression: &MatchExpression{
Expr: `path_regexp('/')`,
},
urlTarget: "https://example.com/",
wantResult: true,
},
{
name: "path_regexp matches end url fragment (MatchPathRE)",
expression: &MatchExpression{
Expr: `path_regexp('^/foo')`,
},
urlTarget: "https://example.com/foo/",
wantResult: true,
},
{
name: "path_regexp does not match fragment at end (MatchPathRE)",
expression: &MatchExpression{
Expr: `path_regexp('bar_at_start', '^/bar')`,
},
urlTarget: "https://example.com/foo/bar",
wantResult: false,
},
{
name: "protocol matches (MatchProtocol)",
expression: &MatchExpression{
Expr: `protocol('HTTPs')`,
},
urlTarget: "https://example.com",
wantResult: true,
},
{
name: "protocol does not match (MatchProtocol)",
expression: &MatchExpression{
Expr: `protocol('grpc')`,
},
urlTarget: "https://example.com",
wantResult: false,
},
{
name: "protocol invocation error no args (MatchProtocol)",
expression: &MatchExpression{
Expr: `protocol()`,
},
urlTarget: "https://example.com",
wantErr: true,
},
{
name: "protocol invocation error too many args (MatchProtocol)",
expression: &MatchExpression{
Expr: `protocol('grpc', 'https')`,
},
urlTarget: "https://example.com",
wantErr: true,
},
{
name: "protocol invocation error wrong arg type (MatchProtocol)",
expression: &MatchExpression{
Expr: `protocol(true)`,
},
urlTarget: "https://example.com",
wantErr: true,
},
{
name: "query does not match against a specific value (MatchQuery)",
expression: &MatchExpression{
Expr: `query({"debug": "1"})`,
},
urlTarget: "https://example.com/foo",
wantResult: false,
},
{
name: "query matches against a specific value (MatchQuery)",
expression: &MatchExpression{
Expr: `query({"debug": "1"})`,
},
urlTarget: "https://example.com/foo/?debug=1",
wantResult: true,
},
{
name: "query matches against multiple values (MatchQuery)",
expression: &MatchExpression{
Expr: `query({"debug": ["0", "1", {http.request.uri.query.debug}+"1"]})`,
},
urlTarget: "https://example.com/foo/?debug=1",
wantResult: true,
},
{
name: "query matches against a wildcard (MatchQuery)",
expression: &MatchExpression{
Expr: `query({"debug": ["*"]})`,
},
urlTarget: "https://example.com/foo/?debug=something",
wantResult: true,
},
{
name: "query matches against a placeholder value (MatchQuery)",
expression: &MatchExpression{
Expr: `query({"debug": {http.request.uri.query.debug}})`,
},
urlTarget: "https://example.com/foo/?debug=1",
wantResult: true,
},
{
name: "query error bad map key type (MatchQuery)",
expression: &MatchExpression{
Expr: `query({1: "1"})`,
},
urlTarget: "https://example.com/foo",
wantErr: true,
},
{
name: "query error typed struct instead of map (MatchQuery)",
expression: &MatchExpression{
Expr: `query(Message{field: "1"})`,
},
urlTarget: "https://example.com/foo",
wantErr: true,
},
{
name: "query error bad map value type (MatchQuery)",
expression: &MatchExpression{
Expr: `query({"debug": 1})`,
},
urlTarget: "https://example.com/foo/?debug=1",
wantErr: true,
},
{
name: "query error no args (MatchQuery)",
expression: &MatchExpression{
Expr: `query()`,
},
urlTarget: "https://example.com/foo/?debug=1",
wantErr: true,
},
{
name: "remote_ip error no args (MatchRemoteIP)",
expression: &MatchExpression{
Expr: `remote_ip()`,
},
urlTarget: "https://example.com/foo",
wantErr: true,
},
{
name: "remote_ip single IP match (MatchRemoteIP)",
expression: &MatchExpression{
Expr: `remote_ip('192.0.2.1')`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "remote_ip forwarded (MatchRemoteIP)",
expression: &MatchExpression{
Expr: `remote_ip('forwarded', '192.0.2.1')`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "remote_ip forwarded not first (MatchRemoteIP)",
expression: &MatchExpression{
Expr: `remote_ip('192.0.2.1', 'forwarded')`,
},
urlTarget: "https://example.com/foo",
wantErr: true,
},
}
)
func TestMatchExpressionMatch(t *testing.T) {
for _, tst := range matcherTests {
tc := tst
t.Run(tc.name, func(t *testing.T) {
err := tc.expression.Provision(caddy.Context{})
if err != nil {
if !tc.wantErr {
t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tc.wantErr)
}
return
}
req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil)
if tc.httpHeader != nil {
req.Header = *tc.httpHeader
}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
if tc.clientCertificate != nil {
block, _ := pem.Decode(clientCert)
if block == nil {
t.Fatalf("failed to decode PEM certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("failed to decode PEM certificate: %v", err)
}
req.TLS = &tls.ConnectionState{
PeerCertificates: []*x509.Certificate{cert},
}
}
if tc.expression.Match(req) != tc.wantResult {
t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
}
})
}
}
func BenchmarkMatchExpressionMatch(b *testing.B) {
for _, tst := range matcherTests {
tc := tst
if tc.wantErr {
continue
}
b.Run(tst.name, func(b *testing.B) {
tc.expression.Provision(caddy.Context{})
req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil)
if tc.httpHeader != nil {
req.Header = *tc.httpHeader
}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
if tc.clientCertificate != nil {
block, _ := pem.Decode(clientCert)
if block == nil {
b.Fatalf("failed to decode PEM certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
b.Fatalf("failed to decode PEM certificate: %v", err)
}
req.TLS = &tls.ConnectionState{
PeerCertificates: []*x509.Certificate{cert},
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
tc.expression.Match(req)
}
})
}
}
func TestMatchExpressionProvision(t *testing.T) { func TestMatchExpressionProvision(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -54,71 +504,3 @@ func TestMatchExpressionProvision(t *testing.T) {
}) })
} }
} }
func TestMatchExpressionMatch(t *testing.T) {
clientCert := []byte(`-----BEGIN CERTIFICATE-----
MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk
eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG
A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF
z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+
fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ
BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A
AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+
eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH
9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g=
-----END CERTIFICATE-----`)
tests := []struct {
name string
expression *MatchExpression
wantErr bool
wantResult bool
clientCertificate []byte
}{
{
name: "boolean matches succeed for placeholder http.request.tls.client.subject",
expression: &MatchExpression{
Expr: "{http.request.tls.client.subject} == 'CN=client.localdomain'",
},
clientCertificate: clientCert,
wantResult: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.expression.Provision(caddy.Context{}); (err != nil) != tt.wantErr {
t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tt.wantErr)
}
req := httptest.NewRequest("GET", "https://example.com/foo", nil)
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
if tt.clientCertificate != nil {
block, _ := pem.Decode(clientCert)
if block == nil {
t.Fatalf("failed to decode PEM certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("failed to decode PEM certificate: %v", err)
}
req.TLS = &tls.ConnectionState{
PeerCertificates: []*x509.Certificate{cert},
}
}
if tt.expression.Match(req) != tt.wantResult {
t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tt.wantResult, tt.expression)
}
})
}
}
+51 -78
View File
@@ -20,7 +20,6 @@
package encode package encode
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"math" "math"
@@ -71,7 +70,7 @@ func (enc *Encode) Provision(ctx caddy.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("loading encoder modules: %v", err) return fmt.Errorf("loading encoder modules: %v", err)
} }
for modName, modIface := range mods.(map[string]interface{}) { for modName, modIface := range mods.(map[string]any) {
err = enc.addEncoding(modIface.(Encoding)) err = enc.addEncoding(modIface.(Encoding))
if err != nil { if err != nil {
return fmt.Errorf("adding encoding %s: %v", modName, err) return fmt.Errorf("adding encoding %s: %v", modName, err)
@@ -142,7 +141,7 @@ func (enc *Encode) addEncoding(e Encoding) error {
enc.writerPools = make(map[string]*sync.Pool) enc.writerPools = make(map[string]*sync.Pool)
} }
enc.writerPools[ae] = &sync.Pool{ enc.writerPools[ae] = &sync.Pool{
New: func() interface{} { New: func() any {
return e.NewEncoder() return e.NewEncoder()
}, },
} }
@@ -160,13 +159,12 @@ func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter
// initResponseWriter initializes the responseWriter instance // initResponseWriter initializes the responseWriter instance
// allocated in openResponseWriter, enabling mid-stack inlining. // allocated in openResponseWriter, enabling mid-stack inlining.
func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter { func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter {
buf := bufPool.Get().(*bytes.Buffer) if httpInterfaces, ok := wrappedRW.(caddyhttp.HTTPInterfaces); ok {
buf.Reset() rw.HTTPInterfaces = httpInterfaces
} else {
// The allocation of ResponseWriterWrapper might be optimized as well. rw.HTTPInterfaces = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW}
rw.ResponseWriterWrapper = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW} }
rw.encodingName = encodingName rw.encodingName = encodingName
rw.buf = buf
rw.config = enc rw.config = enc
return rw return rw
@@ -176,10 +174,9 @@ func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, w
// using the encoding represented by encodingName and // using the encoding represented by encodingName and
// configured by config. // configured by config.
type responseWriter struct { type responseWriter struct {
*caddyhttp.ResponseWriterWrapper caddyhttp.HTTPInterfaces
encodingName string encodingName string
w Encoder w Encoder
buf *bytes.Buffer
config *Encode config *Encode
statusCode int statusCode int
wroteHeader bool wroteHeader bool
@@ -206,28 +203,33 @@ func (rw *responseWriter) Flush() {
// to rw.Write (see bug in #4314) // to rw.Write (see bug in #4314)
return return
} }
rw.ResponseWriterWrapper.Flush() rw.HTTPInterfaces.Flush()
} }
// Write writes to the response. If the response qualifies, // Write writes to the response. If the response qualifies,
// it is encoded using the encoder, which is initialized // it is encoded using the encoder, which is initialized
// if not done so already. // if not done so already.
func (rw *responseWriter) Write(p []byte) (int, error) { func (rw *responseWriter) Write(p []byte) (int, error) {
var n, written int // ignore zero data writes, probably head request
var err error if len(p) == 0 {
return 0, nil
}
if rw.buf != nil && rw.config.MinLength > 0 { // sniff content-type and determine content-length
written = rw.buf.Len() if !rw.wroteHeader && rw.config.MinLength > 0 {
_, err := rw.buf.Write(p) var gtMinLength bool
if err != nil { if len(p) > rw.config.MinLength {
return 0, err gtMinLength = true
} else if cl, err := strconv.Atoi(rw.Header().Get("Content-Length")); err == nil && cl > rw.config.MinLength {
gtMinLength = true
}
if gtMinLength {
if rw.Header().Get("Content-Type") == "" {
rw.Header().Set("Content-Type", http.DetectContentType(p))
}
rw.init()
} }
rw.init()
p = rw.buf.Bytes()
defer func() {
bufPool.Put(rw.buf)
rw.buf = nil
}()
} }
// before we write to the response, we need to make // before we write to the response, we need to make
@@ -236,63 +238,41 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
// and if so, that means we haven't written the // and if so, that means we haven't written the
// header OR the default status code will be written // header OR the default status code will be written
// by the standard library // by the standard library
if rw.statusCode > 0 { if !rw.wroteHeader {
rw.ResponseWriter.WriteHeader(rw.statusCode) if rw.statusCode != 0 {
rw.statusCode = 0 rw.HTTPInterfaces.WriteHeader(rw.statusCode)
}
rw.wroteHeader = true rw.wroteHeader = true
} }
switch { if rw.w != nil {
case rw.w != nil: return rw.w.Write(p)
n, err = rw.w.Write(p) } else {
default: return rw.HTTPInterfaces.Write(p)
n, err = rw.ResponseWriter.Write(p)
} }
n -= written
if n < 0 {
n = 0
}
return n, err
} }
// Close writes any remaining buffered response and // Close writes any remaining buffered response and
// deallocates any active resources. // deallocates any active resources.
func (rw *responseWriter) Close() error { func (rw *responseWriter) Close() error {
var err error // didn't write, probably head request
// only attempt to write the remaining buffered response if !rw.wroteHeader {
// if there are any bytes left to write; otherwise, if cl, err := strconv.Atoi(rw.Header().Get("Content-Length"))
// the handler above us returned an error without writing if err == nil && cl > rw.config.MinLength {
// anything, we'd write to the response when we instead rw.init()
// should simply let the error propagate back down; this }
// is why the check for rw.buf.Len() > 0 is crucial
if rw.buf != nil && rw.buf.Len() > 0 { // issue #5059, don't write status code if not set explicitly.
rw.init() if rw.statusCode != 0 {
p := rw.buf.Bytes() rw.HTTPInterfaces.WriteHeader(rw.statusCode)
defer func() {
bufPool.Put(rw.buf)
rw.buf = nil
}()
switch {
case rw.w != nil:
_, err = rw.w.Write(p)
default:
_, err = rw.ResponseWriter.Write(p)
} }
} else if rw.statusCode != 0 {
// it is possible that a body was not written, and
// a header was not even written yet, even though
// we are closing; ensure the proper status code is
// written exactly once, or we risk breaking requests
// that rely on If-None-Match, for example
rw.ResponseWriter.WriteHeader(rw.statusCode)
rw.statusCode = 0
rw.wroteHeader = true rw.wroteHeader = true
} }
var err error
if rw.w != nil { if rw.w != nil {
err2 := rw.w.Close() err = rw.w.Close()
if err2 != nil && err == nil { rw.w.Reset(nil)
err = err2
}
rw.config.writerPools[rw.encodingName].Put(rw.w) rw.config.writerPools[rw.encodingName].Put(rw.w)
rw.w = nil rw.w = nil
} }
@@ -302,16 +282,15 @@ func (rw *responseWriter) Close() error {
// init should be called before we write a response, if rw.buf has contents. // init should be called before we write a response, if rw.buf has contents.
func (rw *responseWriter) init() { func (rw *responseWriter) init() {
if rw.Header().Get("Content-Encoding") == "" && if rw.Header().Get("Content-Encoding") == "" &&
rw.buf.Len() >= rw.config.MinLength &&
rw.config.Match(rw) { rw.config.Match(rw) {
rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder) rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
rw.w.Reset(rw.ResponseWriter) rw.w.Reset(rw.HTTPInterfaces)
rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975 rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975
rw.Header().Set("Content-Encoding", rw.encodingName) rw.Header().Set("Content-Encoding", rw.encodingName)
rw.Header().Add("Vary", "Accept-Encoding") rw.Header().Add("Vary", "Accept-Encoding")
rw.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-encoded content
} }
rw.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-encoded content
} }
// AcceptedEncodings returns the list of encodings that the // AcceptedEncodings returns the list of encodings that the
@@ -417,12 +396,6 @@ type Precompressed interface {
Suffix() string Suffix() string
} }
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
// defaultMinLength is the minimum length at which to compress content. // defaultMinLength is the minimum length at which to compress content.
const defaultMinLength = 512 const defaultMinLength = 512
+1 -1
View File
@@ -45,7 +45,7 @@ func (z *Zstd) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// used in the Accept-Encoding request headers. // used in the Accept-Encoding request headers.
func (Zstd) AcceptEncoding() string { return "zstd" } func (Zstd) AcceptEncoding() string { return "zstd" }
// NewEncoder returns a new gzip writer. // NewEncoder returns a new Zstandard writer.
func (z Zstd) NewEncoder() encode.Encoder { func (z Zstd) NewEncoder() encode.Encoder {
// The default of 8MB for the window is // The default of 8MB for the window is
// too large for many clients, so we limit // too large for many clients, so we limit
+18 -16
View File
@@ -16,9 +16,12 @@ package fileserver
import ( import (
"bytes" "bytes"
"context"
_ "embed" _ "embed"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/fs"
"net/http" "net/http"
"os" "os"
"path" "path"
@@ -67,9 +70,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
if r.URL.Path == "" || path.Base(origReq.URL.Path) == path.Base(r.URL.Path) { if r.URL.Path == "" || path.Base(origReq.URL.Path) == path.Base(r.URL.Path) {
if !strings.HasSuffix(origReq.URL.Path, "/") { if !strings.HasSuffix(origReq.URL.Path, "/") {
fsrv.logger.Debug("redirecting to trailing slash to preserve hrefs", zap.String("request_path", r.URL.Path)) fsrv.logger.Debug("redirecting to trailing slash to preserve hrefs", zap.String("request_path", r.URL.Path))
origReq.URL.Path += "/" return redirect(w, r, origReq.URL.Path+"/")
http.Redirect(w, r, origReq.URL.String(), http.StatusMovedPermanently)
return nil
} }
} }
@@ -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, 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)
@@ -95,6 +96,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
fsrv.browseApplyQueryParams(w, r, &listing) fsrv.browseApplyQueryParams(w, r, &listing)
buf := bufPool.Get().(*bytes.Buffer) buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf) defer bufPool.Put(buf)
acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ",")) acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ","))
@@ -135,16 +137,16 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
return nil return nil
} }
func (fsrv *FileServer) loadDirectoryContents(dir *os.File, 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(-1) files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable
if err != nil { if err != nil && err != io.EOF {
return browseTemplateContext{}, err return browseTemplateContext{}, err
} }
// 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.
@@ -203,25 +205,25 @@ func (fsrv *FileServer) makeBrowseTemplate(tplCtx *templateContext) (*template.T
return tpl, nil return tpl, nil
} }
// isSymlink return true if f is a symbolic link
func isSymlink(f os.FileInfo) bool {
return f.Mode()&os.ModeSymlink != 0
}
// isSymlinkTargetDir returns true if f's symbolic link target // isSymlinkTargetDir returns true if f's symbolic link target
// is a directory. // is a directory.
func isSymlinkTargetDir(f os.FileInfo, root, urlPath string) bool { func (fsrv *FileServer) isSymlinkTargetDir(f fs.FileInfo, root, urlPath string) bool {
if !isSymlink(f) { if !isSymlink(f) {
return false return false
} }
target := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, f.Name())) target := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, f.Name()))
targetInfo, err := os.Stat(target) targetInfo, err := fs.Stat(fsrv.fileSystem, target)
if err != nil { if err != nil {
return false return false
} }
return targetInfo.IsDir() return targetInfo.IsDir()
} }
// isSymlink return true if f is a symbolic link.
func isSymlink(f fs.FileInfo) bool {
return f.Mode()&os.ModeSymlink != 0
}
// templateContext powers the context used when evaluating the browse template. // templateContext powers the context used when evaluating the browse template.
// It combines browse-specific features with the standard templates handler // It combines browse-specific features with the standard templates handler
// features. // features.
@@ -232,7 +234,7 @@ type templateContext struct {
// bufPool is used to increase the efficiency of file listings. // bufPool is used to increase the efficiency of file listings.
var bufPool = sync.Pool{ var bufPool = sync.Pool{
New: func() interface{} { New: func() any {
return new(bytes.Buffer) return new(bytes.Buffer)
}, },
} }
+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,8 @@
package fileserver package fileserver
import ( import (
"context"
"io/fs"
"net/url" "net/url"
"os" "os"
"path" "path"
@@ -26,22 +28,35 @@ import (
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"go.uber.org/zap"
) )
func (fsrv *FileServer) directoryListing(files []os.FileInfo, 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 _, f := range files { for _, entry := range entries {
name := f.Name() if err := ctx.Err(); err != nil {
break
}
name := entry.Name()
if fileHidden(name, filesToHide) { if fileHidden(name, filesToHide) {
continue continue
} }
isDir := f.IsDir() || isSymlinkTargetDir(f, root, urlPath) info, err := entry.Info()
if err != nil {
fsrv.logger.Error("could not get info about directory entry",
zap.String("name", entry.Name()),
zap.String("root", root))
continue
}
isDir := entry.IsDir() || fsrv.isSymlinkTargetDir(info, root, urlPath)
// add the slash after the escape of path to avoid escaping the slash as well // add the slash after the escape of path to avoid escaping the slash as well
if isDir { if isDir {
@@ -51,11 +66,11 @@ func (fsrv *FileServer) directoryListing(files []os.FileInfo, canGoUp bool, root
fileCount++ fileCount++
} }
size := f.Size() size := info.Size()
fileIsSymlink := isSymlink(f) fileIsSymlink := isSymlink(info)
if fileIsSymlink { if fileIsSymlink {
path := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, f.Name())) path := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, info.Name()))
fileInfo, err := os.Stat(path) fileInfo, err := fs.Stat(fsrv.fileSystem, path)
if err == nil { if err == nil {
size = fileInfo.Size() size = fileInfo.Size()
} }
@@ -73,8 +88,8 @@ func (fsrv *FileServer) directoryListing(files []os.FileInfo, canGoUp bool, root
Name: name, Name: name,
Size: size, Size: size,
URL: u.String(), URL: u.String(),
ModTime: f.ModTime().UTC(), ModTime: info.ModTime().UTC(),
Mode: f.Mode(), Mode: info.Mode(),
}) })
} }
name, _ := url.PathUnescape(urlPath) name, _ := url.PathUnescape(urlPath)
+62 -19
View File
@@ -15,11 +15,13 @@
package fileserver package fileserver
import ( import (
"io/fs"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
@@ -34,16 +36,16 @@ func init() {
// parseCaddyfile parses the file_server directive. It enables the static file // parseCaddyfile parses the file_server directive. It enables the static file
// server and configures it with this syntax: // server and configures it with this syntax:
// //
// file_server [<matcher>] [browse] { // file_server [<matcher>] [browse] {
// root <path> // fs <backend...>
// hide <files...> // root <path>
// index <files...> // hide <files...>
// browse [<template_file>] // index <files...>
// precompressed <formats...> // browse [<template_file>]
// status <status> // precompressed <formats...>
// disable_canonical_uris // status <status>
// } // disable_canonical_uris
// // }
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var fsrv FileServer var fsrv FileServer
@@ -62,6 +64,25 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
for h.NextBlock(0) { for h.NextBlock(0) {
switch h.Val() { switch h.Val() {
case "fs":
if !h.NextArg() {
return nil, h.ArgErr()
}
if fsrv.FileSystemRaw != nil {
return nil, h.Err("file system module already specified")
}
name := h.Val()
modID := "caddy.fs." + name
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
if err != nil {
return nil, err
}
fsys, ok := unm.(fs.FS)
if !ok {
return nil, h.Errf("module %s (%T) is not a supported file system implementation (requires fs.FS)", modID, unm)
}
fsrv.FileSystemRaw = caddyconfig.JSONModuleObject(fsys, "backend", name, nil)
case "hide": case "hide":
fsrv.Hide = h.RemainingArgs() fsrv.Hide = h.RemainingArgs()
if len(fsrv.Hide) == 0 { if len(fsrv.Hide) == 0 {
@@ -155,22 +176,23 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// with a rewrite directive, so this is not a standard handler directive. // with a rewrite directive, so this is not a standard handler directive.
// A try_files directive has this syntax (notice no matcher tokens accepted): // A try_files directive has this syntax (notice no matcher tokens accepted):
// //
// try_files <files...> // try_files <files...> {
// policy first_exist|smallest_size|largest_size|most_recently_modified
// }
// //
// and is basically shorthand for: // and is basically shorthand for:
// //
// @try_files { // @try_files file {
// file { // try_files <files...>
// try_files <files...> // policy first_exist|smallest_size|largest_size|most_recently_modified
// } // }
// } // rewrite @try_files {http.matchers.file.relative}
// rewrite @try_files {http.matchers.file.relative}
// //
// This directive rewrites request paths only, preserving any other part // This directive rewrites request paths only, preserving any other part
// of the URI, unless the part is explicitly given in the file list. For // of the URI, unless the part is explicitly given in the file list. For
// example, if any of the files in the list have a query string: // example, if any of the files in the list have a query string:
// //
// try_files {path} index.php?{query}&p={path} // try_files {path} index.php?{query}&p={path}
// //
// then the query string will not be treated as part of the file name; and // then the query string will not be treated as part of the file name; and
// if that file matches, the given query string will replace any query string // if that file matches, the given query string will replace any query string
@@ -185,6 +207,27 @@ func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
return nil, h.ArgErr() return nil, h.ArgErr()
} }
// parse out the optional try policy
var tryPolicy string
for nesting := h.Nesting(); h.NextBlock(nesting); {
switch h.Val() {
case "policy":
if tryPolicy != "" {
return nil, h.Err("try policy already configured")
}
if !h.NextArg() {
return nil, h.ArgErr()
}
tryPolicy = h.Val()
switch tryPolicy {
case tryPolicyFirstExist, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod:
default:
return nil, h.Errf("unrecognized try policy: %s", tryPolicy)
}
}
}
// makeRoute returns a route that tries the files listed in try // makeRoute returns a route that tries the files listed in try
// and then rewrites to the matched file; userQueryString is // and then rewrites to the matched file; userQueryString is
// appended to the rewrite rule. // appended to the rewrite rule.
@@ -193,7 +236,7 @@ func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
URI: "{http.matchers.file.relative}" + userQueryString, URI: "{http.matchers.file.relative}" + userQueryString,
} }
matcherSet := caddy.ModuleMap{ matcherSet := caddy.ModuleMap{
"file": h.JSON(MatchFile{TryFiles: try}), "file": h.JSON(MatchFile{TryFiles: try, TryPolicy: tryPolicy}),
} }
return h.NewRoute(matcherSet, handler) return h.NewRoute(matcherSet, handler)
} }
+18 -1
View File
@@ -27,6 +27,7 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
caddytpl "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates" caddytpl "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"go.uber.org/zap"
) )
func init() { func init() {
@@ -56,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
}(), }(),
}) })
@@ -70,6 +72,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
browse := fs.Bool("browse") browse := fs.Bool("browse")
templates := fs.Bool("templates") templates := fs.Bool("templates")
accessLog := fs.Bool("access-log") accessLog := fs.Bool("access-log")
debug := fs.Bool("debug")
var handlers []json.RawMessage var handlers []json.RawMessage
@@ -117,13 +120,27 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
Servers: map[string]*caddyhttp.Server{"static": server}, Servers: map[string]*caddyhttp.Server{"static": server},
} }
var false bool
cfg := &caddy.Config{ cfg := &caddy.Config{
Admin: &caddy.AdminConfig{Disabled: true}, Admin: &caddy.AdminConfig{
Disabled: true,
Config: &caddy.ConfigSettings{
Persist: &false,
},
},
AppsRaw: caddy.ModuleMap{ AppsRaw: caddy.ModuleMap{
"http": caddyconfig.JSON(httpApp, nil), "http": caddyconfig.JSON(httpApp, nil),
}, },
} }
if debug {
cfg.Logging = &caddy.Logging{
Logs: map[string]*caddy.CustomLog{
"default": {Level: zap.DebugLevel.CapitalString()},
},
}
}
err := caddy.Run(cfg) err := caddy.Run(cfg)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
+384 -79
View File
@@ -15,17 +15,27 @@
package fileserver package fileserver
import ( import (
"encoding/json"
"fmt" "fmt"
"io/fs"
"net/http" "net/http"
"os" "os"
"path" "path"
"path/filepath"
"runtime"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common"
"github.com/google/cel-go/common/operators"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/parser"
"go.uber.org/zap"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
) )
func init() { func init() {
@@ -47,7 +57,15 @@ func init() {
// the matched file is a directory, "file" otherwise. // the matched file is a directory, "file" otherwise.
// - `{http.matchers.file.remainder}` Set to the remainder // - `{http.matchers.file.remainder}` Set to the remainder
// of the path if the path was split by `split_path`. // of the path if the path was split by `split_path`.
//
// Even though file matching may depend on the OS path
// separator, the placeholder values always use /.
type MatchFile struct { type MatchFile struct {
// The file system implementation to use. By default, the
// local disk file system will be used.
FileSystemRaw json.RawMessage `json:"file_system,omitempty" caddy:"namespace=caddy.fs inline_key=backend"`
fileSystem fs.FS
// The root directory, used for creating absolute // The root directory, used for creating absolute
// file paths, and required when working with // file paths, and required when working with
// relative paths; if not specified, `{http.vars.root}` // relative paths; if not specified, `{http.vars.root}`
@@ -88,6 +106,8 @@ type MatchFile struct {
// Each delimiter must appear at the end of a URI path // Each delimiter must appear at the end of a URI path
// component in order to be used as a split delimiter. // component in order to be used as a split delimiter.
SplitPath []string `json:"split_path,omitempty"` SplitPath []string `json:"split_path,omitempty"`
logger *zap.Logger
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
@@ -100,12 +120,11 @@ func (MatchFile) CaddyModule() caddy.ModuleInfo {
// UnmarshalCaddyfile sets up the matcher from Caddyfile tokens. Syntax: // UnmarshalCaddyfile sets up the matcher from Caddyfile tokens. Syntax:
// //
// file <files...> { // file <files...> {
// root <path> // root <path>
// try_files <files...> // try_files <files...>
// try_policy first_exist|smallest_size|largest_size|most_recently_modified // try_policy first_exist|smallest_size|largest_size|most_recently_modified
// } // }
//
func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() { for d.Next() {
m.TryFiles = append(m.TryFiles, d.RemainingArgs()...) m.TryFiles = append(m.TryFiles, d.RemainingArgs()...)
@@ -139,11 +158,122 @@ func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return nil return nil
} }
// CELLibrary produces options that expose this matcher for use in CEL
// expression matchers.
//
// Example:
//
// expression file({'root': '/srv', 'try_files': [{http.request.uri.path}, '/index.php'], 'try_policy': 'first_exist', 'split_path': ['.php']})
func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) {
requestType := cel.ObjectType("http.Request")
matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcher, error) {
values, err := caddyhttp.CELValueToMapStrList(data)
if err != nil {
return nil, err
}
var root string
if len(values["root"]) > 0 {
root = values["root"][0]
}
var try_policy string
if len(values["try_policy"]) > 0 {
root = values["try_policy"][0]
}
m := MatchFile{
Root: root,
TryFiles: values["try_files"],
TryPolicy: try_policy,
SplitPath: values["split_path"],
}
err = m.Provision(ctx)
return m, err
}
envOptions := []cel.EnvOption{
cel.Macros(parser.NewGlobalVarArgMacro("file", celFileMatcherMacroExpander())),
cel.Function("file", cel.Overload("file_request_map", []*cel.Type{requestType, caddyhttp.CELTypeJSON}, cel.BoolType)),
cel.Function("file_request_map",
cel.Overload("file_request_map", []*cel.Type{requestType, caddyhttp.CELTypeJSON}, cel.BoolType),
cel.SingletonBinaryImpl(caddyhttp.CELMatcherRuntimeFunction("file_request_map", matcherFactory))),
}
programOptions := []cel.ProgramOption{
cel.CustomDecorator(caddyhttp.CELMatcherDecorator("file_request_map", matcherFactory)),
}
return caddyhttp.NewMatcherCELLibrary(envOptions, programOptions), nil
}
func celFileMatcherMacroExpander() parser.MacroExpander {
return func(eh parser.ExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) {
if len(args) == 0 {
return nil, &common.Error{
Message: "matcher requires at least one argument",
}
}
if len(args) == 1 {
arg := args[0]
if isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg) {
return eh.GlobalCall("file",
eh.Ident("request"),
eh.NewMap(
eh.NewMapEntry(eh.LiteralString("try_files"), eh.NewList(arg)),
),
), nil
}
if isCELTryFilesLiteral(arg) {
return eh.GlobalCall("file", eh.Ident("request"), arg), nil
}
return nil, &common.Error{
Location: eh.OffsetLocation(arg.GetId()),
Message: "matcher requires either a map or string literal argument",
}
}
for _, arg := range args {
if !(isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg)) {
return nil, &common.Error{
Location: eh.OffsetLocation(arg.GetId()),
Message: "matcher only supports repeated string literal arguments",
}
}
}
return eh.GlobalCall("file",
eh.Ident("request"),
eh.NewMap(
eh.NewMapEntry(
eh.LiteralString("try_files"), eh.NewList(args...),
),
),
), nil
}
}
// Provision sets up m's defaults. // Provision sets up m's defaults.
func (m *MatchFile) Provision(_ caddy.Context) error { func (m *MatchFile) Provision(ctx caddy.Context) error {
m.logger = ctx.Logger()
// establish the file system to use
if len(m.FileSystemRaw) > 0 {
mod, err := ctx.LoadModule(m, "FileSystemRaw")
if err != nil {
return fmt.Errorf("loading file system module: %v", err)
}
m.fileSystem = mod.(fs.FS)
}
if m.fileSystem == nil {
m.fileSystem = osFS{}
}
if m.Root == "" { if m.Root == "" {
m.Root = "{http.vars.root}" m.Root = "{http.vars.root}"
} }
// if list of files to try was omitted entirely, assume URL path // if list of files to try was omitted entirely, assume URL path
// (use placeholder instead of r.URL.Path; see issue #4146) // (use placeholder instead of r.URL.Path; see issue #4146)
if m.TryFiles == nil { if m.TryFiles == nil {
@@ -169,10 +299,10 @@ func (m MatchFile) Validate() error {
// Match returns true if r matches m. Returns true // Match returns true if r matches m. Returns true
// if a file was matched. If so, four placeholders // if a file was matched. If so, four placeholders
// will be available: // will be available:
// - http.matchers.file.relative // - http.matchers.file.relative: Path to file relative to site root
// - http.matchers.file.absolute // - http.matchers.file.absolute: Path to file including site root
// - http.matchers.file.type // - http.matchers.file.type: file or directory
// - http.matchers.file.remainder // - http.matchers.file.remainder: Portion remaining after splitting file path (if configured)
func (m MatchFile) Match(r *http.Request) bool { func (m MatchFile) Match(r *http.Request) bool {
return m.selectFile(r) return m.selectFile(r)
} }
@@ -182,23 +312,80 @@ func (m MatchFile) Match(r *http.Request) bool {
func (m MatchFile) selectFile(r *http.Request) (matched bool) { func (m MatchFile) selectFile(r *http.Request) (matched bool) {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
root := repl.ReplaceAll(m.Root, ".") root := filepath.Clean(repl.ReplaceAll(m.Root, "."))
// common preparation of the file into parts type matchCandidate struct {
prepareFilePath := func(file string) (suffix, fullpath, remainder string) { fullpath, relative, splitRemainder string
suffix, remainder = m.firstSplit(path.Clean(repl.ReplaceAll(file, "")))
if strings.HasSuffix(file, "/") {
suffix += "/"
}
fullpath = caddyhttp.SanitizedPathJoin(root, suffix)
return
} }
// sets up the placeholders for the matched file // makeCandidates evaluates placeholders in file and expands any glob expressions
setPlaceholders := func(info os.FileInfo, rel string, abs string, remainder string) { // to build a list of file candidates. Special glob characters are escaped in
repl.Set("http.matchers.file.relative", rel) // placeholder replacements so globs cannot be expanded from placeholders, and
repl.Set("http.matchers.file.absolute", abs) // globs are not evaluated on Windows because of its path separator character:
repl.Set("http.matchers.file.remainder", remainder) // escaping is not supported so we can't safely glob on Windows, or we can't
// support placeholders on Windows (pick one). (Actually, evaluating untrusted
// globs is not the end of the world since the file server will still hide any
// hidden files, it just might lead to unexpected behavior.)
makeCandidates := func(file string) []matchCandidate {
// first, evaluate placeholders in the file pattern
expandedFile, err := repl.ReplaceFunc(file, func(variable string, val any) (any, error) {
if runtime.GOOS == "windows" {
return val, nil
}
switch v := val.(type) {
case string:
return globSafeRepl.Replace(v), nil
case fmt.Stringer:
return globSafeRepl.Replace(v.String()), nil
}
return val, nil
})
if err != nil {
m.logger.Error("evaluating placeholders", zap.Error(err))
expandedFile = file // "oh well," I guess?
}
// clean the path and split, if configured -- we must split before
// globbing so that the file system doesn't include the remainder
// ("afterSplit") in the filename; be sure to restore trailing slash
beforeSplit, afterSplit := m.firstSplit(path.Clean(expandedFile))
if strings.HasSuffix(file, "/") {
beforeSplit += "/"
}
// create the full path to the file by prepending the site root
fullPattern := caddyhttp.SanitizedPathJoin(root, beforeSplit)
// expand glob expressions, but not on Windows because Glob() doesn't
// support escaping on Windows due to path separator)
var globResults []string
if runtime.GOOS == "windows" {
globResults = []string{fullPattern} // precious Windows
} else {
globResults, err = fs.Glob(m.fileSystem, fullPattern)
if err != nil {
m.logger.Error("expanding glob", zap.Error(err))
}
}
// for each glob result, combine all the forms of the path
var candidates []matchCandidate
for _, result := range globResults {
candidates = append(candidates, matchCandidate{
fullpath: result,
relative: strings.TrimPrefix(result, root),
splitRemainder: afterSplit,
})
}
return candidates
}
// setPlaceholders creates the placeholders for the matched file
setPlaceholders := func(candidate matchCandidate, info fs.FileInfo) {
repl.Set("http.matchers.file.relative", filepath.ToSlash(candidate.relative))
repl.Set("http.matchers.file.absolute", filepath.ToSlash(candidate.fullpath))
repl.Set("http.matchers.file.remainder", filepath.ToSlash(candidate.splitRemainder))
fileType := "file" fileType := "file"
if info.IsDir() { if info.IsDir() {
@@ -207,76 +394,83 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
repl.Set("http.matchers.file.type", fileType) repl.Set("http.matchers.file.type", fileType)
} }
// match file according to the configured policy
switch m.TryPolicy { switch m.TryPolicy {
case "", tryPolicyFirstExist: case "", tryPolicyFirstExist:
for _, f := range m.TryFiles { for _, pattern := range m.TryFiles {
if err := parseErrorCode(f); err != nil { if err := parseErrorCode(pattern); err != nil {
caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err) caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err)
return return
} }
suffix, fullpath, remainder := prepareFilePath(f) candidates := makeCandidates(pattern)
if info, exists := strictFileExists(fullpath); exists { for _, c := range candidates {
setPlaceholders(info, suffix, fullpath, remainder) if info, exists := m.strictFileExists(c.fullpath); exists {
return true setPlaceholders(c, info)
return true
}
} }
} }
case tryPolicyLargestSize: case tryPolicyLargestSize:
var largestSize int64 var largestSize int64
var largestFilename string var largest matchCandidate
var largestSuffix string var largestInfo os.FileInfo
var remainder string for _, pattern := range m.TryFiles {
var info os.FileInfo candidates := makeCandidates(pattern)
for _, f := range m.TryFiles { for _, c := range candidates {
suffix, fullpath, splitRemainder := prepareFilePath(f) info, err := fs.Stat(m.fileSystem, c.fullpath)
info, err := os.Stat(fullpath) if err == nil && info.Size() > largestSize {
if err == nil && info.Size() > largestSize { largestSize = info.Size()
largestSize = info.Size() largest = c
largestFilename = fullpath largestInfo = info
largestSuffix = suffix }
remainder = splitRemainder
} }
} }
setPlaceholders(info, largestSuffix, largestFilename, remainder) if largestInfo == nil {
return false
}
setPlaceholders(largest, largestInfo)
return true return true
case tryPolicySmallestSize: case tryPolicySmallestSize:
var smallestSize int64 var smallestSize int64
var smallestFilename string var smallest matchCandidate
var smallestSuffix string var smallestInfo os.FileInfo
var remainder string for _, pattern := range m.TryFiles {
var info os.FileInfo candidates := makeCandidates(pattern)
for _, f := range m.TryFiles { for _, c := range candidates {
suffix, fullpath, splitRemainder := prepareFilePath(f) info, err := fs.Stat(m.fileSystem, c.fullpath)
info, err := os.Stat(fullpath) if err == nil && (smallestSize == 0 || info.Size() < smallestSize) {
if err == nil && (smallestSize == 0 || info.Size() < smallestSize) { smallestSize = info.Size()
smallestSize = info.Size() smallest = c
smallestFilename = fullpath smallestInfo = info
smallestSuffix = suffix }
remainder = splitRemainder
} }
} }
setPlaceholders(info, smallestSuffix, smallestFilename, remainder) if smallestInfo == nil {
return false
}
setPlaceholders(smallest, smallestInfo)
return true return true
case tryPolicyMostRecentlyMod: case tryPolicyMostRecentlyMod:
var recentDate time.Time var recent matchCandidate
var recentFilename string var recentInfo os.FileInfo
var recentSuffix string for _, pattern := range m.TryFiles {
var remainder string candidates := makeCandidates(pattern)
var info os.FileInfo for _, c := range candidates {
for _, f := range m.TryFiles { info, err := fs.Stat(m.fileSystem, c.fullpath)
suffix, fullpath, splitRemainder := prepareFilePath(f) if err == nil &&
info, err := os.Stat(fullpath) (recentInfo == nil || info.ModTime().After(recentInfo.ModTime())) {
if err == nil && recent = c
(recentDate.IsZero() || info.ModTime().After(recentDate)) { recentInfo = info
recentDate = info.ModTime() }
recentFilename = fullpath
recentSuffix = suffix
remainder = splitRemainder
} }
} }
setPlaceholders(info, recentSuffix, recentFilename, remainder) if recentInfo == nil {
return false
}
setPlaceholders(recent, recentInfo)
return true return true
} }
@@ -303,8 +497,8 @@ func parseErrorCode(input string) error {
// the file must also be a directory; if it does // the file must also be a directory; if it does
// NOT end in a forward slash, the file must NOT // NOT end in a forward slash, the file must NOT
// be a directory. // be a directory.
func strictFileExists(file string) (os.FileInfo, bool) { func (m MatchFile) strictFileExists(file string) (os.FileInfo, bool) {
stat, err := os.Stat(file) info, err := fs.Stat(m.fileSystem, file)
if err != nil { if err != nil {
// in reality, this can be any error // in reality, this can be any error
// such as permission or even obscure // such as permission or even obscure
@@ -319,11 +513,11 @@ func strictFileExists(file string) (os.FileInfo, bool) {
if strings.HasSuffix(file, separator) { if strings.HasSuffix(file, separator) {
// by convention, file paths ending // by convention, file paths ending
// in a path separator must be a directory // in a path separator must be a directory
return stat, stat.IsDir() return info, info.IsDir()
} }
// by convention, file paths NOT ending // by convention, file paths NOT ending
// in a path separator must NOT be a directory // in a path separator must NOT be a directory
return stat, !stat.IsDir() return info, !info.IsDir()
} }
// firstSplit returns the first result where the path // firstSplit returns the first result where the path
@@ -359,6 +553,116 @@ func indexFold(haystack, needle string) int {
return -1 return -1
} }
// isCELMapLiteral returns whether the expression resolves to a map literal containing
// only string keys with or a placeholder call.
func isCELTryFilesLiteral(e *exprpb.Expr) bool {
switch e.GetExprKind().(type) {
case *exprpb.Expr_StructExpr:
structExpr := e.GetStructExpr()
if structExpr.GetMessageName() != "" {
return false
}
for _, entry := range structExpr.GetEntries() {
mapKey := entry.GetMapKey()
mapVal := entry.GetValue()
if !isCELStringLiteral(mapKey) {
return false
}
mapKeyStr := mapKey.GetConstExpr().GetStringValue()
if mapKeyStr == "try_files" || mapKeyStr == "split_path" {
if !isCELStringListLiteral(mapVal) {
return false
}
} else if mapKeyStr == "try_policy" || mapKeyStr == "root" {
if !(isCELStringExpr(mapVal)) {
return false
}
} else {
return false
}
}
return true
}
return false
}
// isCELStringExpr indicates whether the expression is a supported string expression
func isCELStringExpr(e *exprpb.Expr) bool {
return isCELStringLiteral(e) || isCELCaddyPlaceholderCall(e) || isCELConcatCall(e)
}
// isCELStringLiteral returns whether the expression is a CEL string literal.
func isCELStringLiteral(e *exprpb.Expr) bool {
switch e.GetExprKind().(type) {
case *exprpb.Expr_ConstExpr:
constant := e.GetConstExpr()
switch constant.GetConstantKind().(type) {
case *exprpb.Constant_StringValue:
return true
}
}
return false
}
// isCELCaddyPlaceholderCall returns whether the expression is a caddy placeholder call.
func isCELCaddyPlaceholderCall(e *exprpb.Expr) bool {
switch e.GetExprKind().(type) {
case *exprpb.Expr_CallExpr:
call := e.GetCallExpr()
if call.GetFunction() == "caddyPlaceholder" {
return true
}
}
return false
}
// isCELConcatCall tests whether the expression is a concat function (+) with string, placeholder, or
// other concat call arguments.
func isCELConcatCall(e *exprpb.Expr) bool {
switch e.GetExprKind().(type) {
case *exprpb.Expr_CallExpr:
call := e.GetCallExpr()
if call.GetTarget() != nil {
return false
}
if call.GetFunction() != operators.Add {
return false
}
for _, arg := range call.GetArgs() {
if !isCELStringExpr(arg) {
return false
}
}
return true
}
return false
}
// isCELStringListLiteral returns whether the expression resolves to a list literal
// containing only string constants or a placeholder call.
func isCELStringListLiteral(e *exprpb.Expr) bool {
switch e.GetExprKind().(type) {
case *exprpb.Expr_ListExpr:
list := e.GetListExpr()
for _, elem := range list.GetElements() {
if !isCELStringExpr(elem) {
return false
}
}
return true
}
return false
}
// globSafeRepl replaces special glob characters with escaped
// equivalents. Note that the filepath godoc states that
// escaping is not done on Windows because of the separator.
var globSafeRepl = strings.NewReplacer(
"*", "\\*",
"[", "\\[",
"?", "\\?",
)
const ( const (
tryPolicyFirstExist = "first_exist" tryPolicyFirstExist = "first_exist"
tryPolicyLargestSize = "largest_size" tryPolicyLargestSize = "largest_size"
@@ -368,6 +672,7 @@ const (
// Interface guards // Interface guards
var ( var (
_ caddy.Validator = (*MatchFile)(nil) _ caddy.Validator = (*MatchFile)(nil)
_ caddyhttp.RequestMatcher = (*MatchFile)(nil) _ caddyhttp.RequestMatcher = (*MatchFile)(nil)
_ caddyhttp.CELLibraryProducer = (*MatchFile)(nil)
) )
+130 -20
View File
@@ -15,17 +15,19 @@
package fileserver package fileserver
import ( import (
"context"
"net/http" "net/http"
"net/http/httptest"
"net/url" "net/url"
"os" "os"
"runtime" "runtime"
"testing" "testing"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
) )
func TestFileMatcher(t *testing.T) { func TestFileMatcher(t *testing.T) {
// Windows doesn't like colons in files names // Windows doesn't like colons in files names
isWindows := runtime.GOOS == "windows" isWindows := runtime.GOOS == "windows"
if !isWindows { if !isWindows {
@@ -84,37 +86,38 @@ func TestFileMatcher(t *testing.T) {
}, },
{ {
path: "ملف.txt", // the path file name is not escaped path: "ملف.txt", // the path file name is not escaped
expectedPath: "ملف.txt", expectedPath: "/ملف.txt",
expectedType: "file", expectedType: "file",
matched: true, matched: true,
}, },
{ {
path: url.PathEscape("ملف.txt"), // singly-escaped path path: url.PathEscape("ملف.txt"), // singly-escaped path
expectedPath: "ملف.txt", expectedPath: "/ملف.txt",
expectedType: "file", expectedType: "file",
matched: true, matched: true,
}, },
{ {
path: url.PathEscape(url.PathEscape("ملف.txt")), // doubly-escaped path path: url.PathEscape(url.PathEscape("ملف.txt")), // doubly-escaped path
expectedPath: "%D9%85%D9%84%D9%81.txt", expectedPath: "/%D9%85%D9%84%D9%81.txt",
expectedType: "file", expectedType: "file",
matched: true, matched: true,
}, },
{ {
path: "./with:in-name.txt", // browsers send the request with the path as such path: "./with:in-name.txt", // browsers send the request with the path as such
expectedPath: "with:in-name.txt", expectedPath: "/with:in-name.txt",
expectedType: "file", expectedType: "file",
matched: !isWindows, matched: !isWindows,
}, },
} { } {
m := &MatchFile{ m := &MatchFile{
Root: "./testdata", fileSystem: osFS{},
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"}, Root: "./testdata",
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"},
} }
u, err := url.Parse(tc.path) u, err := url.Parse(tc.path)
if err != nil { if err != nil {
t.Fatalf("Test %d: parsing path: %v", i, err) t.Errorf("Test %d: parsing path: %v", i, err)
} }
req := &http.Request{URL: u} req := &http.Request{URL: u}
@@ -122,24 +125,24 @@ func TestFileMatcher(t *testing.T) {
result := m.Match(req) result := m.Match(req)
if result != tc.matched { if result != tc.matched {
t.Fatalf("Test %d: expected match=%t, got %t", i, tc.matched, result) t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
} }
rel, ok := repl.Get("http.matchers.file.relative") rel, ok := repl.Get("http.matchers.file.relative")
if !ok && result { if !ok && result {
t.Fatalf("Test %d: expected replacer value", i) t.Errorf("Test %d: expected replacer value", i)
} }
if !result { if !result {
continue continue
} }
if rel != tc.expectedPath { if rel != tc.expectedPath {
t.Fatalf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath) t.Errorf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath)
} }
fileType, _ := repl.Get("http.matchers.file.type") fileType, _ := repl.Get("http.matchers.file.type")
if fileType != tc.expectedType { if fileType != tc.expectedType {
t.Fatalf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType) t.Errorf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType)
} }
} }
} }
@@ -210,14 +213,15 @@ func TestPHPFileMatcher(t *testing.T) {
}, },
} { } {
m := &MatchFile{ m := &MatchFile{
Root: "./testdata", fileSystem: osFS{},
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"}, Root: "./testdata",
SplitPath: []string{".php"}, TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"},
SplitPath: []string{".php"},
} }
u, err := url.Parse(tc.path) u, err := url.Parse(tc.path)
if err != nil { if err != nil {
t.Fatalf("Test %d: parsing path: %v", i, err) t.Errorf("Test %d: parsing path: %v", i, err)
} }
req := &http.Request{URL: u} req := &http.Request{URL: u}
@@ -225,24 +229,24 @@ func TestPHPFileMatcher(t *testing.T) {
result := m.Match(req) result := m.Match(req)
if result != tc.matched { if result != tc.matched {
t.Fatalf("Test %d: expected match=%t, got %t", i, tc.matched, result) t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
} }
rel, ok := repl.Get("http.matchers.file.relative") rel, ok := repl.Get("http.matchers.file.relative")
if !ok && result { if !ok && result {
t.Fatalf("Test %d: expected replacer value", i) t.Errorf("Test %d: expected replacer value", i)
} }
if !result { if !result {
continue continue
} }
if rel != tc.expectedPath { if rel != tc.expectedPath {
t.Fatalf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath) t.Errorf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath)
} }
fileType, _ := repl.Get("http.matchers.file.type") fileType, _ := repl.Get("http.matchers.file.type")
if fileType != tc.expectedType { if fileType != tc.expectedType {
t.Fatalf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType) t.Errorf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType)
} }
} }
} }
@@ -259,3 +263,109 @@ func TestFirstSplit(t *testing.T) {
t.Errorf("Expected remainder %s but got %s", expectedRemainder, remainder) t.Errorf("Expected remainder %s but got %s", expectedRemainder, remainder)
} }
} }
var (
expressionTests = []struct {
name string
expression *caddyhttp.MatchExpression
urlTarget string
httpMethod string
httpHeader *http.Header
wantErr bool
wantResult bool
clientCertificate []byte
}{
{
name: "file error no args (MatchFile)",
expression: &caddyhttp.MatchExpression{
Expr: `file()`,
},
urlTarget: "https://example.com/foo",
wantErr: true,
},
{
name: "file error bad try files (MatchFile)",
expression: &caddyhttp.MatchExpression{
Expr: `file({"try_file": ["bad_arg"]})`,
},
urlTarget: "https://example.com/foo",
wantErr: true,
},
{
name: "file match short pattern index.php (MatchFile)",
expression: &caddyhttp.MatchExpression{
Expr: `file("index.php")`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "file match short pattern foo.txt (MatchFile)",
expression: &caddyhttp.MatchExpression{
Expr: `file({http.request.uri.path})`,
},
urlTarget: "https://example.com/foo.txt",
wantResult: true,
},
{
name: "file match index.php (MatchFile)",
expression: &caddyhttp.MatchExpression{
Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}, "/index.php"]})`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "file match long pattern foo.txt (MatchFile)",
expression: &caddyhttp.MatchExpression{
Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`,
},
urlTarget: "https://example.com/foo.txt",
wantResult: true,
},
{
name: "file match long pattern foo.txt with concatenation (MatchFile)",
expression: &caddyhttp.MatchExpression{
Expr: `file({"root": ".", "try_files": ["./testdata" + {http.request.uri.path}]})`,
},
urlTarget: "https://example.com/foo.txt",
wantResult: true,
},
{
name: "file not match long pattern (MatchFile)",
expression: &caddyhttp.MatchExpression{
Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`,
},
urlTarget: "https://example.com/nopenope.txt",
wantResult: false,
},
}
)
func TestMatchExpressionMatch(t *testing.T) {
for _, tst := range expressionTests {
tc := tst
t.Run(tc.name, func(t *testing.T) {
err := tc.expression.Provision(caddy.Context{})
if err != nil {
if !tc.wantErr {
t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tc.wantErr)
}
return
}
req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil)
if tc.httpHeader != nil {
req.Header = *tc.httpHeader
}
repl := caddyhttp.NewTestReplacer(req)
repl.Set("http.vars.root", "./testdata")
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
if tc.expression.Match(req) != tc.wantResult {
t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
}
})
}
}
+126 -38
View File
@@ -15,11 +15,14 @@
package fileserver package fileserver
import ( import (
"encoding/json"
"errors"
"fmt" "fmt"
"io"
"io/fs"
weakrand "math/rand" weakrand "math/rand"
"mime" "mime"
"net/http" "net/http"
"net/url"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@@ -39,10 +42,63 @@ func init() {
caddy.RegisterModule(FileServer{}) caddy.RegisterModule(FileServer{})
} }
// FileServer implements a static file server responder for Caddy. // FileServer implements a handler that serves static files.
//
// The path of the file to serve is constructed by joining the site root
// and the sanitized request path. Any and all files within the root and
// links with targets outside the site root may therefore be accessed.
// For example, with a site root of `/www`, requests to `/foo/bar.txt`
// will serve the file at `/www/foo/bar.txt`.
//
// The request path is sanitized using the Go standard library's
// path.Clean() function (https://pkg.go.dev/path#Clean) before being
// joined to the root. Request paths must be valid and well-formed.
//
// For requests that access directories instead of regular files,
// Caddy will attempt to serve an index file if present. For example,
// a request to `/dir/` will attempt to serve `/dir/index.html` if
// it exists. The index file names to try are configurable. If a
// requested directory does not have an index file, Caddy writes a
// 404 response. Alternatively, file browsing can be enabled with
// the "browse" parameter which shows a list of files when directories
// are requested if no index file is present.
//
// By default, this handler will canonicalize URIs so that requests to
// directories end with a slash, but requests to regular files do not.
// This is enforced with HTTP redirects automatically and can be disabled.
// Canonicalization redirects are not issued, however, if a URI rewrite
// modified the last component of the path (the filename).
//
// This handler sets the Etag and Last-Modified headers for static files.
// It does not perform MIME sniffing to determine Content-Type based on
// contents, but does use the extension (if known); see the Go docs for
// details: https://pkg.go.dev/mime#TypeByExtension
//
// The file server properly handles requests with If-Match,
// If-Unmodified-Since, If-Modified-Since, If-None-Match, Range, and
// If-Range headers. It includes the file's modification time in the
// Last-Modified header of the response.
type FileServer struct { type FileServer struct {
// The file system implementation to use. By default, Caddy uses the local
// disk file system.
//
// File system modules used here must adhere to the following requirements:
// - Implement fs.FS interface.
// - Support seeking on opened files; i.e.returned fs.File values must
// implement the io.Seeker interface. This is required for determining
// Content-Length and satisfying Range requests.
// - fs.File values that represent directories must implement the
// fs.ReadDirFile interface so that directory listings can be procured.
FileSystemRaw json.RawMessage `json:"file_system,omitempty" caddy:"namespace=caddy.fs inline_key=backend"`
fileSystem fs.FS
// The path to the root of the site. Default is `{http.vars.root}` if set, // The path to the root of the site. Default is `{http.vars.root}` if set,
// or current working directory otherwise. // or current working directory otherwise. This should be a trusted value.
//
// Note that a site root is not a sandbox. Although the file server does
// sanitize the request URI to prevent directory traversal, files (including
// links) within the site root may be directly accessed based on the request
// path. Files and folders within the root should be secure and trustworthy.
Root string `json:"root,omitempty"` Root string `json:"root,omitempty"`
// A list of files or folders to hide; the file server will pretend as if // A list of files or folders to hide; the file server will pretend as if
@@ -63,6 +119,7 @@ type FileServer struct {
Hide []string `json:"hide,omitempty"` Hide []string `json:"hide,omitempty"`
// The names of files to try as index files if a folder is requested. // The names of files to try as index files if a folder is requested.
// Default: index.html, index.txt.
IndexNames []string `json:"index_names,omitempty"` IndexNames []string `json:"index_names,omitempty"`
// Enables file listings if a directory was requested and no index // Enables file listings if a directory was requested and no index
@@ -95,8 +152,7 @@ type FileServer struct {
// If no order specified here, the first encoding from the Accept-Encoding header // If no order specified here, the first encoding from the Accept-Encoding header
// that both client and server support is used // that both client and server support is used
PrecompressedOrder []string `json:"precompressed_order,omitempty"` PrecompressedOrder []string `json:"precompressed_order,omitempty"`
precompressors map[string]encode.Precompressed
precompressors map[string]encode.Precompressed
logger *zap.Logger logger *zap.Logger
} }
@@ -111,7 +167,19 @@ func (FileServer) CaddyModule() caddy.ModuleInfo {
// Provision sets up the static files responder. // Provision sets up the static files responder.
func (fsrv *FileServer) Provision(ctx caddy.Context) error { func (fsrv *FileServer) Provision(ctx caddy.Context) error {
fsrv.logger = ctx.Logger(fsrv) fsrv.logger = ctx.Logger()
// establish which file system (possibly a virtual one) we'll be using
if len(fsrv.FileSystemRaw) > 0 {
mod, err := ctx.LoadModule(fsrv, "FileSystemRaw")
if err != nil {
return fmt.Errorf("loading file system module: %v", err)
}
fsrv.fileSystem = mod.(fs.FS)
}
if fsrv.fileSystem == nil {
fsrv.fileSystem = osFS{}
}
if fsrv.Root == "" { if fsrv.Root == "" {
fsrv.Root = "{http.vars.root}" fsrv.Root = "{http.vars.root}"
@@ -131,11 +199,12 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error {
} }
} }
// support precompressed sidecar files
mods, err := ctx.LoadModule(fsrv, "PrecompressedRaw") mods, err := ctx.LoadModule(fsrv, "PrecompressedRaw")
if err != nil { if err != nil {
return fmt.Errorf("loading encoder modules: %v", err) return fmt.Errorf("loading encoder modules: %v", err)
} }
for modName, modIface := range mods.(map[string]interface{}) { for modName, modIface := range mods.(map[string]any) {
p, ok := modIface.(encode.Precompressed) p, ok := modIface.(encode.Precompressed)
if !ok { if !ok {
return fmt.Errorf("module %s is not precompressor", modName) return fmt.Errorf("module %s is not precompressor", modName)
@@ -166,16 +235,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
filesToHide := fsrv.transformHidePaths(repl) filesToHide := fsrv.transformHidePaths(repl)
root := repl.ReplaceAll(fsrv.Root, ".") root := repl.ReplaceAll(fsrv.Root, ".")
// PathUnescape returns an error if the escapes aren't well-formed,
// meaning the count % matches the RFC. Return early if the escape is
// improper.
if _, err := url.PathUnescape(r.URL.Path); err != nil {
fsrv.logger.Debug("improper path escape",
zap.String("site_root", root),
zap.String("request_path", r.URL.Path),
zap.Error(err))
return err
}
filename := caddyhttp.SanitizedPathJoin(root, r.URL.Path) filename := caddyhttp.SanitizedPathJoin(root, r.URL.Path)
fsrv.logger.Debug("sanitized path join", fsrv.logger.Debug("sanitized path join",
@@ -184,12 +244,12 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
zap.String("result", filename)) zap.String("result", filename))
// get information about the file // get information about the file
info, err := os.Stat(filename) info, err := fs.Stat(fsrv.fileSystem, filename)
if err != nil { if err != nil {
err = mapDirOpenError(err, filename) err = fsrv.mapDirOpenError(err, filename)
if os.IsNotExist(err) { 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 os.IsPermission(err) { } else if errors.Is(err, fs.ErrPermission) {
return caddyhttp.Error(http.StatusForbidden, err) return caddyhttp.Error(http.StatusForbidden, err)
} }
return caddyhttp.Error(http.StatusInternalServerError, err) return caddyhttp.Error(http.StatusInternalServerError, err)
@@ -210,7 +270,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
continue continue
} }
indexInfo, err := os.Stat(indexPath) indexInfo, err := fs.Stat(fsrv.fileSystem, indexPath)
if err != nil { if err != nil {
continue continue
} }
@@ -280,7 +340,8 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
} }
} }
var file *os.File var file fs.File
var etag string
// check for precompressed files // check for precompressed files
for _, ae := range encode.AcceptedEncodings(r, fsrv.PrecompressedOrder) { for _, ae := range encode.AcceptedEncodings(r, fsrv.PrecompressedOrder) {
@@ -289,7 +350,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
continue continue
} }
compressedFilename := filename + precompress.Suffix() compressedFilename := filename + precompress.Suffix()
compressedInfo, err := os.Stat(compressedFilename) compressedInfo, err := fs.Stat(fsrv.fileSystem, compressedFilename)
if err != nil || compressedInfo.IsDir() { if err != nil || compressedInfo.IsDir() {
fsrv.logger.Debug("precompressed file not accessible", zap.String("filename", compressedFilename), zap.Error(err)) fsrv.logger.Debug("precompressed file not accessible", zap.String("filename", compressedFilename), zap.Error(err))
continue continue
@@ -301,12 +362,19 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
if caddyErr, ok := err.(caddyhttp.HandlerError); ok && caddyErr.StatusCode == http.StatusServiceUnavailable { if caddyErr, ok := err.(caddyhttp.HandlerError); ok && caddyErr.StatusCode == http.StatusServiceUnavailable {
return err return err
} }
file = nil
continue continue
} }
defer file.Close() defer file.Close()
w.Header().Set("Content-Encoding", ae) w.Header().Set("Content-Encoding", ae)
w.Header().Del("Accept-Ranges") w.Header().Del("Accept-Ranges")
w.Header().Add("Vary", "Accept-Encoding") w.Header().Add("Vary", "Accept-Encoding")
// don't assign info = compressedInfo because sidecars are kind
// of transparent; however we do need to set the Etag:
// https://caddy.community/t/gzipped-sidecar-file-wrong-same-etag/16793
etag = calculateEtag(compressedInfo)
break break
} }
@@ -324,18 +392,18 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
return err // error is already structured return err // error is already structured
} }
defer file.Close() defer file.Close()
etag = calculateEtag(info)
} }
// set the ETag - note that a conditional If-None-Match request is handled // set the Etag - note that a conditional If-None-Match request is handled
// by http.ServeContent below, which checks against this ETag value // by http.ServeContent below, which checks against this Etag value
w.Header().Set("ETag", calculateEtag(info)) w.Header().Set("Etag", etag)
if w.Header().Get("Content-Type") == "" { if w.Header().Get("Content-Type") == "" {
mtyp := mime.TypeByExtension(filepath.Ext(filename)) mtyp := mime.TypeByExtension(filepath.Ext(filename))
if mtyp == "" { if mtyp == "" {
// do not allow Go to sniff the content-type; see // do not allow Go to sniff the content-type; see https://www.youtube.com/watch?v=8t8JYpt0egE
// https://www.youtube.com/watch?v=8t8JYpt0egE
// TODO: If we want a Content-Type, consider writing a default of application/octet-stream - this is secure but violates spec
w.Header()["Content-Type"] = nil w.Header()["Content-Type"] = nil
} else { } else {
w.Header().Set("Content-Type", mtyp) w.Header().Set("Content-Type", mtyp)
@@ -375,7 +443,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
// that errors generated by ServeContent are written immediately // that errors generated by ServeContent are written immediately
// to the response, so we cannot handle them (but errors there // to the response, so we cannot handle them (but errors there
// are rare) // are rare)
http.ServeContent(w, r, info.Name(), info.ModTime(), file) http.ServeContent(w, r, info.Name(), info.ModTime(), file.(io.ReadSeeker))
return nil return nil
} }
@@ -384,10 +452,10 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
// the response is configured to inform the client how to best handle it // the response is configured to inform the client how to best handle it
// and a well-described handler error is returned (do not wrap the // and a well-described handler error is returned (do not wrap the
// returned error value). // returned error value).
func (fsrv *FileServer) openFile(filename string, w http.ResponseWriter) (*os.File, error) { func (fsrv *FileServer) openFile(filename string, w http.ResponseWriter) (fs.File, error) {
file, err := os.Open(filename) file, err := fsrv.fileSystem.Open(filename)
if err != nil { if err != nil {
err = mapDirOpenError(err, filename) err = fsrv.mapDirOpenError(err, filename)
if os.IsNotExist(err) { if os.IsNotExist(err) {
fsrv.logger.Debug("file not found", zap.String("filename", filename), zap.Error(err)) fsrv.logger.Debug("file not found", zap.String("filename", filename), zap.Error(err))
return nil, caddyhttp.Error(http.StatusNotFound, err) return nil, caddyhttp.Error(http.StatusNotFound, err)
@@ -412,8 +480,8 @@ func (fsrv *FileServer) openFile(filename string, w http.ResponseWriter) (*os.Fi
// Adapted from the Go standard library; originally written by Nathaniel Caza. // Adapted from the Go standard library; originally written by Nathaniel Caza.
// https://go-review.googlesource.com/c/go/+/36635/ // https://go-review.googlesource.com/c/go/+/36635/
// https://go-review.googlesource.com/c/go/+/36804/ // https://go-review.googlesource.com/c/go/+/36804/
func mapDirOpenError(originalErr error, name string) error { func (fsrv *FileServer) mapDirOpenError(originalErr error, name string) error {
if os.IsNotExist(originalErr) || os.IsPermission(originalErr) { if errors.Is(originalErr, fs.ErrNotExist) || errors.Is(originalErr, fs.ErrPermission) {
return originalErr return originalErr
} }
@@ -422,12 +490,12 @@ func mapDirOpenError(originalErr error, name string) error {
if parts[i] == "" { if parts[i] == "" {
continue continue
} }
fi, err := os.Stat(strings.Join(parts[:i+1], separator)) fi, err := fs.Stat(fsrv.fileSystem, strings.Join(parts[:i+1], separator))
if err != nil { if err != nil {
return originalErr return originalErr
} }
if !fi.IsDir() { if !fi.IsDir() {
return os.ErrNotExist return fs.ErrNotExist
} }
} }
@@ -545,6 +613,21 @@ func (wr statusOverrideResponseWriter) WriteHeader(int) {
wr.ResponseWriter.WriteHeader(wr.code) wr.ResponseWriter.WriteHeader(wr.code)
} }
// osFS is a simple fs.FS implementation that uses the local
// file system. (We do not use os.DirFS because we do our own
// rooting or path prefixing without being constrained to a single
// root folder. The standard os.DirFS implementation is problematic
// since roots can be dynamic in our application.)
//
// osFS also implements fs.StatFS, fs.GlobFS, fs.ReadDirFS, and fs.ReadFileFS.
type osFS struct{}
func (osFS) Open(name string) (fs.File, error) { return os.Open(name) }
func (osFS) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) }
func (osFS) Glob(pattern string) ([]string, error) { return filepath.Glob(pattern) }
func (osFS) ReadDir(name string) ([]fs.DirEntry, error) { return os.ReadDir(name) }
func (osFS) ReadFile(name string) ([]byte, error) { return os.ReadFile(name) }
var defaultIndexNames = []string{"index.html", "index.txt"} var defaultIndexNames = []string{"index.html", "index.txt"}
const ( const (
@@ -556,4 +639,9 @@ const (
var ( var (
_ caddy.Provisioner = (*FileServer)(nil) _ caddy.Provisioner = (*FileServer)(nil)
_ caddyhttp.MiddlewareHandler = (*FileServer)(nil) _ caddyhttp.MiddlewareHandler = (*FileServer)(nil)
_ fs.StatFS = (*osFS)(nil)
_ fs.GlobFS = (*osFS)(nil)
_ fs.ReadDirFS = (*osFS)(nil)
_ fs.ReadFileFS = (*osFS)(nil)
) )
+1
View File
@@ -0,0 +1 @@
foodir/bar.txt
+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()

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