Compare commits

...

225 Commits

Author SHA1 Message Date
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
Francis Lavoie f7be0ee101 map: Prevent output destinations overlap with Caddyfile shorthands (#4657) 2022-05-06 10:25:31 -06:00
Francis Lavoie f6900fcf53 reverseproxy: Support performing pre-check requests (#4739) 2022-05-06 10:50:26 -04:00
Francis Lavoie ec86a2f7a3 caddyfile: Shortcut for remote_ip for private IP ranges (#4753) 2022-05-04 12:42:37 -06:00
Francis Lavoie e7fbee8c82 reverseproxy: Permit resolver addresses to not specify a port (#4760)
Context: https://caddy.community/t/caddy-2-5-dynamic-upstreams-and-consul-srv-dns/15839

I realized it probably makes sense to allow `:53` to be omitted, since it's the default port for DNS.
2022-05-04 12:40:39 -06:00
Tyler Kropp e84e19a04e templates: Add custom template function registration (#4757)
* Add custom template function registration

* Rename TemplateFunctions to CustomFunctions

* Add documentation

* Document CustomFunctions interface

* Preallocate custom functions map list

* Fix interface name in error message
2022-05-02 14:55:34 -06:00
Francis Lavoie 4a223f5203 reverseproxy: Fix Caddyfile support for replace_status (#4754) 2022-05-02 11:44:28 -06:00
Francis Lavoie af7321511c httpcaddyfile: Fix duplicate access log when debug is on (#4746) 2022-04-28 12:16:25 -04:00
Francis Lavoie 0be3d99543 logging: Implement rename filter, changes field key names (#4745) 2022-04-28 11:38:44 -04:00
Francis Lavoie 3017b245c9 logging: Use RedirectStdLog to capture more stdlib logs (#4732)
* logging: Use `RedirectStdLog`

* .gitignore a file pattern that I'm constantly using for testing
2022-04-28 08:42:30 -06:00
Francis Lavoie 2e4c09155a cmd: Fix unix socket addresses for admin API requests (#4742)
Fixes a regression in c2327161f7
2022-04-28 08:31:59 -06:00
Francis Lavoie dcc98da4d2 caddyhttp: Improve listen addr error message for IPv6 (#4740) 2022-04-28 08:18:45 -06:00
Marco Kaufmann 3ab648382d templates: Add missing backticks in docs (#4737) 2022-04-27 11:41:37 -06:00
Matt Holt 40b193fb79 reverseproxy: Improve hashing LB policies with HRW (#4724)
* reverseproxy: Improve hashing LB policies with HRW

Previously, if a list of upstreams changed, hash-based LB policies
would be greatly affected because the hash relied on the position of
upstreams in the pool. Highest Random Weight or "rendezvous" hashing
is apparently robust to pool changes. It runs in O(n) instead of
O(log n), but n is very small usually.

* Fix bug and update tests
2022-04-27 10:39:22 -06:00
Francis Lavoie d543ad1ffd caddypki: Fix caddy trust command to use the correct API endpoint (#4730) 2022-04-25 22:00:39 -06:00
Francis Lavoie a8bb4a665a httpcaddyfile: Add {vars.*} placeholder shortcut, reverse vars sort order (#4726)
* httpcaddyfile: Add `{vars.*}` placeholder shortcut

I'm yoinking this from my https://github.com/caddyserver/caddy/pull/4657 PR because I think we should get this in ASAP for v2.5.0 along with the new `vars` directive.

* Sort vars by matchers in reverse
2022-04-25 10:47:12 -06:00
Francis Lavoie 3a1e0dbf47 httpcaddyfile: Deprecate paths in site addresses; use zap logs (#4728) 2022-04-25 10:12:10 -06:00
Francis Lavoie 77a77c0219 caddytls: Add propagation_delay, support propagation_timeout -1 (#4723) 2022-04-22 16:09:11 -06:00
Matthew Holt db62942d63 Make file modes consistent
No need to have executable bit on .go or .txt files
2022-04-21 15:06:55 -06:00
Matthew Holt dadd4b59b0 Update smallstep/certificates 2022-04-20 11:32:33 -06:00
Mohammed Al Sahaf d230b33007 ci: use latest Go version on macOS (#4708) 2022-04-15 13:58:48 -04:00
Matthew Holt 0d13173071 ci: Fix typo 2022-04-13 14:11:03 -06:00
Francis Lavoie c3a82f53d5 ci: Ensure we always check for latest version of Go (#4703)
* ci: Ensure we always check for latest version of Go

* Try to force 1.18.1, 1.17.9

* Use includes for the actual go semver

* Use `~` for semver here, apparently

* Try to make tests still run on 1.18.0 for Mac, for now
2022-04-13 14:03:38 -06:00
Matthew Holt 30b6d1f47a cmd: Enhance .env (dotenv) file parsing
Basic support for quoted values, newlines in quoted values, and comments.

Does not support variable or command expansion.
2022-04-13 11:38:20 -06:00
Francis Lavoie bc15b4b0e7 caddypki: Load intermediate for signing on-the-fly (#4669)
* caddypki: Load intermediate for signing on-the-fly

Fixes #4517

Big thanks to @maraino for adding an API in `smallstep/certificates` so that we can fix this

* Debug log

* Trying a hunch, does it need to be a pointer receiver?

* Clarify pointer receiver

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

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-04-13 10:20:42 -06:00
cui fliter e2535233bb fix typo (#4702)
Signed-off-by: cuishuang <imcusg@gmail.com>
2022-04-13 10:13:28 -06:00
Francis Lavoie 00234c8ac2 templates: Switch to BurntSushi/toml (#4700) 2022-04-12 13:48:42 -06:00
Francis Lavoie 6512832f9f cmd: Add --diff option for caddy fmt (#4695) 2022-04-12 14:49:19 -04:00
Francis Lavoie 3e3bb00265 reverseproxy: Add _ms placeholders for proxy durations (#4666)
* reverseproxy: Add `_ms` placeholders for proxy durations

* Add http.request.duration_ms

Also add comments, and change duration_sec to duration_ms

* Add response.duration_ms for consistency

* Add missing godoc comment

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2022-04-11 13:04:05 -06:00
Francis Lavoie e4ce40f8ff reverseproxy: Sync up handleUpgradeResponse with stdlib (#4664)
* reverseproxy: Sync up `handleUpgradeResponse` with stdlib

I had left this as a TODO for when we bump to minimum 1.17, but I should've realized it was under `internal` so it couldn't be used directly.

Copied the functions we needed for parity. Hopefully this is ok!

* Add tests and fix godoc comments

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2022-04-11 12:49:56 -06:00
Y.Horie afca242111 staticfiles: Expand placeholder for index files (#4679) 2022-04-07 15:01:09 -06:00
Francis Lavoie 7d229665ed logging: Caddyfile support for duration_format (#4684)
Somehow, this was missed. Oops!
2022-04-07 11:23:28 -06:00
Francis Lavoie 22d8edb984 cmd: Fix defaulting admin address if empty in config, fixes reload (#4674) 2022-04-03 12:04:33 -04:00
Francis Lavoie 734acc776a chore: Fix for xcaddy builds (#4665)
* chore: Attempt fix for xcaddy builds

* Upgrade smallstep/certificates which avoids the problem
2022-03-28 15:07:43 -06:00
Francis Lavoie b4f1a71397 chore: Bump minimum Go to 1.17 (#4662) 2022-03-25 14:56:29 -04:00
Matthew Holt d06d0e79f8 go.mod: Upgrade CertMagic to v0.16.0
Includes several breaking changes; code base updated accordingly.

- Added lots of context arguments
- Use fs.ErrNotExist
- Rename ACMEManager -> ACMEIssuer; CertificateManager -> Manager
2022-03-25 11:28:54 -06:00
Francis Lavoie a58f240d3e httpcaddyfile: Fix #4640 (auto-HTTPS edgecase) (#4661)
Guh, this is complicated.

Fixes #4640

This also follows up on #4398 (reverting it) which made a change that technically worked, but was incorrect. It changed the condition in `hostsFromKeysNotHTTP` from `&&` to `||`, but then the function no longer did what its name said it would do, and it would return hosts even if they were marked with `http://`, if they used a non-HTTP port. That wasn't the intent of it. The test added in there was kept though, because it is a valid usecase.

The actual fix is to check _earlier_ whether all the addresses explicitly have `http://`, and if so we can short circuit and skip considering the rest.
2022-03-24 22:54:03 -06:00
Francis Lavoie 4b75f3e2f0 chore: Clean up adapt test line endings (#4660)
Lots of the files were using CRLF instead of LF. Mostly my fault cause sometimes I make the files on Windows and VSCode for some reason kept making them with the wrong line endings. Sigh.

Since .txt files typically default to spaces for indentation, I'm also adding an .editorconfig to ensure they use tabs instead
2022-03-24 22:48:45 -06:00
Matthew Holt b8dbecb841 reverseproxy: Include port in A upstreams cache
Should fix #4659
2022-03-24 10:44:36 -06:00
Francis Lavoie 134b805644 caddyfile: Prevent bad block opening tokens (#4655)
* caddyfile: Prevent bad block opening tokens

* Clarifying comments
2022-03-23 12:34:13 -06:00
Artem Mikheev c9b5e7f77b Fix http3 servers dying after reload (#4654) 2022-03-22 19:47:57 -04:00
Matthew Holt 79cbe7bfd0 httpcaddyfile: Add 'vars' directive
See discussion in #4650
2022-03-22 10:47:21 -06:00
Matthew Holt 55b4c12e04 map: Evaluate placeholders in output vals (#4650) 2022-03-21 17:05:38 -06:00
Matthew Holt 2196c92c0e reverseproxy: Don't clear name in SRV upstreams
Fix for dc4d147388
2022-03-21 08:33:24 -06:00
Matthew Holt c2327161f7 cmd: Set Origin header properly on API requests
Ref. https://caddy.community/t/bug-in-enforce-origin/15417
2022-03-19 22:51:32 -06:00
Francis Lavoie c5fffb4ac2 caddyfile: Support for raw token values; improve map, expression (#4643)
* caddyfile: Support for raw token values, improve `map`, `expression`

* Applied code review comments

* Rename RawVal to ValRaw

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2022-03-18 15:08:23 -06:00
Matthew Holt dc4d147388 reverseproxy: Expand SRV/A addrs for cache key
Hopefully fix #4645
2022-03-18 13:42:29 -06:00
Matthew Holt 93c99f6734 map: Support numeric and bool types with Caddyfile
Based on caddyserver/website#221
2022-03-17 17:53:32 -06:00
Francis Lavoie 4e9fbee1e2 ci: Build on Go 1.18, bump actions versions (#4637)
* ci: Build on Go 1.18, bump actions versions

* Revert linter version bump for now

* Try linter again
2022-03-15 22:09:19 +00:00
Francis Lavoie a9c7e94a38 chore: Comment fixes (#4634) 2022-03-13 01:38:11 -05:00
Matthew Holt 3d616e8c6d requestbody: Return HTTP 413 (fix #4558) 2022-03-11 12:34:55 -07:00
Mohammed Al Sahaf b82e22b459 caddyhttp: retain all values of vars matcher when specified multiple times (#4629) 2022-03-11 10:55:37 -05:00
Matthew Holt bf6a1b7538 go.mod: Upgrade some dependencies
Fixes bug in yuin/goldmark
https://github.com/caddyserver/website/issues/217
2022-03-10 11:40:03 -07:00
Francis Lavoie c7d6c4cbb9 reverseproxy: copy_response and copy_response_headers for handle_response routes (#4391)
* reverseproxy: New `copy_response` handler for `handle_response` routes

Followup to #4298 and #4388.

This adds a new `copy_response` handler which may only be used in `reverse_proxy`'s `handle_response` routes, which can be used to actually copy the proxy response downstream. 

Previously, if `handle_response` was used (with routes, not the status code mode), it was impossible to use the upstream's response body at all, because we would always close the body, expecting the routes to write a new body from scratch.

To implement this, I had to refactor `h.reverseProxy()` to move all the code that came after the `HandleResponse` loop into a new function. This new function `h.finalizeResponse()` takes care of preparing the response by removing extra headers, dealing with trailers, then copying the headers and body downstream.

Since basically what we want `copy_response` to do is invoke `h.finalizeResponse()` at a configurable point in time, we need to pass down the proxy handler, the response, and some other state via a new `req.WithContext(ctx)`. Wrapping a new context is pretty much the only way we have to jump a few layers in the HTTP middleware chain and let a handler pick up this information. Feels a bit dirty, but it works.

Also fixed a bug with the `http.reverse_proxy.upstream.duration` placeholder, it always had the same duration as `http.reverse_proxy.upstream.latency`, but the former was meant to be the time taken for the roundtrip _plus_ copying/writing the response.

* Delete the "Content-Length" header if we aren't copying

Fixes a bug where the Content-Length will mismatch the actual bytes written if we skipped copying the response, so we get a message like this when using curl:

```
curl: (18) transfer closed with 18 bytes remaining to read
```

To replicate:

```
{
	admin off
	debug
}

:8881 {
	reverse_proxy 127.0.0.1:8882 {
		@200 status 200
		handle_response @200 {
			header Foo bar
		}
	}
}

:8882 {
	header Content-Type application/json
	respond `{"hello": "world"}` 200
}
```

* Implement `copy_response_headers`, with include/exclude list support

* Apply suggestions from code review

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-03-09 11:00:51 -07:00
Andrii Kushch d0b608af31 tracing: New OpenTelemetry module (#4361)
* opentelemetry: create a new module

* fix imports

* fix test

* Update modules/caddyhttp/opentelemetry/README.md

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* Update modules/caddyhttp/opentelemetry/README.md

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* Update modules/caddyhttp/opentelemetry/README.md

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* Update modules/caddyhttp/opentelemetry/tracer.go

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* rename error ErrUnsupportedTracesProtocol

* replace spaces with tabs in the test data

* Update modules/caddyhttp/opentelemetry/README.md

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

* Update modules/caddyhttp/opentelemetry/README.md

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

* replace spaces with tabs in the README.md

* use default values for a propagation and exporter protocol

* set http attributes with helper

* simplify code

* Cleanup modules/caddyhttp/opentelemetry/README.md

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* Update link in README.md

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* Update documentation in README.md

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* Update link to naming spec in README.md

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* Rename module from opentelemetry to tracing

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* Rename span_name to span

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* Rename span_name to span

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* Simplify otel resource creation

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* handle extra attributes

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* update go.opentelemetry.io/otel/semconv to 1.7.0

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* update go.opentelemetry.io/otel version

* remove environment variable handling

* always use tracecontext,baggage as propagators

* extract tracer name into variable

* rename OpenTelemetry to Tracing

* simplify resource creation

* update go.mod

* rename package from opentelemetry to tracing

* cleanup tests

* update Caddyfile example in README.md

* update README.md

* fix test

* fix module name in README.md

* fix module name in README.md

* change names in README.md and tests

* order imports

* remove redundant tests

* Update documentation README.md

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* Fix grammar

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* Update comments

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* Update comments

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* update go.sum

* update go.sum

* Add otelhttp instrumentation, update OpenTelemetry libraries.

* Use otelhttp instrumentation for instrumenting HTTP requests.

This change uses context.WithValue to inject the next handler into the
request context via a "nextCall" carrier struct, and pass it on to a
standard Go HTTP handler returned by otelhttp.NewHandler. The
underlying handler will extract the next handler from the context,
call it and pass the returned error to the carrier struct.

* use zap.Error() for the error log

* remove README.md

* update dependencies

* clean up the code

* change comment

* move serveHTTP method from separate file

* add syntax to the UnmarshalCaddyfile comment

* go import the file

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

* update dependencies

Co-authored-by: Dave Henderson <dhenderson@gmail.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Vibhav Pant <vibhavp@gmail.com>
Co-authored-by: Alok Naushad <alokme123@gmail.com>
Co-authored-by: Cedric Ziel <cedric@cedric-ziel.com>
2022-03-08 12:18:32 -07:00
Ran Chen d9b1d46325 caddytls: dns_challenge_override_domain for challenge delegation (#4596)
* Add a override_domain option to allow DNS chanllenge delegation

CNAME can be used to delegate answering the chanllenge to another DNS
zone. One usage is to reduce the exposure of the DNS credential [1].
Based on the discussion in caddy/certmagic#160, we are adding an option
to allow the user explicitly specify the domain to delegate, instead of
following the CNAME chain.

This needs caddy/certmagic#160.

* rename override_domain to dns_challenge_override_domain

* Update CertMagic; fix spelling

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2022-03-08 12:03:43 -07:00
Francis Lavoie c8f2834b51 fastcgi: Protect against requests with null bytes in the path (#4614) 2022-03-07 10:06:33 -07:00
Matt Holt ab0455922a reverseproxy: Dynamic upstreams (with SRV and A/AAAA support) (#4470)
* reverseproxy: Begin refactor to enable dynamic upstreams

Streamed here: https://www.youtube.com/watch?v=hj7yzXb11jU

* Implement SRV and A/AAA upstream sources

Also get upstreams at every retry loop iteration instead of just once
before the loop. See #4442.

* Minor tweaks from review

* Limit size of upstreams caches

* Add doc notes deprecating LookupSRV

* Provision dynamic upstreams

Still WIP, preparing to preserve health checker functionality

* Rejigger health checks

Move active health check results into handler-specific Upstreams.

Improve documentation regarding health checks and upstreams.

* Deprecation notice

* Add Caddyfile support, use `caddy.Duration`

* Interface guards

* Implement custom resolvers, add resolvers to http transport Caddyfile

* SRV: fix Caddyfile `name` inline arg, remove proto condition

* Use pointer receiver

* Add debug logs

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2022-03-06 17:43:39 -07:00
Francis Lavoie c50094fc9d reverseproxy: Implement trusted proxies for X-Forwarded-* headers (#4507) 2022-03-06 18:51:55 -05:00
Francis Lavoie d058dee11d reverseproxy: Refactor dial address parsing, augment command parsing (#4616) 2022-03-05 16:34:19 -07:00
Francis Lavoie 09ba9e994e fileserver: Add pass_thru Caddyfile option (#4613) 2022-03-04 20:50:05 -07:00
Matthew Holt be82cc7aca Appease the linter 2022-03-04 20:26:37 -07:00
Matt Holt 2bb8550a4c caddyhttp: Honor wildcard hosts in log SkipHosts (#4606) 2022-03-04 13:44:59 -07:00
Matthew Holt a72acd21b0 core: Retry dynamic config load if config unchanged
(see discussion in #4603)
2022-03-03 21:41:51 -07:00
Matthew Holt a6199cf814 templates: Fix docs for .Args 2022-03-03 11:12:37 -07:00
Matthew Holt ceef70dbc5 core: Retry dynamic config load if error or no-op (#4603)
Also fix ineffectual assignment (unrelated)
2022-03-03 10:58:15 -07:00
Francis Lavoie f5e104944e reverseproxy: Make shallow-ish clone of the request (#4551)
* reverseproxy: Make shallow-ish clone of the request

* Refactor request cloning into separate function

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2022-03-03 09:54:45 -07:00
Matthew Holt 6b385a36f9 caddyhttp: Don't attempt to manage Tailscale certs
If .ts.net domains are explicitly added to config,
don't try to manage a cert for them (it will fail, and our
implicit Tailscale module will
get those certs at run-time).
2022-03-02 13:42:38 -07:00
Matthew Holt 9b7cdfa2f2 caddypki: Try to fix lint warnings 2022-03-02 13:38:05 -07:00
Matthew Holt 78e381b29f caddypki: Refactor /pki/ admin endpoints
Remove /pki/certificates/<ca> endpoint and split into two endpoints:

- GET /pki/ca/<id> to get CA info and certs in JSON format
- GET /pki/ca/<id>/certificates to get cert in PEM chain
2022-03-02 13:00:37 -07:00
ttys3 de490c7cad fastcgi: Set SERVER_PORT to 80 or 443 depending on scheme (#4572) 2022-03-02 11:24:16 -07:00
Francis Lavoie bbad6931e3 pki: Implement API endpoints for certs and caddy trust (#4443)
* admin: Implement /pki/certificates/<id> API

* pki: Lower "skip_install_trust" log level to INFO

See https://github.com/caddyserver/caddy/issues/4058#issuecomment-976132935

It's not necessary to warn about this, because this was an option explicitly configured by the user. Still useful to log, but we don't need to be so loud about it.

* cmd: Export functions needed for PKI app, return API response to caller

* pki: Rewrite `caddy trust` command to use new admin endpoint instead

* pki: Rewrite `caddy untrust` command to support using admin endpoint

* Refactor cmd and pki packages for determining admin API endpoint
2022-03-02 11:08:36 -07:00
Francis Lavoie 5bd96a6ac2 httpcaddyfile: Support explicitly turning off strict_sni_host (#4592) 2022-03-01 20:02:39 -05:00
BitWuehler ac14b64e08 caddyhttp: Support zone identifiers in remote_ip matcher (#4597)
* Update matchers.go

* Update matchers.go

* implementation of zone_id handling

* last changes in zone handling

* give return true values instead of bool

* Apply suggestions from code review

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

* changes as suggested

* Apply suggestions from code review

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

* Update matchers.go

* shortened the Match function

* changed mazcher handling

* Update matchers.go

* delete space

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2022-03-01 15:50:12 -07:00
Francis Lavoie 15c95e9d5b fileserver: Canonical redir when whole path is stripped (#4549) 2022-03-01 15:32:39 -07:00
Matthew Holt bc447e307f core: Config LoadInterval -> LoadDelay for clarity
And improve/clarify docs about this feature

See #4577
2022-03-01 15:05:12 -07:00
Francis Lavoie 87a1f228b4 reverseproxy: Move status replacement intercept to replace_status (#4300) 2022-03-01 14:12:43 -07:00
Matthew Holt acbee94708 core: Revert 7f364c7; simplify dynamic config load
Fixes #4577
2022-03-01 13:00:14 -07:00
Noorain Panjwani 7ea5b2a818 core: Config load interval only reloads if changed (#4603) 2022-03-01 11:32:33 -07:00
Francis Lavoie 186fdba916 caddyhttp: Move HTTP redirect listener to an optional module (#4585) 2022-02-19 15:36:36 -07:00
Mohammed Al Sahaf 7778912d4e ci: update goreleaser (#4582) 2022-02-19 15:16:11 -07:00
Francis Lavoie c921e08296 logging: Add roll_local_time Caddyfile option (#4583) 2022-02-19 15:12:28 -07:00
Francis Lavoie ddbb234d91 caddyhttp: Always log handled errors at debug level (#4584) 2022-02-19 15:10:49 -07:00
Francis Lavoie 0de51593a6 go.mod: Revert version bump of CEL (#4587) 2022-02-19 15:09:09 -07:00
Francis Lavoie 26d633baf8 httpcaddyfile: Disabling OCSP stapling for both managed and unmanaged (#4589) 2022-02-19 14:20:38 -07:00
Matthew Holt ff137d17d0 caddyconfig: Support placeholders in HTTP loader 2022-02-17 22:58:25 -07:00
Matt Holt 57a708d189 caddytls: Support external certificate Managers (like Tailscale) (#4541)
Huge thank-you to Tailscale (https://tailscale.com) for making this change possible!
This is a great feature for Caddy and Tailscale is a great fit for a standard implementation.

* caddytls: GetCertificate modules; Tailscale

* Caddyfile support for get_certificate

Also fix AP provisioning in case of empty subject list (persist loaded
module on struct, much like Issuers, to surive reprovisioning).

And implement start of HTTP cert getter, still WIP.

* Update modules/caddytls/automation.go

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

* Use tsclient package, check status for name

* Implement HTTP cert getter

And use reuse CertMagic's PEM functions for private keys.

* Remove cache option from Tailscale getter

Tailscale does its own caching and we don't need the added complexity...
for now, at least.

* Several updates

- Option to disable cert automation in auto HTTPS
- Support multiple cert managers
- Remove cache feature from cert manager modules
- Minor improvements to auto HTTPS logging

* Run go mod tidy

* Try to get certificates from Tailscale implicitly

Only for domains ending in .ts.net.

I think this is really cool!

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2022-02-17 15:40:34 -07:00
Alok Naushad 32aad90938 admin: Write proper status on invalid requests (#4569) (fix #4561) 2022-02-15 12:13:33 -07:00
Matthew Holt 40b54434f3 admin: Enforce and refactor origin checking
Using URLs seems a little cleaner and more correct

cf: https://caddy.community/t/protect-admin-endpoint/15114

(This used to work. Something must have changed recently.)
2022-02-15 12:08:12 -07:00
Francis Lavoie 1d0425b26f templates: Elaborate on what's supported by the markdown function (#4564) 2022-02-06 22:14:41 -07:00
Francis Lavoie 7557d1d922 reverseproxy: Avoid returning a nil error during GetClientCertificate (#4550) 2022-02-01 23:33:36 -07:00
Matthew Holt ff74a0aa09 go.mod: Upgrade dependencies
Including crucial CertMagic upgrade
2022-02-01 21:00:23 -07:00
Matthew Holt 599c81d753 Interrim upgrade CertMagic
For auto-replace certificate on revocation for on-demand mode,
until a proper release is made.
2022-01-30 22:46:25 -07:00
Dave Henderson 741b0502ee Merge pull request #4545 from hairyhenderson/metrics-restrict-http-methods
metrics: Enforce smaller set of method labels
2022-01-25 15:34:35 -05:00
Dave Henderson 7ca5921a87 move common metrics-related funcs to internal package
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
2022-01-25 15:07:17 -05:00
Francis Lavoie da4a759bad Update modules/caddyhttp/metrics_test.go 2022-01-25 15:07:17 -05:00
Dave Henderson 042abeb431 other is not uppercase
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
2022-01-25 15:07:17 -05:00
Dave Henderson eb891d4683 metrics: Enforce smaller set of method labels
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
2022-01-25 15:07:17 -05:00
Kevin Daudt 44e5e9e43f caddyhttp: Fix test when /tmp/etc already exists (#4544)
The TestFileListing test in tplcontext_test has one test that verifies
if directory traversal is not happening. The context root is set to
'/tmp' and then it tries to open '../../../../../etc', which gets
normalized to '/tmp/etc'.

The test then expects an error to be returned, assuming that '/tmp/etc'
does not exist on the system. When it does exist, it results in a test
failure:

```
--- FAIL: TestFileListing (0.00s)
    tplcontext_test.go:422: Test 4: Expected error but had none
    FAIL
    FAIL
    github.com/caddyserver/caddy/v2/modules/caddyhttp/templates	0.042s
```

Instead of using '/tmp' as root, use a dedicated directory created with
`os.MkdirTemp()` instead. That way, we know that the directory is empty.
2022-01-24 14:41:08 -07:00
Matt Holt bf380d00ab caddyhttp: Reject absurd methods (#4538)
* caddyhttp: Reject absurdly long methods

* Limit method to 32 chars and truncate

* Just reject the request and debug-log it

* Log remote address
2022-01-19 13:44:09 -07:00
Vojtech Vitek 94035c1797 Improve the reverse-proxy CLI --to flag help message (#4535) 2022-01-19 14:51:46 -05:00
Forest Johnson b3f7ce34b4 More explanatory error message from Listen (#4534)
* explain cryptic unix socket listener error related to process kill

https://github.com/caddyserver/caddy/pull/4533

* less ambiguous wording: clean up -> delete

* shorten error message explanation

* link back to pull request in comment for later archeaology
2022-01-19 12:26:44 -07:00
Francis Lavoie a79b4055e5 caddytls: Add internal Caddyfile lifetime, sign_with_root opts (#4513) 2022-01-18 12:19:50 -07:00
Francis Lavoie 5a07156894 httpcaddyfile: Add pki app root and intermediate cert/key config (#4514) 2022-01-18 12:18:31 -07:00
Francis Lavoie bcb7a19cd3 rewrite: Add method Caddyfile directive (#4528) 2022-01-18 12:17:35 -07:00
Francis Lavoie 6e6ce2be6b caddyhttp: Fix HTTP->HTTPS redir not preferring HTTPS port if ambiguous (#4530) 2022-01-18 11:56:00 -07:00
Francis Lavoie 1b7ff5d76c httpcaddyfile: Add default_bind global option (#4531) 2022-01-18 11:29:07 -07:00
Francis Lavoie 93a7a45e7e httpcaddyfile: Fix incorrect handling of IPv6 bind addresses (#4532)
The `net.JoinHostPort()` function has some naiive logic for handling IPv6, it just checks if the host part has a `:` and if so it wraps the host part with `[ ]` but this causes our network type prefix to get wrapped as well, which is invalid for `caddy.NetworkAddress`. Instead, we can just concatenate the host and port manually here to avoid this side-effect.
2022-01-18 11:27:43 -07:00
Matthew Holt 1a7a78a1f2 cmd: Print error if fmt overwrite fails (fix #4524) 2022-01-16 17:30:14 -07:00
Francis Lavoie 1feb65952a rewrite: Fix a double-encode issue when using the {uri} placeholder (#4516) 2022-01-13 12:17:15 -05:00
GallopingKylin 66de438a98 caddytls: Fix MatchRemoteIP provisoning with multiple CIDR ranges (#4522) 2022-01-13 11:56:18 -05:00
rayjlinden 850e1605df caddyhttp: Return HTTP 421 for mismatched Host header (#4023)
Potential fix for #4017 although the consensus is unclear.

Made change to return status code 421 instead of 403 when StrictSNIHost matching is on.
2022-01-12 14:24:22 -07:00
Matthew Holt af1ac9cd2e Fix lint warnings 2022-01-10 23:27:39 -07:00
Matthew Holt 64a3218f5c core: Simplify shared listeners, fix deadline bug
When this listener code was first written, UsagePool didn't exist. We
can simplify much of the wrapped listener logic by utilizing UsagePool.

This also fixes a bug where new servers were able to clear deadlines
set by old servers, even if the old server didn't get booted out of its
Accept() call yet. And with the deadline cleared, they never would.
(Sometimes. Based on reports and difficulty of reproducing the bug,
this behavior was extremely rare.) I don't know why that happened
exactly, maybe some polling mechanism in the kernel and if the timings
worked out just wrong it would expose the bug.

Anyway, now we ensure that only the closer that set the deadline is the
same one that clears it, ensuring that old servers always return out of
Accept(), because the deadline doesn't get cleared until they do.

Of course, all this hinges on the hope that my suspicions in the middle
of the night are correct and that kernels work the way I think they do
in my head.

Also minor enhancement to UsagePool where if a value errors upon
construction (a very real possibility with listeners), it is removed from
the pool. Not 100% sure the sync logic is correct there, or maybe we
don't have to even put it in the pool until after construction, but it's
subtle either way and I think this is safe... right?
2022-01-10 23:24:58 -07:00
Matthew Holt c634bbe9cc caddypki: Return error if no PEM data found
Best guess for https://caddy.community/t/on-fly-certificate-generation-based-on-sni/14639/4
2022-01-07 10:55:11 -07:00
Francis Lavoie 4b9849c792 httpcaddyfile: Support configuring pki app names via global options (#4450) 2022-01-05 22:45:41 -05:00
Francis Lavoie 80d7a356b3 caddyhttp: Redirect HTTP requests on the HTTPS port to https:// (#4313)
* caddyhttp: Redirect HTTP requests on the HTTPS port to https://

* 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-01-05 18:01:15 -07:00
Matthew Holt b4bfa29be2 admin: Require identity for remote (fix #4478) 2022-01-05 17:55:09 -07:00
Matthew Holt 6cadb60fa2 templates: Document .OriginalReq
Close caddyserver/website#91
2022-01-05 13:59:59 -07:00
Денис Телюх 2e46c2ac1d admin, reverseproxy: Stop timers if canceled to avoid goroutine leak (#4482) 2022-01-04 12:14:18 -07:00
Francis Lavoie 249adc1c87 logging: Support turning off roll compression via Caddyfile (#4505) 2022-01-04 12:11:27 -07:00
Francis Lavoie e9dde23024 headers: Fix + in Caddyfile to properly append rather than set (#4506) 2022-01-04 10:10:11 -07:00
Francis Lavoie 3fe2c73dd0 caddyhttp: Fix MatchPath sanitizing (#4499)
This is a followup to #4407, in response to a report on the forums: https://caddy.community/t/php-fastcgi-phishing-redirection/14542

Turns out that doing `TrimRight` to remove trailing dots, _before_ cleaning the path, will cause double-dots at the end of the path to not be cleaned away as they should. We should instead remove the dots _after_ cleaning.
2021-12-30 04:15:48 -05:00
Francis Lavoie 5333c3528b reverseproxy: Fix incorrect health_headers Caddyfile parsing (#4485)
Fixes #4481
2021-12-17 08:53:11 -07:00
Rainer Borene 180ae0cc48 caddyhttp: Implement http.request.uuid placeholder (#4285) 2021-12-15 00:17:53 -07:00
Matthew Holt a1c41210d3 caddypki: Minor tweak, don't use context pointer 2021-12-13 16:13:38 -07:00
Matt Holt ecac03cdcb caddyhttp: Enhance vars matcher (#4433)
* caddyhttp: Enhance vars matcher

Enable "or" logic for multiple values.
Fall back to checking placeholders if not a var name.

* Fix tests (thanks @mohammed90 !)
2021-12-13 13:59:58 -07:00
Francis Lavoie c04d24cafa pki: Avoid provisioning the local CA when not necessary (#4463)
* pki: Avoid provisioning the `local` CA when not necessary

* pki: Refactor CA loading to keep the logic in the PKI app
2021-12-13 12:25:35 -07:00
Francis Lavoie 81ee34e962 httpcaddyfile: Fix sorting edgecase for nested handle_path (#4477) 2021-12-13 13:42:08 -05:00
Mohammed Al Sahaf 78b5356f2b fileserver: do not double-escape paths (#4447) 2021-12-11 09:26:21 -05:00
Francis Lavoie 6f9b6ad78e go.mod: Update smallstep/certificates, no longer need replace (#4475) 2021-12-10 14:58:53 -05:00
Francis Lavoie 4906b9357a go.mod: Update smallstep/truststore, fix build on FreeBSD (#4473) 2021-12-09 15:57:26 -05:00
Runzhi He e90d751732 caddyfile: impove fmt warning message (#4444)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2021-12-07 10:03:58 -07:00
Adam Burgess dce81e85d5 docs: use backticks to not italicise glob path (#4460) 2021-12-05 23:48:40 -07:00
Kévin Dunglas a1b417c832 logging: add support for hashing data (#4434)
* logging: add support for hashing data

* Update modules/logging/filters.go

Co-authored-by: wiese <wiese@users.noreply.github.com>

* Update modules/logging/filters.go

Co-authored-by: wiese <wiese@users.noreply.github.com>

Co-authored-by: wiese <wiese@users.noreply.github.com>
2021-12-02 13:51:37 -07:00
Francis Lavoie 5bf0adad87 caddyhttp: Make logging of credential headers opt-in (#4438) 2021-12-02 13:26:24 -07:00
Francis Lavoie 8e5aafa5cd fastcgi: Fix a TODO, prevent zap using reflection for logging env (#4437)
* fastcgi: Fix a TODO, prevent zap using reflection for logging env

* Update modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2021-12-02 13:23:19 -07:00
Francis Lavoie c133153447 go.mod: Update to latest smallstep/truststore, support FreeBSD (#4453) 2021-11-29 17:15:41 -07:00
Tim Culverhouse ec14ccdd40 templates: fix inconsistent nested includes (#4452) 2021-11-29 12:29:40 -05:00
Francis Lavoie f55b123d63 caddyhttp: Split up logged remote address into IP and port (#4403) 2021-11-29 01:18:35 -05:00
Matt Holt 0eb0b60f47 logging: Remove common_log field and single_field encoder (#4149) (#4282) 2021-11-29 01:08:52 -05:00
Rainer Borene 5e5af50e64 caddyfile: make renew_interval option configurable (#4451) 2021-11-28 17:22:26 -05:00
Francis Lavoie 9ee68c1bd5 reverseproxy: Adjust defaults, document defaults (#4436)
* reverseproxy: Adjust defaults, document defaults

Related to some of the issues in https://github.com/caddyserver/caddy/issues/4245, a complaint about the proxy transport defaults not being properly documented in https://caddy.community/t/default-values-for-directives/14254/6.

- Dug into the stdlib to find the actual defaults for some of the timeouts and buffer limits, documenting them in godoc so the JSON docs get them next release.

- Moved the keep-alive and dial-timeout defaults from `reverseproxy.go` to `httptransport.go`. It doesn't make sense to set defaults in the proxy, because then any time the transport is configured with non-defaults, the keep-alive and dial-timeout defaults are lost!

- Sped up the dial timeout from 10s to 3s, in practice it rarely makes sense to wait a whole 10s for dialing. A shorter timeout helps a lot with the load balancer retries, so using something lower helps with user experience.

* reverseproxy: Make keepalive interval configurable via Caddyfile

* fastcgi: DialTimeout default for fastcgi transport too
2021-11-24 01:32:25 -05:00
Kévin Dunglas 789efa5dee logging: add a regexp filter (#4426) 2021-11-23 10:00:20 -07:00
Kévin Dunglas 8887adb027 logging: add a filter for cookies (#4425)
* feat(logging): add a filter for cookies

* Improve godoc and add validation
2021-11-23 09:40:20 -07:00
Kévin Dunglas bcac2beee7 logging: add a filter for query parameters (#4424)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2021-11-23 04:01:43 -05:00
Mohammed Al Sahaf 1e10f6f725 fileserver: browse: do not encode the paths in breadcrumbs and page title (#4410) 2021-11-23 03:13:09 -05:00
Jeremy Lin c8b5a81607 fileserver: Fix handling of symlink sizes in directory listings (#4415) 2021-11-22 14:59:09 -07:00
Francis Lavoie eead337324 caddyhttp: Log non-500 handler errors at debug level (#4429)
Fixes #4428

It's best to still log handler errors at debug level so that they're hidden by default, but still accessible if additional details are necessary.
2021-11-22 11:58:25 -07:00
Matthew Holt 7d5047c1f1 caddyhttp: Log empty value for typical password headers
Work around for common misconfiguration
2021-11-22 11:31:50 -07:00
Matthew Holt 7f364c777a core: Load config at interval instead of just once 2021-11-16 13:08:22 -07:00
Matthew Holt b47af6ef04 caddyfile: Copy input before parsing (fix #4422) 2021-11-15 14:41:19 -07:00
Jeremy Lin e81369e220 fileserver: Move default browse template into a separate file (#4417)
This makes it easier for users to find the default browse template if they
want to create a custom template based on that. It also makes it easier to
view the template with proper syntax highlighting.
2021-11-15 11:53:54 -07:00
Francis Lavoie e7457b43e4 caddyhttp: Sanitize the path before evaluating path matchers (#4407) 2021-11-08 13:45:03 -07:00
Matthew Holt f376a38b25 go.mod: Update ACMEz and CertMagic 2021-11-08 13:08:50 -07:00
Francis Lavoie 749e55c738 caddycmd: Add --keep-backup to upgrade commands (#4387)
* caddycmd: Add `--skip-cleanup` to upgrade commands

This is a partial fix for https://github.com/caddyserver/caddy/issues/4057, making it possible to retain the old build of Caddy, in case something went wrong.

* caddycmd: Fix duplicate error message

The error message "download succeeded, but unable to execute" was repeated, because it was both in the `listModules`/`showVersion` functions and in the calling `upgradeBuild` function. Oversight when this was refactored.

* caddycmd: Implement fix for performing cleanup on Windows

Without this, the cleanup operation would fail with an error message like this:

upgrade: download succeeded, but unable to clean up backup binary: remove C:\caddy\caddy.exe.tmp: Access is denied.

* caddycmd: Rename to `--keep-backup`, simplify build constraints
2021-11-08 11:35:46 -07:00
Matt Holt 24fda7514d caddytls: Mark storage clean timestamp at end of routine (#4401)
See discussion on 42b7134ffa
2021-11-02 08:27:25 -06:00
Matthew Holt 3385856966 Fix lint message in metrics tests 2021-10-27 13:44:46 -06:00
Francis Lavoie f73f55dba7 reverseproxy: Sanitize scheme and host on incoming requests (#4237)
* caddyhttp: Sanitize scheme and host on incoming requests

* reverseproxy: Sanitize the URL scheme and host before proxying

* 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>
2021-10-26 14:41:28 -06:00
Marc Easen 012d235314 httpcaddyfile: Empty tls policy for internal http localhost (#4398)
* test: replicated empty tls automation policy issue

* fix: empty tls policy for an http:// endpoint running on a non-standard http port
2021-10-26 13:54:19 -06:00
Matthew Holt 997e41deae go.mod: Replace promptui with Apache-compatible fork (fix #4394)
Ideally this needs to be fixed upstream in github.com/manifoldco/promptui, but it appears unmaintained. Our dependency is extremely indirect:

    $ go mod why github.com/juju/ansiterm
    # github.com/juju/ansiterm
    github.com/caddyserver/caddy/v2/modules/caddypki
    github.com/smallstep/certificates/authority
    go.step.sm/cli-utils/ui
    github.com/manifoldco/promptui
    github.com/juju/ansiterm

And it appears that all dependencies in this chain are in conflict with the LGPL license.

Ref:
- https://github.com/manifoldco/promptui/issues/173
- https://github.com/manifoldco/promptui/pull/181

/cc @maraino
2021-10-21 13:44:16 -06:00
Matthew Holt 0ffb2229b0 httpcaddyfile: Preserve IPv6 addresses through normalization (fix #4381)
Remove unnecessary Key() method and improve related tests
2021-10-20 10:27:59 -06:00
Klaus Helenius a21d5a001f fileserver: Prevent focusing filter from scrolling on page load (#4393) 2021-10-20 12:15:58 -04:00
Matthew Holt a2119c09e9 map: Fix 95c03506 (avoid repeated expansions) 2021-10-19 12:25:36 -06:00
Francis Lavoie 062657d0d8 caddycmd: Add --skip-standard to list-modules command, quieter output (#4386)
* caddycmd: Add --skip-standard to list-modules command, quieter output

* caddycmd: Also quiet `caddy upgrade` output, redundant information
2021-10-18 12:19:04 -06:00
Francis Lavoie b092061591 reverseproxy: Prevent copying the response if a response handler ran (#4388) 2021-10-18 14:00:43 -04:00
Y.Horie 64f8b557b1 fileserver: Fix compression breaks using httpInclude (#4352) (#4358) 2021-10-16 11:09:16 -04:00
Matthew Holt 95c035060f map: Fix regex mappings
It didn't really make sense how we were doing them before. See https://caddy.community/t/map-directive-and-regular-expressions/13866/6?u=matt
2021-10-13 17:58:20 -06:00
Matthew Holt c4790d7f9d go.mod: Carefully upgrade some dependencies (fix #4251)
The upgrade of smallstep/certificates fixes #4251. The upgrade of CertMagic fixes an issue reported in the forum that a longer timeout was confirmed to resolve (without any particular explanation, but oh well). Other upgrades have minor improvements and seem safe.
2021-10-12 01:08:28 -06:00
Simão Gomes Viana 837cdc566d caddyhttp: reverseproxy: clarify warning for -insecure (#4379)
The question would only receive bad answers so it's better
to just say what the option actually does.
2021-10-11 16:15:00 -06:00
M. Ángel Jimeno be5f77e84d caddycmd: fix caddy validate/fmt help message (#4377)
* caddycmd: fix caddy validate help message

Fixes #4376

* caddycmd: fix caddy fmt help message
2021-10-11 11:56:03 -04:00
Oleg cbb045a121 caddyhttp: Placeholder for client cert in DER + base64 format (#4241)
* client.certificate_pem_encoded in base64 format

* base64-encoding without pem encoding;naming change

* fix cert.Raw instead of block.bytes
2021-10-01 16:27:29 -06:00
KallyDev c48fadc4a7 Move from deprecated ioutil to os and io packages (#4364) 2021-09-29 11:17:48 -06:00
Matthew Holt 059fc32f00 Revert 3336faf2 (close #4360)
Debug log is correct level for this
2021-09-27 12:06:06 -06:00
Matthew Holt e2d964ea30 Add explanation for project name to readme 2021-09-27 10:33:32 -06:00
Matthew Holt 501da21f20 General minor improvements to docs 2021-09-24 18:31:01 -06:00
Matthew Holt 3336faf254 reverseproxy: Log error at error level (fix #4360) 2021-09-24 18:29:23 -06:00
Tim Culverhouse 16f752125f templates: Add tests for funcInclude and funcImport (#4357)
* Update tplcontext.go

Add {{ render "/path/to/file.ext" $data }} via funcRender

* Update tplcontext.go

* Refactor funcInclude, add funcImport to enable {{block}} and {{template}}

* Fix funcImport return of nil showing up in html

* Update godocs for  and

* Add tests for funcInclude

* Add tests for funcImport

* os.RemoveAll -> os.Remove for TestFuncInclude and TestFuncImport
2021-09-20 12:29:37 -06:00
Slavik 0a5f7a677f fileserver: Make file listing links purple once visited (#4356) 2021-09-19 22:01:11 -06:00
HayatoShiba d3a0259944 fileserver: Fix displayed file size if it is symlink (#4354)
* Fix file size if it is symlink

* change the variable name for readability
2021-09-18 05:51:59 -06:00
Tim Culverhouse 5fda9610f9 templates: Add 'import' action (#4321)
Related to (closed) Issue #2094 on template inheritance. This PR adds a new function called "import" which works like "include", except it only takes one argument and passes it to the referenced file to be used as "." in that file.

* Update tplcontext.go

Add {{ render "/path/to/file.ext" $data }} via funcRender

* Update tplcontext.go

* Refactor funcInclude, add funcImport to enable {{block}} and {{template}}

* Fix funcImport return of nil showing up in html

* Update godocs for  and
2021-09-17 13:00:36 -06:00
Francis Lavoie 3f2c3ecf85 fastcgi: Implement try_files override in Caddyfile directive (#4347) 2021-09-17 08:23:06 -06:00
Francis Lavoie 907e2d8d3a caddyhttp: Add support for triggering errors from try_files (#4346)
* caddyhttp: Add support for triggering errors from `try_files`

* caddyhttp: Use vars instead of placeholders/replacer for matcher errors

* caddyhttp: Add comment for matcher error var key
2021-09-17 00:52:32 -06:00
Mohammed Al Sahaf 33c70f418f fileserver: properly handle escaped/non-ascii paths (#4332)
* fileserver: properly handle escaped/non-ascii paths

* fileserver: tests: accommodate Windows hate of colons in files names
2021-09-16 20:40:31 +00:00
Matthew Holt 2ebfda1ae9 Make copyright notice more consistent
Some files had the old copyright or were missing the license comment entirely.

Also change Light Code Labs to Dyanim in security contact and releases.
2021-09-16 12:50:32 -06:00
Matthew Holt 2392478bd3 templates: Propagate httpError to HTTP response
Now possible with Go 1.17.
See https://github.com/golang/go/issues/34201.
2021-09-15 09:55:57 -06:00
Matthew Holt a437206643 headers: Canonicalize case in replace (fix #4330) 2021-09-13 10:13:32 -06:00
198 changed files with 12486 additions and 2404 deletions
+5
View File
@@ -0,0 +1,5 @@
[*]
end_of_line = lf
[caddytest/integration/caddyfile_adapt/*.txt]
indent_style = tab
+1 -1
View File
@@ -48,7 +48,7 @@ We consider publicly-registered domain names to be public information. This nece
It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, depending on available resources. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. (We get a lot of invalid reports.) Thank you for understanding. It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, depending on available resources. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. (We get a lot of invalid reports.) Thank you for understanding.
When you are ready, please email Matt Holt (the author) directly: matt [at] lightcodelabs [dot com]. When you are ready, please email Matt Holt (the author) directly: matt at dyanim dot com.
Please don't encrypt the email body. It only makes the process more complicated. Please don't encrypt the email body. It only makes the process more complicated.
+26 -9
View File
@@ -19,12 +19,20 @@ 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.16', '1.17' ] go: [ '1.17', '1.18' ]
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.17'
GO_SEMVER: '~1.17.9'
- go: '1.18'
GO_SEMVER: '~1.18.1'
# 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
# SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True') # SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
include:
- os: ubuntu-latest - os: ubuntu-latest
CADDY_BIN_PATH: ./cmd/caddy/caddy CADDY_BIN_PATH: ./cmd/caddy/caddy
SUCCESS: 0 SUCCESS: 0
@@ -41,12 +49,13 @@ jobs:
steps: steps:
- name: Install Go - name: Install Go
uses: actions/setup-go@v2 uses: actions/setup-go@v3
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.GO_SEMVER }}
check-latest: true
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
# These tools would be useful if we later decide to reinvestigate # These tools would be useful if we later decide to reinvestigate
# publishing test/coverage reports to some tool for easier consumption # publishing test/coverage reports to some tool for easier consumption
@@ -69,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
@@ -130,7 +147,7 @@ jobs:
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
steps: steps:
- name: Checkout code into the Go module directory - name: Checkout code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Run Tests - name: Run Tests
run: | run: |
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
@@ -155,7 +172,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- uses: goreleaser/goreleaser-action@v2 - uses: goreleaser/goreleaser-action@v2
with: with:
+18 -6
View File
@@ -16,14 +16,22 @@ 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.17' ] go: [ '1.18' ]
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.18'
GO_SEMVER: '~1.18.1'
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
steps: steps:
- name: Install Go - name: Install Go
uses: actions/setup-go@v2 uses: actions/setup-go@v3
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.GO_SEMVER }}
check-latest: true
- name: Print Go version and environment - name: Print Go version and environment
id: vars id: vars
@@ -34,18 +42,22 @@ 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 }}
- name: Checkout code into the Go module directory - name: Checkout code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Run Build - name: Run Build
env: env:
+9 -4
View File
@@ -16,10 +16,15 @@ jobs:
name: lint name: lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: golangci-lint - uses: actions/setup-go@v3
uses: golangci/golangci-lint-action@v2
with: with:
version: v1.31 go-version: '~1.17.9'
check-latest: true
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.44
# 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
+20 -8
View File
@@ -11,22 +11,30 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ ubuntu-latest ] os: [ ubuntu-latest ]
go: [ '1.17' ] go: [ '1.18' ]
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.18'
GO_SEMVER: '~1.18.1'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Install Go - name: Install Go
uses: actions/setup-go@v2 uses: actions/setup-go@v3
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.GO_SEMVER }}
check-latest: true
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
# Force fetch upstream tags -- because 65 minutes # Force fetch upstream tags -- because 65 minutes
# tl;dr: actions/checkout@v2 runs this line: # tl;dr: actions/checkout@v3 runs this line:
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/ # git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran: # which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
# git fetch --prune --unshallow # git fetch --prune --unshallow
@@ -48,7 +56,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
@@ -83,7 +90,12 @@ 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
@@ -99,7 +111,7 @@ jobs:
TAG: ${{ steps.vars.outputs.version_tag }} TAG: ${{ steps.vars.outputs.version_tag }}
# 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 forseeable future, although # We will continue to push to Gemfury for the foreseeable future, although
# Cloudsmith is probably better, to not break things for existing users of Gemfury. # Cloudsmith is probably better, to not break things for existing users of Gemfury.
# See https://gemfury.com/caddy/deb:caddy # See https://gemfury.com/caddy/deb:caddy
- name: Publish .deb to Gemfury - name: Publish .deb to Gemfury
+1
View File
@@ -1,6 +1,7 @@
_gitignore/ _gitignore/
*.log *.log
Caddyfile Caddyfile
Caddyfile.*
!caddyfile/ !caddyfile/
# artifacts from pprof tooling # artifacts from pprof tooling
+1 -1
View File
@@ -1,6 +1,6 @@
linters-settings: linters-settings:
errcheck: errcheck:
ignore: fmt:.*,io/ioutil:^Read.*,go.uber.org/zap/zapcore:^Add.* ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.*
ignoretests: true ignoretests: true
linters: linters:
+6 -7
View File
@@ -6,8 +6,7 @@ before:
# subsequently causes gorleaser to refuse running. # subsequently causes gorleaser to refuse running.
- 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
- cp ./go.mod caddy-build/go.mod - /bin/sh -c 'cd ./caddy-build && go mod init caddy'
- sed -i.bkp 's|github.com/caddyserver/caddy/v2|caddy|g' ./caddy-build/go.mod
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env # GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate # so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
- go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod - go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod
@@ -36,9 +35,9 @@ builds:
- s390x - s390x
- ppc64le - ppc64le
goarm: goarm:
- 5 - "5"
- 6 - "6"
- 7 - "7"
ignore: ignore:
- goos: darwin - goos: darwin
goarch: arm goarch: arm
@@ -56,7 +55,7 @@ builds:
goarch: s390x goarch: s390x
- goos: freebsd - goos: freebsd
goarch: arm goarch: arm
goarm: 5 goarm: "5"
flags: flags:
- -trimpath - -trimpath
ldflags: ldflags:
@@ -75,7 +74,7 @@ nfpms:
- id: default - id: default
package_name: caddy package_name: caddy
vendor: Light Code Labs vendor: Dyanim
homepage: https://caddyserver.com homepage: https://caddyserver.com
maintainer: Matthew Holt <mholt@users.noreply.github.com> maintainer: Matthew Holt <mholt@users.noreply.github.com>
description: | description: |
+3 -1
View File
@@ -75,7 +75,7 @@ For other install options, see https://caddyserver.com/docs/install.
Requirements: Requirements:
- [Go 1.16 or newer](https://golang.org/dl/) - [Go 1.17 or newer](https://golang.org/dl/)
### For development ### For development
@@ -176,6 +176,8 @@ Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only
## About ## About
Matthew Holt began developing Caddy in 2014 while studying computer science at Brigham Young University. (The name "Caddy" was chosen because this software helps with the tedious, mundane tasks of serving the Web, and is also a single place for multiple things to be organized together.) It soon became the first web server to use HTTPS automatically and by default, and now has hundreds of contributors and has served trillions of HTTPS requests.
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH. **The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH.
- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_ - _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_
+154 -57
View File
@@ -25,8 +25,9 @@ import (
"errors" "errors"
"expvar" "expvar"
"fmt" "fmt"
"hash"
"hash/fnv"
"io" "io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"net/http/pprof" "net/http/pprof"
@@ -43,6 +44,7 @@ import (
"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"
"go.uber.org/zap/zapcore"
) )
// AdminConfig configures Caddy's API endpoint, which is used // AdminConfig configures Caddy's API endpoint, which is used
@@ -92,6 +94,10 @@ type AdminConfig struct {
// //
// EXPERIMENTAL: This feature is subject to change. // EXPERIMENTAL: This feature is subject to change.
Remote *RemoteAdmin `json:"remote,omitempty"` Remote *RemoteAdmin `json:"remote,omitempty"`
// Holds onto the routers so that we can later provision them
// if they require provisioning.
routers []AdminRouter
} }
// ConfigSettings configures the management of configuration. // ConfigSettings configures the management of configuration.
@@ -101,20 +107,26 @@ type ConfigSettings struct {
// are not persisted; only configs that are pushed to Caddy get persisted. // are not persisted; only configs that are pushed to Caddy get persisted.
Persist *bool `json:"persist,omitempty"` Persist *bool `json:"persist,omitempty"`
// Loads a configuration to use. This is helpful if your configs are // Loads a new configuration. This is helpful if your configs are
// managed elsewhere, and you want Caddy to pull its config dynamically // managed elsewhere and you want Caddy to pull its config dynamically
// when it starts. The pulled config completely replaces the current // when it starts. The pulled config completely replaces the current
// one, just like any other config load. It is an error if a pulled // one, just like any other config load. It is an error if a pulled
// config is configured to pull another config. // config is configured to pull another config without a load_delay,
// as this creates a tight loop.
// //
// EXPERIMENTAL: Subject to change. // EXPERIMENTAL: Subject to change.
LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"` LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"`
// The interval to pull config. With a non-zero value, will pull config // The duration after which to load config. If set, config will be pulled
// from config loader (eg. a http loader) with given interval. // from the config loader after this duration. A delay is required if a
// dynamically-loaded config is configured to load yet another config. To
// load configs on a regular interval, ensure this value is set the same
// on all loaded configs; it can also be variable if needed, and to stop
// the loop, simply remove dynamic config loading from the next-loaded
// config.
// //
// EXPERIMENTAL: Subject to change. // EXPERIMENTAL: Subject to change.
LoadInterval Duration `json:"load_interval,omitempty"` LoadDelay Duration `json:"load_delay,omitempty"`
} }
// IdentityConfig configures management of this server's identity. An identity // IdentityConfig configures management of this server's identity. An identity
@@ -184,7 +196,7 @@ type AdminPermissions struct {
// newAdminHandler reads admin's config and returns an http.Handler suitable // newAdminHandler reads admin's config and returns an http.Handler suitable
// for use in an admin endpoint server, which will be listening on listenAddr. // for use in an admin endpoint server, which will be listening on listenAddr.
func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler { func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler {
muxWrap := adminHandler{mux: http.NewServeMux()} muxWrap := adminHandler{mux: http.NewServeMux()}
// secure the local or remote endpoint respectively // secure the local or remote endpoint respectively
@@ -193,6 +205,7 @@ func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) admin
} else { } else {
muxWrap.enforceHost = !addr.isWildcardInterface() muxWrap.enforceHost = !addr.isWildcardInterface()
muxWrap.allowedOrigins = admin.allowedOrigins(addr) muxWrap.allowedOrigins = admin.allowedOrigins(addr)
muxWrap.enforceOrigin = admin.EnforceOrigin
} }
addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) { addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
@@ -243,17 +256,39 @@ func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) admin
for _, route := range router.Routes() { for _, route := range router.Routes() {
addRoute(route.Pattern, handlerLabel, route.Handler) addRoute(route.Pattern, handlerLabel, route.Handler)
} }
admin.routers = append(admin.routers, router)
} }
return muxWrap return muxWrap
} }
// provisionAdminRouters provisions all the router modules
// in the admin.api namespace that need provisioning.
func (admin *AdminConfig) provisionAdminRouters(ctx Context) error {
for _, router := range admin.routers {
provisioner, ok := router.(Provisioner)
if !ok {
continue
}
err := provisioner.Provision(ctx)
if err != nil {
return err
}
}
// We no longer need the routers once provisioned, allow for GC
admin.routers = nil
return nil
}
// allowedOrigins returns a list of origins that are allowed. // allowedOrigins returns a list of origins that are allowed.
// If admin.Origins is nil (null), the provided listen address // If admin.Origins is nil (null), the provided listen address
// will be used as the default origin. If admin.Origins is // will be used as the default origin. If admin.Origins is
// empty, no origins will be allowed, effectively bricking the // empty, no origins will be allowed, effectively bricking the
// endpoint for non-unix-socket endpoints, but whatever. // endpoint for non-unix-socket endpoints, but whatever.
func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string { func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
uniqueOrigins := make(map[string]struct{}) uniqueOrigins := make(map[string]struct{})
for _, o := range admin.Origins { for _, o := range admin.Origins {
uniqueOrigins[o] = struct{}{} uniqueOrigins[o] = struct{}{}
@@ -277,8 +312,23 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
uniqueOrigins[addr.JoinHostPort(0)] = struct{}{} uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
} }
} }
allowed := make([]string, 0, len(uniqueOrigins)) allowed := make([]*url.URL, 0, len(uniqueOrigins))
for origin := range uniqueOrigins { for originStr := range uniqueOrigins {
var origin *url.URL
if strings.Contains(originStr, "://") {
var err error
origin, err = url.Parse(originStr)
if err != nil {
continue
}
origin.Path = ""
origin.RawPath = ""
origin.Fragment = ""
origin.RawFragment = ""
origin.RawQuery = ""
} else {
origin = &url.URL{Host: originStr}
}
allowed = append(allowed, origin) allowed = append(allowed, origin)
} }
return allowed return allowed
@@ -310,25 +360,26 @@ func replaceLocalAdminServer(cfg *Config) error {
} }
}() }()
// always get a valid admin config // set a default if admin wasn't otherwise configured
adminConfig := DefaultAdminConfig if cfg.Admin == nil {
if cfg != nil && cfg.Admin != nil { cfg.Admin = &AdminConfig{
adminConfig = cfg.Admin Listen: DefaultAdminListen,
}
} }
// if new admin endpoint is to be disabled, we're done // if new admin endpoint is to be disabled, we're done
if adminConfig.Disabled { if cfg.Admin.Disabled {
Log().Named("admin").Warn("admin endpoint disabled") Log().Named("admin").Warn("admin endpoint disabled")
return nil return nil
} }
// extract a singular listener address // extract a singular listener address
addr, err := parseAdminListenAddr(adminConfig.Listen, DefaultAdminListen) addr, err := parseAdminListenAddr(cfg.Admin.Listen, DefaultAdminListen)
if err != nil { if err != nil {
return err return err
} }
handler := adminConfig.newAdminHandler(addr, false) handler := cfg.Admin.newAdminHandler(addr, false)
ln, err := Listen(addr.Network, addr.JoinHostPort(0)) ln, err := Listen(addr.Network, addr.JoinHostPort(0))
if err != nil { if err != nil {
@@ -358,8 +409,8 @@ func replaceLocalAdminServer(cfg *Config) error {
adminLogger.Info("admin endpoint started", adminLogger.Info("admin endpoint started",
zap.String("address", addr.String()), zap.String("address", addr.String()),
zap.Bool("enforce_origin", adminConfig.EnforceOrigin), zap.Bool("enforce_origin", cfg.Admin.EnforceOrigin),
zap.Strings("origins", handler.allowedOrigins)) zap.Array("origins", loggableURLArray(handler.allowedOrigins)))
if !handler.enforceHost { if !handler.enforceHost {
adminLogger.Warn("admin endpoint on open interface; host checking disabled", adminLogger.Warn("admin endpoint on open interface; host checking disabled",
@@ -467,6 +518,9 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
} }
// create TLS config that will enforce mutual authentication // create TLS config that will enforce mutual authentication
if identityCertCache == nil {
return fmt.Errorf("cannot enable remote admin without a certificate cache; configure identity management to initialize a certificate cache")
}
cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger, false) cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger, false)
tlsConfig := cmCfg.TLSConfig() tlsConfig := cmCfg.TLSConfig()
tlsConfig.NextProtos = nil // this server does not solve ACME challenges tlsConfig.NextProtos = nil // this server does not solve ACME challenges
@@ -648,10 +702,10 @@ type AdminRoute struct {
type adminHandler struct { type adminHandler struct {
mux *http.ServeMux mux *http.ServeMux
// security for local/plaintext) endpoint, on by default // security for local/plaintext endpoint
enforceOrigin bool enforceOrigin bool
enforceHost bool enforceHost bool
allowedOrigins []string allowedOrigins []*url.URL
// security for remote/encrypted endpoint // security for remote/encrypted endpoint
remoteControl *RemoteAdmin remoteControl *RemoteAdmin
@@ -660,11 +714,17 @@ type adminHandler struct {
// ServeHTTP is the external entry point for API requests. // ServeHTTP is the external entry point for API requests.
// It will only be called once per request. // It will only be called once per request.
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ip, port, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
ip = r.RemoteAddr
port = ""
}
log := Log().Named("admin.api").With( log := Log().Named("admin.api").With(
zap.String("method", r.Method), zap.String("method", r.Method),
zap.String("host", r.Host), zap.String("host", r.Host),
zap.String("uri", r.RequestURI), zap.String("uri", r.RequestURI),
zap.String("remote_addr", r.RemoteAddr), zap.String("remote_ip", ip),
zap.String("remote_port", port),
zap.Reflect("headers", r.Header), zap.Reflect("headers", r.Header),
) )
if r.TLS != nil { if r.TLS != nil {
@@ -771,8 +831,8 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
// rebinding attacks. // rebinding attacks.
func (h adminHandler) checkHost(r *http.Request) error { func (h adminHandler) checkHost(r *http.Request) error {
var allowed bool var allowed bool
for _, allowedHost := range h.allowedOrigins { for _, allowedOrigin := range h.allowedOrigins {
if r.Host == allowedHost { if r.Host == allowedOrigin.Host {
allowed = true allowed = true
break break
} }
@@ -791,59 +851,81 @@ func (h adminHandler) checkHost(r *http.Request) error {
// sites from issuing requests to our listener. It // sites from issuing requests to our listener. It
// returns the origin that was obtained from r. // returns the origin that was obtained from r.
func (h adminHandler) checkOrigin(r *http.Request) (string, error) { func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
origin := h.getOriginHost(r) originStr, origin := h.getOrigin(r)
if origin == "" { if origin == nil {
return origin, APIError{ return "", APIError{
HTTPStatus: http.StatusForbidden, HTTPStatus: http.StatusForbidden,
Err: fmt.Errorf("missing required Origin header"), Err: fmt.Errorf("required Origin header is missing or invalid"),
} }
} }
if !h.originAllowed(origin) { if !h.originAllowed(origin) {
return origin, APIError{ return "", APIError{
HTTPStatus: http.StatusForbidden, HTTPStatus: http.StatusForbidden,
Err: fmt.Errorf("client is not allowed to access from origin %s", origin), Err: fmt.Errorf("client is not allowed to access from origin '%s'", originStr),
} }
} }
return origin, nil return origin.String(), nil
} }
func (h adminHandler) getOriginHost(r *http.Request) string { func (h adminHandler) getOrigin(r *http.Request) (string, *url.URL) {
origin := r.Header.Get("Origin") origin := r.Header.Get("Origin")
if origin == "" { if origin == "" {
origin = r.Header.Get("Referer") origin = r.Header.Get("Referer")
} }
originURL, err := url.Parse(origin) originURL, err := url.Parse(origin)
if err == nil && originURL.Host != "" { if err != nil {
origin = originURL.Host return origin, nil
} }
return origin originURL.Path = ""
originURL.RawPath = ""
originURL.Fragment = ""
originURL.RawFragment = ""
originURL.RawQuery = ""
return origin, originURL
} }
func (h adminHandler) originAllowed(origin string) bool { func (h adminHandler) originAllowed(origin *url.URL) bool {
for _, allowedOrigin := range h.allowedOrigins { for _, allowedOrigin := range h.allowedOrigins {
originCopy := origin if allowedOrigin.Scheme != "" && origin.Scheme != allowedOrigin.Scheme {
if !strings.Contains(allowedOrigin, "://") { continue
// no scheme specified, so allow both
originCopy = strings.TrimPrefix(originCopy, "http://")
originCopy = strings.TrimPrefix(originCopy, "https://")
} }
if originCopy == allowedOrigin { if origin.Host == allowedOrigin.Host {
return true return true
} }
} }
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,
@@ -877,8 +959,8 @@ 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 { if err != nil && !errors.Is(err, errSameConfig) {
return err return err
} }
@@ -897,10 +979,16 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
parts := strings.Split(idPath, "/") parts := strings.Split(idPath, "/")
if len(parts) < 3 || parts[2] == "" { if len(parts) < 3 || parts[2] == "" {
return fmt.Errorf("request path is missing object ID") return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("request path is missing object ID"),
}
} }
if parts[0] != "" || parts[1] != "id" { if parts[0] != "" || parts[1] != "id" {
return fmt.Errorf("malformed object path") return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed object path"),
}
} }
id := parts[2] id := parts[2]
@@ -909,7 +997,10 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
expanded, ok := rawCfgIndex[id] expanded, ok := rawCfgIndex[id]
defer currentCfgMu.RUnlock() defer currentCfgMu.RUnlock()
if !ok { if !ok {
return fmt.Errorf("unknown object ID '%s'", id) return APIError{
HTTPStatus: http.StatusNotFound,
Err: fmt.Errorf("unknown object ID '%s'", id),
}
} }
// piece the full URL path back together // piece the full URL path back together
@@ -931,7 +1022,7 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
Log().Error("unable to notify stopping to service manager", zap.Error(err)) Log().Error("unable to notify stopping to service manager", zap.Error(err))
} }
exitProcess(Log().Named("admin.api")) exitProcess(context.Background(), Log().Named("admin.api"))
return nil return nil
} }
@@ -1181,6 +1272,18 @@ func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
return x509.ParseCertificate(derBytes) return x509.ParseCertificate(derBytes)
} }
type loggableURLArray []*url.URL
func (ua loggableURLArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
if ua == nil {
return nil
}
for _, u := range ua {
enc.AppendString(u.String())
}
return nil
}
var ( var (
// DefaultAdminListen is the address for the local admin // DefaultAdminListen is the address for the local admin
// listener, if none is specified at startup. // listener, if none is specified at startup.
@@ -1190,19 +1293,13 @@ var (
// (TLS-authenticated) admin listener, if enabled and not // (TLS-authenticated) admin listener, if enabled and not
// specified otherwise. // specified otherwise.
DefaultRemoteAdminListen = ":2021" DefaultRemoteAdminListen = ":2021"
// DefaultAdminConfig is the default configuration
// for the local administration endpoint.
DefaultAdminConfig = &AdminConfig{
Listen: DefaultAdminListen,
}
) )
// PIDFile writes a pidfile to the file at filename. It // PIDFile writes a pidfile to the file at filename. It
// will get deleted before the process gracefully exits. // will get deleted before the process gracefully exits.
func PIDFile(filename string) error { func PIDFile(filename string) error {
pid := []byte(strconv.Itoa(os.Getpid()) + "\n") pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
err := ioutil.WriteFile(filename, pid, 0600) err := os.WriteFile(filename, pid, 0600)
if err != nil { if err != nil {
return err return err
} }
+50 -1
View File
@@ -16,6 +16,8 @@ package caddy
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http"
"reflect" "reflect"
"sync" "sync"
"testing" "testing"
@@ -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(`{"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)
+117 -35
View File
@@ -17,10 +17,11 @@ package caddy
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
@@ -111,16 +112,26 @@ func Load(cfgJSON []byte, forceReload bool) error {
} }
}() }()
return changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload) err := changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, "", forceReload)
if errors.Is(err, errSameConfig) {
err = nil // not really an error
}
return err
} }
// changeConfig changes the current config (rawCfg) according to the // changeConfig changes the current config (rawCfg) according to the
// method, traversed via the given path, and uses the given input as // method, traversed via the given path, and uses the given input as
// the new value (if applicable; i.e. "DELETE" doesn't have an input). // the new value (if applicable; i.e. "DELETE" doesn't have an input).
// If the resulting config is the same as the previous, no reload will // If the resulting config is the same as the previous, no reload will
// occur unless forceReload is true. This function is safe for // occur unless forceReload is true. If the config is unchanged and not
// 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,
@@ -133,6 +144,40 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
currentCfgMu.Lock() currentCfgMu.Lock()
defer currentCfgMu.Unlock() defer currentCfgMu.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 {
return err return err
@@ -149,8 +194,8 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
// if nothing changed, no need to do a whole reload unless the client forces it // if nothing changed, no need to do a whole reload unless the client forces it
if !forceReload && bytes.Equal(rawCfgJSON, newCfg) { if !forceReload && bytes.Equal(rawCfgJSON, newCfg) {
Log().Named("admin.api").Info("config is unchanged") Log().Info("config is unchanged")
return nil return errSameConfig
} }
// find any IDs in this config and index them // find any IDs in this config and index them
@@ -269,8 +314,8 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
newCfg.Admin != nil && newCfg.Admin != nil &&
newCfg.Admin.Config != nil && newCfg.Admin.Config != nil &&
newCfg.Admin.Config.LoadRaw != nil && newCfg.Admin.Config.LoadRaw != nil &&
newCfg.Admin.Config.LoadInterval <= 0 { newCfg.Admin.Config.LoadDelay <= 0 {
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_interval") return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_delay")
} }
// run the new config and start all its apps // run the new config and start all its apps
@@ -300,7 +345,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
zap.String("dir", dir), zap.String("dir", dir),
zap.Error(err)) zap.Error(err))
} else { } else {
err := ioutil.WriteFile(ConfigAutosavePath, cfgJSON, 0600) err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
if err == nil { if err == nil {
Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath)) Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath))
} else { } else {
@@ -428,9 +473,16 @@ func run(newCfg *Config, start bool) error {
return nil return nil
} }
// Provision any admin routers which may need to access
// some of the other apps at runtime
err = newCfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return 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 {
@@ -481,49 +533,74 @@ func finishSettingUp(ctx Context, cfg *Config) error {
if err != nil { if err != nil {
return fmt.Errorf("loading config loader module: %s", err) return fmt.Errorf("loading config loader module: %s", err)
} }
runLoadedConfig := func(config []byte) {
Log().Info("applying dynamically-loaded config", zap.String("loader_module", val.(Module).CaddyModule().ID.Name()), zap.Int("pull_interval", int(cfg.Admin.Config.LoadInterval))) logger := Log().Named("config_loader").With(
currentCfgMu.Lock() zap.String("module", val.(Module).CaddyModule().ID.Name()),
err := unsyncedDecodeAndRun(config, false) zap.Int("load_delay", int(cfg.Admin.Config.LoadDelay)))
currentCfgMu.Unlock()
if err == nil { runLoadedConfig := func(config []byte) error {
Log().Info("dynamically-loaded config applied successfully") logger.Info("applying dynamically-loaded config")
} else { err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, "", false)
Log().Error("running dynamically-loaded config failed", zap.Error(err)) if errors.Is(err, errSameConfig) {
return err
} }
if err != nil {
logger.Error("failed to run dynamically-loaded config", zap.Error(err))
return err
} }
if cfg.Admin.Config.LoadInterval > 0 { logger.Info("successfully applied dynamically-loaded config")
return nil
}
if cfg.Admin.Config.LoadDelay > 0 {
go func() { go func() {
// the loop is here to iterate ONLY if there is an error, a no-op config load,
// or an unchanged config; in which case we simply wait the delay and try again
for {
timer := time.NewTimer(time.Duration(cfg.Admin.Config.LoadDelay))
select { select {
// if LoadInterval is positive, will wait for the interval and then run with new config case <-timer.C:
case <-time.After(time.Duration(cfg.Admin.Config.LoadInterval)):
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx) loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
if err != nil { if err != nil {
Log().Error("loading dynamic config failed", zap.Error(err)) logger.Error("failed loading dynamic config; will retry", zap.Error(err))
return continue
}
if loadedConfig == nil {
logger.Info("dynamically-loaded config was nil; will retry")
continue
}
err = runLoadedConfig(loadedConfig)
if errors.Is(err, errSameConfig) {
logger.Info("dynamically-loaded config was unchanged; will retry")
continue
} }
runLoadedConfig(loadedConfig)
case <-ctx.Done(): case <-ctx.Done():
return if !timer.Stop() {
<-timer.C
}
logger.Info("stopping dynamic config loading")
}
break
} }
}() }()
} else { } else {
// if no LoadInterval is provided, will load config synchronously // if no LoadDelay is provided, will load config synchronously
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx) loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
if err != nil { if err != nil {
return fmt.Errorf("loading dynamic config from %T: %v", val, err) return fmt.Errorf("loading dynamic config from %T: %v", val, err)
} }
// do this in a goroutine so current config can finish being loaded; otherwise deadlock // do this in a goroutine so current config can finish being loaded; otherwise deadlock
go runLoadedConfig(loadedConfig) go func() { _ = runLoadedConfig(loadedConfig) }()
} }
} }
return nil return nil
} }
// ConfigLoader is a type that can load a Caddy config. The // ConfigLoader is a type that can load a Caddy config. If
// returned config must be valid Caddy JSON. // the return value is non-nil, it must be valid Caddy JSON;
// if nil or with non-nil error, it is considered to be a
// no-op load and may be retried later.
type ConfigLoader interface { type ConfigLoader interface {
LoadConfig(Context) ([]byte, error) LoadConfig(Context) ([]byte, error)
} }
@@ -584,7 +661,7 @@ func Validate(cfg *Config) error {
// PID file, and shuts down admin endpoint(s) in a goroutine. // PID file, and shuts down admin endpoint(s) in a goroutine.
// 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(logger *zap.Logger) { func exitProcess(ctx context.Context, logger *zap.Logger) {
if logger == nil { if logger == nil {
logger = Log() logger = Log()
} }
@@ -599,7 +676,7 @@ func exitProcess(logger *zap.Logger) {
} }
// clean up certmagic locks // clean up certmagic locks
certmagic.CleanUpOwnLocks(logger) certmagic.CleanUpOwnLocks(ctx, logger)
// remove pidfile // remove pidfile
if pidfile != "" { if pidfile != "" {
@@ -700,13 +777,13 @@ func ParseDuration(s string) (time.Duration, error) {
// have its own unique ID. // have its own unique ID.
func InstanceID() (uuid.UUID, error) { func InstanceID() (uuid.UUID, error) {
uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid") uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid")
uuidFileBytes, err := ioutil.ReadFile(uuidFilePath) uuidFileBytes, err := os.ReadFile(uuidFilePath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
uuid, err := uuid.NewRandom() uuid, err := uuid.NewRandom()
if err != nil { if err != nil {
return uuid, err return uuid, err
} }
err = ioutil.WriteFile(uuidFilePath, []byte(uuid.String()), 0600) err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0600)
return uuid, err return uuid, err
} else if err != nil { } else if err != nil {
return [16]byte{}, err return [16]byte{}, err
@@ -775,5 +852,10 @@ var (
rawCfgIndex map[string]string rawCfgIndex map[string]string
) )
// errSameConfig is returned if the new config is the same
// as the old one. This isn't usually an actual, actionable
// error; it's mostly a sentinel value.
var errSameConfig = errors.New("config is unchanged")
// ImportPath is the package import path for Caddy core. // ImportPath is the package import path for Caddy core.
const ImportPath = "github.com/caddyserver/caddy/v2" const ImportPath = "github.com/caddyserver/caddy/v2"
+1 -1
View File
@@ -88,7 +88,7 @@ func formattingDifference(filename string, body []byte) (caddyconfig.Warning, bo
return caddyconfig.Warning{ return caddyconfig.Warning{
File: filename, File: filename,
Line: line, Line: line,
Message: "input is not formatted with 'caddy fmt'", Message: "Caddyfile input is not formatted; run the 'caddy fmt' command to fix inconsistencies",
}, true }, true
} }
+124 -3
View File
@@ -19,6 +19,7 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"strconv"
"strings" "strings"
) )
@@ -201,6 +202,43 @@ func (d *Dispenser) Val() string {
return d.tokens[d.cursor].Text return d.tokens[d.cursor].Text
} }
// ValRaw gets the raw text of the current token (including quotes).
// If there is no token loaded, it returns empty string.
func (d *Dispenser) ValRaw() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return ""
}
quote := d.tokens[d.cursor].wasQuoted
if quote > 0 {
return string(quote) + d.tokens[d.cursor].Text + string(quote) // string literal
}
return d.tokens[d.cursor].Text
}
// ScalarVal gets value of the current token, converted to the closest
// scalar type. If there is no token loaded, it returns nil.
func (d *Dispenser) ScalarVal() interface{} {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return nil
}
quote := d.tokens[d.cursor].wasQuoted
text := d.tokens[d.cursor].Text
if quote > 0 {
return text // string literal
}
if num, err := strconv.Atoi(text); err == nil {
return num
}
if num, err := strconv.ParseFloat(text, 64); err == nil {
return num
}
if bool, err := strconv.ParseBool(text); err == nil {
return bool
}
return text
}
// Line gets the line number of the current token. // Line gets the line number of the current token.
// If there is no token loaded, it returns 0. // If there is no token loaded, it returns 0.
func (d *Dispenser) Line() int { func (d *Dispenser) Line() int {
@@ -249,6 +287,19 @@ func (d *Dispenser) AllArgs(targets ...*string) bool {
return true return true
} }
// CountRemainingArgs counts the amount of remaining arguments
// (tokens on the same line) without consuming the tokens.
func (d *Dispenser) CountRemainingArgs() int {
count := 0
for d.NextArg() {
count++
}
for i := 0; i < count; i++ {
d.Prev()
}
return count
}
// RemainingArgs loads any more arguments (tokens on the same line) // RemainingArgs loads any more arguments (tokens on the same line)
// into a slice and returns them. Open curly brace tokens also indicate // into a slice and returns them. Open curly brace tokens also indicate
// the end of arguments, and the curly brace is not included in // the end of arguments, and the curly brace is not included in
@@ -261,6 +312,18 @@ func (d *Dispenser) RemainingArgs() []string {
return args return args
} }
// RemainingArgsRaw loads any more arguments (tokens on the same line,
// retaining quotes) into a slice and returns them. Open curly brace
// tokens also indicate the end of arguments, and the curly brace is
// not included in the return value nor is it loaded.
func (d *Dispenser) RemainingArgsRaw() []string {
var args []string
for d.NextArg() {
args = append(args, d.ValRaw())
}
return args
}
// NewFromNextSegment returns a new dispenser with a copy of // NewFromNextSegment returns a new dispenser with a copy of
// the tokens from the current token until the end of the // the tokens from the current token until the end of the
// "directive" whether that be to the end of the line or // "directive" whether that be to the end of the line or
@@ -350,7 +413,11 @@ 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 ...interface{}) error {
err := fmt.Errorf(format, args...) return d.WrapErr(fmt.Errorf(format, args...))
}
// WrapErr takes an existing error and adds the Caddyfile file and line number.
func (d *Dispenser) WrapErr(err error) error {
return fmt.Errorf("%s:%d - Error during parsing: %w", d.File(), d.Line(), err) return fmt.Errorf("%s:%d - Error during parsing: %w", d.File(), d.Line(), err)
} }
@@ -391,6 +458,60 @@ func (d *Dispenser) isNewLine() bool {
if d.cursor > len(d.tokens)-1 { if d.cursor > len(d.tokens)-1 {
return false return false
} }
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line prev := d.tokens[d.cursor-1]
curr := d.tokens[d.cursor]
// If the previous token is from a different file,
// we can assume it's from a different line
if prev.File != curr.File {
return true
}
// The previous token may contain line breaks if
// it was quoted and spanned multiple lines. e.g:
//
// dir "foo
// bar
// baz"
prevLineBreaks := d.numLineBreaks(d.cursor - 1)
// If the previous token (incl line breaks) ends
// on a line earlier than the current token,
// then the current token is on a new line
return prev.Line+prevLineBreaks < curr.Line
}
// isNextOnNewLine determines whether the current token is on a different
// line (higher line number) than the next token. It handles imported
// tokens correctly. If there isn't a next token, it returns true.
func (d *Dispenser) isNextOnNewLine() bool {
if d.cursor < 0 {
return false
}
if d.cursor >= len(d.tokens)-1 {
return true
}
curr := d.tokens[d.cursor]
next := d.tokens[d.cursor+1]
// If the next token is from a different file,
// we can assume it's from a different line
if curr.File != next.File {
return true
}
// The current token may contain line breaks if
// it was quoted and spanned multiple lines. e.g:
//
// dir "foo
// bar
// baz"
currLineBreaks := d.numLineBreaks(d.cursor)
// If the current token (incl line breaks) ends
// on a line earlier than the next token,
// then the next token is on a new line
return curr.Line+currLineBreaks < next.Line
} }
View File
+1
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build gofuzz
// +build gofuzz // +build gofuzz
package caddyfile package caddyfile
+5
View File
@@ -179,6 +179,11 @@ d {
{$F} {$F}
}`, }`,
}, },
{
description: "env var placeholders with port",
input: `:{$PORT}`,
expect: `:{$PORT}`,
},
{ {
description: "comments", description: "comments",
input: `#a "\n" input: `#a "\n"
Executable → Regular
+8 -6
View File
@@ -1,4 +1,4 @@
// Copyright 2015 Light Code Labs, LLC // Copyright 2015 Matthew Holt and The Caddy Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -38,6 +38,7 @@ type (
File string File string
Line int Line int
Text string Text string
wasQuoted rune // enclosing quote character, if any
inSnippet bool inSnippet bool
snippetName string snippetName string
} }
@@ -78,8 +79,9 @@ func (l *lexer) next() bool {
var val []rune var val []rune
var comment, quoted, btQuoted, escaped bool var comment, quoted, btQuoted, escaped bool
makeToken := func() bool { makeToken := func(quoted rune) bool {
l.token.Text = string(val) l.token.Text = string(val)
l.token.wasQuoted = quoted
return true return true
} }
@@ -87,7 +89,7 @@ func (l *lexer) next() bool {
ch, _, err := l.reader.ReadRune() ch, _, err := l.reader.ReadRune()
if err != nil { if err != nil {
if len(val) > 0 { if len(val) > 0 {
return makeToken() return makeToken(0)
} }
if err == io.EOF { if err == io.EOF {
return false return false
@@ -110,10 +112,10 @@ func (l *lexer) next() bool {
escaped = false escaped = false
} else { } else {
if quoted && ch == '"' { if quoted && ch == '"' {
return makeToken() return makeToken('"')
} }
if btQuoted && ch == '`' { if btQuoted && ch == '`' {
return makeToken() return makeToken('`')
} }
} }
if ch == '\n' { if ch == '\n' {
@@ -139,7 +141,7 @@ func (l *lexer) next() bool {
comment = false comment = false
} }
if len(val) > 0 { if len(val) > 0 {
return makeToken() return makeToken(0)
} }
continue continue
} }
+1
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build gofuzz
// +build gofuzz // +build gofuzz
package caddyfile package caddyfile
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2015 Light Code Labs, LLC // Copyright 2015 Matthew Holt and The Caddy Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
Executable → Regular
+35 -21
View File
@@ -1,4 +1,4 @@
// Copyright 2015 Light Code Labs, LLC // Copyright 2015 Matthew Holt and The Caddy Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -17,14 +17,14 @@ package caddyfile
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil" "io"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"go.uber.org/zap"
) )
// Parse parses the input just enough to group tokens, in // Parse parses the input just enough to group tokens, in
@@ -37,7 +37,13 @@ import (
// Environment variables in {$ENVIRONMENT_VARIABLE} notation // Environment variables in {$ENVIRONMENT_VARIABLE} notation
// will be replaced before parsing begins. // will be replaced before parsing begins.
func Parse(filename string, input []byte) ([]ServerBlock, error) { func Parse(filename string, input []byte) ([]ServerBlock, error) {
tokens, err := allTokens(filename, input) // unfortunately, we must copy the input because parsing must
// remain a read-only operation, but we have to expand environment
// variables before we parse, which changes the underlying array (#4422)
inputCopy := make([]byte, len(input))
copy(inputCopy, input)
tokens, err := allTokens(filename, inputCopy)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -51,7 +57,23 @@ func Parse(filename string, input []byte) ([]ServerBlock, error) {
return p.parseAll() return p.parseAll()
} }
// allTokens lexes the entire input, but does not parse it.
// It returns all the tokens from the input, unstructured
// and in order. It may mutate input as it expands env vars.
func allTokens(filename string, input []byte) ([]Token, error) {
inputCopy, err := replaceEnvVars(input)
if err != nil {
return nil, err
}
tokens, err := Tokenize(inputCopy, filename)
if err != nil {
return nil, err
}
return tokens, nil
}
// replaceEnvVars replaces all occurrences of environment variables. // replaceEnvVars replaces all occurrences of environment variables.
// It mutates the underlying array and returns the updated slice.
func replaceEnvVars(input []byte) ([]byte, error) { func replaceEnvVars(input []byte) ([]byte, error) {
var offset int var offset int
for { for {
@@ -96,21 +118,6 @@ func replaceEnvVars(input []byte) ([]byte, error) {
return input, nil return input, nil
} }
// allTokens lexes the entire input, but does not parse it.
// It returns all the tokens from the input, unstructured
// and in order.
func allTokens(filename string, input []byte) ([]Token, error) {
input, err := replaceEnvVars(input)
if err != nil {
return nil, err
}
tokens, err := Tokenize(input, filename)
if err != nil {
return nil, err
}
return tokens, nil
}
type parser struct { type parser struct {
*Dispenser *Dispenser
block ServerBlock // current server block being parsed block ServerBlock // current server block being parsed
@@ -386,7 +393,7 @@ func (p *parser) doImport() error {
} }
if len(matches) == 0 { if len(matches) == 0 {
if strings.ContainsAny(globPattern, "*?[]") { if strings.ContainsAny(globPattern, "*?[]") {
log.Printf("[WARNING] No files matching import glob pattern: %s", importPattern) caddy.Log().Warn("No files matching import glob pattern", zap.String("pattern", importPattern))
} else { } else {
return p.Errf("File to import not found: %s", importPattern) return p.Errf("File to import not found: %s", importPattern)
} }
@@ -447,7 +454,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
return nil, p.Errf("Could not import %s: is a directory", importFile) return nil, p.Errf("Could not import %s: is a directory", importFile)
} }
input, err := ioutil.ReadAll(file) input, err := io.ReadAll(file)
if err != nil { if err != nil {
return nil, p.Errf("Could not read imported file %s: %v", importFile, err) return nil, p.Errf("Could not read imported file %s: %v", importFile, err)
} }
@@ -487,6 +494,13 @@ func (p *parser) directive() error {
for p.Next() { for p.Next() {
if p.Val() == "{" { if p.Val() == "{" {
p.nesting++ p.nesting++
if !p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
return p.Err("Unexpected next token after '{' on same line")
}
} else if p.Val() == "{}" {
if p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
return p.Err("Unexpected '{}' at end of line")
}
} else if p.isNewLine() && p.nesting == 0 { } else if p.isNewLine() && p.nesting == 0 {
p.cursor-- // read too far p.cursor-- // read too far
break break
+20 -7
View File
@@ -1,4 +1,4 @@
// Copyright 2015 Light Code Labs, LLC // Copyright 2015 Matthew Holt and The Caddy Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@ package caddyfile
import ( import (
"bytes" "bytes"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -192,6 +191,20 @@ func TestParseOneAndImport(t *testing.T) {
{``, false, []string{}, []int{}}, {``, false, []string{}, []int{}},
// Unexpected next token after '{' on same line
{`localhost
dir1 { a b }`, true, []string{"localhost"}, []int{}},
// Workaround with quotes
{`localhost
dir1 "{" a b "}"`, false, []string{"localhost"}, []int{5}},
// Unexpected '{}' at end of line
{`localhost
dir1 {}`, true, []string{"localhost"}, []int{}},
// Workaround with quotes
{`localhost
dir1 "{}"`, false, []string{"localhost"}, []int{2}},
// import with args // import with args
{`import testdata/import_args0.txt a`, false, []string{"a"}, []int{}}, {`import testdata/import_args0.txt a`, false, []string{"a"}, []int{}},
{`import testdata/import_args1.txt a b`, false, []string{"a", "b"}, []int{}}, {`import testdata/import_args1.txt a b`, false, []string{"a", "b"}, []int{}},
@@ -280,7 +293,7 @@ func TestRecursiveImport(t *testing.T) {
} }
// test relative recursive import // test relative recursive import
err = ioutil.WriteFile(recursiveFile1, []byte( err = os.WriteFile(recursiveFile1, []byte(
`localhost `localhost
dir1 dir1
import recursive_import_test2`), 0644) import recursive_import_test2`), 0644)
@@ -289,7 +302,7 @@ func TestRecursiveImport(t *testing.T) {
} }
defer os.Remove(recursiveFile1) defer os.Remove(recursiveFile1)
err = ioutil.WriteFile(recursiveFile2, []byte("dir2 1"), 0644) err = os.WriteFile(recursiveFile2, []byte("dir2 1"), 0644)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -314,7 +327,7 @@ func TestRecursiveImport(t *testing.T) {
} }
// test absolute recursive import // test absolute recursive import
err = ioutil.WriteFile(recursiveFile1, []byte( err = os.WriteFile(recursiveFile1, []byte(
`localhost `localhost
dir1 dir1
import `+recursiveFile2), 0644) import `+recursiveFile2), 0644)
@@ -370,7 +383,7 @@ func TestDirectiveImport(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
err = ioutil.WriteFile(directiveFile, []byte(`prop1 1 err = os.WriteFile(directiveFile, []byte(`prop1 1
prop2 2`), 0644) prop2 2`), 0644)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -633,7 +646,7 @@ func TestSnippets(t *testing.T) {
} }
func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) { func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) {
file, err := ioutil.TempFile("", t.Name()) file, err := os.CreateTemp("", t.Name())
if err != nil { if err != nil {
panic(err) // get a stack trace so we know where this was called from. panic(err) // get a stack trace so we know where this was called from.
} }
View File
View File
View File
View File
View File
+20 -26
View File
@@ -102,12 +102,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 +169,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)
} }
@@ -208,13 +217,17 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
} }
// the bind directive specifies hosts, but is optional // the bind directive specifies hosts, 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 {
lnHosts = defaultBind
} else {
lnHosts = []string{""} lnHosts = []string{""}
} }
}
// use a map to prevent duplication // use a map to prevent duplication
listeners := make(map[string]struct{}) listeners := make(map[string]struct{})
@@ -223,7 +236,7 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
if err == nil && addr.IsUnixNetwork() { if err == nil && addr.IsUnixNetwork() {
listeners[host] = struct{}{} listeners[host] = struct{}{}
} else { } else {
listeners[net.JoinHostPort(host, lnPort)] = struct{}{} listeners[host+":"+lnPort] = struct{}{}
} }
} }
@@ -232,6 +245,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
} }
@@ -337,7 +351,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 := net.ParseIP(host); ip != nil {
host = ip.String() if ipv6 := ip.To16(); ipv6 != nil && ipv6.DefaultMask() == nil {
host = ipv6.String()
}
} }
return Address{ return Address{
@@ -349,28 +365,6 @@ func (a Address) Normalize() Address {
} }
} }
// Key returns a string form of a, much like String() does, but this
// method doesn't add anything default that wasn't in the original.
func (a Address) Key() string {
res := ""
if a.Scheme != "" {
res += a.Scheme + "://"
}
if a.Host != "" {
res += a.Host
}
// insert port only if the original has its own explicit port
if a.Port != "" &&
len(a.Original) >= len(res) &&
strings.HasPrefix(a.Original[len(res):], ":"+a.Port) {
res += ":" + a.Port
}
if a.Path != "" {
res += a.Path
}
return res
}
// lowerExceptPlaceholders lowercases s except within // lowerExceptPlaceholders lowercases s except within
// placeholders (substrings in non-escaped '{ }' spans). // placeholders (substrings in non-escaped '{ }' spans).
// See https://github.com/caddyserver/caddy/issues/3264 // See https://github.com/caddyserver/caddy/issues/3264
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build gofuzz
// +build gofuzz // +build gofuzz
package httpcaddyfile package httpcaddyfile
+89 -19
View File
@@ -106,67 +106,128 @@ func TestAddressString(t *testing.T) {
func TestKeyNormalization(t *testing.T) { func TestKeyNormalization(t *testing.T) {
testCases := []struct { testCases := []struct {
input string input string
expect string expect Address
}{ }{
{ {
input: "example.com", input: "example.com",
expect: "example.com", expect: Address{
Host: "example.com",
},
}, },
{ {
input: "http://host:1234/path", input: "http://host:1234/path",
expect: "http://host:1234/path", expect: Address{
Scheme: "http",
Host: "host",
Port: "1234",
Path: "/path",
},
}, },
{ {
input: "HTTP://A/ABCDEF", input: "HTTP://A/ABCDEF",
expect: "http://a/ABCDEF", expect: Address{
Scheme: "http",
Host: "a",
Path: "/ABCDEF",
},
}, },
{ {
input: "A/ABCDEF", input: "A/ABCDEF",
expect: "a/ABCDEF", expect: Address{
Host: "a",
Path: "/ABCDEF",
},
}, },
{ {
input: "A:2015/Path", input: "A:2015/Path",
expect: "a:2015/Path", expect: Address{
Host: "a",
Port: "2015",
Path: "/Path",
},
}, },
{ {
input: "sub.{env.MY_DOMAIN}", input: "sub.{env.MY_DOMAIN}",
expect: "sub.{env.MY_DOMAIN}", expect: Address{
Host: "sub.{env.MY_DOMAIN}",
},
}, },
{ {
input: "sub.ExAmPle", input: "sub.ExAmPle",
expect: "sub.example", expect: Address{
Host: "sub.example",
},
}, },
{ {
input: "sub.\\{env.MY_DOMAIN\\}", input: "sub.\\{env.MY_DOMAIN\\}",
expect: "sub.\\{env.my_domain\\}", expect: Address{
Host: "sub.\\{env.my_domain\\}",
},
}, },
{ {
input: "sub.{env.MY_DOMAIN}.com", input: "sub.{env.MY_DOMAIN}.com",
expect: "sub.{env.MY_DOMAIN}.com", expect: Address{
Host: "sub.{env.MY_DOMAIN}.com",
},
}, },
{ {
input: ":80", input: ":80",
expect: ":80", expect: Address{
Port: "80",
},
}, },
{ {
input: ":443", input: ":443",
expect: ":443", expect: Address{
Port: "443",
},
}, },
{ {
input: ":1234", input: ":1234",
expect: ":1234", expect: Address{
Port: "1234",
},
}, },
{ {
input: "", input: "",
expect: "", expect: Address{},
}, },
{ {
input: ":", input: ":",
expect: "", expect: Address{},
}, },
{ {
input: "[::]", input: "[::]",
expect: "::", expect: Address{
Host: "::",
},
},
{
input: "127.0.0.1",
expect: Address{
Host: "127.0.0.1",
},
},
{
input: "[2001:db8:85a3:8d3:1319:8a2e:370:7348]:1234",
expect: Address{
Host: "2001:db8:85a3:8d3:1319:8a2e:370:7348",
Port: "1234",
},
},
{
// IPv4 address in IPv6 form (#4381)
input: "[::ffff:cff4:e77d]:1234",
expect: Address{
Host: "::ffff:cff4:e77d",
Port: "1234",
},
},
{
input: "::ffff:cff4:e77d",
expect: Address{
Host: "::ffff:cff4:e77d",
},
}, },
} }
for i, tc := range testCases { for i, tc := range testCases {
@@ -175,9 +236,18 @@ func TestKeyNormalization(t *testing.T) {
t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err) t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err)
continue continue
} }
if actual := addr.Normalize().Key(); actual != tc.expect { actual := addr.Normalize()
t.Errorf("Test %d: Input '%s': Expected '%s' but got '%s'", i, tc.input, tc.expect, actual) if actual.Scheme != tc.expect.Scheme {
t.Errorf("Test %d: Input '%s': Expected Scheme='%s' but got Scheme='%s'", i, tc.input, tc.expect.Scheme, actual.Scheme)
}
if actual.Host != tc.expect.Host {
t.Errorf("Test %d: Input '%s': Expected Host='%s' but got Host='%s'", i, tc.input, tc.expect.Host, actual.Host)
}
if actual.Port != tc.expect.Port {
t.Errorf("Test %d: Input '%s': Expected Port='%s' but got Port='%s'", i, tc.input, tc.expect.Port, actual.Port)
}
if actual.Path != tc.expect.Path {
t.Errorf("Test %d: Input '%s': Expected Path='%s' but got Path='%s'", i, tc.input, tc.expect.Path, actual.Path)
} }
} }
} }
+64 -4
View File
@@ -19,8 +19,8 @@ import (
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"html" "html"
"io/ioutil"
"net/http" "net/http"
"os"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
@@ -39,6 +39,7 @@ func init() {
RegisterDirective("bind", parseBind) RegisterDirective("bind", parseBind)
RegisterDirective("tls", parseTLS) RegisterDirective("tls", parseTLS)
RegisterHandlerDirective("root", parseRoot) RegisterHandlerDirective("root", parseRoot)
RegisterHandlerDirective("vars", parseVars)
RegisterHandlerDirective("redir", parseRedir) RegisterHandlerDirective("redir", parseRedir)
RegisterHandlerDirective("respond", parseRespond) RegisterHandlerDirective("respond", parseRespond)
RegisterHandlerDirective("abort", parseAbort) RegisterHandlerDirective("abort", parseAbort)
@@ -82,6 +83,7 @@ func parseBind(h Helper) ([]ConfigValue, error) {
// on_demand // on_demand
// eab <key_id> <mac_key> // eab <key_id> <mac_key>
// issuer <module_name> [...] // issuer <module_name> [...]
// get_certificate <module_name> [...]
// } // }
// //
func parseTLS(h Helper) ([]ConfigValue, error) { func parseTLS(h Helper) ([]ConfigValue, error) {
@@ -93,6 +95,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
var keyType string var keyType string
var internalIssuer *caddytls.InternalIssuer var internalIssuer *caddytls.InternalIssuer
var issuers []certmagic.Issuer var issuers []certmagic.Issuer
var certManagers []certmagic.Manager
var onDemand bool var onDemand bool
for h.Next() { for h.Next() {
@@ -230,7 +233,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
return nil, h.ArgErr() return nil, h.ArgErr()
} }
filename := h.Val() filename := h.Val()
certDataPEM, err := ioutil.ReadFile(filename) certDataPEM, err := os.ReadFile(filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -307,6 +310,22 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
} }
issuers = append(issuers, issuer) issuers = append(issuers, issuer)
case "get_certificate":
if !h.NextArg() {
return nil, h.ArgErr()
}
modName := h.Val()
modID := "tls.get_certificate." + modName
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
if err != nil {
return nil, err
}
certManager, ok := unm.(certmagic.Manager)
if !ok {
return nil, h.Errf("module %s (%T) is not a certmagic.CertificateManager", modID, unm)
}
certManagers = append(certManagers, certManager)
case "dns": case "dns":
if !h.NextArg() { if !h.NextArg() {
return nil, h.ArgErr() return nil, h.ArgErr()
@@ -344,6 +363,22 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
} }
acmeIssuer.Challenges.DNS.Resolvers = args acmeIssuer.Challenges.DNS.Resolvers = args
case "dns_challenge_override_domain":
arg := h.RemainingArgs()
if len(arg) != 1 {
return nil, h.ArgErr()
}
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.DNS == nil {
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
}
acmeIssuer.Challenges.DNS.OverrideDomain = arg[0]
case "ca_root": case "ca_root":
arg := h.RemainingArgs() arg := h.RemainingArgs()
if len(arg) != 1 { if len(arg) != 1 {
@@ -453,6 +488,12 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
Value: true, Value: true,
}) })
} }
for _, certManager := range certManagers {
configVals = append(configVals, ConfigValue{
Class: "tls.cert_manager",
Value: certManager,
})
}
// custom certificate selection // custom certificate selection
if len(certSelector.AnyTag) > 0 { if len(certSelector.AnyTag) > 0 {
@@ -490,6 +531,13 @@ func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) {
return caddyhttp.VarsMiddleware{"root": root}, nil return caddyhttp.VarsMiddleware{"root": root}, nil
} }
// parseVars parses the vars directive. See its UnmarshalCaddyfile method for syntax.
func parseVars(h Helper) (caddyhttp.MiddlewareHandler, error) {
v := new(caddyhttp.VarsMiddleware)
err := v.UnmarshalCaddyfile(h.Dispenser)
return v, err
}
// parseRedir parses the redir directive. Syntax: // parseRedir parses the redir directive. Syntax:
// //
// redir [<matcher>] <to> [<code>] // redir [<matcher>] <to> [<code>]
@@ -532,12 +580,24 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo) body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo)
code = "302" code = "302"
default: default:
// Allow placeholders for the code
if strings.HasPrefix(code, "{") {
break
}
// Try to validate as an integer otherwise
codeInt, err := strconv.Atoi(code) codeInt, err := strconv.Atoi(code)
if err != nil { if err != nil {
return nil, h.Errf("Not a supported redir code type or not valid integer: '%s'", code) return nil, h.Errf("Not a supported redir code type or not valid integer: '%s'", code)
} }
if codeInt < 300 || codeInt > 399 { // Sometimes, a 401 with Location header is desirable because
return nil, h.Errf("Redir code not in the 3xx range: '%v'", codeInt) // requests made with XHR will "eat" the 3xx redirect; so if
// the intent was to redirect to an auth page, a 3xx won't
// work. Responding with 401 allows JS code to read the
// Location header and do a window.location redirect manually.
// see https://stackoverflow.com/a/2573589/846934
// see https://github.com/oauth2-proxy/oauth2-proxy/issues/1522
if codeInt < 300 || (codeInt > 399 && codeInt != 401) {
return nil, h.Errf("Redir code not in the 3xx range or 401: '%v'", codeInt)
} }
} }
+23 -9
View File
@@ -37,8 +37,7 @@ func TestLogDirectiveSyntax(t *testing.T) {
format filter { format filter {
wrap console wrap console
fields { fields {
common_log delete request>remote_ip ip_mask {
request>remote_addr ip_mask {
ipv4 24 ipv4 24
ipv6 32 ipv6 32
} }
@@ -47,7 +46,7 @@ func TestLogDirectiveSyntax(t *testing.T) {
} }
} }
`, `,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"encoder":{"fields":{"common_log":{"filter":"delete"},"request\u003eremote_addr":{"filter":"ip_mask","ipv4_cidr":24,"ipv6_cidr":32}},"format":"filter","wrap":{"format":"console"}},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`, output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"encoder":{"fields":{"request\u003eremote_ip":{"filter":"ip_mask","ipv4_cidr":24,"ipv6_cidr":32}},"format":"filter","wrap":{"format":"console"}},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
expectError: false, expectError: false,
}, },
{ {
@@ -149,6 +148,27 @@ func TestRedirDirectiveSyntax(t *testing.T) {
}`, }`,
expectError: false, expectError: false,
}, },
{
// this is now allowed so a Location header
// can be written and consumed by JS
// in the case of XHR requests
input: `:8080 {
redir * :8081 401
}`,
expectError: false,
},
{
input: `:8080 {
redir * :8081 402
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 {http.reverse_proxy.status_code}
}`,
expectError: false,
},
{ {
input: `:8080 { input: `:8080 {
redir /old.html /new.html htlm redir /old.html /new.html htlm
@@ -161,12 +181,6 @@ func TestRedirDirectiveSyntax(t *testing.T) {
}`, }`,
expectError: true, expectError: true,
}, },
{
input: `:8080 {
redir * :8081 400
}`,
expectError: true,
},
{ {
input: `:8080 { input: `:8080 {
redir * :8081 temp redir * :8081 temp
+37 -1
View File
@@ -37,21 +37,27 @@ import (
// The header directive goes second so that headers // The header directive goes second so that headers
// can be manipulated before doing redirects. // can be manipulated before doing redirects.
var directiveOrder = []string{ var directiveOrder = []string{
"tracing",
"map", "map",
"vars",
"root", "root",
"header", "header",
"copy_response_headers", // only in reverse_proxy's handle_response
"request_body", "request_body",
"redir", "redir",
// URI manipulation // incoming request manipulation
"method",
"rewrite", "rewrite",
"uri", "uri",
"try_files", "try_files",
// middleware handlers; some wrap responses // middleware handlers; some wrap responses
"basicauth", "basicauth",
"forward_auth",
"request_header", "request_header",
"encode", "encode",
"push", "push",
@@ -65,6 +71,7 @@ var directiveOrder = []string{
// handlers that typically respond to requests // handlers that typically respond to requests
"abort", "abort",
"error", "error",
"copy_response", // only in reverse_proxy's handle_response
"respond", "respond",
"metrics", "metrics",
"reverse_proxy", "reverse_proxy",
@@ -340,6 +347,9 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
if err != nil { if err != nil {
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err) return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
} }
dir = normalizeDirectiveName(dir)
for _, result := range results { for _, result := range results {
result.directive = dir result.directive = dir
allResults = append(allResults, result) allResults = append(allResults, result)
@@ -415,6 +425,20 @@ func sortRoutes(routes []ConfigValue) {
jPathLen = len(jPM[0]) jPathLen = len(jPM[0])
} }
// some directives involve setting values which can overwrite
// eachother, so it makes most sense to reverse the order so
// that the lease specific matcher is first; everything else
// has most-specific matcher first
if iDir == "vars" {
// if both directives have no path matcher, use whichever one
// has no matcher first.
if iPathLen == 0 && jPathLen == 0 {
return len(iRoute.MatcherSetsRaw) == 0 && len(jRoute.MatcherSetsRaw) > 0
}
// sort with the least-specific (shortest) path first
return iPathLen < jPathLen
} else {
// if both directives have no path matcher, use whichever one // if both directives have no path matcher, use whichever one
// has any kind of matcher defined first. // has any kind of matcher defined first.
if iPathLen == 0 && jPathLen == 0 { if iPathLen == 0 && jPathLen == 0 {
@@ -423,6 +447,7 @@ func sortRoutes(routes []ConfigValue) {
// sort with the most-specific (longest) path first // sort with the most-specific (longest) path first
return iPathLen > jPathLen return iPathLen > jPathLen
}
}) })
} }
@@ -510,6 +535,17 @@ func (sb serverBlock) hasHostCatchAllKey() bool {
return false return false
} }
// isAllHTTP returns true if all sb keys explicitly specify
// the http:// scheme
func (sb serverBlock) isAllHTTP() bool {
for _, addr := range sb.keys {
if addr.Scheme != "http" {
return false
}
}
return true
}
type ( type (
// UnmarshalFunc is a function which can unmarshal Caddyfile // UnmarshalFunc is a function which can unmarshal Caddyfile
// tokens into zero or more config values using a Helper type. // tokens into zero or more config values using a Helper type.
+103 -50
View File
@@ -17,7 +17,6 @@ package httpcaddyfile
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"reflect" "reflect"
"regexp" "regexp"
"sort" "sort"
@@ -30,6 +29,7 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddypki" "github.com/caddyserver/caddy/v2/modules/caddypki"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"go.uber.org/zap"
) )
func init() { func init() {
@@ -88,33 +88,10 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
return nil, warnings, err return nil, warnings, err
} }
// replace shorthand placeholders (which are // replace shorthand placeholders (which are convenient
// convenient when writing a Caddyfile) with // when writing a Caddyfile) with their actual placeholder
// their actual placeholder identifiers or // identifiers or variable names
// variable names replacer := strings.NewReplacer(placeholderShorthands()...)
replacer := strings.NewReplacer(
"{dir}", "{http.request.uri.path.dir}",
"{file}", "{http.request.uri.path.file}",
"{host}", "{http.request.host}",
"{hostport}", "{http.request.hostport}",
"{port}", "{http.request.port}",
"{method}", "{http.request.method}",
"{path}", "{http.request.uri.path}",
"{query}", "{http.request.uri.query}",
"{remote}", "{http.request.remote}",
"{remote_host}", "{http.request.remote.host}",
"{remote_port}", "{http.request.remote.port}",
"{scheme}", "{http.request.scheme}",
"{uri}", "{http.request.uri}",
"{tls_cipher}", "{http.request.tls.cipher_suite}",
"{tls_version}", "{http.request.tls.version}",
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
"{tls_client_serial}", "{http.request.tls.client.serial}",
"{tls_client_subject}", "{http.request.tls.client.subject}",
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
)
// these are placeholders that allow a user-defined final // these are placeholders that allow a user-defined final
// parameters, but we still want to provide a shorthand // parameters, but we still want to provide a shorthand
@@ -128,6 +105,9 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"}, {regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"}, {regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$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(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
{regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"},
} }
for _, sb := range originalServerBlocks { for _, sb := range originalServerBlocks {
@@ -192,13 +172,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err) return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err)
} }
// As a special case, we want "handle_path" to be sorted dir = normalizeDirectiveName(dir)
// at the same level as "handle", so we force them to use
// the same directive name after their parsing is complete.
// See https://github.com/caddyserver/caddy/issues/3675#issuecomment-678042377
if dir == "handle_path" {
dir = "handle"
}
for _, result := range results { for _, result := range results {
result.directive = dir result.directive = dir
@@ -258,20 +232,13 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
} }
customLogs = append(customLogs, ncl) customLogs = append(customLogs, ncl)
} }
// Apply global log options, when set // Apply global log options, when set
if options["log"] != nil { if options["log"] != nil {
for _, logValue := range options["log"].([]ConfigValue) { for _, logValue := range options["log"].([]ConfigValue) {
addCustomLog(logValue.Value.(namedCustomLog)) addCustomLog(logValue.Value.(namedCustomLog))
} }
} }
// Apply server-specific log options
for _, p := range pairings {
for _, sb := range p.serverBlocks {
for _, clVal := range sb.pile["custom_log"] {
addCustomLog(clVal.Value.(namedCustomLog))
}
}
}
if !hasDefaultLog { if !hasDefaultLog {
// if the default log was not customized, ensure we // if the default log was not customized, ensure we
@@ -284,6 +251,15 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
} }
} }
// Apply server-specific log options
for _, p := range pairings {
for _, sb := range p.serverBlocks {
for _, clVal := range sb.pile["custom_log"] {
addCustomLog(clVal.Value.(namedCustomLog))
}
}
}
// annnd the top-level config, then we're done! // annnd the top-level config, then we're done!
cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)} cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)}
@@ -451,17 +427,29 @@ func (st *ServerType) serversFromPairings(
// handle the auto_https global option // handle the auto_https global option
if autoHTTPS != "on" { if autoHTTPS != "on" {
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
if autoHTTPS == "off" { switch autoHTTPS {
case "off":
srv.AutoHTTPS.Disabled = true srv.AutoHTTPS.Disabled = true
} case "disable_redirects":
if autoHTTPS == "disable_redirects" {
srv.AutoHTTPS.DisableRedir = true srv.AutoHTTPS.DisableRedir = true
} case "disable_certs":
if autoHTTPS == "ignore_loaded_certs" { srv.AutoHTTPS.DisableCerts = true
case "ignore_loaded_certs":
srv.AutoHTTPS.IgnoreLoadedCerts = true srv.AutoHTTPS.IgnoreLoadedCerts = true
} }
} }
// Using paths in site addresses is deprecated
// See ParseAddress() where parsing should later reject paths
// See https://github.com/caddyserver/caddy/pull/4728 for a full explanation
for _, sblock := range p.serverBlocks {
for _, addr := range sblock.keys {
if addr.Path != "" {
caddy.Log().Named("caddyfile").Warn("Using a path in a site address is deprecated; please use the 'handle' directive instead", zap.String("address", addr.String()))
}
}
}
// sort server blocks by their keys; this is important because // sort server blocks by their keys; this is important because
// only the first matching site should be evaluated, and we should // only the first matching site should be evaluated, and we should
// attempt to match most specific site first (host and path), in // attempt to match most specific site first (host and path), in
@@ -549,7 +537,7 @@ func (st *ServerType) serversFromPairings(
// emit warnings if user put unspecified IP addresses; they probably want the bind directive // emit warnings if user put unspecified IP addresses; they probably want the bind directive
for _, h := range hosts { for _, h := range hosts {
if h == "0.0.0.0" || h == "::" { if h == "0.0.0.0" || h == "::" {
log.Printf("[WARNING] Site block has unspecified IP address %s which only matches requests having that Host header; you probably want the 'bind' directive to configure the socket", h) caddy.Log().Named("caddyfile").Warn("Site block has an unspecified IP address which only matches requests having that Host header; you probably want the 'bind' directive to configure the socket", zap.String("address", h))
} }
} }
@@ -585,7 +573,7 @@ func (st *ServerType) serversFromPairings(
} }
for _, addr := range sblock.keys { for _, addr := range sblock.keys {
// if server only uses HTTPS port, auto-HTTPS will not apply // if server only uses HTTP port, auto-HTTPS will not apply
if listenersUseAnyPortOtherThan(srv.Listen, httpPort) { if listenersUseAnyPortOtherThan(srv.Listen, httpPort) {
// exclude any hosts that were defined explicitly with "http://" // exclude any hosts that were defined explicitly with "http://"
// in the key from automated cert management (issue #2998) // in the key from automated cert management (issue #2998)
@@ -1060,6 +1048,19 @@ func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subro
return subroute, nil return subroute, nil
} }
// normalizeDirectiveName ensures directives that should be sorted
// at the same level are named the same before sorting happens.
func normalizeDirectiveName(directive string) string {
// As a special case, we want "handle_path" to be sorted
// at the same level as "handle", so we force them to use
// the same directive name after their parsing is complete.
// See https://github.com/caddyserver/caddy/issues/3675#issuecomment-678042377
if directive == "handle_path" {
directive = "handle"
}
return directive
}
// consolidateRoutes combines routes with the same properties // consolidateRoutes combines routes with the same properties
// (same matchers, same Terminal and Group settings) for a // (same matchers, same Terminal and Group settings) for a
// cleaner overall output. // cleaner overall output.
@@ -1241,6 +1242,58 @@ func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.Modul
return msEncoded, nil return msEncoded, nil
} }
// placeholderShorthands returns a slice of old-new string pairs,
// where the left of the pair is a placeholder shorthand that may
// be used in the Caddyfile, and the right is the replacement.
func placeholderShorthands() []string {
return []string{
"{dir}", "{http.request.uri.path.dir}",
"{file}", "{http.request.uri.path.file}",
"{host}", "{http.request.host}",
"{hostport}", "{http.request.hostport}",
"{port}", "{http.request.port}",
"{method}", "{http.request.method}",
"{path}", "{http.request.uri.path}",
"{query}", "{http.request.uri.query}",
"{remote}", "{http.request.remote}",
"{remote_host}", "{http.request.remote.host}",
"{remote_port}", "{http.request.remote.port}",
"{scheme}", "{http.request.scheme}",
"{uri}", "{http.request.uri}",
"{tls_cipher}", "{http.request.tls.cipher_suite}",
"{tls_version}", "{http.request.tls.version}",
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
"{tls_client_serial}", "{http.request.tls.client.serial}",
"{tls_client_subject}", "{http.request.tls.client.subject}",
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
}
}
// WasReplacedPlaceholderShorthand checks if a token string was
// likely a replaced shorthand of the known Caddyfile placeholder
// replacement outputs. Useful to prevent some user-defined map
// output destinations from overlapping with one of the
// predefined shorthands.
func WasReplacedPlaceholderShorthand(token string) string {
prev := ""
for i, item := range placeholderShorthands() {
// only look at every 2nd item, which is the replacement
if i%2 == 0 {
prev = item
continue
}
if strings.Trim(token, "{}") == strings.Trim(item, "{}") {
// we return the original shorthand so it
// can be used for an error message
return prev
}
}
return ""
}
// 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 interface{}, warnings *[]caddyconfig.Warning) int {
+13 -2
View File
@@ -29,11 +29,13 @@ 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", parseOptStringList)
RegisterGlobalOption("grace_period", parseOptDuration) RegisterGlobalOption("grace_period", 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("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)
@@ -277,6 +279,15 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ interface{}) (interface{}, e
return val, nil return val, nil
} }
func parseOptStringList(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() // consume parameter name
val := d.RemainingArgs()
if len(val) == 0 {
return "", d.ArgErr()
}
return val, nil
}
func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
adminCfg := new(caddy.AdminConfig) adminCfg := new(caddy.AdminConfig)
for d.Next() { for d.Next() {
@@ -382,8 +393,8 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ interface{}) (interface{}, erro
if d.Next() { if d.Next() {
return "", d.ArgErr() return "", d.ArgErr()
} }
if val != "off" && val != "disable_redirects" && val != "ignore_loaded_certs" { if val != "off" && val != "disable_redirects" && val != "disable_certs" && val != "ignore_loaded_certs" {
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects' or 'ignore_loaded_certs'") return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', or 'ignore_loaded_certs'")
} }
return val, nil return val, nil
} }
+160 -2
View File
@@ -16,23 +16,176 @@ package httpcaddyfile
import ( import (
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddypki" "github.com/caddyserver/caddy/v2/modules/caddypki"
) )
func init() {
RegisterGlobalOption("pki", parsePKIApp)
}
// parsePKIApp parses the global log option. Syntax:
//
// pki {
// ca [<id>] {
// name <name>
// root_cn <name>
// intermediate_cn <name>
// root {
// cert <path>
// key <path>
// format <format>
// }
// intermediate {
// cert <path>
// key <path>
// format <format>
// }
// }
// }
//
// When the CA ID is unspecified, 'local' is assumed.
//
func parsePKIApp(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) {
pki := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
for d.Next() {
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "ca":
pkiCa := new(caddypki.CA)
if d.NextArg() {
pkiCa.ID = d.Val()
if d.NextArg() {
return nil, d.ArgErr()
}
}
if pkiCa.ID == "" {
pkiCa.ID = caddypki.DefaultCAID
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "name":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Name = d.Val()
case "root_cn":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.RootCommonName = d.Val()
case "intermediate_cn":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.IntermediateCommonName = d.Val()
case "root":
if pkiCa.Root == nil {
pkiCa.Root = new(caddypki.KeyPair)
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "cert":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Root.Certificate = d.Val()
case "key":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Root.PrivateKey = d.Val()
case "format":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Root.Format = d.Val()
default:
return nil, d.Errf("unrecognized pki ca root option '%s'", d.Val())
}
}
case "intermediate":
if pkiCa.Intermediate == nil {
pkiCa.Intermediate = new(caddypki.KeyPair)
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "cert":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Intermediate.Certificate = d.Val()
case "key":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Intermediate.PrivateKey = d.Val()
case "format":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Intermediate.Format = d.Val()
default:
return nil, d.Errf("unrecognized pki ca intermediate option '%s'", d.Val())
}
}
default:
return nil, d.Errf("unrecognized pki ca option '%s'", d.Val())
}
}
pki.CAs[pkiCa.ID] = pkiCa
default:
return nil, d.Errf("unrecognized pki option '%s'", d.Val())
}
}
}
return pki, nil
}
func (st ServerType) buildPKIApp( func (st ServerType) buildPKIApp(
pairings []sbAddrAssociation, pairings []sbAddrAssociation,
options map[string]interface{}, options map[string]interface{},
warnings []caddyconfig.Warning, warnings []caddyconfig.Warning,
) (*caddypki.PKI, []caddyconfig.Warning, error) { ) (*caddypki.PKI, []caddyconfig.Warning, error) {
pkiApp := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
skipInstallTrust := false skipInstallTrust := false
if _, ok := options["skip_install_trust"]; ok { if _, ok := options["skip_install_trust"]; ok {
skipInstallTrust = true skipInstallTrust = true
} }
falseBool := false falseBool := false
// Load the PKI app configured via global options
var pkiApp *caddypki.PKI
unwrappedPki, ok := options["pki"].(*caddypki.PKI)
if ok {
pkiApp = unwrappedPki
} else {
pkiApp = &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
}
for _, ca := range pkiApp.CAs {
if skipInstallTrust {
ca.InstallTrust = &falseBool
}
pkiApp.CAs[ca.ID] = ca
}
// Add in the CAs configured via directives
for _, p := range pairings { for _, p := range pairings {
for _, sblock := range p.serverBlocks { for _, sblock := range p.serverBlocks {
// find all the CAs that were defined and add them to the app config // find all the CAs that were defined and add them to the app config
@@ -42,10 +195,15 @@ func (st ServerType) buildPKIApp(
if skipInstallTrust { if skipInstallTrust {
ca.InstallTrust = &falseBool ca.InstallTrust = &falseBool
} }
// the CA might already exist from global options, so
// don't overwrite it in that case
if _, ok := pkiApp.CAs[ca.ID]; !ok {
pkiApp.CAs[ca.ID] = ca pkiApp.CAs[ca.ID] = ca
} }
} }
} }
}
// if there was no CAs defined in any of the servers, // if there was no CAs defined in any of the servers,
// and we were requested to not install trust, then // and we were requested to not install trust, then
+20 -4
View File
@@ -42,6 +42,7 @@ type serverOptions struct {
AllowH2C bool AllowH2C bool
ExperimentalHTTP3 bool ExperimentalHTTP3 bool
StrictSNIHost *bool StrictSNIHost *bool
ShouldLogCredentials bool
} }
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) { func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
@@ -134,6 +135,12 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error
} }
serverOpts.MaxHeaderBytes = int(size) serverOpts.MaxHeaderBytes = int(size)
case "log_credentials":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.ShouldLogCredentials = true
case "protocol": case "protocol":
for nesting := d.Nesting(); d.NextBlock(nesting); { for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() { switch d.Val() {
@@ -150,11 +157,14 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error
serverOpts.ExperimentalHTTP3 = true serverOpts.ExperimentalHTTP3 = true
case "strict_sni_host": case "strict_sni_host":
if d.NextArg() { if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
return nil, d.ArgErr() return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
} }
trueBool := true boolVal := true
serverOpts.StrictSNIHost = &trueBool if d.Val() == "insecure_off" {
boolVal = false
}
serverOpts.StrictSNIHost = &boolVal
default: default:
return nil, d.Errf("unrecognized protocol option '%s'", d.Val()) return nil, d.Errf("unrecognized protocol option '%s'", d.Val())
@@ -222,6 +232,12 @@ func applyServerOptions(
server.AllowH2C = opts.AllowH2C server.AllowH2C = opts.AllowH2C
server.ExperimentalHTTP3 = opts.ExperimentalHTTP3 server.ExperimentalHTTP3 = opts.ExperimentalHTTP3
server.StrictSNIHost = opts.StrictSNIHost server.StrictSNIHost = opts.StrictSNIHost
if opts.ShouldLogCredentials {
if server.Logs == nil {
server.Logs = &caddyhttp.ServerLogConfig{}
}
server.Logs.ShouldLogCredentials = opts.ShouldLogCredentials
}
} }
return nil return nil
+26 -1
View File
@@ -101,6 +101,12 @@ func (st ServerType) buildTLSApp(
} }
for _, sblock := range p.serverBlocks { for _, sblock := range p.serverBlocks {
// check the scheme of all the site addresses,
// skip building AP if they all had http://
if sblock.isAllHTTP() {
continue
}
// get values that populate an automation policy for this block // get values that populate an automation policy for this block
ap, err := newBaseAutomationPolicy(options, warnings, true) ap, err := newBaseAutomationPolicy(options, warnings, true)
if err != nil { if err != nil {
@@ -133,6 +139,13 @@ func (st ServerType) buildTLSApp(
ap.Issuers = issuers ap.Issuers = issuers
} }
// certificate managers
if certManagerVals, ok := sblock.pile["tls.cert_manager"]; ok {
for _, certManager := range certManagerVals {
certGetterName := certManager.Value.(caddy.Module).CaddyModule().ID.Name()
ap.ManagersRaw = append(ap.ManagersRaw, caddyconfig.JSONModuleObject(certManager.Value, "via", certGetterName, &warnings))
}
}
// custom bind host // custom bind host
for _, cfgVal := range sblock.pile["bind"] { for _, cfgVal := range sblock.pile["bind"] {
for _, iss := range ap.Issuers { for _, iss := range ap.Issuers {
@@ -286,6 +299,19 @@ func (st ServerType) buildTLSApp(
tlsApp.Automation.StorageCleanInterval = storageCleanInterval tlsApp.Automation.StorageCleanInterval = storageCleanInterval
} }
// set the expired certificates renew interval if configured
if renewCheckInterval, ok := options["renew_interval"].(caddy.Duration); ok {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.RenewCheckInterval = renewCheckInterval
}
// set whether OCSP stapling should be disabled for manually-managed certificates
if ocspConfig, ok := options["ocsp_stapling"].(certmagic.OCSPConfig); ok {
tlsApp.DisableOCSPStapling = ocspConfig.DisableStapling
}
// if any hostnames appear on the same server block as a key with // if any hostnames appear on the same server block as a key with
// no host, they will not be used with route matchers because the // no host, they will not be used with route matchers because the
// hostless key matches all hosts, therefore, it wouldn't be // hostless key matches all hosts, therefore, it wouldn't be
@@ -324,7 +350,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) {
+28 -6
View File
@@ -1,11 +1,26 @@
// 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 caddyconfig package caddyconfig
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"os"
"time" "time"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
@@ -56,21 +71,28 @@ func (HTTPLoader) CaddyModule() caddy.ModuleInfo {
// LoadConfig loads a Caddy config. // LoadConfig loads a Caddy config.
func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) { func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
repl := caddy.NewReplacer()
client, err := hl.makeClient(ctx) client, err := hl.makeClient(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
method := hl.Method method := repl.ReplaceAll(hl.Method, "")
if method == "" { if method == "" {
method = http.MethodGet method = http.MethodGet
} }
req, err := http.NewRequest(method, hl.URL, nil) url := repl.ReplaceAll(hl.URL, "")
req, err := http.NewRequest(method, url, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
req.Header = hl.Headers for key, vals := range hl.Headers {
for _, val := range vals {
req.Header.Add(repl.ReplaceAll(key, ""), repl.ReplaceKnown(val, ""))
}
}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
@@ -81,7 +103,7 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
return nil, fmt.Errorf("server responded with HTTP %d", resp.StatusCode) return nil, fmt.Errorf("server responded with HTTP %d", resp.StatusCode)
} }
body, err := ioutil.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -130,7 +152,7 @@ func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
if len(hl.TLS.RootCAPEMFiles) > 0 { if len(hl.TLS.RootCAPEMFiles) > 0 {
rootPool := x509.NewCertPool() rootPool := x509.NewCertPool()
for _, pemFile := range hl.TLS.RootCAPEMFiles { for _, pemFile := range hl.TLS.RootCAPEMFiles {
pemData, err := ioutil.ReadFile(pemFile) pemData, err := os.ReadFile(pemFile)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed reading ca cert: %v", err) return nil, fmt.Errorf("failed reading ca cert: %v", err)
} }
+46 -1
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
+5 -5
View File
@@ -7,7 +7,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"log" "log"
"net" "net"
"net/http" "net/http"
@@ -129,7 +129,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
return return
} }
defer res.Body.Close() defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body) body, _ := io.ReadAll(res.Body)
var out bytes.Buffer var out bytes.Buffer
_ = json.Indent(&out, body, "", " ") _ = json.Indent(&out, body, "", " ")
@@ -162,7 +162,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
timeElapsed(start, "caddytest: config load time") timeElapsed(start, "caddytest: config load time")
defer res.Body.Close() defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body) body, err := io.ReadAll(res.Body)
if err != nil { if err != nil {
tc.t.Errorf("unable to read response. %s", err) tc.t.Errorf("unable to read response. %s", err)
return err return err
@@ -202,7 +202,7 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
return nil return nil
} }
defer resp.Body.Close() defer resp.Body.Close()
actualBytes, err := ioutil.ReadAll(resp.Body) actualBytes, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil return nil
} }
@@ -471,7 +471,7 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
resp := tc.AssertResponseCode(req, expectedStatusCode) resp := tc.AssertResponseCode(req, expectedStatusCode)
defer resp.Body.Close() defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body) bytes, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
tc.t.Fatalf("unable to read the response body %s", err) tc.t.Fatalf("unable to read the response body %s", err)
} }
@@ -0,0 +1,29 @@
example.com {
bind tcp6/[::]
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
"tcp6/[::]:443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,114 @@
example.com
@a expression {http.error.status_code} == 400
abort @a
@b expression {http.error.status_code} == "401"
abort @b
@c expression {http.error.status_code} == `402`
abort @c
@d expression "{http.error.status_code} == 403"
abort @d
@e expression `{http.error.status_code} == 404`
abort @e
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} == 400"
}
]
},
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} == \"401\""
}
]
},
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} == `402`"
}
]
},
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} == 403"
}
]
},
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} == 404"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,32 @@
:80
file_server {
pass_thru
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
],
"pass_thru": true
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,111 @@
app.example.com {
forward_auth authelia:9091 {
uri /api/verify?rd=https://authelia.example.com
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
}
reverse_proxy backend:8080
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"app.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handle_response": [
{
"match": {
"status_code": [
2
]
},
"routes": [
{
"handle": [
{
"handler": "headers",
"request": {
"set": {
"Remote-Email": [
"{http.reverse_proxy.header.Remote-Email}"
],
"Remote-Groups": [
"{http.reverse_proxy.header.Remote-Groups}"
],
"Remote-Name": [
"{http.reverse_proxy.header.Remote-Name}"
],
"Remote-User": [
"{http.reverse_proxy.header.Remote-User}"
]
}
}
}
]
}
]
}
],
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"X-Forwarded-Method": [
"{http.request.method}"
],
"X-Forwarded-Uri": [
"{http.request.uri}"
]
}
}
},
"rewrite": {
"method": "GET",
"uri": "/api/verify?rd=https://authelia.example.com"
},
"upstreams": [
{
"dial": "authelia:9091"
}
]
},
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "backend:8080"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -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"
}
]
}
]
}
]
}
}
}
}
}
@@ -10,6 +10,7 @@
} }
acme_ca https://example.com acme_ca https://example.com
acme_ca_root /path/to/ca.crt acme_ca_root /path/to/ca.crt
ocsp_stapling off
email test@example.com email test@example.com
admin off admin off
@@ -61,7 +62,8 @@
"module": "internal" "module": "internal"
} }
], ],
"key_type": "ed25519" "key_type": "ed25519",
"disable_ocsp_stapling": true
} }
], ],
"on_demand": { "on_demand": {
@@ -71,7 +73,8 @@
}, },
"ask": "https://example.com" "ask": "https://example.com"
} }
} },
"disable_ocsp_stapling": true
} }
} }
} }
@@ -21,6 +21,7 @@
burst 20 burst 20
} }
storage_clean_interval 7d storage_clean_interval 7d
renew_interval 1d
key_type ed25519 key_type ed25519
} }
@@ -82,6 +83,7 @@
}, },
"ask": "https://example.com" "ask": "https://example.com"
}, },
"renew_interval": 86400000000000,
"storage_clean_interval": 604800000000000 "storage_clean_interval": 604800000000000
} }
} }
@@ -0,0 +1,45 @@
{
debug
}
:8881 {
log {
format console
}
}
----------
{
"logging": {
"logs": {
"default": {
"level": "DEBUG",
"exclude": [
"http.log.access.log0"
]
},
"log0": {
"encoder": {
"format": "console"
},
"level": "DEBUG",
"include": [
"http.log.access.log0"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"logs": {
"default_logger_name": "log0"
}
}
}
}
}
}
@@ -0,0 +1,54 @@
{
default_bind tcp4/0.0.0.0 tcp6/[::]
}
example.com {
}
example.org:12345 {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
"tcp4/0.0.0.0:12345",
"tcp6/[::]:12345"
],
"routes": [
{
"match": [
{
"host": [
"example.org"
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
"tcp4/0.0.0.0:443",
"tcp6/[::]:443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -3,8 +3,7 @@
format filter { format filter {
wrap console wrap console
fields { fields {
common_log delete request>remote_ip ip_mask {
request>remote_addr ip_mask {
ipv4 24 ipv4 24
ipv6 32 ipv6 32
} }
@@ -19,10 +18,7 @@
"custom-logger": { "custom-logger": {
"encoder": { "encoder": {
"fields": { "fields": {
"common_log": { "request\u003eremote_ip": {
"filter": "delete"
},
"request\u003eremote_addr": {
"filter": "ip_mask", "filter": "ip_mask",
"ipv4_cidr": 24, "ipv4_cidr": 24,
"ipv6_cidr": 32 "ipv6_cidr": 32
@@ -1,10 +1,44 @@
{ {
skip_install_trust skip_install_trust
pki {
ca {
name "Local"
root_cn "Custom Local Root Name"
intermediate_cn "Custom Local Intermediate Name"
root {
cert /path/to/cert.pem
key /path/to/key.pem
format pem_file
}
intermediate {
cert /path/to/cert.pem
key /path/to/key.pem
format pem_file
}
}
ca foo {
name "Foo"
root_cn "Custom Foo Root Name"
intermediate_cn "Custom Foo Intermediate Name"
}
}
} }
a.example.com { a.example.com {
tls internal tls internal
} }
acme.example.com {
acme_server {
ca foo
}
}
acme-bar.example.com {
acme_server {
ca bar
}
}
---------- ----------
{ {
"apps": { "apps": {
@@ -15,6 +49,56 @@ a.example.com {
":443" ":443"
], ],
"routes": [ "routes": [
{
"match": [
{
"host": [
"acme-bar.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "bar",
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"acme.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "foo",
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
},
{ {
"match": [ "match": [
{ {
@@ -31,14 +115,42 @@ a.example.com {
}, },
"pki": { "pki": {
"certificate_authorities": { "certificate_authorities": {
"local": { "bar": {
"install_trust": false "install_trust": false
},
"foo": {
"name": "Foo",
"root_common_name": "Custom Foo Root Name",
"intermediate_common_name": "Custom Foo Intermediate Name",
"install_trust": false
},
"local": {
"name": "Local",
"root_common_name": "Custom Local Root Name",
"intermediate_common_name": "Custom Local Intermediate Name",
"install_trust": false,
"root": {
"certificate": "/path/to/cert.pem",
"private_key": "/path/to/key.pem",
"format": "pem_file"
},
"intermediate": {
"certificate": "/path/to/cert.pem",
"private_key": "/path/to/key.pem",
"format": "pem_file"
}
} }
} }
}, },
"tls": { "tls": {
"automation": { "automation": {
"policies": [ "policies": [
{
"subjects": [
"acme-bar.example.com",
"acme.example.com"
]
},
{ {
"subjects": [ "subjects": [
"a.example.com" "a.example.com"
@@ -3,6 +3,9 @@
timeouts { timeouts {
idle 90s idle 90s
} }
protocol {
strict_sni_host insecure_off
}
} }
servers :80 { servers :80 {
timeouts { timeouts {
@@ -13,6 +16,9 @@
timeouts { timeouts {
idle 30s idle 30s
} }
protocol {
strict_sni_host
}
} }
} }
@@ -46,7 +52,8 @@ http://bar.com {
], ],
"terminal": true "terminal": true
} }
] ],
"strict_sni_host": true
}, },
"srv1": { "srv1": {
"listen": [ "listen": [
@@ -70,7 +77,8 @@ http://bar.com {
"listen": [ "listen": [
":8080" ":8080"
], ],
"idle_timeout": 90000000000 "idle_timeout": 90000000000,
"strict_sni_host": false
} }
} }
} }
@@ -1,6 +1,7 @@
{ {
servers { servers {
listener_wrappers { listener_wrappers {
http_redirect
tls tls
} }
timeouts { timeouts {
@@ -10,6 +11,7 @@
idle 30s idle 30s
} }
max_header_size 100MB max_header_size 100MB
log_credentials
protocol { protocol {
allow_h2c allow_h2c
experimental_http3 experimental_http3
@@ -31,6 +33,9 @@ foo.com {
":443" ":443"
], ],
"listener_wrappers": [ "listener_wrappers": [
{
"wrapper": "http_redirect"
},
{ {
"wrapper": "tls" "wrapper": "tls"
} }
@@ -53,6 +58,9 @@ foo.com {
} }
], ],
"strict_sni_host": true, "strict_sni_host": true,
"logs": {
"should_log_credentials": true
},
"experimental_http3": true, "experimental_http3": true,
"allow_h2c": true "allow_h2c": true
} }
@@ -13,6 +13,10 @@
header @images { header @images {
Cache-Control "public, max-age=3600, stale-while-revalidate=86400" Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
} }
header {
+Link "Foo"
+Link "Bar"
}
} }
---------- ----------
{ {
@@ -121,6 +125,17 @@
] ]
} }
} }
},
{
"handler": "headers",
"response": {
"add": {
"Link": [
"Foo",
"Bar"
]
}
}
} }
] ]
} }
@@ -5,12 +5,24 @@ log {
format filter { format filter {
wrap console wrap console
fields { fields {
uri query {
replace foo REDACTED
delete bar
hash baz
}
request>headers>Authorization replace REDACTED request>headers>Authorization replace REDACTED
request>headers>Server delete request>headers>Server delete
request>remote_addr ip_mask { request>headers>Cookie cookie {
replace foo REDACTED
delete bar
hash baz
}
request>remote_ip ip_mask {
ipv4 24 ipv4 24
ipv6 32 ipv6 32
} }
request>headers>Regexp regexp secret REDACTED
request>headers>Hash hash
} }
} }
} }
@@ -33,13 +45,57 @@ log {
"filter": "replace", "filter": "replace",
"value": "REDACTED" "value": "REDACTED"
}, },
"request\u003eheaders\u003eCookie": {
"actions": [
{
"name": "foo",
"type": "replace",
"value": "REDACTED"
},
{
"name": "bar",
"type": "delete"
},
{
"name": "baz",
"type": "hash"
}
],
"filter": "cookie"
},
"request\u003eheaders\u003eHash": {
"filter": "hash"
},
"request\u003eheaders\u003eRegexp": {
"filter": "regexp",
"regexp": "secret",
"value": "REDACTED"
},
"request\u003eheaders\u003eServer": { "request\u003eheaders\u003eServer": {
"filter": "delete" "filter": "delete"
}, },
"request\u003eremote_addr": { "request\u003eremote_ip": {
"filter": "ip_mask", "filter": "ip_mask",
"ipv4_cidr": 24, "ipv4_cidr": 24,
"ipv6_cidr": 32 "ipv6_cidr": 32
},
"uri": {
"actions": [
{
"parameter": "foo",
"type": "replace",
"value": "REDACTED"
},
{
"parameter": "bar",
"type": "delete"
},
{
"parameter": "baz",
"type": "hash"
}
],
"filter": "query"
} }
}, },
"format": "filter", "format": "filter",
@@ -3,6 +3,8 @@
log { log {
output file /var/log/access.log { output file /var/log/access.log {
roll_size 1gb roll_size 1gb
roll_uncompressed
roll_local_time
roll_keep 5 roll_keep 5
roll_keep_for 90d roll_keep_for 90d
} }
@@ -20,8 +22,10 @@ log {
"writer": { "writer": {
"filename": "/var/log/access.log", "filename": "/var/log/access.log",
"output": "file", "output": "file",
"roll_gzip": false,
"roll_keep": 5, "roll_keep": 5,
"roll_keep_days": 90, "roll_keep_days": 90,
"roll_local_time": true,
"roll_size_mb": 954 "roll_size_mb": 954
}, },
"include": [ "include": [
@@ -0,0 +1,126 @@
example.com
map {host} {my_placeholder} {magic_number} {
# Should output boolean "true" and an integer
example.com true 3
# Should output a string and null
foo.example.com "string value"
# Should output two strings (quoted int)
(.*)\.example.com "${1} subdomain" "5"
# Should output null and a string (quoted int)
~.*\.net$ - `7`
# Should output a float and the string "false"
~.*\.xyz$ 123.456 "false"
# Should output two strings, second being escaped quote
default "unknown domain" \"""
}
vars foo bar
vars {
abc true
def 1
ghi 2.3
jkl "mn op"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"defaults": [
"unknown domain",
"\""
],
"destinations": [
"{my_placeholder}",
"{magic_number}"
],
"handler": "map",
"mappings": [
{
"input": "example.com",
"outputs": [
true,
3
]
},
{
"input": "foo.example.com",
"outputs": [
"string value",
null
]
},
{
"input": "(.*)\\.example.com",
"outputs": [
"${1} subdomain",
"5"
]
},
{
"input_regexp": ".*\\.net$",
"outputs": [
null,
"7"
]
},
{
"input_regexp": ".*\\.xyz$",
"outputs": [
123.456,
"false"
]
}
],
"source": "{http.request.host}"
},
{
"foo": "bar",
"handler": "vars"
},
{
"abc": true,
"def": 1,
"ghi": 2.3,
"handler": "vars",
"jkl": "mn op"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -37,6 +37,9 @@
header Bar foo header Bar foo
} }
respond @matcher9 "header matcher with null field matcher" respond @matcher9 "header matcher with null field matcher"
@matcher10 remote_ip private_ranges
respond @matcher10 "remote_ip matcher with private ranges"
} }
---------- ----------
{ {
@@ -101,7 +104,9 @@
"match": [ "match": [
{ {
"vars": { "vars": {
"{http.request.uri}": "/vars-matcher" "{http.request.uri}": [
"/vars-matcher"
]
} }
} }
], ],
@@ -207,6 +212,28 @@
"handler": "static_response" "handler": "static_response"
} }
] ]
},
{
"match": [
{
"remote_ip": {
"ranges": [
"192.168.0.0/16",
"172.16.0.0/12",
"10.0.0.0/8",
"127.0.0.1/8",
"fd00::/8",
"::1"
]
}
}
],
"handle": [
{
"body": "remote_ip matcher with private ranges",
"handler": "static_response"
}
]
} }
] ]
} }
@@ -0,0 +1,27 @@
:8080 {
method FOO
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"handle": [
{
"handler": "rewrite",
"method": "FOO"
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,124 @@
:8884
php_fastcgi localhost:9000 {
# some php_fastcgi-specific subdirectives
split .php .php5
env VAR1 value1
env VAR2 value2
root /var/www
try_files {path} {path}/index.php =404
dial_timeout 3s
read_timeout 10s
write_timeout 20s
# passed through to reverse_proxy (directive order doesn't matter!)
lb_policy random
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
],
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.uri.path}/"
]
},
"status_code": 308
}
]
},
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"=404"
],
"split_path": [
".php",
".php5"
]
}
}
],
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
]
},
{
"match": [
{
"path": [
"*.php",
"*.php5"
]
}
],
"handle": [
{
"handler": "reverse_proxy",
"load_balancing": {
"selection_policy": {
"policy": "random"
}
},
"transport": {
"dial_timeout": 3000000000,
"env": {
"VAR1": "value1",
"VAR2": "value2"
},
"protocol": "fastcgi",
"read_timeout": 10000000000,
"root": "/var/www",
"split_path": [
".php",
".php5"
],
"write_timeout": 20000000000
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,116 @@
:8884 {
reverse_proxy {
dynamic a foo 9000
}
reverse_proxy {
dynamic a {
name foo
port 9000
refresh 5m
resolvers 8.8.8.8 8.8.4.4
dial_timeout 2s
dial_fallback_delay 300ms
}
}
}
:8885 {
reverse_proxy {
dynamic srv _api._tcp.example.com
}
reverse_proxy {
dynamic srv {
service api
proto tcp
name example.com
refresh 5m
resolvers 8.8.8.8 8.8.4.4
dial_timeout 1s
dial_fallback_delay -1s
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"dynamic_upstreams": {
"name": "foo",
"port": "9000",
"source": "a"
},
"handler": "reverse_proxy"
},
{
"dynamic_upstreams": {
"dial_fallback_delay": 300000000,
"dial_timeout": 2000000000,
"name": "foo",
"port": "9000",
"refresh": 300000000000,
"resolver": {
"addresses": [
"8.8.8.8",
"8.8.4.4"
]
},
"source": "a"
},
"handler": "reverse_proxy"
}
]
}
]
},
"srv1": {
"listen": [
":8885"
],
"routes": [
{
"handle": [
{
"dynamic_upstreams": {
"name": "_api._tcp.example.com",
"source": "srv"
},
"handler": "reverse_proxy"
},
{
"dynamic_upstreams": {
"dial_fallback_delay": -1000000000,
"dial_timeout": 1000000000,
"name": "example.com",
"proto": "tcp",
"refresh": 300000000000,
"resolver": {
"addresses": [
"8.8.8.8",
"8.8.4.4"
]
},
"service": "api",
"source": "srv"
},
"handler": "reverse_proxy"
}
]
}
]
}
}
}
}
}
@@ -1,6 +1,14 @@
:8884 :8884
reverse_proxy 127.0.0.1:65535 { reverse_proxy 127.0.0.1:65535 {
@500 status 500
replace_status @500 400
@all status 2xx 3xx 4xx 5xx
replace_status @all {http.error.status_code}
replace_status {http.error.status_code}
@accel header X-Accel-Redirect * @accel header X-Accel-Redirect *
handle_response @accel { handle_response @accel {
respond "Header X-Accel-Redirect!" respond "Header X-Accel-Redirect!"
@@ -39,8 +47,19 @@ reverse_proxy 127.0.0.1:65535 {
respond "Headers Foo, Bar AND statuses 401, 403 and 404!" respond "Headers Foo, Bar AND statuses 401, 403 and 404!"
} }
@changeStatus status 500 @200 status 200
handle_response @changeStatus 400 handle_response @200 {
copy_response_headers {
include Foo Bar
}
respond "Copied headers from the response"
}
@201 status 201
handle_response @201 {
header Foo "Copying the response"
copy_response 404
}
} }
---------- ----------
{ {
@@ -56,6 +75,25 @@ reverse_proxy 127.0.0.1:65535 {
"handle": [ "handle": [
{ {
"handle_response": [ "handle_response": [
{
"match": {
"status_code": [
500
]
},
"status_code": 400
},
{
"match": {
"status_code": [
2,
3,
4,
5
]
},
"status_code": "{http.error.status_code}"
},
{ {
"match": { "match": {
"headers": { "headers": {
@@ -158,10 +196,56 @@ reverse_proxy 127.0.0.1:65535 {
{ {
"match": { "match": {
"status_code": [ "status_code": [
500 200
] ]
}, },
"status_code": 400 "routes": [
{
"handle": [
{
"handler": "copy_response_headers",
"include": [
"Foo",
"Bar"
]
},
{
"body": "Copied headers from the response",
"handler": "static_response"
}
]
}
]
},
{
"match": {
"status_code": [
201
]
},
"routes": [
{
"handle": [
{
"handler": "headers",
"response": {
"set": {
"Foo": [
"Copying the response"
]
}
}
},
{
"handler": "copy_response",
"status_code": 404
}
]
}
]
},
{
"status_code": "{http.error.status_code}"
}, },
{ {
"routes": [ "routes": [
@@ -7,6 +7,7 @@ reverse_proxy 127.0.0.1:65535 {
X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO
X-Empty-Value X-Empty-Value
} }
health_uri /health
} }
---------- ----------
{ {
@@ -38,7 +39,8 @@ reverse_proxy 127.0.0.1:65535 {
"VbG4NZwWnipo", "VbG4NZwWnipo",
"335Q9/MhqcNU3s2TO" "335Q9/MhqcNU3s2TO"
] ]
} },
"uri": "/health"
} }
}, },
"upstreams": [ "upstreams": [
@@ -1,11 +1,11 @@
https://example.com { https://example.com {
reverse_proxy /path http://localhost:54321 { reverse_proxy /path https://localhost:54321 {
header_up Host {host} header_up Host {upstream_hostport}
header_up X-Real-IP {remote} header_up Foo bar
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Port {server_port} method GET
header_up X-Forwarded-Proto "http" rewrite /rewritten?uri={uri}
buffer_requests buffer_requests
@@ -17,11 +17,16 @@ https://example.com {
dial_fallback_delay 5s dial_fallback_delay 5s
response_header_timeout 8s response_header_timeout 8s
expect_continue_timeout 9s expect_continue_timeout 9s
resolvers 8.8.8.8 8.8.4.4
versions h2c 2 versions h2c 2
compression off compression off
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
tls_renegotiation freely
tls_except_ports 8181 8182
} }
} }
} }
@@ -56,37 +61,46 @@ https://example.com {
"headers": { "headers": {
"request": { "request": {
"set": { "set": {
"Foo": [
"bar"
],
"Host": [ "Host": [
"{http.request.host}" "{http.reverse_proxy.upstream.hostport}"
],
"X-Forwarded-For": [
"{http.request.remote}"
],
"X-Forwarded-Port": [
"{server_port}"
],
"X-Forwarded-Proto": [
"http"
],
"X-Real-Ip": [
"{http.request.remote}"
] ]
} }
} }
}, },
"rewrite": {
"method": "GET",
"uri": "/rewritten?uri={http.request.uri}"
},
"transport": { "transport": {
"compression": false, "compression": false,
"dial_fallback_delay": 5000000000, "dial_fallback_delay": 5000000000,
"dial_timeout": 3000000000, "dial_timeout": 3000000000,
"expect_continue_timeout": 9000000000, "expect_continue_timeout": 9000000000,
"keep_alive": { "keep_alive": {
"max_idle_conns_per_host": 2 "max_idle_conns_per_host": 2,
"probe_interval": 30000000000
}, },
"max_conns_per_host": 5, "max_conns_per_host": 5,
"max_response_header_size": 30000000, "max_response_header_size": 30000000,
"protocol": "http", "protocol": "http",
"read_buffer_size": 10000000, "read_buffer_size": 10000000,
"resolver": {
"addresses": [
"8.8.8.8",
"8.8.4.4"
]
},
"response_header_timeout": 8000000000, "response_header_timeout": 8000000000,
"tls": {
"except_ports": [
"8181",
"8182"
],
"renegotiation": "freely"
},
"versions": [ "versions": [
"h2c", "h2c",
"2" "2"
@@ -0,0 +1,56 @@
:8884
reverse_proxy 127.0.0.1:65535 {
trusted_proxies 127.0.0.1
}
reverse_proxy 127.0.0.1:65535 {
trusted_proxies private_ranges
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"trusted_proxies": [
"127.0.0.1"
],
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
},
{
"handler": "reverse_proxy",
"trusted_proxies": [
"192.168.0.0/16",
"172.16.0.0/12",
"10.0.0.0/8",
"127.0.0.1/8",
"fd00::/8",
"::1"
],
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,133 @@
*.example.com {
@foo host foo.example.com
handle @foo {
handle_path /strip* {
respond "this should be first"
}
handle {
respond "this should be second"
}
}
handle {
respond "this should be last"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"*.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"group": "group5",
"handle": [
{
"handler": "subroute",
"routes": [
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "rewrite",
"strip_path_prefix": "/strip"
}
]
},
{
"handle": [
{
"body": "this should be first",
"handler": "static_response"
}
]
}
]
}
],
"match": [
{
"path": [
"/strip*"
]
}
]
},
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "this should be second",
"handler": "static_response"
}
]
}
]
}
]
}
]
}
],
"match": [
{
"host": [
"foo.example.com"
]
}
]
},
{
"group": "group5",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "this should be last",
"handler": "static_response"
}
]
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,59 @@
:80
vars /foobar foo last
vars /foo foo middle
vars * foo first
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"foo": "first",
"handler": "vars"
}
]
},
{
"match": [
{
"path": [
"/foo"
]
}
],
"handle": [
{
"foo": "middle",
"handler": "vars"
}
]
},
{
"match": [
{
"path": [
"/foobar"
]
}
],
"handle": [
{
"foo": "last",
"handler": "vars"
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,98 @@
# (this Caddyfile is contrived, but based on issues #4176 and #4198)
http://example.com {
}
https://example.com {
tls abc@example.com
}
http://localhost:8081 {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
},
"srv2": {
"listen": [
":8081"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
}
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"example.com"
],
"issuers": [
{
"email": "abc@example.com",
"module": "acme"
},
{
"email": "abc@example.com",
"module": "zerossl"
}
]
}
]
}
}
}
}
@@ -0,0 +1,56 @@
# example from issue #4640
http://foo:8447, http://127.0.0.1:8447 {
reverse_proxy 127.0.0.1:8080
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8447"
],
"routes": [
{
"match": [
{
"host": [
"foo",
"127.0.0.1"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "127.0.0.1:8080"
}
]
}
]
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"foo",
"127.0.0.1"
]
}
}
}
}
}
}
@@ -0,0 +1,54 @@
a.example.com {
tls {
issuer internal {
ca foo
lifetime 24h
sign_with_root
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"a.example.com"
],
"issuers": [
{
"ca": "foo",
"lifetime": 86400000000000,
"module": "internal",
"sign_with_root": true
}
]
}
]
}
}
}
}
@@ -3,7 +3,12 @@ localhost
respond "hello from localhost" respond "hello from localhost"
tls { tls {
issuer acme { issuer acme {
propagation_timeout "10m0s" propagation_delay 5m10s
propagation_timeout 10m20s
}
issuer zerossl {
propagation_delay 5m30s
propagation_timeout -1
} }
} }
---------- ----------
@@ -56,10 +61,20 @@ tls {
{ {
"challenges": { "challenges": {
"dns": { "dns": {
"propagation_timeout": 600000000000 "propagation_delay": 310000000000,
"propagation_timeout": 620000000000
} }
}, },
"module": "acme" "module": "acme"
},
{
"challenges": {
"dns": {
"propagation_delay": 330000000000,
"propagation_timeout": -1
}
},
"module": "zerossl"
} }
] ]
} }
@@ -0,0 +1,36 @@
:80 {
tracing /myhandler {
span my-span
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/myhandler"
]
}
],
"handle": [
{
"handler": "tracing",
"span": "my-span"
}
]
}
]
}
}
}
}
}
@@ -3,7 +3,7 @@ package integration
import ( import (
jsonMod "encoding/json" jsonMod "encoding/json"
"fmt" "fmt"
"io/ioutil" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
@@ -14,7 +14,7 @@ import (
func TestCaddyfileAdaptToJSON(t *testing.T) { func TestCaddyfileAdaptToJSON(t *testing.T) {
// load the list of test files from the dir // load the list of test files from the dir
files, err := ioutil.ReadDir("./caddyfile_adapt") files, err := os.ReadDir("./caddyfile_adapt")
if err != nil { if err != nil {
t.Errorf("failed to read caddyfile_adapt dir: %s", err) t.Errorf("failed to read caddyfile_adapt dir: %s", err)
} }
@@ -29,7 +29,7 @@ func TestCaddyfileAdaptToJSON(t *testing.T) {
// read the test file // read the test file
filename := f.Name() filename := f.Name()
data, err := ioutil.ReadFile("./caddyfile_adapt/" + filename) data, err := os.ReadFile("./caddyfile_adapt/" + filename)
if err != nil { if err != nil {
t.Errorf("failed to read %s dir: %s", filename, err) t.Errorf("failed to read %s dir: %s", filename, err)
} }
+24
View File
@@ -101,3 +101,27 @@ func TestReadCookie(t *testing.T) {
// act and assert // act and assert
tester.AssertGetResponse("http://localhost:9080/cookie.html", 200, "<h2>Cookie.ClientName caddytest</h2>") tester.AssertGetResponse("http://localhost:9080/cookie.html", 200, "<h2>Cookie.ClientName caddytest</h2>")
} }
func TestReplIndex(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
}
localhost:9080 {
templates {
root testdata
}
file_server {
root testdata
index "index.{host}.html"
}
}
`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/", 200, "")
}
+3 -4
View File
@@ -2,7 +2,6 @@ package integration
import ( import (
"fmt" "fmt"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"os" "os"
@@ -85,7 +84,7 @@ func TestDialWithPlaceholderUnix(t *testing.T) {
t.SkipNow() t.SkipNow()
} }
f, err := ioutil.TempFile("", "*.sock") f, err := os.CreateTemp("", "*.sock")
if err != nil { if err != nil {
t.Errorf("failed to create TempFile: %s", err) t.Errorf("failed to create TempFile: %s", err)
return return
@@ -387,7 +386,7 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
t.SkipNow() t.SkipNow()
} }
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
f, err := ioutil.TempFile("", "*.sock") f, err := os.CreateTemp("", "*.sock")
if err != nil { if err != nil {
t.Errorf("failed to create TempFile: %s", err) t.Errorf("failed to create TempFile: %s", err)
return return
@@ -442,7 +441,7 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
t.SkipNow() t.SkipNow()
} }
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
f, err := ioutil.TempFile("", "*.sock") f, err := os.CreateTemp("", "*.sock")
if err != nil { if err != nil {
t.Errorf("failed to create TempFile: %s", err) t.Errorf("failed to create TempFile: %s", err)
return return
+5 -6
View File
@@ -6,7 +6,6 @@ import (
"crypto/rand" "crypto/rand"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
@@ -110,7 +109,7 @@ func TestH2ToH2CStream(t *testing.T) {
r, w := io.Pipe() r, w := io.Pipe()
req := &http.Request{ req := &http.Request{
Method: "PUT", Method: "PUT",
Body: ioutil.NopCloser(r), Body: io.NopCloser(r),
URL: &url.URL{ URL: &url.URL{
Scheme: "https", Scheme: "https",
Host: "127.0.0.1:9443", Host: "127.0.0.1:9443",
@@ -134,7 +133,7 @@ func TestH2ToH2CStream(t *testing.T) {
}() }()
defer resp.Body.Close() defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body) bytes, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
t.Fatalf("unable to read the response body %s", err) t.Fatalf("unable to read the response body %s", err)
} }
@@ -319,7 +318,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
r, w := io.Pipe() r, w := io.Pipe()
req := &http.Request{ req := &http.Request{
Method: "PUT", Method: "PUT",
Body: ioutil.NopCloser(r), Body: io.NopCloser(r),
URL: &url.URL{ URL: &url.URL{
Scheme: "https", Scheme: "https",
Host: "127.0.0.1:9443", Host: "127.0.0.1:9443",
@@ -342,7 +341,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
} }
defer resp.Body.Close() defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body) bytes, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
t.Fatalf("unable to read the response body %s", err) t.Fatalf("unable to read the response body %s", err)
} }
@@ -370,7 +369,7 @@ func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server {
} }
defer r.Body.Close() defer r.Body.Close()
bytes, err := ioutil.ReadAll(r.Body) bytes, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
t.Fatalf("unable to read the response body %s", err) t.Fatalf("unable to read the response body %s", err)
} }
+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 (
+114 -52
View File
@@ -22,7 +22,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
@@ -33,6 +32,7 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/aryann/difflib"
"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/caddyfile"
@@ -181,7 +181,7 @@ func cmdRun(fl Flags) (int, error) {
var config []byte var config []byte
var err error var err error
if runCmdResumeFlag { if runCmdResumeFlag {
config, err = ioutil.ReadFile(caddy.ConfigAutosavePath) config, err = os.ReadFile(caddy.ConfigAutosavePath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
// not a bad error; just can't resume if autosave file doesn't exist // not a bad error; just can't resume if autosave file doesn't exist
caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath)) caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
@@ -203,7 +203,7 @@ func cmdRun(fl Flags) (int, error) {
// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive // we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
var configFile string var configFile string
if !runCmdResumeFlag { if !runCmdResumeFlag {
config, configFile, err = loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag) config, configFile, err = LoadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} }
@@ -219,7 +219,7 @@ func cmdRun(fl Flags) (int, error) {
// if we are to report to another process the successful start // if we are to report to another process the successful start
// of the server, do so now by echoing back contents of stdin // of the server, do so now by echoing back contents of stdin
if runCmdPingbackFlag != "" { if runCmdPingbackFlag != "" {
confirmationBytes, err := ioutil.ReadAll(os.Stdin) confirmationBytes, err := io.ReadAll(os.Stdin)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading confirmation bytes from stdin: %v", err) fmt.Errorf("reading confirmation bytes from stdin: %v", err)
@@ -276,25 +276,33 @@ func cmdRun(fl Flags) (int, error) {
} }
func cmdStop(fl Flags) (int, error) { func cmdStop(fl Flags) (int, error) {
stopCmdAddrFlag := fl.String("address") addrFlag := fl.String("address")
configFlag := fl.String("config")
configAdapterFlag := fl.String("adapter")
err := apiRequest(stopCmdAddrFlag, http.MethodPost, "/stop", nil, nil) adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
}
resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/stop", nil, nil)
if err != nil { if err != nil {
caddy.Log().Warn("failed using API to stop instance", zap.Error(err)) caddy.Log().Warn("failed using API to stop instance", zap.Error(err))
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} }
defer resp.Body.Close()
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
func cmdReload(fl Flags) (int, error) { func cmdReload(fl Flags) (int, error) {
reloadCmdConfigFlag := fl.String("config") configFlag := fl.String("config")
reloadCmdConfigAdapterFlag := fl.String("adapter") configAdapterFlag := fl.String("adapter")
reloadCmdAddrFlag := fl.String("address") addrFlag := fl.String("address")
reloadCmdForceFlag := fl.Bool("force") forceFlag := fl.Bool("force")
// get the config in caddy's native format // get the config in caddy's native format
config, configFile, err := loadConfig(reloadCmdConfigFlag, reloadCmdConfigAdapterFlag) config, configFile, err := LoadConfig(configFlag, configAdapterFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} }
@@ -302,30 +310,22 @@ 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")
} }
// get the address of the admin listener; use flag if specified adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
adminAddr := reloadCmdAddrFlag
if adminAddr == "" && len(config) > 0 {
var tmpStruct struct {
Admin caddy.AdminConfig `json:"admin"`
}
err = json.Unmarshal(config, &tmpStruct)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
fmt.Errorf("unmarshaling admin listener address from config: %v", err)
}
adminAddr = tmpStruct.Admin.Listen
} }
// optionally force a config reload // optionally force a config reload
headers := make(http.Header) headers := make(http.Header)
if reloadCmdForceFlag { if forceFlag {
headers.Set("Cache-Control", "must-revalidate") headers.Set("Cache-Control", "must-revalidate")
} }
err = apiRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config)) resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err)
} }
defer resp.Body.Close()
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
@@ -361,6 +361,7 @@ func cmdBuildInfo(fl Flags) (int, error) {
func cmdListModules(fl Flags) (int, error) { func cmdListModules(fl Flags) (int, error) {
packages := fl.Bool("packages") packages := fl.Bool("packages")
versions := fl.Bool("versions") versions := fl.Bool("versions")
skipStandard := fl.Bool("skip-standard")
printModuleInfo := func(mi moduleInfo) { printModuleInfo := func(mi moduleInfo) {
fmt.Print(mi.caddyModuleID) fmt.Print(mi.caddyModuleID)
@@ -389,14 +390,19 @@ func cmdListModules(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
// Standard modules (always shipped with Caddy)
if !skipStandard {
if len(standard) > 0 { if len(standard) > 0 {
for _, mod := range standard { for _, mod := range standard {
printModuleInfo(mod) printModuleInfo(mod)
} }
} }
fmt.Printf("\n Standard modules: %d\n", len(standard)) fmt.Printf("\n Standard modules: %d\n", len(standard))
}
// Non-standard modules (third party plugins)
if len(nonstandard) > 0 { if len(nonstandard) > 0 {
if len(standard) > 0 { if len(standard) > 0 && !skipStandard {
fmt.Println() fmt.Println()
} }
for _, mod := range nonstandard { for _, mod := range nonstandard {
@@ -404,8 +410,10 @@ func cmdListModules(fl Flags) (int, error) {
} }
} }
fmt.Printf("\n Non-standard modules: %d\n", len(nonstandard)) fmt.Printf("\n Non-standard modules: %d\n", len(nonstandard))
// Unknown modules (couldn't get Caddy module info)
if len(unknown) > 0 { if len(unknown) > 0 {
if len(standard) > 0 || len(nonstandard) > 0 { if (len(standard) > 0 && !skipStandard) || len(nonstandard) > 0 {
fmt.Println() fmt.Println()
} }
for _, mod := range unknown { for _, mod := range unknown {
@@ -457,7 +465,7 @@ func cmdAdaptConfig(fl Flags) (int, error) {
fmt.Errorf("unrecognized config adapter: %s", adaptCmdAdapterFlag) fmt.Errorf("unrecognized config adapter: %s", adaptCmdAdapterFlag)
} }
input, err := ioutil.ReadFile(adaptCmdInputFlag) input, err := os.ReadFile(adaptCmdInputFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading input file: %v", err) fmt.Errorf("reading input file: %v", err)
@@ -488,7 +496,9 @@ func cmdAdaptConfig(fl Flags) (int, error) {
if warn.Directive != "" { if warn.Directive != "" {
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message) msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
} }
fmt.Fprintf(os.Stderr, "[WARNING][%s] %s:%d: %s\n", adaptCmdAdapterFlag, warn.File, warn.Line, msg) caddy.Log().Named(adaptCmdAdapterFlag).Warn(msg,
zap.String("file", warn.File),
zap.Int("line", warn.Line))
} }
// validate output if requested // validate output if requested
@@ -511,7 +521,7 @@ func cmdValidateConfig(fl Flags) (int, error) {
validateCmdConfigFlag := fl.String("config") validateCmdConfigFlag := fl.String("config")
validateCmdAdapterFlag := fl.String("adapter") validateCmdAdapterFlag := fl.String("adapter")
input, _, err := loadConfig(validateCmdConfigFlag, validateCmdAdapterFlag) input, _, err := LoadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} }
@@ -541,7 +551,7 @@ func cmdFmt(fl Flags) (int, error) {
// as a special case, read from stdin if the file name is "-" // as a special case, read from stdin if the file name is "-"
if formatCmdConfigFile == "-" { if formatCmdConfigFile == "-" {
input, err := ioutil.ReadAll(os.Stdin) input, err := io.ReadAll(os.Stdin)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading stdin: %v", err) fmt.Errorf("reading stdin: %v", err)
@@ -550,7 +560,7 @@ func cmdFmt(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
input, err := ioutil.ReadFile(formatCmdConfigFile) input, err := os.ReadFile(formatCmdConfigFile)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading input file: %v", err) fmt.Errorf("reading input file: %v", err)
@@ -559,8 +569,22 @@ func cmdFmt(fl Flags) (int, error) {
output := caddyfile.Format(input) output := caddyfile.Format(input)
if fl.Bool("overwrite") { if fl.Bool("overwrite") {
if err := ioutil.WriteFile(formatCmdConfigFile, output, 0600); err != nil { if err := os.WriteFile(formatCmdConfigFile, output, 0600); err != nil {
return caddy.ExitCodeFailedStartup, nil return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
}
} else if fl.Bool("diff") {
diff := difflib.Diff(
strings.Split(string(input), "\n"),
strings.Split(string(output), "\n"))
for _, d := range diff {
switch d.Delta {
case difflib.Common:
fmt.Printf(" %s\n", d.Payload)
case difflib.LeftOnly:
fmt.Printf("- %s\n", d.Payload)
case difflib.RightOnly:
fmt.Printf("+ %s\n", d.Payload)
}
} }
} else { } else {
fmt.Print(string(output)) fmt.Print(string(output))
@@ -633,27 +657,25 @@ commands:
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
// apiRequest makes an API request to the endpoint adminAddr with the // AdminAPIRequest makes an API request according to the CLI flags given,
// given HTTP method and request URI. If body is non-nil, it will be // with the given HTTP method and request URI. If body is non-nil, it will
// assumed to be Content-Type application/json. // be assumed to be Content-Type application/json. The caller should close
func apiRequest(adminAddr, method, uri string, headers http.Header, body io.Reader) error { // the response body. Should only be used by Caddy CLI commands which
// parse the admin address // need to interact with a running instance of Caddy via the admin API.
if adminAddr == "" { func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io.Reader) (*http.Response, error) {
adminAddr = caddy.DefaultAdminListen
}
parsedAddr, err := caddy.ParseNetworkAddress(adminAddr) parsedAddr, err := caddy.ParseNetworkAddress(adminAddr)
if err != nil || parsedAddr.PortRangeSize() > 1 { if err != nil || parsedAddr.PortRangeSize() > 1 {
return fmt.Errorf("invalid admin address %s: %v", adminAddr, err) return nil, fmt.Errorf("invalid admin address %s: %v", adminAddr, err)
} }
origin := parsedAddr.JoinHostPort(0) origin := "http://" + parsedAddr.JoinHostPort(0)
if parsedAddr.IsUnixNetwork() { if parsedAddr.IsUnixNetwork() {
origin = "unixsocket" // hack so that http.NewRequest() is happy origin = "http://unixsocket" // hack so that http.NewRequest() is happy
} }
// form the request // form the request
req, err := http.NewRequest(method, "http://"+origin+uri, body) req, err := http.NewRequest(method, origin+uri, body)
if err != nil { if err != nil {
return fmt.Errorf("making request: %v", err) return nil, fmt.Errorf("making request: %v", err)
} }
if parsedAddr.IsUnixNetwork() { if parsedAddr.IsUnixNetwork() {
// When listening on a unix socket, the admin endpoint doesn't // When listening on a unix socket, the admin endpoint doesn't
@@ -693,20 +715,60 @@ func apiRequest(adminAddr, method, uri string, headers http.Header, body io.Read
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return fmt.Errorf("performing request: %v", err) return nil, fmt.Errorf("performing request: %v", err)
} }
defer resp.Body.Close()
// if it didn't work, let the user know // if it didn't work, let the user know
if resp.StatusCode >= 400 { if resp.StatusCode >= 400 {
respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*10)) respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024*10))
if err != nil { if err != nil {
return fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err) return nil, fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
} }
return fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody) return nil, fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
} }
return nil return resp, nil
}
// DetermineAdminAPIAddress determines which admin API endpoint address should
// be used based on the inputs. By priority: if `address` is specified, then
// it is returned; if `configFile` (and `configAdapter`) are specified, then that
// config will be loaded to find the admin address; otherwise, the default
// admin listen address will be returned.
func DetermineAdminAPIAddress(address, configFile, configAdapter string) (string, error) {
// Prefer the address if specified and non-empty
if address != "" {
return address, nil
}
// Try to load the config from file if specified, with the given adapter name
if configFile != "" {
// get the config in caddy's native format
config, loadedConfigFile, err := LoadConfig(configFile, configAdapter)
if err != nil {
return "", err
}
if loadedConfigFile == "" {
return "", fmt.Errorf("no config file to load")
}
// get the address of the admin listener if set
if len(config) > 0 {
var tmpStruct struct {
Admin caddy.AdminConfig `json:"admin"`
}
err = json.Unmarshal(config, &tmpStruct)
if err != nil {
return "", fmt.Errorf("unmarshaling admin listener address from config: %v", err)
}
if tmpStruct.Admin.Listen != "" {
return tmpStruct.Admin.Listen, nil
}
}
}
// Fallback to the default listen address otherwise
return caddy.DefaultAdminListen, nil
} }
type moduleInfo struct { type moduleInfo struct {
+29 -4
View File
@@ -156,16 +156,19 @@ development environment.`,
RegisterCommand(Command{ RegisterCommand(Command{
Name: "stop", Name: "stop",
Func: cmdStop, Func: cmdStop,
Usage: "[--address <interface>] [--config <path> [--adapter <name>]]",
Short: "Gracefully stops a started Caddy process", Short: "Gracefully stops a started Caddy process",
Long: ` Long: `
Stops the background Caddy process as gracefully as possible. Stops the background Caddy process as gracefully as possible.
It requires that the admin API is enabled and accessible, since it will It requires that the admin API is enabled and accessible, since it will
use the API's /stop endpoint. The address of this request can be use the API's /stop endpoint. The address of this request can be customized
customized using the --address flag if it is not the default.`, using the --address flag, or from the given --config, if not the default.`,
Flags: func() *flag.FlagSet { Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("stop", flag.ExitOnError) fs := flag.NewFlagSet("stop", flag.ExitOnError)
fs.String("address", "", "The address to use to reach the admin API endpoint, if not the default") fs.String("address", "", "The address to use to reach the admin API endpoint, if not the default")
fs.String("config", "", "Configuration file to use to parse the admin address, if --address is not used")
fs.String("adapter", "", "Name of config adapter to apply (when --config is used)")
return fs return fs
}(), }(),
}) })
@@ -208,6 +211,7 @@ config file; otherwise the default is assumed.`,
fs := flag.NewFlagSet("list-modules", flag.ExitOnError) fs := flag.NewFlagSet("list-modules", flag.ExitOnError)
fs.Bool("packages", false, "Print package paths") fs.Bool("packages", false, "Print package paths")
fs.Bool("versions", false, "Print version information") fs.Bool("versions", false, "Print version information")
fs.Bool("skip-standard", false, "Skip printing standard modules")
return fs return fs
}(), }(),
}) })
@@ -259,7 +263,7 @@ Loads and provisions the provided config, but does not start running it.
This reveals any errors with the configuration through the loading and This reveals any errors with the configuration through the loading and
provisioning stages.`, provisioning stages.`,
Flags: func() *flag.FlagSet { Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("load", flag.ExitOnError) fs := flag.NewFlagSet("validate", flag.ExitOnError)
fs.String("config", "", "Input configuration file") fs.String("config", "", "Input configuration file")
fs.String("adapter", "", "Name of config adapter") fs.String("adapter", "", "Name of config adapter")
return fs return fs
@@ -278,12 +282,18 @@ human readability. It prints the result to stdout.
If --overwrite is specified, the output will be written to the config file If --overwrite is specified, the output will be written to the config file
directly instead of printing it. directly instead of printing it.
If --diff is specified, the output will be compared against the input, and
lines will be prefixed with '-' and '+' where they differ. Note that
unchanged lines are prefixed with two spaces for alignment, and that this
is not a valid patch format.
If you wish you use stdin instead of a regular file, use - as the path. If you wish you use stdin instead of a regular file, use - as the path.
When reading from stdin, the --overwrite flag has no effect: the result When reading from stdin, the --overwrite flag has no effect: the result
is always printed to stdout.`, is always printed to stdout.`,
Flags: func() *flag.FlagSet { Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("format", flag.ExitOnError) fs := flag.NewFlagSet("fmt", flag.ExitOnError)
fs.Bool("overwrite", false, "Overwrite the input file with the results") fs.Bool("overwrite", false, "Overwrite the input file with the results")
fs.Bool("diff", false, "Print the differences between the input file and the formatted output")
return fs return fs
}(), }(),
}) })
@@ -295,6 +305,11 @@ is always printed to stdout.`,
Long: ` Long: `
Downloads an updated Caddy binary with the same modules/plugins at the Downloads an updated Caddy binary with the same modules/plugins at the
latest versions. EXPERIMENTAL: May be changed or removed.`, latest versions. EXPERIMENTAL: May be changed or removed.`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("upgrade", flag.ExitOnError)
fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it")
return fs
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
@@ -307,6 +322,11 @@ Downloads an updated Caddy binary with the specified packages (module/plugin)
added. Retains existing packages. Returns an error if the any of packages are added. Retains existing packages. Returns an error if the any of packages are
already included. EXPERIMENTAL: May be changed or removed. already included. EXPERIMENTAL: May be changed or removed.
`, `,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("add-package", flag.ExitOnError)
fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it")
return fs
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
@@ -319,6 +339,11 @@ Downloads an updated Caddy binaries without the specified packages (module/plugi
Returns an error if any of the packages are not included. Returns an error if any of the packages are not included.
EXPERIMENTAL: May be changed or removed. EXPERIMENTAL: May be changed or removed.
`, `,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("remove-package", flag.ExitOnError)
fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it")
return fs
}(),
}) })
} }
+50 -25
View File
@@ -20,7 +20,6 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net" "net"
"os" "os"
@@ -94,7 +93,7 @@ func Main() {
// the bytes in expect, or returns an error if it doesn't. // the bytes in expect, or returns an error if it doesn't.
func handlePingbackConn(conn net.Conn, expect []byte) error { func handlePingbackConn(conn net.Conn, expect []byte) error {
defer conn.Close() defer conn.Close()
confirmationBytes, err := ioutil.ReadAll(io.LimitReader(conn, 32)) confirmationBytes, err := io.ReadAll(io.LimitReader(conn, 32))
if err != nil { if err != nil {
return err return err
} }
@@ -104,15 +103,15 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
return nil return nil
} }
// loadConfig loads the config from configFile and adapts it // LoadConfig loads the config from configFile and adapts it
// using adapterName. If adapterName is specified, configFile // using adapterName. If adapterName is specified, configFile
// must be also. If no configFile is specified, it tries // must be also. If no configFile is specified, it tries
// loading a default config file. The lack of a config file is // loading a default config file. The lack of a config file is
// not treated as an error, but false will be returned if // not treated as an error, but false will be returned if
// there is no config available. It prints any warnings to stderr, // there is no config available. It prints any warnings to stderr,
// and returns the resulting JSON config bytes along with // and returns the resulting JSON config bytes along with
// whether a config file was loaded or not. // the name of the loaded config file (if any).
func loadConfig(configFile, adapterName string) ([]byte, string, error) { func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
// specifying an adapter without a config file is ambiguous // specifying an adapter without a config file is ambiguous
if adapterName != "" && configFile == "" { if adapterName != "" && configFile == "" {
return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)") return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)")
@@ -124,9 +123,9 @@ func loadConfig(configFile, adapterName string) ([]byte, string, error) {
var err error var err error
if configFile != "" { if configFile != "" {
if configFile == "-" { if configFile == "-" {
config, err = ioutil.ReadAll(os.Stdin) config, err = io.ReadAll(os.Stdin)
} else { } else {
config, err = ioutil.ReadFile(configFile) config, err = os.ReadFile(configFile)
} }
if err != nil { if err != nil {
return nil, "", fmt.Errorf("reading config file: %v", err) return nil, "", fmt.Errorf("reading config file: %v", err)
@@ -140,7 +139,7 @@ func loadConfig(configFile, adapterName string) ([]byte, string, error) {
// plugged in, and if so, try using a default Caddyfile // plugged in, and if so, try using a default Caddyfile
cfgAdapter = caddyconfig.GetAdapter("caddyfile") cfgAdapter = caddyconfig.GetAdapter("caddyfile")
if cfgAdapter != nil { if cfgAdapter != nil {
config, err = ioutil.ReadFile("Caddyfile") config, err = os.ReadFile("Caddyfile")
if os.IsNotExist(err) { if os.IsNotExist(err) {
// okay, no default Caddyfile; pretend like this never happened // okay, no default Caddyfile; pretend like this never happened
cfgAdapter = nil cfgAdapter = nil
@@ -263,7 +262,7 @@ func watchConfigFile(filename, adapterName string) {
lastModified = info.ModTime() lastModified = info.ModTime()
// load the contents of the file // load the contents of the file
config, _, err := loadConfig(filename, adapterName) config, _, err := LoadConfig(filename, adapterName)
if err != nil { if err != nil {
logger().Error("unable to load latest config", zap.Error(err)) logger().Error("unable to load latest config", zap.Error(err))
continue continue
@@ -369,42 +368,68 @@ func loadEnvFromFile(envFile string) error {
return nil return nil
} }
// parseEnvFile parses an env file from KEY=VALUE format.
// It's pretty naive. Limited value quotation is supported,
// but variable and command expansions are not supported.
func parseEnvFile(envInput io.Reader) (map[string]string, error) { func parseEnvFile(envInput io.Reader) (map[string]string, error) {
envMap := make(map[string]string) envMap := make(map[string]string)
scanner := bufio.NewScanner(envInput) scanner := bufio.NewScanner(envInput)
var line string var lineNumber int
lineNumber := 0
for scanner.Scan() { for scanner.Scan() {
line = strings.TrimSpace(scanner.Text()) line := strings.TrimSpace(scanner.Text())
lineNumber++ lineNumber++
// skip lines starting with comment // skip empty lines and lines starting with comment
if strings.HasPrefix(line, "#") { if line == "" || strings.HasPrefix(line, "#") {
continue
}
// skip empty line
if len(line) == 0 {
continue continue
} }
// split line into key and value
fields := strings.SplitN(line, "=", 2) fields := strings.SplitN(line, "=", 2)
if len(fields) != 2 { if len(fields) != 2 {
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]
if strings.Contains(fields[0], " ") { // sometimes keys are prefixed by "export " so file can be sourced in bash; ignore it here
return nil, fmt.Errorf("bad key on line %d: contains whitespace", lineNumber) key = strings.TrimPrefix(key, "export ")
}
key := fields[0]
val := fields[1]
// validate key and value
if key == "" { if key == "" {
return nil, fmt.Errorf("missing or empty key on line %d", lineNumber) return nil, fmt.Errorf("missing or empty key on line %d", lineNumber)
} }
if strings.Contains(key, " ") {
return nil, fmt.Errorf("invalid key on line %d: contains whitespace: %s", lineNumber, key)
}
if strings.HasPrefix(val, " ") || strings.HasPrefix(val, "\t") {
return nil, fmt.Errorf("invalid value on line %d: whitespace before value: '%s'", lineNumber, val)
}
// remove any trailing comment after value
if commentStart := strings.Index(val, "#"); commentStart > 0 {
before := val[commentStart-1]
if before == '\t' || before == ' ' {
val = strings.TrimRight(val[:commentStart], " \t")
}
}
// quoted value: support newlines
if strings.HasPrefix(val, `"`) {
for !(strings.HasSuffix(line, `"`) && !strings.HasSuffix(line, `\"`)) {
val = strings.ReplaceAll(val, `\"`, `"`)
if !scanner.Scan() {
break
}
lineNumber++
line = strings.ReplaceAll(scanner.Text(), `\"`, `"`)
val += "\n" + line
}
val = strings.TrimPrefix(val, `"`)
val = strings.TrimSuffix(val, `"`)
}
envMap[key] = val envMap[key] = val
} }
+170
View File
@@ -0,0 +1,170 @@
package caddycmd
import (
"reflect"
"strings"
"testing"
)
func TestParseEnvFile(t *testing.T) {
for i, tc := range []struct {
input string
expect map[string]string
shouldErr bool
}{
{
input: `KEY=value`,
expect: map[string]string{
"KEY": "value",
},
},
{
input: `
KEY=value
OTHER_KEY=Some Value
`,
expect: map[string]string{
"KEY": "value",
"OTHER_KEY": "Some Value",
},
},
{
input: `
KEY=value
INVALID KEY=asdf
OTHER_KEY=Some Value
`,
shouldErr: true,
},
{
input: `
KEY=value
SIMPLE_QUOTED="quoted value"
OTHER_KEY=Some Value
`,
expect: map[string]string{
"KEY": "value",
"SIMPLE_QUOTED": "quoted value",
"OTHER_KEY": "Some Value",
},
},
{
input: `
KEY=value
NEWLINES="foo
bar"
OTHER_KEY=Some Value
`,
expect: map[string]string{
"KEY": "value",
"NEWLINES": "foo\n\tbar",
"OTHER_KEY": "Some Value",
},
},
{
input: `
KEY=value
ESCAPED="\"escaped quotes\"
here"
OTHER_KEY=Some Value
`,
expect: map[string]string{
"KEY": "value",
"ESCAPED": "\"escaped quotes\"\nhere",
"OTHER_KEY": "Some Value",
},
},
{
input: `
export KEY=value
OTHER_KEY=Some Value
`,
expect: map[string]string{
"KEY": "value",
"OTHER_KEY": "Some Value",
},
},
{
input: `
=value
OTHER_KEY=Some Value
`,
shouldErr: true,
},
{
input: `
EMPTY=
OTHER_KEY=Some Value
`,
expect: map[string]string{
"EMPTY": "",
"OTHER_KEY": "Some Value",
},
},
{
input: `
EMPTY=""
OTHER_KEY=Some Value
`,
expect: map[string]string{
"EMPTY": "",
"OTHER_KEY": "Some Value",
},
},
{
input: `
KEY=value
#OTHER_KEY=Some Value
`,
expect: map[string]string{
"KEY": "value",
},
},
{
input: `
KEY=value
COMMENT=foo bar # some comment here
OTHER_KEY=Some Value
`,
expect: map[string]string{
"KEY": "value",
"COMMENT": "foo bar",
"OTHER_KEY": "Some Value",
},
},
{
input: `
KEY=value
WHITESPACE= foo
OTHER_KEY=Some Value
`,
shouldErr: true,
},
{
input: `
KEY=value
WHITESPACE=" foo bar "
OTHER_KEY=Some Value
`,
expect: map[string]string{
"KEY": "value",
"WHITESPACE": " foo bar ",
"OTHER_KEY": "Some Value",
},
},
} {
actual, err := parseEnvFile(strings.NewReader(tc.input))
if err != nil && !tc.shouldErr {
t.Errorf("Test %d: Got error but shouldn't have: %v", i, err)
}
if err == nil && tc.shouldErr {
t.Errorf("Test %d: Did not get error but should have", i)
}
if tc.shouldErr {
continue
}
if !reflect.DeepEqual(tc.expect, actual) {
t.Errorf("Test %d: Expected %v but got %v", i, tc.expect, actual)
}
}
}
+16 -19
View File
@@ -31,7 +31,7 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
func cmdUpgrade(_ Flags) (int, error) { func cmdUpgrade(fl Flags) (int, error) {
_, nonstandard, _, err := getModules() _, nonstandard, _, err := getModules()
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
@@ -41,7 +41,7 @@ func cmdUpgrade(_ Flags) (int, error) {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} }
return upgradeBuild(pluginPkgs) return upgradeBuild(pluginPkgs, fl)
} }
func cmdAddPackage(fl Flags) (int, error) { func cmdAddPackage(fl Flags) (int, error) {
@@ -64,7 +64,7 @@ func cmdAddPackage(fl Flags) (int, error) {
pluginPkgs[arg] = struct{}{} pluginPkgs[arg] = struct{}{}
} }
return upgradeBuild(pluginPkgs) return upgradeBuild(pluginPkgs, fl)
} }
func cmdRemovePackage(fl Flags) (int, error) { func cmdRemovePackage(fl Flags) (int, error) {
@@ -88,10 +88,10 @@ func cmdRemovePackage(fl Flags) (int, error) {
delete(pluginPkgs, arg) delete(pluginPkgs, arg)
} }
return upgradeBuild(pluginPkgs) return upgradeBuild(pluginPkgs, fl)
} }
func upgradeBuild(pluginPkgs map[string]struct{}) (int, error) { func upgradeBuild(pluginPkgs map[string]struct{}, fl Flags) (int, error) {
l := caddy.Log() l := caddy.Log()
thisExecPath, err := os.Executable() thisExecPath, err := os.Executable()
@@ -152,18 +152,23 @@ func upgradeBuild(pluginPkgs map[string]struct{}) (int, error) {
// use the new binary to print out version and module info // use the new binary to print out version and module info
fmt.Print("\nModule versions:\n\n") fmt.Print("\nModule versions:\n\n")
if err = listModules(thisExecPath); err != nil { if err = listModules(thisExecPath); err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute 'caddy list-modules': %v", err)
} }
fmt.Println("\nVersion:") fmt.Println("\nVersion:")
if err = showVersion(thisExecPath); err != nil { if err = showVersion(thisExecPath); err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute 'caddy version': %v", err)
} }
fmt.Println() fmt.Println()
// clean up the backup file // clean up the backup file
if err = os.Remove(backupExecPath); err != nil { if !fl.Bool("keep-backup") {
if err = removeCaddyBinary(backupExecPath); err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to clean up backup binary: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to clean up backup binary: %v", err)
} }
} else {
l.Info("skipped cleaning up the backup file", zap.String("backup_path", backupExecPath))
}
l.Info("upgrade successful; please restart any running Caddy instances", zap.String("executable", thisExecPath)) l.Info("upgrade successful; please restart any running Caddy instances", zap.String("executable", thisExecPath))
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
@@ -220,25 +225,17 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
} }
func listModules(path string) error { func listModules(path string) error {
cmd := exec.Command(path, "list-modules", "--versions") cmd := exec.Command(path, "list-modules", "--versions", "--skip-standard")
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
err := cmd.Run() return cmd.Run()
if err != nil {
return fmt.Errorf("download succeeded, but unable to execute: %v", err)
}
return nil
} }
func showVersion(path string) error { func showVersion(path string) error {
cmd := exec.Command(path, "version") cmd := exec.Command(path, "version")
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
err := cmd.Run() return cmd.Run()
if err != nil {
return fmt.Errorf("download succeeded, but unable to execute: %v", err)
}
return nil
} }
func downloadBuild(qs url.Values) (*http.Response, error) { func downloadBuild(qs url.Values) (*http.Response, error) {
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !windows
// +build !windows
package caddycmd
import (
"os"
)
// removeCaddyBinary removes the Caddy binary at the given path.
//
// On any non-Windows OS, this simply calls os.Remove, since they should
// probably not exhibit any issue with processes deleting themselves.
func removeCaddyBinary(path string) error {
return os.Remove(path)
}
+36
View File
@@ -0,0 +1,36 @@
// 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 caddycmd
import (
"os"
"path/filepath"
"syscall"
)
// removeCaddyBinary removes the Caddy binary at the given path.
//
// On Windows, this uses a syscall to indirectly remove the file,
// because otherwise we get an "Access is denied." error when trying
// to delete the binary while Caddy is still running and performing
// the upgrade. "cmd.exe /C" executes a command specified by the
// following arguments, i.e. "del" which will run as a separate process,
// which avoids the "Access is denied." error.
func removeCaddyBinary(path string) error {
var sI syscall.StartupInfo
var pI syscall.ProcessInformation
argv := syscall.StringToUTF16Ptr(filepath.Join(os.Getenv("windir"), "system32", "cmd.exe") + " /C del " + path)
return syscall.CreateProcess(nil, argv, nil, nil, true, 0, nil, nil, &sI, &pI)
}
+11
View File
@@ -423,6 +423,17 @@ func (ctx Context) App(name string) (interface{}, error) {
return modVal, nil return modVal, nil
} }
// AppIsConfigured returns whether an app named name has been
// configured. Can be called before calling App() to avoid
// instantiating an empty app when that's not desirable.
func (ctx Context) AppIsConfigured(name string) bool {
if _, ok := ctx.cfg.apps[name]; ok {
return true
}
appRaw := ctx.cfg.AppsRaw[name]
return appRaw != nil
}
// Storage returns the configured Caddy storage implementation. // Storage returns the configured Caddy storage implementation.
func (ctx Context) Storage() certmagic.Storage { func (ctx Context) Storage() certmagic.Storage {
return ctx.cfg.storage return ctx.cfg.storage
+1
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build gofuzz
// +build gofuzz // +build gofuzz
package caddy package caddy
+122 -24
View File
@@ -1,35 +1,133 @@
module github.com/caddyserver/caddy/v2 module github.com/caddyserver/caddy/v2
go 1.16 go 1.17
require ( require (
github.com/BurntSushi/toml v1.1.0
github.com/Masterminds/sprig/v3 v3.2.2 github.com/Masterminds/sprig/v3 v3.2.2
github.com/alecthomas/chroma v0.9.2 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.14.5 github.com/caddyserver/certmagic v0.16.1
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.11.4
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/klauspost/compress v1.13.4 github.com/klauspost/compress v1.15.6
github.com/klauspost/cpuid/v2 v2.0.9 github.com/klauspost/cpuid/v2 v2.0.13
github.com/lucas-clemente/quic-go v0.23.0 github.com/lucas-clemente/quic-go v0.28.0
github.com/mholt/acmez v1.0.0 github.com/mholt/acmez v1.0.2
github.com/naoina/go-stringutil v0.1.0 // indirect github.com/prometheus/client_golang v1.12.1
github.com/naoina/toml v0.1.1 github.com/smallstep/certificates v0.19.0
github.com/prometheus/client_golang v1.11.0 github.com/smallstep/cli v0.18.0
github.com/smallstep/certificates v0.16.4 github.com/smallstep/nosql v0.4.0
github.com/smallstep/cli v0.16.1 github.com/smallstep/truststore v0.11.0
github.com/smallstep/nosql v0.3.8 github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2
github.com/smallstep/truststore v0.9.6 github.com/yuin/goldmark v1.4.12
github.com/yuin/goldmark v1.4.0 github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0
go.uber.org/zap v1.19.0 go.opentelemetry.io/otel v1.4.0
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.0
golang.org/x/net v0.0.0-20210614182718-04defd469f4e go.opentelemetry.io/otel/sdk v1.4.0
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 go.uber.org/zap v1.21.0
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08 golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2
google.golang.org/protobuf v1.27.1 golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21
google.golang.org/protobuf v1.28.0
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 (
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/felixge/httpsnoop v1.0.2 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-kit/kit v0.10.0 // indirect
github.com/go-logfmt/logfmt v0.5.0 // indirect
github.com/go-logr/logr v1.2.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.10.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.2.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.9.0 // indirect
github.com/jackc/pgx/v4 v4.14.0 // indirect
github.com/libdns/libdns v0.2.1 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/marten-seemann/qpack v0.2.1 // indirect
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/micromdm/scep/v2 v2.1.0 // indirect
github.com/miekg/dns v1.1.46 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/rs/xid v1.2.1 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/slackhq/nebula v1.5.2 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/urfave/cli v1.22.5 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.0 // indirect
go.opentelemetry.io/otel/internal/metric v0.27.0 // indirect
go.opentelemetry.io/otel/metric v0.27.0 // indirect
go.opentelemetry.io/otel/trace v1.4.0 // indirect
go.opentelemetry.io/proto/otlp v0.12.0 // indirect
go.step.sm/cli-utils v0.7.0 // indirect
go.step.sm/crypto v0.16.1 // indirect
go.step.sm/linkedca v0.15.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
golang.org/x/tools v0.1.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/grpc v1.46.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
howett.net/plist v1.0.0 // indirect
) )
+444 -123
View File
File diff suppressed because it is too large Load Diff
+39
View File
@@ -0,0 +1,39 @@
package metrics
import (
"net/http"
"strconv"
)
func SanitizeCode(s int) string {
switch s {
case 0, 200:
return "200"
default:
return strconv.Itoa(s)
}
}
// Only support the list of "regular" HTTP methods, see
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
var methodMap = map[string]string{
"GET": http.MethodGet, "get": http.MethodGet,
"HEAD": http.MethodHead, "head": http.MethodHead,
"PUT": http.MethodPut, "put": http.MethodPut,
"POST": http.MethodPost, "post": http.MethodPost,
"DELETE": http.MethodDelete, "delete": http.MethodDelete,
"CONNECT": http.MethodConnect, "connect": http.MethodConnect,
"OPTIONS": http.MethodOptions, "options": http.MethodOptions,
"TRACE": http.MethodTrace, "trace": http.MethodTrace,
"PATCH": http.MethodPatch, "patch": http.MethodPatch,
}
// SanitizeMethod sanitizes the method for use as a metric label. This helps
// prevent high cardinality on the method label. The name is always upper case.
func SanitizeMethod(m string) string {
if m, ok := methodMap[m]; ok {
return m
}
return "OTHER"
}
+28
View File
@@ -0,0 +1,28 @@
package metrics
import (
"strings"
"testing"
)
func TestSanitizeMethod(t *testing.T) {
tests := []struct {
method string
expected string
}{
{method: "get", expected: "GET"},
{method: "POST", expected: "POST"},
{method: "OPTIONS", expected: "OPTIONS"},
{method: "connect", expected: "CONNECT"},
{method: "trace", expected: "TRACE"},
{method: "UNKNOWN", expected: "OTHER"},
{method: strings.Repeat("ohno", 9999), expected: "OTHER"},
}
for _, d := range tests {
actual := SanitizeMethod(d.method)
if actual != d.expected {
t.Errorf("Not same: expected %#v, but got %#v", d.expected, actual)
}
}
}
+232 -167
View File
@@ -15,219 +15,225 @@
package caddy package caddy
import ( import (
"context"
"crypto/tls"
"errors"
"fmt" "fmt"
"log"
"net" "net"
"os"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"time" "time"
"github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/http3"
) )
// Listen returns a listener suitable for use in a Caddy module. // Listen is like net.Listen, except Caddy's listeners can overlap
// Always be sure to close listeners when you are done with them. // each other: multiple listeners may be created on the same socket
// at the same time. This is useful because during config changes,
// the new config is started while the old config is still running.
// When Caddy listeners are closed, the closing logic is virtualized
// so the underlying socket isn't actually closed until all uses of
// the socket have been finished. Always be sure to close listeners
// when you are done with them, just like normal listeners.
func Listen(network, addr string) (net.Listener, error) { func Listen(network, addr string) (net.Listener, error) {
lnKey := network + "/" + addr lnKey := network + "/" + addr
listenersMu.Lock() sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
defer listenersMu.Unlock()
// if listener already exists, increment usage counter, then return listener
if lnGlobal, ok := listeners[lnKey]; ok {
atomic.AddInt32(&lnGlobal.usage, 1)
return &fakeCloseListener{
usage: &lnGlobal.usage,
deadline: &lnGlobal.deadline,
deadlineMu: &lnGlobal.deadlineMu,
key: lnKey,
Listener: lnGlobal.ln,
}, nil
}
// or, create new one and save it
ln, err := net.Listen(network, addr) ln, err := net.Listen(network, addr)
if err != nil {
// https://github.com/caddyserver/caddy/pull/4534
if isUnixNetwork(network) && isListenBindAddressAlreadyInUseError(err) {
return nil, fmt.Errorf("%w: this can happen if Caddy was forcefully killed", err)
}
return nil, err
}
return &sharedListener{Listener: ln, key: lnKey}, nil
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
// make sure to start its usage counter at 1 return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener)}, nil
lnGlobal := &globalListener{usage: 1, ln: ln}
listeners[lnKey] = lnGlobal
return &fakeCloseListener{
usage: &lnGlobal.usage,
deadline: &lnGlobal.deadline,
deadlineMu: &lnGlobal.deadlineMu,
key: lnKey,
Listener: ln,
}, nil
} }
// ListenPacket returns a net.PacketConn suitable for use in a Caddy module. // ListenPacket returns a net.PacketConn suitable for use in a Caddy module.
// It is like Listen except for PacketConns.
// Always be sure to close the PacketConn when you are done. // Always be sure to close the PacketConn when you are done.
func ListenPacket(network, addr string) (net.PacketConn, error) { func ListenPacket(network, addr string) (net.PacketConn, error) {
lnKey := network + "/" + addr lnKey := network + "/" + addr
listenersMu.Lock() sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
defer listenersMu.Unlock()
// if listener already exists, increment usage counter, then return listener
if lnGlobal, ok := listeners[lnKey]; ok {
atomic.AddInt32(&lnGlobal.usage, 1)
log.Printf("[DEBUG] %s: Usage counter should not go above 2 or maybe 3, is now: %d", lnKey, atomic.LoadInt32(&lnGlobal.usage)) // TODO: remove
return &fakeClosePacketConn{usage: &lnGlobal.usage, key: lnKey, PacketConn: lnGlobal.pc}, nil
}
// or, create new one and save it
pc, err := net.ListenPacket(network, addr) pc, err := net.ListenPacket(network, addr)
if err != nil {
// https://github.com/caddyserver/caddy/pull/4534
if isUnixNetwork(network) && isListenBindAddressAlreadyInUseError(err) {
return nil, fmt.Errorf("%w: this can happen if Caddy was forcefully killed", err)
}
return nil, err
}
return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
// make sure to start its usage counter at 1 return &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}, nil
lnGlobal := &globalListener{usage: 1, pc: pc}
listeners[lnKey] = lnGlobal
return &fakeClosePacketConn{usage: &lnGlobal.usage, key: lnKey, PacketConn: pc}, nil
} }
// fakeCloseListener's Close() method is a no-op. This allows // ListenQUIC returns a quic.EarlyListener suitable for use in a Caddy module.
// stopping servers that are using the listener without giving // Note that the context passed to Accept is currently ignored, so using
// up the socket; thus, servers become hot-swappable while the // a context other than context.Background is meaningless.
// listener remains running. Listeners should be re-wrapped in func ListenQUIC(addr string, tlsConf *tls.Config) (quic.EarlyListener, error) {
// a new fakeCloseListener each time the listener is reused. lnKey := "quic/" + addr
// Other than the 'closed' field (which pertains to this value
// only), the other fields in this struct should be pointers to sharedEl, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
// the associated globalListener's struct fields (except 'key' el, err := quic.ListenAddrEarly(addr, http3.ConfigureTLSConfig(tlsConf), &quic.Config{})
// which is there for read-only purposes, so it can be a copy). if err != nil {
return nil, err
}
return &sharedQuicListener{EarlyListener: el, key: lnKey}, nil
})
ctx, cancel := context.WithCancel(context.Background())
return &fakeCloseQuicListener{
sharedQuicListener: sharedEl.(*sharedQuicListener),
context: ctx, contextCancel: cancel,
}, err
}
// 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 { type fakeCloseListener struct {
closed int32 // accessed atomically; belongs to this struct only closed int32 // accessed atomically; belongs to this struct only
usage *int32 // accessed atomically; global *sharedListener // embedded, so we also become a net.Listener
deadline *bool // protected by deadlineMu; global
deadlineMu *sync.Mutex // global
key string // global, but read-only, so can be copy
net.Listener // global
} }
// Accept accepts connections until Close() is called.
func (fcl *fakeCloseListener) Accept() (net.Conn, error) { func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
// if the listener is already "closed", return error // if the listener is already "closed", return error
if atomic.LoadInt32(&fcl.closed) == 1 { if atomic.LoadInt32(&fcl.closed) == 1 {
return nil, fcl.fakeClosedErr() return nil, fakeClosedErr(fcl)
} }
// wrap underlying accept // call underlying accept
conn, err := fcl.Listener.Accept() conn, err := fcl.sharedListener.Accept()
if err == nil { if err == nil {
return conn, nil return conn, nil
} }
// accept returned with error // since Accept() returned an error, it may be because our reference to
// TODO: This may be better as a condition variable so the deadline is cleared only once? // the listener (this fakeCloseListener) may have been closed, i.e. the
fcl.deadlineMu.Lock() // server is shutting down; in that case, we need to clear the deadline
if *fcl.deadline { // that we set when Close() was called, and return a non-temporary and
switch ln := fcl.Listener.(type) { // non-timeout error value to the caller, masking the "true" error, so
case *net.TCPListener: // that server loops / goroutines won't retry, linger, and leak
_ = ln.SetDeadline(time.Time{})
case *net.UnixListener:
_ = ln.SetDeadline(time.Time{})
}
*fcl.deadline = false
}
fcl.deadlineMu.Unlock()
if atomic.LoadInt32(&fcl.closed) == 1 { if atomic.LoadInt32(&fcl.closed) == 1 {
// if we canceled the Accept() by setting a deadline // we dereference the sharedListener explicitly even though it's embedded
// on the listener, we need to make sure any callers of // so that it's clear in the code that side-effects are shared with other
// Accept() think the listener was actually closed; // users of this listener, not just our own reference to it; we also don't
// if we return the timeout error instead, callers might // do anything with the error because all we could do is log it, but we
// simply retry, leaking goroutines for longer // 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() { if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
return nil, fcl.fakeClosedErr() return nil, fakeClosedErr(fcl)
} }
} }
return nil, err return nil, err
} }
// Close stops accepting new connections without // Close stops accepting new connections without closing the
// closing the underlying listener, unless no one // underlying listener. The underlying listener is only closed
// else is using it. // if the caller is the last known user of the socket.
func (fcl *fakeCloseListener) Close() error { func (fcl *fakeCloseListener) Close() error {
if atomic.CompareAndSwapInt32(&fcl.closed, 0, 1) { if atomic.CompareAndSwapInt32(&fcl.closed, 0, 1) {
// unfortunately, there is no way to cancel any // There are two ways I know of to get an Accept()
// currently-blocking calls to Accept() that are // function to return to the server loop that called
// awaiting connections since we're not actually // it: close the listener, or set a deadline in the
// closing the listener; so we cheat by setting // past. Obviously, we can't close the socket yet
// a deadline in the past, which forces it to // since others may be using it (hence this whole
// time out; note that this only works for // file). But we can set the deadline in the past,
// certain types of listeners... // and this is kind of cheating, but it works, and
fcl.deadlineMu.Lock() // it apparently even works on Windows.
if !*fcl.deadline { _ = fcl.sharedListener.setDeadline()
switch ln := fcl.Listener.(type) { _, _ = listenerPool.Delete(fcl.sharedListener.key)
case *net.TCPListener:
_ = ln.SetDeadline(time.Now().Add(-1 * time.Minute))
case *net.UnixListener:
_ = ln.SetDeadline(time.Now().Add(-1 * time.Minute))
} }
*fcl.deadline = true
}
fcl.deadlineMu.Unlock()
// since we're no longer using this listener,
// decrement the usage counter and, if no one
// else is using it, close underlying listener
if atomic.AddInt32(fcl.usage, -1) == 0 {
listenersMu.Lock()
delete(listeners, fcl.key)
listenersMu.Unlock()
err := fcl.Listener.Close()
if err != nil {
return err
}
}
}
return nil return nil
} }
func (fcl *fakeCloseListener) fakeClosedErr() error { type fakeCloseQuicListener struct {
closed int32 // accessed atomically; belongs to this struct only
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
context context.Context
contextCancel context.CancelFunc
}
// Currently Accept ignores the passed context, however a situation where
// someone would need a hotswappable QUIC-only (not http3, since it uses context.Background here)
// server on which Accept would be called with non-empty contexts
// (mind that the default net listeners' Accept doesn't take a context argument)
// sounds way too rare for us to sacrifice efficiency here.
func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (quic.EarlyConnection, error) {
conn, err := fcql.sharedQuicListener.Accept(fcql.context)
if err == nil {
return conn, nil
}
// if the listener is "closed", return a fake closed error instead
if 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
// nor a timeout, suitable for making the caller think the
// listener is actually closed
func fakeClosedErr(l interface{ Addr() net.Addr }) error {
return &net.OpError{ return &net.OpError{
Op: "accept", Op: "accept",
Net: fcl.Listener.Addr().Network(), Net: l.Addr().Network(),
Addr: fcl.Listener.Addr(), Addr: l.Addr(),
Err: errFakeClosed, Err: errFakeClosed,
} }
} }
// ErrFakeClosed is the underlying error value returned by
// fakeCloseListener.Accept() after Close() has been called,
// indicating that it is pretending to be closed so that the
// server using it can terminate, while the underlying
// socket is actually left open.
var errFakeClosed = fmt.Errorf("listener 'closed' 😉")
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns.
type fakeClosePacketConn struct { type fakeClosePacketConn struct {
closed int32 // accessed atomically closed int32 // accessed atomically; belongs to this struct only
usage *int32 // accessed atomically *sharedPacketConn // embedded, so we also become a net.PacketConn
key string
net.PacketConn
} }
func (fcpc *fakeClosePacketConn) Close() error { func (fcpc *fakeClosePacketConn) Close() error {
log.Println("[DEBUG] Fake-closing underlying packet conn") // TODO: remove this
if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) { if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
// since we're no longer using this listener, _, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
// decrement the usage counter and, if no one
// else is using it, close underlying listener
if atomic.AddInt32(fcpc.usage, -1) == 0 {
listenersMu.Lock()
delete(listeners, fcpc.key)
listenersMu.Unlock()
err := fcpc.PacketConn.Close()
if err != nil {
return err
} }
}
}
return nil return nil
} }
@@ -249,28 +255,75 @@ 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)
} }
// ErrFakeClosed is the underlying error value returned by // sharedListener is a wrapper over an underlying listener. The listener
// fakeCloseListener.Accept() after Close() has been called, // and the other fields on the struct are shared state that is synchronized,
// indicating that it is pretending to be closed so that the // so sharedListener structs must never be copied (always use a pointer).
// server using it can terminate, while the underlying type sharedListener struct {
// socket is actually left open. net.Listener
var errFakeClosed = fmt.Errorf("listener 'closed' 😉") key string // uniquely identifies this listener
deadline bool // whether a deadline is currently set
// globalListener keeps global state for a listener
// that may be shared by multiple servers. In other
// words, values in this struct exist only once and
// all other uses of these values point to the ones
// in this struct. In particular, the usage count
// (how many callers are using the listener), the
// actual listener, and synchronization of the
// listener's deadline changes are singular, global
// values that must not be copied.
type globalListener struct {
usage int32 // accessed atomically
deadline bool
deadlineMu sync.Mutex deadlineMu sync.Mutex
ln net.Listener }
pc net.PacketConn
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{})
case *net.UnixListener:
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)
case *net.UnixListener:
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()
}
// sharedQuicListener is like sharedListener, but for quic.EarlyListeners.
type sharedQuicListener struct {
quic.EarlyListener
key string
}
// Destruct closes the underlying QUIC listener.
func (sql *sharedQuicListener) Destruct() error {
return sql.EarlyListener.Close()
}
// sharedPacketConn is like sharedListener, but for net.PacketConns.
type sharedPacketConn struct {
net.PacketConn
key string
}
// Destruct closes the underlying socket.
func (spc *sharedPacketConn) Destruct() error {
return spc.PacketConn.Close()
} }
// NetworkAddress contains the individual components // NetworkAddress contains the individual components
@@ -349,6 +402,20 @@ func isUnixNetwork(netw string) bool {
return netw == "unix" || netw == "unixgram" || netw == "unixpacket" return netw == "unix" || netw == "unixgram" || netw == "unixpacket"
} }
func isListenBindAddressAlreadyInUseError(err error) bool {
switch networkOperationError := err.(type) {
case *net.OpError:
switch syscallError := networkOperationError.Err.(type) {
case *os.SyscallError:
if syscallError.Syscall == "bind" {
return true
}
}
}
return false
}
// ParseNetworkAddress parses addr into its individual // ParseNetworkAddress parses addr into its individual
// components. The input string is expected to be of // components. The input string is expected to be of
// the form "network/host:port-range" where any part is // the form "network/host:port-range" where any part is
@@ -445,10 +512,8 @@ type ListenerWrapper interface {
WrapListener(net.Listener) net.Listener WrapListener(net.Listener) net.Listener
} }
var ( // listenerPool stores and allows reuse of active listeners.
listeners = make(map[string]*globalListener) var listenerPool = NewUsagePool()
listenersMu sync.Mutex
)
const maxPortSpan = 65535 const maxPortSpan = 65535
+1
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build gofuzz
// +build gofuzz // +build gofuzz
package caddy package caddy
+9 -4
View File
@@ -18,7 +18,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"os" "os"
"strings" "strings"
@@ -630,9 +629,9 @@ func (StderrWriter) OpenWriter() (io.WriteCloser, error) {
return notClosable{os.Stderr}, nil return notClosable{os.Stderr}, nil
} }
// OpenWriter returns ioutil.Discard that can't be closed. // OpenWriter returns io.Discard that can't be closed.
func (DiscardWriter) OpenWriter() (io.WriteCloser, error) { func (DiscardWriter) OpenWriter() (io.WriteCloser, error) {
return notClosable{ioutil.Discard}, nil return notClosable{io.Discard}, nil
} }
// notClosable is an io.WriteCloser that can't be closed. // notClosable is an io.WriteCloser that can't be closed.
@@ -662,9 +661,15 @@ func newDefaultProductionLog() (*defaultCustomLog, error) {
cl.buildCore() cl.buildCore()
logger := zap.New(cl.core)
// capture logs from other libraries which
// may not be using zap logging directly
_ = zap.RedirectStdLog(logger)
return &defaultCustomLog{ return &defaultCustomLog{
CustomLog: cl, CustomLog: cl,
logger: zap.New(cl.core), logger: logger,
}, nil }, nil
} }
+3 -13
View File
@@ -2,9 +2,8 @@ package caddy
import ( import (
"net/http" "net/http"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2/internal/metrics"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
@@ -46,8 +45,8 @@ func instrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler)
d := newDelegator(w) d := newDelegator(w)
next.ServeHTTP(d, r) next.ServeHTTP(d, r)
counter.With(prometheus.Labels{ counter.With(prometheus.Labels{
"code": sanitizeCode(d.status), "code": metrics.SanitizeCode(d.status),
"method": strings.ToUpper(r.Method), "method": metrics.SanitizeMethod(r.Method),
}).Inc() }).Inc()
}) })
} }
@@ -67,12 +66,3 @@ func (d *delegator) WriteHeader(code int) {
d.status = code d.status = code
d.ResponseWriter.WriteHeader(code) d.ResponseWriter.WriteHeader(code)
} }
func sanitizeCode(s int) string {
switch s {
case 0, 200:
return "200"
default:
return strconv.Itoa(s)
}
}
+7 -27
View File
@@ -18,7 +18,6 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"net"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
@@ -50,6 +49,8 @@ func init() {
// `{http.request.body}` | The request body (⚠️ inefficient; use only for debugging) // `{http.request.body}` | The request body (⚠️ inefficient; use only for debugging)
// `{http.request.cookie.*}` | HTTP request cookie // `{http.request.cookie.*}` | HTTP request cookie
// `{http.request.duration}` | Time up to now spent handling the request (after decoding headers from client) // `{http.request.duration}` | Time up to now spent handling the request (after decoding headers from client)
// `{http.request.duration_ms}` | Same as 'duration', but in milliseconds.
// `{http.request.uuid}` | The request unique identifier
// `{http.request.header.*}` | Specific request header field // `{http.request.header.*}` | Specific request header field
// `{http.request.host.labels.*}` | Request host labels (0-based from right); e.g. for foo.example.com: 0=com, 1=example, 2=foo // `{http.request.host.labels.*}` | Request host labels (0-based from right); e.g. for foo.example.com: 0=com, 1=example, 2=foo
// `{http.request.host}` | The host part of the request's Host header // `{http.request.host}` | The host part of the request's Host header
@@ -77,6 +78,7 @@ func init() {
// `{http.request.tls.client.public_key}` | The public key of the client certificate. // `{http.request.tls.client.public_key}` | The public key of the client certificate.
// `{http.request.tls.client.public_key_sha256}` | The SHA256 checksum of the client's public key. // `{http.request.tls.client.public_key_sha256}` | The SHA256 checksum of the client's public key.
// `{http.request.tls.client.certificate_pem}` | The PEM-encoded value of the certificate. // `{http.request.tls.client.certificate_pem}` | The PEM-encoded value of the certificate.
// `{http.request.tls.client.certificate_der_base64}` | The base64-encoded value of the certificate.
// `{http.request.tls.client.issuer}` | The issuer DN of the client certificate // `{http.request.tls.client.issuer}` | The issuer DN of the client certificate
// `{http.request.tls.client.serial}` | The serial number of the client certificate // `{http.request.tls.client.serial}` | The serial number of the client certificate
// `{http.request.tls.client.subject}` | The subject DN of the client certificate // `{http.request.tls.client.subject}` | The subject DN of the client certificate
@@ -116,7 +118,6 @@ type App struct {
servers []*http.Server servers []*http.Server
h3servers []*http3.Server h3servers []*http3.Server
h3listeners []net.PacketConn
ctx caddy.Context ctx caddy.Context
logger *zap.Logger logger *zap.Logger
@@ -351,22 +352,19 @@ func (app *App) Start() error {
app.logger.Info("enabling experimental HTTP/3 listener", app.logger.Info("enabling experimental HTTP/3 listener",
zap.String("addr", hostport), zap.String("addr", hostport),
) )
h3ln, err := caddy.ListenPacket("udp", hostport) h3ln, err := caddy.ListenQUIC(hostport, tlsCfg)
if err != nil { if err != nil {
return fmt.Errorf("getting HTTP/3 UDP listener: %v", err) return fmt.Errorf("getting HTTP/3 QUIC listener: %v", err)
} }
h3srv := &http3.Server{ h3srv := &http3.Server{
Server: &http.Server{
Addr: hostport, Addr: hostport,
Handler: srv, Handler: srv,
TLSConfig: tlsCfg, TLSConfig: tlsCfg,
ErrorLog: serverLogger, MaxHeaderBytes: srv.MaxHeaderBytes,
},
} }
//nolint:errcheck //nolint:errcheck
go h3srv.Serve(h3ln) go h3srv.ServeListener(h3ln)
app.h3servers = append(app.h3servers, h3srv) app.h3servers = append(app.h3servers, h3srv)
app.h3listeners = append(app.h3listeners, h3ln)
srv.h3server = h3srv srv.h3server = h3srv
} }
///////// /////////
@@ -424,13 +422,6 @@ func (app *App) Stop() error {
} }
} }
// close the http3 servers; it's unclear whether the bug reported in
// https://github.com/caddyserver/caddy/pull/2727#issuecomment-526856566
// was ever truly fixed, since it seemed racey/nondeterministic; but
// recent tests in 2020 were unable to replicate the issue again after
// repeated attempts (the bug manifested after a config reload; i.e.
// reusing a http3 server or listener was problematic), but it seems
// to be working fine now
for _, s := range app.h3servers { for _, s := range app.h3servers {
// TODO: CloseGracefully, once implemented upstream // TODO: CloseGracefully, once implemented upstream
// (see https://github.com/lucas-clemente/quic-go/issues/2103) // (see https://github.com/lucas-clemente/quic-go/issues/2103)
@@ -439,17 +430,6 @@ func (app *App) Stop() error {
return err return err
} }
} }
// closing an http3.Server does not close their underlying listeners
// since apparently the listener can be used both by servers and
// clients at the same time; so we need to manually call Close()
// on the underlying h3 listeners (see lucas-clemente/quic-go#2103)
for _, pc := range app.h3listeners {
err := pc.Close()
if err != nil {
return err
}
}
return nil return nil
} }
+71 -14
View File
@@ -31,13 +31,20 @@ import (
// HTTPS is enabled automatically and by default when // HTTPS is enabled automatically and by default when
// qualifying hostnames are available from the config. // qualifying hostnames are available from the config.
type AutoHTTPSConfig struct { type AutoHTTPSConfig struct {
// If true, automatic HTTPS will be entirely disabled. // If true, automatic HTTPS will be entirely disabled,
// including certificate management and redirects.
Disabled bool `json:"disable,omitempty"` Disabled bool `json:"disable,omitempty"`
// If true, only automatic HTTP->HTTPS redirects will // If true, only automatic HTTP->HTTPS redirects will
// be disabled. // be disabled, but other auto-HTTPS features will
// remain enabled.
DisableRedir bool `json:"disable_redirects,omitempty"` DisableRedir bool `json:"disable_redirects,omitempty"`
// If true, automatic certificate management will be
// disabled, but other auto-HTTPS features will
// remain enabled.
DisableCerts bool `json:"disable_certificates,omitempty"`
// Hosts/domain names listed here will not be included // Hosts/domain names listed here will not be included
// in automatic HTTPS (they will not have certificates // in automatic HTTPS (they will not have certificates
// loaded nor redirects applied). // loaded nor redirects applied).
@@ -104,12 +111,13 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
srv.AutoHTTPS = new(AutoHTTPSConfig) srv.AutoHTTPS = new(AutoHTTPSConfig)
} }
if srv.AutoHTTPS.Disabled { if srv.AutoHTTPS.Disabled {
app.logger.Warn("automatic HTTPS is completely disabled for server", zap.String("server_name", srvName))
continue continue
} }
// skip if all listeners use the HTTP port // skip if all listeners use the HTTP port
if !srv.listenersUseAnyPortOtherThan(app.httpPort()) { if !srv.listenersUseAnyPortOtherThan(app.httpPort()) {
app.logger.Info("server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server", app.logger.Warn("server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server",
zap.String("server_name", srvName), zap.String("server_name", srvName),
zap.Int("http_port", app.httpPort()), zap.Int("http_port", app.httpPort()),
) )
@@ -166,7 +174,16 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// 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)
if srv.AutoHTTPS.DisableCerts {
app.logger.Warn("skipping automated certificate management for server because it is disabled", zap.String("server_name", srvName))
} 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,
@@ -192,6 +209,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
uniqueDomainsForCerts[d] = struct{}{} uniqueDomainsForCerts[d] = struct{}{}
} }
} }
}
// tell the server to use TLS if it is not already doing so // tell the server to use TLS if it is not already doing so
if srv.TLSConnPolicies == nil { if srv.TLSConnPolicies == nil {
@@ -200,19 +218,22 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// nothing left to do if auto redirects are disabled // nothing left to do if auto redirects are disabled
if srv.AutoHTTPS.DisableRedir { if srv.AutoHTTPS.DisableRedir {
app.logger.Warn("automatic HTTP->HTTPS redirects are disabled", zap.String("server_name", srvName))
continue continue
} }
app.logger.Info("enabling automatic HTTP->HTTPS redirects", app.logger.Info("enabling automatic HTTP->HTTPS redirects", zap.String("server_name", srvName))
zap.String("server_name", srvName),
)
// create HTTP->HTTPS redirects // create HTTP->HTTPS redirects
for _, addr := range srv.Listen { for _, listenAddr := range srv.Listen {
// figure out the address we will redirect to... // figure out the address we will redirect to...
addr, err := caddy.ParseNetworkAddress(addr) addr, err := caddy.ParseNetworkAddress(listenAddr)
if err != nil { if err != nil {
return fmt.Errorf("%s: invalid listener address: %v", srvName, addr) msg := "%s: invalid listener address: %v"
if strings.Count(listenAddr, ":") > 1 {
msg = msg + ", there are too many colons, so the port is ambiguous. Did you mean to wrap the IPv6 address with [] brackets?"
}
return fmt.Errorf(msg, srvName, listenAddr)
} }
// this address might not have a hostname, i.e. might be a // this address might not have a hostname, i.e. might be a
@@ -232,7 +253,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// port, we'll have to choose one, so prefer the HTTPS port // port, we'll have to choose one, so prefer the HTTPS port
if _, ok := redirDomains[d]; !ok || if _, ok := redirDomains[d]; !ok ||
addr.StartPort == uint(app.httpsPort()) { addr.StartPort == uint(app.httpsPort()) {
redirDomains[d] = append(redirDomains[d], addr) redirDomains[d] = []caddy.NetworkAddress{addr}
} }
} }
} }
@@ -418,7 +439,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
@@ -459,6 +480,22 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri
} }
} }
// if no external managers were configured, enable
// implicit Tailscale support for convenience
if ap.Managers == nil {
ts, err := implicitTailscale(ctx)
if err != nil {
return err
}
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?
if !foundBasePolicy && len(ap.Subjects) == 0 { if !foundBasePolicy && len(ap.Subjects) == 0 {
basePolicy = ap basePolicy = ap
@@ -467,10 +504,19 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri
} }
if basePolicy == nil { if basePolicy == nil {
// no base policy found, we will make one! // no base policy found; we will make one
basePolicy = new(caddytls.AutomationPolicy) basePolicy = new(caddytls.AutomationPolicy)
} }
if basePolicy.Managers == nil {
// add implicit Tailscale integration, for harmless convenience
ts, err := implicitTailscale(ctx)
if err != nil {
return err
}
basePolicy.Managers = []certmagic.Manager{ts}
}
// if the basePolicy has an existing ACMEIssuer (particularly to // if the basePolicy has an existing ACMEIssuer (particularly to
// include any type that embeds/wraps an ACMEIssuer), let's use it // include any type that embeds/wraps an ACMEIssuer), let's use it
// (I guess we just use the first one?), otherwise we'll make one // (I guess we just use the first one?), otherwise we'll make one
@@ -482,8 +528,8 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri
} }
} }
if baseACMEIssuer == nil { if baseACMEIssuer == nil {
// note that this happens if basePolicy.Issuer is nil // note that this happens if basePolicy.Issuers is empty
// OR if it is not nil but is not an ACMEIssuer // OR if it is not empty but does not have not an ACMEIssuer
baseACMEIssuer = new(caddytls.ACMEIssuer) baseACMEIssuer = new(caddytls.ACMEIssuer)
} }
@@ -653,4 +699,15 @@ func (app *App) automaticHTTPSPhase2() error {
return nil return nil
} }
// implicitTailscale returns a new and provisioned Tailscale module configured to be optional.
func implicitTailscale(ctx caddy.Context) (caddytls.Tailscale, error) {
ts := caddytls.Tailscale{Optional: true}
err := ts.Provision(ctx)
return ts, err
}
func isTailscaleDomain(name string) bool {
return strings.HasSuffix(strings.ToLower(name), ".ts.net")
}
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer } type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
+485 -13
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"
@@ -28,11 +29,16 @@ import (
"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/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" "google.golang.org/protobuf/proto"
) )
@@ -62,6 +68,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 +91,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(m)
// 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,6 +101,29 @@ 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.Declarations(
@@ -102,6 +135,7 @@ func (m *MatchExpression) Provision(_ caddy.Context) error {
), ),
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,7 +143,7 @@ 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())
} }
@@ -121,6 +155,7 @@ func (m *MatchExpression) Provision(_ caddy.Context) error {
// compile the "program" // compile the "program"
m.prg, err = env.Program(checked, m.prg, err = env.Program(checked,
cel.EvalOptions(cel.OptOptimize),
cel.Functions( cel.Functions(
&functions.Overload{ &functions.Overload{
Operator: placeholderFuncName, Operator: placeholderFuncName,
@@ -128,7 +163,6 @@ func (m *MatchExpression) Provision(_ caddy.Context) error {
}, },
), ),
) )
if err != nil { if err != nil {
return fmt.Errorf("compiling CEL program: %s", err) return fmt.Errorf("compiling CEL program: %s", err)
} }
@@ -137,20 +171,27 @@ 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.
func (m *MatchExpression) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { func (m *MatchExpression) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() { for d.Next() {
m.Expr = strings.Join(d.RemainingArgs(), " ") if d.CountRemainingArgs() > 1 {
m.Expr = strings.Join(d.RemainingArgsRaw(), " ")
} else {
m.Expr = d.Val()
}
} }
return nil return nil
} }
@@ -162,13 +203,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)
@@ -180,10 +223,23 @@ 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) ResolveName(name string) (interface{}, 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) (interface{}, error) { func (cr celHTTPRequest) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
return cr.Request, nil return cr.Request, nil
} }
@@ -237,12 +293,428 @@ 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 []*exprpb.Type, fac CELMatcherFactory) (cel.Library, error) {
requestType := decls.NewObjectType("http.Request")
var macro parser.Macro
switch len(matcherDataTypes) {
case 1:
matcherDataType := matcherDataTypes[0]
if isCELStringListType(matcherDataType) {
macro = parser.NewGlobalVarArgMacro(macroName, celMatcherStringListMacroExpander(funcName))
} else if isCELStringType(matcherDataType) {
macro = parser.NewGlobalMacro(macroName, 1, celMatcherStringMacroExpander(funcName))
} else if isCELJSONType(matcherDataType) {
macro = parser.NewGlobalMacro(macroName, 1, celMatcherJSONMacroExpander(funcName))
} else {
return nil, fmt.Errorf("unsupported matcher data type: %s", cel.FormatType(matcherDataType))
}
case 2:
if isCELStringType(matcherDataTypes[0]) && isCELStringType(matcherDataTypes[1]) {
macro = parser.NewGlobalMacro(macroName, 2, celMatcherStringListMacroExpander(funcName))
matcherDataTypes = []*exprpb.Type{CelTypeListString}
} else {
return nil, fmt.Errorf(
"unsupported matcher data type: %s, %s",
cel.FormatType(matcherDataTypes[0]), cel.FormatType(matcherDataTypes[1]),
)
}
case 3:
if isCELStringType(matcherDataTypes[0]) && isCELStringType(matcherDataTypes[1]) && isCELStringType(matcherDataTypes[2]) {
macro = parser.NewGlobalMacro(macroName, 3, celMatcherStringListMacroExpander(funcName))
matcherDataTypes = []*exprpb.Type{CelTypeListString}
} else {
return nil, fmt.Errorf(
"unsupported matcher data type: %s, %s, %s",
cel.FormatType(matcherDataTypes[0]), cel.FormatType(matcherDataTypes[1]), cel.FormatType(matcherDataTypes[2]),
)
}
}
envOptions := []cel.EnvOption{
cel.Macros(macro),
cel.Declarations(
decls.NewFunction(funcName,
decls.NewOverload(
funcName,
append([]*exprpb.Type{requestType}, matcherDataTypes...),
decls.Bool,
),
),
),
}
programOptions := []cel.ProgramOption{
cel.CustomDecorator(CELMatcherDecorator(funcName, fac)),
cel.Functions(
&functions.Overload{
Operator: funcName,
Binary: CELMatcherRuntimeFunction(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]interface{}.
// 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]interface{}{})
mapStrRaw, err := data.ConvertToNative(mapStrType)
if err != nil {
return nil, err
}
mapStrIface := mapStrRaw.(map[string]interface{})
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
}
// isCELJSONType returns whether the type corresponds to JSON input.
func isCELJSONType(t *exprpb.Type) bool {
switch t.GetTypeKind().(type) {
case *exprpb.Type_MapType_:
mapType := t.GetMapType()
return isCELStringType(mapType.GetKeyType()) && mapType.GetValueType().GetDyn() != nil
}
return false
}
// isCELStringType returns whether the type corresponds to a string.
func isCELStringType(t *exprpb.Type) bool {
switch t.GetTypeKind().(type) {
case *exprpb.Type_Primitive:
return t.GetPrimitive() == exprpb.Type_STRING
}
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
}
// isCELStringListType returns whether the type corresponds to a list of strings.
func isCELStringListType(t *exprpb.Type) bool {
switch t.GetTypeKind().(type) {
case *exprpb.Type_ListType_:
return isCELStringType(t.GetListType().GetElemType())
}
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}")`
CelTypeListString = decls.NewListType(decls.String)
CelTypeJson = decls.NewMapType(decls.String, decls.Dyn)
) )
var httpRequestObjectType = decls.NewObjectType("http.Request") var httpRequestObjectType = decls.NewObjectType("http.Request")

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