Compare commits

..

908 Commits

Author SHA1 Message Date
Mohammed Al Sahaf 15faeacb60 cmd: fix auto-detetction of .caddyfile extension (#6356)
* cmd: fix auto-detetction of .caddyfile extension

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* move conditions around and add clarifying comment

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* reject ambiguous config file name

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

---------

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2024-06-02 03:49:38 +00:00
Will Norris f8a2c60297 caddyhttp: properly sanitize requests for root path (#6360)
SanitizePathJoin protects against directory traversal attacks by
checking for requests whose URL path look like they are trying to
request something other than a local file, and returns the root
directory in those cases.

The method is also careful to ensure that requests which contain a
trailing slash include a trailing slash in the returned value.  However,
for requests that contain only a slash (requests for the root path), the
IsLocal check returns early before the matching trailing slash is
re-added.

This change updates SanitizePathJoin to only perform the
filepath.IsLocal check if the cleaned request URL path is non-empty.

---

This change also updates the existing SanitizePathJoin tests to use
filepath.FromSlash rather than filepath.Join. This makes the expected
value a little easier to read, but also has the advantage of not being
processed by filepath.Clean like filepath.Join is. This means that the
exact expect value will be compared, not the result of first cleaning
it.

Fixes #6352
2024-06-02 03:40:59 +00:00
Matthew Holt 01308b4bae I'm so tired of typos 2024-06-01 20:43:35 -06:00
Matthew Holt b7280e6949 caddytls: Implement certmagic.RenewalInfoGetter
Fixes ARI errors reported here:
https://caddy.community/t/error-in-logs-with-updating-ari-after-upgrading-to-caddy-v2-8-1/24320
2024-06-01 18:02:49 -06:00
dependabot[bot] a63767d3f8 build(deps): bump golangci/golangci-lint-action from 5 to 6 (#6361)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 5 to 6.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-02 02:26:31 +03:00
Francis Lavoie 40c582ce82 caddyhttp: Fix merging consecutive client_ip or remote_ip matchers (#6350) 2024-05-30 07:32:17 -06:00
Anton Kovalenko a52917a37d core: MkdirAll appDataDir in InstanceID with 0o700 (#6340)
appDataDir components should be searchable (u+x) when they are
created, or else Caddy is unable to start with an empty HOME.
2024-05-30 10:38:09 +00:00
Ranveer Avhad e6f46c8d78 acmeserver: Add sign_with_root for Caddyfile (#6345)
* Added sign_with_root option available in the Caddyfile

* Added tests for sign_with_root to validate the adapted JSON config
2024-05-27 20:06:54 -04:00
Francis Lavoie f6d2c293e7 caddyfile: Reject global request matchers earlier (#6339) 2024-05-23 20:06:16 -06:00
Matthew Holt 2ce5c65269 core: Fix bug in AppIfConfigured (fix #6336) 2024-05-22 18:47:03 -06:00
a 61917c3443 fix a typo (#6333) 2024-05-21 18:41:41 -04:00
Francis Lavoie 224316eaec autohttps: Move log WARN to INFO, reduce confusion (#6185)
* autohttps: Move log WARN to INFO, reduce confusion

* Change implicit condition back to WARN

---------

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2024-05-20 13:14:39 -06:00
Matt Holt 5f6758dab5 reverseproxy: Support HTTP/3 transport to backend (#6312)
Closes #5086
2024-05-20 13:06:43 -06:00
Francis Lavoie a6a45ff6c5 context: AppIfConfigured returns error; consider not-yet-provisioned modules (#6292)
* context: Add new `AppStrict()` method to avoid instantiating empty apps

* Rename AppStrict -> AppIfConfigured

---------

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2024-05-20 11:14:58 -06:00
Matthew Holt 73e094e1dd Fix lint error about deprecated method in smallstep/certificates/authority 2024-05-20 10:56:25 -06:00
Matthew Holt d79c0f0dec go.mod: Upgrade dependencies 2024-05-20 10:35:27 -06:00
Will Norris db3e19b7b5 caddytls: fix permission requirement with AutomationPolicy (#6328)
Certificate automation has permission modules that are designed to
prevent inappropriate issuance of unbounded or wildcard certificates.
When an explicit cert manager is used, no additional permission should
be necessary. For example, this should be a valid caddyfile:

    https:// {
      tls {
        get_certificate tailscale
      }
      respond OK
    }

This is accomplished when provisioning an AutomationPolicy by tracking
whether there were explicit managers configured directly on the policy
(in the ManagersRaw field). Only when a number of potentially unsafe
conditions are present AND no explicit cert managers are configured is
an error returned.

The problem arises from the fact that ctx.LoadModule deletes the raw
bytes after loading in order to save memory. The first time an
AutomationPolicy is provisioned, the ManagersRaw field is populated, and
everything is fine.

An AutomationPolicy with no subjects is treated as a special "catch-all"
policy. App.createAutomationPolicies ensures that this catch-all policy
has an ACME issuer, and then calls its Provision method again because it
may have changed. This second time Provision is called, ManagesRaw is no
longer populated, and the permission check fails because it appears as
though the policy has no explicit managers.

Address this by storing a new boolean on AutomationPolicy recording
whether it had explicit cert managers configured on it.

Also fix an inverted boolean check on this value when setting
failClosed.

Updates #6060
Updates #6229
Updates #6327

Signed-off-by: Will Norris <will@tailscale.com>
2024-05-20 09:48:59 -06:00
Will Norris 1fc151faec caddytls: remove ClientHelloSNICtxKey (#6326) 2024-05-18 22:47:46 -04:00
Matt Holt 9ba999141b caddyhttp: Trace individual middleware handlers (#6313)
* caddyhttp: Trace individual middleware handlers

* Fix typo
2024-05-18 14:48:42 -06:00
deneb f98f449f05 templates: Add pathEscape template function and use it in file browser (#6278)
* use url.PathEscape in file-server browse template

- add `pathEscape` to c.tpl.Funcs, using `url.PathEscape`
- use `pathEscape` in browse.html in place of `replace`

* document `pathEscape`

* Remove unnecessary pipe of img src to `html`
2024-05-18 12:55:36 -06:00
Will Norris e66040a6f0 caddytls: set server name in context (#6324)
Set the requested server name in a context value for CertGetter
implementations to use. Pass ctx to tscert.GetCertificateWithContext.

Signed-off-by: Will Norris <will@tailscale.com>
2024-05-18 03:52:19 -06:00
Mohammed Al Sahaf 44860482d2 chore: downgrade minimum Go version in go.mod (#6318)
* chore: downgrade minimum Go version in go.mod

* Upgrade certmagic and zerossl

---------

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2024-05-15 19:28:34 +00:00
Mohammed Al Sahaf 4c90f1427f caddytest: normalize the JSON config (#6316)
* caddytest: normalize the JSON config
2024-05-14 07:50:14 +00:00
Kévin Dunglas fb63e2e40c caddyhttp: New experimental handler for intercepting responses (#6232)
* feat: add generic response interceptors

* fix: cs

* rename intercept

* add some docs

* @francislavoie review (first round)

* Update modules/caddyhttp/intercept/intercept.go

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

* shorthands: ir to resp

* mark exported symbols as experimental

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-05-13 17:38:18 +00:00
Matthew Holt 583c585c81 httpcaddyfile: Set challenge ports when http_port or https_port are used 2024-05-11 21:39:56 -06:00
Aziz Rmadi 4356635d12 logging: Add support for additional logger filters other than hostname (#6082)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-05-11 13:31:44 +00:00
Matthew Holt 4af38e5ac8 caddyhttp: Log 4xx as INFO; 5xx as ERROR (close #6106) 2024-05-10 15:52:50 -06:00
Matthew Holt 399186abfc Second half of 6dce493
Not sure how it got unstaged
2024-05-10 15:51:28 -06:00
Matthew Holt 6dce4934f0 caddyhttp: Alter log message when request is unhandled (close #5182) 2024-05-10 15:49:34 -06:00
Francis Lavoie 874d0ce822 chore: Bump Go version in CI (#6310) 2024-05-10 14:56:18 +00:00
Matthew Holt abdf1ae15c go.mod: go 1.22.3
Seeing if this assists with some Go tooling logic
2024-05-10 08:32:44 -06:00
Viktor Szépe d7e3a1974b Fix typos (#6311)
* Fix typos

* Revert

* Revert to "htlm"

* fix indentations
2024-05-10 08:08:54 -06:00
WeidiDeng e60148ecc3 reverseproxy: Pointer to struct when loading modules; remove LazyCertPool (#6307)
* use pointer when loading modules

* change method to pointer type and remove LazyCertPool

* remove lazy pool test

* remove yet another lazy pool test
2024-05-08 19:13:37 -06:00
Matthew Penner 0b5720faa5 tracing: add trace_id var (http.vars.trace_id placeholder) (#6308) 2024-05-08 16:40:40 -06:00
Matthew Holt dd203ad41f go.mod: CertMagic v0.21.0 2024-05-07 10:17:10 -06:00
Ali Asgar b2b29dcd49 reverseproxy: Implement health_follow_redirects (#6302)
* added health_follow_redirect in active health checks

* chore: code format

* chore: refactore reversproxy healthcheck redirect variable name and description of the same

* chore: formatting

* changed reverse proxy health check status code range to be between 200-299

* chore: formatting

---------

Co-authored-by: aliasgar <joancena1268@mail.com>
2024-05-07 08:40:15 -06:00
Florian Apolloner c97292b255 caddypki: Allow use of root CA without a key. Fixes #6290 (#6298)
* Allow usage of root CA without a key. Fixes #6290

* Update modules/caddypki/crypto.go

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-05-07 03:38:26 +00:00
Matthew Holt b52271061d go.mod: Upgrade to quic-go v0.43.1 2024-05-06 20:15:43 -06:00
Mohammed Al Sahaf d05d715a00 reverseproxy: HTTP transport: fix PROXY protocol initialization (#6301) 2024-05-06 20:02:12 -06:00
Matthew Holt 8d7ac18402 caddytls: Ability to drop connections (close #6294) 2024-05-06 19:59:42 -06:00
dependabot[bot] 7e2510ef43 build(deps): bump golangci/golangci-lint-action from 4 to 5 (#6289)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 4 to 5.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-02 03:09:19 +03:00
Francis Lavoie feeb6af403 httpcaddyfile: Fix expression matcher shortcut in snippets (#6288) 2024-05-01 07:43:05 -04:00
Matt Holt d129ae6aec caddytls: Evict internal certs from cache based on issuer (#6266)
* caddytls: Evict internal certs from cache based on issuer

During a config reload, we would keep certs in the cache fi they were used  by the next config. If one config uses InternalIssuer and the other uses a public CA, this behavior is problematic / unintuitive, because there is a big difference between private/public CAs.

This change should ensure that internal issuers are considered when deciding whether to keep or evict from the cache during a reload, by making them distinct from each other and certs from public CAs.

* Make sure new TLS app manages configured certs

* Actually make it work
2024-04-30 16:15:54 -06:00
Mohammed Al Sahaf 87c7127c28 chore: add warn logs when using deprecated fields (#6276) 2024-04-27 15:51:00 -04:00
Matthew Holt 2fc620d38d caddyhttp: Fix linter warning about deprecation 2024-04-27 12:41:17 -06:00
Matthew Holt a46ff50a1c go.mod: Upgrade to quic-go v0.43.0 2024-04-27 12:01:30 -06:00
Matthew Holt cabb5d71c4 fileserver: Set "Vary: Accept-Encoding" header (see #5849) 2024-04-26 19:38:45 -06:00
Matthew Holt ba5811467a events: Add debug log 2024-04-26 18:59:08 -06:00
WeidiDeng 1b9042bcdd reverseproxy: handle buffered data during hijack (#6274) 2024-04-26 09:09:18 -06:00
Mohammed Al Sahaf 4d6370bf92 ci: remove android and plan9 from cross-build workflow (#6268) 2024-04-24 17:31:40 -04:00
Mohammed Al Sahaf c6eb186064 run golangci-lint run --fix --fast (#6270) 2024-04-24 15:17:23 -06:00
clauverjat 76c4cf5a56 caddytls: Option to configure certificate lifetime (#6253)
* Add option to configure certificate lifetime

* Bump CertMagic dep to latest master commit

* Apply suggestions and ran go mod tidy

* Update modules/caddytls/acmeissuer.go

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

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-04-24 14:35:14 -06:00
Francis Lavoie 797973944f replacer: Implement file.* global replacements (#5463)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2024-04-24 16:26:18 -04:00
Matt Holt 6d97d8d87b caddyhttp: Address some Go 1.20 features (#6252)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-04-24 00:05:57 +00:00
Matthew Holt d404005339 Quell linter (false positive) 2024-04-23 11:55:37 -06:00
Aziz Rmadi 868af6a062 reverse_proxy: Add grace_period for SRV upstreams to Caddyfile (#6264) 2024-04-23 07:12:57 -06:00
Mohammed Al Sahaf d2668cdbb0 doc: add verifier in ClientAuthentication caddyfile marshaler doc (#6263) 2024-04-23 07:01:54 -06:00
Matthew Holt 6a02999054 caddytls: Add Caddyfile support for on-demand permission module (close #6260) 2024-04-22 15:47:09 -06:00
Matthew Holt 9f97df2275 reverseproxy: Remove long-deprecated buffering properties
They've been deprecated for over a year and we printed warnings during that time.
2024-04-22 15:34:14 -06:00
Matthew Holt d93e027e01 reverseproxy: Reuse buffered request body even if partially drained
Previous commit only works when the backends don't read any of the body first.
2024-04-22 15:22:50 -06:00
Matthew Holt 613d544a47 reverseproxy: Accept EOF when buffering
Before this change, a read of size (let's say) < 10, into a buffer of size 10, will return EOF because we're using CopyN to limit to the size of the buffer. That resulted in the body being read from later, which should only happen if it couldn't fit in the buffer.

With this change, the body is properly NOT set when it can all fit in the buffer.
2024-04-22 13:12:10 -06:00
Francis Lavoie 726a9a8fde logging: Fix default access logger (#6251)
* logging: Fix default access logger

* Simplify logic, remove retry without port, reject config with port, docs

* Nil check
2024-04-22 06:33:07 -06:00
Matthew Holt d00824f4a6 fileserver: Improve Vary handling (#5849) 2024-04-19 13:43:13 -06:00
Mohammed Al Sahaf 8f87c5d993 cmd: Only validate config is proper JSON if config slice has data (#6250)
* cmd: fix error when running without config

* ci: add smoke test
2024-04-18 15:40:12 -06:00
Mohammed Al Sahaf c6673ad4d8 staticresp: Use the evaluated response body for sniffing JSON content-type (#6249) 2024-04-18 20:31:00 +00:00
Matthew Holt 9ab09433de encode: Slight fix for the previous commit 2024-04-17 19:59:10 -06:00
Matthew Holt 3067074d9c encode: Improve Etag handling (fix #5849)
We also improve Last-Modified handling in the file server.
Both changes should be more compliant with RFC 9110.
2024-04-17 19:12:03 -06:00
Matthew Holt 3efda6fb3a httpcaddyfile: Skip automate loader if disable_certs is specified (fix #6148) 2024-04-17 12:26:03 -06:00
Francis Lavoie 9cd472c031 caddyfile: Populate regexp matcher names by default (#6145)
* caddyfile: Populate regexp matcher names by default

* Some lint cleanup that my VSCode complained about

* Pass down matcher name through expression matcher

* Compat with #6113: fix adapt test, set both styles in replacer
2024-04-17 12:19:14 -06:00
WeidiDeng e0daa39cd3 caddyhttp: record num. bytes read when response writer is hijacked (#6173)
* record the number of bytes read when response writer is hijacked

* record body size when not nil
2024-04-17 15:00:37 +00:00
Francis Lavoie 70953e873a caddyhttp: Support multiple logger names per host (#6088)
* caddyhttp: Support multiple logger names per host

* Lint

* Add adapt test

* Implement "string or array" parsing, keep original `logger_names`

* Rewrite adapter test to be more representative of the usecase
2024-04-16 22:26:18 +00:00
coderwander eafc875ea9 chore: fix some typos in comments (#6243) 2024-04-16 04:10:11 +00:00
dev-polymer 03e0a010d1 encode: Configurable compression level for zstd (#6140)
* Add zstd compression level support

* Refactored zstd levels to string arguments

fastest, default, better, best

* Add comment with list of all available levels

* Corrected data types for config

---------

Co-authored-by: Evgeny Blinov <e.a.blinov@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-04-16 00:21:52 +00:00
Aziz Rmadi 3609a4af75 caddytls: Remove shim code supporting deprecated lego-dns (#6231)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-04-15 21:26:56 +00:00
Mohammed Al Sahaf 26748d06b4 connection policy: add local_ip matcher (#6074)
* connection policy: add `local_ip`

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

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-04-15 21:13:24 +03:00
WeidiDeng b40cacf5ce reverseproxy: Wait for both ends of websocket to close (#6175) 2024-04-15 11:37:37 -06:00
Matt Holt 81413caea2 caddytls: Upgrade ACMEz to v2; support ZeroSSL API; various fixes (#6229)
* WIP: acmez v2, CertMagic, and ZeroSSL issuer upgrades

* caddytls: ZeroSSLIssuer now uses ZeroSSL API instead of ACME

* Fix go.mod

* caddytls: Fix automation related to managers (fix #6060)

* Fix typo (appease linter)

* Fix HTTP validation with ZeroSSL API
2024-04-13 21:31:43 -04:00
Matthew Holt dc9dd2e4b3 caddytls: Still provision permission module if ask is specified
Only needed for JSON configs, and only temporarily as the ask property is deprecated and will be removed.
2024-04-13 17:08:11 -06:00
Aziz Rmadi 567d96c624 fileserver: read etags from precomputed files (#6222) 2024-04-13 06:49:55 -04:00
Matthew Holt 5d8b45c9fb fileserver: Escape # and ? in img src (fix #6237) 2024-04-12 15:59:59 -06:00
Aziz Rmadi 0b381eb766 reverseproxy: Implement modular CA provider for TLS transport (#6065)
* added new modular ca providers to caddy tls HttpTransport

* reverse-proxy, httptransport: added tests and caddyfile support for ca module

---------

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2024-04-12 07:19:14 -06:00
Matthew Holt 83ef61de10 caddyhttp: Apply auto HTTPS redir to all interfaces (fix #6226) 2024-04-12 06:04:47 -06:00
Matthew Holt e1f4b83ffa cmd: Fix panic related to config filename (fix #5919) 2024-04-11 17:04:43 -06:00
Omar Hussein 185ed6fe7c cmd: Assume Caddyfile based on filename prefix and suffix (#5919)
This can be helpful if editors only consider file extensions for certain features.

* added special case support for caddyfile suffix, case insensitive

* Update cmd/main.go

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

* skip caddyfile adapter for registered file extensions

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-04-11 15:28:16 -06:00
Hayder 4a0492f3e1 admin: Make Etag a header, not a trailer (#6208)
* Making eTags a header not a trailer

* Checked the write

* Fixed typo

* Corrected comment

* Added sync Pool

* Changed control flow of buffer reset / putting and changed error code

* Switched from interface{} to any in bufferPool
2024-04-11 21:19:24 +00:00
Hugues Lismonde 654a3bb090 caddyhttp: remove duplicate strings.Count in path matcher (fixes #6233) (#6234) 2024-04-10 08:38:10 -06:00
danish-mehmood f4840cfeb8 caddyconfig: Use empty struct instead of bool in map (close #6224) (#6227) 2024-04-08 17:12:35 -06:00
Ed Pelc a4a64a6f6e gitignore: Add rule for caddyfile.go (#6225) 2024-04-07 02:30:00 +00:00
Hassan Ila 88d65967b5 chore: Fix broken links in README.md (#6223) 2024-04-05 23:48:43 -04:00
Francis Lavoie 1c4a807667 chore: Upgrade some dependencies (#6221) 2024-04-04 18:27:52 -04:00
kylosus 45132c5b24 caddyhttp: Add plaintext response to file_server browse (#6093)
* Added plaintext support to file_server browser

This commit is twofold: First it adds a new optional
field, `return_type`, to `browser` for setting the
default format of the returned index (html, json or plaintext).
This is used when the `Accept` header is set to `/*`.

Second, it adds a preliminary `text/plain`
support to the `file_server` browser that
returns a text representation of the file
system, when an `Accept: text/plain` header
is present, with the behavior discussed above.

* Added more details and better formatting to plaintext browser

* Replaced returnType conditions with a switch statement

* Simplify

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-04-01 18:12:40 +00:00
Hayder 1217449609 admin: Use xxhash for etag (#6207) 2024-03-30 07:24:50 -06:00
reallylowest e0bf179c1a modules: fix some typo in conments (#6206)
Signed-off-by: reallylowest <sunjinping@outlook.com>
2024-03-30 02:45:42 +00:00
Matthew Holt 7b48ce0e7e caddyhttp: Replace sensitive headers with REDACTED (close #5669) 2024-03-29 14:42:20 -06:00
WeidiDeng 924010cd3d caddyhttp: close quic connections when server closes (#6202)
* close quic connections when server closes

* fix lint

* add comment about CloseGracefully
2024-03-29 11:51:46 -06:00
Hayder 74949fb091 reverseproxy: Use xxhash instead of fnv32 for LB (#6203)
* Added Faster Non-cryptographic Hash Function for Load Balancing

* Ran golangci-lint

* Updated hash version and hash return type
2024-03-29 10:56:18 -06:00
Emily ddb1d2c2b1 caddyhttp: add http.request.local{,.host,.port} placeholder (#6182)
* caddyhttp: add `http.request.local{,.host,.port}` placeholder

This is the counterpart of `http.request.remote{,.host,.port}`.

`http.request.remote` operates on the remote client's address, while
`http.request.local` operates on the address the connection arrived on.

Take the following example:

- Caddy serving on `203.0.113.1:80`
- Client on `203.0.113.2`

`http.request.remote.host` would return `203.0.113.2` (client IP)

`http.request.local.host` would return `203.0.113.1` (server IP)
`http.request.local.port` would return `80` (server port)

I find this helpful for debugging setups with multiple servers and/or
multiple network paths (multiple IPs, AnyIP, Anycast).

Co-authored-by: networkException <git@nwex.de>

* caddyhttp: add unit test for `http.request.local{,.host,.port}`

* caddyhttp: add integration test for `http.request.local.port`

* caddyhttp: fix `http.request.local.host` placeholder handling with unix sockets

The implementation matches the one of `http.request.remote.host` now and
returns the unix socket path (just like `http.request.local` already did)
instead of an empty string.

---------

Co-authored-by: networkException <git@nwex.de>
2024-03-27 21:36:53 +00:00
Mohammed Al Sahaf 7f227b9d39 chore: upgrade deps (#6198) 2024-03-27 14:24:18 -04:00
sellskin 0dd0487eba chore: remove repetitive word (#6193)
Signed-off-by: sellskin <mydesk@yeah.net>
2024-03-25 09:05:45 -06:00
Aziz Rmadi db9d167354 Added a null check to avoid segfault on rewrite query ops (#6191) 2024-03-23 01:51:34 -04:00
Aziz Rmadi 29f57faa86 rewrite: uri query replace operation (#6165)
* Implemented query replace oeration

* Modified replace operation to use regexes in caddyfile

* Added more tests to uri query operations
2024-03-22 02:23:42 +00:00
Mohammed Al Sahaf 0c01547037 logging: support ms duration format and add docs (#6187) 2024-03-21 22:17:09 -04:00
Mohammed Al Sahaf e7336cc3bf replacer: use RWMutex to protect static provider (#6184) 2024-03-21 18:15:18 +00:00
Francis Lavoie 97a56d860a caddyhttp: Allow header replacement with empty string (#6163) 2024-03-21 17:29:32 +00:00
Francis Lavoie d13258423d vars: Make nil values act as empty string instead of "<nil>" (#6174) 2024-03-21 11:21:53 -06:00
Marten Seemann 32f7dd44ae chore: Update quic-go to v0.42.0 (#6176)
* update quic-go to v0.42.0

* use a rate limiter to control QUIC source address verification

* Lint

* remove deprecated ListenQUIC

* remove number of requests tracking

* increase the number of handshakes before source address verification is needed

* remove references to request counters

* remove deprecated listen*

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: WeidiDeng <weidi_deng@icloud.com>
2024-03-21 10:56:10 -06:00
Francis Lavoie 63d597c09d caddyhttp: Accept XFF header values with ports, when parsing client IP (#6183) 2024-03-21 10:54:25 -06:00
Sam Ottenhoff e65b97f55b reverseproxy: configurable active health_passes and health_fails (#6154)
* reverseproxy: active health check allows configurable health_passes and health_fails

* Need to reset counters after recovery

* rename methods to be more clear that these are coming from active health checks

* do not export methods
2024-03-20 11:13:35 -06:00
Justin Angel a9768d2fde reverseproxy: Configurable forward proxy URL (#6114)
Co-authored-by: WeidiDeng <weidi_deng@icloud.com>
2024-03-18 04:07:25 +00:00
jbrown-stripe 52822a41cb caddyhttp: upgrade to cel v0.20.0 (#6161)
* upgrade to cel v0.20.0

* Attempt to address feedback and fix linter

* Let's try this

* Take that, you linter!

* Oh there's more

---------


Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Tristan Swadell @TristonianJones
2024-03-13 21:32:42 -06:00
Francis Lavoie 5b5f8feaf7 chore: Bump Chroma to v2.13.0, includes new Caddyfile lexer (#6169) 2024-03-12 12:07:23 +03:00
WeidiDeng c93e30454f caddyhttp: suppress flushing if the response is being buffered (#6150)
* suppress flushing if the response is being buffered

* fix lint

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-03-11 20:03:20 +00:00
WeidiDeng 1bd598e90c chore: encode: use FlushError instead of Flush (#6168)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-03-10 23:04:35 -04:00
WeidiDeng e698ec5139 encode: write status immediately when status code is informational (#6164) 2024-03-10 10:49:49 -04:00
Steffen Busch c27425ef5d httpcaddyfile: Keep deprecated skip_log in directive order (#6153) 2024-03-07 14:34:01 -05:00
Francis Lavoie 258d906140 httpcaddyfile: Add RegisterDirectiveOrder function for plugin authors (#5865)
* httpcaddyfile: Add `RegisterDirectiveOrder` function for plugin authors

* Set up Positional enum

* Linter doesn't like a switch on an enum with default

* Update caddyconfig/httpcaddyfile/directives.go

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

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-03-06 19:41:45 +00:00
Aziz Rmadi 69290d232d rewrite: Implement uri query operations (#6120)
* Implemented basic uri query operations

* Added support for query operations block

* Applied Replacer on all query keys and values

* Implemented rename query key opration

* Rewrite struct: Changed QueryOperations field to Query and comments cleanup

* Cleaned up comments, changed the order of operations and added more tests

* Changed order of fields in queryOps struct to match the operations order
2024-03-06 10:08:46 -05:00
huajin tong 277472d081 fix struct names (#6151)
Signed-off-by: thirdkeyword <fliterdashen@gmail.com>
2024-03-06 13:53:03 +00:00
Francis Lavoie 5a4374bea0 fileserver: Preserve query during canonicalization redirect (#6109)
* fileserver: Preserve query during canonicalization redirect

* Clarify that only a path should be passed
2024-03-05 22:51:26 -07:00
Francis Lavoie 0d44e3ecba logging: Implement log_append handler (#6066)
* logging: Implement `extra_log` handler

* Rename to `log_append`

* Rename `skip_log` to `log_skip`

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-03-05 17:03:59 -07:00
Francis Lavoie 2a78c9c5e4 httpcaddyfile: Allow nameless regexp placeholder shorthand (#6113)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-03-05 23:37:14 +00:00
Francis Lavoie 01d5568b20 logging: Implement append encoder, allow flatter filters config (#6069)
* logging: Implement `add` encoder

* Allow flatter config structure for `filter` & `add`

* Rename to append

* govulncheck was unhappy
2024-03-05 16:24:32 -07:00
Mohammed Al Sahaf 1f4a6fa7e7 ci: fix the integration test TestLeafCertLoaders (#6149) 2024-03-06 02:09:13 +03:00
Francis Lavoie 5ed8689629 vars: Allow overriding http.auth.user.id in replacer as a special case (#6108) 2024-03-05 22:25:38 +00:00
Aziz Rmadi 3ae07a73dc caddytls: clientauth: leaf verifier: make trusted leaf certs source pluggable (#6050)
* Made trusted leaf certificates pluggable into the tls.client_auth.leaf
module

* Added leaf loaders modules: file, folder, pem aand storage

* Cleaned implementation of leaf cert loader modules

* Added tests for leaf certs file and folder loaders

* cmd: fix the output of the `Usage` section (#6138)

* core: OnExit hooks (#6128)

* core: OnExit callbacks

* core: Process-global OnExit callbacks

* ci: bump golangci/golangci-lint-action from 3 to 4 (#6141)

Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3 to 4.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Added more leaf certificate loaders tests and cleaned up code

* Modified leaf cert loaders json field names and cleaned up storage loader comment

* Update modules/caddytls/leaffileloader.go

* Update LeafStorageLoader certificates field name

* Upgraded  protobuf version

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-05 14:55:37 -07:00
Francis Lavoie e473ae6803 cmd: Adjust config load logs/errors (#6032)
* cmd: Adjust config load logs/errors

* Update cmd/main.go

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

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-03-05 19:26:30 +00:00
Matt Holt 72ce78d9af reverseproxy: SRV dynamic upstream failover (#5832)
* Implement grace period, but probably needs sync

* Update cached freshness value

* D'oh, actually use the grace period

* Fix freshness math
2024-03-05 12:08:31 -07:00
dependabot[bot] 8f8204708a ci: bump golangci/golangci-lint-action from 3 to 4 (#6141)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3 to 4.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-02 02:38:57 +03:00
Matt Holt 46c5db92da core: OnExit hooks (#6128)
* core: OnExit callbacks

* core: Process-global OnExit callbacks
2024-03-01 09:57:05 -07:00
Mohammed Al Sahaf de4959fe7b cmd: fix the output of the Usage section (#6138) 2024-03-01 19:00:29 +03:00
Mohammed Al Sahaf 03f703a00e caddytls: verifier: caddyfile: re-add Caddyfile support (#6127)
* caddytls: verifier: caddyfile: re-add Caddyfile support

* appease the linter

* caddytls: client_auth: verifier: change namespace to `tls.client_auth.verifier`
2024-02-26 00:13:48 +03:00
Mohammed Al Sahaf 931656bd68 acmeserver: add policy field to define allow/deny rules (#5796)
* acmeserver: support specifying the allowed challenge types

* add caddyfile adapt tests

* acmeserver: add `policy` field to define allow/deny rules

* allow `omitempty` to work

* add caddyfile support for `policy`

* remove "uri domain" policy

* fmt the files

* add docs

* do not support `CommonName`; the field is deprecated

* r/DNSDomains/Domains/g

* Caddyfile docs

* add tests

* move `Policy` to top of file
2024-02-24 02:26:00 +03:00
Sam Ottenhoff da6a569e85 reverseproxy: cookie should be Secure and SameSite=None when TLS (#6115)
* reverseproxy: cookie should be Secure and SameSite=None when TLS

* Update modules/caddyhttp/reverseproxy/selectionpolicies_test.go

Co-authored-by: Mohammed Al Sahaf <mohammed@caffeinatedwonders.com>

---------

Co-authored-by: Mohammed Al Sahaf <mohammed@caffeinatedwonders.com>
2024-02-23 12:45:58 -07:00
Francis Lavoie 4512be49a9 caddytest: Rename adapt tests to *.caddyfiletest extension (#6119) 2024-02-21 00:37:40 +00:00
José Carlos Chávez f8143a3af1 tests: uses testing.TB interface for helper to be able to use test server in benchmarks. (#6103) 2024-02-20 22:04:14 +00:00
bbaa 8bbf8ec629 caddyfile: Assert having a space after heredoc marker to simply check (#6117) 2024-02-20 12:29:20 +00:00
Francis Lavoie 4284e39a17 chore: Update Chroma to get the new Caddyfile lexer (#6118) 2024-02-20 06:23:39 -05:00
WeidiDeng 53f7035299 reverseproxy: use context.WithoutCancel (#6116) 2024-02-19 20:25:02 -07:00
Aziz Rmadi b893c8c5f8 caddyfile: Reject directives in the place of site addresses (#6104)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-02-19 00:22:48 +00:00
Matt Holt 127788807f caddyhttp: Register post-shutdown callbacks (#5948) 2024-02-14 21:21:23 -07:00
Francis Lavoie 2c48dda109 caddyhttp: Only attempt to enable full duplex for HTTP/1.x (#6102) 2024-02-13 13:45:38 -05:00
Francis Lavoie 30d63648f5 caddyauth: Drop support for scrypt (#6091) 2024-02-12 19:33:54 +00:00
Mohammed Al Sahaf 21744b6c4c Revert "caddyfile: Reject long heredoc markers (#6098)" (#6100)
This reverts commit e7a534d0a3.
2024-02-12 18:06:22 +00:00
Francis Lavoie f9e11158bc caddyauth: Rename basicauth to basic_auth (#6092) 2024-02-12 17:34:23 +00:00
Francis Lavoie 91ec75441a logging: Inline Caddyfile syntax for ip_mask filter (#6094) 2024-02-12 17:15:35 +00:00
Francis Lavoie e7a534d0a3 caddyfile: Reject long heredoc markers (#6098)
Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2024-02-11 13:30:14 -05:00
Francis Lavoie c78ebb3d6a chore: Rename CI jobs, run on M1 mac (#6089)
* Try macos-14 for fun

* Decouple OS names and VM names

* Shorten `cross-build-test` to `build`
2024-02-09 15:31:26 -07:00
Kévin Dunglas a6d9f9be5b Merge pull request #6081 from dunglas/fix/encode-match 2024-02-09 09:41:44 +01:00
Kévin Dunglas 2348ac897a update comment 2024-02-09 09:35:55 +01:00
Kévin Dunglas d3f23a8eeb improved list 2024-02-09 09:35:55 +01:00
Kévin Dunglas 60abd72c7a fix: add back text/* 2024-02-09 09:35:55 +01:00
Kévin Dunglas b8f729b88f fix: add more media types to the compressed by default list 2024-02-09 09:35:55 +01:00
Mohammed Al Sahaf e1aa862e6a acmeserver: support specifying the allowed challenge types (#5794)
* acmeserver: support specifying the allowed challenge types

* add caddyfile adapt tests

* introduce basic acme_server test

* skip acme test on unsuitable environments

* skip integration tests of ACME

* documentation

* add negative-scenario test for mismatched allowed challenges

* a bit more docs

* fix tests for ACME challenges

* appease the linter

* skip ACME tests on s390x

* enable ACME challenge tests on all machines

* 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>
2024-02-08 11:42:03 +03:00
Francis Lavoie 8c2a72ad07 matchers: Drop forwarded option from remote_ip matcher (#6085) 2024-02-07 10:09:29 -05:00
Francis Lavoie bde46211e3 caddyhttp: Test cases for %2F and %252F (#6084) 2024-02-07 05:13:17 -05:00
WeidiDeng bc1e63198d bump to golang 1.22 (#6083) 2024-02-07 02:13:58 -05:00
Aziz Rmadi feb07a7b59 fileserver: Browse can show symlink target if enabled (#5973)
* Added optional subdirective to browse allowing to reveal symlink paths.

* Update modules/caddyhttp/fileserver/browsetplcontext.go

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-02-06 04:31:26 +00:00
Aziz Rmadi a7479302fc core: Support NO_COLOR env var to disable log coloring (#6078) 2024-02-01 19:12:42 -07:00
dependabot[bot] 223f314331 build(deps): bump peter-evans/repository-dispatch from 2 to 3 (#6080)
Bumps [peter-evans/repository-dispatch](https://github.com/peter-evans/repository-dispatch) from 2 to 3.
- [Release notes](https://github.com/peter-evans/repository-dispatch/releases)
- [Commits](https://github.com/peter-evans/repository-dispatch/compare/v2...v3)

---
updated-dependencies:
- dependency-name: peter-evans/repository-dispatch
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 18:34:40 -05:00
Matthew Holt 1919c08ecc Update comment in setcap helper script 2024-01-31 12:59:26 -07:00
Matt Holt 57c5b921a4 caddytls: Make on-demand 'ask' permission modular (#6055)
* caddytls: Make on-demand 'ask' permission modular

This makes the 'ask' endpoint a module, which means that developers can
write custom plugins for granting permission for on-demand certificates.

Kicking myself that we didn't do it this way at the beginning, but who coulda known...

* Lint

* Error on conflicting config

* Fix bad merge

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-01-30 16:11:29 -07:00
Francis Lavoie e1b9a9d7b0 core: Add ctx.Slogger() which returns an slog logger (#5945) 2024-01-25 12:31:15 -07:00
Marten Seemann 697cc593a1 chore: Update quic-go to v0.41.0, bump Go minimum to 1.21 (#6043)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-01-25 13:58:19 -05:00
Yolan Romailler 2fe69a828f chore: enabling a few more linters (#5961)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-01-25 15:24:58 +00:00
bbaa c369df5c37 caddyfile: Correctly close the heredoc when the closing marker appears immediately (#6062) 2024-01-25 14:55:00 +00:00
bbaa 7c48b5fdbb caddyfile: Switch to slices.Equal for better performance (#6061) 2024-01-25 14:46:08 +00:00
Mohammed Al Sahaf e965b111cd tls: modularize trusted CA providers (#5784)
* tls: modularize client authentication trusted CA

* add `omitempty` to `CARaw`

* docs

* initial caddyfile support

* revert anything related to leaf cert validation

The certs are used differently than the CA pool flow

* complete caddyfile unmarshalling implementation

* Caddyfile syntax documentation

* enhance caddyfile parsing and documentation

Apply suggestions from code review

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

* add client_auth caddyfile tests

* add caddyfile unmarshalling tests

* fix and add missed adapt tests

* fix rebase issue

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-01-25 11:44:41 +03:00
Francis Lavoie b9c40e7111 logging: Automatic wrap default for filter encoder (#5980)
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-01-25 04:00:22 +00:00
Francis Lavoie f5344f8cad caddyhttp: Fix panic when request missing ClientIPVarKey (#6040) 2024-01-24 00:45:50 +00:00
Francis Lavoie 750d0b8331 caddyfile: Normalize & flatten all unmarshalers (#6037) 2024-01-23 19:36:59 -05:00
Mohammed Al Sahaf 54823f52bc cmd: reverseproxy: log: use caddy logger (#6042) 2024-01-23 10:52:02 -07:00
Aziz Rmadi ed7e3c906a matchers: query now ANDs multiple keys (#6054)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-01-22 02:36:44 +00:00
bbaa c0273f1f04 caddyfile: Add heredoc support to fmt command (#6056) 2024-01-22 02:24:49 +00:00
Kévin Dunglas dba556fe4b refactor: move automaxprocs init in caddycmd.Main() 2024-01-19 11:17:35 +01:00
Aziz Rmadi d9aded016c caddyfile: Allow heredoc blank lines (#6051) 2024-01-18 22:57:18 -05:00
Aziz Rmadi 4181c79a81 httpcaddyfile: Add optional status code argument to handle_errors directive (#5965)
Co-authored-by: Aziz Rmadi <azizrmadi@Azizs-MacBook-Air.local>
2024-01-16 01:24:17 -05:00
Francis Lavoie 5e2f1b5ced httpcaddyfile: Rewrite root and rewrite parsing to allow omitting matcher (#5844) 2024-01-15 09:57:08 -07:00
Francis Lavoie f3e849e49f fileserver: Implement caddyfile.Unmarshaler interface (#5850) 2024-01-13 21:32:44 +00:00
Bas Westerbaan f658fd05ac reverseproxy: Add tls_curves option to HTTP transport (#5851) 2024-01-13 20:56:23 +00:00
Nebez Briefkani cc0c0cf03e caddyhttp: Security enhancements for client IP parsing (#5805)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-01-13 20:46:37 +00:00
Aziz Rmadi 80acf1bf23 replacer: Fix escaped closing braces (#5995) 2024-01-13 20:24:03 +00:00
a c839a98ff5 filesystem: Globally declared filesystems, fs directive (#5833) 2024-01-13 20:12:43 +00:00
Mohammed Al Sahaf b359ca565c ci/cd: use the build tag nobadger to exclude badgerdb (#6031)
* ci/cd: use the build tag `nobadger` to exclude badgerdb

* upgrade github.com/google/certificate-transparency-go@master
2024-01-10 21:04:11 +03:00
Subhaditya Nath c2d889f85e httpcaddyfile: Fix redir <to> html (#6001) 2024-01-10 12:24:47 +00:00
Zach Galvin cb86319bd5 httpcaddyfile: Support client auth verifiers (#6022)
* Added verifier case

Update author

* Update verifier to match struct tag

* gci run
2024-01-09 23:14:51 +00:00
Rithvik Vibhu ed41c924cf tls: add reuse_private_keys (#6025) 2024-01-09 16:00:31 -07:00
Fred Cox d9ff7b1872 reverseproxy: Only change Content-Length when full request is buffered (#5830)
fixes: https://github.com/caddyserver/caddy/issues/5829

Signed-off-by: Fred Cox <mcfedr@gmail.com>
2024-01-09 12:59:30 -07:00
Aaron Brady 76611fa150 Switch Solaris-derivatives away from listen_unix (#6021)
Solaris 10 and Illumos are missing SO_REUSEPORT. Treat them more like
Windows (i.e. use the listener pool).
2024-01-06 05:09:20 -05:00
dependabot[bot] 8a50f191bf build(deps): bump actions/upload-artifact from 3 to 4 (#6013)
* build(deps): bump actions/upload-artifact from 3 to 4

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Disable compression

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-01-02 08:23:25 +00:00
dependabot[bot] 4f3f6e35e8 build(deps): bump actions/setup-go from 4 to 5 (#6012)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-02 07:13:31 +00:00
Mohammed Al Sahaf 787f6b257f chore: check against errors of io/fs instead of os (#6011)
* chore: replace `os.ErrNotExist` with `fs.ErrNotExist`

* check against permission error from `io/fs` package
2024-01-02 08:48:55 +03:00
networkException b568a10dd4 caddyhttp: support unix sockets in caddy respond command (#6010)
previously the `caddy respond` command would treat the argument
passed to --listen as a TCP socket address, iterating over a possible
port range.

this patch factors the server creation out into a separate function,
allowing this to be reused in case the listen address is a unix network
address.
2023-12-31 22:34:00 -05:00
Steffen Busch 8f9ffc587e fileserver: Add total file size to directory listing (#6003)
* browse: Add total file size to directory listing

* Apply suggestion to remove "in "

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

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-12-30 18:47:13 +00:00
Francis Lavoie f976c84d9e httpcaddyfile: Fix cert file decoding to load multiple PEM in one file (#5997) 2023-12-20 08:37:21 -07:00
dependabot[bot] 1bf72db6ff build(deps): bump golang.org/x/crypto from 0.16.0 to 0.17.0 (#5994)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.16.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-18 16:11:51 -07:00
Kévin Dunglas d54dcf1598 cmd: use automaxprocs for better perf in containers (#5711)
* feat: use automaxprocs for better perf in containers

* better logs

* cs
2023-12-18 15:50:26 -07:00
Francis Lavoie 3248e4c89f logging: Add zap.Option support (#5944) 2023-12-18 20:48:34 +00:00
Francis Lavoie da7d8cb26d httpcaddyfile: Sort skip_hosts for deterministic JSON (#5990)
* httpcaddyfile: Sort skip_hosts for deterministic JSON

* Update caddyconfig/httpcaddyfile/httptype.go

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

* Fix test

* Bah

---------

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2023-12-18 12:54:52 -07:00
Tim Geoghegan 387545a895 metrics: Record request metrics on HTTP errors (#5979) 2023-12-15 20:14:00 +00:00
Aziz Rmadi b49ec05161 go.mod: Updated quic-go to v0.40.1 (#5983) 2023-12-14 22:42:01 -07:00
Kévin Dunglas b16aba5c27 fileserver: Enable compression for command by default (#5855)
* feat: enable compression for file-server

* refactor

* const

* Update help text

* Update modules/caddyhttp/fileserver/command.go

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-12-13 20:44:22 -07:00
David DeMoss 362f33daae fileserver: New --precompressed flag (#5880)
exposes the file_server precompressed functionality to be used with the
file-server command

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-12-13 20:26:20 -07:00
Francis Lavoie 3d7d60f7cf caddyhttp: Add uuid to access logs when used (#5859) 2023-12-13 15:40:15 -07:00
Mohammed Al Sahaf dc12bd9743 proxyprotocol: use github.com/pires/go-proxyproto (#5915)
* proxyprotocol: use github.com/pires/go-proxyproto

* Fix typo: r/generelly/generally

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

* add config options for `Deny` CIDR and fallback policy

* use `netip` package & trust unix sockets

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-12-13 09:07:43 -07:00
Jens-Uwe Mager 56c6b3f673 cmd: Preserve LastModified date when exporting storage (#5968) 2023-12-13 09:06:06 -07:00
Aziz Rmadi cbbd1df904 core: Always make AppDataDir for InstanceID (#5976) 2023-12-13 07:39:10 -07:00
Benjamin Marwell 7d919af01b chore: cross-build for AIX (#5971) 2023-12-11 12:55:04 +00:00
Matt Holt 4a09cf0dc0 caddytls: Sync distributed storage cleaning (#5940)
* caddytls: Log out remote addr to detect abuse

* caddytls: Sync distributed storage cleaning

* Handle errors

* Update certmagic to fix tiny bug

* Split off port when logging remote IP

* Upgrade CertMagic
2023-12-07 11:00:02 -07:00
Andreas Kohn b24ae63ea6 caddytls: Context to DecisionFunc (#5923)
See https://github.com/caddyserver/certmagic/pull/255
2023-12-07 10:40:13 -07:00
Mohammed Al Sahaf 4173e2c77a tls: accept placeholders in string values of certificate loaders (#5963)
* tls: loader: accept placeholders in string values

* appease the linter
2023-12-04 09:23:15 -07:00
Matt Holt 18f34290d2 templates: Offically make templates extensible (#5939)
* templates: Offically make templates extensible

This supercedes #4757 (and #4568) by making template extensions
configurable.

The previous implementation was never documented AFAIK and had only
1 consumer, which I'll notify as a courtesy.

* templates: Add 'maybe' function for optional components

* Try to fix lint error
2023-11-28 09:39:14 -07:00
WeidiDeng 22eecdb90c http2 uses new round-robin scheduler (#5946) 2023-11-24 01:54:27 +00:00
WeidiDeng 4de2c1c65e panic when reading from backend failed to propagate stream error (#5952) 2023-11-23 03:18:18 -05:00
dlorenc 878d491834 chore: Bump otel to v1.21.0. (#5949)
Signed-off-by: Dan Lorenc <dlorenc@chainguard.dev>
2023-11-22 17:02:13 +03:00
WeidiDeng 96f638eaad httpredirectlistener: Only set read limit for when request is HTTP (#5917) 2023-11-20 12:31:36 +00:00
Matthew Holt 7e52db8280 fileserver: Add .m4v for browse template icon 2023-11-14 13:39:57 -07:00
Mohammed Al Sahaf 3b3d678714 Revert "caddyhttp: Use sync.Pool to reduce lengthReader allocations (#5848)" (#5924) 2023-11-01 13:17:02 -04:00
WeidiDeng ee358550e4 go.mod: update quic-go version to v0.40.0 (#5922) 2023-10-31 14:05:34 -04:00
Marten Seemann 3f55efcfde update quic-go to v0.39.3 (#5918) 2023-10-27 07:52:12 -04:00
WeidiDeng f71d779009 chore: Fix usage pool comment (#5916) 2023-10-25 23:05:20 -04:00
Mohammed Al Sahaf d949caf459 test: acmeserver: add smoke test for the ACME server directory (#5914) 2023-10-24 13:59:53 -04:00
Mariano Cano ac0ad4da84 Upgrade acmeserver to github.com/go-chi/chi/v5 (#5913)
This commit upgrades the router used in the acmeserver to
github.com/go-chi/chi/v5. In the latest release of step-ca, the router
used by certificates was upgraded to that version.

Fixes #5911

Signed-off-by: Mariano Cano <mariano.cano@gmail.com>
2023-10-23 21:02:11 -04:00
Francis Lavoie 4c10a05431 caddyhttp: Adjust scheme placeholder docs (#5910) 2023-10-22 17:47:16 -04:00
Matthew Holt fe2a02bf7a go.mod: Upgrade quic-go to v0.39.1 2023-10-20 15:23:35 -06:00
Ethan Brown (Domino) 9fc55a9792 go.mod: CVE-2023-45142 Update opentelemetry (#5908) 2023-10-20 21:15:48 +00:00
Francis Lavoie 4e8245df0b templates: Delete headers on httpError to reset to clean slate (#5905) 2023-10-18 16:43:14 -06:00
Francis Lavoie ac1f20b9e4 httpcaddyfile: Remove port from logger names (#5881)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-10-16 23:57:03 -06:00
Matt Holt 174c19a953 core: Apply SO_REUSEPORT to UDP sockets (#5725)
* core: Apply SO_REUSEPORT to UDP sockets

For some reason, 10 months ago when I implemented SO_REUSEPORT
for TCP, I didn't realize, or forgot, that it can be used for UDP too. It is a
much better solution than using deadline hacks to reuse a socket, at
least for TCP.

Then https://github.com/mholt/caddy-l4/issues/132 was posted,
in which we see that UDP servers never actually stopped when the
L4 app was stopped. I verified this using this command:

    $ nc -u 127.0.0.1 55353

combined with POSTing configs to the /load admin endpoint (which
alternated between an echo server and a proxy server so I could tell
which config was being used).

I refactored the code to use SO_REUSEPORT for UDP, but of course
we still need graceful reloads on all platforms, not just Unix, so I
also implemented a deadline hack similar to what we used for
TCP before. That implementation for TCP was not perfect, possibly
having a logical (not data) race condition; but for UDP so far it
seems to be working. Verified the same way I verified that SO_REUSEPORT
works.

I think this code is slightly cleaner and I'm fairly confident this code
is effective.

* Check error

* Fix return

* Fix var name

* implement Unwrap interface and clean up

* move unix packet conn to platform specific file

* implement Unwrap for unix packet conn

* Move sharedPacketConn into proper file

* Fix Windows

* move sharedPacketConn and fakeClosePacketConn to proper file

---------

Co-authored-by: Weidi Deng <weidi_deng@icloud.com>
2023-10-16 22:17:32 -06:00
Harish Shan c8559c4485 caddyhttp: Use sync.Pool to reduce lengthReader allocations (#5848)
* Use sync.Pool to reduce lengthReader allocations

Signed-off-by: Harish Shan <140232061+perhapsmaple@users.noreply.github.com>

* Add defer putLengthReader to prevent leak

Signed-off-by: Harish Shan <140232061+perhapsmaple@users.noreply.github.com>

* Cleanup in putLengthReader

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

---------

Signed-off-by: Harish Shan <140232061+perhapsmaple@users.noreply.github.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-10-16 14:42:01 -06:00
Thanmay Nath 24b0ecc310 cmd: Add newline character to version string in CLI output (#5895) 2023-10-16 09:58:32 -06:00
WeidiDeng 7c82e265da core: quic listener will manage the underlying socket by itself (#5749)
* core: quic listener will manage the underlying socket by itself.

* format code

* rename sharedQUICTLSConfig to sharedQUICState, and it will now manage the number of active requests

* add comment

* strict unwrap type

* fix unwrap

* remove comment
2023-10-16 09:28:15 -06:00
Francis Lavoie 0900844c81 templates: Clarify include args docs, add .ClientIP (#5898) 2023-10-15 20:58:46 -04:00
Francis Lavoie 7984e6f6fd httpcaddyfile: Fix TLS automation policy merging with get_certificate (#5896) 2023-10-14 14:23:50 -06:00
Mohammed Al Sahaf d70608b656 cmd: upgrade: resolve symlink of the executable (#5891) 2023-10-13 17:19:22 -04:00
WeidiDeng 1f60328e17 caddyfile: Fix variadic placeholder false positive when token contains : (#5883) 2023-10-13 02:28:20 -04:00
Norman Soetbeer 0e204b730a admin: Respond with 4xx on non-existing config path (#5870)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-10-11 20:24:29 +00:00
Francis Lavoie fae195ac7e ci: Force the Go version for govulncheck (#5879) 2023-10-11 20:09:02 +00:00
Forza 130f6d1f83 fileserver: Set canonical URL on browse template (#5867)
* Browse.html: Add canonical URL and home-link

When contents are equal, but maybe just a sort order is different, it is good to add `<link rel="canonical" href="base-path/" />`. This helps search engines propeely index the page.

I also added a link to the home page with the name of `{{.Host}}` just above the bread crumbs to make the page clearer.

https://paste.tnonline.net/files/28Wun5CQZiqA_Screenshot_20231007_134435_Opera.png

* Update browse.html
2023-10-11 13:47:38 -06:00
Bas Westerbaan 289934f3d1 tls: Add X25519Kyber768Draft00 PQ "curve" behind build tag (#5852)
… when compiled with cfgo (https://github.com/cloudflare/go).
2023-10-11 13:45:37 -06:00
Matt Holt 3a3182fba3 reverseproxy: Add more debug logs (#5793)
* reverseproxy: Add more debug logs

This makes debug logging very noisy when reverse proxying, but I guess
that's the point.

This has shown to be useful in troubleshooting infrastructure issues.

* Update modules/caddyhttp/reverseproxy/streaming.go

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

* Update modules/caddyhttp/reverseproxy/streaming.go

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

* Add opt-in `trace_logs` option

* Rename to VerboseLogs

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-10-11 13:36:20 -06:00
Francis Lavoie e8b8d4a8cd reverseproxy: Fix least_conn policy regression (#5862) 2023-10-11 16:04:28 +00:00
Francis Lavoie a8586b05aa reverseproxy: Add logging for dynamic A upstreams (#5857) 2023-10-11 09:50:44 -06:00
Francis Lavoie 05dbe1c171 reverseproxy: Replace health header placeholders (#5861) 2023-10-11 09:50:28 -06:00
Francis Lavoie 33d8d2c6b5 httpcaddyfile: Sort TLS SNI matcher for deterministic JSON output (#5860)
* httpcaddyfile: Sort TLS SNI matcher, for deterministic adapt output

* Update caddyconfig/httpcaddyfile/httptype.go

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-10-11 09:47:07 -06:00
Francis Lavoie 9c419f1e1a cmd: Fix exiting with custom status code, add caddy -v (#5874)
* Simplify variables for commands

* Add --envfile support for adapt command

* Carry custom status code for commands to os.Exit()

* cmd: add `-v` and `--version` to root caddy command

* Add `--envfile` to `caddy environ`, extract flag parsing to func

---------

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2023-10-11 09:46:18 -06:00
Fred Cox b245ecd325 reverseproxy: fix parsing Caddyfile fails for unlimited request/response buffers (#5828) 2023-10-11 04:42:40 -04:00
Francis Lavoie 2a6859a5e4 reverseproxy: Fix retries on "upstreams unavailable" error (#5841) 2023-10-10 22:07:20 +00:00
Đỗ Trọng Hải df99502977 httpcaddyfile: Enable TLS for catch-all site if tls directive is specified (#5808) 2023-10-10 21:46:39 +00:00
Christoph e0aaefab80 encode: Add application/wasm* to the default content types (#5869) 2023-10-10 21:18:37 +00:00
Kévin Dunglas fa5a579b60 fileserver: Add command shortcuts -l and -a (#5854) 2023-10-10 20:57:18 +00:00
Matthew Holt 88b4fbf244 go.mod: Upgrade dependencies incl. x/net/http
Possibly important for the HTTP/2 Rapid Reset issue.
2023-10-10 12:01:20 -06:00
Thanmay Nath 5653c36bc2 templates: Add dummy RemoteAddr to httpInclude request, proxy compatibility (#5845)
* Enhancement: Allow X-Forwarded-For Header in httpInclude Virtual Requests

The goal of this enhancement is to modify the funcHTTPInclude function in the Caddy codebase to include the X-Forwarded-For header in the virtual request. This change will enable reverse proxies to set the X-Forwarded-For header, ensuring that the client's IP address is correctly provided to the target endpoint. This modification is essential for applications that depend on the X-Forwarded-For header for various functionalities, such as authentication, logging, or content customization.

* Updated tplcontext.go - set `virtReq.RemoteAddr = "127.0.0.1"`

i have made the suggested changes

* Apply suggestions from code review

* Update modules/caddyhttp/templates/tplcontext.go

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-10-07 20:47:34 +00:00
Patrick Koenig 4feac4d83c reverseproxy: Allow fallthrough for response handlers without routes (#5780) 2023-10-05 23:15:26 -04:00
Kévin Dunglas 82c356f254 fix: caddytest.AssertResponseCode error message (#5853) 2023-10-02 20:55:09 +00:00
dependabot[bot] 1405683c2b build(deps): bump goreleaser/goreleaser-action from 4 to 5 (#5847)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-02 00:34:43 +00:00
dependabot[bot] 89c407aa34 build(deps): bump actions/checkout from 3 to 4 (#5846)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-01 20:13:54 -04:00
Matthew Holt 58ab3a01a0 caddyhttp: Use LimitedReader for HTTPRedirectListener 2023-09-26 07:32:46 -06:00
glowinthedark a306c5f769 fileserver: browse template SVG icons and UI tweaks (#5812)
* fileserver browse.html UI tweaks: folder-symlink icon, search

fileserver browse.html UI tweaks: folder-symlink icon, search

- ui - add folder-symlink SVG icon
- search: use `<input type="search">` instead of `text`
- fix npe with `sizebar.style.width` = null in grid mode

* tabify whitespace

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

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-09-15 19:05:45 -06:00
Pascal Vorwerk 1e0dea59ef reverseproxy: fix nil pointer dereference in AUpstreams.GetUpstreams (#5811)
fix a nil pointer dereference in AUpstreams.GetUpstreams when AUpstreams.Versions is not set (fixes caddyserver#5809)

Signed-off-by: Pascal Vorwerk <info@fossores.de>
2023-09-10 19:08:02 -04:00
Đỗ Trọng Hải 2cac3c5491 httpcaddyfile: fix placeholder shorthands in named routes (#5791)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-09-08 14:38:44 -04:00
Evan Van Dam f2ab7099db cmd: Prevent overwriting existing env vars with --envfile (#5803)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-09-07 02:19:24 +00:00
Đỗ Trọng Hải 50cea4e263 ci: Run govulncheck (#5790)
* feat(ci): check vuln Go mods in CI

* fix(ci): correct directive for govulncheck

* refactor(ci): move govulncheck to lint.yml

* refactor(lint): move govulncheck to different job
2023-09-05 11:31:25 -04:00
Paul Jeannot 1b73e3862d logging: query filter for array of strings (#5779)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-08-29 22:59:43 +00:00
Francis Lavoie c46ec3b500 logging: Clone array on log filters, prevent side-effects (#5786)
Fixes https://caddy.community/t/is-caddy-mutating-header-content-from-logging-settings/20947
2023-08-29 11:41:39 -06:00
Matthew Holt ed8bb13c5d fileserver: Export BrowseTemplate
This allows programs embedding Caddy to customize the browse template.
2023-08-29 09:34:20 -06:00
Mohammed Al Sahaf b7e472d548 ci: ensure short-sha is exported correctly on all platforms (#5781) 2023-08-25 16:06:44 +00:00
Francis Lavoie 7103ea096f caddyfile: Fix case where heredoc marker is empty after newline (#5769)
Fixes `panic: runtime error: slice bounds out of range [:3] with capacity 2`

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-08-24 03:27:57 +00:00
WeidiDeng 888c6d7e93 go.mod: Update quic-go to v0.38.0 (#5772)
* go.mod: Update quic-go to v0.38.0

* run "go mod tidy"

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-08-24 02:55:28 +00:00
Matt Holt b377208ede chore: Appease gosec linter (#5777)
These happen to be harmless memory aliasing
but I guess the linter can't know that and we
can't really prove it in general.
2023-08-23 20:47:54 -06:00
WeidiDeng 4776f62caa replacer: change timezone to UTC for "time.now.http" placeholders (#5774) 2023-08-22 02:41:25 -04:00
Francis Lavoie 38a7b6b3d0 caddyfile: Adjust error formatting (#5765) 2023-08-20 08:51:03 -06:00
Marten Seemann 84d5e1c5d6 update quic-go to v0.37.6 (#5767) 2023-08-19 23:34:15 +00:00
Karun Agarwal 288216e1fb httpcaddyfile: Stricter errors for site and upstream address schemes (#5757)
Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-08-19 07:28:25 -04:00
Francis Lavoie 10053f7570 caddyfile: Loosen heredoc parsing (#5761) 2023-08-19 10:32:32 +00:00
Mohammed Al Sahaf 0a6d3333b2 fileserver: docs: clarify the ability to produce JSON array with browse (#5751) 2023-08-18 19:04:08 +00:00
guangwu 568fd2b286 fix package typo (#5764)
Signed-off-by: guoguangwu <guoguangwu@magic-shield.com>
2023-08-18 08:20:46 -06:00
Matthew Holt f11c3c9f5a go.mod: Upgrade CertMagic and quic-go 2023-08-17 11:34:48 -06:00
Matt Holt 936ee918ee reverseproxy: Always return new upstreams (fix #5736) (#5752)
* reverseproxy: Always return new upstreams (fix #5736)

* Fix healthcheck logger race
2023-08-17 11:33:40 -06:00
Jacob Gadikian d6f86cccf5 ci: use gci linter (#5708)
* use gofmput to format code

* use gci to format imports

* reconfigure gci

* linter autofixes

* rearrange imports a little

* export GOOS=windows golangci-lint run ./... --fix
2023-08-14 09:41:15 -06:00
Matthew Holt 2d7d806fcf fileserver: Slightly more fitting icons 2023-08-11 20:53:11 -06:00
pistasjis d8135505d3 cmd: Require config for caddy validate (fix #5612) (#5614)
* Require config for caddy validate - fixes #5612

Signed-off-by: Pistasj <hi@pistasjis.net>

* Try making adjacent Caddyfile check its own function

Signed-off-by: Pistasj <hi@pistasjis.net>

* add Francis' suggestion

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

* Refactor

* Fix borked commit, sigh

---------

Signed-off-by: Pistasj <hi@pistasjis.net>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2023-08-09 17:40:37 +00:00
Matthew Holt 11166889c5 Fix tests
I thought Go ordered JSON objects when marshaling, but I guess not.
2023-08-09 11:25:59 -06:00
Matthew Holt 080db93817 caddytls: Update docs for on-demand config 2023-08-09 11:15:01 -06:00
Francis Lavoie a8492c064d fileserver: Don't repeat error for invalid method inside error context (#5705) 2023-08-09 17:12:09 +00:00
Matt Holt 6cdcc2a782 ci: Update to Go 1.21 (#5719)
* ci: Update to Go 1.21

* Bump quic-go to v0.37.4

* Check EnableFullDuplex err

* Linter bug suppression

See https://github.com/timakin/bodyclose/issues/52

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-08-09 12:34:28 -04:00
Aaron Dewes fbb0ecfa32 ci: Add riscv64 (64-bit RISC-V) to goreleaser (#5720)
This will add 64-bit RISC-V Linux prebuilts for Caddy.
2023-08-08 12:11:53 -06:00
Shyim 5b9c850ab3 go.mod: Upgrade golang.org/x/net to 0.14.0 (#5718) 2023-08-08 11:23:26 -06:00
Jacob Gadikian b32f265eca ci: Use gofumpt to format code (#5707) 2023-08-07 19:40:31 +00:00
Matthew Holt 431adc0980 templates: Fix httpInclude (fix #5698)
Allowable during feature freeze because this is a simple, non-invasive
bug fix only.
2023-08-07 12:53:21 -06:00
Matthew Holt a8cc5d1a7d go.mod: Upgrade to quic-go v0.37.3
Fixes #5680 once and for all! Hopefully :)

Thank you @marten-seemann for your excellent work!
2023-08-05 18:10:15 -06:00
Emily 8d304a4566 cmd: Split unix sockets for admin endpoint addresses (#5696)
* cmd: fix cli when admin endpoint uses new unix socket permission format

Fixes a bug where the following Caddyfile

```Caddyfile
{
	admin unix/admin.sock|0660
}
```

and `caddy reload --config Caddyfile`
would throw the following error instead of reloading it:

```
INFO    using provided configuration    {"config_file": "Caddyfile", "config_adapter": ""}
Error: sending configuration to instance: performing request: Post "http://127.0.0.1/load": dial unix admin.sock|0660: connect: no such file or directory
[ERROR] exit status 1
```

---

This bug also affected `caddy start` and `caddy stop`.

* Move splitter function to internal

---------

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2023-08-06 00:09:16 +00:00
Mohammed Al Sahaf 65e33fc1ee reverseproxy: do not parse upstream address too early if it contains replaceble parts (#5695)
* reverseproxy: do not parse upstream address too early if it contains replaceble parts

* remove unused method

* cleanup

* accommodate partially replaceable port
2023-08-05 23:30:02 +02:00
WeidiDeng 9f34383c02 caddyfile: check that matched key is not a substring of the replacement key (#5685) 2023-08-04 10:44:38 -06:00
Mohammed Al Sahaf b07b198764 chore: use --clean instead of --rm-dist for goreleaser (#5691) 2023-08-04 16:08:54 +00:00
Matthew Holt 51b1bfb125 go.mod: Upgrade quic-go to v0.37.2 (fix #5680) 2023-08-03 18:44:03 -06:00
Matthew Holt c049bab458 fileserver: browse: Render SVG images in grid 2023-08-03 12:53:47 -06:00
WeidiDeng e2fc08bd34 reverseproxy: Fix hijack ordering which broke websockets (#5679) 2023-08-03 04:08:12 +00:00
Herman Slatman 4aa4f3ac70 httpcaddyfile: Fix string does not match ~[]E error (#5675)
Only happens for some people. Unable to confirm.
2023-08-03 00:41:37 +00:00
Francis Lavoie 1913930783 encode: Fix infinite recursion (#5672) 2023-08-02 18:21:11 -06:00
Francis Lavoie cd486c25d1 caddyhttp: Make use of http.ResponseController (#5654)
* caddyhttp: Make use of http.ResponseController

Also syncs the reverseproxy implementation with stdlib's which now uses ResponseController as well https://github.com/golang/go/commit/2449bbb5e614954ce9e99c8a481ea2ee73d72d61

* Enable full-duplex for HTTP/1.1

* Appease linter

* Add warning for builds with Go 1.20, so it's less surprising to users

* Improved godoc for EnableFullDuplex, copied text from stdlib

* Only wrap in encode if not already wrapped
2023-08-02 20:03:26 +00:00
Matthew Holt e198c605bd go.mod: Upgrade dependencies esp. smallstep/certificates
This prevents initialization of a .step folder when it's not used.
2023-08-02 11:48:59 -06:00
Matt Holt f66493efef core: Allow loopback hosts for admin endpoint (fix #5650) (#5664) 2023-08-02 11:13:52 -06:00
Francis Lavoie 5c51c1db2c httpcaddyfile: Allow hostnames & logger name overrides for log directive (#5643)
* httpcaddyfile: Allow `hostnames` override for log directive

* Implement access logger name overrides

* Fix panic & default logger clobbering edgecase
2023-08-02 03:13:46 -04:00
mmm444 da23501457 reverseproxy: Connection termination cleanup (#5663) 2023-08-01 14:01:12 +00:00
Matthew Holt 94749e119a go.mod: Use quic-go 0.37.1
Should fix panic in Go 1.21 where there was no RemoteAddr.
2023-07-31 16:31:17 -06:00
Omar Ramadan d7d16360d4 reverseproxy: Export ipVersions type (#5648)
allows AUpstreams to be instantiated externally
2023-07-25 12:50:21 -06:00
Matthew Holt 4df27a20c8 go.mod: Use latest CertMagic (v0.19.1)
Fixes race condition
2023-07-25 10:31:47 -06:00
Matthew Holt 18c309b5fa caddyhttp: Preserve original error (fix #5652) 2023-07-25 09:41:56 -06:00
ydylla e041962b66 fileserver: add lazy image loading (#5646) 2023-07-22 15:50:36 +00:00
Marten Seemann f45a6de20d go.mod: Update quic-go to v0.37.0, bump to Go 1.20 minimum (#5644)
* update quic-go to v0.37.0

* Bump to Go 1.20

* Bump golangci-lint version, yml syntax consistency

* Use skip-pkg-cache workaround

* Workaround needed for both?

* Seeding weakrand is no longer necessary

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-07-21 22:00:48 -06:00
Matt Holt b51dc5d5d0 core: Refine mutex during reloads (fix #5628) (#5645)
Separate currentCtxMu to protect currentCtx, and a new
rawCfgMu to protect rawCfg and synchronize loads.
2023-07-21 15:32:20 -06:00
bt90 f857b32d65 go.mod: update quic-go to v0.36.2 (#5636) 2023-07-17 14:16:43 -06:00
Matthew Holt 4e36b4c9d1 fileserver: Tweak grid view of browse template
All cells on row have same height.
Center-align vertically.
2023-07-17 11:18:40 -06:00
Mohammed Al Sahaf 27bc16abed fileserver: add export-template sub-command to file-server (#5630) 2023-07-13 15:54:48 -06:00
WeidiDeng bbe1952a59 caddyfile: Fix comparing if two tokens are on the same line (#5626)
* fix comparing if two tokens are on the same line

* compare tokens from copies when importing
2023-07-12 14:32:22 -06:00
Matt Holt 0e2c7e1d35 caddytls: Reuse certificate cache through reloads (#5623)
* caddytls: Don't purge cert cache on config reload

* Update CertMagic

This actually avoids reloading managed certs from storage
when already in the cache, d'oh.

* Fix bug; re-implement HasCertificateForSubject

* Update go.mod: CertMagic tag
2023-07-11 19:10:58 +00:00
Matt Holt 7ceef91295 Minor tweaks to security.md 2023-07-08 14:02:09 -06:00
Matthew Holt 5dec11f2a0 reverseproxy: Pointer receiver
This avoids copying the Upstream, which has an atomically-accessed value
in it.
2023-07-08 13:42:51 -06:00
Matthew Holt 66114cb155 caddyhttp: Trim dot/space only on Windows (fix #5613)
Follow-up to #2917. Path matcher needs to trim dots and spaces but only
on Windows.
2023-07-08 13:42:13 -06:00
Marten Seemann 7914ba3573 update quic-go to v0.36.1 (#5611) 2023-07-01 19:34:27 -04:00
Matthew Holt dfe17c33ef caddyconfig: Specify config adapter for HTTP loader (close #5607) 2023-06-30 20:04:32 -06:00
WeidiDeng 710824c3ce core: Embed net.UDPConn to gain optimizations (#5606)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-06-30 16:31:26 -06:00
Mohammed Al Sahaf d8ae801068 chore: remove deprecated property rlcp in goreleaser config (#5608) 2023-06-30 16:53:56 -04:00
Emily 119e8794bc core: Skip chmod for abstract unix sockets (#5596)
because those aren't real paths on the filesystem and thus can't be `chmod`ed
2023-06-24 18:25:02 -06:00
Emily 22927e278d core: Add optional unix socket file permissions (#4741)
* core: Add optional unix socket file permissions

This commit also changes the default unix socket file permissions to `u=w,g=,o=` (octal: `0200`).
It used to default to the shell's umask (usually `u=rwx,g=rx,o=rx`, octal: `0755`).

`/run/caddy.sock` -> `/run/caddy.sock` with `0200` default perms
`/run/caddy.sock|0222` -> `/run/caddy.sock` with `0222` perms

`|` instead of `:` is used as a separator, to account for the `:` in Windows drive letters (e.g. `C:\absolute\path.sock`)

Fun fact:
The old unix(7) man page (pre Jun 2016) stated a socket needs both read and write perms.
Turns out, only write perms are needed.
Corrected in https://github.com/mkerrisk/man-pages/commit/7578ea2f85b272363d22680d69e7d32f0b59c83b
Despite this, most implementations still default to read+write to this date.

* Add cases with Windows paths to test

* Require write perms for the owning user
2023-06-23 14:49:41 -06:00
Francis Lavoie 7a69ae7571 reverseproxy: Honor tls_except_port for active health checks (#5591) 2023-06-22 16:20:30 -06:00
Matthew Holt 2b2addebb8 Appease linter 2023-06-21 17:59:54 -06:00
Matthew Holt 9563666bfb Fix compile on Windows, hopefully 2023-06-21 17:47:23 -06:00
Matthew Holt 806341e089 core: Properly preserve unix sockets (fix #5568) 2023-06-21 17:16:01 -06:00
Matthew Holt 0468508e92 go.mod: Upgrade CertMagic for hotfix 2023-06-21 13:25:38 -06:00
Matthew Holt 415d1e7b6f go.mod: Upgrade some dependencies 2023-06-21 13:25:38 -06:00
Omer Demirok 1a36b06cd4 chore: upgrade otel (#5586) 2023-06-21 11:46:42 -06:00
Marten Seemann 398c12ae9b go.mod: Update quic-go to v0.36.0 (#5584) 2023-06-21 06:56:12 -04:00
Saber Haj Rabiee 361946eb0c reverseproxy: weighted_round_robin load balancing policy (#5579)
* added weighted round robin algorithm to load balancer

* added an adapt integration test for wrr and fixed a typo

* changed args format to Caddyfile args convention

* added provisioner and validator for wrr

* simplified the code and improved doc
2023-06-20 11:42:58 -06:00
mmm444 424ae0f420 reverseproxy: Experimental streaming timeouts (#5567)
* reverseproxy: WIP streaming timeouts

* More verbose logging by using the child logger

* reverseproxy: Implement streaming timeouts

* reverseproxy: Refactor cleanup

* reverseproxy: Avoid **time.Timer

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-06-19 15:54:43 -06:00
guangwu 4548b7de8e chore: remove refs of deprecated io/ioutil (#5576) 2023-06-16 21:27:57 -06:00
Francis Lavoie 3b19aa2b5a headers: Allow > to defer shortcut for replacements (#5574) 2023-06-15 17:18:55 -06:00
Dominik Roos 6a41b62e70 caddyhttp: Support custom network for HTTP/3 (#5573)
Allow registering a custom network mapping for HTTP/3. This is useful
if the original network for HTTP/1.1 and HTTP/2 is not a standard `unix`,
`tcp4`, or `tcp6` network. To keep backwards compatibility, we fall back
to `udp` if the original network is not registered in the mapping.

Fixes #5555
2023-06-13 19:33:39 -06:00
Corin Langosch 2ddb717144 reverseproxy: Fix parsing of source IP in case it's an ipv6 address (#5569) 2023-06-12 09:35:22 -06:00
365cent 56af1ceb32 fileserver: browse: Better grid layout (#5564)
* feat: better implementation of grid layout

* fix: vertical alignment
2023-06-05 07:39:57 +00:00
Matthew Holt 4ba03c9d38 caddytls: Clarify some JSON config docs 2023-06-04 22:15:50 -06:00
Cass C 078f130a51 cmd: Implement storage import/export (#5532)
* cmd: Implement 'storage import' and 'storage export' CLI commands.

These commands use the certmagic.Storage interface. In particular,
storage implementations should ensure that their List() functions
correctly enumerate all keys when called with an empty prefix and
recursive == true. Also, Stat() calls on keys holding values instead
of nested keys are expected to set KeyInfo.IsTerminal = true.

* remove errors.Join
2023-06-02 13:04:31 -06:00
Matthew Holt 9c180a5988 go.mod: Upgrade quic-go to 0.35.1 2023-06-01 11:28:33 -06:00
Marten Seemann 467b7e3a9c update quic-go to v0.35.0 (#5560) 2023-05-30 05:41:57 -04:00
kassienull 31d75acc9c templates: Add readFile action that does not evaluate templates (#5553)
* Create an includeRaw template function to include a file without parsing it as a template.

Some formatting fixes

* Rename to readFile, various docs adjustments

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-05-26 10:16:28 -06:00
WeidiDeng 9cde715525 caddyfile: Track import name instead of modifying filename (#5540)
* Merge branch 'master' into import_file_stack

* remove space in log key
2023-05-25 13:05:00 -06:00
Jonathan Davies 942fbb37ec core: Use SO_REUSEPORT_LB on FreeBSD (#5554)
to balance load between threads.
2023-05-23 10:56:00 -06:00
WeidiDeng cee4441cb1 caddyfile: Do not replace import tokens if they are part of a snippet (#5539)
* fix variadic placeholder in imported file which also imports

* fix tests.

* skip replacing args when imported token may be part of a snippet
2023-05-22 15:36:55 -06:00
Matt Holt 5bd9c49042 fileserver: Don't set Etag if mtime is 0 or 1 (close #5548) (#5550) 2023-05-22 14:17:15 -06:00
pistasjis cdd3884b32 fileserver: browse: minor tweaks for grid view, dark mode (#5545)
* Make grid entries take up full width on mobile and fix breadcrumb color issue in dark mode

Signed-off-by: Pistasj <odyssey346@disroot.org>

* Do mholt's suggestions

Signed-off-by: Pistasj <odyssey346@disroot.org>

---------

Signed-off-by: Pistasj <odyssey346@disroot.org>
2023-05-20 17:23:17 -06:00
Charles Duffy 2615c9c524 fileserver: Only set Etag if not already set (fix #5546) (#5547) 2023-05-20 17:21:43 -06:00
pistasjis 5336bc0fb6 fileserver: Fix file browser breadcrumb font (#5543)
Signed-off-by: Pistasj <odyssey346@disroot.org>
2023-05-19 11:08:47 -06:00
WeidiDeng 29452647d8 caddyhttp: Fix h3 shutdown (#5541)
* swap h3server close and listener close, avoid quic-listener not closing

* fix typo
2023-05-19 10:00:00 -06:00
Matthew Holt bd34cb6b4e fileserver: More filetypes for browse icons 2023-05-19 09:59:44 -06:00
pistasjis 2d236ead3e fileserver: Fix file browser footer in grid mode (#5536)
* Fix file browser footer in grid

Signed-off-by: Odyssey <odyssey346@disroot.org>

* Fix file browser footer while in grid mode

Signed-off-by: Pistasj <odyssey346@disroot.org>

* Do mholt's suggestions

Signed-off-by: Odyssey <odyssey346@disroot.org>

---------

Signed-off-by: Odyssey <odyssey346@disroot.org>
Signed-off-by: Pistasj <odyssey346@disroot.org>
2023-05-19 09:51:21 -06:00
Matthew Holt 38cb587e0f cmd: Avoid spammy log messages (fix #5538)
I forgot there are two calls to LoadConfig() here that needed replacing.
2023-05-17 16:13:15 -06:00
Matthew Holt ca14b6edd9 httpcaddyfile: Sort Caddyfile slice
Makes list deterministic. See #5538
2023-05-17 13:50:32 -06:00
Francis Lavoie cbf16f6d9e caddyhttp: Implement named routes, invoke directive (#5107)
* caddyhttp: Implement named routes, `invoke` directive

* gofmt

* Add experimental marker

* Adjust route compile comments
2023-05-16 15:27:52 +00:00
Tran Phong 13a37688dc rewrite: use escaped path, fix #5278 (#5504)
* use escaped path while rewriting

Signed-off-by: TP-O <letranphong2k1@gmail.com>

* restore line break

---------

Signed-off-by: TP-O <letranphong2k1@gmail.com>
2023-05-16 09:16:07 -06:00
Francis Lavoie e8352aef38 headers: Add > Caddyfile shortcut for enabling defer (#5535) 2023-05-16 01:18:13 -04:00
Matthew Holt 36546cd8b9 go.mod: Upgrade several dependencies 2023-05-15 16:56:27 -06:00
Francis Lavoie 75b690d248 reverseproxy: Expand port ranges to multiple upstreams in CLI + Caddyfile (#5494)
* reverseproxy: Expand port ranges to multiple upstreams in CLI + Caddyfile

* Add clarifying comment
2023-05-15 12:14:50 -06:00
Matt Holt 52d7335c2b fileserver: Use EscapedPath for browse (#5534)
* fileserver: Use EscapedPath for browse

Fix #5143

* Fixes if filter element is not present

* Remove extraneous line
2023-05-15 10:48:05 -06:00
Matt Holt 96919acc9d caddyhttp: Refactor cert Managers (fix #5415) (#5533) 2023-05-15 10:47:30 -06:00
Matthew Holt e96aafe1ca Slightly more helpful error message 2023-05-13 08:04:42 -06:00
Matt Holt a02ecb0f88 caddytls: Check for nil ALPN; close #5470 (#5473)
* Check for nil ALPN; close #5470

* Apply patch

* Actually I want to try this
2023-05-13 07:09:20 -06:00
Matthew Holt 5ebb7d496d cmd: Reduce spammy logs from --watch 2023-05-12 11:04:02 -06:00
jjiang-stripe cfc85ae8ca caddyhttp: Add a getter for Server.name (#5531) 2023-05-11 10:34:05 -06:00
Matt Holt faf0399e80 caddytls: Configurable fallback SNI (#5527)
* Initial implementation of fallback_sni

* Apply upstream patch
2023-05-10 14:29:29 -06:00
WeidiDeng 808b05c3b4 caddyhttp: Update quic's TLS configs after reload (#5517) (fix #4849)
* fix http3 outdated certificates after config reload

* delegate quic tls GetConfigForClient to another struct.

* change type and method names
fix lint

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-05-10 14:25:09 -06:00
Matthew Holt 12b2f22092 Add doc comment about changing admin endpoint 2023-05-09 20:05:27 -06:00
Yehonatan Ezron 571fc034d3 feature: watch include directory (#5521)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-05-08 22:49:16 +00:00
Mohammed Al Sahaf bef1a739db chore: remove deprecated linters (#5525) 2023-05-08 13:47:33 -06:00
Matthew Holt 0de6064c3b go.mod: Upgrade CertMagic again 2023-05-07 23:40:30 -06:00
Matthew Holt 774f228868 go.mod: Upgrade CertMagic 2023-05-06 11:30:27 -06:00
Francis Lavoie b19946f6af reverseproxy: Optimize base case for least_conn and random_choose policies (#5487)
When only a single request has the least amount of requests, there's no need to compute a random number, because the modulo of 1 will always be 0 anyways.
2023-05-05 20:53:48 -06:00
Francis Lavoie 335cd2e8a4 reverseproxy: Fix active health check header canonicalization, refactor (#5446) 2023-05-05 15:19:22 -06:00
Francis Lavoie 48598e1f2a reverseproxy: Add fallback for some policies, instead of always random (#5488) 2023-05-05 15:08:10 -06:00
Matthew Holt cdce452edc logging: Actually honor the SoftStart parameter 2023-05-04 16:30:34 -06:00
Matthew Holt f3e8b9d95f logging: Soft start for net writer (close #5520)
If enabled and there is an error when opening the net writer, ignore the
error and report it along with subsequent logs to stderr.
2023-05-04 16:29:03 -06:00
eanavitarte c8032867b1 fastcgi: Fix capture_stderr (#5515) 2023-05-04 00:40:49 +00:00
Francis Lavoie 3f20a7c9f3 acmeserver: Configurable resolvers, fix smallstep deprecations (#5500)
* acmeserver: Configurable `resolvers`, fix smallstep deprecations

* Improve default net/port

* Update proxy resolvers parsing to use the new function

* Update listeners.go

Co-authored-by: itsxaos <33079230+itsxaos@users.noreply.github.com>

---------

Co-authored-by: itsxaos <33079230+itsxaos@users.noreply.github.com>
2023-05-03 17:07:22 +00:00
Matthew Holt 1af419e7ec go.mod: Update some dependencies 2023-04-28 09:47:28 -06:00
Dave Henderson f0e3981774 logging: Add traceID field to access logs when tracing is active (#5507)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-04-27 02:46:41 +00:00
Kévin Dunglas 1c9ea0113d caddyhttp: Impl ResponseWriter.Unwrap(), prep for Go 1.20's ResponseController (#5509)
* feat: add support for ResponseWriter.Unwrap()

* cherry-pick Francis' code
2023-04-26 19:44:01 -04:00
Y.Horie 2b04e09fa7 reverseproxy: Fix reinitialize upstream healthy metrics (#5498)
Co-authored-by: Dávid Szabó <david.szabo97@gmail.com>
2023-04-25 09:59:26 -06:00
cui fliter 3443a8a056 fix some comments (#5508)
Signed-off-by: cui fliter <imcusg@gmail.com>
2023-04-25 09:54:42 -06:00
Stéphane Mourey 2943c41884 templates: Add fileStat function (#5497)
* Add isDir template function

* Update modules/caddyhttp/templates/tplcontext.go

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

* Fix funcIsDir return value on error

* Fix funcIsDir return false when root file system not specified

* Add stat function, remove isDir function

* Remove isDir function (really)

* Rename stat to fileStat

---------

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2023-04-24 10:36:37 -06:00
Francis Lavoie 53b6fab125 caddyfile: Stricter parsing, error for brace on new line (#5505) 2023-04-20 18:43:51 +00:00
Matthew Holt c6ac350a3b core: Return default logger if no modules loaded
Fix report from:
https://caddy.community/t/remote-caddyfile-invalid-memory-address-or-nil-pointer-dereference/19700/3
2023-04-20 10:27:40 -06:00
Francis Lavoie b301a3df70 celmatcher: Implement pkix.Name conversion to string (#5492) 2023-04-19 11:55:22 -04:00
Francis Lavoie 998c6e06a7 chore: Adjustments to CI caching (#5495) 2023-04-14 21:38:33 -04:00
Francis Lavoie 4636109ce1 reverseproxy: Remove deprecated lookup_srv (#5396) 2023-04-10 20:08:40 +00:00
Matt Holt 205b142614 cmd: Support ' quotes in envfile parsing (#5437) 2023-04-10 13:55:45 -06:00
Matt Holt ff35ba9ec3 Update contributing guidelines (#5466)
* Update contributing guidelines

* Request disclosure as a courtesy
2023-04-10 13:08:32 -06:00
WeidiDeng d8d87a378f caddyhttp: Serve http2 when listener wrapper doesn't return *tls.Conn (#4929)
* Serve http2 when listener wrapper doesn't return *tls.Conn

* close conn when h2server serveConn returns

* merge from upstream

* rebase from latest

* run New and Closed ConnState hook for h2 conns

* go fmt

* fix lint

* Add comments

* reorder import
2023-04-10 17:05:02 +00:00
Francis Lavoie f8b59e77f8 reverseproxy: Add query and client_ip_hash lb policies (#5468) 2023-04-04 03:31:47 +00:00
Matthew Holt 508cf2aa22 cmd: Create pidfile before config load (close #5477) 2023-04-03 11:57:16 -06:00
Kid f9bd2d3e92 fileserver: Add color-scheme meta tag (#5475) 2023-04-02 22:44:21 -04:00
dependabot[bot] b1366c7e46 build(deps): bump actions/setup-go from 3 to 4 (#5474)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-02 00:36:16 -04:00
Corin Langosch b6fe5d4b41 proxyprotocol: Add PROXY protocol support to reverse_proxy, add HTTP listener wrapper (#5424)
Co-authored-by: WeidiDeng <weidi_deng@icloud.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-03-31 17:44:53 -04:00
Francis Lavoie 66e571e687 reverseproxy: Add mention of which half a copyBuffer err comes from (#5472)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-03-31 15:46:29 -04:00
Francis Lavoie 2b3046de36 caddyhttp: Log request body bytes read (#5461) 2023-03-27 22:40:15 +00:00
Mohammed Al Sahaf 1aef807c71 log: Make sink logs encodable (#5441)
* log: make `sink` encodable

* deduplicate logger fields

* extract common fields into `BaseLog` and embed it into `SinkLog`

* amend godoc on `BaseLog` and `SinkLog`

* minor style change

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-03-27 21:41:24 +00:00
Francis Lavoie e16a886814 caddytls: Eval replacer on automation policy subjects (#5459)
Also renamed the field to SubjectsRaw, which can be considered a breaking change but I don't expect this to affect much.
2023-03-27 21:16:22 +00:00
黑墨水鱼 dd86171d67 headers: Support deleting all headers as first op (#5464)
* Delete all existing fields when fieldName is `*`

* Rearrange deletion before addition in headers

* Revert "Rearrange deletion before addition in headers"

This reverts commit 1b50eeeccc92ccd660c7896d8283c7d9e5d1fcb0.

* Treat deleting all headers as a special case

* 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>
2023-03-27 21:05:18 +00:00
Francis Lavoie f5a13a4ab4 replacer: Add HTTP time format (#5458) 2023-03-27 20:51:13 +00:00
Francis Lavoie 10b265d252 reverseproxy: Header up/down support for CLI command (#5460) 2023-03-27 20:35:31 +00:00
Francis Lavoie 05e9974570 caddyhttp: Determine real client IP if trusted proxies configured (#5104)
* caddyhttp: Determine real client IP if trusted proxies configured

* Support customizing client IP header

* Implement client_ip matcher, deprecate remote_ip's forwarded option
2023-03-27 20:22:59 +00:00
Francis Lavoie 330be2d8c7 httpcaddyfile: Adjust path matcher sorting to solve for specificity (#5462) 2023-03-27 15:43:44 -04:00
Matt Holt 0cc49c053f caddytls: Zero out throttle window first (#5443)
* caddytls: Zero out throttle window first

* Don't error for on-demand 

Fixes https://github.com/caddyserver/caddy/commit/b97c76fb4789b8da0b80f5a2c1c1c5bebba163b5

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-03-20 12:06:00 -06:00
Mohammed Al Sahaf a7db0cfe55 ci: add --yes to cosign arguments (#5440) 2023-03-17 10:36:59 -06:00
Trea Hauet 2182270a2c reverseproxy: Reset Content-Length to prevent FastCGI from hanging (#5435)
Fixes: https://github.com/caddyserver/caddy/issues/5420
2023-03-16 11:42:16 -06:00
Matthew Holt a7af7c486e caddytls: Allow on-demand w/o ask for internal-only 2023-03-14 10:29:27 -06:00
Matthew Holt b97c76fb47 caddytls: Require 'ask' endpoint for on-demand TLS 2023-03-14 10:02:44 -06:00
Matt Holt 6cc3cbbc69 fileserver: New file browse template (#5427)
* fileserver: New file browse template

* Redo extension/icon logic; minor color tweaks

* Fine-tune image display
2023-03-10 18:19:31 +00:00
Matthew Holt 9e943319b4 go.mod: Upgrade dependencies 2023-03-09 10:33:25 -07:00
Chris Reeves b420561737 tracing: Support autoprop from OTEL_PROPAGATORS (#5147)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-03-09 09:02:35 -07:00
Marten Seemann c05e3898b9 caddyhttp: Enable 0-RTT QUIC (#5425) 2023-03-09 08:58:31 -07:00
WeidiDeng b3f0cea2c3 encode: flush status code when hijacked. (#5419) 2023-03-06 09:13:48 -07:00
esell 94d41a9d86 fileserver: Remove trailing slash on fs filenames (#5417) 2023-03-03 14:45:17 -07:00
Matt Holt 99d47050e9 core: Eliminate unnecessary shutdown delay on Unix (#5413)
* core: Eliminate unnecessary shutdown delay on Unix

Fix #5393, alternate to #5405

* Comments, cleanup, adjust logs

* Fix build constraint
2023-03-03 04:00:18 +00:00
Francis Lavoie 85375861f6 caddyhttp: Fix vars_regexp matcher with placeholders (#5408)
Changed to match the `vars` matcher's logic for handling placeholders
2023-03-02 09:01:54 -07:00
Francis Lavoie f6bab8ba85 context: Rename func to AppIfConfigured (#5397) 2023-02-27 18:58:27 +00:00
Emily Lange 941eae5f61 reverseproxy: allow specifying ip version for dynamic a upstream (#5401)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-02-27 17:23:09 +00:00
Mohammed Al Sahaf 096971e313 ci/cd: ship tarballs with vendored deps (#5403) 2023-02-26 22:06:15 +00:00
Francis Lavoie f3379f650a caddyfile: Fix heredoc fuzz crasher, drop trailing newline (#5404)
Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2023-02-26 16:56:48 -05:00
Francis Lavoie 960150bb03 caddyfile: Implement heredoc support (#5385) 2023-02-26 00:34:27 +00:00
Francis Lavoie 9e6919550b cmd: Expand cobra support, add short flags (#5379)
* cmd: Expand cobra support

* Convert commands to cobra, add short flags

* Fix version command typo

Co-authored-by: Emily Lange <git@indeednotjames.com>

* Apply suggestions from code review

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

---------

Co-authored-by: Emily Lange <git@indeednotjames.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-02-24 16:09:12 -07:00
Matthew Holt 167981d258 ci: Update minimum Go version to 1.19 2023-02-24 13:45:44 -07:00
Matthew Holt 8cb1bb4af3 go.mod: Upgrade quic-go to v0.33.0 (Go 1.19 min) 2023-02-24 13:35:56 -07:00
Mohammed Al Sahaf e3909cc385 reverseproxy: refactor HTTP transport layer (#5369)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Weidi Deng <weidi_deng@icloud.com>
2023-02-24 19:54:04 +00:00
Francis Lavoie be53e432fc caddytls: Relax the warning for on-demand (#5384) 2023-02-22 11:41:01 -07:00
Francis Lavoie 79de6df93d cmd: Strict unmarshal for validate (#5383) 2023-02-22 11:39:40 -07:00
WeidiDeng 8bc05e598d caddyfile: Implement variadics for import args placeholders (#5249)
* implement variadic placeholders
imported snippets reflect actual lines in file

* add import directive line number for imported snippets
add tests for parsing

* add realfile field to help debug import cycle detection.

* use file field to reflect import chain

* Switch syntax, deprecate old syntax, refactoring

- Moved the import args handling to a separate file
- Using {args[0:1]} syntax now
- Deprecate {args.*} syntax
- Use a replacer map for better control over the parsing
- Add plenty of warnings when invalid placeholders are detected
- Renaming variables, cleanup comments for readability
- More tests to cover edgecases I could think of
- Minor cleanup to snippet tracking in tokens, drop a redundant boolean field in tokens

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-02-16 17:08:36 -07:00
Emily Lange bf54892a73 cmd: make caddy fmt hints more clear (#5378) 2023-02-16 16:34:12 -07:00
Francis Lavoie 5ded580444 cmd: Adjust documentation for commands (#5377) 2023-02-16 09:14:07 -07:00
Matthew Holt 0db29e2ce9 go.mod: Upgrade acmez and x/net
x/net 0.7.0 contains a security patch apparently.
2023-02-14 12:08:31 -07:00
Matt Holt 4b119a475f reverseproxy: Don't buffer chunked requests (fix #5366) (#5367)
* reverseproxy: Don't buffer chunked requests (fix #5366)

Mostly reverts 845bc4d50b (#5289)

Adds warning for unsafe config.

Deprecates unsafe properties in favor of simpler, safer designed ones.

* Update modules/caddyhttp/reverseproxy/caddyfile.go

Co-authored-by: Y.Horie <u5.horie@gmail.com>

* Update modules/caddyhttp/reverseproxy/reverseproxy.go

Co-authored-by: Y.Horie <u5.horie@gmail.com>

* Update modules/caddyhttp/reverseproxy/reverseproxy.go

Co-authored-by: Y.Horie <u5.horie@gmail.com>

* Remove unused code

---------

Co-authored-by: Y.Horie <u5.horie@gmail.com>
2023-02-11 17:25:29 -07:00
Francis Lavoie 90798f3eea go.mod: Upgrade various dependencies (#5362)
* chore: Upgrade various dependencies

* Support CEL file matcher with no args

* Document `http.request.orig_uri.path.*`, reorder placeholders in docs

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-02-08 17:49:17 +00:00
Steffen Brüheim 536c28d4dc core: Support Windows absolute paths for UDS proxy upstreams (#5114)
* added some tests for parseUpstreamDialAddress

Test 4 fails because it produces "[[::1]]:80" instead of "[::1]:80"

* support absolute windows path in unix reverse proxy address

* make IsUnixNetwork public, support +h2c and reuse it
* add new tests
2023-02-08 10:05:09 -07:00
WeidiDeng c77a6bea66 reverseproxy: Log status code and byte count for websockets (#5140)
* log response size for websocket request

* record size when using hijack bufio.Writer
2023-02-06 16:14:59 -07:00
Francis Lavoie 12bcbe2c49 caddyhttp: Pluggable trusted proxy IP range sources (#5328)
* caddyhttp: Pluggable trusted proxy IP range sources

* Add request to the IPRangeSource interface
2023-02-06 12:44:11 -07:00
Matthew Holt f6f1d8fc89 Run go.mod tidy 2023-02-06 12:24:01 -07:00
Y.Horie 8d3a1b8bcb caddyauth: Use singleflight for basic auth (#5344)
* caddyauth: Add singleflight for basic auth

* Fixes #5338
* it occurred the thunder herd problem like this https://medium.com/@mhrlife/avoid-duplicate-requests-while-filling-cache-98c687879f59

* Update modules/caddyhttp/caddyauth/basicauth.go

Fix comment

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

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-02-06 18:29:03 +00:00
Francis Lavoie ac83b7e218 admin: Add CADDY_ADMIN env var to override the default (#5332) 2023-02-06 17:55:16 +00:00
Francis Lavoie e62b5fb586 chore: Build with Go 1.20, keep minimum at 1.18 for now (#5353) 2023-02-06 11:29:20 -05:00
Amis Shokoohi 94b8d56096 cmd: Add --envfile flag to validate command (#5350)
Fixes https://github.com/caddyserver/caddy/issues/5346
2023-01-31 16:27:35 -05:00
Amis Shokoohi 8c0b49bf03 cmd: fmt exit successfully after overwriting config file (#5351)
Fixes https://github.com/caddyserver/caddy/issues/5349
2023-01-31 11:24:44 -05:00
Francis Lavoie 201b9b41f9 chore: Fix warning "range variable captured by func literal" (#5348) 2023-01-31 03:07:57 -05:00
Matthew Holt 0a3efd1641 caddytls: Debug log for ask endpoint 2023-01-30 09:30:53 -07:00
Y.Horie d73660f7c3 httpcaddyfile: Add persist_config global option (#5339)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-01-27 23:31:37 -05:00
Francis Lavoie 7f2a93e6c3 caddyfile: Allow overriding server names (#5323) 2023-01-27 14:56:39 -05:00
Y.Horie e9d95ab29f reverseproxy: Add flag to short command to disable redirects (#5330)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Fixes undefined
2023-01-25 09:40:08 -05:00
David Frickert 962310204f tracing: Support placeholders in span name (#5329)
Fixes https://github.com/caddyserver/caddy/issues/5171
2023-01-25 02:26:44 -05:00
Brad Fitzpatrick 98867ac346 go.mod: bump tscert package to fix Tailscale 1.34+ on Windows (#5331)
As of Tailscale 1.34.0 on Windows, Tailscale now uses a named pipe to
connect to the local tailscale service.

This pulls in tailscale/tscert#5 as reported in tailscale/tscert#4.

(Sorry, we should've noticed this earlier!)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-01-24 20:01:54 -05:00
Y.Horie 5805b3ca11 cmd: caddy fmt return code is 1 if not formatted (#5297)
* cmd: Fix caddy fmt if input isn't formatted

* Fixes #5294
* return exit 1 with an error message

* cmd: Use formattingDifference for caddy fmt

* #5294
* expose caddyfile.formattingDifference
2023-01-21 21:28:37 -07:00
Y.Horie d6d7511699 httpcaddyfile: Warn on importing empty file; skip dotfiles (#5320)
* httpcaddyfile: Change the parse rules when empty file or dotfile with a glob.

* Fixes #5295
* Empty file should just log a warning, and result in no tokens.
* The last segment of the path is '*', it should skip any dotfiles.
* The last segment of the path is '.*', it should read all dotfiles in a dir.

* httpcaddyfile: Regard empty files as import files which include only white space.
2023-01-21 10:22:36 -07:00
Y.Horie 8d6870fd06 chore: Fix typo, coral -> cobra (#5325) 2023-01-21 10:27:58 -05:00
WeidiDeng c38a040e85 httpcaddyfile: Fix handle grouping inside route (#5315)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-01-18 16:04:41 -05:00
Alexandre Vicenzi e8ad9b32c9 go.mod: Update golang.org/x/net to v0.5.0 (#5314) 2023-01-17 07:07:07 -05:00
Y.Horie 62e8b21724 chore: Fix caddyfile.replaceEnvVars return (#5311) 2023-01-17 06:57:42 -05:00
Francis Lavoie 223cbe3d0b caddyhttp: Add server-level trusted_proxies config (#5103) 2023-01-10 00:08:23 -05:00
Yannick Ihmels 66ce0c5c63 caddytls: Add test cases for Caddyfile tls options (#5293) 2023-01-09 15:18:12 -05:00
Y.Horie 845bc4d50b reverseproxy: Fix hanging for Transfer-Encoding: chunked (#5289)
* Fixes #5236
* enable request body buffering in reverse proxy
  when the request header has Transfer-Encoding: chunked
2023-01-09 00:13:34 -07:00
Emily Lange e450a7377b reverseproxy: Don't enable auto-https when --from flag is http (#5269) 2023-01-06 15:42:07 -05:00
Matt Holt d74f6fd967 reverseproxy: Set origreq in active health check (#5284)
* reverseproxy: Set origreq in active health check

Fix #5281

* Oops; dereference Request
2023-01-06 15:06:38 -05:00
Yannick Ihmels 55035d327a caddytls: Add dns_ttl config, improve Caddyfile tls options (#5287) 2023-01-06 14:44:00 -05:00
Matthew Holt 4e9ad50f65 fileserver: Add a couple test cases
With placeholders
2023-01-04 11:07:27 -07:00
Matt Holt 05a4637489 Update README.md
Attempt to fix logo that was appearing black in some browsers (perhaps due to CSP?).

Thanks to @IndeedNotJames for investigating! Hopefully this works.
2023-01-01 16:27:06 -07:00
Matt Holt bd74f94496 Update README.md
Update logo and fix test result badge
2022-12-31 10:10:32 -07:00
Francis Lavoie b40548ff61 ci: Fix goreleaser deprecation (#5270) 2022-12-28 13:11:39 -05:00
TAKAHASHI Shuuji 4e54e48409 ci: Update GitHub Actions to avoid set-output deprecation (#5271) 2022-12-28 12:05:42 -05:00
Mohammed Al Sahaf b166b90083 ci: exclude dependbot from running tests on s390x machine (#5266) 2022-12-22 14:13:47 -05:00
darkweak dac7cacd4d encode: Respect Cache-Control no-transform (#5257)
* encode: respect Cache-Control HTTP header no-transform

* encode: switch to strings.Contains
2022-12-20 13:26:53 -07:00
dependabot[bot] af93517c2d build(deps): bump goreleaser/goreleaser-action from 2 to 4 (#5264)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-19 19:47:33 -05:00
dependabot[bot] 3b724a2082 build(deps): bump actions/upload-artifact from 1 to 3 (#5262)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-19 19:14:43 -05:00
dependabot[bot] 329af5ced9 build(deps): bump actions/cache from 2 to 3 (#5263)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-19 18:56:52 -05:00
dependabot[bot] cd49847edb build(deps): bump peter-evans/repository-dispatch from 1 to 2 (#5261)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-19 18:44:54 -05:00
John Losito d3d76d6ac2 ci: Check for github action updates monthly (#5258) 2022-12-19 14:57:56 -07:00
Lukas Vogel c3b5b1811c cmd: Avoid panic when printing version without build info (#5210)
* version: don't panic if read build info doesn't work

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

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

* exit early
2022-12-19 14:23:45 -07:00
Emily Lange 4fe5e64e46 readme: white ZeroSSL text color in dark mode (#5259)
* readme: white ZeroSSL text color in dark mode

* fix: keep `valign` for GitHub mobile app
2022-12-19 13:01:30 -07:00
IndeedNotJames e10ed7b00d readme: darker variants of logos in dark mode (#5248) 2022-12-12 10:18:30 -07:00
Matthew Holt fac35db9dc go.mod: Update quic-go to v0.31.0
And fix a comment typo
2022-12-08 08:55:04 -07:00
Kyle McCullough bfaf2a8201 acme_server: Configurable default lifetime for issued certificates (#5232)
* acme_server: add certificate lifetime configuration option

Signed-off-by: Kyle McCullough <kylemcc@gmail.com>

* pki: allow intermediate cert lifetime to be configured

Signed-off-by: Kyle McCullough <kylemcc@gmail.com>

Signed-off-by: Kyle McCullough <kylemcc@gmail.com>
2022-12-06 00:12:26 -07:00
Mohammed Al Sahaf fef9cb3e05 caddytest: internalize init config into '.go' file (#5230) 2022-12-05 18:49:41 +00:00
Alban Lecocq d4a7d89f56 reverseproxy: Improve hostByHashing distribution (#5229)
* If upstreams are all using same host but with different ports
ie:
foobar:4001
foobar:4002
foobar:4003
...
Because fnv-1a has not a good enough avalanche effect
Then the hostByHashing result is not well balanced over
all upstreams

As last byte FNV input tend to affect few bits, the idea is to change
the concatenation order between the key and the upstream strings
So the upstream last byte have more impact on hash diffusion
2022-12-05 11:28:12 -07:00
Matthew Holt ae77a56ac8 Clarify some docs 2022-11-30 16:03:31 -07:00
bit 762b02789a admin: set certmagic cache logger (#5173)
same way it is set in modules/caddytls/tls.go
2022-11-23 20:49:22 -07:00
Mariano Cano 6f8fe01da1 caddypki: Use go.step.sm/crypto to generate the PKI (#5217)
This commit replaces the use of github.com/smallstep/cli to generate the
root and intermediate certificates and uses go.step.sm/crypto instead.

It also upgrades the version of github.com/smallstep/certificates to the
latest version.
2022-11-23 20:47:42 -07:00
bit ac96455a9a admin: fix certificate renewal for admin (#5169)
certmagic.New takes a template and returns pointer to the new config.
GetConfigForCert later must return a pointer to the new config not the
template.

fixes #5162
2022-11-23 11:48:37 -07:00
Francis Lavoie ee7c92ec9b reverseproxy: Mask the WS close message when we're the client (#5199)
* reverseproxy: Mask the WS close message when we're the client

* weakrand

* Bump golangci-lint version so path ignores work on Windows

* gofmt

* ugh, gofmt everything, I guess
2022-11-14 09:38:02 -07:00
Jonathan Garcia 33fdea8f26 caddypki: Prefer user-configured root instead of generating new one (#5189)
instead of generating a new root certificate at the default location
load the certificate from the configuration.
fixes: #5181
2022-11-08 12:13:46 -07:00
Ashish Kurmi 6efd1b3bb1 ci: set least privilged token for github actions for lint workflow (#5179)
* ci: set least privilged token for github actions

Signed-off-by: Ashish Kurmi <akurmi@stepsecurity.io>

* ci:reverting github actions permissions for all but lint workflow

Signed-off-by: Ashish Kurmi <akurmi@stepsecurity.io>
2022-11-06 08:01:36 +00:00
Alexander Graf 087f126cf4 caddyhttp: Canonicalize header field names (#5176) 2022-10-29 16:35:44 -04:00
Benjamin Chalmers 1fa4cb7ba1 caddytest: Increased sleep between retries to reduce flakey tests in CI (#5160)
* Incresed sleep between retries to reduce flakey tests in CI

* Also changed wait time for admin

* Modified time to make it more reliable

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2022-10-27 22:12:30 +00:00
Mohammed Al Sahaf f20a8e7aa0 cmd: replace deprecate func use (#5170) 2022-10-25 17:56:38 +03:00
Matthew Holt 798c4a3ba4 go.mod: Upgrade some dependencies
Quic-go 0.30 should be faster
2022-10-24 12:20:39 -06:00
Matthew Holt 817470dd66 httploader: Close resp body on bad status code
Related to #5158
2022-10-24 12:18:32 -06:00
Chris Lahaye bbe3663167 caddyconfig: Fix httploader leak from unused responses (#5159)
fixes #5158

Signed-off-by: Chris Lahaye <mail@chrislahaye.com>

Signed-off-by: Chris Lahaye <mail@chrislahaye.com>
2022-10-24 11:58:30 -06:00
XYenon ed503118dd caddyhttp: add placeholder {http.request.orig_uri.path.*} (#5161) 2022-10-24 11:57:50 -06:00
Matt Holt a3ae146cbd fileserver: Reject non-GET/HEAD requests (close #5166) (#5167)
* fileserver: Reject non-GET/HEAD requests (close #5166)

* Set Allow header according to RFC 9110 10.2.1
2022-10-24 10:23:57 -06:00
Matt Holt 4bf6cb4199 fileserver: Reject ADS and short name paths; trim trailing dots and spaces on Windows (#5148)
* fileserver: Reject ADS and short name paths

* caddyhttp: Trim trailing space and dot on Windows

Windows ignores trailing dots and spaces in filenames.

* Fix test

* Adjust path filters

* Revert Windows test

* Actually revert the test

* Just check for colons
2022-10-18 21:55:25 -06:00
Scott Mebberson 72e7edda1f map: Clarified how destination values should be formatted (#5156) 2022-10-18 18:14:53 -06:00
BakaFT a999b70727 cmd: Add missing \n to HelpTemplate (#5151) 2022-10-17 11:51:41 +03:00
Francis Lavoie 1cd594963e docs: Fix templates documentation, stray newline breaks godoc (#5149) 2022-10-16 12:25:44 -04:00
Matt Holt 6bad878a22 httpcaddyfile: Improve detection of indistinguishable TLS automation policies (#5120)
* httpcaddyfile: Skip some logic if auto_https off

* Try removing this check altogether...

* Refine test timeouts slightly, sigh

* caddyhttp: Assume udp for unrecognized network type

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

* Add comment to document my lack of knowledge

* Clean up and prepare to merge

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

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

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

* caddyconfig: dependency-free http config loading retries

* caddyconfig: support `retry_delay` in http loader

* httploader: Implement retries

* Apply suggestions from code review

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

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

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

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

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

* Prefer embedded version, then CustomVersion

* Prefer "unknown" for full version over empty

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

* Add test for ip_mask filter

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

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

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

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

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

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

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

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

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

Prefer caddy.NetworkAddress.Listen() instead.

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

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

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

- Unlink unix socket before and after use

* Appease the linter

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

Fixes setting the first scheme.

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

* Don't serve HTTP/3 over unix sockets

This requires upstream support, if even useful

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

* Fix build tag

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

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

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

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

This avoids blocking during config reloads.

* Don't quit process until servers shut down

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

* caddyhttp: Even faster shutdowns

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

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

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

* Fix comment typo

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

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

* Minor tweaks

* Run gofmt

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

* Resolve merge conflict with master

* Add Caddyfile support; various fixes

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

* Appease linter

* Minor tweaks

* Add doc comment

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

Related to #4644

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

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

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

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

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

* Refactor to use vars middleware

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

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

* Refactor solution; simplify, add descriptive comment

* Move network to host, not copy

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

Fix #5028

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

* Update modules/caddyhttp/matchers.go

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

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

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

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

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

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

* Fix tests

* Fix bugs and tests

* Attempt Windows fix, sigh

* debug Windows, WIP

* Continue debugging Windows

* Another attempt at Windows

* Plz Windows

* Cmon...

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

This adds support for early hints in the static_response handler.

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

* Deprecate scrypt, allow using decoded bcrypt hashes

* Add TODO note

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

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

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

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

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

* CloseMessage --> closeMessage

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

* Use httpguts, duh

* Use map instead of sync.Map

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

Just an experiment for now

* Fix lint by logging error

* TCP Keepalive configuration (#4865)

* initial attempt at TCP Keepalive configuration

* core: implement tcp-keepalive for linux

* move canSetKeepAlive interface

* Godoc for keepalive server parameter

* handle return values

* log keepalive errors

* Clean up after bad merge

* Merge in pluggable network types

From 1edc1a45e3

* Slight refactor, fix from recent merge conflict

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

* close underlying connection when operation failed

* allocate bufWriter and streamWriter only once

* refactor record writing

* rebase from master

* handle err

* Fix type assertion

Also reduce some duplication

* Refactor client and clientCloser for logging

Should reduce allocations

* Minor cosmetic adjustments; apply Apache license

* Appease the linter

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

* Update templates.go

* Update templates.go

* Fix gofmt

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

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

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

This can be useful for custom listeners.

This feature/API is experimental and may change!

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

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

* add `manpage` command

* limit Caddy tagline to root `help` only

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

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

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

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

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

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

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

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

* Virtualize FS for file matcher; minor tweaks

* Fix tests and rename dirFS -> osFS

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

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

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

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

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

* Add Caddyfile support for `retry_match`

* Refactor to deduplicate matcher parsing logic

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

* admin: Minor refactor of etag facilities

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

* support etags

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

* Refactor and simplify setScheme

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

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

* Move TLS servername replacement into it's own function

* Moved SNI servername replacement into httptransport.

* Solve issue when dynamic upstreams use wrong protocol upstream.

* Revert previous commit.

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

* Added SkipTLSPorts option to http transport.

* Fix typo in test config file.

* Rename config option as suggested by Matt

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

* Update code to match renamed config option.

* Fix typo in config option name.

* Fix another typo that I missed.

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

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

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

* Move TLS servername replacement into it's own function

* Moved SNI servername replacement into httptransport.

* Solve issue when dynamic upstreams use wrong protocol upstream.

* Revert previous commit.

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

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

* Update modules/caddyhttp/reverseproxy/httptransport.go

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

* Update modules/caddyhttp/reverseproxy/httptransport.go

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

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

* caddytls: Cleanups for ClientCertValidator changes

caddytls: Cleanups for ClientCertValidator changes

* Update modules/caddytls/connpolicy.go

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

* Update modules/caddytls/connpolicy.go

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

* Update modules/caddytls/connpolicy.go

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

* Update modules/caddytls/connpolicy.go

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

* Update modules/caddytls/connpolicy.go

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

* Update modules/caddytls/connpolicy.go

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

* Unexported field Validators, corrected renaming of LeafVerificationValidator to LeafCertClientAuth

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

* Apply suggestions from code review

* Register module; fix compilation

* Add log for deprecation notice

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

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

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

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

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

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

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

* Fix ordering of server blocks
2022-05-08 21:32:10 -04:00
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
Francis Lavoie a779e1b383 fastcgi: Fix Caddyfile parsing when handle_response is used (#4342) 2021-09-11 14:12:21 -06:00
Matthew Holt 46ab93be51 go.mod: Update CertMagic
Adds one more debug log
2021-09-03 11:42:13 -06:00
Mohammed Al Sahaf e0fc46a911 ci: revert workaround implemented in #4306 (#4328) 2021-09-03 10:05:04 -04:00
peymaneh 9f6393c64c cmd: export CaddyVersion(), Commands() (#4316)
* cmd: Export CaddyVersion()

* cmd: Add getter Commands()
2021-09-01 18:08:02 -06:00
Francis Lavoie 105dac8c2a ci: Only test cross-build on latest Go version (#4319)
This generated way too many test jobs, which weren't really that useful. Cross-build is just to keep us posted on which architectures are building okay, so it's not necessary to do it twice. Only plan9 is not working at this point (see https://github.com/caddyserver/caddy/issues/3615)
2021-08-31 13:44:07 -06:00
Steffen Brüheim 4ebf100f09 encode: ignore flushing until after first write (#4318)
* encode: ignore flushing until after first write (fix #4314)

The first write will determine if encoding has to be done and will add an Content-Encoding. Until then Flushing has to be delayed so the Content-Encoding header can be added before headers and status code is written. (A passthrough flush would write header and status code)

* Update modules/caddyhttp/encode/encode.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2021-08-31 13:36:36 -06:00
440 changed files with 46108 additions and 11500 deletions
+5
View File
@@ -0,0 +1,5 @@
[*]
end_of_line = lf
[caddytest/integration/caddyfile_adapt/*.caddyfiletest]
indent_style = tab
+1
View File
@@ -0,0 +1 @@
*.go text eol=lf
+17 -7
View File
@@ -1,7 +1,7 @@
Contributing to Caddy
=====================
Welcome! Thank you for choosing to be a part of our community. Caddy wouldn't be great without your involvement!
Welcome! Thank you for choosing to be a part of our community. Caddy wouldn't be nearly as excellent without your involvement!
For starters, we invite you to join [the Caddy forum](https://caddy.community) where you can hang out with other Caddy users and developers.
@@ -25,7 +25,7 @@ Other menu items:
You can have a huge impact on the project by helping with its code. To contribute code to Caddy, first submit or comment in an issue to discuss your contribution, then open a [pull request](https://github.com/caddyserver/caddy/pulls) (PR). If you're new to our community, that's okay: **we gladly welcome pull requests from anyone, regardless of your native language or coding experience.** You can get familiar with Caddy's code base by using [code search at Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy).
We hold contributions to a high standard for quality :bowtie:, so don't be surprised if we ask for revisions&mdash;even if it seems small or insignificant. Please don't take it personally. :blue_heart: If your change is on the right track, we can guide you to make it mergable.
We hold contributions to a high standard for quality :bowtie:, so don't be surprised if we ask for revisions&mdash;even if it seems small or insignificant. Please don't take it personally. :blue_heart: If your change is on the right track, we can guide you to make it mergeable.
Here are some of the expectations we have of contributors:
@@ -35,19 +35,29 @@ Here are some of the expectations we have of contributors:
- **Keep related commits together in a PR.** We do want pull requests to be small, but you should also keep multiple related commits in the same PR if they rely on each other.
- **Write tests.** Tests are essential! Written properly, they ensure your change works, and that other changes in the future won't break your change. CI checks should pass.
- **Write tests.** Good, automated tests are very valuable! Written properly, they ensure your change works, and that other changes in the future won't break your change. CI checks should pass.
- **Benchmarks should be included for optimizations.** Optimizations sometimes make code harder to read or have changes that are less than obvious. They should be proven with benchmarks or profiling.
- **Benchmarks should be included for optimizations.** Optimizations sometimes make code harder to read or have changes that are less than obvious. They should be proven with benchmarks and profiling.
- **[Squash](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) insignificant commits.** Every commit should be significant. Commits which merely rewrite a comment or fix a typo can be combined into another commit that has more substance. Interactive rebase can do this, or a simpler way is `git reset --soft <diverging-commit>` then `git commit -s`.
- **Own your contributions.** Caddy is a growing project, and it's much better when individual contributors help maintain their change after it is merged.
- **Be responsible for and maintain your contributions.** Caddy is a growing project, and it's much better when individual contributors help maintain their change after it is merged.
- **Use comments properly.** We expect good godoc comments for package-level functions, types, and values. Comments are also useful whenever the purpose for a line of code is not obvious.
- **Pull requests may still get closed.** The longer a PR stays open and idle, the more likely it is to be closed. If we haven't reviewed it in a while, it probably means the change is not a priority. Please don't take this personally, we're trying to balance a lot of tasks! If nobody else has commented or reacted to the PR, it likely means your change is useful only to you. The reality is this happens quite a bit. We don't tend to accept PRs that aren't generally helpful. For these reasons or others, the PR may get closed even after a review. We are not obligated to accept all proposed changes, even if the best justification we can give is something vague like, "It doesn't sit right." Sometimes PRs are just the wrong thing or the wrong time. Because it is open source, you can always build your own modified version of Caddy with a change you need, even if we reject it in the official repo.
- **Pull requests may still get closed.** The longer a PR stays open and idle, the more likely it is to be closed. If we haven't reviewed it in a while, it probably means the change is not a priority. Please don't take this personally, we're trying to balance a lot of tasks! If nobody else has commented or reacted to the PR, it likely means your change is useful only to you. The reality is this happens quite a lot. We don't tend to accept PRs that aren't generally helpful. For these reasons or others, the PR may get closed even after a review. We are not obligated to accept all proposed changes, even if the best justification we can give is something vague like, "It doesn't sit right." Sometimes PRs are just the wrong thing or the wrong time. Because it is open source, you can always build your own modified version of Caddy with a change you need, even if we reject it in the official repo. Plus, because Caddy is extensible, it's possible your feature could make a great plugin instead!
We often grant [collaborator status](#collaborator-instructions) to contributors who author one or more significant, high-quality PRs that are merged into the code base!
- **You certify that you wrote and comprehend the code you submit.** The Caddy project welcomes original contributions that comply with [our CLA](https://cla-assistant.io/caddyserver/caddy), meaning that authors must be able to certify that they created or have rights to the code they are contributing. In addition, we require that code is not simply copy-pasted from Q/A sites or AI language models without full comprehension and rigorous testing. In other words: contributors are allowed to refer to communities for assistance and use AI tools such as language models for inspiration, but code which originates from or is assisted by these resources MUST be:
- Licensed for you to freely share
- Fully comprehended by you (be able to explain every line of code)
- Verified by automated tests when feasible, or thorough manual tests otherwise
We have found that current language models (LLMs, like ChatGPT) may understand code syntax and even problem spaces to an extent, but often fail in subtle ways to convey true knowledge and produce correct algorithms. Integrated tools such as GitHub Copilot and Sourcegraph Cody may be used for inspiration, but code generated by these tools still needs to meet our criteria for licensing, human comprehension, and testing. These tools may be used to help write code comments and tests as long as you can certify they are accurate and correct. Note that it is often more trouble than it's worth to certify that Copilot (for example) is not giving you code that is possibly plagiarised, unlicensed, or licensed with incompatible terms -- as the Caddy project cannot accept such contributions. If that's too difficult for you (or impossible), then we recommend using these resources only for inspiration and write your own code. Ultimately, you (the contributor) are responsible for the code you're submitting.
As a courtesy to reviewers, we kindly ask that you disclose when contributing code that was generated by an AI tool or copied from another website so we can be aware of what to look for in code review.
We often grant [collaborator status](#collaborator-instructions) to contributors who author one or more significant, high-quality PRs that are merged into the code base.
#### HOW TO MAKE A PULL REQUEST TO CADDY
+4 -4
View File
@@ -7,7 +7,7 @@ The Caddy project would like to make sure that it stays on top of all practicall
| Version | Supported |
| ------- | ------------------ |
| 2.x | :white_check_mark: |
| 2.x | ✔️ |
| 1.x | :x: |
| < 1.x | :x: |
@@ -24,7 +24,7 @@ We do not accept reports if the steps imply or require a compromised system or t
Client-side exploits are out of scope. In other words, it is not a bug in Caddy if the web browser does something unsafe, even if the downloaded content was served by Caddy. (Those kinds of exploits can generally be mitigated by proper configuration of HTTP headers.) As a general rule, the content served by Caddy is not considered in scope because content is configurable by the site owner or the associated web application.
Security bugs in code dependencies are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code.
Security bugs in code dependencies (including Go's standard library) are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code.
## Reporting a Vulnerability
@@ -42,13 +42,13 @@ We'll need enough information to verify the bug and make a patch. To speed thing
- Specific minimal steps to reproduce the issue from scratch
- A working patch
Please DO NOT use containers, VMs, cloud instances or services, or any other complex infrastructure in your steps. Always prefer `curl` instead of web browsers.
Please DO NOT use containers, VMs, cloud instances or services, or any other complex infrastructure in your steps. Always prefer `curl -v` instead of web browsers.
We consider publicly-registered domain names to be public information. This necessary in order to maintain the integrity of certificate transparency, public DNS, and other public trust systems. Do not redact domain names from your reports. The actual content of your domain name affects Caddy's behavior, so we need the exact domain name(s) to reproduce with, or your report will be ignored.
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.
+7
View File
@@ -0,0 +1,7 @@
---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
+56 -43
View File
@@ -18,35 +18,53 @@ jobs:
# Default is true, cancels jobs for other platforms in the matrix if one fails
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
go: [ '1.16', '1.17' ]
os:
- linux
- mac
- windows
go:
- '1.21'
- '1.22'
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.21'
GO_SEMVER: '~1.21.0'
- go: '1.22'
GO_SEMVER: '~1.22.3'
# Set some variables per OS, usable via ${{ matrix.VAR }}
# OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories)
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
# SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
include:
- os: ubuntu-latest
- os: linux
OS_LABEL: ubuntu-latest
CADDY_BIN_PATH: ./cmd/caddy/caddy
SUCCESS: 0
- os: macos-latest
- os: mac
OS_LABEL: macos-14
CADDY_BIN_PATH: ./cmd/caddy/caddy
SUCCESS: 0
- os: windows-latest
- os: windows
OS_LABEL: windows-latest
CADDY_BIN_PATH: ./cmd/caddy/caddy.exe
SUCCESS: 'True'
runs-on: ${{ matrix.os }}
runs-on: ${{ matrix.OS_LABEL }}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.GO_SEMVER }}
check-latest: true
# These tools would be useful if we later decide to reinvestigate
# publishing test/coverage reports to some tool for easier consumption
@@ -55,10 +73,11 @@ jobs:
# go get github.com/axw/gocov/gocov
# go get github.com/AlekSi/gocov-xml
# go get -u github.com/jstemmer/go-junit-report
# echo "::add-path::$(go env GOPATH)/bin"
# echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- name: Print Go version and environment
id: vars
shell: bash
run: |
printf "Using go at: $(which go)\n"
printf "Go version: $(go version)\n"
@@ -68,16 +87,7 @@ jobs:
env
printf "Git version: $(git version)\n\n"
# Calculate the short SHA1 hash of the git commit
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
uses: actions/cache@v2
with:
path: ${{ steps.vars.outputs.go_cache }}
key: ${{ runner.os }}-${{ matrix.go }}-go-ci-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go }}-go-ci
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Get dependencies
run: |
@@ -89,13 +99,20 @@ jobs:
env:
CGO_ENABLED: 0
run: |
go build -trimpath -ldflags="-w -s" -v
go build -tags nobdger -trimpath -ldflags="-w -s" -v
- name: Smoke test Caddy
working-directory: ./cmd/caddy
run: |
./caddy start
./caddy stop
- name: Publish Build Artifact
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v4
with:
name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
path: ${{ matrix.CADDY_BIN_PATH }}
compression-level: 0
# Commented bits below were useful to allow the job to continue
# even if the tests fail, so we can publish the report separately
@@ -105,8 +122,8 @@ jobs:
# continue-on-error: true
run: |
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
go test -v -coverprofile="cover-profile.out" -short -race ./...
# echo "::set-output name=status::$?"
go test -tags nobadger -v -coverprofile="cover-profile.out" -short -race ./...
# echo "status=$?" >> $GITHUB_OUTPUT
# Relevant step if we reinvestigate publishing test/coverage reports
# - name: Prepare coverage reports
@@ -118,7 +135,7 @@ jobs:
# To return the correct result even though we set 'continue-on-error: true'
# - name: Coerce correct build result
# if: matrix.os != 'windows-latest' && steps.step_test.outputs.status != ${{ matrix.SUCCESS }}
# if: matrix.os != 'windows' && steps.step_test.outputs.status != ${{ matrix.SUCCESS }}
# run: |
# echo "step_test ${{ steps.step_test.outputs.status }}\n"
# exit 1
@@ -126,11 +143,11 @@ jobs:
s390x-test:
name: test (s390x on IBM Z)
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == github.repository
if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]'
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
steps:
- name: Checkout code into the Go module directory
uses: actions/checkout@v2
- name: Checkout code
uses: actions/checkout@v4
- name: Run Tests
run: |
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
@@ -139,30 +156,26 @@ jobs:
short_sha=$(git rev-parse --short HEAD)
# The environment is fresh, so there's no point in keeping accepting and adding the key.
rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . caddy-ci@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t caddy-ci@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -v ./..."
rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . "$CI_USER"@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$CI_USER"@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -tags nobadger -v ./..."
test_result=$?
# There's no need leaving the files around
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null caddy-ci@ci-s390x.caddyserver.com "rm -rf /var/tmp/'$short_sha'"
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$CI_USER"@ci-s390x.caddyserver.com "rm -rf /var/tmp/'$short_sha'"
echo "Test exit code: $test_result"
exit $test_result
env:
SSH_KEY: ${{ secrets.S390X_SSH_KEY }}
CI_USER: ${{ secrets.CI_USER }}
goreleaser-check:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
- name: Create 'caddy-build'
run: mkdir -p caddy-build
- name: Checkout code
uses: actions/checkout@v4
- uses: goreleaser/goreleaser-action@v2
- uses: goreleaser/goreleaser-action@v5
with:
version: latest
args: check
env:
TAG: ${{ steps.vars.outputs.version_tag }}
+29 -22
View File
@@ -11,19 +11,41 @@ on:
- 2.*
jobs:
cross-build-test:
build:
strategy:
fail-fast: false
matrix:
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
go: [ '1.16', '1.17' ]
goos:
- 'aix'
- 'linux'
- 'solaris'
- 'illumos'
- 'dragonfly'
- 'freebsd'
- 'openbsd'
- 'windows'
- 'darwin'
- 'netbsd'
go:
- '1.22'
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.22'
GO_SEMVER: '~1.22.3'
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
go-version: ${{ matrix.GO_SEMVER }}
check-latest: true
- name: Print Go version and environment
id: vars
@@ -34,29 +56,14 @@ jobs:
go env
printf "\n\nSystem environment:\n\n"
env
echo "::set-output name=go_cache::$(go env GOCACHE)"
- name: Cache the build cache
uses: actions/cache@v2
with:
path: ${{ steps.vars.outputs.go_cache }}
key: cross-build-go${{ matrix.go }}-${{ matrix.goos }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
cross-build-go${{ matrix.go }}-${{ matrix.goos }}
- name: Checkout code into the Go module directory
uses: actions/checkout@v2
- name: Run Build
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goos == 'aix' && 'ppc64' || 'amd64' }}
shell: bash
continue-on-error: true
working-directory: ./cmd/caddy
run: |
GOOS=$GOOS go build -trimpath -o caddy-"$GOOS"-amd64 2> /dev/null
if [ $? -ne 0 ]; then
echo "::warning ::$GOOS Build Failed"
exit 0
fi
GOOS=$GOOS GOARCH=$GOARCH go build -tags nobadger -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null
+47 -5
View File
@@ -10,16 +10,58 @@ on:
- master
- 2.*
permissions:
contents: read
jobs:
# From https://github.com/golangci/golangci-lint-action
golangci:
permissions:
contents: read # for actions/checkout to fetch code
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
name: lint
runs-on: ubuntu-latest
strategy:
matrix:
os:
- linux
- mac
- windows
include:
- os: linux
OS_LABEL: ubuntu-latest
- os: mac
OS_LABEL: macos-14
- os: windows
OS_LABEL: windows-latest
runs-on: ${{ matrix.OS_LABEL }}
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
version: v1.31
go-version: '~1.22.3'
check-latest: true
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.55
# Windows times out frequently after about 5m50s if we don't set a longer timeout.
args: --timeout 10m
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true
govulncheck:
runs-on: ubuntu-latest
steps:
- name: govulncheck
uses: golang/govulncheck-action@v1
with:
go-version-input: '~1.22.3'
check-latest: true
+44 -31
View File
@@ -10,23 +10,40 @@ jobs:
name: Release
strategy:
matrix:
os: [ ubuntu-latest ]
go: [ '1.17' ]
os:
- ubuntu-latest
go:
- '1.22'
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.22'
GO_SEMVER: '~1.22.3'
runs-on: ${{ matrix.os }}
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
# https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings
permissions:
id-token: write
# https://docs.github.com/en/rest/overview/permissions-required-for-github-apps#permission-on-contents
# "Releases" is part of `contents`, so it needs the `write`
contents: write
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.GO_SEMVER }}
check-latest: true
# Force fetch upstream tags -- because 65 minutes
# tl;dr: actions/checkout@v2 runs this line:
# tl;dr: actions/checkout@v4 runs this line:
# 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:
# git fetch --prune --unshallow
@@ -46,9 +63,8 @@ jobs:
go env
printf "\n\nSystem environment:\n\n"
env
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=go_cache::$(go env GOCACHE)"
echo "version_tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
# Add "pip install" CLI tools to PATH
echo ~/.local/bin >> $GITHUB_PATH
@@ -60,10 +76,10 @@ jobs:
TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"`
TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"`
TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"`
echo "::set-output name=tag_major::${TAG_MAJOR}"
echo "::set-output name=tag_minor::${TAG_MINOR}"
echo "::set-output name=tag_patch::${TAG_PATCH}"
echo "::set-output name=tag_special::${TAG_SPECIAL}"
echo "tag_major=${TAG_MAJOR}" >> $GITHUB_OUTPUT
echo "tag_minor=${TAG_MINOR}" >> $GITHUB_OUTPUT
echo "tag_patch=${TAG_PATCH}" >> $GITHUB_OUTPUT
echo "tag_special=${TAG_SPECIAL}" >> $GITHUB_OUTPUT
# Cloudsmith CLI tooling for pushing releases
# See https://help.cloudsmith.io/docs/cli
@@ -80,30 +96,27 @@ jobs:
# tags are only accepted if signed by Matt's key
git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1
- name: Cache the build cache
uses: actions/cache@v2
with:
path: ${{ steps.vars.outputs.go_cache }}
key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go${{ matrix.go }}-release
- name: Create the 'caddy-build' dir for GoReleaser
run: |
mkdir -p caddy-build
- name: Install Cosign
uses: sigstore/cosign-installer@main
- name: Cosign version
run: cosign version
- name: Install Syft
uses: anchore/sbom-action/download-syft@main
- name: Syft version
run: syft version
# GoReleaser will take care of publishing those artifacts into the release
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
uses: goreleaser/goreleaser-action@v5
with:
version: latest
args: release --rm-dist
args: release --clean --timeout 60m
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.vars.outputs.version_tag }}
COSIGN_EXPERIMENTAL: 1
# 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.
# See https://gemfury.com/caddy/deb:caddy
- name: Publish .deb to Gemfury
+4 -3
View File
@@ -10,14 +10,15 @@ jobs:
name: Release Published
strategy:
matrix:
os: [ ubuntu-latest ]
os:
- ubuntu-latest
runs-on: ${{ matrix.os }}
steps:
# See https://github.com/peter-evans/repository-dispatch
- name: Trigger event on caddyserver/dist
uses: peter-evans/repository-dispatch@v1
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/dist
@@ -25,7 +26,7 @@ jobs:
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
- name: Trigger event on caddyserver/caddy-docker
uses: peter-evans/repository-dispatch@v1
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/caddy-docker
+4
View File
@@ -1,7 +1,9 @@
_gitignore/
*.log
Caddyfile
Caddyfile.*
!caddyfile/
!caddyfile.go
# artifacts from pprof tooling
*.prof
@@ -10,6 +12,8 @@ Caddyfile
# build artifacts and helpers
cmd/caddy/caddy
cmd/caddy/caddy.exe
cmd/caddy/tmp/*.exe
cmd/caddy/.env
# mac specific
.DS_Store
+91 -21
View File
@@ -1,39 +1,82 @@
linters-settings:
errcheck:
ignore: fmt:.*,io/ioutil:^Read.*,go.uber.org/zap/zapcore:^Add.*
ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.*
ignoretests: true
gci:
sections:
- standard # Standard section: captures all standard packages.
- default # Default section: contains all imports that could not be matched to another section type.
- prefix(github.com/caddyserver/caddy/v2/cmd) # ensure that this is always at the top and always has a line break.
- prefix(github.com/caddyserver/caddy) # Custom section: groups all imports with the specified Prefix.
# Skip generated files.
# Default: true
skip-generated: true
# Enable custom order of sections.
# If `true`, make the section order the same as the order of `sections`.
# Default: false
custom-order: true
exhaustive:
ignore-enum-types: reflect.Kind|svc.Cmd
linters:
disable-all: true
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- deadcode
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- errcheck
- errname
- exhaustive
- exportloopref
- gci
- gofmt
- goimports
- gofumpt
- gosec
- gosimple
- govet
- ineffassign
- importas
- misspell
- prealloc
- promlinter
- sloglint
- sqlclosecheck
- staticcheck
- structcheck
- tenv
- testableexamples
- testifylint
- tparallel
- typecheck
- unconvert
- unused
- varcheck
- wastedassign
- whitespace
- zerologlint
# these are implicitly disabled:
# - asciicheck
# - containedctx
# - contextcheck
# - cyclop
# - depguard
# - dogsled
# - dupl
# - exhaustive
# - exportloopref
# - errchkjson
# - errorlint
# - exhaustruct
# - execinquery
# - exhaustruct
# - forbidigo
# - forcetypeassert
# - funlen
# - gci
# - ginkgolinter
# - gocheckcompilerdirectives
# - gochecknoglobals
# - gochecknoinits
# - gochecksumtype
# - gocognit
# - goconst
# - gocritic
@@ -41,27 +84,47 @@ linters:
# - godot
# - godox
# - goerr113
# - gofumpt
# - goheader
# - golint
# - gomnd
# - gomoddirectives
# - gomodguard
# - goprintffuncname
# - interfacer
# - gosmopolitan
# - grouper
# - inamedparam
# - interfacebloat
# - ireturn
# - lll
# - maligned
# - loggercheck
# - maintidx
# - makezero
# - mirror
# - musttag
# - nakedret
# - nestif
# - nilerr
# - nilnil
# - nlreturn
# - noctx
# - nolintlint
# - nonamedreturns
# - nosprintfhostport
# - paralleltest
# - perfsprint
# - predeclared
# - protogetter
# - reassign
# - revive
# - rowserrcheck
# - scopelint
# - sqlclosecheck
# - stylecheck
# - tagalign
# - tagliatelle
# - testpackage
# - thelper
# - unparam
# - whitespace
# - usestdlibvars
# - varnamelen
# - wrapcheck
# - wsl
run:
@@ -80,19 +143,26 @@ output:
issues:
exclude-rules:
# we aren't calling unknown URL
- text: "G107" # G107: Url provided to HTTP request as taint input
- text: 'G107' # G107: Url provided to HTTP request as taint input
linters:
- gosec
# as a web server that's expected to handle any template, this is totally in the hands of the user.
- text: "G203" # G203: Use of unescaped data in HTML templates
- text: 'G203' # G203: Use of unescaped data in HTML templates
linters:
- gosec
# we're shelling out to known commands, not relying on user-defined input.
- text: "G204" # G204: Audit use of command execution
- text: 'G204' # G204: Audit use of command execution
linters:
- gosec
# the choice of weakrand is deliberate, hence the named import "weakrand"
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go
text: "G404" # G404: Insecure random number source (rand)
text: 'G404' # G404: Insecure random number source (rand)
linters:
- gosec
- path: modules/caddyhttp/reverseproxy/streaming.go
text: 'G404' # G404: Insecure random number source (rand)
linters:
- gosec
- path: modules/logging/filters.go
linters:
- dupl
+88 -12
View File
@@ -4,18 +4,26 @@ before:
# This is so we can run goreleaser on tag without Git complaining of being dirty. The main.go in cmd/caddy directory
# cannot be built within that directory due to changes necessary for the build causing Git to be dirty, which
# subsequently causes gorleaser to refuse running.
- rm -rf caddy-build caddy-dist vendor
# vendor Caddy deps
- go mod vendor
- mkdir -p caddy-build
- cp cmd/caddy/main.go caddy-build/main.go
- cp ./go.mod caddy-build/go.mod
- sed -i.bkp 's|github.com/caddyserver/caddy/v2|caddy|g' ./caddy-build/go.mod
- /bin/sh -c 'cd ./caddy-build && go mod init caddy'
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
- go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod
# as of Go 1.16, `go` commands no longer automatically change go.{mod,sum}. We now have to explicitly
# run `go mod tidy`. The `/bin/sh -c '...'` is because goreleaser can't find cd in PATH without shell invocation.
- /bin/sh -c 'cd ./caddy-build && go mod tidy'
# vendor the deps of the prepared to-build module
- /bin/sh -c 'cd ./caddy-build && go mod vendor'
- git clone --depth 1 https://github.com/caddyserver/dist caddy-dist
- mkdir -p caddy-dist/man
- go mod download
- go run cmd/caddy/main.go manpage --directory ./caddy-dist/man
- gzip -r ./caddy-dist/man/
- /bin/sh -c 'go run cmd/caddy/main.go completion bash > ./caddy-dist/scripts/bash-completion'
builds:
- env:
@@ -35,10 +43,11 @@ builds:
- arm64
- s390x
- ppc64le
- riscv64
goarm:
- 5
- 6
- 7
- "5"
- "6"
- "7"
ignore:
- goos: darwin
goarch: arm
@@ -46,28 +55,93 @@ builds:
goarch: ppc64le
- goos: darwin
goarch: s390x
- goos: darwin
goarch: riscv64
- goos: windows
goarch: ppc64le
- goos: windows
goarch: s390x
- goos: windows
goarch: riscv64
- goos: freebsd
goarch: ppc64le
- goos: freebsd
goarch: s390x
- goos: freebsd
goarch: riscv64
- goos: freebsd
goarch: arm
goarm: 5
goarm: "5"
flags:
- -trimpath
- -mod=readonly
ldflags:
- -s -w
tags:
- nobadger
signs:
- cmd: cosign
signature: "${artifact}.sig"
certificate: '{{ trimsuffix (trimsuffix .Env.artifact ".zip") ".tar.gz" }}.pem'
args: ["sign-blob", "--yes", "--output-signature=${signature}", "--output-certificate", "${certificate}", "${artifact}"]
artifacts: all
sboms:
- artifacts: binary
documents:
- >-
{{ .ProjectName }}_
{{- .Version }}_
{{- if eq .Os "darwin" }}mac{{ else }}{{ .Os }}{{ end }}_
{{- .Arch }}
{{- with .Arm }}v{{ . }}{{ end }}
{{- with .Mips }}_{{ . }}{{ end }}
{{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}.sbom
cmd: syft
args: ["$artifact", "--file", "${document}", "--output", "cyclonedx-json"]
archives:
- format_overrides:
- id: default
format_overrides:
- goos: windows
format: zip
replacements:
darwin: mac
name_template: >-
{{ .ProjectName }}_
{{- .Version }}_
{{- if eq .Os "darwin" }}mac{{ else }}{{ .Os }}{{ end }}_
{{- .Arch }}
{{- with .Arm }}v{{ . }}{{ end }}
{{- with .Mips }}_{{ . }}{{ end }}
{{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}
# package the 'caddy-build' directory into a tarball,
# allowing users to build the exact same set of files as ours.
- id: source
meta: true
name_template: "{{ .ProjectName }}_{{ .Version }}_buildable-artifact"
files:
- src: LICENSE
dst: ./LICENSE
- src: README.md
dst: ./README.md
- src: AUTHORS
dst: ./AUTHORS
- src: ./caddy-build
dst: ./
source:
enabled: true
name_template: '{{ .ProjectName }}_{{ .Version }}_src'
format: 'tar.gz'
# Additional files/template/globs you want to add to the source archive.
#
# Default: empty.
files:
- vendor
checksum:
algorithm: sha512
@@ -75,7 +149,7 @@ nfpms:
- id: default
package_name: caddy
vendor: Light Code Labs
vendor: Dyanim
homepage: https://caddyserver.com
maintainer: Matthew Holt <mholt@users.noreply.github.com>
description: |
@@ -97,19 +171,21 @@ nfpms:
- src: ./caddy-dist/welcome/index.html
dst: /usr/share/caddy/index.html
- src: ./caddy-dist/scripts/completions/bash-completion
- src: ./caddy-dist/scripts/bash-completion
dst: /etc/bash_completion.d/caddy
- src: ./caddy-dist/config/Caddyfile
dst: /etc/caddy/Caddyfile
type: config
- src: ./caddy-dist/man/*
dst: /usr/share/man/man8/
scripts:
postinstall: ./caddy-dist/scripts/postinstall.sh
preremove: ./caddy-dist/scripts/preremove.sh
postremove: ./caddy-dist/scripts/postremove.sh
release:
github:
owner: caddyserver
+29 -15
View File
@@ -1,13 +1,19 @@
<p align="center">
<a href="https://caddyserver.com"><img src="https://user-images.githubusercontent.com/1128849/36338535-05fb646a-136f-11e8-987b-e6901e717d5a.png" alt="Caddy" width="450"></a>
<a href="https://caddyserver.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/1128849/210187358-e2c39003-9a5e-4dd5-a783-6deb6483ee72.svg">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1128849/210187356-dfb7f1c5-ac2e-43aa-bb23-fc014280ae1f.svg">
<img src="https://user-images.githubusercontent.com/1128849/210187356-dfb7f1c5-ac2e-43aa-bb23-fc014280ae1f.svg" alt="Caddy" width="550">
</picture>
</a>
<br>
<h3 align="center">a <a href="https://zerossl.com"><img src="https://caddyserver.com/resources/images/zerossl-logo.svg" height="28" valign="middle"></a> project</h3>
<h3 align="center">a <a href="https://zerossl.com"><img src="https://user-images.githubusercontent.com/55066419/208327323-2770dc16-ec09-43a0-9035-c5b872c2ad7f.svg" height="28" style="vertical-align: -7.7px" valign="middle"></a> project</h3>
</p>
<hr>
<h3 align="center">Every site on HTTPS</h3>
<p align="center">Caddy is an extensible server platform that uses TLS by default.</p>
<p align="center">
<a href="https://github.com/caddyserver/caddy/actions?query=workflow%3ACross-Platform"><img src="https://github.com/caddyserver/caddy/workflows/Cross-Platform/badge.svg"></a>
<a href="https://github.com/caddyserver/caddy/actions/workflows/ci.yml"><img src="https://github.com/caddyserver/caddy/actions/workflows/ci.yml/badge.svg"></a>
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-%23007d9c.svg"></a>
<br>
<a href="https://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" alt="@caddyserver on Twitter"></a>
@@ -40,11 +46,17 @@
<p align="center">
<b>Powered by</b>
<br>
<a href="https://github.com/caddyserver/certmagic"><img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250"></a>
<a href="https://github.com/caddyserver/certmagic">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/55066419/206946718-740b6371-3df3-4d72-a822-47e4c48af999.png">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png">
<img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250">
</picture>
</a>
</p>
## [Features](https://caddyserver.com/v2)
## [Features](https://caddyserver.com/features)
- **Easy configuration** with the [Caddyfile](https://caddyserver.com/docs/caddyfile)
- **Powerful configuration** with its [native JSON config](https://caddyserver.com/docs/json/)
@@ -57,28 +69,28 @@
- Multi-issuer fallback
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
- **Production-ready** after serving trillions of requests and managing millions of TLS certificates
- **Scales to tens of thousands of sites** ... and probably more
- **HTTP/1.1, HTTP/2, and experimental HTTP/3** support
- **Scales to hundreds of thousands of sites** as proven in production
- **HTTP/1.1, HTTP/2, and HTTP/3** all supported by default
- **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat
- **Runs anywhere** with **no external dependencies** (not even libc)
- Written in Go, a language with higher **memory safety guarantees** than other servers
- Actually **fun to use**
- So, so much more to [discover](https://caddyserver.com/v2)
- So much more to [discover](https://caddyserver.com/features)
## Install
The simplest, cross-platform way is to download from [GitHub Releases](https://github.com/caddyserver/caddy/releases) and place the executable file in your PATH.
The simplest, cross-platform way to get started is to download Caddy from [GitHub Releases](https://github.com/caddyserver/caddy/releases) and place the executable file in your PATH.
For other install options, see https://caddyserver.com/docs/install.
See [our online documentation](https://caddyserver.com/docs/install) for other install instructions.
## Build from source
Requirements:
- [Go 1.16 or newer](https://golang.org/dl/)
- [Go 1.21 or newer](https://golang.org/dl/)
### For development
_**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions in the next section._
```bash
@@ -164,9 +176,9 @@ The docs are also open source. You can contribute to them here: https://github.c
## Getting help
- We **strongly recommend** that all professionals or companies using Caddy get a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed.
- We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed.
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way! If Caddy is benefitting your company, please consider a sponsorship! This not only helps fund full-time work to ensure the longevity of the project, it's also a great look for your company to your customers and potential customers!
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way! We can offer private help to sponsors. If Caddy is benefitting your company, please consider a sponsorship. This not only helps fund full-time work to ensure the longevity of the project, it provides your company the resources, support, and discounts you need; along with being a great look for your company to your customers and potential customers!
- Individuals can exchange help for free on our community forum at https://caddy.community. Remember that people give help out of their spare time and good will. The best way to get help is to give it first!
@@ -176,6 +188,8 @@ Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only
## About
Matthew Holt began developing Caddy in 2014 while studying computer science at Brigham Young University. (The name "Caddy" was chosen because this software helps with the tedious, mundane tasks of serving the Web, and is also a single place for multiple things to be organized together.) It soon became the first web server to use HTTPS automatically and by default, and now has hundreds of contributors and has served trillions of HTTPS requests.
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH.
- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_
@@ -183,4 +197,4 @@ Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only
Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company.
Debian package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that enables your organization to create, store and share packages in any format, to any place, with total confidence.
Debian package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that enables your organization to create, store and share packages in any format, to any place, with total confidence.
+260 -90
View File
@@ -25,8 +25,8 @@ import (
"errors"
"expvar"
"fmt"
"hash"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/pprof"
@@ -39,12 +39,24 @@ import (
"sync"
"time"
"github.com/caddyserver/caddy/v2/notify"
"github.com/caddyserver/certmagic"
"github.com/cespare/xxhash/v2"
"github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func init() {
// The hard-coded default `DefaultAdminListen` can be overridden
// by setting the `CADDY_ADMIN` environment variable.
// The environment variable may be used by packagers to change
// the default admin address to something more appropriate for
// that platform. See #5317 for discussion.
if env, exists := os.LookupEnv("CADDY_ADMIN"); exists {
DefaultAdminListen = env
}
}
// AdminConfig configures Caddy's API endpoint, which is used
// to manage Caddy while it is running.
type AdminConfig struct {
@@ -56,7 +68,14 @@ type AdminConfig struct {
// The address to which the admin endpoint's listener should
// bind itself. Can be any single network address that can be
// parsed by Caddy. Default: localhost:2019
// parsed by Caddy. Accepts placeholders.
// Default: the value of the `CADDY_ADMIN` environment variable,
// or `localhost:2019` otherwise.
//
// Remember: When changing this value through a config reload,
// be sure to use the `--address` CLI flag to specify the current
// admin address if the currently-running admin endpoint is not
// the default address.
Listen string `json:"listen,omitempty"`
// If true, CORS headers will be emitted, and requests to the
@@ -92,6 +111,10 @@ type AdminConfig struct {
//
// EXPERIMENTAL: This feature is subject to change.
Remote *RemoteAdmin `json:"remote,omitempty"`
// Holds onto the routers so that we can later provision them
// if they require provisioning.
routers []AdminRouter
}
// ConfigSettings configures the management of configuration.
@@ -101,20 +124,26 @@ type ConfigSettings struct {
// are not persisted; only configs that are pushed to Caddy get persisted.
Persist *bool `json:"persist,omitempty"`
// Loads a configuration to use. This is helpful if your configs are
// managed elsewhere, and you want Caddy to pull its config dynamically
// Loads a new configuration. This is helpful if your configs are
// managed elsewhere and you want Caddy to pull its config dynamically
// when it starts. The pulled config completely replaces the current
// one, just like any other config load. It is an error if a pulled
// config is configured to pull another config.
// config is configured to pull another config without a load_delay,
// as this creates a tight loop.
//
// EXPERIMENTAL: Subject to change.
LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"`
// The interval to pull config. With a non-zero value, will pull config
// from config loader (eg. a http loader) with given interval.
// The duration after which to load config. If set, config will be pulled
// from the config loader after this duration. A delay is required if a
// dynamically-loaded config is configured to load yet another config. To
// load configs on a regular interval, ensure this value is set the same
// on all loaded configs; it can also be variable if needed, and to stop
// the loop, simply remove dynamic config loading from the next-loaded
// config.
//
// EXPERIMENTAL: Subject to change.
LoadInterval Duration `json:"load_interval,omitempty"`
LoadDelay Duration `json:"load_delay,omitempty"`
}
// IdentityConfig configures management of this server's identity. An identity
@@ -145,7 +174,7 @@ type IdentityConfig struct {
//
// EXPERIMENTAL: Subject to change.
type RemoteAdmin struct {
// The address on which to start the secure listener.
// The address on which to start the secure listener. Accepts placeholders.
// Default: :2021
Listen string `json:"listen,omitempty"`
@@ -184,7 +213,7 @@ type AdminPermissions struct {
// newAdminHandler reads admin's config and returns an http.Handler suitable
// for use in an admin endpoint server, which will be listening on listenAddr.
func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler {
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler {
muxWrap := adminHandler{mux: http.NewServeMux()}
// secure the local or remote endpoint respectively
@@ -193,6 +222,7 @@ func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) admin
} else {
muxWrap.enforceHost = !addr.isWildcardInterface()
muxWrap.allowedOrigins = admin.allowedOrigins(addr)
muxWrap.enforceOrigin = admin.EnforceOrigin
}
addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
@@ -243,17 +273,39 @@ func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) admin
for _, route := range router.Routes() {
addRoute(route.Pattern, handlerLabel, route.Handler)
}
admin.routers = append(admin.routers, router)
}
return muxWrap
}
// provisionAdminRouters provisions all the router modules
// in the admin.api namespace that need provisioning.
func (admin *AdminConfig) provisionAdminRouters(ctx Context) error {
for _, router := range admin.routers {
provisioner, ok := router.(Provisioner)
if !ok {
continue
}
err := provisioner.Provision(ctx)
if err != nil {
return err
}
}
// We no longer need the routers once provisioned, allow for GC
admin.routers = nil
return nil
}
// allowedOrigins returns a list of origins that are allowed.
// If admin.Origins is nil (null), the provided listen address
// will be used as the default origin. If admin.Origins is
// empty, no origins will be allowed, effectively bricking the
// endpoint for non-unix-socket endpoints, but whatever.
func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
uniqueOrigins := make(map[string]struct{})
for _, o := range admin.Origins {
uniqueOrigins[o] = struct{}{}
@@ -266,7 +318,32 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
// messages. If the requested URI does not include an Internet host
// name for the service being requested, then the Host header field MUST
// be given with an empty value."
//
// UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6.
// Understandable, but frustrating. See:
// https://github.com/golang/go/issues/60374
// See also the discussion here:
// https://github.com/golang/go/issues/61431
//
// We can no longer conform to RFC 2616 Section 14.26 from either Go or curl
// in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a
// bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin
// security checks, the infosec community assures me that it is secure to do
// so, because:
// 1) Browsers do not allow access to unix sockets
// 2) DNS is irrelevant to unix sockets
//
// I am not quite ready to trust either of those external factors, so instead
// of disabling Host/Origin checks, we now allow specific Host values when
// accessing the admin endpoint over unix sockets. I definitely don't trust
// DNS (e.g. I don't trust 'localhost' to always resolve to the local host),
// and IP shouldn't even be used, but if it is for some reason, I think we can
// at least be reasonably assured that 127.0.0.1 and ::1 route to the local
// machine, meaning that a hypothetical browser origin would have to be on the
// local machine as well.
uniqueOrigins[""] = struct{}{}
uniqueOrigins["127.0.0.1"] = struct{}{}
uniqueOrigins["::1"] = struct{}{}
} else {
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
@@ -277,8 +354,23 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
}
}
allowed := make([]string, 0, len(uniqueOrigins))
for origin := range uniqueOrigins {
allowed := make([]*url.URL, 0, len(uniqueOrigins))
for originStr := range uniqueOrigins {
var origin *url.URL
if strings.Contains(originStr, "://") {
var err error
origin, err = url.Parse(originStr)
if err != nil {
continue
}
origin.Path = ""
origin.RawPath = ""
origin.Fragment = ""
origin.RawFragment = ""
origin.RawQuery = ""
} else {
origin = &url.URL{Host: originStr}
}
allowed = append(allowed, origin)
}
return allowed
@@ -290,17 +382,19 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
// that there is always an admin server (unless it is explicitly
// configured to be disabled).
func replaceLocalAdminServer(cfg *Config) error {
// always be sure to close down the old admin endpoint
// always* be sure to close down the old admin endpoint
// as gracefully as possible, even if the new one is
// disabled -- careful to use reference to the current
// (old) admin endpoint since it will be different
// when the function returns
// (* except if the new one fails to start)
oldAdminServer := localAdminServer
var err error
defer func() {
// do the shutdown asynchronously so that any
// current API request gets a response; this
// goroutine may last a few seconds
if oldAdminServer != nil {
if oldAdminServer != nil && err == nil {
go func(oldAdminServer *http.Server) {
err := stopAdminServer(oldAdminServer)
if err != nil {
@@ -310,27 +404,28 @@ func replaceLocalAdminServer(cfg *Config) error {
}
}()
// always get a valid admin config
adminConfig := DefaultAdminConfig
if cfg != nil && cfg.Admin != nil {
adminConfig = cfg.Admin
// set a default if admin wasn't otherwise configured
if cfg.Admin == nil {
cfg.Admin = &AdminConfig{
Listen: DefaultAdminListen,
}
}
// if new admin endpoint is to be disabled, we're done
if adminConfig.Disabled {
if cfg.Admin.Disabled {
Log().Named("admin").Warn("admin endpoint disabled")
return nil
}
// extract a singular listener address
addr, err := parseAdminListenAddr(adminConfig.Listen, DefaultAdminListen)
addr, err := parseAdminListenAddr(cfg.Admin.Listen, DefaultAdminListen)
if err != nil {
return err
}
handler := adminConfig.newAdminHandler(addr, false)
handler := cfg.Admin.newAdminHandler(addr, false)
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{})
if err != nil {
return err
}
@@ -351,15 +446,15 @@ func replaceLocalAdminServer(cfg *Config) error {
serverMu.Lock()
server := localAdminServer
serverMu.Unlock()
if err := server.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
if err := server.Serve(ln.(net.Listener)); !errors.Is(err, http.ErrServerClosed) {
adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
}
}()
adminLogger.Info("admin endpoint started",
zap.String("address", addr.String()),
zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
zap.Strings("origins", handler.allowedOrigins))
zap.Bool("enforce_origin", cfg.Admin.EnforceOrigin),
zap.Array("origins", loggableURLArray(handler.allowedOrigins)))
if !handler.enforceHost {
adminLogger.Warn("admin endpoint on open interface; host checking disabled",
@@ -379,7 +474,6 @@ func manageIdentity(ctx Context, cfg *Config) error {
// import the caddytls package -- but it works
if cfg.Admin.Identity.IssuersRaw == nil {
cfg.Admin.Identity.IssuersRaw = []json.RawMessage{
json.RawMessage(`{"module": "zerossl"}`),
json.RawMessage(`{"module": "acme"}`),
}
}
@@ -390,7 +484,7 @@ func manageIdentity(ctx Context, cfg *Config) error {
if err != nil {
return fmt.Errorf("loading identity issuer modules: %s", err)
}
for _, issVal := range val.([]interface{}) {
for _, issVal := range val.([]any) {
cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer))
}
}
@@ -467,6 +561,9 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
}
// create TLS config that will enforce mutual authentication
if identityCertCache == nil {
return fmt.Errorf("cannot enable remote admin without a certificate cache; configure identity management to initialize a certificate cache")
}
cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger, false)
tlsConfig := cmCfg.TLSConfig()
tlsConfig.NextProtos = nil // this server does not solve ACME challenges
@@ -494,10 +591,11 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
serverMu.Unlock()
// start listener
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
lnAny, err := addr.Listen(ctx, 0, net.ListenConfig{})
if err != nil {
return err
}
ln := lnAny.(net.Listener)
ln = tls.NewListener(ln, tlsConfig)
go func() {
@@ -516,12 +614,13 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
}
func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool) *certmagic.Config {
var cmCfg *certmagic.Config
if ident == nil {
// user might not have configured identity; that's OK, we can still make a
// certmagic config, although it'll be mostly useless for remote management
ident = new(IdentityConfig)
}
cmCfg := &certmagic.Config{
template := certmagic.Config{
Storage: DefaultStorage, // do not act as part of a cluster (this is for the server's local identity)
Logger: logger,
Issuers: ident.issuers,
@@ -531,9 +630,11 @@ func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool)
GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
return cmCfg, nil
},
Logger: logger.Named("cache"),
})
}
return certmagic.New(identityCertCache, *cmCfg)
cmCfg = certmagic.New(identityCertCache, template)
return cmCfg
}
// IdentityCredentials returns this instance's configured, managed identity credentials
@@ -648,10 +749,10 @@ type AdminRoute struct {
type adminHandler struct {
mux *http.ServeMux
// security for local/plaintext) endpoint, on by default
// security for local/plaintext endpoint
enforceOrigin bool
enforceHost bool
allowedOrigins []string
allowedOrigins []*url.URL
// security for remote/encrypted endpoint
remoteControl *RemoteAdmin
@@ -660,11 +761,17 @@ type adminHandler struct {
// ServeHTTP is the external entry point for API requests.
// It will only be called once per request.
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ip, port, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
ip = r.RemoteAddr
port = ""
}
log := Log().Named("admin.api").With(
zap.String("method", r.Method),
zap.String("host", r.Host),
zap.String("uri", r.RequestURI),
zap.String("remote_addr", r.RemoteAddr),
zap.String("remote_ip", ip),
zap.String("remote_port", port),
zap.Reflect("headers", r.Header),
)
if r.TLS != nil {
@@ -771,8 +878,8 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
// rebinding attacks.
func (h adminHandler) checkHost(r *http.Request) error {
var allowed bool
for _, allowedHost := range h.allowedOrigins {
if r.Host == allowedHost {
for _, allowedOrigin := range h.allowedOrigins {
if r.Host == allowedOrigin.Host {
allowed = true
break
}
@@ -791,59 +898,96 @@ func (h adminHandler) checkHost(r *http.Request) error {
// sites from issuing requests to our listener. It
// returns the origin that was obtained from r.
func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
origin := h.getOriginHost(r)
if origin == "" {
return origin, APIError{
originStr, origin := h.getOrigin(r)
if origin == nil {
return "", APIError{
HTTPStatus: http.StatusForbidden,
Err: fmt.Errorf("missing required Origin header"),
Err: fmt.Errorf("required Origin header is missing or invalid"),
}
}
if !h.originAllowed(origin) {
return origin, APIError{
return "", APIError{
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")
if origin == "" {
origin = r.Header.Get("Referer")
}
originURL, err := url.Parse(origin)
if err == nil && originURL.Host != "" {
origin = originURL.Host
if err != nil {
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 {
originCopy := origin
if !strings.Contains(allowedOrigin, "://") {
// no scheme specified, so allow both
originCopy = strings.TrimPrefix(originCopy, "http://")
originCopy = strings.TrimPrefix(originCopy, "https://")
if allowedOrigin.Scheme != "" && origin.Scheme != allowedOrigin.Scheme {
continue
}
if originCopy == allowedOrigin {
if origin.Host == allowedOrigin.Host {
return true
}
}
return false
}
// etagHasher returns a the hasher we used on the config to both
// produce and verify ETags.
func etagHasher() hash.Hash { return xxhash.New() }
// makeEtag returns an Etag header value (including quotes) for
// the given config path and hash of contents at that path.
func makeEtag(path string, hash hash.Hash) string {
return fmt.Sprintf(`"%s %x"`, path, hash.Sum(nil))
}
// This buffer pool is used to keep buffers for
// reading the config file during eTag header generation
var bufferPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func handleConfig(w http.ResponseWriter, r *http.Request) error {
switch r.Method {
case http.MethodGet:
w.Header().Set("Content-Type", "application/json")
hash := etagHasher()
err := readConfig(r.URL.Path, w)
// Read the config into a buffer instead of writing directly to
// the response writer, as we want to set the ETag as the header,
// not the trailer.
buf := bufferPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufferPool.Put(buf)
configWriter := io.MultiWriter(buf, hash)
err := readConfig(r.URL.Path, configWriter)
if err != nil {
return APIError{HTTPStatus: http.StatusBadRequest, Err: err}
}
// we could consider setting up a sync.Pool for the summed
// hashes to reduce GC pressure.
w.Header().Set("Etag", makeEtag(r.URL.Path, hash))
_, err = w.Write(buf.Bytes())
if err != nil {
return APIError{HTTPStatus: http.StatusInternalServerError, Err: err}
}
return nil
case http.MethodPost,
@@ -877,8 +1021,8 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
err := changeConfig(r.Method, r.URL.Path, body, forceReload)
if err != nil {
err := changeConfig(r.Method, r.URL.Path, body, r.Header.Get("If-Match"), forceReload)
if err != nil && !errors.Is(err, errSameConfig) {
return err
}
@@ -897,19 +1041,28 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
parts := strings.Split(idPath, "/")
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" {
return fmt.Errorf("malformed object path")
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed object path"),
}
}
id := parts[2]
// map the ID to the expanded path
currentCfgMu.RLock()
rawCfgMu.RLock()
expanded, ok := rawCfgIndex[id]
defer currentCfgMu.RUnlock()
rawCfgMu.RUnlock()
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
@@ -927,11 +1080,7 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
}
}
if err := notify.NotifyStopping(); err != nil {
Log().Error("unable to notify stopping to service manager", zap.Error(err))
}
exitProcess(Log().Named("admin.api"))
exitProcess(context.Background(), Log().Named("admin.api"))
return nil
}
@@ -939,11 +1088,11 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
// the operation at path according to method, using body and out as
// needed. This is a low-level, unsynchronized function; most callers
// will want to use changeConfig or readConfig instead. This requires a
// read or write lock on currentCfgMu, depending on method (GET needs
// read or write lock on currentCtxMu, depending on method (GET needs
// only a read lock; all others need a write lock).
func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error {
var err error
var val interface{}
var val any
// if there is a request body, decode it into the
// variable that will be set in the config according
@@ -980,16 +1129,16 @@ func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error
parts = parts[:len(parts)-1]
}
var ptr interface{} = rawCfg
var ptr any = rawCfg
traverseLoop:
for i, part := range parts {
switch v := ptr.(type) {
case map[string]interface{}:
case map[string]any:
// if the next part enters a slice, and the slice is our destination,
// handle it specially (because appending to the slice copies the slice
// header, which does not replace the original one like we want)
if arr, ok := v[part].([]interface{}); ok && i == len(parts)-2 {
if arr, ok := v[part].([]any); ok && i == len(parts)-2 {
var idx int
if method != http.MethodPost {
idxStr := parts[len(parts)-1]
@@ -1011,7 +1160,7 @@ traverseLoop:
}
case http.MethodPost:
if ellipses {
valArray, ok := val.([]interface{})
valArray, ok := val.([]any)
if !ok {
return fmt.Errorf("final element is not an array")
}
@@ -1046,9 +1195,9 @@ traverseLoop:
case http.MethodPost:
// if the part is an existing list, POST appends to
// it, otherwise it just sets or creates the value
if arr, ok := v[part].([]interface{}); ok {
if arr, ok := v[part].([]any); ok {
if ellipses {
valArray, ok := val.([]interface{})
valArray, ok := val.([]any)
if !ok {
return fmt.Errorf("final element is not an array")
}
@@ -1061,15 +1210,27 @@ traverseLoop:
}
case http.MethodPut:
if _, ok := v[part]; ok {
return fmt.Errorf("[%s] key already exists: %s", path, part)
return APIError{
HTTPStatus: http.StatusConflict,
Err: fmt.Errorf("[%s] key already exists: %s", path, part),
}
}
v[part] = val
case http.MethodPatch:
if _, ok := v[part]; !ok {
return fmt.Errorf("[%s] key does not exist: %s", path, part)
return APIError{
HTTPStatus: http.StatusNotFound,
Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
}
}
v[part] = val
case http.MethodDelete:
if _, ok := v[part]; !ok {
return APIError{
HTTPStatus: http.StatusNotFound,
Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
}
}
delete(v, part)
default:
return fmt.Errorf("unrecognized method %s", method)
@@ -1079,12 +1240,12 @@ traverseLoop:
// might not exist yet; that's OK but we need to make them as
// we go, while we still have a pointer from the level above
if v[part] == nil && method == http.MethodPut {
v[part] = make(map[string]interface{})
v[part] = make(map[string]any)
}
ptr = v[part]
}
case []interface{}:
case []any:
partInt, err := strconv.Atoi(part)
if err != nil {
return fmt.Errorf("[/%s] invalid array index '%s': %v",
@@ -1106,7 +1267,7 @@ traverseLoop:
// RemoveMetaFields removes meta fields like "@id" from a JSON message
// by using a simple regular expression. (An alternate way to do this
// would be to delete them from the raw, map[string]interface{}
// would be to delete them from the raw, map[string]any
// representation as they are indexed, then iterate the index we made
// and add them back after encoding as JSON, but this is simpler.)
func RemoveMetaFields(rawJSON []byte) []byte {
@@ -1158,7 +1319,10 @@ func (e APIError) Error() string {
// parseAdminListenAddr extracts a singular listen address from either addr
// or defaultAddr, returning the network and the address of the listener.
func parseAdminListenAddr(addr string, defaultAddr string) (NetworkAddress, error) {
input := addr
input, err := NewReplacer().ReplaceOrErr(addr, true, true)
if err != nil {
return NetworkAddress{}, fmt.Errorf("replacing listen address: %v", err)
}
if input == "" {
input = defaultAddr
}
@@ -1181,6 +1345,18 @@ func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
return x509.ParseCertificate(derBytes)
}
type loggableURLArray []*url.URL
func (ua loggableURLArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
if ua == nil {
return nil
}
for _, u := range ua {
enc.AppendString(u.String())
}
return nil
}
var (
// DefaultAdminListen is the address for the local admin
// listener, if none is specified at startup.
@@ -1190,19 +1366,13 @@ var (
// (TLS-authenticated) admin listener, if enabled and not
// specified otherwise.
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
// will get deleted before the process gracefully exits.
func PIDFile(filename string) error {
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
err := ioutil.WriteFile(filename, pid, 0600)
err := os.WriteFile(filename, pid, 0o600)
if err != nil {
return err
}
@@ -1232,7 +1402,7 @@ const (
)
var bufPool = sync.Pool{
New: func() interface{} {
New: func() any {
return new(bytes.Buffer)
},
}
+57 -2
View File
@@ -16,6 +16,8 @@ package caddy
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"sync"
"testing"
@@ -73,6 +75,12 @@ func TestUnsyncedConfigAccess(t *testing.T) {
path: "/bar/qq",
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
},
{
method: "DELETE",
path: "/bar/qq",
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
shouldErr: true,
},
{
method: "POST",
path: "/list",
@@ -113,7 +121,7 @@ func TestUnsyncedConfigAccess(t *testing.T) {
}
// decode the expected config so we can do a convenient DeepEqual
var expectedDecoded interface{}
var expectedDecoded any
err = json.Unmarshal([]byte(tc.expect), &expectedDecoded)
if err != nil {
t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err)
@@ -139,10 +147,57 @@ func TestLoadConcurrent(t *testing.T) {
wg.Done()
}()
}
wg.Wait()
}
type fooModule struct {
IntField int
StrField string
}
func (fooModule) CaddyModule() ModuleInfo {
return ModuleInfo{
ID: "foo",
New: func() Module { return new(fooModule) },
}
}
func (fooModule) Start() error { return nil }
func (fooModule) Stop() error { return nil }
func TestETags(t *testing.T) {
RegisterModule(fooModule{})
if err := Load([]byte(`{"admin": {"listen": "localhost:2999"}, "apps": {"foo": {"strField": "abc", "intField": 0}}}`), true); err != nil {
t.Fatalf("loading: %s", err)
}
const key = "/" + rawConfigKey + "/apps/foo"
// try update the config with the wrong etag
err := changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}}`), fmt.Sprintf(`"/%s not_an_etag"`, rawConfigKey), false)
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
t.Fatalf("expected precondition failed; got %v", err)
}
// get the etag
hash := etagHasher()
if err := readConfig(key, hash); err != nil {
t.Fatalf("reading: %s", err)
}
// do the same update with the correct key
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}`), makeEtag(key, hash), false)
if err != nil {
t.Fatalf("expected update to work; got %v", err)
}
// now try another update. The hash should no longer match and we should get precondition failed
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 2}`), makeEtag(key, hash), false)
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
t.Fatalf("expected precondition failed; got %v", err)
}
}
func BenchmarkLoad(b *testing.B) {
for i := 0; i < b.N; i++ {
Load(testCfg, true)
+404 -118
View File
@@ -17,10 +17,12 @@ package caddy
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"io/fs"
"log"
"net/http"
"os"
@@ -30,12 +32,15 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/caddyserver/caddy/v2/notify"
"github.com/caddyserver/certmagic"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2/internal/filesystems"
"github.com/caddyserver/caddy/v2/notify"
)
// Config is the top (or beginning) of the Caddy configuration structure.
@@ -80,6 +85,9 @@ type Config struct {
storage certmagic.Storage
cancelFunc context.CancelFunc
// filesystems is a dict of filesystems that will later be loaded from and added to.
filesystems FileSystems
}
// App is a thing that Caddy runs.
@@ -101,26 +109,50 @@ func Run(cfg *Config) error {
// if it is different from the current config or
// forceReload is true.
func Load(cfgJSON []byte, forceReload bool) error {
if err := notify.NotifyReloading(); err != nil {
Log().Error("unable to notify reloading to service manager", zap.Error(err))
if err := notify.Reloading(); err != nil {
Log().Error("unable to notify service manager of reloading state", zap.Error(err))
}
// after reload, notify system of success or, if
// failure, update with status (error message)
var err error
defer func() {
if err := notify.NotifyReadiness(); err != nil {
Log().Error("unable to notify readiness to service manager", zap.Error(err))
if err != nil {
if notifyErr := notify.Error(err, 0); notifyErr != nil {
Log().Error("unable to notify to service manager of reload error",
zap.Error(notifyErr),
zap.String("reload_err", err.Error()))
}
return
}
if err := notify.Ready(); err != nil {
Log().Error("unable to notify to service manager of ready state", zap.Error(err))
}
}()
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
// method, traversed via the given path, and uses the given input as
// the new value (if applicable; i.e. "DELETE" doesn't have an input).
// If the resulting config is the same as the previous, no reload will
// occur unless forceReload is true. 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.
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 {
case http.MethodGet,
http.MethodHead,
@@ -130,8 +162,42 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
return fmt.Errorf("method not allowed")
}
currentCfgMu.Lock()
defer currentCfgMu.Unlock()
rawCfgMu.Lock()
defer rawCfgMu.Unlock()
if ifMatchHeader != "" {
// expect the first and last character to be quotes
if len(ifMatchHeader) < 2 || ifMatchHeader[0] != '"' || ifMatchHeader[len(ifMatchHeader)-1] != '"' {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed If-Match header; expect quoted string"),
}
}
// read out the parts
parts := strings.Fields(ifMatchHeader[1 : len(ifMatchHeader)-1])
if len(parts) != 2 {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed If-Match header; expect format \"<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)
if err != nil {
@@ -149,8 +215,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 !forceReload && bytes.Equal(rawCfgJSON, newCfg) {
Log().Named("admin.api").Info("config is unchanged")
return nil
Log().Info("config is unchanged")
return errSameConfig
}
// find any IDs in this config and index them
@@ -172,7 +238,7 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
// with what caddy is still running; we need to
// unmarshal it again because it's likely that
// pointers deep in our rawCfg map were modified
var oldCfg interface{}
var oldCfg any
err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
if err2 != nil {
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
@@ -197,18 +263,18 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
// readConfig traverses the current config to path
// and writes its JSON encoding to out.
func readConfig(path string, out io.Writer) error {
currentCfgMu.RLock()
defer currentCfgMu.RUnlock()
rawCfgMu.RLock()
defer rawCfgMu.RUnlock()
return unsyncedConfigAccess(http.MethodGet, path, nil, out)
}
// indexConfigObjects recursively searches ptr for object fields named
// "@id" and maps that ID value to the full configPath in the index.
// This function is NOT safe for concurrent access; obtain a write lock
// on currentCfgMu.
func indexConfigObjects(ptr interface{}, configPath string, index map[string]string) error {
// on currentCtxMu.
func indexConfigObjects(ptr any, configPath string, index map[string]string) error {
switch val := ptr.(type) {
case map[string]interface{}:
case map[string]any:
for k, v := range val {
if k == idKey {
switch idVal := v.(type) {
@@ -227,7 +293,7 @@ func indexConfigObjects(ptr interface{}, configPath string, index map[string]str
return err
}
}
case []interface{}:
case []any:
// traverse each element of the array recursively
for i := range val {
err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index)
@@ -245,7 +311,7 @@ func indexConfigObjects(ptr interface{}, configPath string, index map[string]str
// it as the new config, replacing any other current config.
// It does NOT update the raw config state, as this is a
// lower-level function; most callers will want to use Load
// instead. A write lock on currentCfgMu is required! If
// instead. A write lock on rawCfgMu is required! If
// allowPersist is false, it will not be persisted to disk,
// even if it is configured to.
func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
@@ -254,7 +320,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
strippedCfgJSON := RemoveMetaFields(cfgJSON)
var newCfg *Config
err := strictUnmarshalJSON(strippedCfgJSON, &newCfg)
err := StrictUnmarshalJSON(strippedCfgJSON, &newCfg)
if err != nil {
return err
}
@@ -269,22 +335,24 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
newCfg.Admin != nil &&
newCfg.Admin.Config != nil &&
newCfg.Admin.Config.LoadRaw != nil &&
newCfg.Admin.Config.LoadInterval <= 0 {
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_interval")
newCfg.Admin.Config.LoadDelay <= 0 {
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_delay")
}
// run the new config and start all its apps
err = run(newCfg, true)
ctx, err := run(newCfg, true)
if err != nil {
return err
}
// swap old config with the new one
oldCfg := currentCfg
currentCfg = newCfg
// swap old context (including its config) with the new one
currentCtxMu.Lock()
oldCtx := currentCtx
currentCtx = ctx
currentCtxMu.Unlock()
// Stop, Cleanup each old app
unsyncedStop(oldCfg)
unsyncedStop(oldCtx)
// autosave a non-nil config, if not disabled
if allowPersist &&
@@ -294,13 +362,13 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
newCfg.Admin.Config.Persist == nil ||
*newCfg.Admin.Config.Persist) {
dir := filepath.Dir(ConfigAutosavePath)
err := os.MkdirAll(dir, 0700)
err := os.MkdirAll(dir, 0o700)
if err != nil {
Log().Error("unable to create folder for config autosave",
zap.String("dir", dir),
zap.Error(err))
} else {
err := ioutil.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0o600)
if err == nil {
Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath))
} else {
@@ -328,7 +396,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
// This is a low-level function; most callers
// will want to use Run instead, which also
// updates the config's raw state.
func run(newCfg *Config, start bool) error {
func run(newCfg *Config, start bool) (Context, error) {
// because we will need to roll back any state
// modifications if this function errors, we
// keep a single error value and scope all
@@ -359,8 +427,8 @@ func run(newCfg *Config, start bool) error {
cancel()
// also undo any other state changes we made
if currentCfg != nil {
certmagic.Default.Storage = currentCfg.storage
if currentCtx.cfg != nil {
certmagic.Default.Storage = currentCtx.cfg.storage
}
}
}()
@@ -372,17 +440,20 @@ func run(newCfg *Config, start bool) error {
}
err = newCfg.Logging.openLogs(ctx)
if err != nil {
return err
return ctx, err
}
// start the admin endpoint (and stop any prior one)
if start {
err = replaceLocalAdminServer(newCfg)
if err != nil {
return fmt.Errorf("starting caddy administration endpoint: %v", err)
return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
}
}
// create the new filesystem map
newCfg.filesystems = &filesystems.FilesystemMap{}
// prepare the new config for use
newCfg.apps = make(map[string]App)
@@ -408,7 +479,7 @@ func run(newCfg *Config, start bool) error {
return nil
}()
if err != nil {
return err
return ctx, err
}
// Load and Provision each app and their submodules
@@ -421,16 +492,23 @@ func run(newCfg *Config, start bool) error {
return nil
}()
if err != nil {
return err
return ctx, err
}
if !start {
return nil
return ctx, 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 ctx, err
}
// Start
err = func() error {
var started []string
started := make([]string, 0, len(newCfg.apps))
for name, a := range newCfg.apps {
err := a.Start()
if err != nil {
@@ -450,12 +528,12 @@ func run(newCfg *Config, start bool) error {
return nil
}()
if err != nil {
return err
return ctx, err
}
// now that the user's config is running, finish setting up anything else,
// such as remote admin endpoint, config loader, etc.
return finishSettingUp(ctx, newCfg)
return ctx, finishSettingUp(ctx, newCfg)
}
// finishSettingUp should be run after all apps have successfully started.
@@ -481,49 +559,74 @@ func finishSettingUp(ctx Context, cfg *Config) error {
if err != nil {
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)))
currentCfgMu.Lock()
err := unsyncedDecodeAndRun(config, false)
currentCfgMu.Unlock()
if err == nil {
Log().Info("dynamically-loaded config applied successfully")
} else {
Log().Error("running dynamically-loaded config failed", zap.Error(err))
logger := Log().Named("config_loader").With(
zap.String("module", val.(Module).CaddyModule().ID.Name()),
zap.Int("load_delay", int(cfg.Admin.Config.LoadDelay)))
runLoadedConfig := func(config []byte) error {
logger.Info("applying dynamically-loaded config")
err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, "", false)
if errors.Is(err, errSameConfig) {
return err
}
if err != nil {
logger.Error("failed to run dynamically-loaded config", zap.Error(err))
return err
}
logger.Info("successfully applied dynamically-loaded config")
return nil
}
if cfg.Admin.Config.LoadInterval > 0 {
if cfg.Admin.Config.LoadDelay > 0 {
go func() {
select {
// if LoadInterval is positive, will wait for the interval and then run with new config
case <-time.After(time.Duration(cfg.Admin.Config.LoadInterval)):
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
if err != nil {
Log().Error("loading dynamic config failed", zap.Error(err))
return
// the loop is here to iterate ONLY if there is an error, a no-op config load,
// or an unchanged config; in which case we simply wait the delay and try again
for {
timer := time.NewTimer(time.Duration(cfg.Admin.Config.LoadDelay))
select {
case <-timer.C:
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
if err != nil {
logger.Error("failed loading dynamic config; will retry", zap.Error(err))
continue
}
if loadedConfig == nil {
logger.Info("dynamically-loaded config was nil; will retry")
continue
}
err = runLoadedConfig(loadedConfig)
if errors.Is(err, errSameConfig) {
logger.Info("dynamically-loaded config was unchanged; will retry")
continue
}
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
logger.Info("stopping dynamic config loading")
}
runLoadedConfig(loadedConfig)
case <-ctx.Done():
return
break
}
}()
} 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)
if err != nil {
return fmt.Errorf("loading dynamic config from %T: %v", val, err)
}
// do this in a goroutine so current config can finish being loaded; otherwise deadlock
go runLoadedConfig(loadedConfig)
go func() { _ = runLoadedConfig(loadedConfig) }()
}
}
return nil
}
// ConfigLoader is a type that can load a Caddy config. The
// returned config must be valid Caddy JSON.
// ConfigLoader is a type that can load a Caddy config. If
// the return value is non-nil, it must be valid Caddy JSON;
// if nil or with non-nil error, it is considered to be a
// no-op load and may be retried later.
type ConfigLoader interface {
LoadConfig(Context) ([]byte, error)
}
@@ -535,29 +638,42 @@ type ConfigLoader interface {
// stop the others. Stop should only be called
// if not replacing with a new config.
func Stop() error {
currentCfgMu.Lock()
defer currentCfgMu.Unlock()
unsyncedStop(currentCfg)
currentCfg = nil
currentCtxMu.RLock()
ctx := currentCtx
currentCtxMu.RUnlock()
rawCfgMu.Lock()
unsyncedStop(ctx)
currentCtxMu.Lock()
currentCtx = Context{}
currentCtxMu.Unlock()
rawCfgJSON = nil
rawCfgIndex = nil
rawCfg[rawConfigKey] = nil
rawCfgMu.Unlock()
return nil
}
// unsyncedStop stops cfg from running, but has
// no locking around cfg. It is a no-op if cfg is
// nil. If any app returns an error when stopping,
// unsyncedStop stops ctx from running, but has
// no locking around ctx. It is a no-op if ctx has a
// nil cfg. If any app returns an error when stopping,
// it is logged and the function continues stopping
// the next app. This function assumes all apps in
// cfg were successfully started first.
func unsyncedStop(cfg *Config) {
if cfg == nil {
// ctx were successfully started first.
//
// A lock on rawCfgMu is required, even though this
// function does not access rawCfg, that lock
// synchronizes the stop/start of apps.
func unsyncedStop(ctx Context) {
if ctx.cfg == nil {
return
}
// stop each app
for name, a := range cfg.apps {
for name, a := range ctx.cfg.apps {
err := a.Stop()
if err != nil {
log.Printf("[ERROR] stop %s: %v", name, err)
@@ -565,13 +681,13 @@ func unsyncedStop(cfg *Config) {
}
// clean up all modules
cfg.cancelFunc()
ctx.cfg.cancelFunc()
}
// Validate loads, provisions, and validates
// cfg, but does not start running it.
func Validate(cfg *Config) error {
err := run(cfg, false)
_, err := run(cfg, false)
if err == nil {
cfg.cancelFunc() // call Cleanup on all modules
}
@@ -584,13 +700,22 @@ func Validate(cfg *Config) error {
// PID file, and shuts down admin endpoint(s) in a goroutine.
// Errors are logged along the way, and an appropriate exit
// code is emitted.
func exitProcess(logger *zap.Logger) {
func exitProcess(ctx context.Context, logger *zap.Logger) {
// let the rest of the program know we're quitting
atomic.StoreInt32(exiting, 1)
// give the OS or service/process manager our 2 weeks' notice: we quit
if err := notify.Stopping(); err != nil {
Log().Error("unable to notify service manager of stopping state", zap.Error(err))
}
if logger == nil {
logger = Log()
}
logger.Warn("exiting; byeee!! 👋")
exitCode := ExitCodeSuccess
lastContext := ActiveContext()
// stop all apps
if err := Stop(); err != nil {
@@ -599,7 +724,7 @@ func exitProcess(logger *zap.Logger) {
}
// clean up certmagic locks
certmagic.CleanUpOwnLocks(logger)
certmagic.CleanUpOwnLocks(ctx, logger)
// remove pidfile
if pidfile != "" {
@@ -612,6 +737,16 @@ func exitProcess(logger *zap.Logger) {
}
}
// execute any process-exit callbacks
for _, exitFunc := range lastContext.exitFuncs {
exitFunc(ctx)
}
exitFuncsMu.Lock()
for _, exitFunc := range exitFuncs {
exitFunc(ctx)
}
exitFuncsMu.Unlock()
// shut down admin endpoint(s) in goroutines so that
// if this function was called from an admin handler,
// it has a chance to return gracefully
@@ -644,6 +779,29 @@ func exitProcess(logger *zap.Logger) {
}()
}
var exiting = new(int32) // accessed atomically
// Exiting returns true if the process is exiting.
// EXPERIMENTAL API: subject to change or removal.
func Exiting() bool { return atomic.LoadInt32(exiting) == 1 }
// OnExit registers a callback to invoke during process exit.
// This registration is PROCESS-GLOBAL, meaning that each
// function should only be registered once forever, NOT once
// per config load (etc).
//
// EXPERIMENTAL API: subject to change or removal.
func OnExit(f func(context.Context)) {
exitFuncsMu.Lock()
exitFuncs = append(exitFuncs, f)
exitFuncsMu.Unlock()
}
var (
exitFuncs []func(context.Context)
exitFuncsMu sync.Mutex
)
// Duration can be an integer or a string. An integer is
// interpreted as nanoseconds. If a string, it is a Go
// time.Duration value such as `300ms`, `1.5h`, or `2h45m`;
@@ -668,8 +826,12 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
// ParseDuration parses a duration string, adding
// support for the "d" unit meaning number of days,
// where a day is assumed to be 24h.
// where a day is assumed to be 24h. The maximum
// input string length is 1024.
func ParseDuration(s string) (time.Duration, error) {
if len(s) > 1024 {
return 0, fmt.Errorf("parsing duration: input string too long")
}
var inNumber bool
var numStart int
for i := 0; i < len(s); i++ {
@@ -699,14 +861,19 @@ func ParseDuration(s string) (time.Duration, error) {
// regardless of storage configuration, since each instance is intended to
// have its own unique ID.
func InstanceID() (uuid.UUID, error) {
uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid")
uuidFileBytes, err := ioutil.ReadFile(uuidFilePath)
if os.IsNotExist(err) {
appDataDir := AppDataDir()
uuidFilePath := filepath.Join(appDataDir, "instance.uuid")
uuidFileBytes, err := os.ReadFile(uuidFilePath)
if errors.Is(err, fs.ErrNotExist) {
uuid, err := uuid.NewRandom()
if err != nil {
return uuid, err
}
err = ioutil.WriteFile(uuidFilePath, []byte(uuid.String()), 0600)
err = os.MkdirAll(appDataDir, 0o700)
if err != nil {
return uuid, err
}
err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0o600)
return uuid, err
} else if err != nil {
return [16]byte{}, err
@@ -714,36 +881,144 @@ func InstanceID() (uuid.UUID, error) {
return uuid.ParseBytes(uuidFileBytes)
}
// GoModule returns the build info of this Caddy
// build from debug.BuildInfo (requires Go modules).
// If no version information is available, a non-nil
// value will still be returned, but with an
// unknown version.
func GoModule() *debug.Module {
var mod debug.Module
return goModule(&mod)
}
// CustomVersion is an optional string that overrides Caddy's
// reported version. It can be helpful when downstream packagers
// need to manually set Caddy's version. If no other version
// information is available, the short form version (see
// Version()) will be set to CustomVersion, and the full version
// will include CustomVersion at the beginning.
//
// Set this variable during `go build` with `-ldflags`:
//
// -ldflags '-X github.com/caddyserver/caddy/v2.CustomVersion=v2.6.2'
//
// for example.
var CustomVersion string
// goModule holds the actual implementation of GoModule.
// Allocating debug.Module in GoModule() and passing a
// reference to goModule enables mid-stack inlining.
func goModule(mod *debug.Module) *debug.Module {
mod.Version = "unknown"
// Version returns the Caddy version in a simple/short form, and
// a full version string. The short form will not have spaces and
// is intended for User-Agent strings and similar, but may be
// omitting valuable information. Note that Caddy must be compiled
// in a special way to properly embed complete version information.
// First this function tries to get the version from the embedded
// build info provided by go.mod dependencies; then it tries to
// get info from embedded VCS information, which requires having
// built Caddy from a git repository. If no version is available,
// this function returns "(devel)" because Go uses that, but for
// the simple form we change it to "unknown". If still no version
// is available (e.g. no VCS repo), then it will use CustomVersion;
// CustomVersion is always prepended to the full version string.
//
// See relevant Go issues: https://github.com/golang/go/issues/29228
// and https://github.com/golang/go/issues/50603.
//
// This function is experimental and subject to change or removal.
func Version() (simple, full string) {
// the currently-recommended way to build Caddy involves
// building it as a dependency so we can extract version
// information from go.mod tooling; once the upstream
// Go issues are fixed, we should just be able to use
// bi.Main... hopefully.
var module *debug.Module
bi, ok := debug.ReadBuildInfo()
if ok {
mod.Path = bi.Main.Path
// The recommended way to build Caddy involves
// creating a separate main module, which
// TODO: track related Go issue: https://github.com/golang/go/issues/29228
// once that issue is fixed, we should just be able to use bi.Main... hopefully.
for _, dep := range bi.Deps {
if dep.Path == ImportPath {
return dep
if !ok {
if CustomVersion != "" {
full = CustomVersion
simple = CustomVersion
return
}
full = "unknown"
simple = "unknown"
return
}
// find the Caddy module in the dependency list
for _, dep := range bi.Deps {
if dep.Path == ImportPath {
module = dep
break
}
}
if module != nil {
simple, full = module.Version, module.Version
if module.Sum != "" {
full += " " + module.Sum
}
if module.Replace != nil {
full += " => " + module.Replace.Path
if module.Replace.Version != "" {
simple = module.Replace.Version + "_custom"
full += "@" + module.Replace.Version
}
if module.Replace.Sum != "" {
full += " " + module.Replace.Sum
}
}
return &bi.Main
}
return mod
if full == "" {
var vcsRevision string
var vcsTime time.Time
var vcsModified bool
for _, setting := range bi.Settings {
switch setting.Key {
case "vcs.revision":
vcsRevision = setting.Value
case "vcs.time":
vcsTime, _ = time.Parse(time.RFC3339, setting.Value)
case "vcs.modified":
vcsModified, _ = strconv.ParseBool(setting.Value)
}
}
if vcsRevision != "" {
var modified string
if vcsModified {
modified = "+modified"
}
full = fmt.Sprintf("%s%s (%s)", vcsRevision, modified, vcsTime.Format(time.RFC822))
simple = vcsRevision
// use short checksum for simple, if hex-only
if _, err := hex.DecodeString(simple); err == nil {
simple = simple[:8]
}
// append date to simple since it can be convenient
// to know the commit date as part of the version
if !vcsTime.IsZero() {
simple += "-" + vcsTime.Format("20060102")
}
}
}
if full == "" {
if CustomVersion != "" {
full = CustomVersion
} else {
full = "unknown"
}
} else if CustomVersion != "" {
full = CustomVersion + " " + full
}
if simple == "" || simple == "(devel)" {
if CustomVersion != "" {
simple = CustomVersion
} else {
simple = "unknown"
}
}
return
}
// ActiveContext returns the currently-active context.
// This function is experimental and might be changed
// or removed in the future.
func ActiveContext() Context {
currentCtxMu.RLock()
defer currentCtxMu.RUnlock()
return currentCtx
}
// CtxKey is a value type for use with context.WithValue.
@@ -751,18 +1026,19 @@ type CtxKey string
// This group of variables pertains to the current configuration.
var (
// currentCfgMu protects everything in this var block.
currentCfgMu sync.RWMutex
// currentCfg is the currently-running configuration.
currentCfg *Config
// currentCtx is the root context for the currently-running
// configuration, which can be accessed through this value.
// If the Config contained in this value is not nil, then
// a config is currently active/running.
currentCtx Context
currentCtxMu sync.RWMutex
// rawCfg is the current, generic-decoded configuration;
// we initialize it as a map with one field ("config")
// to maintain parity with the API endpoint and to avoid
// the special case of having to access/mutate the variable
// directly without traversing into it.
rawCfg = map[string]interface{}{
rawCfg = map[string]any{
rawConfigKey: nil,
}
@@ -773,7 +1049,17 @@ var (
// rawCfgIndex is the map of user-assigned ID to expanded
// path, for converting /id/ paths to /config/ paths.
rawCfgIndex map[string]string
// rawCfgMu protects all the rawCfg fields and also
// essentially synchronizes config changes/reloads.
rawCfgMu sync.RWMutex
)
// errSameConfig is returned if the new config is the same
// as the old one. This isn't usually an actual, actionable
// error; it's mostly a sentinel value.
var errSameConfig = errors.New("config is unchanged")
// ImportPath is the package import path for Caddy core.
// This identifier may be removed in the future.
const ImportPath = "github.com/caddyserver/caddy/v2"
+22 -26
View File
@@ -29,12 +29,12 @@ type Adapter struct {
}
// Adapt converts the Caddyfile config in body to Caddy JSON.
func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []caddyconfig.Warning, error) {
func (a Adapter) Adapt(body []byte, options map[string]any) ([]byte, []caddyconfig.Warning, error) {
if a.ServerType == nil {
return nil, nil, fmt.Errorf("no server type")
}
if options == nil {
options = make(map[string]interface{})
options = make(map[string]any)
}
filename, _ := options["filename"].(string)
@@ -52,9 +52,9 @@ func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []c
return nil, warnings, err
}
// lint check: see if input was properly formatted; sometimes messy files files parse
// lint check: see if input was properly formatted; sometimes messy files parse
// successfully but result in logical errors (the Caddyfile is a bad format, I'm sorry)
if warning, different := formattingDifference(filename, body); different {
if warning, different := FormattingDifference(filename, body); different {
warnings = append(warnings, warning)
}
@@ -63,10 +63,10 @@ func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []c
return result, warnings, err
}
// formattingDifference returns a warning and true if the formatted version
// FormattingDifference returns a warning and true if the formatted version
// is any different from the input; empty warning and false otherwise.
// TODO: also perform this check on imported files
func formattingDifference(filename string, body []byte) (caddyconfig.Warning, bool) {
func FormattingDifference(filename string, body []byte) (caddyconfig.Warning, bool) {
// replace windows-style newlines to normalize comparison
normalizedBody := bytes.Replace(body, []byte("\r\n"), []byte("\n"), -1)
@@ -88,35 +88,31 @@ func formattingDifference(filename string, body []byte) (caddyconfig.Warning, bo
return caddyconfig.Warning{
File: filename,
Line: line,
Message: "input is not formatted with 'caddy fmt'",
Message: "Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies",
}, true
}
// Unmarshaler is a type that can unmarshal
// Caddyfile tokens to set itself up for a
// JSON encoding. The goal of an unmarshaler
// is not to set itself up for actual use,
// but to set itself up for being marshaled
// into JSON. Caddyfile-unmarshaled values
// will not be used directly; they will be
// encoded as JSON and then used from that.
// Implementations must be able to support
// multiple segments (instances of their
// directive or batch of tokens); typically
// this means wrapping all token logic in
// a loop: `for d.Next() { ... }`.
// Unmarshaler is a type that can unmarshal Caddyfile tokens to
// set itself up for a JSON encoding. The goal of an unmarshaler
// is not to set itself up for actual use, but to set itself up for
// being marshaled into JSON. Caddyfile-unmarshaled values will not
// be used directly; they will be encoded as JSON and then used from
// that. Implementations _may_ be able to support multiple segments
// (instances of their directive or batch of tokens); typically this
// means wrapping parsing logic in a loop: `for d.Next() { ... }`.
// More commonly, only a single segment is supported, so a simple
// `d.Next()` at the start should be used to consume the module
// identifier token (directive name, etc).
type Unmarshaler interface {
UnmarshalCaddyfile(d *Dispenser) error
}
// ServerType is a type that can evaluate a Caddyfile and set up a caddy config.
type ServerType interface {
// Setup takes the server blocks which
// contain tokens, as well as options
// (e.g. CLI flags) and creates a Caddy
// config, along with any warnings or
// an error.
Setup([]ServerBlock, map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error)
// Setup takes the server blocks which contain tokens,
// as well as options (e.g. CLI flags) and creates a
// Caddy config, along with any warnings or an error.
Setup([]ServerBlock, map[string]any) (*caddy.Config, []caddyconfig.Warning, error)
}
// UnmarshalModule instantiates a module with the given ID and invokes
+153 -28
View File
@@ -19,6 +19,7 @@ import (
"fmt"
"io"
"log"
"strconv"
"strings"
)
@@ -29,6 +30,10 @@ type Dispenser struct {
tokens []Token
cursor int
nesting int
// A map of arbitrary context data that can be used
// to pass through some information to unmarshalers.
context map[string]any
}
// NewDispenser returns a Dispenser filled with the given tokens.
@@ -100,12 +105,12 @@ func (d *Dispenser) nextOnSameLine() bool {
d.cursor++
return true
}
if d.cursor >= len(d.tokens) {
if d.cursor >= len(d.tokens)-1 {
return false
}
if d.cursor < len(d.tokens)-1 &&
d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
curr := d.tokens[d.cursor]
next := d.tokens[d.cursor+1]
if !isNextOnNewLine(curr, next) {
d.cursor++
return true
}
@@ -121,12 +126,12 @@ func (d *Dispenser) NextLine() bool {
d.cursor++
return true
}
if d.cursor >= len(d.tokens) {
if d.cursor >= len(d.tokens)-1 {
return false
}
if d.cursor < len(d.tokens)-1 &&
(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
curr := d.tokens[d.cursor]
next := d.tokens[d.cursor+1]
if isNextOnNewLine(curr, next) {
d.cursor++
return true
}
@@ -145,15 +150,15 @@ func (d *Dispenser) NextLine() bool {
//
// Proper use of this method looks like this:
//
// for nesting := d.Nesting(); d.NextBlock(nesting); {
// }
// for nesting := d.Nesting(); d.NextBlock(nesting); {
// }
//
// However, in simple cases where it is known that the
// Dispenser is new and has not already traversed state
// by a loop over NextBlock(), this will do:
//
// for d.NextBlock(0) {
// }
// for d.NextBlock(0) {
// }
//
// As with other token parsing logic, a loop over
// NextBlock() should be contained within a loop over
@@ -201,6 +206,46 @@ func (d *Dispenser) Val() string {
return d.tokens[d.cursor].Text
}
// ValRaw gets the raw text of the current token (including quotes).
// If the token was a heredoc, then the delimiter is not included,
// because that is not relevant to any unmarshaling logic at this time.
// If there is no token loaded, it returns empty string.
func (d *Dispenser) ValRaw() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return ""
}
quote := d.tokens[d.cursor].wasQuoted
if quote > 0 && quote != '<' {
// string literal
return string(quote) + d.tokens[d.cursor].Text + string(quote)
}
return d.tokens[d.cursor].Text
}
// ScalarVal gets value of the current token, converted to the closest
// scalar type. If there is no token loaded, it returns nil.
func (d *Dispenser) ScalarVal() any {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return nil
}
quote := d.tokens[d.cursor].wasQuoted
text := d.tokens[d.cursor].Text
if quote > 0 {
return text // string literal
}
if num, err := strconv.Atoi(text); err == nil {
return num
}
if num, err := strconv.ParseFloat(text, 64); err == nil {
return num
}
if bool, err := strconv.ParseBool(text); err == nil {
return bool
}
return text
}
// Line gets the line number of the current token.
// If there is no token loaded, it returns 0.
func (d *Dispenser) Line() int {
@@ -249,6 +294,19 @@ func (d *Dispenser) AllArgs(targets ...*string) bool {
return true
}
// CountRemainingArgs counts the amount of remaining arguments
// (tokens on the same line) without consuming the tokens.
func (d *Dispenser) CountRemainingArgs() int {
count := 0
for d.NextArg() {
count++
}
for i := 0; i < count; i++ {
d.Prev()
}
return count
}
// RemainingArgs loads any more arguments (tokens on the same line)
// into a slice and returns them. Open curly brace tokens also indicate
// the end of arguments, and the curly brace is not included in
@@ -261,6 +319,18 @@ func (d *Dispenser) RemainingArgs() []string {
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
// the tokens from the current token until the end of the
// "directive" whether that be to the end of the line or
@@ -325,22 +395,22 @@ func (d *Dispenser) Reset() {
// an argument.
func (d *Dispenser) ArgErr() error {
if d.Val() == "{" {
return d.Err("Unexpected token '{', expecting argument")
return d.Err("unexpected token '{', expecting argument")
}
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
return d.Errf("wrong argument count or unexpected line ending after '%s'", d.Val())
}
// SyntaxErr creates a generic syntax error which explains what was
// found and what was expected.
func (d *Dispenser) SyntaxErr(expected string) error {
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
msg := fmt.Sprintf("syntax error: unexpected token '%s', expecting '%s', at %s:%d import chain: ['%s']", d.Val(), expected, d.File(), d.Line(), strings.Join(d.Token().imports, "','"))
return errors.New(msg)
}
// EOFErr returns an error indicating that the dispenser reached
// the end of the input when searching for the next token.
func (d *Dispenser) EOFErr() error {
return d.Errf("Unexpected EOF")
return d.Errf("unexpected EOF")
}
// Err generates a custom parse-time error with a message of msg.
@@ -349,9 +419,16 @@ func (d *Dispenser) Err(msg string) error {
}
// Errf is like Err, but for formatted error messages
func (d *Dispenser) Errf(format string, args ...interface{}) error {
err := fmt.Errorf(format, args...)
return fmt.Errorf("%s:%d - Error during parsing: %w", d.File(), d.Line(), err)
func (d *Dispenser) Errf(format string, args ...any) error {
return d.WrapErr(fmt.Errorf(format, args...))
}
// WrapErr takes an existing error and adds the Caddyfile file and line number.
func (d *Dispenser) WrapErr(err error) error {
if len(d.Token().imports) > 0 {
return fmt.Errorf("%w, at %s:%d import chain ['%s']", err, d.File(), d.Line(), strings.Join(d.Token().imports, "','"))
}
return fmt.Errorf("%w, at %s:%d", err, d.File(), d.Line())
}
// Delete deletes the current token and returns the updated slice
@@ -371,14 +448,42 @@ func (d *Dispenser) Delete() []Token {
return d.tokens
}
// numLineBreaks counts how many line breaks are in the token
// value given by the token index tknIdx. It returns 0 if the
// token does not exist or there are no line breaks.
func (d *Dispenser) numLineBreaks(tknIdx int) int {
if tknIdx < 0 || tknIdx >= len(d.tokens) {
return 0
// DeleteN is the same as Delete, but can delete many tokens at once.
// If there aren't N tokens available to delete, none are deleted.
func (d *Dispenser) DeleteN(amount int) []Token {
if amount > 0 && d.cursor >= (amount-1) && d.cursor <= len(d.tokens)-1 {
d.tokens = append(d.tokens[:d.cursor-(amount-1)], d.tokens[d.cursor+1:]...)
d.cursor -= amount
}
return strings.Count(d.tokens[tknIdx].Text, "\n")
return d.tokens
}
// SetContext sets a key-value pair in the context map.
func (d *Dispenser) SetContext(key string, value any) {
if d.context == nil {
d.context = make(map[string]any)
}
d.context[key] = value
}
// GetContext gets the value of a key in the context map.
func (d *Dispenser) GetContext(key string) any {
if d.context == nil {
return nil
}
return d.context[key]
}
// GetContextString gets the value of a key in the context map
// as a string, or an empty string if the key does not exist.
func (d *Dispenser) GetContextString(key string) string {
if d.context == nil {
return ""
}
if val, ok := d.context[key].(string); ok {
return val
}
return ""
}
// isNewLine determines whether the current token is on a different
@@ -391,6 +496,26 @@ func (d *Dispenser) isNewLine() bool {
if d.cursor > len(d.tokens)-1 {
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]
return isNextOnNewLine(prev, curr)
}
// isNextOnNewLine determines whether the current token is on a different
// line (higher line number) than the next token. It handles imported
// tokens correctly. If there isn't a next token, it returns true.
func (d *Dispenser) isNextOnNewLine() bool {
if d.cursor < 0 {
return false
}
if d.cursor >= len(d.tokens)-1 {
return true
}
curr := d.tokens[d.cursor]
next := d.tokens[d.cursor+1]
return isNextOnNewLine(curr, next)
}
const MatcherNameCtxKey = "matcher_name"
+1 -1
View File
@@ -305,7 +305,7 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
}
var ErrBarIsFull = errors.New("bar is full")
ErrBarIsFull := errors.New("bar is full")
bookingError := d.Errf("unable to reserve: %w", ErrBarIsFull)
if !errors.Is(bookingError, ErrBarIsFull) {
t.Errorf("Errf(): should be able to unwrap the error chain")
+83 -1
View File
@@ -17,6 +17,7 @@ package caddyfile
import (
"bytes"
"io"
"slices"
"unicode"
)
@@ -31,6 +32,14 @@ func Format(input []byte) []byte {
out := new(bytes.Buffer)
rdr := bytes.NewReader(input)
type heredocState int
const (
heredocClosed heredocState = 0
heredocOpening heredocState = 1
heredocOpened heredocState = 2
)
var (
last rune // the last character that was written to the result
@@ -47,6 +56,11 @@ func Format(input []byte) []byte {
quoted bool // whether we're in a quoted segment
escaped bool // whether current char is escaped
heredoc heredocState // whether we're in a heredoc
heredocEscaped bool // whether heredoc is escaped
heredocMarker []rune
heredocClosingMarker []rune
nesting int // indentation level
)
@@ -75,6 +89,62 @@ func Format(input []byte) []byte {
panic(err)
}
// detect whether we have the start of a heredoc
if !quoted && !(heredoc != heredocClosed || heredocEscaped) &&
space && last == '<' && ch == '<' {
write(ch)
heredoc = heredocOpening
space = false
continue
}
if heredoc == heredocOpening {
if ch == '\n' {
if len(heredocMarker) > 0 && heredocMarkerRegexp.MatchString(string(heredocMarker)) {
heredoc = heredocOpened
} else {
heredocMarker = nil
heredoc = heredocClosed
nextLine()
continue
}
write(ch)
continue
}
if unicode.IsSpace(ch) {
// a space means it's just a regular token and not a heredoc
heredocMarker = nil
heredoc = heredocClosed
} else {
heredocMarker = append(heredocMarker, ch)
write(ch)
continue
}
}
// if we're in a heredoc, all characters are read&write as-is
if heredoc == heredocOpened {
heredocClosingMarker = append(heredocClosingMarker, ch)
if len(heredocClosingMarker) > len(heredocMarker)+1 { // We assert that the heredocClosingMarker is followed by a unicode.Space
heredocClosingMarker = heredocClosingMarker[1:]
}
// check if we're done
if unicode.IsSpace(ch) && slices.Equal(heredocClosingMarker[:len(heredocClosingMarker)-1], heredocMarker) {
heredocMarker = nil
heredocClosingMarker = nil
heredoc = heredocClosed
} else {
write(ch)
if ch == '\n' {
heredocClosingMarker = heredocClosingMarker[:0]
}
continue
}
}
if last == '<' && space {
space = false
}
if comment {
if ch == '\n' {
comment = false
@@ -98,6 +168,9 @@ func Format(input []byte) []byte {
}
if escaped {
if ch == '<' {
heredocEscaped = true
}
write(ch)
escaped = false
continue
@@ -117,6 +190,7 @@ func Format(input []byte) []byte {
if unicode.IsSpace(ch) {
space = true
heredocEscaped = false
if ch == '\n' {
newLines++
}
@@ -153,7 +227,10 @@ func Format(input []byte) []byte {
openBraceWritten = true
nextLine()
newLines = 0
nesting++
// prevent infinite nesting from ridiculous inputs (issue #4169)
if nesting < 10 {
nesting++
}
}
switch {
@@ -202,6 +279,11 @@ func Format(input []byte) []byte {
write('{')
openBraceWritten = true
}
if spacePrior && ch == '<' {
space = true
}
write(ch)
beginningOfLine = false
+1 -1
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// +build gofuzz
//go:build gofuzz
package caddyfile
+75
View File
@@ -179,6 +179,11 @@ d {
{$F}
}`,
},
{
description: "env var placeholders with port",
input: `:{$PORT}`,
expect: `:{$PORT}`,
},
{
description: "comments",
input: `#a "\n"
@@ -357,6 +362,76 @@ block {
block {
}
`,
},
{
description: "keep heredoc as-is",
input: `block {
heredoc <<HEREDOC
Here's more than one space Here's more than one space
HEREDOC
}
`,
expect: `block {
heredoc <<HEREDOC
Here's more than one space Here's more than one space
HEREDOC
}
`,
},
{
description: "Mixing heredoc with regular part",
input: `block {
heredoc <<HEREDOC
Here's more than one space Here's more than one space
HEREDOC
respond "More than one space will be eaten" 200
}
block2 {
heredoc <<HEREDOC
Here's more than one space Here's more than one space
HEREDOC
respond "More than one space will be eaten" 200
}
`,
expect: `block {
heredoc <<HEREDOC
Here's more than one space Here's more than one space
HEREDOC
respond "More than one space will be eaten" 200
}
block2 {
heredoc <<HEREDOC
Here's more than one space Here's more than one space
HEREDOC
respond "More than one space will be eaten" 200
}
`,
},
{
description: "Heredoc as regular token",
input: `block {
heredoc <<HEREDOC "More than one space will be eaten"
}
`,
expect: `block {
heredoc <<HEREDOC "More than one space will be eaten"
}
`,
},
{
description: "Escape heredoc",
input: `block {
heredoc \<<HEREDOC
respond "More than one space will be eaten" 200
}
`,
expect: `block {
heredoc \<<HEREDOC
respond "More than one space will be eaten" 200
}
`,
},
} {
+160
View File
@@ -0,0 +1,160 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"regexp"
"strconv"
"strings"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
)
// parseVariadic determines if the token is a variadic placeholder,
// and if so, determines the index range (start/end) of args to use.
// Returns a boolean signaling whether a variadic placeholder was found,
// and the start and end indices.
func parseVariadic(token Token, argCount int) (bool, int, int) {
if !strings.HasPrefix(token.Text, "{args[") {
return false, 0, 0
}
if !strings.HasSuffix(token.Text, "]}") {
return false, 0, 0
}
argRange := strings.TrimSuffix(strings.TrimPrefix(token.Text, "{args["), "]}")
if argRange == "" {
caddy.Log().Named("caddyfile").Warn(
"Placeholder "+token.Text+" cannot have an empty index",
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
return false, 0, 0
}
start, end, found := strings.Cut(argRange, ":")
// If no ":" delimiter is found, this is not a variadic.
// The replacer will pick this up.
if !found {
return false, 0, 0
}
// A valid token may contain several placeholders, and
// they may be separated by ":". It's not variadic.
// https://github.com/caddyserver/caddy/issues/5716
if strings.Contains(start, "}") || strings.Contains(end, "{") {
return false, 0, 0
}
var (
startIndex = 0
endIndex = argCount
err error
)
if start != "" {
startIndex, err = strconv.Atoi(start)
if err != nil {
caddy.Log().Named("caddyfile").Warn(
"Variadic placeholder "+token.Text+" has an invalid start index",
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
return false, 0, 0
}
}
if end != "" {
endIndex, err = strconv.Atoi(end)
if err != nil {
caddy.Log().Named("caddyfile").Warn(
"Variadic placeholder "+token.Text+" has an invalid end index",
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
return false, 0, 0
}
}
// bound check
if startIndex < 0 || startIndex > endIndex || endIndex > argCount {
caddy.Log().Named("caddyfile").Warn(
"Variadic placeholder "+token.Text+" indices are out of bounds, only "+strconv.Itoa(argCount)+" argument(s) exist",
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
return false, 0, 0
}
return true, startIndex, endIndex
}
// makeArgsReplacer prepares a Replacer which can replace
// non-variadic args placeholders in imported tokens.
func makeArgsReplacer(args []string) *caddy.Replacer {
repl := caddy.NewEmptyReplacer()
repl.Map(func(key string) (any, bool) {
// TODO: Remove the deprecated {args.*} placeholder
// support at some point in the future
if matches := argsRegexpIndexDeprecated.FindStringSubmatch(key); len(matches) > 0 {
// What's matched may be a substring of the key
if matches[0] != key {
return nil, false
}
value, err := strconv.Atoi(matches[1])
if err != nil {
caddy.Log().Named("caddyfile").Warn(
"Placeholder {args." + matches[1] + "} has an invalid index")
return nil, false
}
if value >= len(args) {
caddy.Log().Named("caddyfile").Warn(
"Placeholder {args." + matches[1] + "} index is out of bounds, only " + strconv.Itoa(len(args)) + " argument(s) exist")
return nil, false
}
caddy.Log().Named("caddyfile").Warn(
"Placeholder {args." + matches[1] + "} deprecated, use {args[" + matches[1] + "]} instead")
return args[value], true
}
// Handle args[*] form
if matches := argsRegexpIndex.FindStringSubmatch(key); len(matches) > 0 {
// What's matched may be a substring of the key
if matches[0] != key {
return nil, false
}
if strings.Contains(matches[1], ":") {
caddy.Log().Named("caddyfile").Warn(
"Variadic placeholder {args[" + matches[1] + "]} must be a token on its own")
return nil, false
}
value, err := strconv.Atoi(matches[1])
if err != nil {
caddy.Log().Named("caddyfile").Warn(
"Placeholder {args[" + matches[1] + "]} has an invalid index")
return nil, false
}
if value >= len(args) {
caddy.Log().Named("caddyfile").Warn(
"Placeholder {args[" + matches[1] + "]} index is out of bounds, only " + strconv.Itoa(len(args)) + " argument(s) exist")
return nil, false
}
return args[value], true
}
// Not an args placeholder, ignore
return nil, false
})
return repl
}
var (
argsRegexpIndexDeprecated = regexp.MustCompile(`args\.(.+)`)
argsRegexpIndex = regexp.MustCompile(`args\[(.+)]`)
)
+7 -4
View File
@@ -21,19 +21,20 @@ import (
type adjacency map[string][]string
type importGraph struct {
nodes map[string]bool
nodes map[string]struct{}
edges adjacency
}
func (i *importGraph) addNode(name string) {
if i.nodes == nil {
i.nodes = make(map[string]bool)
i.nodes = make(map[string]struct{})
}
if _, exists := i.nodes[name]; exists {
return
}
i.nodes[name] = true
i.nodes[name] = struct{}{}
}
func (i *importGraph) addNodes(names []string) {
for _, name := range names {
i.addNode(name)
@@ -43,6 +44,7 @@ func (i *importGraph) addNodes(names []string) {
func (i *importGraph) removeNode(name string) {
delete(i.nodes, name)
}
func (i *importGraph) removeNodes(names []string) {
for _, name := range names {
i.removeNode(name)
@@ -64,7 +66,7 @@ func (i *importGraph) addEdge(from, to string) error {
}
if i.nodes == nil {
i.nodes = make(map[string]bool)
i.nodes = make(map[string]struct{})
}
if i.edges == nil {
i.edges = make(adjacency)
@@ -73,6 +75,7 @@ func (i *importGraph) addEdge(from, to string) error {
i.edges[from] = append(i.edges[from], to)
return nil
}
func (i *importGraph) addEdges(from string, tos []string) error {
for _, to := range tos {
err := i.addEdge(from, to)
Executable → Regular
+241 -33
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");
// you may not use this file except in compliance with the License.
@@ -17,7 +17,10 @@ package caddyfile
import (
"bufio"
"bytes"
"fmt"
"io"
"regexp"
"strings"
"unicode"
)
@@ -35,14 +38,41 @@ type (
// Token represents a single parsable unit.
Token struct {
File string
Line int
Text string
inSnippet bool
snippetName string
File string
imports []string
Line int
Text string
wasQuoted rune // enclosing quote character, if any
heredocMarker string
snippetName string
}
)
// Tokenize takes bytes as input and lexes it into
// a list of tokens that can be parsed as a Caddyfile.
// Also takes a filename to fill the token's File as
// the source of the tokens, which is important to
// determine relative paths for `import` directives.
func Tokenize(input []byte, filename string) ([]Token, error) {
l := lexer{}
if err := l.load(bytes.NewReader(input)); err != nil {
return nil, err
}
var tokens []Token
for {
found, err := l.next()
if err != nil {
return nil, err
}
if !found {
break
}
l.token.File = filename
tokens = append(tokens, l.token)
}
return tokens, nil
}
// load prepares the lexer to scan an input for tokens.
// It discards any leading byte order mark.
func (l *lexer) load(input io.Reader) error {
@@ -74,27 +104,107 @@ func (l *lexer) load(input io.Reader) error {
// may be escaped. The rest of the line is skipped
// if a "#" character is read in. Returns true if
// a token was loaded; false otherwise.
func (l *lexer) next() bool {
func (l *lexer) next() (bool, error) {
var val []rune
var comment, quoted, btQuoted, escaped bool
var comment, quoted, btQuoted, inHeredoc, heredocEscaped, escaped bool
var heredocMarker string
makeToken := func() bool {
makeToken := func(quoted rune) bool {
l.token.Text = string(val)
l.token.wasQuoted = quoted
l.token.heredocMarker = heredocMarker
return true
}
for {
// Read a character in; if err then if we had
// read some characters, make a token. If we
// reached EOF, then no more tokens to read.
// If no EOF, then we had a problem.
ch, _, err := l.reader.ReadRune()
if err != nil {
if len(val) > 0 {
return makeToken()
if inHeredoc {
return false, fmt.Errorf("incomplete heredoc <<%s on line #%d, expected ending marker %s", heredocMarker, l.line+l.skippedLines, heredocMarker)
}
return makeToken(0), nil
}
if err == io.EOF {
return false
return false, nil
}
panic(err)
return false, err
}
// detect whether we have the start of a heredoc
if !(quoted || btQuoted) && !(inHeredoc || heredocEscaped) &&
len(val) > 1 && string(val[:2]) == "<<" {
// a space means it's just a regular token and not a heredoc
if ch == ' ' {
return makeToken(0), nil
}
// skip CR, we only care about LF
if ch == '\r' {
continue
}
// after hitting a newline, we know that the heredoc marker
// is the characters after the two << and the newline.
// we reset the val because the heredoc is syntax we don't
// want to keep.
if ch == '\n' {
if len(val) == 2 {
return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alpha-numeric characters, dashes and underscores; got empty string", l.line)
}
// check if there's too many <
if string(val[:3]) == "<<<" {
return false, fmt.Errorf("too many '<' for heredoc on line #%d; only use two, for example <<END", l.line)
}
heredocMarker = string(val[2:])
if !heredocMarkerRegexp.Match([]byte(heredocMarker)) {
return false, fmt.Errorf("heredoc marker on line #%d must contain only alpha-numeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
}
inHeredoc = true
l.skippedLines++
val = nil
continue
}
val = append(val, ch)
continue
}
// if we're in a heredoc, all characters are read as-is
if inHeredoc {
val = append(val, ch)
if ch == '\n' {
l.skippedLines++
}
// check if we're done, i.e. that the last few characters are the marker
if len(val) >= len(heredocMarker) && heredocMarker == string(val[len(val)-len(heredocMarker):]) {
// set the final value
val, err = l.finalizeHeredoc(val, heredocMarker)
if err != nil {
return false, err
}
// set the line counter, and make the token
l.line += l.skippedLines
l.skippedLines = 0
return makeToken('<'), nil
}
// stay in the heredoc until we find the ending marker
continue
}
// track whether we found an escape '\' for the next
// iteration to be contextually aware
if !escaped && !btQuoted && ch == '\\' {
escaped = true
continue
@@ -109,26 +219,29 @@ func (l *lexer) next() bool {
}
escaped = false
} else {
if quoted && ch == '"' {
return makeToken()
}
if btQuoted && ch == '`' {
return makeToken()
if (quoted && ch == '"') || (btQuoted && ch == '`') {
return makeToken(ch), nil
}
}
// allow quoted text to wrap continue on multiple lines
if ch == '\n' {
l.line += 1 + l.skippedLines
l.skippedLines = 0
}
// collect this character as part of the quoted token
val = append(val, ch)
continue
}
if unicode.IsSpace(ch) {
// ignore CR altogether, we only actually care about LF (\n)
if ch == '\r' {
continue
}
// end of the line
if ch == '\n' {
// newlines can be escaped to chain arguments
// onto multiple lines; else, increment the line count
if escaped {
l.skippedLines++
escaped = false
@@ -136,14 +249,18 @@ func (l *lexer) next() bool {
l.line += 1 + l.skippedLines
l.skippedLines = 0
}
// comments (#) are single-line only
comment = false
}
// any kind of space means we're at the end of this token
if len(val) > 0 {
return makeToken()
return makeToken(0), nil
}
continue
}
// comments must be at the start of a token,
// in other words, preceded by space or newline
if ch == '#' && len(val) == 0 {
comment = true
}
@@ -164,7 +281,12 @@ func (l *lexer) next() bool {
}
if escaped {
val = append(val, '\\')
// allow escaping the first < to skip the heredoc syntax
if ch == '<' {
heredocEscaped = true
} else {
val = append(val, '\\')
}
escaped = false
}
@@ -172,20 +294,106 @@ func (l *lexer) next() bool {
}
}
// Tokenize takes bytes as input and lexes it into
// a list of tokens that can be parsed as a Caddyfile.
// Also takes a filename to fill the token's File as
// the source of the tokens, which is important to
// determine relative paths for `import` directives.
func Tokenize(input []byte, filename string) ([]Token, error) {
l := lexer{}
if err := l.load(bytes.NewReader(input)); err != nil {
return nil, err
// finalizeHeredoc takes the runes read as the heredoc text and the marker,
// and processes the text to strip leading whitespace, returning the final
// value without the leading whitespace.
func (l *lexer) finalizeHeredoc(val []rune, marker string) ([]rune, error) {
stringVal := string(val)
// find the last newline of the heredoc, which is where the contents end
lastNewline := strings.LastIndex(stringVal, "\n")
// collapse the content, then split into separate lines
lines := strings.Split(stringVal[:lastNewline+1], "\n")
// figure out how much whitespace we need to strip from the front of every line
// by getting the string that precedes the marker, on the last line
paddingToStrip := stringVal[lastNewline+1 : len(stringVal)-len(marker)]
// iterate over each line and strip the whitespace from the front
var out string
for lineNum, lineText := range lines[:len(lines)-1] {
if lineText == "" || lineText == "\r" {
out += "\n"
continue
}
// find an exact match for the padding
index := strings.Index(lineText, paddingToStrip)
// if the padding doesn't match exactly at the start then we can't safely strip
if index != 0 {
return nil, fmt.Errorf("mismatched leading whitespace in heredoc <<%s on line #%d [%s], expected whitespace [%s] to match the closing marker", marker, l.line+lineNum+1, lineText, paddingToStrip)
}
// strip, then append the line, with the newline, to the output.
// also removes all "\r" because Windows.
out += strings.ReplaceAll(lineText[len(paddingToStrip):]+"\n", "\r", "")
}
var tokens []Token
for l.next() {
l.token.File = filename
tokens = append(tokens, l.token)
// Remove the trailing newline from the loop
if len(out) > 0 && out[len(out)-1] == '\n' {
out = out[:len(out)-1]
}
return tokens, nil
// return the final value
return []rune(out), nil
}
// Quoted returns true if the token was enclosed in quotes
// (i.e. double quotes, backticks, or heredoc).
func (t Token) Quoted() bool {
return t.wasQuoted > 0
}
// NumLineBreaks counts how many line breaks are in the token text.
func (t Token) NumLineBreaks() int {
lineBreaks := strings.Count(t.Text, "\n")
if t.wasQuoted == '<' {
// heredocs have an extra linebreak because the opening
// delimiter is on its own line and is not included in the
// token Text itself, and the trailing newline is removed.
lineBreaks += 2
}
return lineBreaks
}
// Clone returns a deep copy of the token.
func (t Token) Clone() Token {
return Token{
File: t.File,
imports: append([]string{}, t.imports...),
Line: t.Line,
Text: t.Text,
wasQuoted: t.wasQuoted,
heredocMarker: t.heredocMarker,
snippetName: t.snippetName,
}
}
var heredocMarkerRegexp = regexp.MustCompile("^[A-Za-z0-9_-]+$")
// isNextOnNewLine tests whether t2 is on a different line from t1
func isNextOnNewLine(t1, t2 Token) bool {
// If the second token is from a different file,
// we can assume it's from a different line
if t1.File != t2.File {
return true
}
// If the second token is from a different import chain,
// we can assume it's from a different line
if len(t1.imports) != len(t2.imports) {
return true
}
for i, im := range t1.imports {
if im != t2.imports[i] {
return true
}
}
// If the first token (incl line breaks) ends
// on a line earlier than the next token,
// then the second token is on a new line
return t1.Line+t1.NumLineBreaks() < t2.Line
}
+1 -1
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// +build gofuzz
//go:build gofuzz
package caddyfile
+272 -11
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");
// you may not use this file except in compliance with the License.
@@ -18,13 +18,13 @@ import (
"testing"
)
type lexerTestCase struct {
input []byte
expected []Token
}
func TestLexer(t *testing.T) {
testCases := []lexerTestCase{
testCases := []struct {
input []byte
expected []Token
expectErr bool
errorMessage string
}{
{
input: []byte(`host:123`),
expected: []Token{
@@ -249,12 +249,273 @@ func TestLexer(t *testing.T) {
{Line: 1, Text: `quotes`},
},
},
{
input: []byte(`heredoc <<EOF
content
EOF same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: "content"},
{Line: 3, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<VERY-LONG-MARKER
content
VERY-LONG-MARKER same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: "content"},
{Line: 3, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<EOF
extra-newline
EOF same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: "extra-newline\n"},
{Line: 4, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<EOF
EOF
HERE same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: ``},
{Line: 3, Text: `HERE`},
{Line: 3, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<EOF
EOF same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: ""},
{Line: 2, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<EOF
content
EOF same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: "content"},
{Line: 3, Text: `same-line-arg`},
},
},
{
input: []byte(`prev-line
heredoc <<EOF
multi
line
content
EOF same-line-arg
next-line
`),
expected: []Token{
{Line: 1, Text: `prev-line`},
{Line: 2, Text: `heredoc`},
{Line: 2, Text: "\tmulti\n\tline\n\tcontent"},
{Line: 6, Text: `same-line-arg`},
{Line: 7, Text: `next-line`},
},
},
{
input: []byte(`escaped-heredoc \<< >>`),
expected: []Token{
{Line: 1, Text: `escaped-heredoc`},
{Line: 1, Text: `<<`},
{Line: 1, Text: `>>`},
},
},
{
input: []byte(`not-a-heredoc <EOF
content
`),
expected: []Token{
{Line: 1, Text: `not-a-heredoc`},
{Line: 1, Text: `<EOF`},
{Line: 2, Text: `content`},
},
},
{
input: []byte(`not-a-heredoc <<<EOF content`),
expected: []Token{
{Line: 1, Text: `not-a-heredoc`},
{Line: 1, Text: `<<<EOF`},
{Line: 1, Text: `content`},
},
},
{
input: []byte(`not-a-heredoc "<<" ">>"`),
expected: []Token{
{Line: 1, Text: `not-a-heredoc`},
{Line: 1, Text: `<<`},
{Line: 1, Text: `>>`},
},
},
{
input: []byte(`not-a-heredoc << >>`),
expected: []Token{
{Line: 1, Text: `not-a-heredoc`},
{Line: 1, Text: `<<`},
{Line: 1, Text: `>>`},
},
},
{
input: []byte(`not-a-heredoc <<HERE SAME LINE
content
HERE same-line-arg
`),
expected: []Token{
{Line: 1, Text: `not-a-heredoc`},
{Line: 1, Text: `<<HERE`},
{Line: 1, Text: `SAME`},
{Line: 1, Text: `LINE`},
{Line: 2, Text: `content`},
{Line: 3, Text: `HERE`},
{Line: 3, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<s
s
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: ""},
},
},
{
input: []byte("\u000Aheredoc \u003C\u003C\u0073\u0073\u000A\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F\u000A\u0073\u0073\u000A\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F\u000A\u00BF\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F"),
expected: []Token{
{
Line: 2,
Text: "heredoc",
},
{
Line: 2,
Text: "\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F",
},
{
Line: 5,
Text: "\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F",
},
{
Line: 6,
Text: "\u00BF\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F",
},
},
},
{
input: []byte("not-a-heredoc <<\n"),
expectErr: true,
errorMessage: "missing opening heredoc marker on line #1; must contain only alpha-numeric characters, dashes and underscores; got empty string",
},
{
input: []byte(`heredoc <<<EOF
content
EOF same-line-arg
`),
expectErr: true,
errorMessage: "too many '<' for heredoc on line #1; only use two, for example <<END",
},
{
input: []byte(`heredoc <<EOF
content
`),
expectErr: true,
errorMessage: "incomplete heredoc <<EOF on line #3, expected ending marker EOF",
},
{
input: []byte(`heredoc <<EOF
content
EOF
`),
expectErr: true,
errorMessage: "mismatched leading whitespace in heredoc <<EOF on line #2 [\tcontent], expected whitespace [\t\t] to match the closing marker",
},
{
input: []byte(`heredoc <<EOF
content
EOF
`),
expectErr: true,
errorMessage: "mismatched leading whitespace in heredoc <<EOF on line #2 [ content], expected whitespace [\t\t] to match the closing marker",
},
{
input: []byte(`heredoc <<EOF
The next line is a blank line
The previous line is a blank line
EOF`),
expected: []Token{
{Line: 1, Text: "heredoc"},
{Line: 1, Text: "The next line is a blank line\n\nThe previous line is a blank line"},
},
},
{
input: []byte(`heredoc <<EOF
One tab indented heredoc with blank next line
One tab indented heredoc with blank previous line
EOF`),
expected: []Token{
{Line: 1, Text: "heredoc"},
{Line: 1, Text: "One tab indented heredoc with blank next line\n\nOne tab indented heredoc with blank previous line"},
},
},
{
input: []byte(`heredoc <<EOF
The next line is a blank line with one tab
The previous line is a blank line with one tab
EOF`),
expected: []Token{
{Line: 1, Text: "heredoc"},
{Line: 1, Text: "The next line is a blank line with one tab\n\t\nThe previous line is a blank line with one tab"},
},
},
{
input: []byte(`heredoc <<EOF
The next line is a blank line with one tab less than the correct indentation
The previous line is a blank line with one tab less than the correct indentation
EOF`),
expectErr: true,
errorMessage: "mismatched leading whitespace in heredoc <<EOF on line #3 [\t], expected whitespace [\t\t] to match the closing marker",
},
}
for i, testCase := range testCases {
actual, err := Tokenize(testCase.input, "")
if testCase.expectErr {
if err == nil {
t.Fatalf("expected error, got actual: %v", actual)
continue
}
if err.Error() != testCase.errorMessage {
t.Fatalf("expected error '%v', got: %v", testCase.errorMessage, err)
}
continue
}
if err != nil {
t.Errorf("%v", err)
t.Fatalf("%v", err)
}
lexerCompare(t, i, testCase.expected, actual)
}
@@ -262,17 +523,17 @@ func TestLexer(t *testing.T) {
func lexerCompare(t *testing.T, n int, expected, actual []Token) {
if len(expected) != len(actual) {
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
t.Fatalf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
}
for i := 0; i < len(actual) && i < len(expected); i++ {
if actual[i].Line != expected[i].Line {
t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d",
t.Fatalf("Test case %d token %d ('%s'): expected line %d but was line %d",
n, i, expected[i].Text, expected[i].Line, actual[i].Line)
break
}
if actual[i].Text != expected[i].Text {
t.Errorf("Test case %d token %d: expected text '%s' but was '%s'",
t.Fatalf("Test case %d token %d: expected text '%s' but was '%s'",
n, i, expected[i].Text, actual[i].Text)
break
}
Executable → Regular
+197 -69
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");
// you may not use this file except in compliance with the License.
@@ -17,13 +17,13 @@ package caddyfile
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
)
@@ -37,22 +37,36 @@ import (
// Environment variables in {$ENVIRONMENT_VARIABLE} notation
// will be replaced before parsing begins.
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 {
return nil, err
}
p := parser{
Dispenser: NewDispenser(tokens),
importGraph: importGraph{
nodes: make(map[string]bool),
nodes: make(map[string]struct{}),
edges: make(adjacency),
},
}
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) {
return Tokenize(replaceEnvVars(input), filename)
}
// replaceEnvVars replaces all occurrences of environment variables.
func replaceEnvVars(input []byte) ([]byte, error) {
// It mutates the underlying array and returns the updated slice.
func replaceEnvVars(input []byte) []byte {
var offset int
for {
begin := bytes.Index(input[offset:], spanOpen)
@@ -93,22 +107,7 @@ func replaceEnvVars(input []byte) ([]byte, error) {
// continue at the end of the replacement
offset = begin + len(envVarBytes)
}
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
return input
}
type parser struct {
@@ -150,7 +149,6 @@ func (p *parser) begin() error {
}
err := p.addresses()
if err != nil {
return err
}
@@ -161,6 +159,25 @@ func (p *parser) begin() error {
return nil
}
if ok, name := p.isNamedRoute(); ok {
// we just need a dummy leading token to ease parsing later
nameToken := p.Token()
nameToken.Text = name
// named routes only have one key, the route name
p.block.Keys = []Token{nameToken}
p.block.IsNamedRoute = true
// get all the tokens from the block, including the braces
tokens, err := p.blockTokens(true)
if err != nil {
return err
}
tokens = append([]Token{nameToken}, tokens...)
p.block.Segments = []Segment{tokens}
return nil
}
if ok, name := p.isSnippet(); ok {
if p.definedSnippets == nil {
p.definedSnippets = map[string][]Token{}
@@ -169,16 +186,15 @@ func (p *parser) begin() error {
return p.Errf("redeclaration of previously declared snippet %s", name)
}
// consume all tokens til matched close brace
tokens, err := p.snippetTokens()
tokens, err := p.blockTokens(false)
if err != nil {
return err
}
// Just as we need to track which file the token comes from, we need to
// keep track of which snippets do the tokens come from. This is helpful
// in tracking import cycles across files/snippets by namespacing them. Without
// this we end up with false-positives in cycle-detection.
// keep track of which snippet the token comes from. This is helpful
// in tracking import cycles across files/snippets by namespacing them.
// Without this, we end up with false-positives in cycle-detection.
for k, v := range tokens {
v.inSnippet = true
v.snippetName = name
tokens[k] = v
}
@@ -195,11 +211,17 @@ func (p *parser) addresses() error {
var expectingAnother bool
for {
tkn := p.Val()
value := p.Val()
token := p.Token()
// special case: import directive replaces tokens during parse-time
if tkn == "import" && p.isNewLine() {
err := p.doImport()
// Reject request matchers if trying to define them globally
if strings.HasPrefix(value, "@") {
return p.Errf("request matchers may not be defined globally, they must be in a site block; found %s", value)
}
// Special case: import directive replaces tokens during parse-time
if value == "import" && p.isNewLine() {
err := p.doImport(0)
if err != nil {
return err
}
@@ -207,9 +229,9 @@ func (p *parser) addresses() error {
}
// Open brace definitely indicates end of addresses
if tkn == "{" {
if value == "{" {
if expectingAnother {
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
return p.Errf("Expected another address but had '%s' - check for extra comma", value)
}
// Mark this server block as being defined with braces.
// This is used to provide a better error message when
@@ -221,15 +243,15 @@ func (p *parser) addresses() error {
}
// Users commonly forget to place a space between the address and the '{'
if strings.HasSuffix(tkn, "{") {
return p.Errf("Site addresses cannot end with a curly brace: '%s' - put a space between the token and the brace", tkn)
if strings.HasSuffix(value, "{") {
return p.Errf("Site addresses cannot end with a curly brace: '%s' - put a space between the token and the brace", value)
}
if tkn != "" { // empty token possible if user typed ""
if value != "" { // empty token possible if user typed ""
// Trailing comma indicates another address will follow, which
// may possibly be on the next line
if tkn[len(tkn)-1] == ',' {
tkn = tkn[:len(tkn)-1]
if value[len(value)-1] == ',' {
value = value[:len(value)-1]
expectingAnother = true
} else {
expectingAnother = false // but we may still see another one on this line
@@ -238,11 +260,12 @@ func (p *parser) addresses() error {
// If there's a comma here, it's probably because they didn't use a space
// between their two domains, e.g. "foo.com,bar.com", which would not be
// parsed as two separate site addresses.
if strings.Contains(tkn, ",") {
return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", tkn)
if strings.Contains(value, ",") {
return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", value)
}
p.block.Keys = append(p.block.Keys, tkn)
token.Text = value
p.block.Keys = append(p.block.Keys, token)
}
// Advance token and possibly break out of loop or return error
@@ -299,7 +322,7 @@ func (p *parser) directives() error {
// special case: import directive replaces tokens during parse-time
if p.Val() == "import" {
err := p.doImport()
err := p.doImport(1)
if err != nil {
return err
}
@@ -325,7 +348,7 @@ func (p *parser) directives() error {
// is on the token before where the import directive was. In
// other words, call Next() to access the first token that was
// imported.
func (p *parser) doImport() error {
func (p *parser) doImport(nesting int) error {
// syntax checks
if !p.NextArg() {
return p.ArgErr()
@@ -338,11 +361,8 @@ func (p *parser) doImport() error {
// grab remaining args as placeholder replacements
args := p.RemainingArgs()
// add args to the replacer
repl := caddy.NewEmptyReplacer()
for index, arg := range args {
repl.Set("args."+strconv.Itoa(index), arg)
}
// set up a replacer for non-variadic args replacement
repl := makeArgsReplacer(args)
// splice out the import directive and its arguments
// (2 tokens, plus the length of args)
@@ -380,16 +400,29 @@ func (p *parser) doImport() error {
return p.Errf("Glob pattern may only contain one wildcard (*), but has others: %s", globPattern)
}
matches, err = filepath.Glob(globPattern)
if err != nil {
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
}
if len(matches) == 0 {
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 {
return p.Errf("File to import not found: %s", importPattern)
}
} else {
// See issue #5295 - should skip any files that start with a . when iterating over them.
sep := string(filepath.Separator)
segGlobPattern := strings.Split(globPattern, sep)
if strings.HasPrefix(segGlobPattern[len(segGlobPattern)-1], "*") {
var tmpMatches []string
for _, m := range matches {
seg := strings.Split(m, sep)
if !strings.HasPrefix(seg[len(seg)-1], ".") {
tmpMatches = append(tmpMatches, m)
}
}
matches = tmpMatches
}
}
// collect all the imported tokens
@@ -404,7 +437,7 @@ func (p *parser) doImport() error {
}
nodeName := p.File()
if p.Token().inSnippet {
if p.Token().snippetName != "" {
nodeName += fmt.Sprintf(":%s", p.Token().snippetName)
}
p.importGraph.addNode(nodeName)
@@ -415,13 +448,69 @@ func (p *parser) doImport() error {
}
// copy the tokens so we don't overwrite p.definedSnippets
tokensCopy := make([]Token, len(importedTokens))
copy(tokensCopy, importedTokens)
tokensCopy := make([]Token, 0, len(importedTokens))
var (
maybeSnippet bool
maybeSnippetId bool
index int
)
// run the argument replacer on the tokens
for index, token := range tokensCopy {
token.Text = repl.ReplaceKnown(token.Text, "")
tokensCopy[index] = token
// golang for range slice return a copy of value
// similarly, append also copy value
for i, token := range importedTokens {
// update the token's imports to refer to import directive filename, line number and snippet name if there is one
if token.snippetName != "" {
token.imports = append(token.imports, fmt.Sprintf("%s:%d (import %s)", p.File(), p.Line(), token.snippetName))
} else {
token.imports = append(token.imports, fmt.Sprintf("%s:%d (import)", p.File(), p.Line()))
}
// naive way of determine snippets, as snippets definition can only follow name + block
// format, won't check for nesting correctness or any other error, that's what parser does.
if !maybeSnippet && nesting == 0 {
// first of the line
if i == 0 || isNextOnNewLine(tokensCopy[i-1], token) {
index = 0
} else {
index++
}
if index == 0 && len(token.Text) >= 3 && strings.HasPrefix(token.Text, "(") && strings.HasSuffix(token.Text, ")") {
maybeSnippetId = true
}
}
switch token.Text {
case "{":
nesting++
if index == 1 && maybeSnippetId && nesting == 1 {
maybeSnippet = true
maybeSnippetId = false
}
case "}":
nesting--
if nesting == 0 && maybeSnippet {
maybeSnippet = false
}
}
if maybeSnippet {
tokensCopy = append(tokensCopy, token)
continue
}
foundVariadic, startIndex, endIndex := parseVariadic(token, len(args))
if foundVariadic {
for _, arg := range args[startIndex:endIndex] {
token.Text = arg
tokensCopy = append(tokensCopy, token)
}
} else {
token.Text = repl.ReplaceKnown(token.Text, "")
tokensCopy = append(tokensCopy, token)
}
}
// splice the imported tokens in the place of the import statement
@@ -447,11 +536,17 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
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 {
return nil, p.Errf("Could not read imported file %s: %v", importFile, err)
}
// only warning in case of empty files
if len(input) == 0 || len(strings.TrimSpace(string(input))) == 0 {
caddy.Log().Warn("Import file is empty", zap.String("file", importFile))
return []Token{}, nil
}
importedTokens, err := allTokens(importFile, input)
if err != nil {
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
@@ -477,7 +572,6 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
// are loaded into the current server block for later use
// by directive setup functions.
func (p *parser) directive() error {
// a segment is a list of tokens associated with this directive
var segment Segment
@@ -487,6 +581,16 @@ func (p *parser) directive() error {
for p.Next() {
if p.Val() == "{" {
p.nesting++
if !p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
return p.Err("Unexpected next token after '{' on same line")
}
if p.isNewLine() {
return p.Err("Unexpected '{' on a new line; did you mean to place the '{' on the previous 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 {
p.cursor-- // read too far
break
@@ -495,7 +599,7 @@ func (p *parser) directive() error {
} else if p.Val() == "}" && p.nesting == 0 {
return p.Err("Unexpected '}' because no matching opening brace")
} else if p.Val() == "import" && p.isNewLine() {
if err := p.doImport(); err != nil {
if err := p.doImport(1); err != nil {
return err
}
p.cursor-- // cursor is advanced when we continue, so roll back one more
@@ -536,28 +640,43 @@ func (p *parser) closeCurlyBrace() error {
return nil
}
func (p *parser) isNamedRoute() (bool, string) {
keys := p.block.Keys
// A named route block is a single key with parens, prefixed with &.
if len(keys) == 1 && strings.HasPrefix(keys[0].Text, "&(") && strings.HasSuffix(keys[0].Text, ")") {
return true, strings.TrimSuffix(keys[0].Text[2:], ")")
}
return false, ""
}
func (p *parser) isSnippet() (bool, string) {
keys := p.block.Keys
// A snippet block is a single key with parens. Nothing else qualifies.
if len(keys) == 1 && strings.HasPrefix(keys[0], "(") && strings.HasSuffix(keys[0], ")") {
return true, strings.TrimSuffix(keys[0][1:], ")")
if len(keys) == 1 && strings.HasPrefix(keys[0].Text, "(") && strings.HasSuffix(keys[0].Text, ")") {
return true, strings.TrimSuffix(keys[0].Text[1:], ")")
}
return false, ""
}
// read and store everything in a block for later replay.
func (p *parser) snippetTokens() ([]Token, error) {
// snippet must have curlies.
func (p *parser) blockTokens(retainCurlies bool) ([]Token, error) {
// block must have curlies.
err := p.openCurlyBrace()
if err != nil {
return nil, err
}
nesting := 1 // count our own nesting in snippets
nesting := 1 // count our own nesting
tokens := []Token{}
if retainCurlies {
tokens = append(tokens, p.Token())
}
for p.Next() {
if p.Val() == "}" {
nesting--
if nesting == 0 {
if retainCurlies {
tokens = append(tokens, p.Token())
}
break
}
}
@@ -577,9 +696,18 @@ func (p *parser) snippetTokens() ([]Token, error) {
// head of the server block with tokens, which are
// grouped by segments.
type ServerBlock struct {
HasBraces bool
Keys []string
Segments []Segment
HasBraces bool
Keys []Token
Segments []Segment
IsNamedRoute bool
}
func (sb ServerBlock) GetKeysText() []string {
res := []string{}
for _, k := range sb.Keys {
res = append(res, k.Text)
}
return res
}
// DispenseDirective returns a dispenser that contains
+206 -30
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");
// you may not use this file except in compliance with the License.
@@ -16,17 +16,101 @@ package caddyfile
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func TestParseVariadic(t *testing.T) {
args := make([]string, 10)
for i, tc := range []struct {
input string
result bool
}{
{
input: "",
result: false,
},
{
input: "{args[1",
result: false,
},
{
input: "1]}",
result: false,
},
{
input: "{args[:]}aaaaa",
result: false,
},
{
input: "aaaaa{args[:]}",
result: false,
},
{
input: "{args.}",
result: false,
},
{
input: "{args.1}",
result: false,
},
{
input: "{args[]}",
result: false,
},
{
input: "{args[:]}",
result: true,
},
{
input: "{args[:]}",
result: true,
},
{
input: "{args[0:]}",
result: true,
},
{
input: "{args[:0]}",
result: true,
},
{
input: "{args[-1:]}",
result: false,
},
{
input: "{args[:11]}",
result: false,
},
{
input: "{args[10:0]}",
result: false,
},
{
input: "{args[0:10]}",
result: true,
},
{
input: "{args[0]}:{args[1]}:{args[2]}",
result: false,
},
} {
token := Token{
File: "test",
Line: 1,
Text: tc.input,
}
if v, _, _ := parseVariadic(token, len(args)); v != tc.result {
t.Errorf("Test %d error expectation failed Expected: %t, got %t", i, tc.result, v)
}
}
}
func TestAllTokens(t *testing.T) {
input := []byte("a b c\nd e")
expected := []string{"a", "b", "c", "d", "e"}
tokens, err := allTokens("TestAllTokens", input)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
@@ -64,10 +148,11 @@ func TestParseOneAndImport(t *testing.T) {
"localhost",
}, []int{1}},
{`localhost:1234
{
`localhost:1234
dir1 foo bar`, false, []string{
"localhost:1234",
}, []int{3},
"localhost:1234",
}, []int{3},
},
{`localhost {
@@ -188,10 +273,49 @@ func TestParseOneAndImport(t *testing.T) {
{`import testdata/not_found.txt`, true, []string{}, []int{}},
// empty file should just log a warning, and result in no tokens
{`import testdata/empty.txt`, false, []string{}, []int{}},
{`import testdata/only_white_space.txt`, false, []string{}, []int{}},
// import path/to/dir/* should skip any files that start with a . when iterating over them.
{`localhost
dir1 arg1
import testdata/glob/*`, false, []string{
"localhost",
}, []int{2, 3, 1}},
// import path/to/dir/.* should continue to read all dotfiles in a dir.
{`import testdata/glob/.*`, false, []string{
"host1",
}, []int{1, 2}},
{`""`, false, []string{}, []int{}},
{``, false, []string{}, []int{}},
// Unexpected next token after '{' on same line
{`localhost
dir1 { a b }`, true, []string{"localhost"}, []int{}},
// Unexpected '{' on a new 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 testdata/import_args0.txt a`, false, []string{"a"}, []int{}},
{`import testdata/import_args1.txt a b`, false, []string{"a", "b"}, []int{}},
@@ -223,7 +347,7 @@ func TestParseOneAndImport(t *testing.T) {
i, len(test.keys), len(result.Keys))
continue
}
for j, addr := range result.Keys {
for j, addr := range result.GetKeysText() {
if addr != test.keys[j] {
t.Errorf("Test %d, key %d: Expected '%s', but was '%s'",
i, j, test.keys[j], addr)
@@ -255,8 +379,9 @@ func TestRecursiveImport(t *testing.T) {
}
isExpected := func(got ServerBlock) bool {
if len(got.Keys) != 1 || got.Keys[0] != "localhost" {
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
textKeys := got.GetKeysText()
if len(textKeys) != 1 || textKeys[0] != "localhost" {
t.Errorf("got keys unexpected: expect localhost, got %v", textKeys)
return false
}
if len(got.Segments) != 2 {
@@ -280,16 +405,16 @@ func TestRecursiveImport(t *testing.T) {
}
// test relative recursive import
err = ioutil.WriteFile(recursiveFile1, []byte(
err = os.WriteFile(recursiveFile1, []byte(
`localhost
dir1
import recursive_import_test2`), 0644)
import recursive_import_test2`), 0o644)
if err != nil {
t.Fatal(err)
}
defer os.Remove(recursiveFile1)
err = ioutil.WriteFile(recursiveFile2, []byte("dir2 1"), 0644)
err = os.WriteFile(recursiveFile2, []byte("dir2 1"), 0o644)
if err != nil {
t.Fatal(err)
}
@@ -314,10 +439,10 @@ func TestRecursiveImport(t *testing.T) {
}
// test absolute recursive import
err = ioutil.WriteFile(recursiveFile1, []byte(
err = os.WriteFile(recursiveFile1, []byte(
`localhost
dir1
import `+recursiveFile2), 0644)
import `+recursiveFile2), 0o644)
if err != nil {
t.Fatal(err)
}
@@ -350,8 +475,9 @@ func TestDirectiveImport(t *testing.T) {
}
isExpected := func(got ServerBlock) bool {
if len(got.Keys) != 1 || got.Keys[0] != "localhost" {
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
textKeys := got.GetKeysText()
if len(textKeys) != 1 || textKeys[0] != "localhost" {
t.Errorf("got keys unexpected: expect localhost, got %v", textKeys)
return false
}
if len(got.Segments) != 2 {
@@ -370,8 +496,8 @@ func TestDirectiveImport(t *testing.T) {
t.Fatal(err)
}
err = ioutil.WriteFile(directiveFile, []byte(`prop1 1
prop2 2`), 0644)
err = os.WriteFile(directiveFile, []byte(`prop1 1
prop2 2`), 0o644)
if err != nil {
t.Fatal(err)
}
@@ -492,7 +618,7 @@ func TestParseAll(t *testing.T) {
i, len(test.keys[j]), j, len(block.Keys))
continue
}
for k, addr := range block.Keys {
for k, addr := range block.GetKeysText() {
if addr != test.keys[j][k] {
t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'",
i, j, k, test.keys[j][k], addr)
@@ -591,16 +717,43 @@ func TestEnvironmentReplacement(t *testing.T) {
expect: "}{$",
},
} {
actual, err := replaceEnvVars([]byte(test.input))
if err != nil {
t.Fatal(err)
}
actual := replaceEnvVars([]byte(test.input))
if !bytes.Equal(actual, []byte(test.expect)) {
t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
}
}
}
func TestImportReplacementInJSONWithBrace(t *testing.T) {
for i, test := range []struct {
args []string
input string
expect string
}{
{
args: []string{"123"},
input: "{args[0]}",
expect: "123",
},
{
args: []string{"123"},
input: `{"key":"{args[0]}"}`,
expect: `{"key":"123"}`,
},
{
args: []string{"123", "123"},
input: `{"key":[{args[0]},{args[1]}]}`,
expect: `{"key":[123,123]}`,
},
} {
repl := makeArgsReplacer(test.args)
actual := repl.ReplaceKnown(test.input, "")
if actual != test.expect {
t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
}
}
}
func TestSnippets(t *testing.T) {
p := testParser(`
(common) {
@@ -618,7 +771,7 @@ func TestSnippets(t *testing.T) {
if len(blocks) != 1 {
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
}
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
if actual, expected := blocks[0].GetKeysText()[0], "http://example.com"; expected != actual {
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
}
if len(blocks[0].Segments) != 2 {
@@ -633,7 +786,7 @@ func TestSnippets(t *testing.T) {
}
func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) {
file, err := ioutil.TempFile("", t.Name())
file, err := os.CreateTemp("", t.Name())
if err != nil {
panic(err) // get a stack trace so we know where this was called from.
}
@@ -650,7 +803,7 @@ func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) {
fileName := writeStringToTempFileOrDie(t, `
http://example.com {
# This isn't an import directive, it's just an arg with value 'import'
basicauth / import password
basic_auth / import password
}
`)
// Parse the root file that imports the other one.
@@ -661,12 +814,12 @@ func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) {
}
auth := blocks[0].Segments[0]
line := auth[0].Text + " " + auth[1].Text + " " + auth[2].Text + " " + auth[3].Text
if line != "basicauth / import password" {
if line != "basic_auth / import password" {
// Previously, it would be changed to:
// basicauth / import /path/to/test/dir/password
// basic_auth / import /path/to/test/dir/password
// referencing a file that (probably) doesn't exist and changing the
// password!
t.Errorf("Expected basicauth tokens to be 'basicauth / import password' but got %#q", line)
t.Errorf("Expected basic_auth tokens to be 'basic_auth / import password' but got %#q", line)
}
}
@@ -693,7 +846,7 @@ func TestSnippetAcrossMultipleFiles(t *testing.T) {
if len(blocks) != 1 {
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
}
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
if actual, expected := blocks[0].GetKeysText()[0], "http://example.com"; expected != actual {
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
}
if len(blocks[0].Segments) != 1 {
@@ -704,6 +857,29 @@ func TestSnippetAcrossMultipleFiles(t *testing.T) {
}
}
func TestRejectsGlobalMatcher(t *testing.T) {
p := testParser(`
@rejected path /foo
(common) {
gzip foo
errors stderr
}
http://example.com {
import common
}
`)
_, err := p.parseAll()
if err == nil {
t.Fatal("Expected an error, but got nil")
}
expected := "request matchers may not be defined globally, they must be in a site block; found @rejected, at Testfile:2"
if err.Error() != expected {
t.Errorf("Expected error to be '%s' but got '%v'", expected, err)
}
}
func testParser(input string) parser {
return parser{Dispenser: NewTestDispenser(input)}
}
View File
+4
View File
@@ -0,0 +1,4 @@
host1 {
dir1
dir2 arg1
}
+2
View File
@@ -0,0 +1,2 @@
dir2 arg1 arg2
dir3
+1 -1
View File
@@ -1 +1 @@
{args.0}
{args[0]}
+1 -1
View File
@@ -1 +1 @@
{args.0} {args.1}
{args[0]} {args[1]}
View File
View File
View File
View File
View File
+7
View File
@@ -0,0 +1,7 @@
 
+5 -5
View File
@@ -24,7 +24,7 @@ import (
// Adapter is a type which can adapt a configuration to Caddy JSON.
// It returns the results and any warnings, or an error.
type Adapter interface {
Adapt(body []byte, options map[string]interface{}) ([]byte, []Warning, error)
Adapt(body []byte, options map[string]any) ([]byte, []Warning, error)
}
// Warning represents a warning or notice related to conversion.
@@ -48,7 +48,7 @@ func (w Warning) String() string {
// are converted to warnings. This is convenient when filling config
// structs that require a json.RawMessage, without having to worry
// about errors.
func JSON(val interface{}, warnings *[]Warning) json.RawMessage {
func JSON(val any, warnings *[]Warning) json.RawMessage {
b, err := json.Marshal(val)
if err != nil {
if warnings != nil {
@@ -64,9 +64,9 @@ func JSON(val interface{}, warnings *[]Warning) json.RawMessage {
// for encoding module values where the module name has to be described within
// the object by a certain key; for example, `"handler": "file_server"` for a
// file server HTTP handler (fieldName="handler" and fieldVal="file_server").
// The val parameter must encode into a map[string]interface{} (i.e. it must be
// The val parameter must encode into a map[string]any (i.e. it must be
// a struct or map). Any errors are converted into warnings.
func JSONModuleObject(val interface{}, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage {
func JSONModuleObject(val any, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage {
// encode to a JSON object first
enc, err := json.Marshal(val)
if err != nil {
@@ -77,7 +77,7 @@ func JSONModuleObject(val interface{}, fieldName, fieldVal string, warnings *[]W
}
// then decode the object
var tmp map[string]interface{}
var tmp map[string]any
err = json.Unmarshal(enc, &tmp)
if err != nil {
if warnings != nil {
+73 -48
View File
@@ -17,16 +17,18 @@ package httpcaddyfile
import (
"fmt"
"net"
"net/netip"
"reflect"
"sort"
"strconv"
"strings"
"unicode"
"github.com/caddyserver/certmagic"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/certmagic"
)
// mapAddressToServerBlocks returns a map of listener address to list of server
@@ -35,12 +37,12 @@ import (
// server block that share the same address stay grouped together so the config
// isn't repeated unnecessarily. For example, this Caddyfile:
//
// example.com {
// bind 127.0.0.1
// }
// www.example.com, example.net/path, localhost:9999 {
// bind 127.0.0.1 1.2.3.4
// }
// example.com {
// bind 127.0.0.1
// }
// www.example.com, example.net/path, localhost:9999 {
// bind 127.0.0.1 1.2.3.4
// }
//
// has two server blocks to start with. But expressed in this Caddyfile are
// actually 4 listener addresses: 127.0.0.1:443, 1.2.3.4:443, 127.0.0.1:9999,
@@ -76,7 +78,8 @@ import (
// multiple addresses to the same lists of server blocks (a many:many mapping).
// (Doing this is essentially a map-reduce technique.)
func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock,
options map[string]interface{}) (map[string][]serverBlock, error) {
options map[string]any,
) (map[string][]serverBlock, error) {
sbmap := make(map[string][]serverBlock)
for i, sblock := range originalServerBlocks {
@@ -85,15 +88,15 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBloc
// will be served by them; this has the effect of treating each
// key of a server block as its own, but without having to repeat its
// contents in cases where multiple keys really can be served together
addrToKeys := make(map[string][]string)
addrToKeys := make(map[string][]caddyfile.Token)
for j, key := range sblock.block.Keys {
// a key can have multiple listener addresses if there are multiple
// arguments to the 'bind' directive (although they will all have
// the same port, since the port is defined by the key or is implicit
// through automatic HTTPS)
addrs, err := st.listenerAddrsForServerBlockKey(sblock, key, options)
addrs, err := st.listenerAddrsForServerBlockKey(sblock, key.Text, options)
if err != nil {
return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key, err)
return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key.Text, err)
}
// associate this key with each listener address it is served on
@@ -102,18 +105,26 @@ 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
// server block, we iterate that mapping and create a list of
// new server blocks for each address where the keys of the
// server block are only the ones which use the address; but
// 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
parsedKeys := make([]Address, 0, len(keys))
for _, key := range keys {
addr, err := ParseAddress(key)
addr, err := ParseAddress(key.Text)
if err != nil {
return nil, fmt.Errorf("parsing key '%s': %v", key, err)
return nil, fmt.Errorf("parsing key '%s': %v", key.Text, err)
}
parsedKeys = append(parsedKeys, addr.Normalize())
}
@@ -161,6 +172,7 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
delete(addrToServerBlocks, otherAddr)
}
}
sort.Strings(a.addresses)
sbaddrs = append(sbaddrs, a)
}
@@ -174,14 +186,28 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
return sbaddrs
}
// listenerAddrsForServerBlockKey essentially converts the Caddyfile
// site addresses to Caddy listener addresses for each server block.
func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string,
options map[string]interface{}) ([]string, error) {
options map[string]any,
) ([]string, error) {
addr, err := ParseAddress(key)
if err != nil {
return nil, fmt.Errorf("parsing key: %v", err)
}
addr = addr.Normalize()
switch addr.Scheme {
case "wss":
return nil, fmt.Errorf("the scheme wss:// is only supported in browsers; use https:// instead")
case "ws":
return nil, fmt.Errorf("the scheme ws:// is only supported in browsers; use http:// instead")
case "https", "http", "":
// Do nothing or handle the valid schemes
default:
return nil, fmt.Errorf("unsupported URL scheme %s://", addr.Scheme)
}
// figure out the HTTP and HTTPS ports; either
// use defaults, or override with user config
httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort)
@@ -207,24 +233,42 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
return nil, fmt.Errorf("[%s] scheme and port violate convention", key)
}
// the bind directive specifies hosts, but is optional
lnHosts := make([]string, 0, len(sblock.pile))
// the bind directive specifies hosts (and potentially network), but is optional
lnHosts := make([]string, 0, len(sblock.pile["bind"]))
for _, cfgVal := range sblock.pile["bind"] {
lnHosts = append(lnHosts, cfgVal.Value.([]string)...)
}
if len(lnHosts) == 0 {
lnHosts = []string{""}
if defaultBind, ok := options["default_bind"].([]string); ok {
lnHosts = defaultBind
} else {
lnHosts = []string{""}
}
}
// use a map to prevent duplication
listeners := make(map[string]struct{})
for _, host := range lnHosts {
addr, err := caddy.ParseNetworkAddress(host)
if err == nil && addr.IsUnixNetwork() {
listeners[host] = struct{}{}
} else {
listeners[net.JoinHostPort(host, lnPort)] = struct{}{}
for _, lnHost := range lnHosts {
// normally we would simply append the port,
// but if lnHost is IPv6, we need to ensure it
// is enclosed in [ ]; net.JoinHostPort does
// this for us, but lnHost might also have a
// network type in front (e.g. "tcp/") leading
// to "[tcp/::1]" which causes parsing failures
// later; what we need is "tcp/[::1]", so we have
// to split the network and host, then re-combine
network, host, ok := strings.Cut(lnHost, "/")
if !ok {
host = network
network = ""
}
host = strings.Trim(host, "[]") // IPv6
networkAddr := caddy.JoinNetworkAddress(network, host, lnPort)
addr, err := caddy.ParseNetworkAddress(networkAddr)
if err != nil {
return nil, fmt.Errorf("parsing network address: %v", err)
}
listeners[addr.String()] = struct{}{}
}
// now turn map into list
@@ -232,6 +276,7 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
for lnStr := range listeners {
listenersList = append(listenersList, lnStr)
}
sort.Strings(listenersList)
return listenersList, nil
}
@@ -336,8 +381,10 @@ func (a Address) Normalize() Address {
// ensure host is normalized if it's an IP address
host := strings.TrimSpace(a.Host)
if ip := net.ParseIP(host); ip != nil {
host = ip.String()
if ip, err := netip.ParseAddr(host); err == nil {
if ip.Is6() && !ip.Is4() && !ip.Is4In6() {
host = ip.String()
}
}
return Address{
@@ -349,28 +396,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
// placeholders (substrings in non-escaped '{ }' spans).
// See https://github.com/caddyserver/caddy/issues/3264
+1 -1
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// +build gofuzz
//go:build gofuzz
package httpcaddyfile
+102 -32
View File
@@ -106,67 +106,128 @@ func TestAddressString(t *testing.T) {
func TestKeyNormalization(t *testing.T) {
testCases := []struct {
input string
expect string
expect Address
}{
{
input: "example.com",
expect: "example.com",
input: "example.com",
expect: Address{
Host: "example.com",
},
},
{
input: "http://host:1234/path",
expect: "http://host:1234/path",
input: "http://host:1234/path",
expect: Address{
Scheme: "http",
Host: "host",
Port: "1234",
Path: "/path",
},
},
{
input: "HTTP://A/ABCDEF",
expect: "http://a/ABCDEF",
input: "HTTP://A/ABCDEF",
expect: Address{
Scheme: "http",
Host: "a",
Path: "/ABCDEF",
},
},
{
input: "A/ABCDEF",
expect: "a/ABCDEF",
input: "A/ABCDEF",
expect: Address{
Host: "a",
Path: "/ABCDEF",
},
},
{
input: "A:2015/Path",
expect: "a:2015/Path",
input: "A:2015/Path",
expect: Address{
Host: "a",
Port: "2015",
Path: "/Path",
},
},
{
input: "sub.{env.MY_DOMAIN}",
expect: "sub.{env.MY_DOMAIN}",
input: "sub.{env.MY_DOMAIN}",
expect: Address{
Host: "sub.{env.MY_DOMAIN}",
},
},
{
input: "sub.ExAmPle",
expect: "sub.example",
input: "sub.ExAmPle",
expect: Address{
Host: "sub.example",
},
},
{
input: "sub.\\{env.MY_DOMAIN\\}",
expect: "sub.\\{env.my_domain\\}",
input: "sub.\\{env.MY_DOMAIN\\}",
expect: Address{
Host: "sub.\\{env.my_domain\\}",
},
},
{
input: "sub.{env.MY_DOMAIN}.com",
expect: "sub.{env.MY_DOMAIN}.com",
input: "sub.{env.MY_DOMAIN}.com",
expect: Address{
Host: "sub.{env.MY_DOMAIN}.com",
},
},
{
input: ":80",
expect: ":80",
input: ":80",
expect: Address{
Port: "80",
},
},
{
input: ":443",
expect: ":443",
input: ":443",
expect: Address{
Port: "443",
},
},
{
input: ":1234",
expect: ":1234",
input: ":1234",
expect: Address{
Port: "1234",
},
},
{
input: "",
expect: "",
expect: Address{},
},
{
input: ":",
expect: "",
expect: Address{},
},
{
input: "[::]",
expect: "::",
input: "[::]",
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 {
@@ -175,9 +236,18 @@ func TestKeyNormalization(t *testing.T) {
t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err)
continue
}
if actual := addr.Normalize().Key(); actual != tc.expect {
t.Errorf("Test %d: Input '%s': Expected '%s' but got '%s'", i, tc.input, tc.expect, actual)
actual := addr.Normalize()
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)
}
}
}
File diff suppressed because it is too large Load Diff
+163 -11
View File
@@ -1,6 +1,7 @@
package httpcaddyfile
import (
"strings"
"testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -37,8 +38,7 @@ func TestLogDirectiveSyntax(t *testing.T) {
format filter {
wrap console
fields {
common_log delete
request>remote_addr ip_mask {
request>remote_ip ip_mask {
ipv4 24
ipv6 32
}
@@ -47,17 +47,18 @@ 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,
},
{
input: `:8080 {
log invalid {
log name-override {
output file foo.log
}
}
`,
expectError: true,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`,
expectError: false,
},
} {
@@ -149,6 +150,27 @@ func TestRedirDirectiveSyntax(t *testing.T) {
}`,
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 {
redir /old.html /new.html htlm
@@ -161,12 +183,6 @@ func TestRedirDirectiveSyntax(t *testing.T) {
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 400
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 temp
@@ -199,3 +215,139 @@ func TestRedirDirectiveSyntax(t *testing.T) {
}
}
}
func TestImportErrorLine(t *testing.T) {
for i, tc := range []struct {
input string
errorFunc func(err error) bool
}{
{
input: `(t1) {
abort {args[:]}
}
:8080 {
import t1
import t1 true
}`,
errorFunc: func(err error) bool {
return err != nil && strings.Contains(err.Error(), "Caddyfile:6 (import t1)")
},
},
{
input: `(t1) {
abort {args[:]}
}
:8080 {
import t1 true
}`,
errorFunc: func(err error) bool {
return err != nil && strings.Contains(err.Error(), "Caddyfile:5 (import t1)")
},
},
{
input: `
import testdata/import_variadic_snippet.txt
:8080 {
import t1 true
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
{
input: `
import testdata/import_variadic_with_import.txt
:8080 {
import t1 true
import t2 true
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
} {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
_, _, err := adapter.Adapt([]byte(tc.input), nil)
if !tc.errorFunc(err) {
t.Errorf("Test %d error expectation failed, got %s", i, err)
continue
}
}
}
func TestNestedImport(t *testing.T) {
for i, tc := range []struct {
input string
errorFunc func(err error) bool
}{
{
input: `(t1) {
respond {args[0]} {args[1]}
}
(t2) {
import t1 {args[0]} 202
}
:8080 {
handle {
import t2 "foobar"
}
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
{
input: `(t1) {
respond {args[:]}
}
(t2) {
import t1 {args[0]} {args[1]}
}
:8080 {
handle {
import t2 "foobar" 202
}
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
{
input: `(t1) {
respond {args[0]} {args[1]}
}
(t2) {
import t1 {args[:]}
}
:8080 {
handle {
import t2 "foobar" 202
}
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
} {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
_, _, err := adapter.Adapt([]byte(tc.input), nil)
if !tc.errorFunc(err) {
t.Errorf("Test %d error expectation failed, got %s", i, err)
continue
}
}
}
+156 -34
View File
@@ -27,37 +27,58 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
// directiveOrder specifies the order
// to apply directives in HTTP routes.
// defaultDirectiveOrder specifies the default order
// to apply directives in HTTP routes. This must only
// consist of directives that are included in Caddy's
// standard distribution.
//
// The root directive goes first in case rewrites or
// redirects depend on existence of files, i.e. the
// file matcher, which must know the root first.
// e.g. The 'root' directive goes near the start in
// case rewrites or redirects depend on existence of
// files, i.e. the file matcher, which must know the
// root first.
//
// The header directive goes second so that headers
// can be manipulated before doing redirects.
var directiveOrder = []string{
// e.g. The 'header' directive goes before 'redir' so
// that headers can be manipulated before doing redirects.
//
// e.g. The 'respond' directive is near the end because it
// writes a response and terminates the middleware chain.
var defaultDirectiveOrder = []string{
"tracing",
// set variables that may be used by other directives
"map",
"vars",
"fs",
"root",
"log_append",
"skip_log", // TODO: deprecated, renamed to log_skip
"log_skip",
"log_name",
"header",
"copy_response_headers", // only in reverse_proxy's handle_response
"request_body",
"redir",
// URI manipulation
// incoming request manipulation
"method",
"rewrite",
"uri",
"try_files",
// middleware handlers; some wrap responses
"basicauth",
"basicauth", // TODO: deprecated, renamed to basic_auth
"basic_auth",
"forward_auth",
"request_header",
"encode",
"push",
"intercept",
"templates",
// special routing & dispatching directives
"invoke",
"handle",
"handle_path",
"route",
@@ -65,6 +86,7 @@ var directiveOrder = []string{
// handlers that typically respond to requests
"abort",
"error",
"copy_response", // only in reverse_proxy's handle_response
"respond",
"metrics",
"reverse_proxy",
@@ -73,6 +95,11 @@ var directiveOrder = []string{
"acme_server",
}
// directiveOrder specifies the order to apply directives
// in HTTP routes, after being modified by either the
// plugins or by the user via the "order" global option.
var directiveOrder = defaultDirectiveOrder
// directiveIsOrdered returns true if dir is
// a known, ordered (sorted) directive.
func directiveIsOrdered(dir string) bool {
@@ -119,6 +146,58 @@ func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
})
}
// RegisterDirectiveOrder registers the default order for a
// directive from a plugin.
//
// This is useful when a plugin has a well-understood place
// it should run in the middleware pipeline, and it allows
// users to avoid having to define the order themselves.
//
// The directive dir may be placed in the position relative
// to ('before' or 'after') a directive included in Caddy's
// standard distribution. It cannot be relative to another
// plugin's directive.
//
// EXPERIMENTAL: This API may change or be removed.
func RegisterDirectiveOrder(dir string, position Positional, standardDir string) {
// check if directive was already ordered
if directiveIsOrdered(dir) {
panic("directive '" + dir + "' already ordered")
}
if position != Before && position != After {
panic("the 2nd argument must be either 'before' or 'after', got '" + position + "'")
}
// check if directive exists in standard distribution, since
// we can't allow plugins to depend on one another; we can't
// guarantee the order that plugins are loaded in.
foundStandardDir := false
for _, d := range defaultDirectiveOrder {
if d == standardDir {
foundStandardDir = true
}
}
if !foundStandardDir {
panic("the 3rd argument '" + standardDir + "' must be a directive that exists in the standard distribution of Caddy")
}
// insert directive into proper position
newOrder := directiveOrder
for i, d := range newOrder {
if d != standardDir {
continue
}
if position == Before {
newOrder = append(newOrder[:i], append([]string{dir}, newOrder[i:]...)...)
} else if position == After {
newOrder = append(newOrder[:i+1], append([]string{dir}, newOrder[i+1:]...)...)
}
break
}
directiveOrder = newOrder
}
// RegisterGlobalOption registers a unique global option opt with
// an associated unmarshaling (setup) function. When the global
// option opt is encountered in a Caddyfile, setupFunc will be
@@ -135,8 +214,8 @@ func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) {
type Helper struct {
*caddyfile.Dispenser
// State stores intermediate variables during caddyfile adaptation.
State map[string]interface{}
options map[string]interface{}
State map[string]any
options map[string]any
warnings *[]caddyconfig.Warning
matcherDefs map[string]caddy.ModuleMap
parentBlock caddyfile.ServerBlock
@@ -144,7 +223,7 @@ type Helper struct {
}
// Option gets the option keyed by name.
func (h Helper) Option(name string) interface{} {
func (h Helper) Option(name string) any {
return h.options[name]
}
@@ -164,11 +243,12 @@ func (h Helper) Caddyfiles() []string {
for file := range files {
filesSlice = append(filesSlice, file)
}
sort.Strings(filesSlice)
return filesSlice
}
// JSON converts val into JSON. Any errors are added to warnings.
func (h Helper) JSON(val interface{}) json.RawMessage {
func (h Helper) JSON(val any) json.RawMessage {
return caddyconfig.JSON(val, h.warnings)
}
@@ -207,7 +287,8 @@ func (h Helper) ExtractMatcherSet() (caddy.ModuleMap, error) {
// NewRoute returns config values relevant to creating a new HTTP route.
func (h Helper) NewRoute(matcherSet caddy.ModuleMap,
handler caddyhttp.MiddlewareHandler) []ConfigValue {
handler caddyhttp.MiddlewareHandler,
) []ConfigValue {
mod, err := caddy.GetModule(caddy.GetModuleID(handler))
if err != nil {
*h.warnings = append(*h.warnings, caddyconfig.Warning{
@@ -259,12 +340,6 @@ func (h Helper) GroupRoutes(vals []ConfigValue) {
}
}
// NewBindAddresses returns config values relevant to adding
// listener bind addresses to the config.
func (h Helper) NewBindAddresses(addrs []string) []ConfigValue {
return []ConfigValue{{Class: "bind", Value: addrs}}
}
// WithDispenser returns a new instance based on d. All others Helper
// fields are copied, so typically maps are shared with this new instance.
func (h Helper) WithDispenser(d *caddyfile.Dispenser) Helper {
@@ -281,7 +356,7 @@ func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
return nil, err
}
return buildSubroute(allResults, h.groupCounter)
return buildSubroute(allResults, h.groupCounter, true)
}
// parseSegmentAsConfig parses the segment such that its subdirectives
@@ -340,6 +415,9 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
if err != nil {
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
}
dir = normalizeDirectiveName(dir)
for _, result := range results {
result.directive = dir
allResults = append(allResults, result)
@@ -365,7 +443,7 @@ type ConfigValue struct {
// The value to be used when building the config.
// Generally its type is associated with the
// name of the Class.
Value interface{}
Value any
directive string
}
@@ -396,7 +474,7 @@ func sortRoutes(routes []ConfigValue) {
return false
}
// decode the path matchers, if there is just one of them
// decode the path matchers if there is just one matcher set
var iPM, jPM caddyhttp.MatchPath
if len(iRoute.MatcherSetsRaw) == 1 {
_ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &iPM)
@@ -405,24 +483,47 @@ func sortRoutes(routes []ConfigValue) {
_ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &jPM)
}
// sort by longer path (more specific) first; missing path
// matchers or multi-matchers are treated as zero-length paths
// if there is only one path in the path matcher, sort by longer path
// (more specific) first; missing path matchers or multi-matchers are
// treated as zero-length paths
var iPathLen, jPathLen int
if len(iPM) > 0 {
if len(iPM) == 1 {
iPathLen = len(iPM[0])
}
if len(jPM) > 0 {
if len(jPM) == 1 {
jPathLen = len(jPM[0])
}
// if both directives have no path matcher, use whichever one
// has any kind of matcher defined first.
if iPathLen == 0 && jPathLen == 0 {
sortByPath := func() bool {
// we can only confidently compare path lengths if both
// directives have a single path to match (issue #5037)
if iPathLen > 0 && jPathLen > 0 {
// if both paths are the same except for a trailing wildcard,
// sort by the shorter path first (which is more specific)
if strings.TrimSuffix(iPM[0], "*") == strings.TrimSuffix(jPM[0], "*") {
return iPathLen < jPathLen
}
// sort most-specific (longest) path first
return iPathLen > jPathLen
}
// if both directives don't have a single path to compare,
// sort whichever one has a matcher first; if both have
// a matcher, sort equally (stable sort preserves order)
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
}()
// some directives involve setting values which can overwrite
// each other, so it makes most sense to reverse the order so
// that the least-specific matcher is first, allowing the last
// matching one to win
if iDir == "vars" {
return !sortByPath
}
// sort with the most-specific (longest) path first
return iPathLen > jPathLen
// everything else is most-specific matcher first
return sortByPath
})
}
@@ -510,6 +611,27 @@ func (sb serverBlock) hasHostCatchAllKey() bool {
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
}
// Positional are the supported modes for ordering directives.
type Positional string
const (
Before Positional = "before"
After Positional = "after"
First Positional = "first"
Last Positional = "last"
)
type (
// UnmarshalFunc is a function which can unmarshal Caddyfile
// tokens into zero or more config values using a Helper type.
@@ -531,7 +653,7 @@ type (
// tokens from a global option. It is passed the tokens to parse and
// existing value from the previous instance of this global option
// (if any). It returns the value to associate with this global option.
UnmarshalGlobalFunc func(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error)
UnmarshalGlobalFunc func(d *caddyfile.Dispenser, existingVal any) (any, error)
)
var registeredDirectives = make(map[string]UnmarshalFunc)
+6 -3
View File
@@ -31,20 +31,23 @@ func TestHostsFromKeys(t *testing.T) {
[]Address{
{Original: ":2015", Port: "2015"},
},
[]string{}, []string{},
[]string{},
[]string{},
},
{
[]Address{
{Original: ":443", Port: "443"},
},
[]string{}, []string{},
[]string{},
[]string{},
},
{
[]Address{
{Original: "foo", Host: "foo"},
{Original: ":2015", Port: "2015"},
},
[]string{}, []string{"foo"},
[]string{},
[]string{"foo"},
},
{
[]Address{
+464 -186
View File
@@ -17,13 +17,15 @@ package httpcaddyfile
import (
"encoding/json"
"fmt"
"log"
"net"
"reflect"
"regexp"
"slices"
"sort"
"strconv"
"strings"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -48,32 +50,27 @@ type App struct {
}
// ServerType can set up a config from an HTTP Caddyfile.
type ServerType struct {
}
type ServerType struct{}
// Setup makes a config from the tokens.
func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
options map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error) {
func (st ServerType) Setup(
inputServerBlocks []caddyfile.ServerBlock,
options map[string]any,
) (*caddy.Config, []caddyconfig.Warning, error) {
var warnings []caddyconfig.Warning
gc := counter{new(int)}
state := make(map[string]interface{})
state := make(map[string]any)
// load all the server blocks and associate them with a "pile"
// of config values; also prohibit duplicate keys because they
// can make a config confusing if more than one server block is
// chosen to handle a request - we actually will make each
// server block's route terminal so that only one will run
sbKeys := make(map[string]struct{})
// load all the server blocks and associate them with a "pile" of config values
originalServerBlocks := make([]serverBlock, 0, len(inputServerBlocks))
for i, sblock := range inputServerBlocks {
for _, sblock := range inputServerBlocks {
for j, k := range sblock.Keys {
if j == 0 && strings.HasPrefix(k, "@") {
return nil, warnings, fmt.Errorf("cannot define a matcher outside of a site block: '%s'", k)
if j == 0 && strings.HasPrefix(k.Text, "@") {
return nil, warnings, fmt.Errorf("%s:%d: cannot define a matcher outside of a site block: '%s'", k.File, k.Line, k.Text)
}
if _, ok := sbKeys[k]; ok {
return nil, warnings, fmt.Errorf("duplicate site address not allowed: '%s' in %v (site block %d, key %d)", k, sblock.Keys, i, j)
if _, ok := registeredDirectives[k.Text]; ok {
return nil, warnings, fmt.Errorf("%s:%d: parsed '%s' as a site address, but it is a known directive; directives must appear in a site block", k.File, k.Line, k.Text)
}
sbKeys[k] = struct{}{}
}
originalServerBlocks = append(originalServerBlocks, serverBlock{
block: sblock,
@@ -88,58 +85,18 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
return nil, warnings, err
}
// replace shorthand placeholders (which are
// convenient when writing a Caddyfile) with
// their actual placeholder identifiers or
// variable names
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}",
)
// this will replace both static and user-defined placeholder shorthands
// with actual identifiers used by Caddy
replacer := NewShorthandReplacer()
// these are placeholders that allow a user-defined final
// parameters, but we still want to provide a shorthand
// for those, so we use a regexp to replace
regexpReplacements := []struct {
search *regexp.Regexp
replace string
}{
{regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"},
{regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"},
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
{regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings, replacer)
if err != nil {
return nil, warnings, err
}
for _, sb := range originalServerBlocks {
for _, segment := range sb.block.Segments {
for i := 0; i < len(segment); i++ {
// simple string replacements
segment[i].Text = replacer.Replace(segment[i].Text)
// complex regexp replacements
for _, r := range regexpReplacements {
segment[i].Text = r.search.ReplaceAllString(segment[i].Text, r.replace)
}
}
for i := range sb.block.Segments {
replacer.ApplyToSegment(&sb.block.Segments[i])
}
if len(sb.block.Keys) == 0 {
@@ -192,18 +149,24 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err)
}
// 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 dir == "handle_path" {
dir = "handle"
}
dir = normalizeDirectiveName(dir)
for _, result := range results {
result.directive = dir
sb.pile[result.Class] = append(sb.pile[result.Class], result)
}
// specially handle named routes that were pulled out from
// the invoke directive, which could be nested anywhere within
// some subroutes in this directive; we add them to the pile
// for this server block
if state[namedRouteKey] != nil {
for name := range state[namedRouteKey].(map[string]struct{}) {
result := ConfigValue{Class: namedRouteKey, Value: name}
sb.pile[result.Class] = append(sb.pile[result.Class], result)
}
state[namedRouteKey] = nil
}
}
}
@@ -225,10 +188,11 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
// now that each server is configured, make the HTTP app
httpApp := caddyhttp.App{
HTTPPort: tryInt(options["http_port"], &warnings),
HTTPSPort: tryInt(options["https_port"], &warnings),
GracePeriod: tryDuration(options["grace_period"], &warnings),
Servers: servers,
HTTPPort: tryInt(options["http_port"], &warnings),
HTTPSPort: tryInt(options["https_port"], &warnings),
GracePeriod: tryDuration(options["grace_period"], &warnings),
ShutdownDelay: tryDuration(options["shutdown_delay"], &warnings),
Servers: servers,
}
// then make the TLS app
@@ -250,40 +214,44 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
if ncl.name == "" {
return
}
if ncl.name == "default" {
if ncl.name == caddy.DefaultLoggerName {
hasDefaultLog = true
}
if _, ok := options["debug"]; ok && ncl.log.Level == "" {
ncl.log.Level = "DEBUG"
if _, ok := options["debug"]; ok && ncl.log != nil && ncl.log.Level == "" {
ncl.log.Level = zap.DebugLevel.CapitalString()
}
customLogs = append(customLogs, ncl)
}
// Apply global log options, when set
if options["log"] != nil {
for _, logValue := range options["log"].([]ConfigValue) {
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 the default log was not customized, ensure we
// configure it with any applicable options
if _, ok := options["debug"]; ok {
customLogs = append(customLogs, namedCustomLog{
name: "default",
log: &caddy.CustomLog{Level: "DEBUG"},
name: caddy.DefaultLoggerName,
log: &caddy.CustomLog{
BaseLog: caddy.BaseLog{Level: zap.DebugLevel.CapitalString()},
},
})
}
}
// 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!
cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)}
@@ -306,6 +274,12 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
if !reflect.DeepEqual(pkiApp, &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}) {
cfg.AppsRaw["pki"] = caddyconfig.JSON(pkiApp, &warnings)
}
if filesystems, ok := options["filesystem"].(caddy.Module); ok {
cfg.AppsRaw["caddy.filesystems"] = caddyconfig.JSON(
filesystems,
&warnings)
}
if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok {
cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr,
"module",
@@ -315,28 +289,60 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
if adminConfig, ok := options["admin"].(*caddy.AdminConfig); ok && adminConfig != nil {
cfg.Admin = adminConfig
}
if pc, ok := options["persist_config"].(string); ok && pc == "off" {
if cfg.Admin == nil {
cfg.Admin = new(caddy.AdminConfig)
}
if cfg.Admin.Config == nil {
cfg.Admin.Config = new(caddy.ConfigSettings)
}
cfg.Admin.Config.Persist = new(bool)
}
if len(customLogs) > 0 {
if cfg.Logging == nil {
cfg.Logging = &caddy.Logging{
Logs: make(map[string]*caddy.CustomLog),
}
}
// Add the default log first if defined, so that it doesn't
// accidentally get re-created below due to the Exclude logic
for _, ncl := range customLogs {
if ncl.name == caddy.DefaultLoggerName && ncl.log != nil {
cfg.Logging.Logs[caddy.DefaultLoggerName] = ncl.log
break
}
}
// Add the rest of the custom logs
for _, ncl := range customLogs {
if ncl.log == nil || ncl.name == caddy.DefaultLoggerName {
continue
}
if ncl.name != "" {
cfg.Logging.Logs[ncl.name] = ncl.log
}
// most users seem to prefer not writing access logs
// to the default log when they are directed to a
// file or have any other special customization
if ncl.name != "default" && len(ncl.log.Include) > 0 {
defaultLog, ok := cfg.Logging.Logs["default"]
if ncl.name != caddy.DefaultLoggerName && len(ncl.log.Include) > 0 {
defaultLog, ok := cfg.Logging.Logs[caddy.DefaultLoggerName]
if !ok {
defaultLog = new(caddy.CustomLog)
cfg.Logging.Logs["default"] = defaultLog
cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog
}
defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...)
// avoid duplicates by sorting + compacting
sort.Strings(defaultLog.Exclude)
defaultLog.Exclude = slices.Compact[[]string, string](defaultLog.Exclude)
}
}
// we may have not actually added anything, so remove if empty
if len(cfg.Logging.Logs) == 0 {
cfg.Logging = nil
}
}
return cfg, warnings, nil
@@ -346,14 +352,14 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
// which is expected to be the first server block if it has zero
// keys. It returns the updated list of server blocks with the
// global options block removed, and updates options accordingly.
func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options map[string]interface{}) ([]serverBlock, error) {
func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options map[string]any) ([]serverBlock, error) {
if len(serverBlocks) == 0 || len(serverBlocks[0].block.Keys) > 0 {
return serverBlocks, nil
}
for _, segment := range serverBlocks[0].block.Segments {
opt := segment.Directive()
var val interface{}
var val any
var err error
disp := caddyfile.NewDispenser(segment)
@@ -419,16 +425,92 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
return serverBlocks[1:], nil
}
// extractNamedRoutes pulls out any named route server blocks
// so they don't get parsed as sites, and stores them in options
// for later.
func (ServerType) extractNamedRoutes(
serverBlocks []serverBlock,
options map[string]any,
warnings *[]caddyconfig.Warning,
replacer ShorthandReplacer,
) ([]serverBlock, error) {
namedRoutes := map[string]*caddyhttp.Route{}
gc := counter{new(int)}
state := make(map[string]any)
// copy the server blocks so we can
// splice out the named route ones
filtered := append([]serverBlock{}, serverBlocks...)
index := -1
for _, sb := range serverBlocks {
index++
if !sb.block.IsNamedRoute {
continue
}
// splice out this block, because we know it's not a real server
filtered = append(filtered[:index], filtered[index+1:]...)
index--
if len(sb.block.Segments) == 0 {
continue
}
wholeSegment := caddyfile.Segment{}
for i := range sb.block.Segments {
// replace user-defined placeholder shorthands in extracted named routes
replacer.ApplyToSegment(&sb.block.Segments[i])
// zip up all the segments since ParseSegmentAsSubroute
// was designed to take a directive+
wholeSegment = append(wholeSegment, sb.block.Segments[i]...)
}
h := Helper{
Dispenser: caddyfile.NewDispenser(wholeSegment),
options: options,
warnings: warnings,
matcherDefs: nil,
parentBlock: sb.block,
groupCounter: gc,
State: state,
}
handler, err := ParseSegmentAsSubroute(h)
if err != nil {
return nil, err
}
subroute := handler.(*caddyhttp.Subroute)
route := caddyhttp.Route{}
if len(subroute.Routes) == 1 && len(subroute.Routes[0].MatcherSetsRaw) == 0 {
// if there's only one route with no matcher, then we can simplify
route.HandlersRaw = append(route.HandlersRaw, subroute.Routes[0].HandlersRaw[0])
} else {
// otherwise we need the whole subroute
route.HandlersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", subroute.CaddyModule().ID.Name(), h.warnings)}
}
namedRoutes[sb.block.GetKeysText()[0]] = &route
}
options["named_routes"] = namedRoutes
return filtered, nil
}
// serversFromPairings creates the servers for each pairing of addresses
// to server blocks. Each pairing is essentially a server definition.
func (st *ServerType) serversFromPairings(
pairings []sbAddrAssociation,
options map[string]interface{},
options map[string]any,
warnings *[]caddyconfig.Warning,
groupCounter counter,
) (map[string]*caddyhttp.Server, error) {
servers := make(map[string]*caddyhttp.Server)
defaultSNI := tryString(options["default_sni"], warnings)
fallbackSNI := tryString(options["fallback_sni"], warnings)
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
if hp, ok := options["http_port"].(int); ok {
@@ -444,6 +526,23 @@ func (st *ServerType) serversFromPairings(
}
for i, p := range pairings {
// detect ambiguous site definitions: server blocks which
// have the same host bound to the same interface (listener
// address), otherwise their routes will improperly be added
// to the same server (see issue #4635)
for j, sblock1 := range p.serverBlocks {
for _, key := range sblock1.block.GetKeysText() {
for k, sblock2 := range p.serverBlocks {
if k == j {
continue
}
if sliceContains(sblock2.block.GetKeysText(), key) {
return nil, fmt.Errorf("ambiguous site definition: %s", key)
}
}
}
}
srv := &caddyhttp.Server{
Listen: p.addresses,
}
@@ -451,17 +550,29 @@ func (st *ServerType) serversFromPairings(
// handle the auto_https global option
if autoHTTPS != "on" {
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
if autoHTTPS == "off" {
switch autoHTTPS {
case "off":
srv.AutoHTTPS.Disabled = true
}
if autoHTTPS == "disable_redirects" {
case "disable_redirects":
srv.AutoHTTPS.DisableRedir = true
}
if autoHTTPS == "ignore_loaded_certs" {
case "disable_certs":
srv.AutoHTTPS.DisableCerts = true
case "ignore_loaded_certs":
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
// only the first matching site should be evaluated, and we should
// attempt to match most specific site first (host and path), in
@@ -518,15 +629,6 @@ func (st *ServerType) serversFromPairings(
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
autoHTTPSWillAddConnPolicy := autoHTTPS != "off"
// if a catch-all server block (one which accepts all hostnames) exists in this pairing,
// we need to know that so that we can configure logs properly (see #3878)
var catchAllSblockExists bool
for _, sblock := range p.serverBlocks {
if len(sblock.hostsFromKeys(false)) == 0 {
catchAllSblockExists = true
}
}
// if needed, the ServerLogConfig is initialized beforehand so
// that all server blocks can populate it with data, even when not
// coming with a log directive
@@ -537,6 +639,24 @@ func (st *ServerType) serversFromPairings(
}
}
// add named routes to the server if 'invoke' was used inside of it
configuredNamedRoutes := options["named_routes"].(map[string]*caddyhttp.Route)
for _, sblock := range p.serverBlocks {
if len(sblock.pile[namedRouteKey]) == 0 {
continue
}
for _, value := range sblock.pile[namedRouteKey] {
if srv.NamedRoutes == nil {
srv.NamedRoutes = map[string]*caddyhttp.Route{}
}
name := value.Value.(string)
if configuredNamedRoutes[name] == nil {
return nil, fmt.Errorf("cannot invoke named route '%s', which was not defined", name)
}
srv.NamedRoutes[name] = configuredNamedRoutes[name]
}
}
// create a subroute for each site in the server block
for _, sblock := range p.serverBlocks {
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
@@ -549,7 +669,7 @@ func (st *ServerType) serversFromPairings(
// emit warnings if user put unspecified IP addresses; they probably want the bind directive
for _, h := range hosts {
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))
}
}
@@ -566,14 +686,21 @@ func (st *ServerType) serversFromPairings(
cp.DefaultSNI = defaultSNI
break
}
if h == fallbackSNI {
hosts = append(hosts, "")
cp.FallbackSNI = fallbackSNI
break
}
}
if len(hosts) > 0 {
slices.Sort(hosts) // for deterministic JSON output
cp.MatchersRaw = caddy.ModuleMap{
"sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones
}
} else {
cp.DefaultSNI = defaultSNI
cp.FallbackSNI = fallbackSNI
}
// only append this policy if it actually changes something
@@ -585,7 +712,7 @@ func (st *ServerType) serversFromPairings(
}
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) {
// exclude any hosts that were defined explicitly with "http://"
// in the key from automated cert management (issue #2998)
@@ -599,10 +726,20 @@ func (st *ServerType) serversFromPairings(
}
}
// If TLS is specified as directive, it will also result in 1 or more connection policy being created
// Thus, catch-all address with non-standard port, e.g. :8443, can have TLS enabled without
// specifying prefix "https://"
// Second part of the condition is to allow creating TLS conn policy even though `auto_https` has been disabled
// ensuring compatibility with behavior described in below link
// https://caddy.community/t/making-sense-of-auto-https-and-why-disabling-it-still-serves-https-instead-of-http/9761
createdTLSConnPolicies, ok := sblock.pile["tls.connection_policy"]
hasTLSEnabled := (ok && len(createdTLSConnPolicies) > 0) ||
(addr.Host != "" && srv.AutoHTTPS != nil && !sliceContains(srv.AutoHTTPS.Skip, addr.Host))
// we'll need to remember if the address qualifies for auto-HTTPS, so we
// can add a TLS conn policy if necessary
if addr.Scheme == "https" ||
(addr.Scheme != "http" && addr.Host != "" && addr.Port != httpPort) {
(addr.Scheme != "http" && addr.Port != httpPort && hasTLSEnabled) {
addressQualifiesForTLS = true
}
// predict whether auto-HTTPS will add the conn policy for us; if so, we
@@ -627,7 +764,7 @@ func (st *ServerType) serversFromPairings(
// set up each handler directive, making sure to honor directive order
dirRoutes := sblock.pile["route"]
siteSubroute, err := buildSubroute(dirRoutes, groupCounter)
siteSubroute, err := buildSubroute(dirRoutes, groupCounter, true)
if err != nil {
return nil, err
}
@@ -640,10 +777,19 @@ func (st *ServerType) serversFromPairings(
if srv.Errors == nil {
srv.Errors = new(caddyhttp.HTTPErrorConfig)
}
sort.SliceStable(errorSubrouteVals, func(i, j int) bool {
sri, srj := errorSubrouteVals[i].Value.(*caddyhttp.Subroute), errorSubrouteVals[j].Value.(*caddyhttp.Subroute)
if len(sri.Routes[0].MatcherSetsRaw) == 0 && len(srj.Routes[0].MatcherSetsRaw) != 0 {
return false
}
return true
})
errorsSubroute := &caddyhttp.Subroute{}
for _, val := range errorSubrouteVals {
sr := val.Value.(*caddyhttp.Subroute)
srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, sr, matcherSetsEnc, p, warnings)
errorsSubroute.Routes = append(errorsSubroute.Routes, sr.Routes...)
}
srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, errorsSubroute, matcherSetsEnc, p, warnings)
}
// add log associations
@@ -651,25 +797,39 @@ func (st *ServerType) serversFromPairings(
sblockLogHosts := sblock.hostsFromKeys(true)
for _, cval := range sblock.pile["custom_log"] {
ncl := cval.Value.(namedCustomLog)
if sblock.hasHostCatchAllKey() {
// if `no_hostname` is set, then this logger will not
// be associated with any of the site block's hostnames,
// and only be usable via the `log_name` directive
// or the `access_logger_names` variable
if ncl.noHostname {
continue
}
if sblock.hasHostCatchAllKey() && len(ncl.hostnames) == 0 {
// all requests for hosts not able to be listed should use
// this log because it's a catch-all-hosts server block
srv.Logs.DefaultLoggerName = ncl.name
} else {
// map each host to the user's desired logger name
for _, h := range sblockLogHosts {
// if the custom logger name is non-empty, add it to the map;
// otherwise, only map to an empty logger name if this or
// another site block on this server has a catch-all host (in
// which case only requests with mapped hostnames will be
// access-logged, so it'll be necessary to add them to the
// map even if they use default logger)
if ncl.name != "" || catchAllSblockExists {
if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string)
}
srv.Logs.LoggerNames[h] = ncl.name
} else if len(ncl.hostnames) > 0 {
// if the logger overrides the hostnames, map that to the logger name
for _, h := range ncl.hostnames {
if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]caddyhttp.StringArray)
}
srv.Logs.LoggerNames[h] = append(srv.Logs.LoggerNames[h], ncl.name)
}
} else {
// otherwise, map each host to the logger name
for _, h := range sblockLogHosts {
// strip the port from the host, if any
host, _, err := net.SplitHostPort(h)
if err != nil {
host = h
}
if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]caddyhttp.StringArray)
}
srv.Logs.LoggerNames[host] = append(srv.Logs.LoggerNames[host], ncl.name)
}
}
}
@@ -686,6 +846,11 @@ func (st *ServerType) serversFromPairings(
}
}
// sort for deterministic JSON output
if srv.Logs != nil {
slices.Sort(srv.Logs.SkipHosts)
}
// a server cannot (natively) serve both HTTP and HTTPS at the
// same time, so make sure the configuration isn't in conflict
err := detectConflictingSchemes(srv, p.serverBlocks, options)
@@ -707,8 +872,8 @@ func (st *ServerType) serversFromPairings(
// policy missing for any HTTPS-enabled hosts, if so, add it... maybe?
if addressQualifiesForTLS &&
!hasCatchAllTLSConnPolicy &&
(len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "") {
srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI})
(len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "" || fallbackSNI != "") {
srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI, FallbackSNI: fallbackSNI})
}
// tidy things up a bit
@@ -723,13 +888,13 @@ func (st *ServerType) serversFromPairings(
err := applyServerOptions(servers, options, warnings)
if err != nil {
return nil, err
return nil, fmt.Errorf("applying global server options: %v", err)
}
return servers, nil
}
func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock, options map[string]interface{}) error {
func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock, options map[string]any) error {
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp)
@@ -917,18 +1082,39 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
subroute *caddyhttp.Subroute,
matcherSetsEnc []caddy.ModuleMap,
p sbAddrAssociation,
warnings *[]caddyconfig.Warning) caddyhttp.RouteList {
warnings *[]caddyconfig.Warning,
) caddyhttp.RouteList {
// nothing to do if... there's nothing to do
if len(matcherSetsEnc) == 0 && len(subroute.Routes) == 0 && subroute.Errors == nil {
return routeList
}
// No need to wrap the handlers in a subroute if this is the only server block
// and there is no matcher for it (doing so would produce unnecessarily nested
// JSON), *unless* there is a host matcher within this site block; if so, then
// we still need to wrap in a subroute because otherwise the host matcher from
// the inside of the site block would be a top-level host matcher, which is
// subject to auto-HTTPS (cert management), and using a host matcher within
// a site block is a valid, common pattern for excluding domains from cert
// management, leading to unexpected behavior; see issue #5124.
wrapInSubroute := true
if len(matcherSetsEnc) == 0 && len(p.serverBlocks) == 1 {
// no need to wrap the handlers in a subroute if this is
// the only server block and there is no matcher for it
routeList = append(routeList, subroute.Routes...)
} else {
var hasHostMatcher bool
outer:
for _, route := range subroute.Routes {
for _, ms := range route.MatcherSetsRaw {
for matcherName := range ms {
if matcherName == "host" {
hasHostMatcher = true
break outer
}
}
}
}
wrapInSubroute = hasHostMatcher
}
if wrapInSubroute {
route := caddyhttp.Route{
// the semantics of a site block in the Caddyfile dictate
// that only the first matching one is evaluated, since
@@ -946,20 +1132,25 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
if len(route.MatcherSetsRaw) > 0 || len(route.HandlersRaw) > 0 {
routeList = append(routeList, route)
}
} else {
routeList = append(routeList, subroute.Routes...)
}
return routeList
}
// buildSubroute turns the config values, which are expected to be routes
// into a clean and orderly subroute that has all the routes within it.
func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subroute, error) {
for _, val := range routes {
if !directiveIsOrdered(val.directive) {
return nil, fmt.Errorf("directive '%s' is not ordered, so it cannot be used here", val.directive)
func buildSubroute(routes []ConfigValue, groupCounter counter, needsSorting bool) (*caddyhttp.Subroute, error) {
if needsSorting {
for _, val := range routes {
if !directiveIsOrdered(val.directive) {
return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here - try placing within a route block or using the order global option", val.directive)
}
}
}
sortRoutes(routes)
sortRoutes(routes)
}
subroute := new(caddyhttp.Subroute)
@@ -1060,6 +1251,19 @@ func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subro
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
// (same matchers, same Terminal and Group settings) for a
// cleaner overall output.
@@ -1087,19 +1291,24 @@ func matcherSetFromMatcherToken(
if tkn.Text == "*" {
// match all requests == no matchers, so nothing to do
return nil, true, nil
} else if strings.HasPrefix(tkn.Text, "/") {
// convenient way to specify a single path match
}
// convenient way to specify a single path match
if strings.HasPrefix(tkn.Text, "/") {
return caddy.ModuleMap{
"path": caddyconfig.JSON(caddyhttp.MatchPath{tkn.Text}, warnings),
}, true, nil
} else if strings.HasPrefix(tkn.Text, matcherPrefix) {
// pre-defined matcher
}
// pre-defined matcher
if strings.HasPrefix(tkn.Text, matcherPrefix) {
m, ok := matcherDefs[tkn.Text]
if !ok {
return nil, false, fmt.Errorf("unrecognized matcher name: %+v", tkn.Text)
}
return m, true, nil
}
return nil, false, nil
}
@@ -1189,41 +1398,83 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod
}
func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.ModuleMap) error {
for d.Next() {
definitionName := d.Val()
d.Next() // advance to the first token
if _, ok := matchers[definitionName]; ok {
return fmt.Errorf("matcher is defined more than once: %s", definitionName)
}
matchers[definitionName] = make(caddy.ModuleMap)
// this is the "name" for "named matchers"
definitionName := d.Val()
// in case there are multiple instances of the same matcher, concatenate
// their tokens (we expect that UnmarshalCaddyfile should be able to
// handle more than one segment); otherwise, we'd overwrite other
// instances of the matcher in this set
tokensByMatcherName := make(map[string][]caddyfile.Token)
for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
matcherName := d.Val()
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
if _, ok := matchers[definitionName]; ok {
return fmt.Errorf("matcher is defined more than once: %s", definitionName)
}
matchers[definitionName] = make(caddy.ModuleMap)
// given a matcher name and the tokens following it, parse
// the tokens as a matcher module and record it
makeMatcher := func(matcherName string, tokens []caddyfile.Token) error {
// create a new dispenser from the tokens
dispenser := caddyfile.NewDispenser(tokens)
// set the matcher name (without @) in the dispenser context so
// that matcher modules can access it to use it as their name
// (e.g. regexp matchers which use the name for capture groups)
dispenser.SetContext(caddyfile.MatcherNameCtxKey, definitionName[1:])
mod, err := caddy.GetModule("http.matchers." + matcherName)
if err != nil {
return fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
}
for matcherName, tokens := range tokensByMatcherName {
mod, err := caddy.GetModule("http.matchers." + matcherName)
if err != nil {
return fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
}
err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(tokens))
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
}
err = unm.UnmarshalCaddyfile(dispenser)
if err != nil {
return err
}
rm, ok := unm.(caddyhttp.RequestMatcher)
if !ok {
return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
}
matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
return nil
}
// if the next token is quoted, we can assume it's not a matcher name
// and that it's probably an 'expression' matcher
if d.NextArg() {
if d.Token().Quoted() {
// since it was missing the matcher name, we insert a token
// in front of the expression token itself; we use Clone() to
// make the new token to keep the same the import location as
// the next token, if this is within a snippet or imported file.
// see https://github.com/caddyserver/caddy/issues/6287
expressionToken := d.Token().Clone()
expressionToken.Text = "expression"
err := makeMatcher("expression", []caddyfile.Token{expressionToken, d.Token()})
if err != nil {
return err
}
rm, ok := unm.(caddyhttp.RequestMatcher)
if !ok {
return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
}
matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
return nil
}
// if it wasn't quoted, then we need to rewind after calling
// d.NextArg() so the below properly grabs the matcher name
d.Prev()
}
// in case there are multiple instances of the same matcher, concatenate
// their tokens (we expect that UnmarshalCaddyfile should be able to
// handle more than one segment); otherwise, we'd overwrite other
// instances of the matcher in this set
tokensByMatcherName := make(map[string][]caddyfile.Token)
for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
matcherName := d.Val()
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
}
for matcherName, tokens := range tokensByMatcherName {
err := makeMatcher(matcherName, tokens)
if err != nil {
return err
}
}
return nil
@@ -1241,9 +1492,31 @@ func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.Modul
return msEncoded, nil
}
// 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,
// it downgrades the error to a warning and returns 0.
func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int {
func tryInt(val any, warnings *[]caddyconfig.Warning) int {
intVal, ok := val.(int)
if val != nil && !ok && warnings != nil {
*warnings = append(*warnings, caddyconfig.Warning{Message: "not an integer type"})
@@ -1251,7 +1524,7 @@ func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int {
return intVal
}
func tryString(val interface{}, warnings *[]caddyconfig.Warning) string {
func tryString(val any, warnings *[]caddyconfig.Warning) string {
stringVal, ok := val.(string)
if val != nil && !ok && warnings != nil {
*warnings = append(*warnings, caddyconfig.Warning{Message: "not a string type"})
@@ -1259,7 +1532,7 @@ func tryString(val interface{}, warnings *[]caddyconfig.Warning) string {
return stringVal
}
func tryDuration(val interface{}, warnings *[]caddyconfig.Warning) caddy.Duration {
func tryDuration(val any, warnings *[]caddyconfig.Warning) caddy.Duration {
durationVal, ok := val.(caddy.Duration)
if val != nil && !ok && warnings != nil {
*warnings = append(*warnings, caddyconfig.Warning{Message: "not a duration type"})
@@ -1334,8 +1607,10 @@ func (c counter) nextGroup() string {
}
type namedCustomLog struct {
name string
log *caddy.CustomLog
name string
hostnames []string
log *caddy.CustomLog
noHostname bool
}
// sbAddrAssociation is a mapping from a list of
@@ -1346,7 +1621,10 @@ type sbAddrAssociation struct {
serverBlocks []serverBlock
}
const matcherPrefix = "@"
const (
matcherPrefix = "@"
namedRouteKey = "named_route"
)
// Interface guard
var _ caddyfile.ServerType = (*ServerType)(nil)
+272 -215
View File
@@ -17,23 +17,29 @@ package httpcaddyfile
import (
"strconv"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/v2/acme"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
)
func init() {
RegisterGlobalOption("debug", parseOptTrue)
RegisterGlobalOption("http_port", parseOptHTTPPort)
RegisterGlobalOption("https_port", parseOptHTTPSPort)
RegisterGlobalOption("default_bind", parseOptStringList)
RegisterGlobalOption("grace_period", parseOptDuration)
RegisterGlobalOption("shutdown_delay", parseOptDuration)
RegisterGlobalOption("default_sni", parseOptSingleString)
RegisterGlobalOption("fallback_sni", parseOptSingleString)
RegisterGlobalOption("order", parseOptOrder)
RegisterGlobalOption("storage", parseOptStorage)
RegisterGlobalOption("storage_clean_interval", parseOptDuration)
RegisterGlobalOption("renew_interval", parseOptDuration)
RegisterGlobalOption("ocsp_interval", parseOptDuration)
RegisterGlobalOption("acme_ca", parseOptSingleString)
RegisterGlobalOption("acme_ca_root", parseOptSingleString)
RegisterGlobalOption("acme_dns", parseOptACMEDNS)
@@ -48,112 +54,112 @@ func init() {
RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
RegisterGlobalOption("servers", parseServerOptions)
RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
RegisterGlobalOption("cert_lifetime", parseOptDuration)
RegisterGlobalOption("log", parseLogOptions)
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
RegisterGlobalOption("persist_config", parseOptPersistConfig)
}
func parseOptTrue(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { return true, nil }
func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil }
func parseOptHTTPPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
func parseOptHTTPPort(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
var httpPort int
for d.Next() {
var httpPortStr string
if !d.AllArgs(&httpPortStr) {
return 0, d.ArgErr()
}
var err error
httpPort, err = strconv.Atoi(httpPortStr)
if err != nil {
return 0, d.Errf("converting port '%s' to integer value: %v", httpPortStr, err)
}
var httpPortStr string
if !d.AllArgs(&httpPortStr) {
return 0, d.ArgErr()
}
var err error
httpPort, err = strconv.Atoi(httpPortStr)
if err != nil {
return 0, d.Errf("converting port '%s' to integer value: %v", httpPortStr, err)
}
return httpPort, nil
}
func parseOptHTTPSPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
func parseOptHTTPSPort(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
var httpsPort int
for d.Next() {
var httpsPortStr string
if !d.AllArgs(&httpsPortStr) {
return 0, d.ArgErr()
}
var err error
httpsPort, err = strconv.Atoi(httpsPortStr)
if err != nil {
return 0, d.Errf("converting port '%s' to integer value: %v", httpsPortStr, err)
}
var httpsPortStr string
if !d.AllArgs(&httpsPortStr) {
return 0, d.ArgErr()
}
var err error
httpsPort, err = strconv.Atoi(httpsPortStr)
if err != nil {
return 0, d.Errf("converting port '%s' to integer value: %v", httpsPortStr, err)
}
return httpsPort, nil
}
func parseOptOrder(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
// get directive name
if !d.Next() {
return nil, d.ArgErr()
}
dirName := d.Val()
if _, ok := registeredDirectives[dirName]; !ok {
return nil, d.Errf("%s is not a registered directive", dirName)
}
// get positional token
if !d.Next() {
return nil, d.ArgErr()
}
pos := Positional(d.Val())
newOrder := directiveOrder
for d.Next() {
// get directive name
if !d.Next() {
return nil, d.ArgErr()
}
dirName := d.Val()
if _, ok := registeredDirectives[dirName]; !ok {
return nil, d.Errf("%s is not a registered directive", dirName)
// if directive exists, first remove it
for i, d := range newOrder {
if d == dirName {
newOrder = append(newOrder[:i], newOrder[i+1:]...)
break
}
}
// get positional token
if !d.Next() {
return nil, d.ArgErr()
}
pos := d.Val()
// if directive exists, first remove it
for i, d := range newOrder {
if d == dirName {
newOrder = append(newOrder[:i], newOrder[i+1:]...)
break
}
}
// act on the positional
switch pos {
case "first":
newOrder = append([]string{dirName}, newOrder...)
if d.NextArg() {
return nil, d.ArgErr()
}
directiveOrder = newOrder
return newOrder, nil
case "last":
newOrder = append(newOrder, dirName)
if d.NextArg() {
return nil, d.ArgErr()
}
directiveOrder = newOrder
return newOrder, nil
case "before":
case "after":
default:
return nil, d.Errf("unknown positional '%s'", pos)
}
// get name of other directive
if !d.NextArg() {
return nil, d.ArgErr()
}
otherDir := d.Val()
// act on the positional
switch pos {
case First:
newOrder = append([]string{dirName}, newOrder...)
if d.NextArg() {
return nil, d.ArgErr()
}
directiveOrder = newOrder
return newOrder, nil
case Last:
newOrder = append(newOrder, dirName)
if d.NextArg() {
return nil, d.ArgErr()
}
directiveOrder = newOrder
return newOrder, nil
case Before:
case After:
default:
return nil, d.Errf("unknown positional '%s'", pos)
}
// insert directive into proper position
for i, d := range newOrder {
if d == otherDir {
if pos == "before" {
newOrder = append(newOrder[:i], append([]string{dirName}, newOrder[i:]...)...)
} else if pos == "after" {
newOrder = append(newOrder[:i+1], append([]string{dirName}, newOrder[i+1:]...)...)
}
break
// get name of other directive
if !d.NextArg() {
return nil, d.ArgErr()
}
otherDir := d.Val()
if d.NextArg() {
return nil, d.ArgErr()
}
// insert directive into proper position
for i, d := range newOrder {
if d == otherDir {
if pos == Before {
newOrder = append(newOrder[:i], append([]string{dirName}, newOrder[i:]...)...)
} else if pos == After {
newOrder = append(newOrder[:i+1], append([]string{dirName}, newOrder[i+1:]...)...)
}
break
}
}
@@ -162,7 +168,7 @@ func parseOptOrder(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
return newOrder, nil
}
func parseOptStorage(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
func parseOptStorage(d *caddyfile.Dispenser, _ any) (any, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
@@ -181,7 +187,7 @@ func parseOptStorage(d *caddyfile.Dispenser, _ interface{}) (interface{}, error)
return storage, nil
}
func parseOptDuration(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
@@ -195,7 +201,7 @@ func parseOptDuration(d *caddyfile.Dispenser, _ interface{}) (interface{}, error
return caddy.Duration(dur), nil
}
func parseOptACMEDNS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
@@ -207,66 +213,67 @@ func parseOptACMEDNS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error)
if err != nil {
return nil, err
}
prov, ok := unm.(certmagic.ACMEDNSProvider)
prov, ok := unm.(certmagic.DNSProvider)
if !ok {
return nil, d.Errf("module %s (%T) is not a certmagic.ACMEDNSProvider", modID, unm)
return nil, d.Errf("module %s (%T) is not a certmagic.DNSProvider", modID, unm)
}
return prov, nil
}
func parseOptACMEEAB(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) {
eab := new(acme.EAB)
for d.Next() {
if d.NextArg() {
return nil, d.ArgErr()
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "key_id":
if !d.NextArg() {
return nil, d.ArgErr()
}
eab.KeyID = d.Val()
case "mac_key":
if !d.NextArg() {
return nil, d.ArgErr()
}
eab.MACKey = d.Val()
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
d.Next() // consume option name
if d.NextArg() {
return nil, d.ArgErr()
}
for d.NextBlock(0) {
switch d.Val() {
case "key_id":
if !d.NextArg() {
return nil, d.ArgErr()
}
eab.KeyID = d.Val()
case "mac_key":
if !d.NextArg() {
return nil, d.ArgErr()
}
eab.MACKey = d.Val()
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
}
}
return eab, nil
}
func parseOptCertIssuer(d *caddyfile.Dispenser, existing interface{}) (interface{}, error) {
func parseOptCertIssuer(d *caddyfile.Dispenser, existing any) (any, error) {
d.Next() // consume option name
var issuers []certmagic.Issuer
if existing != nil {
issuers = existing.([]certmagic.Issuer)
}
for d.Next() { // consume option name
if !d.Next() { // get issuer module name
return nil, d.ArgErr()
}
modID := "tls.issuance." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
iss, ok := unm.(certmagic.Issuer)
if !ok {
return nil, d.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm)
}
issuers = append(issuers, iss)
// get issuer module name
if !d.Next() {
return nil, d.ArgErr()
}
modID := "tls.issuance." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
iss, ok := unm.(certmagic.Issuer)
if !ok {
return nil, d.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm)
}
issuers = append(issuers, iss)
return issuers, nil
}
func parseOptSingleString(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() // consume parameter name
func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
if !d.Next() {
return "", d.ArgErr()
}
@@ -277,34 +284,43 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ interface{}) (interface{}, e
return val, nil
}
func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
func parseOptStringList(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
val := d.RemainingArgs()
if len(val) == 0 {
return "", d.ArgErr()
}
return val, nil
}
func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
adminCfg := new(caddy.AdminConfig)
for d.Next() {
if d.NextArg() {
listenAddress := d.Val()
if listenAddress == "off" {
adminCfg.Disabled = true
if d.Next() { // Do not accept any remaining options including block
return nil, d.Err("No more option is allowed after turning off admin config")
}
} else {
adminCfg.Listen = listenAddress
if d.NextArg() { // At most 1 arg is allowed
return nil, d.ArgErr()
}
if d.NextArg() {
listenAddress := d.Val()
if listenAddress == "off" {
adminCfg.Disabled = true
if d.Next() { // Do not accept any remaining options including block
return nil, d.Err("No more option is allowed after turning off admin config")
}
} else {
adminCfg.Listen = listenAddress
if d.NextArg() { // At most 1 arg is allowed
return nil, d.ArgErr()
}
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "enforce_origin":
adminCfg.EnforceOrigin = true
}
for d.NextBlock(0) {
switch d.Val() {
case "enforce_origin":
adminCfg.EnforceOrigin = true
case "origins":
adminCfg.Origins = d.RemainingArgs()
case "origins":
adminCfg.Origins = d.RemainingArgs()
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
}
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
}
}
if adminCfg.Listen == "" && !adminCfg.Disabled {
@@ -313,58 +329,85 @@ func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
return adminCfg, nil
}
func parseOptOnDemand(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
if d.NextArg() {
return nil, d.ArgErr()
}
var ond *caddytls.OnDemandConfig
for d.Next() {
if d.NextArg() {
return nil, d.ArgErr()
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "ask":
if !d.NextArg() {
return nil, d.ArgErr()
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
ond.Ask = d.Val()
case "interval":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, err
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
if ond.RateLimit == nil {
ond.RateLimit = new(caddytls.RateLimit)
}
ond.RateLimit.Interval = caddy.Duration(dur)
case "burst":
if !d.NextArg() {
return nil, d.ArgErr()
}
burst, err := strconv.Atoi(d.Val())
if err != nil {
return nil, err
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
if ond.RateLimit == nil {
ond.RateLimit = new(caddytls.RateLimit)
}
ond.RateLimit.Burst = burst
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "ask":
if !d.NextArg() {
return nil, d.ArgErr()
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
if ond.PermissionRaw != nil {
return nil, d.Err("on-demand TLS permission module (or 'ask') already specified")
}
perm := caddytls.PermissionByHTTP{Endpoint: d.Val()}
ond.PermissionRaw = caddyconfig.JSONModuleObject(perm, "module", "http", nil)
case "permission":
if !d.NextArg() {
return nil, d.ArgErr()
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
if ond.PermissionRaw != nil {
return nil, d.Err("on-demand TLS permission module (or 'ask') already specified")
}
modName := d.Val()
modID := "tls.permission." + modName
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
perm, ok := unm.(caddytls.OnDemandPermission)
if !ok {
return nil, d.Errf("module %s (%T) is not an on-demand TLS permission module", modID, unm)
}
ond.PermissionRaw = caddyconfig.JSONModuleObject(perm, "module", modName, nil)
case "interval":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, err
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
if ond.RateLimit == nil {
ond.RateLimit = new(caddytls.RateLimit)
}
ond.RateLimit.Interval = caddy.Duration(dur)
case "burst":
if !d.NextArg() {
return nil, d.ArgErr()
}
burst, err := strconv.Atoi(d.Val())
if err != nil {
return nil, err
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
if ond.RateLimit == nil {
ond.RateLimit = new(caddytls.RateLimit)
}
ond.RateLimit.Burst = burst
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
}
}
if ond == nil {
@@ -373,8 +416,8 @@ func parseOptOnDemand(d *caddyfile.Dispenser, _ interface{}) (interface{}, error
return ond, nil
}
func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() // consume parameter name
func parseOptPersistConfig(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
if !d.Next() {
return "", d.ArgErr()
}
@@ -382,17 +425,32 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ interface{}) (interface{}, erro
if d.Next() {
return "", d.ArgErr()
}
if val != "off" && val != "disable_redirects" && val != "ignore_loaded_certs" {
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects' or 'ignore_loaded_certs'")
if val != "off" {
return "", d.Errf("persist_config must be 'off'")
}
return val, nil
}
func parseServerOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
if !d.Next() {
return "", d.ArgErr()
}
val := d.Val()
if d.Next() {
return "", d.ArgErr()
}
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', 'disable_certs', or 'ignore_loaded_certs'")
}
return val, nil
}
func parseServerOptions(d *caddyfile.Dispenser, _ any) (any, error) {
return unmarshalCaddyfileServerOptions(d)
}
func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
var val string
if !d.AllArgs(&val) {
@@ -408,18 +466,17 @@ func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ interface{}) (interface{
// parseLogOptions parses the global log option. Syntax:
//
// log [name] {
// output <writer_module> ...
// format <encoder_module> ...
// level <level>
// include <namespaces...>
// exclude <namespaces...>
// }
// log [name] {
// output <writer_module> ...
// format <encoder_module> ...
// level <level>
// include <namespaces...>
// exclude <namespaces...>
// }
//
// When the name argument is unspecified, this directive modifies the default
// logger.
//
func parseLogOptions(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) {
func parseLogOptions(d *caddyfile.Dispenser, existingVal any) (any, error) {
currentNames := make(map[string]struct{})
if existingVal != nil {
innerVals, ok := existingVal.([]ConfigValue)
@@ -454,7 +511,7 @@ func parseLogOptions(d *caddyfile.Dispenser, existingVal interface{}) (interface
return configValues, nil
}
func parseOptPreferredChains(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
func parseOptPreferredChains(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next()
return caddytls.ParseCaddyfilePreferredChainsOptions(d)
}
+173 -5
View File
@@ -15,24 +15,187 @@
package httpcaddyfile
import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"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>
// intermediate_lifetime <duration>
// 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 any) (any, error) {
d.Next() // consume app name
pki := &caddypki.PKI{
CAs: make(map[string]*caddypki.CA),
}
for d.NextBlock(0) {
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 "intermediate_lifetime":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, err
}
pkiCa.IntermediateLifetime = caddy.Duration(dur)
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(
pairings []sbAddrAssociation,
options map[string]interface{},
options map[string]any,
warnings []caddyconfig.Warning,
) (*caddypki.PKI, []caddyconfig.Warning, error) {
pkiApp := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
skipInstallTrust := false
if _, ok := options["skip_install_trust"]; ok {
skipInstallTrust = true
}
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 _, sblock := range p.serverBlocks {
// find all the CAs that were defined and add them to the app config
@@ -42,7 +205,12 @@ func (st ServerType) buildPKIApp(
if skipInstallTrust {
ca.InstallTrust = &falseBool
}
pkiApp.CAs[ca.ID] = ca
// 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
}
}
}
}
+262 -139
View File
@@ -18,11 +18,12 @@ import (
"encoding/json"
"fmt"
"github.com/dustin/go-humanize"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/dustin/go-humanize"
)
// serverOptions collects server config overrides parsed from Caddyfile global options
@@ -33,137 +34,227 @@ type serverOptions struct {
ListenerAddress string
// These will all map 1:1 to the caddyhttp.Server struct
ListenerWrappersRaw []json.RawMessage
ReadTimeout caddy.Duration
ReadHeaderTimeout caddy.Duration
WriteTimeout caddy.Duration
IdleTimeout caddy.Duration
MaxHeaderBytes int
AllowH2C bool
ExperimentalHTTP3 bool
StrictSNIHost *bool
Name string
ListenerWrappersRaw []json.RawMessage
ReadTimeout caddy.Duration
ReadHeaderTimeout caddy.Duration
WriteTimeout caddy.Duration
IdleTimeout caddy.Duration
KeepAliveInterval caddy.Duration
MaxHeaderBytes int
EnableFullDuplex bool
Protocols []string
StrictSNIHost *bool
TrustedProxiesRaw json.RawMessage
TrustedProxiesStrict int
ClientIPHeaders []string
ShouldLogCredentials bool
Metrics *caddyhttp.Metrics
Trace bool // TODO: EXPERIMENTAL
}
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
d.Next() // consume option name
serverOpts := serverOptions{}
for d.Next() {
if d.NextArg() {
serverOpts.ListenerAddress = d.Val()
if d.NextArg() {
serverOpts.ListenerAddress = d.Val()
return nil, d.ArgErr()
}
}
for d.NextBlock(0) {
switch d.Val() {
case "name":
if serverOpts.ListenerAddress == "" {
return nil, d.Errf("cannot set a name for a server without a listener address")
}
if !d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.Name = d.Val()
case "listener_wrappers":
for nesting := d.Nesting(); d.NextBlock(nesting); {
modID := "caddy.listeners." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
listenerWrapper, ok := unm.(caddy.ListenerWrapper)
if !ok {
return nil, fmt.Errorf("module %s (%T) is not a listener wrapper", modID, unm)
}
jsonListenerWrapper := caddyconfig.JSONModuleObject(
listenerWrapper,
"wrapper",
listenerWrapper.(caddy.Module).CaddyModule().ID.Name(),
nil,
)
serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper)
}
case "timeouts":
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "read_body":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing read_body timeout duration: %v", err)
}
serverOpts.ReadTimeout = caddy.Duration(dur)
case "read_header":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing read_header timeout duration: %v", err)
}
serverOpts.ReadHeaderTimeout = caddy.Duration(dur)
case "write":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing write timeout duration: %v", err)
}
serverOpts.WriteTimeout = caddy.Duration(dur)
case "idle":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing idle timeout duration: %v", err)
}
serverOpts.IdleTimeout = caddy.Duration(dur)
default:
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
}
}
case "keepalive_interval":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing keepalive interval duration: %v", err)
}
serverOpts.KeepAliveInterval = caddy.Duration(dur)
case "max_header_size":
var sizeStr string
if !d.AllArgs(&sizeStr) {
return nil, d.ArgErr()
}
size, err := humanize.ParseBytes(sizeStr)
if err != nil {
return nil, d.Errf("parsing max_header_size: %v", err)
}
serverOpts.MaxHeaderBytes = int(size)
case "enable_full_duplex":
if d.NextArg() {
return nil, d.ArgErr()
}
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "listener_wrappers":
for nesting := d.Nesting(); d.NextBlock(nesting); {
modID := "caddy.listeners." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
listenerWrapper, ok := unm.(caddy.ListenerWrapper)
if !ok {
return nil, fmt.Errorf("module %s (%T) is not a listener wrapper", modID, unm)
}
jsonListenerWrapper := caddyconfig.JSONModuleObject(
listenerWrapper,
"wrapper",
listenerWrapper.(caddy.Module).CaddyModule().ID.Name(),
nil,
)
serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper)
}
serverOpts.EnableFullDuplex = true
case "timeouts":
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "read_body":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing read_body timeout duration: %v", err)
}
serverOpts.ReadTimeout = caddy.Duration(dur)
case "read_header":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing read_header timeout duration: %v", err)
}
serverOpts.ReadHeaderTimeout = caddy.Duration(dur)
case "write":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing write timeout duration: %v", err)
}
serverOpts.WriteTimeout = caddy.Duration(dur)
case "idle":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing idle timeout duration: %v", err)
}
serverOpts.IdleTimeout = caddy.Duration(dur)
default:
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
}
}
case "max_header_size":
var sizeStr string
if !d.AllArgs(&sizeStr) {
return nil, d.ArgErr()
}
size, err := humanize.ParseBytes(sizeStr)
if err != nil {
return nil, d.Errf("parsing max_header_size: %v", err)
}
serverOpts.MaxHeaderBytes = int(size)
case "protocol":
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "allow_h2c":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.AllowH2C = true
case "experimental_http3":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.ExperimentalHTTP3 = true
case "strict_sni_host":
if d.NextArg() {
return nil, d.ArgErr()
}
trueBool := true
serverOpts.StrictSNIHost = &trueBool
default:
return nil, d.Errf("unrecognized protocol option '%s'", d.Val())
}
}
default:
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
case "log_credentials":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.ShouldLogCredentials = true
case "protocols":
protos := d.RemainingArgs()
for _, proto := range protos {
if proto != "h1" && proto != "h2" && proto != "h2c" && proto != "h3" {
return nil, d.Errf("unknown protocol '%s': expected h1, h2, h2c, or h3", proto)
}
if sliceContains(serverOpts.Protocols, proto) {
return nil, d.Errf("protocol %s specified more than once", proto)
}
serverOpts.Protocols = append(serverOpts.Protocols, proto)
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
case "strict_sni_host":
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
}
boolVal := true
if d.Val() == "insecure_off" {
boolVal = false
}
serverOpts.StrictSNIHost = &boolVal
case "trusted_proxies":
if !d.NextArg() {
return nil, d.Err("trusted_proxies expects an IP range source module name as its first argument")
}
modID := "http.ip_sources." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
source, ok := unm.(caddyhttp.IPRangeSource)
if !ok {
return nil, fmt.Errorf("module %s (%T) is not an IP range source", modID, unm)
}
jsonSource := caddyconfig.JSONModuleObject(
source,
"source",
source.(caddy.Module).CaddyModule().ID.Name(),
nil,
)
serverOpts.TrustedProxiesRaw = jsonSource
case "trusted_proxies_strict":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.TrustedProxiesStrict = 1
case "client_ip_headers":
headers := d.RemainingArgs()
for _, header := range headers {
if sliceContains(serverOpts.ClientIPHeaders, header) {
return nil, d.Errf("client IP header %s specified more than once", header)
}
serverOpts.ClientIPHeaders = append(serverOpts.ClientIPHeaders, header)
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
case "metrics":
if d.NextArg() {
return nil, d.ArgErr()
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
serverOpts.Metrics = new(caddyhttp.Metrics)
case "trace":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.Trace = true
default:
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
}
}
return serverOpts, nil
@@ -172,26 +263,30 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error
// applyServerOptions sets the server options on the appropriate servers
func applyServerOptions(
servers map[string]*caddyhttp.Server,
options map[string]interface{},
warnings *[]caddyconfig.Warning,
options map[string]any,
_ *[]caddyconfig.Warning,
) error {
// If experimental HTTP/3 is enabled, enable it on each server.
// We already know there won't be a conflict with serverOptions because
// we validated earlier that "experimental_http3" cannot be set at the same
// time as "servers"
if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
*warnings = append(*warnings, caddyconfig.Warning{Message: "the 'experimental_http3' global option is deprecated, please use the 'servers > protocol > experimental_http3' option instead"})
for _, srv := range servers {
srv.ExperimentalHTTP3 = true
}
}
serverOpts, ok := options["servers"].([]serverOptions)
if !ok {
return nil
}
for _, server := range servers {
// check for duplicate names, which would clobber the config
existingNames := map[string]bool{}
for _, opts := range serverOpts {
if opts.Name == "" {
continue
}
if existingNames[opts.Name] {
return fmt.Errorf("cannot use duplicate server name '%s'", opts.Name)
}
existingNames[opts.Name] = true
}
// collect the server name overrides
nameReplacements := map[string]string{}
for key, server := range servers {
// find the options that apply to this server
opts := func() *serverOptions {
for _, entry := range serverOpts {
@@ -218,10 +313,38 @@ func applyServerOptions(
server.ReadHeaderTimeout = opts.ReadHeaderTimeout
server.WriteTimeout = opts.WriteTimeout
server.IdleTimeout = opts.IdleTimeout
server.KeepAliveInterval = opts.KeepAliveInterval
server.MaxHeaderBytes = opts.MaxHeaderBytes
server.AllowH2C = opts.AllowH2C
server.ExperimentalHTTP3 = opts.ExperimentalHTTP3
server.EnableFullDuplex = opts.EnableFullDuplex
server.Protocols = opts.Protocols
server.StrictSNIHost = opts.StrictSNIHost
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
server.ClientIPHeaders = opts.ClientIPHeaders
server.TrustedProxiesStrict = opts.TrustedProxiesStrict
server.Metrics = opts.Metrics
if opts.ShouldLogCredentials {
if server.Logs == nil {
server.Logs = new(caddyhttp.ServerLogConfig)
}
server.Logs.ShouldLogCredentials = opts.ShouldLogCredentials
}
if opts.Trace {
// TODO: THIS IS EXPERIMENTAL (MAY 2024)
if server.Logs == nil {
server.Logs = new(caddyhttp.ServerLogConfig)
}
server.Logs.Trace = opts.Trace
}
if opts.Name != "" {
nameReplacements[key] = opts.Name
}
}
// rename the servers if marked to do so
for old, new := range nameReplacements {
servers[new] = servers[old]
delete(servers, old)
}
return nil
+94
View File
@@ -0,0 +1,94 @@
package httpcaddyfile
import (
"regexp"
"strings"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
type ComplexShorthandReplacer struct {
search *regexp.Regexp
replace string
}
type ShorthandReplacer struct {
complex []ComplexShorthandReplacer
simple *strings.Replacer
}
func NewShorthandReplacer() ShorthandReplacer {
// replace shorthand placeholders (which are convenient
// when writing a Caddyfile) with their actual placeholder
// identifiers or variable names
replacer := strings.NewReplacer(placeholderShorthands()...)
// these are placeholders that allow a user-defined final
// parameters, but we still want to provide a shorthand
// for those, so we use a regexp to replace
regexpReplacements := []ComplexShorthandReplacer{
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
{regexp.MustCompile(`{cookie\.([\w-]*)}`), "{http.request.cookie.$1}"},
{regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"},
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
{regexp.MustCompile(`{file\.([\w-]*)}`), "{http.request.uri.path.file.$1}"},
{regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"},
{regexp.MustCompile(`{re\.([\w-\.]*)}`), "{http.regexp.$1}"},
{regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
{regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
{regexp.MustCompile(`{resp\.([\w-\.]*)}`), "{http.intercept.$1}"},
{regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"},
{regexp.MustCompile(`{file_match\.([\w-]*)}`), "{http.matchers.file.$1}"},
}
return ShorthandReplacer{
complex: regexpReplacements,
simple: replacer,
}
}
// 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}",
"{uuid}", "{http.request.uuid}",
"{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}",
"{client_ip}", "{http.vars.client_ip}",
}
}
// ApplyToSegment replaces shorthand placeholder to its full placeholder, understandable by Caddy.
func (s ShorthandReplacer) ApplyToSegment(segment *caddyfile.Segment) {
if segment != nil {
for i := 0; i < len(*segment); i++ {
// simple string replacements
(*segment)[i].Text = s.simple.Replace((*segment)[i].Text)
// complex regexp replacements
for _, r := range s.complex {
(*segment)[i].Text = r.search.ReplaceAllString((*segment)[i].Text, r.replace)
}
}
}
}
@@ -0,0 +1,9 @@
(t2) {
respond 200 {
body {args[:]}
}
}
:8082 {
import t2 false
}
@@ -0,0 +1,9 @@
(t1) {
respond 200 {
body {args[:]}
}
}
:8081 {
import t1 false
}
@@ -0,0 +1,15 @@
(t1) {
respond 200 {
body {args[:]}
}
}
:8081 {
import t1 false
}
import import_variadic.txt
:8083 {
import t2 true
}
+169 -79
View File
@@ -23,20 +23,20 @@ import (
"strconv"
"strings"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/v2/acme"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
)
func (st ServerType) buildTLSApp(
pairings []sbAddrAssociation,
options map[string]interface{},
options map[string]any,
warnings []caddyconfig.Warning,
) (*caddytls.TLS, []caddyconfig.Warning, error) {
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
var certLoaders []caddytls.CertificateLoader
@@ -44,37 +44,32 @@ func (st ServerType) buildTLSApp(
if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp)
}
httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
if hsp, ok := options["https_port"].(int); ok {
httpsPort = strconv.Itoa(hsp)
autoHTTPS := "on"
if ah, ok := options["auto_https"].(string); ok {
autoHTTPS = ah
}
// count how many server blocks have a TLS-enabled key with
// no host, and find all hosts that share a server block with
// a hostless key, so that they don't get forgotten/omitted
// by auto-HTTPS (since they won't appear in route matchers)
var serverBlocksWithTLSHostlessKey int
// find all hosts that share a server block with a hostless
// key, so that they don't get forgotten/omitted by auto-HTTPS
// (since they won't appear in route matchers)
httpsHostsSharedWithHostlessKey := make(map[string]struct{})
for _, pair := range pairings {
for _, sb := range pair.serverBlocks {
for _, addr := range sb.keys {
if addr.Host == "" {
// this address has no hostname, but if it's explicitly set
// to HTTPS, then we need to count it as being TLS-enabled
if addr.Scheme == "https" || addr.Port == httpsPort {
serverBlocksWithTLSHostlessKey++
}
// this server block has a hostless key, now
// go through and add all the hosts to the set
for _, otherAddr := range sb.keys {
if otherAddr.Original == addr.Original {
continue
}
if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
if autoHTTPS != "off" {
for _, pair := range pairings {
for _, sb := range pair.serverBlocks {
for _, addr := range sb.keys {
if addr.Host == "" {
// this server block has a hostless key, now
// go through and add all the hosts to the set
for _, otherAddr := range sb.keys {
if otherAddr.Original == addr.Original {
continue
}
if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
}
}
break
}
break
}
}
}
@@ -101,6 +96,12 @@ func (st ServerType) buildTLSApp(
}
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
ap, err := newBaseAutomationPolicy(options, warnings, true)
if err != nil {
@@ -117,6 +118,11 @@ func (st ServerType) buildTLSApp(
ap.OnDemand = true
}
// reuse private keys tls
if _, ok := sblock.pile["tls.reuse_private_keys"]; ok {
ap.ReusePrivateKeys = true
}
if keyTypeVals, ok := sblock.pile["tls.key_type"]; ok {
ap.KeyType = keyTypeVals[0].Value.(string)
}
@@ -128,11 +134,31 @@ func (st ServerType) buildTLSApp(
issuers = append(issuers, issuerVal.Value.(certmagic.Issuer))
}
if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) {
// this more correctly implements an error check that was removed
// below; try it with this config:
//
// :443 {
// bind 127.0.0.1
// }
//
// :443 {
// bind ::1
// tls {
// issuer acme
// }
// }
return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers)
}
ap.Issuers = issuers
}
// 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
for _, cfgVal := range sblock.pile["bind"] {
for _, iss := range ap.Issuers {
@@ -163,34 +189,30 @@ func (st ServerType) buildTLSApp(
}
}
// first make sure this block is allowed to create an automation policy;
// doing so is forbidden if it has a key with no host (i.e. ":443")
// we used to ensure this block is allowed to create an automation policy;
// doing so was forbidden if it has a key with no host (i.e. ":443")
// and if there is a different server block that also has a key with no
// host -- since a key with no host matches any host, we need its
// associated automation policy to have an empty Subjects list, i.e. no
// host filter, which is indistinguishable between the two server blocks
// because automation is not done in the context of a particular server...
// this is an example of a poor mapping from Caddyfile to JSON but that's
// the least-leaky abstraction I could figure out
if len(sblockHosts) == 0 {
if serverBlocksWithTLSHostlessKey > 1 {
// this server block and at least one other has a key with no host,
// making the two indistinguishable; it is misleading to define such
// a policy within one server block since it actually will apply to
// others as well
return nil, warnings, fmt.Errorf("cannot make a TLS automation policy from a server block that has a host-less address when there are other TLS-enabled server block addresses lacking a host")
}
if catchAllAP == nil {
// this server block has a key with no hosts, but there is not yet
// a catch-all automation policy (probably because no global options
// were set), so this one becomes it
catchAllAP = ap
}
// the least-leaky abstraction I could figure out -- however, this check
// was preventing certain listeners, like those provided by plugins, from
// being used as desired (see the Tailscale listener plugin), so I removed
// the check: and I think since I originally wrote the check I added a new
// check above which *properly* detects this ambiguity without breaking the
// listener plugin; see the check above with a commented example config
if len(sblockHosts) == 0 && catchAllAP == nil {
// this server block has a key with no hosts, but there is not yet
// a catch-all automation policy (probably because no global options
// were set), so this one becomes it
catchAllAP = ap
}
// associate our new automation policy with this server block's hosts
ap.Subjects = sblock.hostsFromKeysNotHTTP(httpPort)
sort.Strings(ap.Subjects) // solely for deterministic test results
ap.SubjectsRaw = sblock.hostsFromKeysNotHTTP(httpPort)
sort.Strings(ap.SubjectsRaw) // solely for deterministic test results
// if a combination of public and internal names were given
// for this same server block and no issuer was specified, we
@@ -200,7 +222,11 @@ func (st ServerType) buildTLSApp(
var ap2 *caddytls.AutomationPolicy
if len(ap.Issuers) == 0 {
var internal, external []string
for _, s := range ap.Subjects {
for _, s := range ap.SubjectsRaw {
// do not create Issuers for Tailscale domains; they will be given a Manager instead
if isTailscaleDomain(s) {
continue
}
if !certmagic.SubjectQualifiesForCert(s) {
return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s)
}
@@ -218,10 +244,10 @@ func (st ServerType) buildTLSApp(
}
}
if len(external) > 0 && len(internal) > 0 {
ap.Subjects = external
ap.SubjectsRaw = external
apCopy := *ap
ap2 = &apCopy
ap2.Subjects = internal
ap2.SubjectsRaw = internal
ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
}
}
@@ -286,6 +312,27 @@ func (st ServerType) buildTLSApp(
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 the OCSP check interval if configured
if ocspCheckInterval, ok := options["ocsp_interval"].(caddy.Duration); ok {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.OCSPCheckInterval = ocspCheckInterval
}
// set whether OCSP stapling should be disabled for manually-managed certificates
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
// no host, they will not be used with route matchers because the
// hostless key matches all hosts, therefore, it wouldn't be
@@ -297,16 +344,18 @@ func (st ServerType) buildTLSApp(
internalAP := &caddytls.AutomationPolicy{
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
}
for h := range httpsHostsSharedWithHostlessKey {
al = append(al, h)
if !certmagic.SubjectQualifiesForPublicCert(h) {
internalAP.Subjects = append(internalAP.Subjects, h)
if autoHTTPS != "off" && autoHTTPS != "disable_certs" {
for h := range httpsHostsSharedWithHostlessKey {
al = append(al, h)
if !certmagic.SubjectQualifiesForPublicCert(h) {
internalAP.SubjectsRaw = append(internalAP.SubjectsRaw, h)
}
}
}
if len(al) > 0 {
tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
}
if len(internalAP.Subjects) > 0 {
if len(internalAP.SubjectsRaw) > 0 {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
@@ -324,21 +373,17 @@ func (st ServerType) buildTLSApp(
globalPreferredChains := options["preferred_chains"]
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil
if hasGlobalACMEDefaults {
// for _, ap := range tlsApp.Automation.Policies {
for i := 0; i < len(tlsApp.Automation.Policies); i++ {
ap := tlsApp.Automation.Policies[i]
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
// for public names, create default issuers which will later be filled in with configured global defaults
// (internal names will implicitly use the internal issuer at auto-https time)
ap.Issuers = caddytls.DefaultIssuers()
emailStr, _ := globalEmail.(string)
ap.Issuers = caddytls.DefaultIssuers(emailStr)
// if a specific endpoint is configured, can't use multiple default issuers
if globalACMECA != nil {
if strings.Contains(globalACMECA.(string), "zerossl") {
ap.Issuers = []certmagic.Issuer{&caddytls.ZeroSSLIssuer{ACMEIssuer: new(caddytls.ACMEIssuer)}}
} else {
ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
}
ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
}
}
}
@@ -373,7 +418,7 @@ func (st ServerType) buildTLSApp(
// for convenience)
automationHostSet := make(map[string]struct{})
for _, ap := range tlsApp.Automation.Policies {
for _, s := range ap.Subjects {
for _, s := range ap.SubjectsRaw {
if _, ok := automationHostSet[s]; ok {
return nil, warnings, fmt.Errorf("hostname appears in more than one automation policy, making certificate management ambiguous: %s", s)
}
@@ -395,7 +440,7 @@ func (st ServerType) buildTLSApp(
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interface{}) error {
func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) error {
acmeWrapper, ok := issuer.(acmeCapable)
if !ok {
return nil
@@ -411,6 +456,8 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf
globalACMEDNS := options["acme_dns"]
globalACMEEAB := options["acme_eab"]
globalPreferredChains := options["preferred_chains"]
globalCertLifetime := options["cert_lifetime"]
globalHTTPPort, globalHTTPSPort := options["http_port"], options["https_port"]
if globalEmail != nil && acmeIssuer.Email == "" {
acmeIssuer.Email = globalEmail.(string)
@@ -434,6 +481,27 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf
if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil {
acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference)
}
if globalHTTPPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.HTTP == nil || acmeIssuer.Challenges.HTTP.AlternatePort == 0) {
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.HTTP == nil {
acmeIssuer.Challenges.HTTP = new(caddytls.HTTPChallengeConfig)
}
acmeIssuer.Challenges.HTTP.AlternatePort = globalHTTPPort.(int)
}
if globalHTTPSPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.TLSALPN == nil || acmeIssuer.Challenges.TLSALPN.AlternatePort == 0) {
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.TLSALPN == nil {
acmeIssuer.Challenges.TLSALPN = new(caddytls.TLSALPNChallengeConfig)
}
acmeIssuer.Challenges.TLSALPN.AlternatePort = globalHTTPSPort.(int)
}
if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
}
return nil
}
@@ -442,7 +510,11 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf
// for any other automation policies. A nil policy (and no error) will be
// returned if there are no default/global options. However, if always is
// true, a non-nil value will always be returned (unless there is an error).
func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
func newBaseAutomationPolicy(
options map[string]any,
_ []caddyconfig.Warning,
always bool,
) (*caddytls.AutomationPolicy, error) {
issuers, hasIssuers := options["cert_issuer"]
_, hasLocalCerts := options["local_certs"]
keyType, hasKeyType := options["key_type"]
@@ -494,7 +566,7 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
if automationPolicyIsSubset(aps[j], aps[i]) {
return false
}
return len(aps[i].Subjects) > len(aps[j].Subjects)
return len(aps[i].SubjectsRaw) > len(aps[j].SubjectsRaw)
})
emptyAPCount := 0
@@ -502,7 +574,7 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
// compute the number of empty policies (disregarding subjects) - see #4128
emptyAP := new(caddytls.AutomationPolicy)
for i := 0; i < len(aps); i++ {
emptyAP.Subjects = aps[i].Subjects
emptyAP.SubjectsRaw = aps[i].SubjectsRaw
if reflect.DeepEqual(aps[i], emptyAP) {
emptyAPCount++
if !automationPolicyHasAllPublicNames(aps[i]) {
@@ -539,12 +611,14 @@ outer:
// eaten up by the one with subjects; and if both have subjects, we
// need to combine their lists
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) &&
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
aps[i].MustStaple == aps[j].MustStaple &&
aps[i].KeyType == aps[j].KeyType &&
aps[i].OnDemand == aps[j].OnDemand &&
aps[i].ReusePrivateKeys == aps[j].ReusePrivateKeys &&
aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio {
if len(aps[i].Subjects) > 0 && len(aps[j].Subjects) == 0 {
if len(aps[i].SubjectsRaw) > 0 && len(aps[j].SubjectsRaw) == 0 {
// later policy (at j) has no subjects ("catch-all"), so we can
// remove the identical-but-more-specific policy that comes first
// AS LONG AS it is not shadowed by another policy before it; e.g.
@@ -559,9 +633,9 @@ outer:
}
} else {
// avoid repeated subjects
for _, subj := range aps[j].Subjects {
if !sliceContains(aps[i].Subjects, subj) {
aps[i].Subjects = append(aps[i].Subjects, subj)
for _, subj := range aps[j].SubjectsRaw {
if !sliceContains(aps[i].SubjectsRaw, subj) {
aps[i].SubjectsRaw = append(aps[i].SubjectsRaw, subj)
}
}
aps = append(aps[:j], aps[j+1:]...)
@@ -577,15 +651,15 @@ outer:
// automationPolicyIsSubset returns true if a's subjects are a subset
// of b's subjects.
func automationPolicyIsSubset(a, b *caddytls.AutomationPolicy) bool {
if len(b.Subjects) == 0 {
if len(b.SubjectsRaw) == 0 {
return true
}
if len(a.Subjects) == 0 {
if len(a.SubjectsRaw) == 0 {
return false
}
for _, aSubj := range a.Subjects {
for _, aSubj := range a.SubjectsRaw {
var inSuperset bool
for _, bSubj := range b.Subjects {
for _, bSubj := range b.SubjectsRaw {
if certmagic.MatchWildcard(aSubj, bSubj) {
inSuperset = true
break
@@ -616,17 +690,33 @@ func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int {
// subjectQualifiesForPublicCert is like certmagic.SubjectQualifiesForPublicCert() except
// that this allows domains with multiple wildcard levels like '*.*.example.com' to qualify
// if the automation policy has OnDemand enabled (i.e. this function is more lenient).
//
// IP subjects are considered as non-qualifying for public certs. Technically, there are
// now public ACME CAs as well as non-ACME CAs that issue IP certificates. But this function
// is used solely for implicit automation (defaults), where it gets really complicated to
// keep track of which issuers support IP certificates in which circumstances. Currently,
// issuers that support IP certificates are very few, and all require some sort of config
// from the user anyway (such as an account credential). Since we cannot implicitly and
// automatically get public IP certs without configuration from the user, we treat IPs as
// not qualifying for public certificates. Users should expressly configure an issuer
// that supports IP certs for that purpose.
func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) bool {
return !certmagic.SubjectIsIP(subj) &&
!certmagic.SubjectIsInternal(subj) &&
(strings.Count(subj, "*.") < 2 || ap.OnDemand)
}
// automationPolicyHasAllPublicNames returns true if all the names on the policy
// do NOT qualify for public certs OR are tailscale domains.
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
for _, subj := range ap.Subjects {
if !subjectQualifiesForPublicCert(ap, subj) {
for _, subj := range ap.SubjectsRaw {
if !subjectQualifiesForPublicCert(ap, subj) || isTailscaleDomain(subj) {
return false
}
}
return true
}
func isTailscaleDomain(name string) bool {
return strings.HasSuffix(strings.ToLower(name), ".ts.net")
}
+2 -2
View File
@@ -47,8 +47,8 @@ func TestAutomationPolicyIsSubset(t *testing.T) {
expect: false,
},
} {
apA := &caddytls.AutomationPolicy{Subjects: test.a}
apB := &caddytls.AutomationPolicy{Subjects: test.b}
apA := &caddytls.AutomationPolicy{SubjectsRaw: test.a}
apB := &caddytls.AutomationPolicy{SubjectsRaw: test.b}
if actual := automationPolicyIsSubset(apA, apB); actual != test.expect {
t.Errorf("Test %d: Expected %t but got %t (A: %v B: %v)", i, test.expect, actual, test.a, test.b)
}
+87 -20
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
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"io"
"net/http"
"os"
"time"
"github.com/caddyserver/caddy/v2"
@@ -15,8 +30,14 @@ func init() {
caddy.RegisterModule(HTTPLoader{})
}
// HTTPLoader can load Caddy configs over HTTP(S). It can adapt the config
// based on the Content-Type header of the HTTP response.
// HTTPLoader can load Caddy configs over HTTP(S).
//
// If the response is not a JSON config, a config adapter must be specified
// either in the loader config (`adapter`), or in the Content-Type HTTP header
// returned in the HTTP response from the server. The Content-Type header is
// read just like the admin API's `/load` endpoint. Uf you don't have control
// over the HTTP server (but can still trust its response), you can override
// the Content-Type header by setting the `adapter` property in this config.
type HTTPLoader struct {
// The method for the request. Default: GET
Method string `json:"method,omitempty"`
@@ -30,6 +51,11 @@ type HTTPLoader struct {
// Maximum time allowed for a complete connection and request.
Timeout caddy.Duration `json:"timeout,omitempty"`
// The name of the config adapter to use, if any. Only needed
// if the HTTP response is not a JSON config and if the server's
// Content-Type header is missing or incorrect.
Adapter string `json:"adapter,omitempty"`
TLS *struct {
// Present this instance's managed remote identity credentials to the server.
UseServerIdentity bool `json:"use_server_identity,omitempty"`
@@ -56,23 +82,30 @@ func (HTTPLoader) CaddyModule() caddy.ModuleInfo {
// LoadConfig loads a Caddy config.
func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
repl := caddy.NewReplacer()
client, err := hl.makeClient(ctx)
if err != nil {
return nil, err
}
method := hl.Method
method := repl.ReplaceAll(hl.Method, "")
if method == "" {
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 {
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 := doHttpCallWithRetries(ctx, client, req)
if err != nil {
return nil, err
}
@@ -81,22 +114,59 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
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 {
return nil, err
}
result, warnings, err := adaptByContentType(resp.Header.Get("Content-Type"), body)
// adapt the config based on either manually-configured adapter or server's response header
ct := resp.Header.Get("Content-Type")
if hl.Adapter != "" {
ct = "text/" + hl.Adapter
}
result, warnings, err := adaptByContentType(ct, body)
if err != nil {
return nil, err
}
for _, warn := range warnings {
ctx.Logger(hl).Warn(warn.String())
ctx.Logger().Warn(warn.String())
}
return result, nil
}
func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response, error) {
resp, err := client.Do(request)
if err != nil {
return nil, fmt.Errorf("problem calling http loader url: %v", err)
} else if resp.StatusCode < 200 || resp.StatusCode > 499 {
resp.Body.Close()
return nil, fmt.Errorf("bad response status code from http loader url: %v", resp.StatusCode)
}
return resp, nil
}
func doHttpCallWithRetries(ctx caddy.Context, client *http.Client, request *http.Request) (*http.Response, error) {
var resp *http.Response
var err error
const maxAttempts = 10
for i := 0; i < maxAttempts; i++ {
resp, err = attemptHttpCall(client, request)
if err != nil && i < maxAttempts-1 {
select {
case <-time.After(time.Millisecond * 500):
case <-ctx.Done():
return resp, ctx.Err()
}
} else {
break
}
}
return resp, err
}
func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
client := &http.Client{
Timeout: time.Duration(hl.Timeout),
@@ -107,30 +177,27 @@ func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
// client authentication
if hl.TLS.UseServerIdentity {
certs, err := ctx.IdentityCredentials(ctx.Logger(hl))
certs, err := ctx.IdentityCredentials(ctx.Logger())
if err != nil {
return nil, fmt.Errorf("getting server identity credentials: %v", err)
}
if tlsConfig == nil {
tlsConfig = new(tls.Config)
}
tlsConfig.Certificates = certs
// See https://github.com/securego/gosec/issues/1054#issuecomment-2072235199
//nolint:gosec
tlsConfig = &tls.Config{Certificates: certs}
} else if hl.TLS.ClientCertificateFile != "" && hl.TLS.ClientCertificateKeyFile != "" {
cert, err := tls.LoadX509KeyPair(hl.TLS.ClientCertificateFile, hl.TLS.ClientCertificateKeyFile)
if err != nil {
return nil, err
}
if tlsConfig == nil {
tlsConfig = new(tls.Config)
}
tlsConfig.Certificates = []tls.Certificate{cert}
//nolint:gosec
tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
}
// trusted server certs
if len(hl.TLS.RootCAPEMFiles) > 0 {
rootPool := x509.NewCertPool()
for _, pemFile := range hl.TLS.RootCAPEMFiles {
pemData, err := ioutil.ReadFile(pemFile)
pemData, err := os.ReadFile(pemFile)
if err != nil {
return nil, fmt.Errorf("failed reading ca cert: %v", err)
}
+49 -5
View File
@@ -58,6 +58,10 @@ func (al adminLoad) Routes() []caddy.AdminRoute {
Pattern: "/load",
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
}
// 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.
func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, error) {
// assume JSON as the default
@@ -144,12 +189,11 @@ func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, err
}
// adapter name should be suffix of MIME type
slashIdx := strings.Index(ct, "/")
if slashIdx < 0 {
_, adapterName, slashFound := strings.Cut(ct, "/")
if !slashFound {
return nil, nil, fmt.Errorf("malformed Content-Type")
}
adapterName := ct[slashIdx+1:]
cfgAdapter := GetAdapter(adapterName)
if cfgAdapter == nil {
return nil, nil, fmt.Errorf("unrecognized config adapter '%s'", adapterName)
@@ -164,7 +208,7 @@ func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, err
}
var bufPool = sync.Pool{
New: func() interface{} {
New: func() any {
return new(bytes.Buffer)
},
}
+62 -46
View File
@@ -7,7 +7,8 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"io"
"io/fs"
"log"
"net"
"net/http"
@@ -22,9 +23,10 @@ import (
"time"
"github.com/aryann/difflib"
"github.com/caddyserver/caddy/v2/caddyconfig"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/caddyserver/caddy/v2/caddyconfig"
// plug in Caddy modules here
_ "github.com/caddyserver/caddy/v2/modules/standard"
)
@@ -34,7 +36,7 @@ type Defaults struct {
// Port we expect caddy to listening on
AdminPort int
// Certificates we expect to be loaded before attempting to run the tests
Certifcates []string
Certificates []string
// TestRequestTimeout is the time to wait for a http request to
TestRequestTimeout time.Duration
// LoadRequestTimeout is the time to wait for the config to be loaded against the caddy server
@@ -43,8 +45,8 @@ type Defaults struct {
// Default testing values
var Default = Defaults{
AdminPort: 2019,
Certifcates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
AdminPort: 2999, // different from what a real server also running on a developer's machine might be
Certificates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
TestRequestTimeout: 5 * time.Second,
LoadRequestTimeout: 5 * time.Second,
}
@@ -58,12 +60,11 @@ var (
type Tester struct {
Client *http.Client
configLoaded bool
t *testing.T
t testing.TB
}
// NewTester will create a new testing client with an attached cookie jar
func NewTester(t *testing.T) *Tester {
func NewTester(t testing.TB) *Tester {
jar, err := cookiejar.New(nil)
if err != nil {
t.Fatalf("failed to create cookiejar: %s", err)
@@ -94,13 +95,12 @@ func timeElapsed(start time.Time, name string) {
// InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type.
func (tc *Tester) InitServer(rawConfig string, configType string) {
if err := tc.initServer(rawConfig, configType); err != nil {
tc.t.Logf("failed to load config: %s", err)
tc.t.Fail()
}
if err := tc.ensureConfigRunning(rawConfig, configType); err != nil {
tc.t.Logf("failed ensurng config is running: %s", err)
tc.t.Logf("failed ensuring config is running: %s", err)
tc.t.Fail()
}
}
@@ -108,13 +108,12 @@ func (tc *Tester) InitServer(rawConfig string, configType string) {
// InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type.
func (tc *Tester) initServer(rawConfig string, configType string) error {
if testing.Short() {
tc.t.SkipNow()
return nil
}
err := validateTestPrerequisites()
err := validateTestPrerequisites(tc.t)
if err != nil {
tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err)
return nil
@@ -122,14 +121,13 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
tc.t.Cleanup(func() {
if tc.t.Failed() && tc.configLoaded {
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
tc.t.Log("unable to read the current config")
return
}
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
body, _ := io.ReadAll(res.Body)
var out bytes.Buffer
_ = json.Indent(&out, body, "", " ")
@@ -138,6 +136,20 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
})
rawConfig = prependCaddyFilePath(rawConfig)
// normalize JSON config
if configType == "json" {
tc.t.Logf("Before: %s", rawConfig)
var conf any
if err := json.Unmarshal([]byte(rawConfig), &conf); err != nil {
return err
}
c, err := json.Marshal(conf)
if err != nil {
return err
}
rawConfig = string(c)
tc.t.Logf("After: %s", rawConfig)
}
client := &http.Client{
Timeout: Default.LoadRequestTimeout,
}
@@ -162,7 +174,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
timeElapsed(start, "caddytest: config load time")
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
body, err := io.ReadAll(res.Body)
if err != nil {
tc.t.Errorf("unable to read response. %s", err)
return err
@@ -186,7 +198,7 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil)
}
var expected interface{}
var expected any
err := json.Unmarshal(expectedBytes, &expected)
if err != nil {
return err
@@ -196,17 +208,17 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
Timeout: Default.LoadRequestTimeout,
}
fetchConfig := func(client *http.Client) interface{} {
fetchConfig := func(client *http.Client) any {
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
return nil
}
defer resp.Body.Close()
actualBytes, err := ioutil.ReadAll(resp.Body)
actualBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil
}
var actual interface{}
var actual any
err = json.Unmarshal(actualBytes, &actual)
if err != nil {
return nil
@@ -214,37 +226,53 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
return actual
}
for retries := 4; retries > 0; retries-- {
for retries := 10; retries > 0; retries-- {
if reflect.DeepEqual(expected, fetchConfig(client)) {
return nil
}
time.Sleep(10 * time.Millisecond)
time.Sleep(1 * time.Second)
}
tc.t.Errorf("POSTed configuration isn't active")
return errors.New("EnsureConfigRunning: POSTed configuration isn't active")
}
const initConfig = `{
admin localhost:2999
}
`
// validateTestPrerequisites ensures the certificates are available in the
// designated path and Caddy sub-process is running.
func validateTestPrerequisites() error {
func validateTestPrerequisites(t testing.TB) error {
// check certificates are found
for _, certName := range Default.Certifcates {
if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) {
for _, certName := range Default.Certificates {
if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("caddy integration test certificates (%s) not found", certName)
}
}
if isCaddyAdminRunning() != nil {
// setup the init config file, and set the cleanup afterwards
f, err := os.CreateTemp("", "")
if err != nil {
return err
}
t.Cleanup(func() {
os.Remove(f.Name())
})
if _, err := f.WriteString(initConfig); err != nil {
return err
}
// start inprocess caddy server
os.Args = []string{"caddy", "run"}
os.Args = []string{"caddy", "run", "--config", f.Name(), "--adapter", "caddyfile"}
go func() {
caddycmd.Main()
}()
// wait for caddy to start serving the initial config
for retries := 4; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
time.Sleep(10 * time.Millisecond)
for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
time.Sleep(1 * time.Second)
}
}
@@ -267,7 +295,6 @@ func isCaddyAdminRunning() error {
}
func getIntegrationDir() string {
_, filename, _, ok := runtime.Caller(1)
if !ok {
panic("unable to determine the current file path")
@@ -287,7 +314,6 @@ func prependCaddyFilePath(rawConfig string) string {
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
func CreateTestingTransport() *http.Transport {
dialer := net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 5 * time.Second,
@@ -315,7 +341,6 @@ func CreateTestingTransport() *http.Transport {
// AssertLoadError will load a config and expect an error
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
tc := NewTester(t)
err := tc.initServer(rawConfig, configType)
@@ -326,7 +351,6 @@ func AssertLoadError(t *testing.T, rawConfig string, configType string, expected
// AssertRedirect makes a request and asserts the redirection happens
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
@@ -363,15 +387,14 @@ func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, e
}
// CompareAdapt adapts a config and then compares it against an expected result
func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string, expectedResponse string) bool {
func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool {
cfgAdapter := caddyconfig.GetAdapter(adapterName)
if cfgAdapter == nil {
t.Logf("unrecognized config adapter '%s'", adapterName)
return false
}
options := make(map[string]interface{})
options := make(map[string]any)
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
if err != nil {
@@ -423,7 +446,7 @@ func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string,
}
// AssertAdapt adapts a config and then tests it against an expected result
func AssertAdapt(t *testing.T, rawConfig string, adapterName string, expectedResponse string) {
func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) {
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
if !ok {
t.Fail()
@@ -432,7 +455,7 @@ func AssertAdapt(t *testing.T, rawConfig string, adapterName string, expectedRes
// Generic request functions
func applyHeaders(t *testing.T, req *http.Request, requestHeaders []string) {
func applyHeaders(t testing.TB, req *http.Request, requestHeaders []string) {
requestContentType := ""
for _, requestHeader := range requestHeaders {
arr := strings.SplitAfterN(requestHeader, ":", 2)
@@ -452,14 +475,13 @@ func applyHeaders(t *testing.T, req *http.Request, requestHeaders []string) {
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
resp, err := tc.Client.Do(req)
if err != nil {
tc.t.Fatalf("failed to call server %s", err)
}
if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.RequestURI, expectedStatusCode, resp.StatusCode)
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.URL.RequestURI(), expectedStatusCode, resp.StatusCode)
}
return resp
@@ -467,11 +489,10 @@ func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int)
// AssertResponse request a URI and assert the status code and the body contains a string
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
resp := tc.AssertResponseCode(req, expectedStatusCode)
defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body)
bytes, err := io.ReadAll(resp.Body)
if err != nil {
tc.t.Fatalf("unable to read the response body %s", err)
}
@@ -489,7 +510,6 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
// AssertGetResponse GET a URI and expect a statusCode and body text
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("GET", requestURI, nil)
if err != nil {
tc.t.Fatalf("unable to create request %s", err)
@@ -500,7 +520,6 @@ func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, e
// AssertDeleteResponse request a URI and expect a statusCode and body text
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("DELETE", requestURI, nil)
if err != nil {
tc.t.Fatalf("unable to create request %s", err)
@@ -511,7 +530,6 @@ func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int
// AssertPostResponseBody POST to a URI and assert the response code and body
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("POST", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
@@ -525,7 +543,6 @@ func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []str
// AssertPutResponseBody PUT to a URI and assert the response code and body
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PUT", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
@@ -539,7 +556,6 @@ func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []stri
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PATCH", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
+96
View File
@@ -1,6 +1,7 @@
package caddytest
import (
"net/http"
"strings"
"testing"
)
@@ -31,3 +32,98 @@ func TestReplaceCertificatePaths(t *testing.T) {
t.Error("expected redirect uri to be unchanged")
}
}
func TestLoadUnorderedJSON(t *testing.T) {
tester := NewTester(t)
tester.InitServer(`
{
"logging": {
"logs": {
"default": {
"level": "DEBUG",
"writer": {
"output": "stdout"
}
},
"sStdOutLogs": {
"level": "DEBUG",
"writer": {
"output": "stdout"
},
"include": [
"http.*",
"admin.*"
]
},
"sFileLogs": {
"level": "DEBUG",
"writer": {
"output": "stdout"
},
"include": [
"http.*",
"admin.*"
]
}
}
},
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
},
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"s_server": {
"listen": [
":9443",
":9080"
],
"routes": [
{
"handle": [
{
"handler": "static_response",
"body": "Hello"
}
]
},
{
"match": [
{
"host": [
"localhost",
"127.0.0.1"
]
}
]
}
],
"logs": {
"default_logger_name": "sStdOutLogs",
"logger_names": {
"localhost": "sStdOutLogs",
"127.0.0.1": "sFileLogs"
}
}
}
}
}
}
}
`, "json")
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
if err != nil {
t.Fail()
return
}
tester.AssertResponseCode(req, 200)
}
+206
View File
@@ -0,0 +1,206 @@
package integration
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"net"
"net/http"
"strings"
"testing"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddytest"
"github.com/mholt/acmez/v2"
"github.com/mholt/acmez/v2/acme"
smallstepacme "github.com/smallstep/certificates/acme"
"go.uber.org/zap"
)
const acmeChallengePort = 9081
// Test the basic functionality of Caddy's ACME server
func TestACMEServerWithDefaults(t *testing.T) {
ctx := context.Background()
logger, err := zap.NewDevelopment()
if err != nil {
t.Error(err)
return
}
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
local_certs
}
acme.localhost {
acme_server
}
`, "caddyfile")
client := acmez.Client{
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
},
}
accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Errorf("generating account key: %v", err)
}
account := acme.Account{
Contact: []string{"mailto:you@example.com"},
TermsOfServiceAgreed: true,
PrivateKey: accountPrivateKey,
}
account, err = client.NewAccount(ctx, account)
if err != nil {
t.Errorf("new account: %v", err)
return
}
// Every certificate needs a key.
certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Errorf("generating certificate key: %v", err)
return
}
certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"})
if err != nil {
t.Errorf("obtaining certificate: %v", err)
return
}
// ACME servers should usually give you the entire certificate chain
// in PEM format, and sometimes even alternate chains! It's up to you
// which one(s) to store and use, but whatever you do, be sure to
// store the certificate and key somewhere safe and secure, i.e. don't
// lose them!
for _, cert := range certs {
t.Logf("Certificate %q:\n%s\n\n", cert.URL, cert.ChainPEM)
}
}
func TestACMEServerWithMismatchedChallenges(t *testing.T) {
ctx := context.Background()
logger := caddy.Log().Named("acmez")
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
local_certs
}
acme.localhost {
acme_server {
challenges tls-alpn-01
}
}
`, "caddyfile")
client := acmez.Client{
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
},
}
accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Errorf("generating account key: %v", err)
}
account := acme.Account{
Contact: []string{"mailto:you@example.com"},
TermsOfServiceAgreed: true,
PrivateKey: accountPrivateKey,
}
account, err = client.NewAccount(ctx, account)
if err != nil {
t.Errorf("new account: %v", err)
return
}
// Every certificate needs a key.
certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Errorf("generating certificate key: %v", err)
return
}
certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"})
if len(certs) > 0 {
t.Errorf("expected '0' certificates, but received '%d'", len(certs))
}
if err == nil {
t.Error("expected errors, but received none")
}
const expectedErrMsg = "no solvers available for remaining challenges (configured=[http-01] offered=[tls-alpn-01] remaining=[tls-alpn-01])"
if !strings.Contains(err.Error(), expectedErrMsg) {
t.Errorf(`received error message does not match expectation: expected="%s" received="%s"`, expectedErrMsg, err.Error())
}
}
// naiveHTTPSolver is a no-op acmez.Solver for example purposes only.
type naiveHTTPSolver struct {
srv *http.Server
logger *zap.Logger
}
func (s *naiveHTTPSolver) Present(ctx context.Context, challenge acme.Challenge) error {
smallstepacme.InsecurePortHTTP01 = acmeChallengePort
s.srv = &http.Server{
Addr: fmt.Sprintf(":%d", acmeChallengePort),
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
host = r.Host
}
s.logger.Info("received request on challenge server", zap.String("path", r.URL.Path))
if r.Method == "GET" && r.URL.Path == challenge.HTTP01ResourcePath() && strings.EqualFold(host, challenge.Identifier.Value) {
w.Header().Add("Content-Type", "text/plain")
w.Write([]byte(challenge.KeyAuthorization))
r.Close = true
s.logger.Info("served key authentication",
zap.String("identifier", challenge.Identifier.Value),
zap.String("challenge", "http-01"),
zap.String("remote", r.RemoteAddr),
)
}
}),
}
l, err := net.Listen("tcp", fmt.Sprintf(":%d", acmeChallengePort))
if err != nil {
return err
}
s.logger.Info("present challenge", zap.Any("challenge", challenge))
go s.srv.Serve(l)
return nil
}
func (s naiveHTTPSolver) CleanUp(ctx context.Context, challenge acme.Challenge) error {
smallstepacme.InsecurePortHTTP01 = 0
s.logger.Info("cleanup", zap.Any("challenge", challenge))
if s.srv != nil {
s.srv.Close()
}
return nil
}
+204
View File
@@ -0,0 +1,204 @@
package integration
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"strings"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
"github.com/mholt/acmez/v2"
"github.com/mholt/acmez/v2/acme"
"go.uber.org/zap"
)
func TestACMEServerDirectory(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
local_certs
admin localhost:2999
http_port 9080
https_port 9443
pki {
ca local {
name "Caddy Local Authority"
}
}
}
acme.localhost:9443 {
acme_server
}
`, "caddyfile")
tester.AssertGetResponse(
"https://acme.localhost:9443/acme/local/directory",
200,
`{"newNonce":"https://acme.localhost:9443/acme/local/new-nonce","newAccount":"https://acme.localhost:9443/acme/local/new-account","newOrder":"https://acme.localhost:9443/acme/local/new-order","revokeCert":"https://acme.localhost:9443/acme/local/revoke-cert","keyChange":"https://acme.localhost:9443/acme/local/key-change"}
`)
}
func TestACMEServerAllowPolicy(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
local_certs
admin localhost:2999
http_port 9080
https_port 9443
pki {
ca local {
name "Caddy Local Authority"
}
}
}
acme.localhost {
acme_server {
challenges http-01
allow {
domains localhost
}
}
}
`, "caddyfile")
ctx := context.Background()
logger, err := zap.NewDevelopment()
if err != nil {
t.Error(err)
return
}
client := acmez.Client{
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
},
}
accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Errorf("generating account key: %v", err)
}
account := acme.Account{
Contact: []string{"mailto:you@example.com"},
TermsOfServiceAgreed: true,
PrivateKey: accountPrivateKey,
}
account, err = client.NewAccount(ctx, account)
if err != nil {
t.Errorf("new account: %v", err)
return
}
// Every certificate needs a key.
certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Errorf("generating certificate key: %v", err)
return
}
{
certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"})
if err != nil {
t.Errorf("obtaining certificate for allowed domain: %v", err)
return
}
// ACME servers should usually give you the entire certificate chain
// in PEM format, and sometimes even alternate chains! It's up to you
// which one(s) to store and use, but whatever you do, be sure to
// store the certificate and key somewhere safe and secure, i.e. don't
// lose them!
for _, cert := range certs {
t.Logf("Certificate %q:\n%s\n\n", cert.URL, cert.ChainPEM)
}
}
{
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"not-matching.localhost"})
if err == nil {
t.Errorf("obtaining certificate for 'not-matching.localhost' domain")
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
t.Logf("unexpected error: %v", err)
}
}
}
func TestACMEServerDenyPolicy(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
local_certs
admin localhost:2999
http_port 9080
https_port 9443
pki {
ca local {
name "Caddy Local Authority"
}
}
}
acme.localhost {
acme_server {
deny {
domains deny.localhost
}
}
}
`, "caddyfile")
ctx := context.Background()
logger, err := zap.NewDevelopment()
if err != nil {
t.Error(err)
return
}
client := acmez.Client{
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
},
}
accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Errorf("generating account key: %v", err)
}
account := acme.Account{
Contact: []string{"mailto:you@example.com"},
TermsOfServiceAgreed: true,
PrivateKey: accountPrivateKey,
}
account, err = client.NewAccount(ctx, account)
if err != nil {
t.Errorf("new account: %v", err)
return
}
// Every certificate needs a key.
certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Errorf("generating certificate key: %v", err)
return
}
{
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"})
if err == nil {
t.Errorf("obtaining certificate for 'deny.localhost' domain")
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
t.Logf("unexpected error: %v", err)
}
}
}
+21 -1
View File
@@ -11,6 +11,8 @@ func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
admin localhost:2999
skip_install_trust
http_port 9080
https_port 9443
}
@@ -25,6 +27,8 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
}
@@ -39,6 +43,8 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
}
@@ -53,6 +59,9 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
@@ -74,7 +83,14 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
]
}
}
}
},
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
}
}
}
`, "json")
@@ -85,6 +101,8 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
local_certs
@@ -108,6 +126,8 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSit
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
local_certs
@@ -0,0 +1,65 @@
{
pki {
ca custom-ca {
name "Custom CA"
}
}
}
acme.example.com {
acme_server {
ca custom-ca
challenges dns-01
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "custom-ca",
"challenges": [
"dns-01"
],
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"custom-ca": {
"name": "Custom CA"
}
}
}
}
}
@@ -0,0 +1,62 @@
{
pki {
ca custom-ca {
name "Custom CA"
}
}
}
acme.example.com {
acme_server {
ca custom-ca
challenges
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "custom-ca",
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"custom-ca": {
"name": "Custom CA"
}
}
}
}
}
@@ -0,0 +1,108 @@
{
pki {
ca internal {
name "Internal"
root_cn "Internal Root Cert"
intermediate_cn "Internal Intermediate Cert"
}
ca internal-long-lived {
name "Long-lived"
root_cn "Internal Root Cert 2"
intermediate_cn "Internal Intermediate Cert 2"
}
}
}
acme-internal.example.com {
acme_server {
ca internal
}
}
acme-long-lived.example.com {
acme_server {
ca internal-long-lived
lifetime 7d
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme-long-lived.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "internal-long-lived",
"handler": "acme_server",
"lifetime": 604800000000000
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"acme-internal.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "internal",
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"internal": {
"name": "Internal",
"root_common_name": "Internal Root Cert",
"intermediate_common_name": "Internal Intermediate Cert"
},
"internal-long-lived": {
"name": "Long-lived",
"root_common_name": "Internal Root Cert 2",
"intermediate_common_name": "Internal Intermediate Cert 2"
}
}
}
}
}
@@ -0,0 +1,66 @@
{
pki {
ca custom-ca {
name "Custom CA"
}
}
}
acme.example.com {
acme_server {
ca custom-ca
challenges dns-01 http-01
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "custom-ca",
"challenges": [
"dns-01",
"http-01"
],
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"custom-ca": {
"name": "Custom CA"
}
}
}
}
}
@@ -0,0 +1,67 @@
{
pki {
ca internal {
name "Internal"
root_cn "Internal Root Cert"
intermediate_cn "Internal Intermediate Cert"
}
}
}
acme.example.com {
acme_server {
ca internal
sign_with_root
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "internal",
"handler": "acme_server",
"sign_with_root": true
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"internal": {
"name": "Internal",
"root_common_name": "Internal Root Cert",
"intermediate_common_name": "Internal Intermediate Cert"
}
}
}
}
}
@@ -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,37 @@
:8443 {
tls internal {
on_demand
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8443"
],
"tls_connection_policies": [
{}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"issuers": [
{
"module": "internal"
}
],
"on_demand": true
}
]
}
}
}
}
@@ -11,6 +11,7 @@ encode gzip zstd {
header Content-Type application/xhtml+xml*
header Content-Type application/atom+xml*
header Content-Type application/rss+xml*
header Content-Type application/wasm*
header Content-Type image/svg+xml*
}
}
@@ -47,6 +48,7 @@ encode {
"application/xhtml+xml*",
"application/atom+xml*",
"application/rss+xml*",
"application/wasm*",
"image/svg+xml*"
]
},
@@ -0,0 +1,245 @@
foo.localhost {
root * /srv
error /private* "Unauthorized" 410
error /fivehundred* "Internal Server Error" 500
handle_errors 5xx {
respond "Error In range [500 .. 599]"
}
handle_errors 410 {
respond "404 or 410 error"
}
}
bar.localhost {
root * /srv
error /private* "Unauthorized" 410
error /fivehundred* "Internal Server Error" 500
handle_errors 5xx {
respond "Error In range [500 .. 599] from second site"
}
handle_errors 410 {
respond "404 or 410 error from second site"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"foo.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"handle": [
{
"error": "Internal Server Error",
"handler": "error",
"status_code": 500
}
],
"match": [
{
"path": [
"/fivehundred*"
]
}
]
},
{
"handle": [
{
"error": "Unauthorized",
"handler": "error",
"status_code": 410
}
],
"match": [
{
"path": [
"/private*"
]
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"bar.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"handle": [
{
"error": "Internal Server Error",
"handler": "error",
"status_code": 500
}
],
"match": [
{
"path": [
"/fivehundred*"
]
}
]
},
{
"handle": [
{
"error": "Unauthorized",
"handler": "error",
"status_code": 410
}
],
"match": [
{
"path": [
"/private*"
]
}
]
}
]
}
],
"terminal": true
}
],
"errors": {
"routes": [
{
"match": [
{
"host": [
"foo.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "404 or 410 error",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} in [410]"
}
]
},
{
"handle": [
{
"body": "Error In range [500 .. 599]",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} \u003e= 500 \u0026\u0026 {http.error.status_code} \u003c= 599"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"bar.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "404 or 410 error from second site",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} in [410]"
}
]
},
{
"handle": [
{
"body": "Error In range [500 .. 599] from second site",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} \u003e= 500 \u0026\u0026 {http.error.status_code} \u003c= 599"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
}
@@ -0,0 +1,120 @@
{
http_port 3010
}
localhost:3010 {
root * /srv
error /private* "Unauthorized" 410
error /hidden* "Not found" 404
handle_errors 4xx {
respond "Error in the [400 .. 499] range"
}
}
----------
{
"apps": {
"http": {
"http_port": 3010,
"servers": {
"srv0": {
"listen": [
":3010"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"handle": [
{
"error": "Unauthorized",
"handler": "error",
"status_code": 410
}
],
"match": [
{
"path": [
"/private*"
]
}
]
},
{
"handle": [
{
"error": "Not found",
"handler": "error",
"status_code": 404
}
],
"match": [
{
"path": [
"/hidden*"
]
}
]
}
]
}
],
"terminal": true
}
],
"errors": {
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Error in the [400 .. 499] range",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
}
@@ -0,0 +1,153 @@
{
http_port 2099
}
localhost:2099 {
root * /srv
error /private* "Unauthorized" 410
error /threehundred* "Moved Permanently" 301
error /internalerr* "Internal Server Error" 500
handle_errors 500 3xx {
respond "Error code is equal to 500 or in the [300..399] range"
}
handle_errors 4xx {
respond "Error in the [400 .. 499] range"
}
}
----------
{
"apps": {
"http": {
"http_port": 2099,
"servers": {
"srv0": {
"listen": [
":2099"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"handle": [
{
"error": "Moved Permanently",
"handler": "error",
"status_code": 301
}
],
"match": [
{
"path": [
"/threehundred*"
]
}
]
},
{
"handle": [
{
"error": "Internal Server Error",
"handler": "error",
"status_code": 500
}
],
"match": [
{
"path": [
"/internalerr*"
]
}
]
},
{
"handle": [
{
"error": "Unauthorized",
"handler": "error",
"status_code": 410
}
],
"match": [
{
"path": [
"/private*"
]
}
]
}
]
}
],
"terminal": true
}
],
"errors": {
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Error in the [400 .. 499] range",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499"
}
]
},
{
"handle": [
{
"body": "Error code is equal to 500 or in the [300..399] range",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} \u003e= 300 \u0026\u0026 {http.error.status_code} \u003c= 399 || {http.error.status_code} in [500]"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
}
@@ -0,0 +1,120 @@
{
http_port 3010
}
localhost:3010 {
root * /srv
error /private* "Unauthorized" 410
error /hidden* "Not found" 404
handle_errors 404 410 {
respond "404 or 410 error"
}
}
----------
{
"apps": {
"http": {
"http_port": 3010,
"servers": {
"srv0": {
"listen": [
":3010"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"handle": [
{
"error": "Unauthorized",
"handler": "error",
"status_code": 410
}
],
"match": [
{
"path": [
"/private*"
]
}
]
},
{
"handle": [
{
"error": "Not found",
"handler": "error",
"status_code": 404
}
],
"match": [
{
"path": [
"/hidden*"
]
}
]
}
]
}
],
"terminal": true
}
],
"errors": {
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "404 or 410 error",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} in [404, 410]"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
}
@@ -0,0 +1,148 @@
{
http_port 2099
}
localhost:2099 {
root * /srv
error /private* "Unauthorized" 410
error /hidden* "Not found" 404
error /internalerr* "Internal Server Error" 500
handle_errors {
respond "Fallback route: code outside the [400..499] range"
}
handle_errors 4xx {
respond "Error in the [400 .. 499] range"
}
}
----------
{
"apps": {
"http": {
"http_port": 2099,
"servers": {
"srv0": {
"listen": [
":2099"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"handle": [
{
"error": "Internal Server Error",
"handler": "error",
"status_code": 500
}
],
"match": [
{
"path": [
"/internalerr*"
]
}
]
},
{
"handle": [
{
"error": "Unauthorized",
"handler": "error",
"status_code": 410
}
],
"match": [
{
"path": [
"/private*"
]
}
]
},
{
"handle": [
{
"error": "Not found",
"handler": "error",
"status_code": 404
}
],
"match": [
{
"path": [
"/hidden*"
]
}
]
}
]
}
],
"terminal": true
}
],
"errors": {
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Error in the [400 .. 499] range",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499"
}
]
},
{
"handle": [
{
"body": "Fallback route: code outside the [400..499] range",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
}
@@ -0,0 +1,162 @@
(snippet) {
@g `{http.error.status_code} == 404`
}
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
@f `{http.error.status_code} == 404`
abort @f
import snippet
abort @g
----------
{
"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": {
"expr": "{http.error.status_code} == 403",
"name": "d"
}
}
]
},
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": {
"expr": "{http.error.status_code} == 404",
"name": "e"
}
}
]
},
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": {
"expr": "{http.error.status_code} == 404",
"name": "f"
}
}
]
},
{
"handle": [
{
"abort": true,
"handler": "static_response"
}
],
"match": [
{
"expression": {
"expr": "{http.error.status_code} == 404",
"name": "g"
}
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,40 @@
:8080 {
root * ./
file_server {
etag_file_extensions .b3sum .sha256
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "./"
},
{
"etag_file_extensions": [
".b3sum",
".sha256"
],
"handler": "file_server",
"hide": [
"./Caddyfile"
]
}
]
}
]
}
}
}
}
}
@@ -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"
}
]
}
]
}
]
}
}
}
}
}
@@ -3,6 +3,7 @@
http_port 8080
https_port 8443
grace_period 5s
shutdown_delay 10s
default_sni localhost
order root first
storage file_system {
@@ -10,6 +11,7 @@
}
acme_ca https://example.com
acme_ca_root /path/to/ca.crt
ocsp_stapling off
email test@example.com
admin off
@@ -44,6 +46,7 @@
"http_port": 8080,
"https_port": 8443,
"grace_period": 5000000000,
"shutdown_delay": 10000000000,
"servers": {
"srv0": {
"listen": [
@@ -61,17 +64,22 @@
"module": "internal"
}
],
"key_type": "ed25519"
"key_type": "ed25519",
"disable_ocsp_stapling": true
}
],
"on_demand": {
"permission": {
"endpoint": "https://example.com",
"module": "http"
},
"rate_limit": {
"interval": 30000000000,
"burst": 20
},
"ask": "https://example.com"
}
}
}
},
"disable_ocsp_stapling": true
}
}
}
@@ -21,6 +21,8 @@
burst 20
}
storage_clean_interval 7d
renew_interval 1d
ocsp_interval 2d
key_type ed25519
}
@@ -61,6 +63,14 @@
"issuers": [
{
"ca": "https://example.com",
"challenges": {
"http": {
"alternate_port": 8080
},
"tls-alpn": {
"alternate_port": 8443
}
},
"email": "test@example.com",
"external_account": {
"key_id": "4K2scIVbBpNd-78scadB2g",
@@ -76,12 +86,17 @@
}
],
"on_demand": {
"permission": {
"endpoint": "https://example.com",
"module": "http"
},
"rate_limit": {
"interval": 30000000000,
"burst": 20
},
"ask": "https://example.com"
}
},
"ocsp_interval": 172800000000000,
"renew_interval": 86400000000000,
"storage_clean_interval": 604800000000000
}
}
@@ -71,11 +71,14 @@
}
],
"on_demand": {
"permission": {
"endpoint": "https://example.com",
"module": "http"
},
"rate_limit": {
"interval": 30000000000,
"burst": 20
},
"ask": "https://example.com"
}
}
}
}
@@ -0,0 +1,36 @@
{
http_port 8080
persist_config off
admin {
origins localhost:2019 [::1]:2019 127.0.0.1:2019 192.168.10.128
}
}
:80
----------
{
"admin": {
"listen": "localhost:2019",
"origins": [
"localhost:2019",
"[::1]:2019",
"127.0.0.1:2019",
"192.168.10.128"
],
"config": {
"persist": false
}
},
"apps": {
"http": {
"http_port": 8080,
"servers": {
"srv0": {
"listen": [
":80"
]
}
}
}
}
}
@@ -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 {
wrap console
fields {
common_log delete
request>remote_addr ip_mask {
request>remote_ip ip_mask {
ipv4 24
ipv6 32
}
@@ -19,10 +18,7 @@
"custom-logger": {
"encoder": {
"fields": {
"common_log": {
"filter": "delete"
},
"request\u003eremote_addr": {
"request\u003eremote_ip": {
"filter": "ip_mask",
"ipv4_cidr": 24,
"ipv6_cidr": 32

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