Compare commits

..

162 Commits

Author SHA1 Message Date
Mohammed Al Sahaf 1b453dd4fb ci: force fetch the upstream tags (#3947) 2020-12-30 21:02:54 +00:00
Dave Henderson ebc278ec98 metrics: allow disabling OpenMetrics negotiation (#3944)
* metrics: allow disabling OpenMetrics negotiation

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* fixup! metrics: allow disabling OpenMetrics negotiation
2020-12-30 11:44:02 -07:00
Francis Lavoie 79f3af9927 ci: Add pushing to cloudsmith (#3941)
* ci: Add pushing to cloudsmith

* ci: Update comments, remove env TODO

* ci: Fix Cloudsmith installation by setting PATH

* docs: Add Cloudsmith attribution to README

* ci: Switch to keeping armv7 as the armhf .deb
2020-12-30 10:54:58 -07:00
Matthew Holt d8bcf5be4e fileserver: Fix "go up" links in browse listings (closes #3942)
At some point we changed how paths are represented down the function calls of browse listings and forgot to update the canGoUp logic. I think this is right? It's simpler now.
2020-12-30 08:05:01 -07:00
Francis Lavoie 38a83ca6f8 ci: Update goreleaser to fix deprecation notices (#3945)
See https://goreleaser.com/deprecations#nfpmsfiles and https://goreleaser.com/deprecations#nfpmsconfig_files
2020-12-30 09:28:20 -05:00
Mohammed Al Sahaf 2b90cdba52 ci: reject tags if not signed by Matthew Holt's key (#3932)
* ci: reject tags if not signed by Matthew Holt's key

* ci: don't reject tags if an intermediate commits are not signed
2020-12-29 12:52:13 -07:00
Matthew Holt 635f075f18 caddyfile: Fix minor bug in formatter 2020-12-16 15:22:16 -07:00
Matthew Holt e384f07a3c caddytls: Improve alt chain preference settings
This allows for finer-grained control when choosing alternate chains than
simply the previous/Certbot-esque behavior of "choose first chain that
contains an issuer's common name." This update allows you to sort by
length (if optimizing for efficiency on the wire) and also to select the
chain with a specific root CommonName.
2020-12-15 12:16:04 -07:00
Matthew Holt 132525de3b reverseproxy: Minor lint fixes 2020-12-14 15:30:55 -07:00
Matthew Holt deedf8abb0 caddyhttp: Optionally use forwarded IP for remote_ip matcher
The remote_ip matcher was reading the X-Forwarded-For header by default, but this behavior was not documented in anything that was released. This is also a less secure default, as it is trivially easy to spoof request headers. Reading IPs from that header should be optional, and it should not be the default.

This is technically a breaking change, but anyone relying on the undocumented behavior was just doing so by coincidence/luck up to this point since it was never in any released documentation. We'll still add a mention in the release notes about this.
2020-12-10 16:09:30 -07:00
Matthew Holt 63bda6a0dc caddyhttp: Clean up internal auto-HTTPS redirect code
Refactor redirect route creation into own function.

Improve condition for appending port.
Fixes a bug manifested through new test case:
TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses
2020-12-10 14:36:46 -07:00
Matthew Holt b8a799df9f caddyhttp: Document that remote_ip reads X-Forwarded-For header
https://caddy.community/t/remote-ip-behaviour/10762?u=matt
2020-12-09 13:07:11 -07:00
Matthew Holt a748151666 go.mod: Update CertMagic (fix #3911) 2020-12-09 13:07:11 -07:00
Jack Baron c898a37f40 httpcaddyfile: support matching headers that do not exist (#3909)
* add integration test for null header matcher

* implement null header matcher syntax

* avoid repeating magic !

* check for field following ! character
2020-12-09 11:28:14 -07:00
Matthew Holt 31fbcd7401 go.mod: Upgrade some dependencies 2020-12-08 14:06:52 -07:00
Matthew Holt 7e719157d9 httpcaddyfile: Decrement counter when removing conn policy (fix #3906) 2020-12-07 14:22:47 -07:00
Francis Lavoie 6e9ac248dd fastcgi: Set PATH_INFO to file matcher remainder as fallback (#3739)
* fastcgi: Set PATH_INFO to file matcher remainder as fallback

* fastcgi: Avoid changing scriptName when not necessary

* Stylistic tweaks

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2020-12-04 17:12:13 -07:00
Marten Seemann 5643dc3fb9 go.mod: update quic-go to v0.19.3 (#3901) 2020-12-04 08:49:26 -07:00
Matthew Holt 3d0e046238 caddyauth: Use structured log 2020-12-03 11:33:55 -07:00
Matthew Holt bac82073d0 Merge remote-tracking branch 'origin/master' 2020-12-03 11:33:18 -07:00
Jordi Masip e7a5a3850f cmd: add ability to read config from stdin (#3898) 2020-12-03 10:02:18 -07:00
Matthew Holt aca7ef0d4c Add setcap script to gitignore 2020-12-02 13:48:13 -07:00
Matthew Holt 792fca40f1 Minor comments 2020-12-02 13:27:08 -07:00
Matthew Holt 9157051f45 caddyhttp: Optimize large host matchers 2020-12-02 13:26:28 -07:00
Cuong Manh Le 4cff36d731 caddyauth: Use buffered channel passed to signal.Notify (#3895)
The docs at os/signal.Notify warn about this signal delivery loss bug at
https://golang.org/pkg/os/signal/#Notify, which says:

    Package signal will not block sending to c: the caller must ensure
    that c has sufficient buffer space to keep up with the expected signal
    rate. For a channel used for notification of just one signal value,
    a buffer of size 1 is sufficient.

Caught by a static analysis tool from Orijtech, Inc. called "sigchanyzer"
2020-12-01 08:27:46 -07:00
Francis Lavoie a26f70a12b headers: Fix Caddyfile parsing with request matcher (#3892) 2020-11-30 10:20:30 -07:00
Francis Lavoie 4afcdc49d1 docs: Mention {http.auth.user.id} placeholder in basicauth JSON docs (#3886) 2020-11-26 22:31:25 -05:00
Matthew Holt 7d7434c9ce fileserver: Add debug logging 2020-11-26 09:37:42 -07:00
Daniel Santos 53aa60afff reverseproxy: Handle "operation was canceled" errors (#3816)
* fix(caddy): Avoid "operation was canceled" errors

- Also add error handling for StatusGatewayTimeout

* revert(caddy): Revert 504 handling

- This will potentially break load balancing and health checks

* Handle client cancellation as different error

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2020-11-25 10:54:23 -07:00
Matt Holt b0f8fc7aae caddytls: Configure trusted CAs from PEM files (#3882)
Closes #3563
2020-11-25 10:53:00 -07:00
Matthew Holt 03d853e2ec httpcaddyfile: Fix test on Windows 2020-11-24 18:04:37 -07:00
Matthew Holt 63afffc2e3 httpcaddyfile: Proper log config with catch-all blocks (fix #3878) 2020-11-24 16:36:58 -07:00
Matthew Holt 2d5498ee6f Update readme 2020-11-24 12:57:12 -07:00
Matthew Holt 0a7721dcfe fileserver: Preserve transformed root (fix #3838) 2020-11-24 12:24:44 -07:00
Ian c5197f5999 acme_server: fix reload of acme database (#3874)
* acme_server: Refactor database creation apart from authority creation

This is a WIP commit that doesn't really offer anything other than
setting us up for using a UsagePool to gracefully reload acme_server
configs.

* Implement UsagePool

* Remove unused context

* Fix initializing non-ACME CA

This will handle cases where a DB is not provided

* Sanitize acme db path and clean debug logs

* Move regex to package level to prevent recompiling
2020-11-23 13:58:26 -07:00
Ian 06ba006f9b acme_server: switch to bbolt storage (#3868)
* acme_server: switch to bbolt storage

There have been some issues with the badger storage engine
being used by the embedded acme_server. This will replace
the storage engine with bbolt

* Switch database path back to acme_server/db and remove if directory
2020-11-23 13:03:58 -07:00
Francis Lavoie c6dec30535 caddyfile: Add support for env var defaults; add tests (#3682)
* caddyfile: Add support for env var defaults, tests

* caddyfile: Use ?? instead, fix redundant cast, remove env chaining

* caddyfile: Use : instead
2020-11-23 12:51:35 -07:00
Francis Lavoie 3cfefeb0f7 httpcaddyfile: Configure servers via global options (#3836)
* httpcaddyfile: First pass at implementing server options

* httpcaddyfile: Add listener wrapper support

* httpcaddyfile: Sort sbaddrs to make adapt output more deterministic

* httpcaddyfile: Add server options adapt tests

* httpcaddyfile: Windows line endings lol

* caddytest: More windows line endings lol (sorry Matt)

* Update caddyconfig/httpcaddyfile/serveroptions.go

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

* httpcaddyfile: Reword listener address "matcher"

* Apply suggestions from code review

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

* httpcaddyfile: Deprecate experimental_http3 option (moved to servers)

* httpcaddyfile: Remove validation step, no longer needed

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-11-23 12:46:50 -07:00
Francis Lavoie 4a641f6c6f reverseproxy: Add Caddyfile scheme shorthand for h2c (#3629)
* reverseproxy: Add Caddyfile scheme shorthand for h2c

* reverseproxy: Use parentheses for condition

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

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-11-23 12:18:26 -07:00
Dave Henderson bd17eb205d ci: Use golangci's github action for linting (#3794)
* ci: Use golangci's github action for linting

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Fix most of the staticcheck lint errors

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Fix the prealloc lint errors

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Fix the misspell lint errors

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Fix the varcheck lint errors

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Fix the errcheck lint errors

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Fix the bodyclose lint errors

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Fix the deadcode lint errors

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Fix the unused lint errors

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Fix the gosec lint errors

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Fix the gosimple lint errors

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Fix the ineffassign lint errors

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Fix the staticcheck lint errors

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Revert the misspell change, use a neutral English

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Remove broken golangci-lint CI job

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Re-add errantly-removed weakrand initialization

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* don't break the loop and return

* Removing extra handling for null rootKey

* unignore RegisterModule/RegisterAdapter

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

* single-line log message

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

* Fix lint after a1808b0dbf209c615e438a496d257ce5e3acdce2 was merged

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Revert ticker change, ignore it instead

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Ignore some of the write errors

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Remove blank line

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Use lifetime

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* close immediately

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

* Preallocate configVals

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Update modules/caddytls/distributedstek/distributedstek.go

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-11-22 14:50:29 -07:00
Marten Seemann 1e480b818b go.mod: update quic-go to v0.19.2 (#3880) 2020-11-21 14:54:11 -07:00
Francis Lavoie 96058538f0 reverseproxy: Logging for streaming and upgrades (#3689)
* reverseproxy: Enable error logging for connection upgrades

* reverseproxy: Change some of the error levels, unsugar

* Use unsugared log in one spot

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2020-11-20 14:24:58 -07:00
Dimitri Masson 6e0849d4c2 reverseproxy: Implement cookie hash selection policy (#3809)
* add CookieHashSelection for session affinity

* add CookieHashSelection for session affinity

* register module

* reverse_proxy: Add and fix cookie lb_policy

* reverse_proxy: Manage hmac.write error on cookie hash selection

* reverse_proxy: fix some comments

* reverse_proxy: variable `cookieValue` is inside the else block

* reverse_proxy: Abstract duplicate nuanced logic of reservoir sampling into a function

* reverse_proxy: Set a default secret is indeed useless

* reverse_proxy: add configuration syntax for cookie lb_policy

* reverse_proxy: doc typo and improvement

Co-authored-by: utick <123liuqingdong@163.com>
2020-11-20 12:39:26 -07:00
Gilbert Gilb's b0d5c2c8ae headers: Support default header values in Caddyfile with '?' (#3807)
* implement default values for header directive

closes #3804

* remove `set_default` header op and rely on "require" handler instead

This has the following advantages over the previous attempt:

- It does not introduce a new operation for headers, but rather nicely
  extends over an existing feature in the header handler.
- It removes the need to specify the header as "deferred" because it is
  already implicitely deferred by the use of the require handler. This
  should be less confusing to the user.

* add integration test for header directive in caddyfile

* bubble up errors when parsing caddyfile header directive

* don't export unnecessarily and don't canonicalize headers unnecessarily

* fix response headers not passed in blocks

* caddyfile: fix clash when using default header in block

Each header is now set in a separate handler so that it doesn't clash
with other headers set/added/deleted in the same block.

* caddyhttp: New idle_timeout default of 5m

* reverseproxy: fix random hangs on http/2 requests with server push (#3875)

see https://github.com/golang/go/issues/42534

* Refactor and cleanup with improvements

* More specific link

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
Co-authored-by: Денис Телюх <telyukh.denis@gmail.com>
2020-11-20 12:38:16 -07:00
Matthew Holt 12cc69ab7a Merge branch 'master' of https://github.com/caddyserver/caddy 2020-11-18 16:14:55 -07:00
Matthew Holt 349457cc1b caddyhttp: Return error if error handling error
Before, if there was an error in the error handler, we would not write a
status code, which resulted in Go writing a 200 for us by default, which
does not make sense when there's an error. Now we write the second
error's status if available, otherwise 500.
2020-11-18 16:14:50 -07:00
Денис Телюх 6ea6f3ebe0 reverseproxy: fix random hangs on http/2 requests with server push (#3875)
see https://github.com/golang/go/issues/42534
2020-11-18 11:53:43 -07:00
Matthew Holt 1438e4dbc8 caddyhttp: New idle_timeout default of 5m 2020-11-18 10:57:54 -07:00
Matthew Holt 4fc570711e caddyhttp: Fix header matcher when using nil
Uncovered in #3807
2020-11-17 11:29:43 -07:00
Dimitri Masson 99b8f44486 reverse_proxy: Fix random_choose selection policy (#3811) 2020-11-16 12:47:15 -07:00
Nicola Piccinini 670b723e38 requestbody: Add Caddyfile support (#3859)
* Add Caddyfile support for request_body:

```
  request_body {
    max_size 10000000
  }
```

* Improve Caddyfile parser for request_body module

* Remove unnecessary `continue`

* Add sample for caddyfile_adapt_test
2020-11-16 11:43:39 -07:00
Matt Holt 13781e67ab caddytls: Support multiple issuers (#3862)
* caddytls: Support multiple issuers

Defaults are Let's Encrypt and ZeroSSL.

There are probably bugs.

* Commit updated integration tests, d'oh

* Update go.mod
2020-11-16 11:05:55 -07:00
Aurelia 7a3d9d81fe basicauth: Minor internal improvements (#3861)
* nitpicks and small improvements in basicauth module

1:
roll two if statements into one, since err will be nil in the second case anyhow

2:
unlock cache mutex after reading the key, as this happens by-value and reduces code complexity

3:
switch cache sync.Mutex to sync.RWMutex for better concurrency on cache fast track

* allocate the right kind of mutex
2020-11-13 15:28:21 -07:00
Matthew Holt 95af4262a8 caddytls: Support ACME alt cert chain preferences 2020-11-12 15:03:07 -07:00
Matthew Holt 3db60e6cba Update contact info 2020-11-12 15:03:07 -07:00
Gaurav Dhameeja 7c28ecb5f4 httpcaddyfile: Add certificate_pem placeholder short, add to godoc (#3846)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2020-11-04 13:37:41 -05:00
Mohammed Al Sahaf 9e28f60aab ci: remove the continuous fuzzing job (#3845)
Between Github Actions deprecting a command we use[0] and Fuzzit planning to deprecate their standalone service after being acquired by Gitlab[1][2], there are no reasons to keep this job.

[0] https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/

[1] https://about.gitlab.com/press/releases/2020-06-11-gitlab-acquires-peach-tech-and-fuzzit-to-expand-devsecops-offering.html

[2] https://fuzzit.dev/2020/06/11/news-fuzzit-is-acquired-by-gitlab/
2020-11-04 16:32:07 +00:00
Francis Lavoie b4f49e2962 caddyhttp: Merge query matchers in Caddyfile (#3839)
Also, turns out that `Add` on headers will work even if there's nothing there yet, so we can remove the condition I introduced in #3832
2020-11-02 16:05:01 -07:00
Christoph Kluge dd26875ffc logging: Fix for IP filtering 2020-11-02 16:01:58 -07:00
Francis Lavoie eda9a1b377 fastcgi: Add timeouts support to Caddyfile adapter (#3842)
* fastcgi: Add timeouts support to Caddyfile adapter

* fastcgi: Use tabs instead of spaces
2020-11-02 15:11:17 -07:00
Francis Lavoie 860cc6adfe reverseproxy: Wire up some http transport options in Caddyfile (#3843) 2020-11-02 14:59:02 -07:00
Matt Holt 8d038ca515 fileserver: Improve and clarify file hiding logic (#3844)
* fileserver: Improve and clarify file hiding logic

* Oops, forgot to run integration tests

* Make this one integration test OS-agnostic

* See if this appeases the Windows gods

* D'oh
2020-11-02 14:20:12 -07:00
Matthew Holt 937ec34201 caddyauth: Prevent user enumeration by timing
Always follow the code path of hashing and comparing a plaintext
password even if the account is not found by the given username; this
ensures that similar CPU cycles are spent for both valid and invalid
usernames.

Thanks to @tylerlm for helping and looking into this!
2020-10-31 10:51:05 -06:00
Francis Lavoie 966d5e6b42 caddyhttp: Merge header matchers in Caddyfile (#3832) 2020-10-31 10:27:01 -06:00
Francis Lavoie b66099379d reverseproxy: Add max_idle_conns_per_host; fix godocs (#3829) 2020-10-30 12:05:21 -06:00
Jason McCallister c9fdff9976 reverseproxy: caddyfile: Don't add port if upstream has placeholder (#3819)
* check if the host is a placeholder

* Update modules/caddyhttp/reverseproxy/caddyfile.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-10-29 13:51:42 -06:00
Matt Holt db4f1c0277 httpcaddyfile: Revise automation policy generation (#3824)
* httpcaddyfile: Revise automation policy generation

This should fix a frustrating edge case where wildcard subjects are
used, which potentially get shadowed by more specific versions of
themselves; see the new tests for an example. This change is motivated
by an actual customer requirement.

Although all the tests pass, this logic is incredibly complex and
nuanced, and I'm worried it is not correct. But it took me about 4 days
to get this far on a solution. I did my best.

* Fix typo
2020-10-28 20:36:00 -06:00
Matthew Holt b6e96d6f4a go.mod: Update CertMagic 2020-10-22 12:42:06 -06:00
Matthew Holt b6686a54d8 httpcaddyfile: Improve AP logic with OnDemand
We have users that have site blocks like *.*.tld with on-demand TLS
enabled. While *.*.tld does not qualify for a publicly-trusted cert due
to its wildcards, On-Demand TLS does not actually obtain a cert with
those wildcards, since it uses the actual hostname on the handshake.

This improves on that logic, but I am still not 100% satisfied with the
result since I think we need to also check if another site block is more
specific, like foo.example.tld, which might not have on-demand TLS
enabled, and make sure an automation policy gets created before the
more general policy with on-demand...
2020-10-22 12:40:23 -06:00
Matthew Holt 97caf368ee readme: Add zerossl 2020-10-19 10:44:46 -06:00
Matt Holt 385adf5d87 caddyhttp: Restore original request params before error handlers (#3781)
* caddyhttp: Restore original request params before error handlers

Fixes #3717

* Add comment
2020-10-13 10:52:39 -06:00
Matt Holt c7efb0307d reverseproxy: Fix dial placeholders, SRV, active health checks (#3780)
* reverseproxy: Fix dial placeholders, SRV, active health checks

Supercedes #3776
Partially reverts or updates #3756, #3693, and #3695

* reverseproxy: add integration tests

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2020-10-13 10:35:20 -06:00
AJ ONeal e34d9f1244 readme: Add link to website for download instructions (#3785)
* add Webi as install method

* link to install page
2020-10-09 11:32:09 -06:00
Matthew Holt ef8a372a1c map: Bug fixes; null literal with hyphen in Caddyfile 2020-10-02 16:08:28 -06:00
Matthew Holt 0fc47e8357 map: Apply default if mapped output is nil 2020-10-02 15:23:52 -06:00
Matthew Holt 25d2b4bf29 map: Reimplement; multiple outputs; optimize 2020-10-02 14:23:56 -06:00
Matt Holt 023d702f30 Update SECURITY.md 2020-10-01 17:11:10 -06:00
Mohammed Al Sahaf 6722426f1a reverseproxy: allow no port for SRV; fix regression in d55d50b (#3756)
* reverseproxy: fix breakage in handling SRV lookup introduced by 3695

* reverseproxy: validate against incompatible config options with lookup_srv

* reverseproxy: add integration test cases for validations involving lookup_srv

* reverseproxy: clarify the reason for skipping an iteration

* grammar.. Oxford comma

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

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

Fixes #3753
2020-10-01 14:05:39 -06:00
Aleksei 3b9eae70c9 reverseproxy: Change 500 error to 502 for lookup_srv config (#3771)
Fixes #3763
2020-10-01 14:02:31 -06:00
Mohammed Al Sahaf aa9c3eb732 reverseproxy: default to port 80 for upstreams in Caddyfile (#3772)
* reverseproxy: default to port 80 for port-less upstream dial addresses

* reverseproxy: replace integration test with an adapter test

Fixes #3761
2020-10-01 13:53:19 -06:00
Christian Flach fdfdc03339 reverseproxy: Ignore RFC 1521 params in Content-Type header (#3758)
Without this change, a Content-Type header like "text/event-stream;charset=utf-8"
would not trigger the immediate flushing.

Fixes #3765
2020-10-01 12:15:45 -06:00
Dave Henderson dadfe1933b metrics: fix handler to not run the next route (#3769)
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
2020-10-01 10:57:14 -06:00
Dave Henderson 85152679ce admin: lower log level to Debug for /metrics requests (#3749)
* admin: lower log level to Debug for /metrics requests

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* 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>
2020-09-25 18:00:08 -06:00
Mohammed Al Sahaf a33e4b5426 caddyfile: Add support for vars and vars_regexp matchers (#3730)
* caddyfile: support vars and vars_regexp matchers in the caddyfile

* caddyfile: matchers: Brian Kernighan said printf is good debugging tool but didn't say keep them around
2020-09-25 17:50:26 -06:00
Dave Henderson f197cec7f3 metrics: Always track method label in uppercase (#3742)
* metrics: Always track method label in uppercase

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Just use strings.ToUpper for clarity

Signed-off-by: Dave Henderson <dhenderson@gmail.com>
2020-09-22 20:10:34 -06:00
Francis Lavoie be6daa5fd4 httpcaddyfile: Fix panic when parsing route with matchers (#3746)
Fixes #3745
2020-09-22 17:37:15 -06:00
Francis Lavoie fe27f9cf0c httpcaddyfile: Disallow args on route/handle directive family (#3740) 2020-09-21 13:44:41 -06:00
Dave Henderson b1d456d8ab metrics: Fix panic when headers aren't written (#3737)
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
2020-09-21 13:42:47 -06:00
Dave Henderson d16ede358a metrics: Fix hidden panic while observing with bad exemplars (#3733)
* metrics: Fixing panic while observing with bad exemplars

Signed-off-by: Dave Henderson <dhenderson@gmail.com>

* Minor cleanup

The server is already added to the context. So, we can simply use that
to get the server name, which is a field on the server.

* Add integration test for auto HTTP->HTTPS redirects

A test like this would have caught the problem in the first place

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2020-09-17 21:46:24 -06:00
Matthew Holt c82c231ba7 caddyhttp: Remove server name from metrics
For some reason this breaks automatic HTTP->HTTPS redirects. I am not
sure why yet, but as a hotfix remove this until we understand it better.
2020-09-17 17:23:58 -06:00
Matthew Holt 3ee663dee1 go.mod: Upgrade dependencies 2020-09-17 12:35:25 -06:00
Dave Henderson 8ec51bbede metrics: Initial integration of Prometheus metrics (#3709)
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
2020-09-17 12:01:20 -06:00
Mohammed Al Sahaf bc453fa6ae reverseproxy: Correct alternate port for active health checks (#3693)
* reverseproxy: construct active health-check transport from scratch (Fixes #3691)

* reverseproxy: do upstream health-check on the correct alternative port

* reverseproxy: add integration test for health-check on alternative port

* reverseproxy: put back the custom transport for health-check http client

* reverseproxy: cleanup health-check integration test

* reverseproxy: fix health-check of unix socket upstreams

* reverseproxy: skip unix socket tests on Windows

* tabs > spaces

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

* make the linter (and @francislavoie) happy

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

* One more lint fix

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

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2020-09-17 10:25:34 -06:00
Francis Lavoie e3324aa6de httpcaddyfile: Ensure handle_path is sorted equally to handle (#3676)
* httpcaddyfile: Ensure handle_path is sorted as equal to handle

* httpcaddyfile: Make mutual exclusivity grouping deterministic (I hope)

* httpcaddyfile: Add comment linking to the issue being fixed

* httpcaddyfile: Typo fix, comment clarity

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

* Update caddyconfig/httpcaddyfile/httptype.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-09-16 20:01:22 -06:00
Mohammed Al Sahaf d55d50b3b3 reverseproxy: Enforce port range size of 1 at provision (#3695)
* reverse_proxy: ensure upstream address has port range of only 1

* reverse_proxy: don't log the error if upstream range size is more than 1
2020-09-16 19:48:37 -06:00
Francis Lavoie b95b87381a fileserver: Fix try_files for directories; windows fix (#3684)
* fileserver: Fix try_files for directories, windows fix

* fileserver: Add new file type placeholder, refactoring, tests

* fileserver: Review cleanup

* fileserver: Flip the return args order
2020-09-16 18:09:28 -06:00
Gaurav Dhameeja b01bb275b3 caddyhttp: New placeholder for PEM of client certificate (#3662)
* Fix-3585: added placeholder for a PEM encoded value of the certificate

* Update modules/caddyhttp/replacer.go

Change type of block and empty headers removed

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

* fixed tests

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-09-16 15:06:51 -06:00
Francis Lavoie 309c1fec62 logging: Implement Caddyfile support for filter encoder (#3578)
* logging: Implement Caddyfile support for filter encoder

* logging: Add support for parsing IP masks from strings


wip

* logging: Implement Caddyfile support for ip_mask

* logging: Get rid of unnecessary logic to allow strings, not that useful

* logging: Add adapt test
2020-09-15 12:37:41 -06:00
Matthew Penner b88e2b6a49 cmd: Allow caddy fmt to read from stdin (#3680)
* Allow 'caddy fmt' to read from stdin

* fmt: use '-' as the file name for reading from stdin

* Minor adjustments

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2020-09-14 12:30:12 -06:00
Matthew Holt 4217217bad httpcaddyfile: Properly record whether we added catch-all conn policy
We recently introduced `if !cp.SettingsEmpty()` which conditionally
adds the connection policy to the list. If the condition evaluates to
false, the policy wouldn't actually be added, even if
hasCatchAllTLSConnPolicy was set to true on the previous line.

Now we set that variable in accordance with whether we actually add
the policy.

While debugging this I noticed that catch-all policies added early in
that loop (i.e. not at the end if we later determine we need one) are
not always at the end of the list. They should be, though, since they
are selected by which one matches first, and having a catch-all first
would nullify any more specific ones later in the list. So I added a
sort in consolidateConnPolicies to take care of that.

Should fix #3670 and
https://caddy.community/t/combining-on-demand-tls-with-custom-ssl-certs-doesnt-seem-to-work-in-2-1-1/9719
but I won't know for sure until somebody verifies it, since at least in
the GitHub issue there is not yet enough information (the configs are
redacted).
2020-09-11 13:45:21 -06:00
Matt Holt 1c5969b576 fileserver: Fix new file hide tests on Windows (#3719) 2020-09-11 13:09:16 -06:00
Matthew Holt 0ee4378227 fileserver: Improve file hiding logic for directories and prefixes
Now, a filename to hide that is specified without a path separator will
count as hidden if it appears in any component of the file path (not
only the last component); semantically, this means hiding a file by only
its name (without any part of a path) will hide both files and folders,
e.g. hiding ".git" will hide "/.git" and also "/.git/foo".

We also do prefix matching so that hiding "/.git" will hide "/.git"
and "/.git/foo" but not "/.gitignore".

The remaining logic is a globular match like before.
2020-09-11 12:20:39 -06:00
Matthew Holt 9859ab8148 caddytls: Fix resolvers option of acme issuer (Caddyfile)
Reported in:
https://caddy.community/t/dns-challenge-with-namecheap-and-split-horizon-dns/9611/17?u=matt
2020-09-09 10:21:59 -06:00
Francis Lavoie 00e6b77fe4 caddytls: Add dns config to acmeissuer (#3701) 2020-09-08 11:36:46 -06:00
Mohammed Al Sahaf d4f249741e browse: align template to struct field renames from 4940325 (#3706) 2020-09-08 10:45:48 -06:00
Francis Lavoie 04f50a9759 caddyhttp: Wrap http.Server logging with zap (#3668) 2020-09-08 10:44:58 -06:00
Francis Lavoie 4cd7ae35b3 reverseproxy: Add buffer_requests option to reverse_proxy directive (#3710) 2020-09-08 10:37:46 -06:00
Matthew Holt 24f34780b6 caddytls: Customize DNS resolvers for DNS challenge with Caddyfile 2020-08-31 13:23:26 -06:00
Matthew Holt 724b74d981 reverseproxy: Abort active health checks on context cancellation 2020-08-31 13:22:34 -06:00
Matthew Holt 4940325844 fileserver: Fix inconsistencies in browse JSON 2020-08-31 12:33:43 -06:00
Matthew Holt 744d04c258 caddytls: Configure custom DNS resolvers for DNS challenge (close #2476)
And #3391

Maybe also related: #3664
2020-08-21 20:30:14 -06:00
Francis Lavoie ecbc1f85c5 ci: Tweaks for multi go version tests (#3673) 2020-08-20 22:40:26 -04:00
Matthew Holt 997ef522bc go.mod: Use v0.15(.1) of smallstep libs
Update internal issuer for compatibility -- yay simpler code!

The .1 version also fixes non-critical SAN extensions that caused trust
issues on several clients.
2020-08-20 19:28:25 -06:00
Francis Lavoie 0279a57ac4 ci: Upgrade to Go 1.15 (#3642)
* ci: Try Go 1.15 RC1 out of curiosity

* Go 1.15 was released; let's try it

* Update to latest quic-go

* Attempt at fixing broken test

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2020-08-20 14:04:10 -06:00
Matthew Holt c94f5bb7dd reverseproxy: Make default buffer size const 2020-08-17 16:17:16 -06:00
Francis Lavoie 0afbab8667 httpcaddyfile: Improve directive sorting logic (#3658)
* httpcaddyfile: Flip `root` directive sort order

* httpcaddyfile: Sort directives with any matcher before those with none

* httpcaddyfile: Generalize reverse sort directives, improve logic

* httpcaddyfile: Fix "spelling" issue

* httpcaddyfile: Turns out the second change precludes the first


httpcaddyfile: Delete test that no longer makes sense

* httpcaddyfile: Shorten logic

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

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-08-17 16:15:51 -06:00
Francis Lavoie fc65320e9c reverseproxy: Support header selection policy on Host field (#3653) 2020-08-17 15:14:46 -06:00
Matthew Holt e385be9225 Update comment and Caddy 1 EOL 2020-08-11 11:26:19 -06:00
Matt Holt 66863aad3b caddytls: Add support for ZeroSSL; add Caddyfile support for issuers (#3633)
* caddytls: Add support for ZeroSSL; add Caddyfile support for issuers

Configuring issuers explicitly in a Caddyfile is not easily compatible
with existing ACME-specific parameters such as email or acme_ca which
infer the kind of issuer it creates (this is complicated now because
the ZeroSSL issuer wraps the ACME issuer)... oh well, we can revisit
that later if we need to.

New Caddyfile global option:

    {
        cert_issuer <name> ...
    }

Or, alternatively, as a tls subdirective:

    tls {
        issuer <name> ...
    }

For example, to use ZeroSSL with an API key:

    {
        cert_issuser zerossl API_KEY
    }

For now, that still uses ZeroSSL's ACME endpoint; it fetches EAB
credentials for you. You can also provide the EAB credentials directly
just like any other ACME endpoint:

    {
        cert_issuer acme {
            eab KEY_ID MAC_KEY
        }
    }

All these examples use the new global option (or tls subdirective). You
can still use traditional/existing options with ZeroSSL, since it's
just another ACME endpoint:

    {
        acme_ca  https://acme.zerossl.com/v2/DV90
        acme_eab KEY_ID MAC_KEY
    }

That's all there is to it. You just can't mix-and-match acme_* options
with cert_issuer, because it becomes confusing/ambiguous/complicated to
merge the settings.

* Fix broken test

This test was asserting buggy behavior, oops - glad this branch both
discovers and fixes the bug at the same time!

* Fix broken test (post-merge)

* Update modules/caddytls/acmeissuer.go

Fix godoc comment

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

* Add support for ZeroSSL's EAB-by-email endpoint

Also transform the ACMEIssuer into ZeroSSLIssuer implicitly if set to
the ZeroSSL endpoint without EAB (the ZeroSSLIssuer is needed to
generate EAB if not already provided); this is now possible with either
an API key or an email address.

* go.mod: Use latest certmagic, acmez, and x/net

* Wrap underlying logic rather than repeating it

Oops, duh

* Form-encode email info into request body for EAB endpoint

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2020-08-11 08:58:06 -06:00
Matthew Holt c42bfaf31e go.mod: Bump CertMagic 2020-08-08 08:42:01 -06:00
Matthew Holt e2f913bb7f reverseproxy: Minor fixes and cleanup
Now use context cancellation to stop active health checker, which is
simpler than and just as effective as using a separate stop channel.
2020-08-07 18:02:24 -06:00
Matt Holt 65a09524c3 caddyhttp: Add TLS client cert info to logs (#3640) 2020-08-07 12:12:29 -06:00
Matthew Holt c6d6a775a1 go.mod: Update some dependencies
We can't update smallstep/nosql and klauspost/cpuid yet because of
upstream breakage.
2020-08-06 14:36:21 -06:00
Matt Holt 4accf737a6 ci: Ignore s390x failures (#3644)
As of early August 2020 the VM has been down for several days due to
lack of power due related to bad weather at the data center... sigh.
2020-08-06 14:17:40 -06:00
Matthew Holt ff19bddac5 httpcaddyfile: Avoid repeated subjects in APs (fix #3618)
When consolidating automation policies, ensure same subject names do not
get appended to list.
2020-08-06 13:56:23 -06:00
Francis Lavoie 584eba94a4 httpcaddyfile: Allow named matchers in route blocks (#3632) 2020-08-05 13:42:29 -06:00
Kevin Lin 904f149e5b reverse_proxy: fix bidirectional streams with encodings (fix #3606) (#3620)
* reverse_proxy: fix bi-h2stream breaking gzip encode handle(#3606).

* reverse_proxy: check http version of both sides to avoid affecting non-h2 upstream.

* Minor cleanup; apply review suggestions

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2020-08-03 20:50:38 -06:00
Ye Zhihao 8b80a3201f httpcaddyfile: Bring enforce_origin and origins to admin config (#3595)
* Bring `ensure_origin` and `origins` to caddyfile admin config

* Add unit test for caddyfile admin config update

* Add caddyfile adapt test for typical admin setup

* httpcaddyfile: Replace admin config error message when there's more arguments than needed

Replace d.Err() to d.ArgErr() since the latter provides similarly informative error message

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

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-08-03 13:44:38 -06:00
Matthew Holt 68529e2f9e cmd: Print caddy version with environ or --environ (#3627) 2020-08-03 10:42:42 -06:00
Mohammed Al Sahaf 399eff415c ci: Include tracking of GOOS for which Caddy fails to build (#3617)
* ci: include tracking of GOOS for which Caddy fails to build

* ci: split cross-build check into separate workflow

* ci: cross-build check: make it clear the cross-build check is not a blocker

* ci: cross-build check: set annotation instead of failing the build

* ci: cross-build check: explicitly set continue-on-error to force success marker

* ci: cross-build check: send stderr to /dev/null

* ci: Simplify workflow names

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>
2020-08-01 20:23:22 +00:00
Matt Holt c054a818a1 fileserver: Fix newly-introduced failing test on Linux (#3625)
* fileserver: First attempt to fix failing test on Linux

I think I updated the wrong test case before

* Make new test function

I guess what we really are trying to test is the case insensitivity of
firstSplit. So a new test function is better for that.
2020-08-01 12:43:30 -06:00
Bart af5c148ed1 admin,templates,core: Minor enhancements and error handling (#3607)
* fix 2 possible bugs

* handle unhandled errors
2020-07-31 16:54:18 -06:00
v-rosa 514eef33fe caddyhttp: Add support to resolve DN in CEL expression (#3608) 2020-07-31 15:06:30 -06:00
Matthew Holt 3860b235d0 fileserver: Don't assume len(str) == len(ToLower(str)) (fix #3623)
We can't use a positional index on an original string that we got from
its lower-cased equivalent. Implement our own IndexFold() function b/c
the std lib does not have one.
2020-07-31 13:55:01 -06:00
Ye Zhihao 6f73a358f4 httpcaddyfile: Add compression to http transport config (#3624)
* httpcaddyfile: Add `compression` to http transport config

* Add caddyfile adapt test for typical h2c setup
2020-07-31 11:30:20 -06:00
Matt Holt 6a14e2c2a8 caddytls: Replace lego with acmez (#3621)
* Replace lego with acmez; upgrade CertMagic

* Update integration test
2020-07-30 15:18:14 -06:00
Patrick Hein 2bc30bb780 templates: Implement placeholders function (#3324)
* caddyhttp, httpcaddyfile: Implement placeholders in template

* caddyhttp, httpcaddyfile: Remove support for placeholder shorthands in templates

* Update modules/caddyhttp/templates/templates.go

updates JSON doc

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

* Update modules/caddyhttp/templates/tplcontext.go

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

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-07-20 17:17:38 -06:00
Matthew Holt 28d870c193 go.mod: Update quic-go, truststore, and goldmark 2020-07-20 14:57:40 -06:00
Francis Lavoie fb9d874fa9 caddyfile: Export Tokenize function for lexing (#3549) 2020-07-20 13:55:51 -06:00
Matt Holt 6cea1f239d push: Implement HTTP/2 server push (#3573)
* push: Implement HTTP/2 server push (close #3551)

* push: Abstract header ops by embedding into new struct type

This will allow us to add more fields to customize headers in
push-specific ways in the future.

* push: Ensure Link resources are pushed before response is written

* Change header name from X-Caddy-Push to Caddy-Push
2020-07-20 12:28:40 -06:00
Manuel Dalla Lana 2ae8c11927 fastcgi: Add resolve_root_symlink (#3587) 2020-07-20 12:16:13 -06:00
Kevin Lin e9b1d7dcb4 reverse_proxy: flush HTTP/2 response when ContentLength is unknown (#3561)
* reverse proxy: Support more h2 stream scenarios (#3556)

* reverse proxy: add integration test for better h2 stream (#3556)

* reverse proxy: adjust comments as francislavoie suggests

* link to issue #3556 in the comments
2020-07-20 12:14:46 -06:00
Mohammed Al Sahaf bd9d796e6e reverseproxy: add support for custom DNS resolver (#3479)
* reverse proxy: add support for custom resolver

* reverse proxy: don't pollute the global resolver with bootstrap resolver setup

* Improve documentation of reverseproxy.UpstreamResolver fields

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

* reverse proxy: clarify the name resolution conventions of upstream resolvers and bootstrap resolver

* remove support for bootstraper of resolver

* godoc and code-style changes

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-07-18 15:00:00 -06:00
Matthew Holt 246a31aacd reverseproxy: Restore request's original host and header (fix #3509)
We already restore them within the retry loop, but after successful
proxy we didn't reset them, so as handlers bubble back up, they would
see the values used for proxying.

Thanks to @ziddey for identifying the cause.
2020-07-17 17:54:58 -06:00
Francis Lavoie 0665a86eb7 fastcgi: Ensure leading slash, omit SERVER_PORT if empty for compliance (#3570)
See https://tools.ietf.org/html/rfc3875#section-4.1.13 for SCRIPT_NAME requiring leading slash
See https://tools.ietf.org/html/rfc3875#section-4.1.15 for SERVER_PORT requiring omission if empty
2020-07-17 14:48:50 -06:00
Francis Lavoie 3fdaf50785 fastcgi: Fill REMOTE_USER with http.auth.user.id placeholder (#3577)
Completing a TODO!
2020-07-17 13:33:40 -06:00
Francis Lavoie 19cc2bd3c3 reverseproxy: Fix Caddyfile parsing for empty non-http transports (#3576)
* reverseproxy: Fix Caddyfile parsing for empty non-http transports

* Update modules/caddyhttp/reverseproxy/caddyfile.go

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

* Rename empty transport test

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-07-17 13:18:32 -06:00
Matthew Holt 705de11bef readme: Minor tweaks 2020-07-17 12:53:48 -06:00
Matthew Holt 8a0fff58aa caddyauth: hash-password: Set bcrypt cost to 14 (#3580) 2020-07-17 12:20:53 -06:00
Matthew Holt 6f0f159ba5 caddyhttp: Add {http.request.body} placeholder 2020-07-16 19:25:37 -06:00
Matthew Holt 6eafd4e82f readme: Update badges 2020-07-16 15:29:06 -06:00
Matthew Holt eda54c22a6 logging: ⚠️ Deprecate logfmt encoder
It is essentially broken because it occludes many log fields.

See: https://github.com/caddyserver/caddy/issues/3575
2020-07-13 16:18:34 -06:00
Matthew Holt 2c71fb116b chore: Rename file to be consistent 2020-07-11 17:53:33 -06:00
Kévin Dunglas 724613a1be docs: Remove extra word in README.md (#3564) 2020-07-10 10:00:24 -04:00
snu-ceyda 735c86658d fileserver: Enable browse pagination with offset parameter (#3542)
* Update browse.go

* Update browselisting.go

* Update browsetpl.go

* fix linter err

* Update modules/caddyhttp/fileserver/browse.go

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

* Update modules/caddyhttp/fileserver/browselisting.go

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

* Update browsetpl.go

change from -> offset

* Update browse.go

* Update browselisting.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-07-08 23:56:15 -06:00
Matthew Holt a2dae1d43f templates: Fix front matter closing fence search
This makes it choose first matching closing fence instead of last one,
which could appear in document body.
2020-07-08 16:46:56 -06:00
Matthew Holt efc0cc5e85 caddytls: Move initial storage clean op into goroutine
Sometimes this operation can take a while (we observed 7 minutes
recently, with a large, globally-distributed storage backend).
2020-07-08 10:59:49 -06:00
Matthew Holt 0bf2565c37 caddyhttp: Reorder some access log fields; add host matcher test case
This field order reads a little more naturally.
2020-07-07 08:11:35 -06:00
Matthew Holt 7bfe5b6c95 httpcaddyfile: Reorder automation policy logic (close #3550) 2020-07-07 08:10:37 -06:00
Matthew Holt 2a5599e2ad go.mod: Upgrade and downgrade smallstep, quic-go, and cpuid
Closes #3537 and fixes #3535
2020-07-06 12:10:35 -06:00
Greg Anders c35820012b templates: Disable hard wraps in Markdown rendering (#3553) 2020-07-06 11:53:40 -06:00
Francis Lavoie 2d0f8831f8 ci: Fix another oops with publish workflow (#3536) 2020-06-30 15:36:17 -04:00
152 changed files with 9328 additions and 2255 deletions
+2 -2
View File
@@ -11,7 +11,7 @@ Please note that we consider publicly-registered domain names to be public infor
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| 2.x | :white_check_mark: | | 2.x | :white_check_mark: |
| 1.x | :white_check_mark: (deprecating soon) | | 1.x | :x: |
| < 1.x | :x: | | < 1.x | :x: |
## Reporting a Vulnerability ## Reporting a Vulnerability
@@ -22,6 +22,6 @@ We'll need enough information to verify the bug and make a patch. It will speed
Please also understand that due to our nature as an open source project, we do not have a budget to award security bounties. We can only thank you. Please also understand that due to our nature as an open source project, we do not have a budget to award security bounties. We can only thank you.
If your report is valid and a patch is released, we will not reveal your identity by default. If you wish to be credited, please give us the name to use. If your report is valid and a patch is released, we will not reveal your identity by default. If you wish to be credited, please give us the name to use and/or your GitHub username. If you don't provide this we can't credit you.
Thanks for responsibly helping Caddy&mdash;and thousands of websites&mdash;be more secure! Thanks for responsibly helping Caddy&mdash;and thousands of websites&mdash;be more secure!
+10 -23
View File
@@ -1,6 +1,6 @@
# Used as inspiration: https://github.com/mvdan/github-actions-golang # Used as inspiration: https://github.com/mvdan/github-actions-golang
name: Cross-Platform name: Tests
on: on:
push: push:
@@ -17,7 +17,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ] os: [ ubuntu-latest, macos-latest, windows-latest ]
go-version: [ 1.14.x ] go: [ '1.14', '1.15' ]
# Set some variables per OS, usable via ${{ matrix.VAR }} # Set some variables per OS, usable via ${{ matrix.VAR }}
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing # CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
@@ -39,9 +39,9 @@ jobs:
steps: steps:
- name: Install Go - name: Install Go
uses: actions/setup-go@v1 uses: actions/setup-go@v2
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
@@ -69,12 +69,12 @@ jobs:
echo "::set-output name=go_cache::$(go env GOCACHE)" echo "::set-output name=go_cache::$(go env GOCACHE)"
- name: Cache the build cache - name: Cache the build cache
uses: actions/cache@v1 uses: actions/cache@v2
with: with:
path: ${{ steps.vars.outputs.go_cache }} path: ${{ steps.vars.outputs.go_cache }}
key: ${{ runner.os }}-go-ci-${{ hashFiles('**/go.sum') }} key: ${{ runner.os }}-${{ matrix.go }}-go-ci-${{ hashFiles('**/go.sum') }}
restore-keys: | restore-keys: |
${{ runner.os }}-go-ci ${{ runner.os }}-${{ matrix.go }}-go-ci
- name: Get dependencies - name: Get dependencies
run: | run: |
@@ -91,7 +91,7 @@ jobs:
- name: Publish Build Artifact - name: Publish Build Artifact
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v1
with: with:
name: caddy_v2_${{ runner.os }}_${{ steps.vars.outputs.short_sha }} name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
path: ${{ matrix.CADDY_BIN_PATH }} path: ${{ matrix.CADDY_BIN_PATH }}
# Commented bits below were useful to allow the job to continue # Commented bits below were useful to allow the job to continue
@@ -124,6 +124,7 @@ jobs:
name: test (s390x on IBM Z) name: test (s390x on IBM Z)
runs-on: ubuntu-latest 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
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
steps: steps:
- name: Checkout code into the Go module directory - name: Checkout code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v2
@@ -147,26 +148,12 @@ jobs:
env: env:
SSH_KEY: ${{ secrets.S390X_SSH_KEY }} SSH_KEY: ${{ secrets.S390X_SSH_KEY }}
# From https://github.com/reviewdog/action-golangci-lint
golangci-lint:
name: runner / golangci-lint
runs-on: ubuntu-latest
steps:
- name: Checkout code into the Go module directory
uses: actions/checkout@v2
- name: Run golangci-lint
uses: reviewdog/action-golangci-lint@v1
# uses: docker://reviewdog/action-golangci-lint:v1 # pre-build docker image
with:
github_token: ${{ secrets.github_token }}
goreleaser-check: goreleaser-check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: goreleaser/goreleaser-action@v1 - uses: goreleaser/goreleaser-action@v2
with: with:
version: latest version: latest
args: check args: check
+60
View File
@@ -0,0 +1,60 @@
name: Cross-Build
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
cross-build-test:
strategy:
fail-fast: false
matrix:
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
go: [ '1.14', '1.15' ]
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Print Go version and environment
id: vars
run: |
printf "Using go at: $(which go)\n"
printf "Go version: $(go version)\n"
printf "\n\nGo environment:\n\n"
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 }}
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
-73
View File
@@ -1,73 +0,0 @@
name: Fuzzing
on:
# Daily midnight fuzzing
schedule:
- cron: '0 0 * * *'
jobs:
fuzzing:
name: Fuzzing
strategy:
matrix:
os: [ ubuntu-latest ]
go-version: [ 1.14.x ]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Download go-fuzz tools and the Fuzzit CLI, move Fuzzit CLI to GOBIN
# If we decide we need to prevent this from running on forks, we can use this line:
# if: github.repository == 'caddyserver/caddy'
run: |
go get -v github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
wget -q -O fuzzit https://github.com/fuzzitdev/fuzzit/releases/download/v2.4.77/fuzzit_Linux_x86_64
chmod a+x fuzzit
mv fuzzit $(go env GOPATH)/bin
echo "::add-path::$(go env GOPATH)/bin"
- name: Generate fuzzers & submit them to Fuzzit
continue-on-error: true
env:
FUZZIT_API_KEY: ${{ secrets.FUZZIT_API_KEY }}
SYSTEM_PULLREQUEST_SOURCEBRANCH: ${{ github.ref }}
BUILD_SOURCEVERSION: ${{ github.sha }}
run: |
# debug
echo "PR Source Branch: $SYSTEM_PULLREQUEST_SOURCEBRANCH"
echo "Source version: $BUILD_SOURCEVERSION"
declare -A fuzzers_funcs=(\
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="FuzzParseAddress" \
["./listeners_fuzz.go"]="FuzzParseNetworkAddress" \
["./replacer_fuzz.go"]="FuzzReplacer" \
)
declare -A fuzzers_targets=(\
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="parse-address" \
["./listeners_fuzz.go"]="parse-network-address" \
["./replacer_fuzz.go"]="replacer" \
)
fuzz_type="fuzzing"
for f in $(find . -name \*_fuzz.go); do
FUZZER_DIRECTORY=$(dirname "$f")
echo "go-fuzz-build func ${fuzzers_funcs[$f]} residing in $f"
go-fuzz-build -func "${fuzzers_funcs[$f]}" -o "$FUZZER_DIRECTORY/${fuzzers_targets[$f]}.zip" "$FUZZER_DIRECTORY"
fuzzit create job --engine go-fuzz caddyserver/"${fuzzers_targets[$f]}" "$FUZZER_DIRECTORY"/"${fuzzers_targets[$f]}.zip" --api-key "${FUZZIT_API_KEY}" --type "${fuzz_type}" --branch "${SYSTEM_PULLREQUEST_SOURCEBRANCH}" --revision "${BUILD_SOURCEVERSION}"
echo "Completed $f"
done
+23
View File
@@ -0,0 +1,23 @@
name: Lint
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
# From https://github.com/golangci/golangci-lint-action
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.31
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true
+84 -10
View File
@@ -11,21 +11,30 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ ubuntu-latest ] os: [ ubuntu-latest ]
go-version: [ 1.14.x ] go: [ '1.15' ]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Install Go - name: Install Go
uses: actions/setup-go@v1 uses: actions/setup-go@v2
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
with:
fetch-depth: 0
# So GoReleaser can generate the changelog properly # Force fetch upstream tags -- because 65 minutes
- name: Unshallowify the repo clone # tl;dr: actions/checkout@v2 runs this line:
run: git fetch --prune --unshallow # 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
# which doesn't overwrite that tag because that would be destructive.
# Credit to @francislavoie for the investigation.
# https://github.com/actions/checkout/issues/290#issuecomment-680260080
- name: Force fetch upstream tags
run: git fetch --tags --force
# https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027 # https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
- name: Print Go version and environment - name: Print Go version and environment
@@ -41,6 +50,9 @@ jobs:
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)" echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
echo "::set-output name=go_cache::$(go env GOCACHE)" echo "::set-output name=go_cache::$(go env GOCACHE)"
# Add "pip install" CLI tools to PATH
echo ~/.local/bin >> $GITHUB_PATH
# Parse semver # Parse semver
TAG=${GITHUB_REF/refs\/tags\//} TAG=${GITHUB_REF/refs\/tags\//}
SEMVER_RE='[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z\.-]*\)' SEMVER_RE='[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z\.-]*\)'
@@ -53,17 +65,32 @@ jobs:
echo "::set-output name=tag_patch::${TAG_PATCH}" echo "::set-output name=tag_patch::${TAG_PATCH}"
echo "::set-output name=tag_special::${TAG_SPECIAL}" echo "::set-output name=tag_special::${TAG_SPECIAL}"
# Cloudsmith CLI tooling for pushing releases
# See https://help.cloudsmith.io/docs/cli
- name: Install Cloudsmith CLI
run: pip install --upgrade cloudsmith-cli
- name: Validate commits and tag signatures
run: |
# Import Matt Holt's key
curl 'https://github.com/mholt.gpg' | gpg --import
echo "Verifying the tag: ${{ steps.vars.outputs.version_tag }}"
# 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 - name: Cache the build cache
uses: actions/cache@v1 uses: actions/cache@v2
with: with:
path: ${{ steps.vars.outputs.go_cache }} path: ${{ steps.vars.outputs.go_cache }}
key: ${{ runner.os }}-go-release-${{ hashFiles('**/go.sum') }} key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }}
restore-keys: | restore-keys: |
${{ runner.os }}-go-release ${{ runner.os }}-go${{ matrix.go }}-release
# GoReleaser will take care of publishing those artifacts into the release # GoReleaser will take care of publishing those artifacts into the release
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v1 uses: goreleaser/goreleaser-action@v2
with: with:
version: latest version: latest
args: release --rm-dist args: release --rm-dist
@@ -72,12 +99,59 @@ jobs:
TAG: ${{ steps.vars.outputs.version_tag }} TAG: ${{ steps.vars.outputs.version_tag }}
# Only publish on non-special tags (e.g. non-beta) # Only publish on non-special tags (e.g. non-beta)
# We will continue to push to Gemfury for the forseeable future, although
# 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 - name: Publish .deb to Gemfury
if: ${{ steps.vars.outputs.tag_special == '' }} if: ${{ steps.vars.outputs.tag_special == '' }}
env: env:
GEMFURY_PUSH_TOKEN: ${{ secrets.GEMFURY_PUSH_TOKEN }} GEMFURY_PUSH_TOKEN: ${{ secrets.GEMFURY_PUSH_TOKEN }}
run: | run: |
for filename in dist/*.deb; do for filename in dist/*.deb; do
# armv6 and armv7 are both "armhf" so we can skip the duplicate
if [[ "$filename" == *"armv6"* ]]; then
echo "Skipping $filename"
continue
fi
curl -F package=@"$filename" https://${GEMFURY_PUSH_TOKEN}:@push.fury.io/caddy/ curl -F package=@"$filename" https://${GEMFURY_PUSH_TOKEN}:@push.fury.io/caddy/
done done
# Publish only special tags (unstable/beta/rc) to the "testing" repo
# See https://cloudsmith.io/~caddy/repos/testing/
- name: Publish .deb to Cloudsmith (special tags)
if: ${{ steps.vars.outputs.tag_special != '' }}
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
run: |
for filename in dist/*.deb; do
# armv6 and armv7 are both "armhf" so we can skip the duplicate
if [[ "$filename" == *"armv6"* ]]; then
echo "Skipping $filename"
continue
fi
echo "Pushing $filename to 'testing'"
cloudsmith push deb caddy/testing/any-distro/any-version $filename
done
# Publish stable tags to Cloudsmith to both repos, "stable" and "testing"
# See https://cloudsmith.io/~caddy/repos/stable/
- name: Publish .deb to Cloudsmith (stable tags)
if: ${{ steps.vars.outputs.tag_special == '' }}
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
run: |
for filename in dist/*.deb; do
# armv6 and armv7 are both "armhf" so we can skip the duplicate
if [[ "$filename" == *"armv6"* ]]; then
echo "Skipping $filename"
continue
fi
echo "Pushing $filename to 'stable'"
cloudsmith push deb caddy/stable/any-distro/any-version $filename
echo "Pushing $filename to 'testing'"
cloudsmith push deb caddy/testing/any-distro/any-version $filename
done
+1 -1
View File
@@ -30,5 +30,5 @@ jobs:
token: ${{ secrets.REPO_DISPATCH_TOKEN }} token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/caddy-docker repository: caddyserver/caddy-docker
event-type: release-tagged event-type: release-tagged
client-payload: '{"tag": "${{ github.release.tag_name }}"}' client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
+2 -1
View File
@@ -7,9 +7,10 @@ Caddyfile
*.prof *.prof
*.test *.test
# build artifacts # build artifacts and helpers
cmd/caddy/caddy cmd/caddy/caddy
cmd/caddy/caddy.exe cmd/caddy/caddy.exe
cmd/caddy/setcap*
# mac specific # mac specific
.DS_Store .DS_Store
+52 -5
View File
@@ -1,21 +1,68 @@
linters-settings: linters-settings:
errcheck: errcheck:
ignore: fmt:.*,io/ioutil:^Read.*,github.com/caddyserver/caddy/v2/caddyconfig:RegisterAdapter,github.com/caddyserver/caddy/v2:RegisterModule ignore: fmt:.*,io/ioutil:^Read.*,go.uber.org/zap/zapcore:^Add.*
ignoretests: true ignoretests: true
misspell:
locale: US
linters: linters:
disable-all: true
enable: enable:
- bodyclose - bodyclose
- prealloc - deadcode
- unconvert
- errcheck - errcheck
- gofmt - gofmt
- goimports - goimports
- gosec - gosec
- gosimple
- govet
- ineffassign - ineffassign
- misspell - misspell
- prealloc
- staticcheck
- structcheck
- typecheck
- unconvert
- unused
- varcheck
# these are implicitly disabled:
# - asciicheck
# - depguard
# - dogsled
# - dupl
# - exhaustive
# - exportloopref
# - funlen
# - gci
# - gochecknoglobals
# - gochecknoinits
# - gocognit
# - goconst
# - gocritic
# - gocyclo
# - godot
# - godox
# - goerr113
# - gofumpt
# - goheader
# - golint
# - gomnd
# - gomodguard
# - goprintffuncname
# - interfacer
# - lll
# - maligned
# - nakedret
# - nestif
# - nlreturn
# - noctx
# - nolintlint
# - rowserrcheck
# - scopelint
# - sqlclosecheck
# - stylecheck
# - testpackage
# - unparam
# - whitespace
# - wsl
run: run:
# default concurrency is a available CPU number. # default concurrency is a available CPU number.
+17 -7
View File
@@ -84,13 +84,22 @@ nfpms:
# - rpm # - rpm
bindir: /usr/bin bindir: /usr/bin
files: contents:
./caddy-dist/init/caddy.service: /lib/systemd/system/caddy.service - src: ./caddy-dist/init/caddy.service
./caddy-dist/init/caddy-api.service: /lib/systemd/system/caddy-api.service dst: /lib/systemd/system/caddy.service
./caddy-dist/welcome/index.html: /usr/share/caddy/index.html
./caddy-dist/scripts/completions/bash-completion: /etc/bash_completion.d/caddy - src: ./caddy-dist/init/caddy-api.service
config_files: dst: /lib/systemd/system/caddy-api.service
./caddy-dist/config/Caddyfile: /etc/caddy/Caddyfile
- src: ./caddy-dist/welcome/index.html
dst: /usr/share/caddy/index.html
- src: ./caddy-dist/scripts/completions/bash-completion
dst: /etc/bash_completion.d/caddy
- src: ./caddy-dist/config/Caddyfile
dst: /etc/caddy/Caddyfile
type: config
scripts: scripts:
postinstall: ./caddy-dist/scripts/postinstall.sh postinstall: ./caddy-dist/scripts/postinstall.sh
@@ -112,5 +121,6 @@ changelog:
- '^chore:' - '^chore:'
- '^ci:' - '^ci:'
- '^docs?:' - '^docs?:'
- '^readme:'
- '^tests?:' - '^tests?:'
- '^\w+\s+' # a hack to remove commit messages without colons thus don't correspond to a package - '^\w+\s+' # a hack to remove commit messages without colons thus don't correspond to a package
+56 -14
View File
@@ -1,21 +1,25 @@
<p align="center"> <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"><img src="https://user-images.githubusercontent.com/1128849/36338535-05fb646a-136f-11e8-987b-e6901e717d5a.png" alt="Caddy" width="450"></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>
</p> </p>
<hr>
<h3 align="center">Every site on HTTPS</h3> <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">Caddy is an extensible server platform that uses TLS by default.</p>
<p align="center"> <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?query=workflow%3ACross-Platform"><img src="https://github.com/caddyserver/caddy/workflows/Cross-Platform/badge.svg"></a>
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-blue.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>
<a href="https://app.fuzzit.dev/orgs/caddyserver-gh/dashboard"><img src="https://app.fuzzit.dev/badge?org_id=caddyserver-gh"></a>
<br> <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> <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>
<a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a> <a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
<br>
<a href="https://sourcegraph.com/github.com/caddyserver/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/caddyserver/caddy/-/badge.svg" alt="Caddy on Sourcegraph"></a> <a href="https://sourcegraph.com/github.com/caddyserver/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/caddyserver/caddy/-/badge.svg" alt="Caddy on Sourcegraph"></a>
<a href="https://cloudsmith.io/~caddy/repos/"><img src="https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith" alt="Cloudsmith"></a>
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/caddyserver/caddy/releases">Download</a> · <a href="https://github.com/caddyserver/caddy/releases">Releases</a> ·
<a href="https://caddyserver.com/docs/">Documentation</a> · <a href="https://caddyserver.com/docs/">Documentation</a> ·
<a href="https://caddy.community">Community</a> <a href="https://caddy.community">Get Help</a>
</p> </p>
@@ -23,6 +27,7 @@
### Menu ### Menu
- [Features](#features) - [Features](#features)
- [Install](#install)
- [Build from source](#build-from-source) - [Build from source](#build-from-source)
- [For development](#for-development) - [For development](#for-development)
- [With version information and/or plugins](#with-version-information-andor-plugins) - [With version information and/or plugins](#with-version-information-andor-plugins)
@@ -39,25 +44,32 @@
</p> </p>
## Features ## [Features](https://caddyserver.com/v2)
- **Easy configuration** with the [Caddyfile](https://caddyserver.com/docs/caddyfile) - **Easy configuration** with the [Caddyfile](https://caddyserver.com/docs/caddyfile)
- **Powerful configuration** with its [native JSON config](https://caddyserver.com/docs/json/) - **Powerful configuration** with its [native JSON config](https://caddyserver.com/docs/json/)
- **Dynamic configuration** with the [JSON API](https://caddyserver.com/docs/api) - **Dynamic configuration** with the [JSON API](https://caddyserver.com/docs/api)
- [**Config adapters**](https://caddyserver.com/docs/config-adapters) if you don't like JSON - [**Config adapters**](https://caddyserver.com/docs/config-adapters) if you don't like JSON
- **Automatic HTTPS** by default - **Automatic HTTPS** by default
- [Let's Encrypt](https://letsencrypt.org) for public sites - [ZeroSSL](https://zerossl.com) and [Let's Encrypt](https://letsencrypt.org) for public names
- Fully-managed local CA for internal names & IPs - Fully-managed local CA for internal names & IPs
- Can coordinate with other Caddy instances in a cluster - Can coordinate with other Caddy instances in a cluster
- Multi-issuer fallback
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues - **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
- **Production-ready** after serving trillions of requests and managing millions of TLS certificates
- **Scales to tens of thousands of sites** ... and probably more
- **HTTP/1.1, HTTP/2, and experimental HTTP/3** support - **HTTP/1.1, HTTP/2, and experimental HTTP/3** support
- **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat - **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat
- **Runs anywhere** with **no external dependencies** (not even libc) - **Runs anywhere** with **no external dependencies** (not even libc)
- Written in Go, a language with higher **memory safety guarantees** than other servers - Written in Go, a language with higher **memory safety guarantees** than other servers
- Actually **fun to use** - Actually **fun to use**
- So, so much more to discover - So, so much more to [discover](https://caddyserver.com/v2)
## 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.
For other install options, see https://caddyserver.com/docs/download.
## Build from source ## Build from source
@@ -67,17 +79,41 @@ Requirements:
### For development ### 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 ```bash
$ git clone "https://github.com/caddyserver/caddy.git" $ git clone "https://github.com/caddyserver/caddy.git"
$ cd caddy/cmd/caddy/ $ cd caddy/cmd/caddy/
$ go build $ go build
``` ```
_**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions below._ When you run Caddy, it may try to bind to low ports unless otherwise specified in your config. If your OS requires elevated privileges, you will need to give your new binary permission to do so. On Linux, this can be done easily with: `sudo setcap cap_net_bind_service=+ep ./caddy`
If you prefer to use `go run` which creates temporary binaries, you can still do this. Make an executable file called `setcap.sh` (or whatever you want) with these contents:
```bash
#!/bin/sh
sudo setcap cap_net_bind_service=+ep "$1"
"$@"
```
then you can use `go run` like so:
```bash
$ go run -exec ./setcap.sh main.go
```
If you don't want to type your password for `setcap`, use `sudo visudo` to edit your sudoers file and allow your user account to run that command without a password, for example:
```
username ALL=(ALL:ALL) NOPASSWD: /usr/sbin/setcap
```
replacing `username` with your actual username. Please be careful and only do this if you know what you are doing! We are only qualified to document how to use Caddy, not Go tooling or your computer, and we are providing these instructions for convenience only; please learn how to use your own computer at your own risk and make any needful adjustments.
### With version information and/or plugins ### With version information and/or plugins
Using [our builder tool](https://github.com/caddyserver/xcaddy)... Using [our builder tool, `xcaddy`](https://github.com/caddyserver/xcaddy)...
``` ```
$ xcaddy build $ xcaddy build
@@ -89,8 +125,8 @@ $ xcaddy build
2. Change into it: `cd caddy` 2. Change into it: `cd caddy`
3. Copy [Caddy's main.go](https://github.com/caddyserver/caddy/blob/master/cmd/caddy/main.go) into the empty folder. Add imports for any custom plugins you want to add. 3. Copy [Caddy's main.go](https://github.com/caddyserver/caddy/blob/master/cmd/caddy/main.go) into the empty folder. Add imports for any custom plugins you want to add.
4. Initialize a Go module: `go mod init caddy` 4. Initialize a Go module: `go mod init caddy`
5. (Optional) Pin Caddy version: `go get github.com/caddyserver/caddy/v2@TAG` replacing `TAG` with a git tag or commit. 5. (Optional) Pin Caddy version: `go get github.com/caddyserver/caddy/v2@version` replacing `version` with a git tag or commit.
6. (Optional) Add plugins by adding their import: `_ "IMPORT_PATH"` 6. (Optional) Add plugins by adding their import: `_ "import/path/here"`
7. Compile: `go build` 7. Compile: `go build`
@@ -100,7 +136,7 @@ $ xcaddy build
The [Caddy website](https://caddyserver.com/docs/) has documentation that includes tutorials, quick-start guides, reference, and more. The [Caddy website](https://caddyserver.com/docs/) has documentation that includes tutorials, quick-start guides, reference, and more.
**We recommend that all users do our [Getting Started](https://caddyserver.com/docs/getting-started) guide to become familiar with using Caddy.** **We recommend that all users -- regardless of experience level -- do our [Getting Started](https://caddyserver.com/docs/getting-started) guide to become familiar with using Caddy.**
If you've only got a minute, [the website has several quick-start tutorials](https://caddyserver.com/docs/quick-starts) to choose from! However, after finishing a quick-start tutorial, please read more documentation to understand how the software works. 🙂 If you've only got a minute, [the website has several quick-start tutorials](https://caddyserver.com/docs/quick-starts) to choose from! However, after finishing a quick-start tutorial, please read more documentation to understand how the software works. 🙂
@@ -119,7 +155,7 @@ The primary way to configure Caddy is through [its API](https://caddyserver.com/
Caddy exposes an unprecedented level of control compared to any web server in existence. In Caddy, you are usually setting the actual values of the initialized types in memory that power everything from your HTTP handlers and TLS handshakes to your storage medium. Caddy is also ridiculously extensible, with a powerful plugin system that makes vast improvements over other web servers. Caddy exposes an unprecedented level of control compared to any web server in existence. In Caddy, you are usually setting the actual values of the initialized types in memory that power everything from your HTTP handlers and TLS handshakes to your storage medium. Caddy is also ridiculously extensible, with a powerful plugin system that makes vast improvements over other web servers.
To wield the power of this design, you need to know how the config document is structured. Please see the [our documentation site](https://caddyserver.com/docs/) for details about [Caddy's config structure](https://caddyserver.com/docs/json/). To wield the power of this design, you need to know how the config document is structured. Please see [our documentation site](https://caddyserver.com/docs/) for details about [Caddy's config structure](https://caddyserver.com/docs/json/).
Nearly all of Caddy's configuration is contained in a single config document, rather than being scattered across CLI flags and env variables and a configuration file as with other web servers. This makes managing your server config more straightforward and reduces hidden variables/factors. Nearly all of Caddy's configuration is contained in a single config document, rather than being scattered across CLI flags and env variables and a configuration file as with other web servers. This makes managing your server config more straightforward and reduces hidden variables/factors.
@@ -138,6 +174,8 @@ The docs are also open source. You can contribute to them here: https://github.c
- 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 **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.
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way!
- Individuals can exchange help for free on our community forum at https://caddy.community. Remember that people give help out of their spare time and good will. The best way to get help is to give it first! - Individuals can exchange help for free on our community forum at https://caddy.community. Remember that people give help out of their spare time and good will. The best way to get help is to give it first!
Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only for bug reports and feature requests, i.e. actionable development items (support questions will usually be referred to the forums). Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only for bug reports and feature requests, i.e. actionable development items (support questions will usually be referred to the forums).
@@ -146,7 +184,11 @@ Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only
## About ## About
**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 Light Code Labs, LLC. **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 apilayer GmbH.
- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_ - _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_
- _Author on Twitter: [@mholt6](https://twitter.com/mholt6)_ - _Author on Twitter: [@mholt6](https://twitter.com/mholt6)_
Caddy is a project of [ZeroSSL](https://zerossl.com), an [apilayer](https://apilayer.com) 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.
+52 -18
View File
@@ -18,6 +18,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"expvar" "expvar"
"fmt" "fmt"
"io" "io"
@@ -34,6 +35,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -106,34 +108,53 @@ func (admin AdminConfig) newAdminHandler(addr NetworkAddress) adminHandler {
mux: http.NewServeMux(), mux: http.NewServeMux(),
} }
addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
labels := prometheus.Labels{"path": pattern, "handler": handlerLabel}
h = instrumentHandlerCounter(
adminMetrics.requestCount.MustCurryWith(labels),
h,
)
muxWrap.mux.Handle(pattern, h)
}
// addRoute just calls muxWrap.mux.Handle after // addRoute just calls muxWrap.mux.Handle after
// wrapping the handler with error handling // wrapping the handler with error handling
addRoute := func(pattern string, h AdminHandler) { addRoute := func(pattern string, handlerLabel string, h AdminHandler) {
wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := h.ServeHTTP(w, r) err := h.ServeHTTP(w, r)
if err != nil {
labels := prometheus.Labels{
"path": pattern,
"handler": handlerLabel,
"method": strings.ToUpper(r.Method),
}
adminMetrics.requestErrors.With(labels).Inc()
}
muxWrap.handleError(w, r, err) muxWrap.handleError(w, r, err)
}) })
muxWrap.mux.Handle(pattern, wrapper) addRouteWithMetrics(pattern, handlerLabel, wrapper)
} }
const handlerLabel = "admin"
// register standard config control endpoints // register standard config control endpoints
addRoute("/"+rawConfigKey+"/", AdminHandlerFunc(handleConfig)) addRoute("/"+rawConfigKey+"/", handlerLabel, AdminHandlerFunc(handleConfig))
addRoute("/id/", AdminHandlerFunc(handleConfigID)) addRoute("/id/", handlerLabel, AdminHandlerFunc(handleConfigID))
addRoute("/stop", AdminHandlerFunc(handleStop)) addRoute("/stop", handlerLabel, AdminHandlerFunc(handleStop))
// register debugging endpoints // register debugging endpoints
muxWrap.mux.HandleFunc("/debug/pprof/", pprof.Index) addRouteWithMetrics("/debug/pprof/", handlerLabel, http.HandlerFunc(pprof.Index))
muxWrap.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) addRouteWithMetrics("/debug/pprof/cmdline", handlerLabel, http.HandlerFunc(pprof.Cmdline))
muxWrap.mux.HandleFunc("/debug/pprof/profile", pprof.Profile) addRouteWithMetrics("/debug/pprof/profile", handlerLabel, http.HandlerFunc(pprof.Profile))
muxWrap.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) addRouteWithMetrics("/debug/pprof/symbol", handlerLabel, http.HandlerFunc(pprof.Symbol))
muxWrap.mux.HandleFunc("/debug/pprof/trace", pprof.Trace) addRouteWithMetrics("/debug/pprof/trace", handlerLabel, http.HandlerFunc(pprof.Trace))
muxWrap.mux.Handle("/debug/vars", expvar.Handler()) addRouteWithMetrics("/debug/vars", handlerLabel, expvar.Handler())
// register third-party module endpoints // register third-party module endpoints
for _, m := range GetModules("admin.api") { for _, m := range GetModules("admin.api") {
router := m.New().(AdminRouter) router := m.New().(AdminRouter)
handlerLabel := m.ID.Name()
for _, route := range router.Routes() { for _, route := range router.Routes() {
addRoute(route.Pattern, route.Handler) addRoute(route.Pattern, handlerLabel, route.Handler)
} }
} }
@@ -235,15 +256,20 @@ func replaceAdmin(cfg *Config) error {
MaxHeaderBytes: 1024 * 64, MaxHeaderBytes: 1024 * 64,
} }
go adminServer.Serve(ln) adminLogger := Log().Named("admin")
go func() {
if err := adminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
}
}()
Log().Named("admin").Info("admin endpoint started", adminLogger.Info("admin endpoint started",
zap.String("address", addr.String()), zap.String("address", addr.String()),
zap.Bool("enforce_origin", adminConfig.EnforceOrigin), zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
zap.Strings("origins", handler.allowedOrigins)) zap.Strings("origins", handler.allowedOrigins))
if !handler.enforceHost { if !handler.enforceHost {
Log().Named("admin").Warn("admin endpoint on open interface; host checking disabled", adminLogger.Warn("admin endpoint on open interface; host checking disabled",
zap.String("address", addr.String())) zap.String("address", addr.String()))
} }
@@ -285,13 +311,18 @@ type adminHandler struct {
// ServeHTTP is the external entry point for API requests. // ServeHTTP is the external entry point for API requests.
// It will only be called once per request. // It will only be called once per request.
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Log().Named("admin.api").Info("received request", log := Log().Named("admin.api").With(
zap.String("method", r.Method), zap.String("method", r.Method),
zap.String("host", r.Host), zap.String("host", r.Host),
zap.String("uri", r.RequestURI), zap.String("uri", r.RequestURI),
zap.String("remote_addr", r.RemoteAddr), zap.String("remote_addr", r.RemoteAddr),
zap.Reflect("headers", r.Header), zap.Reflect("headers", r.Header),
) )
if r.RequestURI == "/metrics" {
log.Debug("received request")
} else {
log.Info("received request")
}
h.serveHTTP(w, r) h.serveHTTP(w, r)
} }
@@ -367,7 +398,10 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(apiErr.Code) w.WriteHeader(apiErr.Code)
json.NewEncoder(w).Encode(apiErr) encErr := json.NewEncoder(w).Encode(apiErr)
if encErr != nil {
Log().Named("admin.api").Error("failed to encode error response", zap.Error(encErr))
}
} }
// checkHost returns a handler that wraps next such that // checkHost returns a handler that wraps next such that
@@ -807,7 +841,7 @@ var (
// will get deleted before the process gracefully exits. // will get deleted before the process gracefully exits.
func PIDFile(filename string) error { func PIDFile(filename string) error {
pid := []byte(strconv.Itoa(os.Getpid()) + "\n") pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
err := ioutil.WriteFile(filename, pid, 0644) err := ioutil.WriteFile(filename, pid, 0600)
if err != nil { if err != nil {
return err return err
} }
+1 -1
View File
@@ -471,7 +471,7 @@ func stopAndCleanup() error {
} }
certmagic.CleanUpOwnLocks() certmagic.CleanUpOwnLocks()
if pidfile != "" { if pidfile != "" {
os.Remove(pidfile) return os.Remove(pidfile)
} }
return nil return nil
} }
+2
View File
@@ -78,6 +78,8 @@ func Format(input []byte) []byte {
if comment { if comment {
if ch == '\n' { if ch == '\n' {
comment = false comment = false
nextLine()
continue
} else { } else {
write(ch) write(ch)
continue continue
+11
View File
@@ -310,6 +310,17 @@ baz`,
input: `redir / /some/#/path`, input: `redir / /some/#/path`,
expect: `redir / /some/#/path`, expect: `redir / /some/#/path`,
}, },
{
description: "brace does not fold into comment above",
input: `# comment
{
foo
}`,
expect: `# comment
{
foo
}`,
},
} { } {
// the formatter should output a trailing newline, // the formatter should output a trailing newline,
// even if the tests aren't written to expect that // even if the tests aren't written to expect that
+19
View File
@@ -16,6 +16,7 @@ package caddyfile
import ( import (
"bufio" "bufio"
"bytes"
"io" "io"
"unicode" "unicode"
) )
@@ -168,3 +169,21 @@ func (l *lexer) next() bool {
val = append(val, ch) val = append(val, ch)
} }
} }
// 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 l.next() {
l.token.File = filename
tokens = append(tokens, l.token)
}
return tokens, nil
}
+37 -47
View File
@@ -15,37 +15,35 @@
package caddyfile package caddyfile
import ( import (
"log"
"strings"
"testing" "testing"
) )
type lexerTestCase struct { type lexerTestCase struct {
input string input []byte
expected []Token expected []Token
} }
func TestLexer(t *testing.T) { func TestLexer(t *testing.T) {
testCases := []lexerTestCase{ testCases := []lexerTestCase{
{ {
input: `host:123`, input: []byte(`host:123`),
expected: []Token{ expected: []Token{
{Line: 1, Text: "host:123"}, {Line: 1, Text: "host:123"},
}, },
}, },
{ {
input: `host:123 input: []byte(`host:123
directive`, directive`),
expected: []Token{ expected: []Token{
{Line: 1, Text: "host:123"}, {Line: 1, Text: "host:123"},
{Line: 3, Text: "directive"}, {Line: 3, Text: "directive"},
}, },
}, },
{ {
input: `host:123 { input: []byte(`host:123 {
directive directive
}`, }`),
expected: []Token{ expected: []Token{
{Line: 1, Text: "host:123"}, {Line: 1, Text: "host:123"},
{Line: 1, Text: "{"}, {Line: 1, Text: "{"},
@@ -54,7 +52,7 @@ func TestLexer(t *testing.T) {
}, },
}, },
{ {
input: `host:123 { directive }`, input: []byte(`host:123 { directive }`),
expected: []Token{ expected: []Token{
{Line: 1, Text: "host:123"}, {Line: 1, Text: "host:123"},
{Line: 1, Text: "{"}, {Line: 1, Text: "{"},
@@ -63,12 +61,12 @@ func TestLexer(t *testing.T) {
}, },
}, },
{ {
input: `host:123 { input: []byte(`host:123 {
#comment #comment
directive directive
# comment # comment
foobar # another comment foobar # another comment
}`, }`),
expected: []Token{ expected: []Token{
{Line: 1, Text: "host:123"}, {Line: 1, Text: "host:123"},
{Line: 1, Text: "{"}, {Line: 1, Text: "{"},
@@ -78,10 +76,10 @@ func TestLexer(t *testing.T) {
}, },
}, },
{ {
input: `host:123 { input: []byte(`host:123 {
# hash inside string is not a comment # hash inside string is not a comment
redir / /some/#/path redir / /some/#/path
}`, }`),
expected: []Token{ expected: []Token{
{Line: 1, Text: "host:123"}, {Line: 1, Text: "host:123"},
{Line: 1, Text: "{"}, {Line: 1, Text: "{"},
@@ -92,14 +90,14 @@ func TestLexer(t *testing.T) {
}, },
}, },
{ {
input: "# comment at beginning of file\n# comment at beginning of line\nhost:123", input: []byte("# comment at beginning of file\n# comment at beginning of line\nhost:123"),
expected: []Token{ expected: []Token{
{Line: 3, Text: "host:123"}, {Line: 3, Text: "host:123"},
}, },
}, },
{ {
input: `a "quoted value" b input: []byte(`a "quoted value" b
foobar`, foobar`),
expected: []Token{ expected: []Token{
{Line: 1, Text: "a"}, {Line: 1, Text: "a"},
{Line: 1, Text: "quoted value"}, {Line: 1, Text: "quoted value"},
@@ -108,7 +106,7 @@ func TestLexer(t *testing.T) {
}, },
}, },
{ {
input: `A "quoted \"value\" inside" B`, input: []byte(`A "quoted \"value\" inside" B`),
expected: []Token{ expected: []Token{
{Line: 1, Text: "A"}, {Line: 1, Text: "A"},
{Line: 1, Text: `quoted "value" inside`}, {Line: 1, Text: `quoted "value" inside`},
@@ -116,7 +114,7 @@ func TestLexer(t *testing.T) {
}, },
}, },
{ {
input: "An escaped \"newline\\\ninside\" quotes", input: []byte("An escaped \"newline\\\ninside\" quotes"),
expected: []Token{ expected: []Token{
{Line: 1, Text: "An"}, {Line: 1, Text: "An"},
{Line: 1, Text: "escaped"}, {Line: 1, Text: "escaped"},
@@ -125,7 +123,7 @@ func TestLexer(t *testing.T) {
}, },
}, },
{ {
input: "An escaped newline\\\noutside quotes", input: []byte("An escaped newline\\\noutside quotes"),
expected: []Token{ expected: []Token{
{Line: 1, Text: "An"}, {Line: 1, Text: "An"},
{Line: 1, Text: "escaped"}, {Line: 1, Text: "escaped"},
@@ -135,7 +133,7 @@ func TestLexer(t *testing.T) {
}, },
}, },
{ {
input: "line1\\\nescaped\nline2\nline3", input: []byte("line1\\\nescaped\nline2\nline3"),
expected: []Token{ expected: []Token{
{Line: 1, Text: "line1"}, {Line: 1, Text: "line1"},
{Line: 1, Text: "escaped"}, {Line: 1, Text: "escaped"},
@@ -144,7 +142,7 @@ func TestLexer(t *testing.T) {
}, },
}, },
{ {
input: "line1\\\nescaped1\\\nescaped2\nline4\nline5", input: []byte("line1\\\nescaped1\\\nescaped2\nline4\nline5"),
expected: []Token{ expected: []Token{
{Line: 1, Text: "line1"}, {Line: 1, Text: "line1"},
{Line: 1, Text: "escaped1"}, {Line: 1, Text: "escaped1"},
@@ -154,34 +152,34 @@ func TestLexer(t *testing.T) {
}, },
}, },
{ {
input: `"unescapable\ in quotes"`, input: []byte(`"unescapable\ in quotes"`),
expected: []Token{ expected: []Token{
{Line: 1, Text: `unescapable\ in quotes`}, {Line: 1, Text: `unescapable\ in quotes`},
}, },
}, },
{ {
input: `"don't\escape"`, input: []byte(`"don't\escape"`),
expected: []Token{ expected: []Token{
{Line: 1, Text: `don't\escape`}, {Line: 1, Text: `don't\escape`},
}, },
}, },
{ {
input: `"don't\\escape"`, input: []byte(`"don't\\escape"`),
expected: []Token{ expected: []Token{
{Line: 1, Text: `don't\\escape`}, {Line: 1, Text: `don't\\escape`},
}, },
}, },
{ {
input: `un\escapable`, input: []byte(`un\escapable`),
expected: []Token{ expected: []Token{
{Line: 1, Text: `un\escapable`}, {Line: 1, Text: `un\escapable`},
}, },
}, },
{ {
input: `A "quoted value with line input: []byte(`A "quoted value with line
break inside" { break inside" {
foobar foobar
}`, }`),
expected: []Token{ expected: []Token{
{Line: 1, Text: "A"}, {Line: 1, Text: "A"},
{Line: 1, Text: "quoted value with line\n\t\t\t\t\tbreak inside"}, {Line: 1, Text: "quoted value with line\n\t\t\t\t\tbreak inside"},
@@ -191,13 +189,13 @@ func TestLexer(t *testing.T) {
}, },
}, },
{ {
input: `"C:\php\php-cgi.exe"`, input: []byte(`"C:\php\php-cgi.exe"`),
expected: []Token{ expected: []Token{
{Line: 1, Text: `C:\php\php-cgi.exe`}, {Line: 1, Text: `C:\php\php-cgi.exe`},
}, },
}, },
{ {
input: `empty "" string`, input: []byte(`empty "" string`),
expected: []Token{ expected: []Token{
{Line: 1, Text: `empty`}, {Line: 1, Text: `empty`},
{Line: 1, Text: ``}, {Line: 1, Text: ``},
@@ -205,7 +203,7 @@ func TestLexer(t *testing.T) {
}, },
}, },
{ {
input: "skip those\r\nCR characters", input: []byte("skip those\r\nCR characters"),
expected: []Token{ expected: []Token{
{Line: 1, Text: "skip"}, {Line: 1, Text: "skip"},
{Line: 1, Text: "those"}, {Line: 1, Text: "those"},
@@ -214,13 +212,13 @@ func TestLexer(t *testing.T) {
}, },
}, },
{ {
input: "\xEF\xBB\xBF:8080", // test with leading byte order mark input: []byte("\xEF\xBB\xBF:8080"), // test with leading byte order mark
expected: []Token{ expected: []Token{
{Line: 1, Text: ":8080"}, {Line: 1, Text: ":8080"},
}, },
}, },
{ {
input: "simple `backtick quoted` string", input: []byte("simple `backtick quoted` string"),
expected: []Token{ expected: []Token{
{Line: 1, Text: `simple`}, {Line: 1, Text: `simple`},
{Line: 1, Text: `backtick quoted`}, {Line: 1, Text: `backtick quoted`},
@@ -228,7 +226,7 @@ func TestLexer(t *testing.T) {
}, },
}, },
{ {
input: "multiline `backtick\nquoted\n` string", input: []byte("multiline `backtick\nquoted\n` string"),
expected: []Token{ expected: []Token{
{Line: 1, Text: `multiline`}, {Line: 1, Text: `multiline`},
{Line: 1, Text: "backtick\nquoted\n"}, {Line: 1, Text: "backtick\nquoted\n"},
@@ -236,7 +234,7 @@ func TestLexer(t *testing.T) {
}, },
}, },
{ {
input: "nested `\"quotes inside\" backticks` string", input: []byte("nested `\"quotes inside\" backticks` string"),
expected: []Token{ expected: []Token{
{Line: 1, Text: `nested`}, {Line: 1, Text: `nested`},
{Line: 1, Text: `"quotes inside" backticks`}, {Line: 1, Text: `"quotes inside" backticks`},
@@ -244,7 +242,7 @@ func TestLexer(t *testing.T) {
}, },
}, },
{ {
input: "reverse-nested \"`backticks` inside\" quotes", input: []byte("reverse-nested \"`backticks` inside\" quotes"),
expected: []Token{ expected: []Token{
{Line: 1, Text: `reverse-nested`}, {Line: 1, Text: `reverse-nested`},
{Line: 1, Text: "`backticks` inside"}, {Line: 1, Text: "`backticks` inside"},
@@ -254,22 +252,14 @@ func TestLexer(t *testing.T) {
} }
for i, testCase := range testCases { for i, testCase := range testCases {
actual := tokenize(testCase.input) actual, err := Tokenize(testCase.input, "")
if err != nil {
t.Errorf("%v", err)
}
lexerCompare(t, i, testCase.expected, actual) lexerCompare(t, i, testCase.expected, actual)
} }
} }
func tokenize(input string) (tokens []Token) {
l := lexer{}
if err := l.load(strings.NewReader(input)); err != nil {
log.Printf("[ERROR] load failed: %v", err)
}
for l.next() {
tokens = append(tokens, l.token)
}
return
}
func lexerCompare(t *testing.T, n int, expected, actual []Token) { func lexerCompare(t *testing.T, n int, expected, actual []Token) {
if len(expected) != len(actual) { if len(expected) != len(actual) {
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual)) t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
+20 -13
View File
@@ -60,21 +60,31 @@ func replaceEnvVars(input []byte) ([]byte, error) {
end += begin + len(spanOpen) // make end relative to input, not begin end += begin + len(spanOpen) // make end relative to input, not begin
// get the name; if there is no name, skip it // get the name; if there is no name, skip it
envVarName := input[begin+len(spanOpen) : end] envString := input[begin+len(spanOpen) : end]
if len(envVarName) == 0 { if len(envString) == 0 {
offset = end + len(spanClose) offset = end + len(spanClose)
continue continue
} }
// split the string into a key and an optional default
envParts := strings.SplitN(string(envString), envVarDefaultDelimiter, 2)
// do a lookup for the env var, replace with the default if not found
envVarValue, found := os.LookupEnv(envParts[0])
if !found && len(envParts) == 2 {
envVarValue = envParts[1]
}
// get the value of the environment variable // get the value of the environment variable
envVarValue := []byte(os.ExpandEnv(os.Getenv(string(envVarName)))) // note that this causes one-level deep chaining
envVarBytes := []byte(envVarValue)
// splice in the value // splice in the value
input = append(input[:begin], input = append(input[:begin],
append(envVarValue, input[end+len(spanClose):]...)...) append(envVarBytes, input[end+len(spanClose):]...)...)
// continue at the end of the replacement // continue at the end of the replacement
offset = begin + len(envVarValue) offset = begin + len(envVarBytes)
} }
return input, nil return input, nil
} }
@@ -87,16 +97,10 @@ func allTokens(filename string, input []byte) ([]Token, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
l := new(lexer) tokens, err := Tokenize(input, filename)
err = l.load(bytes.NewReader(input))
if err != nil { if err != nil {
return nil, err return nil, err
} }
var tokens []Token
for l.next() {
l.token.File = filename
tokens = append(tokens, l.token)
}
return tokens, nil return tokens, nil
} }
@@ -554,4 +558,7 @@ func (s Segment) Directive() string {
// spanOpen and spanClose are used to bound spans that // spanOpen and spanClose are used to bound spans that
// contain the name of an environment variable. // contain the name of an environment variable.
var spanOpen, spanClose = []byte{'{', '$'}, []byte{'}'} var (
spanOpen, spanClose = []byte{'{', '$'}, []byte{'}'}
envVarDefaultDelimiter = ":"
)
+17
View File
@@ -478,6 +478,7 @@ func TestParseAll(t *testing.T) {
func TestEnvironmentReplacement(t *testing.T) { func TestEnvironmentReplacement(t *testing.T) {
os.Setenv("FOOBAR", "foobar") os.Setenv("FOOBAR", "foobar")
os.Setenv("CHAINED", "$FOOBAR")
for i, test := range []struct { for i, test := range []struct {
input string input string
@@ -523,6 +524,22 @@ func TestEnvironmentReplacement(t *testing.T) {
input: "{$FOOBAR}{$FOOBAR}", input: "{$FOOBAR}{$FOOBAR}",
expect: "foobarfoobar", expect: "foobarfoobar",
}, },
{
input: "{$CHAINED}",
expect: "$FOOBAR", // should not chain env expands
},
{
input: "{$FOO:default}",
expect: "default",
},
{
input: "foo{$BAR:bar}baz",
expect: "foobarbaz",
},
{
input: "foo{$BAR:$FOOBAR}baz",
expect: "foo$FOOBARbaz", // should not chain env expands
},
{ {
input: "{$FOOBAR", input: "{$FOOBAR",
expect: "{$FOOBAR", expect: "{$FOOBAR",
+8
View File
@@ -18,6 +18,7 @@ import (
"fmt" "fmt"
"net" "net"
"reflect" "reflect"
"sort"
"strconv" "strconv"
"strings" "strings"
"unicode" "unicode"
@@ -163,6 +164,13 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
sbaddrs = append(sbaddrs, a) sbaddrs = append(sbaddrs, a)
} }
// sort them by their first address (we know there will always be at least one)
// to avoid problems with non-deterministic ordering (makes tests flaky)
sort.Slice(sbaddrs, func(i, j int) bool {
return sbaddrs[i].addresses[0] < sbaddrs[j].addresses[0]
})
return sbaddrs return sbaddrs
} }
+74 -49
View File
@@ -29,6 +29,8 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
) )
@@ -73,8 +75,10 @@ func parseBind(h Helper) ([]ConfigValue, error) {
// load <paths...> // load <paths...>
// ca <acme_ca_endpoint> // ca <acme_ca_endpoint>
// ca_root <pem_file> // ca_root <pem_file>
// dns <provider_name> // dns <provider_name> [...]
// on_demand // on_demand
// eab <key_id> <mac_key>
// issuer <module_name> [...]
// } // }
// //
func parseTLS(h Helper) ([]ConfigValue, error) { func parseTLS(h Helper) ([]ConfigValue, error) {
@@ -84,6 +88,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
var certSelector caddytls.CustomCertSelectionPolicy var certSelector caddytls.CustomCertSelectionPolicy
var acmeIssuer *caddytls.ACMEIssuer var acmeIssuer *caddytls.ACMEIssuer
var internalIssuer *caddytls.InternalIssuer var internalIssuer *caddytls.InternalIssuer
var issuers []certmagic.Issuer
var onDemand bool var onDemand bool
for h.Next() { for h.Next() {
@@ -262,6 +267,42 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
} }
acmeIssuer.CA = arg[0] acmeIssuer.CA = arg[0]
case "eab":
arg := h.RemainingArgs()
if len(arg) != 2 {
return nil, h.ArgErr()
}
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
acmeIssuer.ExternalAccount = &acme.EAB{
KeyID: arg[0],
MACKey: arg[1],
}
case "issuer":
if !h.NextArg() {
return nil, h.ArgErr()
}
modName := h.Val()
mod, err := caddy.GetModule("tls.issuance." + modName)
if err != nil {
return nil, h.Errf("getting issuer module '%s': %v", modName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, h.Errf("issuer module '%s' is not a Caddyfile unmarshaler", mod.ID)
}
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
if err != nil {
return nil, err
}
issuer, ok := unm.(certmagic.Issuer)
if !ok {
return nil, h.Errf("module %s is not a certmagic.Issuer", mod.ID)
}
issuers = append(issuers, issuer)
case "dns": case "dns":
if !h.NextArg() { if !h.NextArg() {
return nil, h.ArgErr() return nil, h.ArgErr()
@@ -315,7 +356,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
} }
// begin building the final config values // begin building the final config values
var configVals []ConfigValue configVals := []ConfigValue{}
// certificate loaders // certificate loaders
if len(fileLoader) > 0 { if len(fileLoader) > 0 {
@@ -331,28 +372,25 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
}) })
} }
// issuer if len(issuers) > 0 && (acmeIssuer != nil || internalIssuer != nil) {
if acmeIssuer != nil && internalIssuer != nil { // some tls subdirectives are shortcuts that implicitly configure issuers, and the
// the logic to support this would be complex // user can also configure issuers explicitly using the issuer subdirective; the
return nil, h.Err("cannot use both ACME and internal issuers in same server block") // logic to support both would likely be complex, or at least unintuitive
return nil, h.Err("cannot mix issuer subdirective (explicit issuers) with other issuer-specific subdirectives (implicit issuers)")
} }
if acmeIssuer != nil { for _, issuer := range issuers {
// fill in global defaults, if configured
if email := h.Option("email"); email != nil && acmeIssuer.Email == "" {
acmeIssuer.Email = email.(string)
}
if acmeCA := h.Option("acme_ca"); acmeCA != nil && acmeIssuer.CA == "" {
acmeIssuer.CA = acmeCA.(string)
}
if caPemFile := h.Option("acme_ca_root"); caPemFile != nil {
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, caPemFile.(string))
}
configVals = append(configVals, ConfigValue{ configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer", Class: "tls.cert_issuer",
Value: acmeIssuer, Value: issuer,
}) })
} else if internalIssuer != nil { }
if acmeIssuer != nil {
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: disambiguateACMEIssuer(acmeIssuer),
})
}
if internalIssuer != nil {
configVals = append(configVals, ConfigValue{ configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer", Class: "tls.cert_issuer",
Value: internalIssuer, Value: internalIssuer,
@@ -466,36 +504,23 @@ func parseRespond(h Helper) (caddyhttp.MiddlewareHandler, error) {
func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) { func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) {
sr := new(caddyhttp.Subroute) sr := new(caddyhttp.Subroute)
for h.Next() { allResults, err := parseSegmentAsConfig(h)
for nesting := h.Nesting(); h.NextBlock(nesting); { if err != nil {
dir := h.Val() return nil, err
}
dirFunc, ok := registeredDirectives[dir] for _, result := range allResults {
if !ok { switch handler := result.Value.(type) {
return nil, h.Errf("unrecognized directive: %s", dir) case caddyhttp.Route:
} sr.Routes = append(sr.Routes, handler)
case caddyhttp.Subroute:
subHelper := h // directives which return a literal subroute instead of a route
subHelper.Dispenser = h.NewFromNextSegment() // means they intend to keep those handlers together without
// them being reordered; we're doing that anyway since we're in
results, err := dirFunc(subHelper) // the route directive, so just append its handlers
if err != nil { sr.Routes = append(sr.Routes, handler.Routes...)
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err) default:
} return nil, h.Errf("%s directive returned something other than an HTTP route or subroute: %#v (only handler directives can be used in routes)", result.directive, result.Value)
for _, result := range results {
switch handler := result.Value.(type) {
case caddyhttp.Route:
sr.Routes = append(sr.Routes, handler)
case caddyhttp.Subroute:
// directives which return a literal subroute instead of a route
// means they intend to keep those handlers together without
// them being reordered; we're doing that anyway since we're in
// the route directive, so just append its handlers
sr.Routes = append(sr.Routes, handler.Routes...)
default:
return nil, h.Errf("%s directive returned something other than an HTTP route or subroute: %#v (only handler directives can be used in routes)", dir, result.Value)
}
}
} }
} }
+42 -14
View File
@@ -41,6 +41,7 @@ var directiveOrder = []string{
"root", "root",
"header", "header",
"request_body",
"redir", "redir",
"rewrite", "rewrite",
@@ -55,13 +56,15 @@ var directiveOrder = []string{
"encode", "encode",
"templates", "templates",
// special routing directives // special routing & dispatching directives
"handle", "handle",
"handle_path", "handle_path",
"route", "route",
"push",
// handlers that typically respond to requests // handlers that typically respond to requests
"respond", "respond",
"metrics",
"reverse_proxy", "reverse_proxy",
"php_fastcgi", "php_fastcgi",
"file_server", "file_server",
@@ -100,20 +103,11 @@ func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
return nil, h.ArgErr() return nil, h.ArgErr()
} }
matcherSet, ok, err := h.MatcherToken() matcherSet, err := h.ExtractMatcherSet()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if ok {
// strip matcher token; we don't need to
// use the return value here because a
// new dispenser should have been made
// solely for this directive's tokens,
// with no other uses of same slice
h.Dispenser.Delete()
}
h.Dispenser.Reset() // pretend this lookahead never happened
val, err := setupFunc(h) val, err := setupFunc(h)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -198,7 +192,12 @@ func (h Helper) ExtractMatcherSet() (caddy.ModuleMap, error) {
return nil, err return nil, err
} }
if hasMatcher { if hasMatcher {
h.Dispenser.Delete() // strip matcher token // strip matcher token; we don't need to
// use the return value here because a
// new dispenser should have been made
// solely for this directive's tokens,
// with no other uses of same slice
h.Dispenser.Delete()
} }
h.Dispenser.Reset() // pretend this lookahead never happened h.Dispenser.Reset() // pretend this lookahead never happened
return matcherSet, nil return matcherSet, nil
@@ -268,9 +267,26 @@ func (h Helper) NewBindAddresses(addrs []string) []ConfigValue {
// are themselves treated as directives, from which a subroute is built // are themselves treated as directives, from which a subroute is built
// and returned. // and returned.
func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) { func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
allResults, err := parseSegmentAsConfig(h)
if err != nil {
return nil, err
}
return buildSubroute(allResults, h.groupCounter)
}
// parseSegmentAsConfig parses the segment such that its subdirectives
// are themselves treated as directives, including named matcher definitions,
// and the raw Config structs are returned.
func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
var allResults []ConfigValue var allResults []ConfigValue
for h.Next() { for h.Next() {
// don't allow non-matcher args on the first line
if h.NextArg() {
return nil, h.ArgErr()
}
// slice the linear list of tokens into top-level segments // slice the linear list of tokens into top-level segments
var segments []caddyfile.Segment var segments []caddyfile.Segment
for nesting := h.Nesting(); h.NextBlock(nesting); { for nesting := h.Nesting(); h.NextBlock(nesting); {
@@ -285,13 +301,17 @@ func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
} }
// find and extract any embedded matcher definitions in this scope // find and extract any embedded matcher definitions in this scope
for i, seg := range segments { for i := 0; i < len(segments); i++ {
seg := segments[i]
if strings.HasPrefix(seg.Directive(), matcherPrefix) { if strings.HasPrefix(seg.Directive(), matcherPrefix) {
// parse, then add the matcher to matcherDefs
err := parseMatcherDefinitions(caddyfile.NewDispenser(seg), matcherDefs) err := parseMatcherDefinitions(caddyfile.NewDispenser(seg), matcherDefs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// remove the matcher segment (consumed), then step back the loop
segments = append(segments[:i], segments[i+1:]...) segments = append(segments[:i], segments[i+1:]...)
i--
} }
} }
@@ -318,7 +338,7 @@ func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
} }
} }
return buildSubroute(allResults, h.groupCounter) return allResults, nil
} }
// ConfigValue represents a value to be added to the final // ConfigValue represents a value to be added to the final
@@ -385,6 +405,14 @@ func sortRoutes(routes []ConfigValue) {
if len(jPM) > 0 { if len(jPM) > 0 {
jPathLen = len(jPM[0]) 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 {
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
}
// sort with the most-specific (longest) path first
return iPathLen > jPathLen return iPathLen > jPathLen
}) })
} }
+91 -30
View File
@@ -99,6 +99,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
"{tls_client_issuer}", "{http.request.tls.client.issuer}", "{tls_client_issuer}", "{http.request.tls.client.issuer}",
"{tls_client_serial}", "{http.request.tls.client.serial}", "{tls_client_serial}", "{http.request.tls.client.serial}",
"{tls_client_subject}", "{http.request.tls.client.subject}", "{tls_client_subject}", "{http.request.tls.client.subject}",
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
) )
// these are placeholders that allow a user-defined final // these are placeholders that allow a user-defined final
@@ -172,6 +173,15 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
if err != nil { if err != nil {
return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err) return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err)
} }
// As a special case, we want "handle_path" to be sorted
// at the same level as "handle", so we force them to use
// the same directive name after their parsing is complete.
// See https://github.com/caddyserver/caddy/issues/3675#issuecomment-678042377
if dir == "handle_path" {
dir = "handle"
}
for _, result := range results { for _, result := range results {
result.directive = dir result.directive = dir
sb.pile[result.Class] = append(sb.pile[result.Class], result) sb.pile[result.Class] = append(sb.pile[result.Class], result)
@@ -208,13 +218,6 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
return nil, warnings, err return nil, warnings, err
} }
// if experimental HTTP/3 is enabled, enable it on each server
if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
for _, srv := range httpApp.Servers {
srv.ExperimentalHTTP3 = true
}
}
// extract any custom logs, and enforce configured levels // extract any custom logs, and enforce configured levels
var customLogs []namedCustomLog var customLogs []namedCustomLog
var hasDefaultLog bool var hasDefaultLog bool
@@ -261,12 +264,8 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
storageCvtr.(caddy.Module).CaddyModule().ID.Name(), storageCvtr.(caddy.Module).CaddyModule().ID.Name(),
&warnings) &warnings)
} }
if adminConfig, ok := options["admin"].(string); ok && adminConfig != "" { if adminConfig, ok := options["admin"].(*caddy.AdminConfig); ok && adminConfig != nil {
if adminConfig == "off" { cfg.Admin = adminConfig
cfg.Admin = &caddy.AdminConfig{Disabled: true}
} else {
cfg.Admin = &caddy.AdminConfig{Listen: adminConfig}
}
} }
if len(customLogs) > 0 { if len(customLogs) > 0 {
if cfg.Logging == nil { if cfg.Logging == nil {
@@ -305,23 +304,54 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
} }
for _, segment := range serverBlocks[0].block.Segments { for _, segment := range serverBlocks[0].block.Segments {
dir := segment.Directive() opt := segment.Directive()
var val interface{} var val interface{}
var err error var err error
disp := caddyfile.NewDispenser(segment) disp := caddyfile.NewDispenser(segment)
dirFunc, ok := registeredGlobalOptions[dir] optFunc, ok := registeredGlobalOptions[opt]
if !ok { if !ok {
tkn := segment[0] tkn := segment[0]
return nil, fmt.Errorf("%s:%d: unrecognized global option: %s", tkn.File, tkn.Line, dir) return nil, fmt.Errorf("%s:%d: unrecognized global option: %s", tkn.File, tkn.Line, opt)
} }
val, err = dirFunc(disp) val, err = optFunc(disp)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err) return nil, fmt.Errorf("parsing caddyfile tokens for '%s': %v", opt, err)
} }
options[dir] = val // As a special case, fold multiple "servers" options together
// in an array instead of overwriting a possible existing value
if opt == "servers" {
existingOpts, ok := options[opt].([]serverOptions)
if !ok {
existingOpts = []serverOptions{}
}
serverOpts, ok := val.(serverOptions)
if !ok {
return nil, fmt.Errorf("unexpected type from 'servers' global options")
}
options[opt] = append(existingOpts, serverOpts)
continue
}
options[opt] = val
}
// If we got "servers" options, we'll sort them by their listener address
if serverOpts, ok := options["servers"].([]serverOptions); ok {
sort.Slice(serverOpts, func(i, j int) bool {
return len(serverOpts[i].ListenerAddress) > len(serverOpts[j].ListenerAddress)
})
// Reject the config if there are duplicate listener address
seen := make(map[string]bool)
for _, entry := range serverOpts {
if _, alreadySeen := seen[entry.ListenerAddress]; alreadySeen {
return nil, fmt.Errorf("cannot have 'servers' global options with duplicate listener addresses: %s", entry.ListenerAddress)
}
seen[entry.ListenerAddress] = true
}
} }
return serverBlocks[1:], nil return serverBlocks[1:], nil
@@ -420,6 +450,15 @@ func (st *ServerType) serversFromPairings(
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
autoHTTPSWillAddConnPolicy := autoHTTPS != "off" autoHTTPSWillAddConnPolicy := autoHTTPS != "off"
// if a catch-all server block (one which accepts all hostnames) exists in this pairing,
// we need to know that so that we can configure logs properly (see #3878)
var catchAllSblockExists bool
for _, sblock := range p.serverBlocks {
if len(sblock.hostsFromKeys(false)) == 0 {
catchAllSblockExists = true
}
}
// create a subroute for each site in the server block // create a subroute for each site in the server block
for _, sblock := range p.serverBlocks { for _, sblock := range p.serverBlocks {
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock) matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
@@ -450,12 +489,12 @@ func (st *ServerType) serversFromPairings(
} }
} else { } else {
cp.DefaultSNI = defaultSNI cp.DefaultSNI = defaultSNI
hasCatchAllTLSConnPolicy = true
} }
// only append this policy if it actually changes something // only append this policy if it actually changes something
if !cp.SettingsEmpty() { if !cp.SettingsEmpty() {
srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp) srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
hasCatchAllTLSConnPolicy = len(hosts) == 0
} }
} }
} }
@@ -533,13 +572,13 @@ func (st *ServerType) serversFromPairings(
} else { } else {
// map each host to the user's desired logger name // map each host to the user's desired logger name
for _, h := range sblockLogHosts { for _, h := range sblockLogHosts {
// if the custom logger name is non-empty, add it to // if the custom logger name is non-empty, add it to the map;
// the map; otherwise, only map to an empty logger // otherwise, only map to an empty logger name if this or
// name if the server block has a catch-all host (in // another site block on this server has a catch-all host (in
// which case only requests with mapped hostnames will // which case only requests with mapped hostnames will be
// be access-logged, so it'll be necessary to add them // access-logged, so it'll be necessary to add them to the
// to the map even if they use default logger) // map even if they use default logger)
if ncl.name != "" || len(hosts) == 0 { if ncl.name != "" || catchAllSblockExists {
if srv.Logs.LoggerNames == nil { if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string) srv.Logs.LoggerNames = make(map[string]string)
} }
@@ -596,6 +635,11 @@ func (st *ServerType) serversFromPairings(
servers[fmt.Sprintf("srv%d", i)] = srv servers[fmt.Sprintf("srv%d", i)] = srv
} }
err := applyServerOptions(servers, options, warnings)
if err != nil {
return nil, err
}
return servers, nil return servers, nil
} }
@@ -657,9 +701,15 @@ func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock,
return nil return nil
} }
// consolidateConnPolicies removes empty TLS connection policies and combines // consolidateConnPolicies sorts any catch-all policy to the end, removes empty TLS connection
// equivalent ones for a cleaner overall output. // policies, and combines equivalent ones for a cleaner overall output.
func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.ConnectionPolicies, error) { func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.ConnectionPolicies, error) {
// catch-all policies (those without any matcher) should be at the
// end, otherwise it nullifies any more specific policies
sort.SliceStable(cps, func(i, j int) bool {
return cps[j].MatchersRaw == nil && cps[i].MatchersRaw != nil
})
for i := 0; i < len(cps); i++ { for i := 0; i < len(cps); i++ {
// compare it to the others // compare it to the others
for j := 0; j < len(cps); j++ { for j := 0; j < len(cps); j++ {
@@ -852,7 +902,18 @@ func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subro
// root directives would overwrite previously-matched ones; they should not cascade // root directives would overwrite previously-matched ones; they should not cascade
"root": {}, "root": {},
} }
for meDir, info := range mutuallyExclusiveDirs {
// we need to deterministically loop over each of these directives
// in order to keep the group numbers consistent
keys := make([]string, 0, len(mutuallyExclusiveDirs))
for k := range mutuallyExclusiveDirs {
keys = append(keys, k)
}
sort.Strings(keys)
for _, meDir := range keys {
info := mutuallyExclusiveDirs[meDir]
// see how many instances of the directive there are // see how many instances of the directive there are
for _, r := range routes { for _, r := range routes {
if r.directive == meDir { if r.directive == meDir {
@@ -164,6 +164,58 @@ func TestGlobalOptions(t *testing.T) {
expectWarn: false, expectWarn: false,
expectError: true, expectError: true,
}, },
{
input: `
{
admin {
enforce_origin
origins 192.168.1.1:2020 127.0.0.1:2020
}
}
:80
`,
expectWarn: false,
expectError: false,
},
{
input: `
{
admin 127.0.0.1:2020 {
enforce_origin
origins 192.168.1.1:2020 127.0.0.1:2020
}
}
:80
`,
expectWarn: false,
expectError: false,
},
{
input: `
{
admin 192.168.1.1:2020 127.0.0.1:2020 {
enforce_origin
origins 192.168.1.1:2020 127.0.0.1:2020
}
}
:80
`,
expectWarn: false,
expectError: true,
},
{
input: `
{
admin off {
enforce_origin
origins 192.168.1.1:2020 127.0.0.1:2020
}
}
:80
`,
expectWarn: false,
expectError: true,
},
} { } {
adapter := caddyfile.Adapter{ adapter := caddyfile.Adapter{
+68 -11
View File
@@ -20,6 +20,8 @@ import (
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
) )
func init() { func init() {
@@ -34,12 +36,14 @@ func init() {
RegisterGlobalOption("acme_ca_root", parseOptSingleString) RegisterGlobalOption("acme_ca_root", parseOptSingleString)
RegisterGlobalOption("acme_dns", parseOptSingleString) RegisterGlobalOption("acme_dns", parseOptSingleString)
RegisterGlobalOption("acme_eab", parseOptACMEEAB) RegisterGlobalOption("acme_eab", parseOptACMEEAB)
RegisterGlobalOption("cert_issuer", parseOptCertIssuer)
RegisterGlobalOption("email", parseOptSingleString) RegisterGlobalOption("email", parseOptSingleString)
RegisterGlobalOption("admin", parseOptAdmin) RegisterGlobalOption("admin", parseOptAdmin)
RegisterGlobalOption("on_demand_tls", parseOptOnDemand) RegisterGlobalOption("on_demand_tls", parseOptOnDemand)
RegisterGlobalOption("local_certs", parseOptTrue) RegisterGlobalOption("local_certs", parseOptTrue)
RegisterGlobalOption("key_type", parseOptSingleString) RegisterGlobalOption("key_type", parseOptSingleString)
RegisterGlobalOption("auto_https", parseOptAutoHTTPS) RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
RegisterGlobalOption("servers", parseServerOptions)
} }
func parseOptTrue(d *caddyfile.Dispenser) (interface{}, error) { func parseOptTrue(d *caddyfile.Dispenser) (interface{}, error) {
@@ -182,7 +186,7 @@ func parseOptStorage(d *caddyfile.Dispenser) (interface{}, error) {
} }
func parseOptACMEEAB(d *caddyfile.Dispenser) (interface{}, error) { func parseOptACMEEAB(d *caddyfile.Dispenser) (interface{}, error) {
eab := new(caddytls.ExternalAccountBinding) eab := new(acme.EAB)
for d.Next() { for d.Next() {
if d.NextArg() { if d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
@@ -195,11 +199,11 @@ func parseOptACMEEAB(d *caddyfile.Dispenser) (interface{}, error) {
} }
eab.KeyID = d.Val() eab.KeyID = d.Val()
case "hmac": case "mac_key":
if !d.NextArg() { if !d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
} }
eab.HMAC = d.Val() eab.MACKey = d.Val()
default: default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val()) return nil, d.Errf("unrecognized parameter '%s'", d.Val())
@@ -209,6 +213,33 @@ func parseOptACMEEAB(d *caddyfile.Dispenser) (interface{}, error) {
return eab, nil return eab, nil
} }
func parseOptCertIssuer(d *caddyfile.Dispenser) (interface{}, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
if !d.Next() { // get issuer module name
return nil, d.ArgErr()
}
modName := d.Val()
mod, err := caddy.GetModule("tls.issuance." + modName)
if err != nil {
return nil, d.Errf("getting issuer module '%s': %v", modName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, d.Errf("issuer module '%s' is not a Caddyfile unmarshaler", mod.ID)
}
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
if err != nil {
return nil, err
}
iss, ok := unm.(certmagic.Issuer)
if !ok {
return nil, d.Errf("module %s is not a certmagic.Issuer", mod.ID)
}
return iss, nil
}
func parseOptSingleString(d *caddyfile.Dispenser) (interface{}, error) { func parseOptSingleString(d *caddyfile.Dispenser) (interface{}, error) {
d.Next() // consume parameter name d.Next() // consume parameter name
if !d.Next() { if !d.Next() {
@@ -222,17 +253,39 @@ func parseOptSingleString(d *caddyfile.Dispenser) (interface{}, error) {
} }
func parseOptAdmin(d *caddyfile.Dispenser) (interface{}, error) { func parseOptAdmin(d *caddyfile.Dispenser) (interface{}, error) {
if d.Next() { adminCfg := new(caddy.AdminConfig)
var listenAddress string for d.Next() {
if !d.AllArgs(&listenAddress) { if d.NextArg() {
return "", d.ArgErr() 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 listenAddress == "" { for nesting := d.Nesting(); d.NextBlock(nesting); {
listenAddress = caddy.DefaultAdminListen switch d.Val() {
case "enforce_origin":
adminCfg.EnforceOrigin = true
case "origins":
adminCfg.Origins = d.RemainingArgs()
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
}
} }
return listenAddress, nil
} }
return "", nil if adminCfg.Listen == "" && !adminCfg.Disabled {
adminCfg.Listen = caddy.DefaultAdminListen
}
return adminCfg, nil
} }
func parseOptOnDemand(d *caddyfile.Dispenser) (interface{}, error) { func parseOptOnDemand(d *caddyfile.Dispenser) (interface{}, error) {
@@ -309,3 +362,7 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser) (interface{}, error) {
} }
return val, nil return val, nil
} }
func parseServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
return unmarshalCaddyfileServerOptions(d)
}
+235
View File
@@ -0,0 +1,235 @@
// 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 httpcaddyfile
import (
"encoding/json"
"fmt"
"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
type serverOptions struct {
// If set, will only apply these options to servers that contain a
// listener address that matches exactly. If empty, will apply to all
// servers that were not already matched by another serverOptions.
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
}
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
serverOpts := serverOptions{}
for d.Next() {
if d.NextArg() {
serverOpts.ListenerAddress = d.Val()
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); {
mod, err := caddy.GetModule("caddy.listeners." + d.Val())
if err != nil {
return nil, fmt.Errorf("finding listener module '%s': %v", d.Val(), err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, fmt.Errorf("listener module '%s' is not a Caddyfile unmarshaler", mod)
}
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
if err != nil {
return nil, err
}
listenerWrapper, ok := unm.(caddy.ListenerWrapper)
if !ok {
return nil, fmt.Errorf("module %s is not a listener wrapper", mod)
}
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 "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())
}
}
}
return serverOpts, nil
}
// applyServerOptions sets the server options on the appropriate servers
func applyServerOptions(
servers map[string]*caddyhttp.Server,
options map[string]interface{},
warnings *[]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 {
// find the options that apply to this server
opts := func() *serverOptions {
for _, entry := range serverOpts {
if entry.ListenerAddress == "" {
return &entry
}
for _, listener := range server.Listen {
if entry.ListenerAddress == listener {
return &entry
}
}
}
return nil
}()
// if none apply, then move to the next server
if opts == nil {
continue
}
// set all the options
server.ListenerWrappersRaw = opts.ListenerWrappersRaw
server.ReadTimeout = opts.ReadTimeout
server.ReadHeaderTimeout = opts.ReadHeaderTimeout
server.WriteTimeout = opts.WriteTimeout
server.IdleTimeout = opts.IdleTimeout
server.MaxHeaderBytes = opts.MaxHeaderBytes
server.AllowH2C = opts.AllowH2C
server.ExperimentalHTTP3 = opts.ExperimentalHTTP3
server.StrictSNIHost = opts.StrictSNIHost
}
return nil
}
+310 -195
View File
@@ -21,12 +21,14 @@ import (
"reflect" "reflect"
"sort" "sort"
"strconv" "strconv"
"strings"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
) )
func (st ServerType) buildTLSApp( func (st ServerType) buildTLSApp(
@@ -74,163 +76,148 @@ func (st ServerType) buildTLSApp(
} }
} }
// a catch-all automation policy is used as a "default" for all subjects that
// don't have custom configuration explicitly associated with them; this
// is only to add if the global settings or defaults are non-empty
catchAllAP, err := newBaseAutomationPolicy(options, warnings, false) catchAllAP, err := newBaseAutomationPolicy(options, warnings, false)
if err != nil { if err != nil {
return nil, warnings, err return nil, warnings, err
} }
if catchAllAP != nil {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
}
for _, p := range pairings { for _, p := range pairings {
for _, sblock := range p.serverBlocks { for _, sblock := range p.serverBlocks {
// get values that populate an automation policy for this block // get values that populate an automation policy for this block
var ap *caddytls.AutomationPolicy ap, err := newBaseAutomationPolicy(options, warnings, true)
if err != nil {
return nil, warnings, err
}
sblockHosts := sblock.hostsFromKeys(false) sblockHosts := sblock.hostsFromKeys(false)
if len(sblockHosts) == 0 { if len(sblockHosts) == 0 && catchAllAP != nil {
ap = catchAllAP ap = catchAllAP
} }
// on-demand tls // on-demand tls
if _, ok := sblock.pile["tls.on_demand"]; ok { if _, ok := sblock.pile["tls.on_demand"]; ok {
if ap == nil {
var err error
ap, err = newBaseAutomationPolicy(options, warnings, true)
if err != nil {
return nil, warnings, err
}
}
ap.OnDemand = true ap.OnDemand = true
} }
// certificate issuers // certificate issuers
if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok { if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
var issuers []certmagic.Issuer
for _, issuerVal := range issuerVals { for _, issuerVal := range issuerVals {
issuer := issuerVal.Value.(certmagic.Issuer) ap.Issuers = append(ap.Issuers, issuerVal.Value.(certmagic.Issuer))
if ap == nil { }
var err error if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) {
ap, err = newBaseAutomationPolicy(options, warnings, true) 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)
if err != nil {
return nil, warnings, err
}
}
if ap == catchAllAP && !reflect.DeepEqual(ap.Issuer, issuer) {
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.Issuer, issuer)
}
ap.Issuer = issuer
} }
} }
// custom bind host // custom bind host
for _, cfgVal := range sblock.pile["bind"] { for _, cfgVal := range sblock.pile["bind"] {
// either an existing issuer is already configured (and thus, ap is not for _, iss := range ap.Issuers {
// nil), or we need to configure an issuer, so we need ap to be non-nil // if an issuer was already configured and it is NOT an ACME issuer,
if ap == nil { // skip, since we intend to adjust only ACME issuers; ensure we
ap, err = newBaseAutomationPolicy(options, warnings, true) // include any issuer that embeds/wraps an underlying ACME issuer
if err != nil { var acmeIssuer *caddytls.ACMEIssuer
return nil, warnings, err if acmeWrapper, ok := iss.(acmeCapable); ok {
acmeIssuer = acmeWrapper.GetACMEIssuer()
}
if acmeIssuer == nil {
continue
} }
}
// if an issuer was already configured and it is NOT an ACME // proceed to configure the ACME issuer's bind host, without
// issuer, skip, since we intend to adjust only ACME issuers // overwriting any existing settings
var acmeIssuer *caddytls.ACMEIssuer if acmeIssuer.Challenges == nil {
if ap.Issuer != nil { acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
var ok bool }
if acmeIssuer, ok = ap.Issuer.(*caddytls.ACMEIssuer); !ok { if acmeIssuer.Challenges.BindHost == "" {
break // only binding to one host is supported
var bindHost string
if bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 {
bindHost = bindHosts[0]
}
acmeIssuer.Challenges.BindHost = bindHost
} }
} }
// proceed to configure the ACME issuer's bind host, without
// overwriting any existing settings
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.BindHost == "" {
// only binding to one host is supported
var bindHost string
if bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 {
bindHost = bindHosts[0]
}
acmeIssuer.Challenges.BindHost = bindHost
}
ap.Issuer = acmeIssuer // we'll encode it later
} }
if ap != nil { // first make sure this block is allowed to create an automation policy;
if ap.Issuer != nil { // doing so is forbidden if it has a key with no host (i.e. ":443")
// encode issuer now that it's all set up // and if there is a different server block that also has a key with no
issuerName := ap.Issuer.(caddy.Module).CaddyModule().ID.Name() // host -- since a key with no host matches any host, we need its
ap.IssuerRaw = caddyconfig.JSONModuleObject(ap.Issuer, "module", issuerName, &warnings) // 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
}
}
// first make sure this block is allowed to create an automation policy; // associate our new automation policy with this server block's hosts
// doing so is forbidden if it has a key with no host (i.e. ":443") ap.Subjects = sblockHosts
// and if there is a different server block that also has a key with no sort.Strings(ap.Subjects) // solely for deterministic test results
// 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 // if a combination of public and internal names were given
// host filter, which is indistinguishable between the two server blocks // for this same server block and no issuer was specified, we
// because automation is not done in the context of a particular server... // need to separate them out in the automation policies so
// this is an example of a poor mapping from Caddyfile to JSON but that's // that the internal names can use the internal issuer and
// the least-leaky abstraction I could figure out // the other names can use the default/public/ACME issuer
if len(sblockHosts) == 0 { var ap2 *caddytls.AutomationPolicy
if serverBlocksWithTLSHostlessKey > 1 { if len(ap.Issuers) == 0 {
// this server block and at least one other has a key with no host, var internal, external []string
// making the two indistinguishable; it is misleading to define such for _, s := range ap.Subjects {
// a policy within one server block since it actually will apply to if !certmagic.SubjectQualifiesForCert(s) {
// others as well return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s)
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 { // we don't use certmagic.SubjectQualifiesForPublicCert() because of one nuance:
// this server block has a key with no hosts, but there is not yet // names like *.*.tld that may not qualify for a public certificate are actually
// a catch-all automation policy (probably because no global options // fine when used with OnDemand, since OnDemand (currently) does not obtain
// were set), so this one becomes it // wildcards (if it ever does, there will be a separate config option to enable
catchAllAP = ap // it that we would need to check here) since the hostname is known at handshake;
// and it is unexpected to switch to internal issuer when the user wants to get
// regular certificates on-demand for a class of certs like *.*.tld.
if !certmagic.SubjectIsIP(s) && !certmagic.SubjectIsInternal(s) && (strings.Count(s, "*.") < 2 || ap.OnDemand) {
external = append(external, s)
} else {
internal = append(internal, s)
} }
} }
if len(external) > 0 && len(internal) > 0 {
// associate our new automation policy with this server block's hosts, ap.Subjects = external
// unless, of course, the server block has a key with no hosts, in which apCopy := *ap
// case its automation policy becomes or blends with the default/global ap2 = &apCopy
// automation policy because, of necessity, it applies to all hostnames ap2.Subjects = internal
// (i.e. it has no Subjects filter) -- in that case, we'll append it last ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
if ap != catchAllAP {
ap.Subjects = sblockHosts
// if a combination of public and internal names were given
// for this same server block and no issuer was specified, we
// need to separate them out in the automation policies so
// that the internal names can use the internal issuer and
// the other names can use the default/public/ACME issuer
var ap2 *caddytls.AutomationPolicy
if ap.Issuer == nil {
var internal, external []string
for _, s := range ap.Subjects {
if certmagic.SubjectQualifiesForPublicCert(s) {
external = append(external, s)
} else {
internal = append(internal, s)
}
}
if len(external) > 0 && len(internal) > 0 {
ap.Subjects = external
apCopy := *ap
ap2 = &apCopy
ap2.Subjects = internal
ap2.IssuerRaw = caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)
}
}
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
if ap2 != nil {
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap2)
}
} }
} }
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
if ap2 != nil {
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap2)
}
// certificate loaders // certificate loaders
if clVals, ok := sblock.pile["tls.cert_loader"]; ok { if clVals, ok := sblock.pile["tls.cert_loader"]; ok {
@@ -286,7 +273,7 @@ func (st ServerType) buildTLSApp(
// get internal certificates by default rather than ACME // get internal certificates by default rather than ACME
var al caddytls.AutomateLoader var al caddytls.AutomateLoader
internalAP := &caddytls.AutomationPolicy{ internalAP := &caddytls.AutomationPolicy{
IssuerRaw: json.RawMessage(`{"module":"internal"}`), IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
} }
for h := range hostsSharedWithHostlessKey { for h := range hostsSharedWithHostlessKey {
al = append(al, h) al = append(al, h)
@@ -304,23 +291,54 @@ func (st ServerType) buildTLSApp(
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, internalAP) tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, internalAP)
} }
// if there is a global/catch-all automation policy, ensure it goes last // if there are any global options set for issuers (ACME ones in particular), make sure they
if catchAllAP != nil { // take effect in every automation policy that does not have any issuers
// first, encode its issuer, if there is one if tlsApp.Automation != nil {
if catchAllAP.Issuer != nil { globalEmail := options["email"]
issuerName := catchAllAP.Issuer.(caddy.Module).CaddyModule().ID.Name() globalACMECA := options["acme_ca"]
catchAllAP.IssuerRaw = caddyconfig.JSONModuleObject(catchAllAP.Issuer, "module", issuerName, &warnings) globalACMECARoot := options["acme_ca_root"]
} globalACMEDNS := options["acme_dns"]
globalACMEEAB := options["acme_eab"]
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil
if hasGlobalACMEDefaults {
for _, ap := range tlsApp.Automation.Policies {
if len(ap.Issuers) == 0 {
acme, zerosslACME := new(caddytls.ACMEIssuer), new(caddytls.ACMEIssuer)
zerossl := &caddytls.ZeroSSLIssuer{ACMEIssuer: zerosslACME}
ap.Issuers = []certmagic.Issuer{acme, zerossl} // TODO: keep this in sync with Caddy's other issuer defaults elsewhere, like in caddytls/automation.go (DefaultIssuers).
// then append it to the end of the policies list // if a non-ZeroSSL endpoint is specified, we assume we can't use the ZeroSSL issuer successfully
if tlsApp.Automation == nil { if globalACMECA != nil && !strings.Contains(globalACMECA.(string), "zerossl") {
tlsApp.Automation = new(caddytls.AutomationConfig) ap.Issuers = []certmagic.Issuer{acme}
}
}
}
} }
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
} }
// do a little verification & cleanup // finalize and verify policies; do cleanup
if tlsApp.Automation != nil { if tlsApp.Automation != nil {
for i, ap := range tlsApp.Automation.Policies {
// ensure all issuers have global defaults filled in
for j, issuer := range ap.Issuers {
err := fillInGlobalACMEDefaults(issuer, options)
if err != nil {
return nil, warnings, fmt.Errorf("filling in global issuer defaults for AP %d, issuer %d: %v", i, j, err)
}
}
// encode all issuer values we created, so they will be rendered in the output
if len(ap.Issuers) > 0 && ap.IssuersRaw == nil {
for _, iss := range ap.Issuers {
issuerName := iss.(caddy.Module).CaddyModule().ID.Name()
ap.IssuersRaw = append(ap.IssuersRaw, caddyconfig.JSONModuleObject(iss, "module", issuerName, &warnings))
}
}
}
// consolidate automation policies that are the exact same
tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
// ensure automation policies don't overlap subjects (this should be // ensure automation policies don't overlap subjects (this should be
// an error at provision-time as well, but catch it in the adapt phase // an error at provision-time as well, but catch it in the adapt phase
// for convenience) // for convenience)
@@ -334,29 +352,74 @@ func (st ServerType) buildTLSApp(
} }
} }
// consolidate automation policies that are the exact same // if nothing remains, remove any excess values to clean up the resulting config
tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies) if len(tlsApp.Automation.Policies) == 0 {
tlsApp.Automation.Policies = nil
}
if reflect.DeepEqual(tlsApp.Automation, new(caddytls.AutomationConfig)) {
tlsApp.Automation = nil
}
} }
return tlsApp, warnings, nil return tlsApp, warnings, nil
} }
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interface{}) error {
acmeWrapper, ok := issuer.(acmeCapable)
if !ok {
return nil
}
acmeIssuer := acmeWrapper.GetACMEIssuer()
if acmeIssuer == nil {
return nil
}
globalEmail := options["email"]
globalACMECA := options["acme_ca"]
globalACMECARoot := options["acme_ca_root"]
globalACMEDNS := options["acme_dns"]
globalACMEEAB := options["acme_eab"]
if globalEmail != nil && acmeIssuer.Email == "" {
acmeIssuer.Email = globalEmail.(string)
}
if globalACMECA != nil && acmeIssuer.CA == "" {
acmeIssuer.CA = globalACMECA.(string)
}
if globalACMECARoot != nil && !sliceContains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
}
if globalACMEDNS != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) {
provName := globalACMEDNS.(string)
dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
if err != nil {
return fmt.Errorf("getting DNS provider module named '%s': %v", provName, err)
}
acmeIssuer.Challenges = &caddytls.ChallengesConfig{
DNS: &caddytls.DNSChallengeConfig{
ProviderRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "name", provName, nil),
},
}
}
if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB)
}
return nil
}
// newBaseAutomationPolicy returns a new TLS automation policy that gets // newBaseAutomationPolicy returns a new TLS automation policy that gets
// its values from the global options map. It should be used as the base // its values from the global options map. It should be used as the base
// for any other automation policies. A nil policy (and no error) will be // for any other automation policies. A nil policy (and no error) will be
// returned if there are no default/global options. However, if always is // returned if there are no default/global options. However, if always is
// true, a non-nil value will always be returned (unless there is an error). // true, a non-nil value will always be returned (unless there is an error).
func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) { func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
acmeCA, hasACMECA := options["acme_ca"] issuer, hasIssuer := options["cert_issuer"]
acmeCARoot, hasACMECARoot := options["acme_ca_root"] _, hasLocalCerts := options["local_certs"]
acmeDNS, hasACMEDNS := options["acme_dns"]
acmeEAB, hasACMEEAB := options["acme_eab"]
email, hasEmail := options["email"]
localCerts, hasLocalCerts := options["local_certs"]
keyType, hasKeyType := options["key_type"] keyType, hasKeyType := options["key_type"]
hasGlobalAutomationOpts := hasACMECA || hasACMECARoot || hasACMEDNS || hasACMEEAB || hasEmail || hasLocalCerts || hasKeyType hasGlobalAutomationOpts := hasIssuer || hasLocalCerts || hasKeyType
// if there are no global options related to automation policies // if there are no global options related to automation policies
// set, then we can just return right away // set, then we can just return right away
@@ -368,57 +431,64 @@ func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddycon
} }
ap := new(caddytls.AutomationPolicy) ap := new(caddytls.AutomationPolicy)
if hasKeyType {
ap.KeyType = keyType.(string)
}
if localCerts != nil { if hasIssuer && hasLocalCerts {
// internal issuer enabled trumps any ACME configurations; useful in testing return nil, fmt.Errorf("global options are ambiguous: local_certs is confusing when combined with cert_issuer, because local_certs is also a specific kind of issuer")
ap.Issuer = new(caddytls.InternalIssuer) // we'll encode it later }
} else {
if acmeCA == nil { if hasIssuer {
acmeCA = "" ap.Issuers = []certmagic.Issuer{issuer.(certmagic.Issuer)}
} } else if hasLocalCerts {
if email == nil { ap.Issuers = []certmagic.Issuer{new(caddytls.InternalIssuer)}
email = ""
}
mgr := &caddytls.ACMEIssuer{
CA: acmeCA.(string),
Email: email.(string),
}
if acmeDNS != nil {
provName := acmeDNS.(string)
dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
if err != nil {
return nil, fmt.Errorf("getting DNS provider module named '%s': %v", provName, err)
}
mgr.Challenges = &caddytls.ChallengesConfig{
DNS: &caddytls.DNSChallengeConfig{
ProviderRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "name", provName, &warnings),
},
}
}
if acmeCARoot != nil {
mgr.TrustedRootsPEMFiles = []string{acmeCARoot.(string)}
}
if acmeEAB != nil {
mgr.ExternalAccount = acmeEAB.(*caddytls.ExternalAccountBinding)
}
if keyType != nil {
ap.KeyType = keyType.(string)
}
ap.Issuer = mgr // we'll encode it later
} }
return ap, nil return ap, nil
} }
// disambiguateACMEIssuer returns an issuer based on the properties of acmeIssuer.
// If acmeIssuer implicitly configures a certain kind of ACMEIssuer (for example,
// ZeroSSL), the proper wrapper over acmeIssuer will be returned instead.
func disambiguateACMEIssuer(acmeIssuer *caddytls.ACMEIssuer) certmagic.Issuer {
// as a special case, we integrate with ZeroSSL's ACME endpoint if it looks like an
// implicit ZeroSSL configuration (this requires a wrapper type over ACMEIssuer
// because of the EAB generation; if EAB is provided, we can use plain ACMEIssuer)
if strings.Contains(acmeIssuer.CA, "acme.zerossl.com") && acmeIssuer.ExternalAccount == nil {
return &caddytls.ZeroSSLIssuer{ACMEIssuer: acmeIssuer}
}
return acmeIssuer
}
// consolidateAutomationPolicies combines automation policies that are the same, // consolidateAutomationPolicies combines automation policies that are the same,
// for a cleaner overall output. // for a cleaner overall output.
func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy { func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
for i := 0; i < len(aps); i++ { // sort from most specific to least specific; we depend on this ordering
for j := 0; j < len(aps); j++ { sort.SliceStable(aps, func(i, j int) bool {
if j == i { if automationPolicyIsSubset(aps[i], aps[j]) {
continue return true
} }
if automationPolicyIsSubset(aps[j], aps[i]) {
return false
}
return len(aps[i].Subjects) > len(aps[j].Subjects)
})
// remove any empty policies (except subjects, of course)
emptyAP := new(caddytls.AutomationPolicy)
for i := 0; i < len(aps); i++ {
emptyAP.Subjects = aps[i].Subjects
if reflect.DeepEqual(aps[i], emptyAP) {
aps = append(aps[:i], aps[i+1:]...)
i--
}
}
// remove or combine duplicate policies
for i := 0; i < len(aps); i++ {
// compare only with next policies; we sorted by specificity so we must not delete earlier policies
for j := i + 1; j < len(aps); j++ {
// if they're exactly equal in every way, just keep one of them // if they're exactly equal in every way, just keep one of them
if reflect.DeepEqual(aps[i], aps[j]) { if reflect.DeepEqual(aps[i], aps[j]) {
aps = append(aps[:j], aps[j+1:]...) aps = append(aps[:j], aps[j+1:]...)
@@ -432,30 +502,75 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
// otherwise the one without any subjects (a catch-all) would be // otherwise the one without any subjects (a catch-all) would be
// eaten up by the one with subjects; and if both have subjects, we // eaten up by the one with subjects; and if both have subjects, we
// need to combine their lists // need to combine their lists
if bytes.Equal(aps[i].IssuerRaw, aps[j].IssuerRaw) && if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) && bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
aps[i].MustStaple == aps[j].MustStaple && aps[i].MustStaple == aps[j].MustStaple &&
aps[i].KeyType == aps[j].KeyType && aps[i].KeyType == aps[j].KeyType &&
aps[i].OnDemand == aps[j].OnDemand && aps[i].OnDemand == aps[j].OnDemand &&
aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio { aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio {
if len(aps[i].Subjects) == 0 && len(aps[j].Subjects) > 0 { if len(aps[i].Subjects) > 0 && len(aps[j].Subjects) == 0 {
aps = append(aps[:j], aps[j+1:]...) // later policy (at j) has no subjects ("catch-all"), so we can
} else if len(aps[i].Subjects) > 0 && len(aps[j].Subjects) == 0 { // remove the identical-but-more-specific policy that comes first
aps = append(aps[:i], aps[i+1:]...) // AS LONG AS it is not shadowed by another policy before it; e.g.
// if policy i is for example.com, policy i+1 is '*.com', and policy
// j is catch-all, we cannot remove policy i because that would
// cause example.com to be served by the less specific policy for
// '*.com', which might be different (yes we've seen this happen)
if automationPolicyShadows(i, aps) >= j {
aps = append(aps[:i], aps[i+1:]...)
}
} else { } else {
aps[i].Subjects = append(aps[i].Subjects, aps[j].Subjects...) // avoid repeated subjects
for _, subj := range aps[j].Subjects {
if !sliceContains(aps[i].Subjects, subj) {
aps[i].Subjects = append(aps[i].Subjects, subj)
}
}
aps = append(aps[:j], aps[j+1:]...) aps = append(aps[:j], aps[j+1:]...)
j--
} }
i--
break
} }
} }
} }
// ensure any catch-all policies go last
sort.SliceStable(aps, func(i, j int) bool {
return len(aps[i].Subjects) > len(aps[j].Subjects)
})
return aps return aps
} }
// 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 {
return true
}
if len(a.Subjects) == 0 {
return false
}
for _, aSubj := range a.Subjects {
var inSuperset bool
for _, bSubj := range b.Subjects {
if certmagic.MatchWildcard(aSubj, bSubj) {
inSuperset = true
break
}
}
if !inSuperset {
return false
}
}
return true
}
// automationPolicyShadows returns the index of a policy that aps[i] shadows;
// in other words, for all policies after position i, if that policy covers
// the same subjects but is less specific, that policy's position is returned,
// or -1 if no shadowing is found. For example, if policy i is for
// "foo.example.com" and policy i+2 is for "*.example.com", then i+2 will be
// returned, since that policy is shadowed by i, which is in front.
func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int {
for j := i + 1; j < len(aps); j++ {
if automationPolicyIsSubset(aps[i], aps[j]) {
return j
}
}
return -1
}
+56
View File
@@ -0,0 +1,56 @@
package httpcaddyfile
import (
"testing"
"github.com/caddyserver/caddy/v2/modules/caddytls"
)
func TestAutomationPolicyIsSubset(t *testing.T) {
for i, test := range []struct {
a, b []string
expect bool
}{
{
a: []string{"example.com"},
b: []string{},
expect: true,
},
{
a: []string{},
b: []string{"example.com"},
expect: false,
},
{
a: []string{"foo.example.com"},
b: []string{"*.example.com"},
expect: true,
},
{
a: []string{"foo.example.com"},
b: []string{"foo.example.com"},
expect: true,
},
{
a: []string{"foo.example.com"},
b: []string{"example.com"},
expect: false,
},
{
a: []string{"example.com", "foo.example.com"},
b: []string{"*.com", "*.*.com"},
expect: true,
},
{
a: []string{"example.com", "foo.example.com"},
b: []string{"*.com"},
expect: false,
},
} {
apA := &caddytls.AutomationPolicy{Subjects: test.a}
apB := &caddytls.AutomationPolicy{Subjects: 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)
}
}
}
+13 -8
View File
@@ -124,10 +124,10 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
return return
} }
defer res.Body.Close() defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body) body, _ := ioutil.ReadAll(res.Body)
var out bytes.Buffer var out bytes.Buffer
json.Indent(&out, body, "", " ") _ = json.Indent(&out, body, "", " ")
tc.t.Logf("----------- failed with config -----------\n%s", out.String()) tc.t.Logf("----------- failed with config -----------\n%s", out.String())
} }
}) })
@@ -221,10 +221,11 @@ func isCaddyAdminRunning() error {
client := &http.Client{ client := &http.Client{
Timeout: Default.LoadRequestTimeout, Timeout: Default.LoadRequestTimeout,
} }
_, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil { if err != nil {
return errors.New("caddy integration test caddy server not running. Expected to be listening on localhost:2019") return errors.New("caddy integration test caddy server not running. Expected to be listening on localhost:2019")
} }
resp.Body.Close()
return nil return nil
} }
@@ -272,7 +273,7 @@ func CreateTestingTransport() *http.Transport {
IdleConnTimeout: 90 * time.Second, IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 5 * time.Second, TLSHandshakeTimeout: 5 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
} }
} }
@@ -313,9 +314,13 @@ func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, e
if err != nil { if err != nil {
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err) tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err)
} }
if loc == nil && expectedToLocation != "" {
if expectedToLocation != loc.String() { tc.t.Errorf("requesting \"%s\" expected a Location header, but didn't get one", requestURI)
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String()) }
if loc != nil {
if expectedToLocation != loc.String() {
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String())
}
} }
return resp return resp
@@ -430,7 +435,7 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
body := string(bytes) body := string(bytes)
if !strings.Contains(body, expectedBody) { if body != expectedBody {
tc.t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body) tc.t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
} }
+82
View File
@@ -0,0 +1,82 @@
package integration
import (
"net/http"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
}
localhost
respond "Yahaha! You found me!"
`, "caddyfile")
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
}
func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
}
localhost:9443
respond "Yahaha! You found me!"
`, "caddyfile")
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
}
func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
}
localhost:1234
respond "Yahaha! You found me!"
`, "caddyfile")
tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect)
}
func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"ingress_server": {
"listen": [
":9080",
":9443"
],
"routes": [
{
"match": [
{
"host": ["localhost"]
}
]
}
]
}
}
}
}
}
`, "json")
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
}
@@ -54,9 +54,12 @@
"automation": { "automation": {
"policies": [ "policies": [
{ {
"issuer": { "issuers": [
"module": "internal" {
} "module": "internal"
}
],
"key_type": "ed25519"
} }
], ],
"on_demand": { "on_demand": {
@@ -9,8 +9,8 @@
} }
acme_ca https://example.com acme_ca https://example.com
acme_eab { acme_eab {
key_id 4K2scIVbBpNd-78scadB2g key_id 4K2scIVbBpNd-78scadB2g
hmac abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh mac_key abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh
} }
acme_ca_root /path/to/ca.crt acme_ca_root /path/to/ca.crt
email test@example.com email test@example.com
@@ -57,18 +57,20 @@
"automation": { "automation": {
"policies": [ "policies": [
{ {
"issuer": { "issuers": [
"ca": "https://example.com", {
"email": "test@example.com", "ca": "https://example.com",
"external_account": { "email": "test@example.com",
"hmac": "abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh", "external_account": {
"key_id": "4K2scIVbBpNd-78scadB2g" "key_id": "4K2scIVbBpNd-78scadB2g",
}, "mac_key": "abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh"
"module": "acme", },
"trusted_roots_pem_files": [ "module": "acme",
"/path/to/ca.crt" "trusted_roots_pem_files": [
] "/path/to/ca.crt"
}, ]
}
],
"key_type": "ed25519" "key_type": "ed25519"
} }
], ],
@@ -0,0 +1,83 @@
{
debug
http_port 8080
https_port 8443
default_sni localhost
order root first
storage file_system {
root /data
}
acme_ca https://example.com
acme_ca_root /path/to/ca.crt
email test@example.com
admin {
origins localhost:2019 [::1]:2019 127.0.0.1:2019 192.168.10.128
}
on_demand_tls {
ask https://example.com
interval 30s
burst 20
}
local_certs
key_type ed25519
}
:80
----------
{
"admin": {
"listen": "localhost:2019",
"origins": [
"localhost:2019",
"[::1]:2019",
"127.0.0.1:2019",
"192.168.10.128"
]
},
"logging": {
"logs": {
"default": {
"level": "DEBUG"
}
}
},
"storage": {
"module": "file_system",
"root": "/data"
},
"apps": {
"http": {
"http_port": 8080,
"https_port": 8443,
"servers": {
"srv0": {
"listen": [
":80"
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"issuers": [
{
"module": "internal"
}
],
"key_type": "ed25519"
}
],
"on_demand": {
"rate_limit": {
"interval": 30000000000,
"burst": 20
},
"ask": "https://example.com"
}
}
}
}
}
@@ -0,0 +1,83 @@
{
servers {
timeouts {
idle 90s
}
}
servers :80 {
timeouts {
idle 60s
}
}
servers :443 {
timeouts {
idle 30s
}
}
}
foo.com {
}
http://bar.com {
}
:8080 {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"idle_timeout": 30000000000,
"routes": [
{
"match": [
{
"host": [
"foo.com"
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
":80"
],
"idle_timeout": 60000000000,
"routes": [
{
"match": [
{
"host": [
"bar.com"
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"bar.com"
]
}
},
"srv2": {
"listen": [
":8080"
],
"idle_timeout": 90000000000
}
}
}
}
}
@@ -0,0 +1,62 @@
{
servers {
listener_wrappers {
tls
}
timeouts {
read_body 30s
read_header 30s
write 30s
idle 30s
}
max_header_size 100MB
protocol {
allow_h2c
experimental_http3
strict_sni_host
}
}
}
foo.com {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"listener_wrappers": [
{
"wrapper": "tls"
}
],
"read_timeout": 30000000000,
"read_header_timeout": 30000000000,
"write_timeout": 30000000000,
"idle_timeout": 30000000000,
"max_header_bytes": 100000000,
"routes": [
{
"match": [
{
"host": [
"foo.com"
]
}
],
"terminal": true
}
],
"strict_sni_host": true,
"experimental_http3": true,
"allow_h2c": true
}
}
}
}
}
@@ -0,0 +1,105 @@
:80 {
handle /api/* {
respond "api"
}
handle_path /static/* {
respond "static"
}
handle {
respond "handle"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"group": "group3",
"match": [
{
"path": [
"/static/*"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "rewrite",
"strip_path_prefix": "/static"
}
]
},
{
"handle": [
{
"body": "static",
"handler": "static_response"
}
]
}
]
}
]
},
{
"group": "group3",
"match": [
{
"path": [
"/api/*"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "api",
"handler": "static_response"
}
]
}
]
}
]
},
{
"group": "group3",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "handle",
"handler": "static_response"
}
]
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,132 @@
:80 {
header Denis "Ritchie"
header +Edsger "Dijkstra"
header ?John "von Neumann"
header -Wolfram
header {
Grace: "Hopper" # some users habitually suffix field names with a colon
+Ray "Solomonoff"
?Tim "Berners-Lee"
defer
}
@images path /images/*
header @images {
Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/images/*"
]
}
],
"handle": [
{
"handler": "headers",
"response": {
"set": {
"Cache-Control": [
"public, max-age=3600, stale-while-revalidate=86400"
]
}
}
}
]
},
{
"handle": [
{
"handler": "headers",
"response": {
"set": {
"Denis": [
"Ritchie"
]
}
}
},
{
"handler": "headers",
"response": {
"add": {
"Edsger": [
"Dijkstra"
]
}
}
},
{
"handler": "headers",
"response": {
"require": {
"headers": {
"John": null
}
},
"set": {
"John": [
"von Neumann"
]
}
}
},
{
"handler": "headers",
"response": {
"deferred": true,
"delete": [
"Wolfram"
]
}
},
{
"handler": "headers",
"response": {
"add": {
"Ray": [
"Solomonoff"
]
},
"deferred": true,
"set": {
"Grace": [
"Hopper"
]
}
}
},
{
"handler": "headers",
"response": {
"require": {
"headers": {
"Tim": null
}
},
"set": {
"Tim": [
"Berners-Lee"
]
}
}
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,78 @@
http://localhost:2020 {
log
respond 200
}
:2020 {
respond 418
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":2020"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"status_code": 200
}
]
}
]
}
],
"terminal": true
},
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"status_code": 418
}
]
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
},
"logs": {
"logger_names": {
"localhost:2020": ""
},
"skip_unmapped_hosts": true
}
}
}
}
}
}
@@ -0,0 +1,69 @@
:80
log {
output stdout
format filter {
wrap console
fields {
request>headers>Authorization delete
request>headers>Server delete
request>remote_addr ip_mask {
ipv4 24
ipv6 32
}
}
}
}
----------
{
"logging": {
"logs": {
"default": {
"exclude": [
"http.log.access.log0"
]
},
"log0": {
"writer": {
"output": "stdout"
},
"encoder": {
"fields": {
"request\u003eheaders\u003eAuthorization": {
"filter": "delete"
},
"request\u003eheaders\u003eServer": {
"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": [
":80"
],
"logs": {
"default_logger_name": "log0"
}
}
}
}
}
}
@@ -9,6 +9,34 @@
@matcher3 not method PUT @matcher3 not method PUT
respond @matcher3 "not put" respond @matcher3 "not put"
@matcher4 vars "{http.request.uri}" "/vars-matcher"
respond @matcher4 "from vars matcher"
@matcher5 vars_regexp static "{http.request.uri}" `\.([a-f0-9]{6})\.(css|js)$`
respond @matcher5 "from vars_regexp matcher with name"
@matcher6 vars_regexp "{http.request.uri}" `\.([a-f0-9]{6})\.(css|js)$`
respond @matcher6 "from vars_regexp matcher without name"
@matcher7 {
header Foo bar
header Foo foobar
header Bar foo
}
respond @matcher7 "header matcher merging values of the same field"
@matcher8 {
query foo=bar foo=baz bar=foo
query bar=baz
}
respond @matcher8 "query matcher merging pairs with the same keys"
@matcher9 {
header !Foo
header Bar foo
}
respond @matcher9 "header matcher with null field matcher"
} }
---------- ----------
{ {
@@ -68,6 +96,117 @@
"handler": "static_response" "handler": "static_response"
} }
] ]
},
{
"match": [
{
"vars": {
"{http.request.uri}": "/vars-matcher"
}
}
],
"handle": [
{
"body": "from vars matcher",
"handler": "static_response"
}
]
},
{
"match": [
{
"vars_regexp": {
"{http.request.uri}": {
"name": "static",
"pattern": "\\.([a-f0-9]{6})\\.(css|js)$"
}
}
}
],
"handle": [
{
"body": "from vars_regexp matcher with name",
"handler": "static_response"
}
]
},
{
"match": [
{
"vars_regexp": {
"{http.request.uri}": {
"pattern": "\\.([a-f0-9]{6})\\.(css|js)$"
}
}
}
],
"handle": [
{
"body": "from vars_regexp matcher without name",
"handler": "static_response"
}
]
},
{
"match": [
{
"header": {
"Bar": [
"foo"
],
"Foo": [
"bar",
"foobar"
]
}
}
],
"handle": [
{
"body": "header matcher merging values of the same field",
"handler": "static_response"
}
]
},
{
"match": [
{
"query": {
"bar": [
"foo",
"baz"
],
"foo": [
"bar",
"baz"
]
}
}
],
"handle": [
{
"body": "query matcher merging pairs with the same keys",
"handler": "static_response"
}
]
},
{
"match": [
{
"header": {
"Bar": [
"foo"
],
"Foo": null
}
}
],
"handle": [
{
"body": "header matcher with null field matcher",
"handler": "static_response"
}
]
} }
] ]
} }
@@ -0,0 +1,31 @@
:80 {
route {
# unused matchers should not panic
# see https://github.com/caddyserver/caddy/issues/3745
@matcher1 path /path1
@matcher2 path /path2
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"handler": "subroute"
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,36 @@
:80 {
metrics /metrics {
disable_openmetrics
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/metrics"
]
}
],
"handle": [
{
"disable_openmetrics": true,
"handler": "metrics"
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,33 @@
:80 {
metrics /metrics
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/metrics"
]
}
],
"handle": [
{
"handler": "metrics"
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,132 @@
:8886
route {
# Add trailing slash for directory requests
@canonicalPath {
file {
try_files {path}/index.php
}
not path */
}
redir @canonicalPath {path}/ 308
# If the requested file does not exist, try index files
@indexFiles {
file {
try_files {path} {path}/index.php index.php
split_path .php
}
}
rewrite @indexFiles {http.matchers.file.relative}
# Proxy PHP files to the FastCGI responder
@phpFiles {
path *.php
}
reverse_proxy @phpFiles 127.0.0.1:9000 {
transport fastcgi {
split .php
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8886"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{
"dial": "127.0.0.1:9000"
}
]
}
],
"match": [
{
"path": [
"*.php"
]
}
]
}
]
}
]
}
]
}
}
}
}
}
@@ -1,15 +1,18 @@
:8884 :8884
php_fastcgi localhost:9000 { php_fastcgi localhost:9000 {
# some php_fastcgi-specific subdirectives # some php_fastcgi-specific subdirectives
split .php .php5 split .php .php5
env VAR1 value1 env VAR1 value1
env VAR2 value2 env VAR2 value2
root /var/www root /var/www
index off index off
dial_timeout 3s
read_timeout 10s
write_timeout 20s
# passed through to reverse_proxy (directive order doesn't matter!) # passed through to reverse_proxy (directive order doesn't matter!)
lb_policy random lb_policy random
} }
---------- ----------
{ {
@@ -39,16 +42,19 @@ php_fastcgi localhost:9000 {
} }
}, },
"transport": { "transport": {
"dial_timeout": 3000000000,
"env": { "env": {
"VAR1": "value1", "VAR1": "value1",
"VAR2": "value2" "VAR2": "value2"
}, },
"protocol": "fastcgi", "protocol": "fastcgi",
"read_timeout": 10000000000,
"root": "/var/www", "root": "/var/www",
"split_path": [ "split_path": [
".php", ".php",
".php5" ".php5"
] ],
"write_timeout": 20000000000
}, },
"upstreams": [ "upstreams": [
{ {
@@ -0,0 +1,113 @@
whoami.example.com {
reverse_proxy whoami
}
app.example.com {
reverse_proxy app:80
}
unix.example.com {
reverse_proxy unix//path/to/socket
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"whoami.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "whoami:80"
}
]
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"unix.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "unix//path/to/socket"
}
]
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"app.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "app:80"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,45 @@
localhost
request_body {
max_size 1MB
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "request_body",
"max_size": 1000000
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,36 @@
:8884
reverse_proxy 127.0.0.1:65535 {
transport fastcgi
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi"
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,38 @@
:8884
reverse_proxy h2c://localhost:8080
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http",
"versions": [
"h2c",
"2"
]
},
"upstreams": [
{
"dial": "localhost:8080"
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,119 @@
https://example.com {
reverse_proxy /path http://localhost:54321 {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Port {server_port}
header_up X-Forwarded-Proto "http"
buffer_requests
transport http {
read_buffer 10MB
write_buffer 20MB
max_response_header 30MB
dial_timeout 3s
dial_fallback_delay 5s
response_header_timeout 8s
expect_continue_timeout 9s
versions h2c 2
compression off
max_conns_per_host 5
max_idle_conns_per_host 2
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"buffer_requests": true,
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"Host": [
"{http.request.host}"
],
"X-Forwarded-For": [
"{http.request.remote}"
],
"X-Forwarded-Port": [
"{server_port}"
],
"X-Forwarded-Proto": [
"http"
],
"X-Real-Ip": [
"{http.request.remote}"
]
}
}
},
"transport": {
"compression": false,
"dial_fallback_delay": 5000000000,
"dial_timeout": 3000000000,
"expect_continue_timeout": 9000000000,
"max_conns_per_host": 5,
"max_idle_conns_per_host": 2,
"max_response_header_size": 30000000,
"protocol": "http",
"read_buffer_size": 10000000,
"response_header_timeout": 8000000000,
"versions": [
"h2c",
"2"
],
"write_buffer_size": 20000000
},
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
],
"match": [
{
"path": [
"/path"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,51 @@
:80
respond 200
@untrusted not remote_ip 10.1.1.0/24
respond @untrusted 401
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"not": [
{
"remote_ip": {
"ranges": [
"10.1.1.0/24"
]
}
}
]
}
],
"handle": [
{
"handler": "static_response",
"status_code": 401
}
]
},
{
"handle": [
{
"handler": "static_response",
"status_code": 200
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,86 @@
{
local_certs
}
*.tld, *.*.tld {
tls {
on_demand
}
}
foo.tld, www.foo.tld {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"foo.tld",
"www.foo.tld"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"*.tld",
"*.*.tld"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"foo.tld",
"www.foo.tld"
],
"issuers": [
{
"module": "internal"
}
]
},
{
"subjects": [
"*.*.tld",
"*.tld"
],
"issuers": [
{
"module": "internal"
}
],
"on_demand": true
},
{
"issuers": [
{
"module": "internal"
}
]
}
]
}
}
}
}
@@ -0,0 +1,137 @@
# https://github.com/caddyserver/caddy/issues/3906
a.a {
tls internal
respond 403
}
http://b.b https://b.b:8443 {
tls internal
respond 404
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.a"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"status_code": 403
}
]
}
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"b.b"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"status_code": 404
}
]
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"b.b"
]
}
},
"srv2": {
"listen": [
":8443"
],
"routes": [
{
"match": [
{
"host": [
"b.b"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"status_code": 404
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"a.a",
"b.b"
],
"issuers": [
{
"module": "internal"
}
]
}
]
}
}
}
}
+28
View File
@@ -0,0 +1,28 @@
package integration
import (
"net/http"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestBrowse(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
}
http://localhost:9080 {
file_server browse
}
`, "caddyfile")
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
if err != nil {
t.Fail()
return
}
tester.AssertResponseCode(req, 200)
}
+64 -71
View File
@@ -8,7 +8,6 @@ import (
) )
func TestMap(t *testing.T) { func TestMap(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(`{ tester.InitServer(`{
@@ -18,25 +17,24 @@ func TestMap(t *testing.T) {
localhost:9080 { localhost:9080 {
map http.request.method dest-name { map {http.request.method} {dest-1} {dest-2} {
default unknown default unknown1 unknown2
G.T get-called ~G.T get-called
POST post-called POST post-called foobar
} }
respond /version 200 { respond /version 200 {
body "hello from localhost {dest-name}" body "hello from localhost {dest-1} {dest-2}"
} }
} }
`, "caddyfile") `, "caddyfile")
// act and assert // act and assert
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called") tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called unknown2")
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called") tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called foobar")
} }
func TestMapRespondWithDefault(t *testing.T) { func TestMapRespondWithDefault(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(`{ tester.InitServer(`{
@@ -46,9 +44,9 @@ func TestMapRespondWithDefault(t *testing.T) {
localhost:9080 { localhost:9080 {
map http.request.method dest-name { map {http.request.method} {dest-name} {
default unknown default unknown
GET get-called GET get-called
} }
respond /version 200 { respond /version 200 {
@@ -63,80 +61,75 @@ func TestMapRespondWithDefault(t *testing.T) {
} }
func TestMapAsJson(t *testing.T) { func TestMapAsJson(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(`{ tester.InitServer(`
{
"apps": { "apps": {
"http": { "http": {
"http_port": 9080, "http_port": 9080,
"https_port": 9443, "https_port": 9443,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
":9080" ":9080"
], ],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [ "routes": [
{ {
"handle": [ "handle": [
{
"handler": "map",
"source": "http.request.method",
"destination": "dest-name",
"default": "unknown",
"items": [
{ {
"expression": "GET", "handler": "subroute",
"value": "get-called" "routes": [
}, {
{ "handle": [
"expression": "POST", {
"value": "post-called" "handler": "map",
"source": "{http.request.method}",
"destinations": ["dest-name"],
"defaults": ["unknown"],
"mappings": [
{
"input": "GET",
"outputs": ["get-called"]
},
{
"input": "POST",
"outputs": ["post-called"]
}
]
}
]
},
{
"handle": [
{
"body": "hello from localhost {dest-name}",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": ["/version"]
}
]
}
]
} }
] ],
} "match": [
] {
}, "host": ["localhost"]
{ }
"handle": [ ],
{ "terminal": true
"body": "hello from localhost {dest-name}",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
} }
] ]
}
],
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
} }
]
} }
} }
}
} }
} }`, "json")
`, "json")
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called") tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called")
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called") tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called")
+438
View File
@@ -0,0 +1,438 @@
package integration
import (
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"runtime"
"strings"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestSRVReverseProxy(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"lookup_srv": "srv.host.service.consul"
}
]
}
]
}
]
}
}
}
}
}
`, "json")
}
func TestSRVWithDial(t *testing.T) {
caddytest.AssertLoadError(t, `
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "tcp/address.to.upstream:80",
"lookup_srv": "srv.host.service.consul"
}
]
}
]
}
]
}
}
}
}
}
`, "json", `upstream: specifying dial address is incompatible with lookup_srv: 0: {\"dial\": \"tcp/address.to.upstream:80\", \"lookup_srv\": \"srv.host.service.consul\"}`)
}
func TestDialWithPlaceholderUnix(t *testing.T) {
if runtime.GOOS == "windows" {
t.SkipNow()
}
f, err := ioutil.TempFile("", "*.sock")
if err != nil {
t.Errorf("failed to create TempFile: %s", err)
return
}
// a hack to get a file name within a valid path to use as socket
socketName := f.Name()
os.Remove(f.Name())
server := http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Hello, World!"))
}),
}
unixListener, err := net.Listen("unix", socketName)
if err != nil {
t.Errorf("failed to listen on the socket: %s", err)
return
}
go server.Serve(unixListener)
t.Cleanup(func() {
server.Close()
})
runtime.Gosched() // Allow other goroutines to run
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "unix/{http.request.header.X-Caddy-Upstream-Dial}"
}
]
}
]
}
]
}
}
}
}
}
`, "json")
req, err := http.NewRequest(http.MethodGet, "http://localhost:8080", nil)
if err != nil {
t.Fail()
return
}
req.Header.Set("X-Caddy-Upstream-Dial", socketName)
tester.AssertResponse(req, 200, "Hello, World!")
}
func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "static_response",
"body": "Hello, World!"
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
}
},
"srv1": {
"listen": [
":9080"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "{http.request.header.X-Caddy-Upstream-Dial}"
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
}
}
}
}
}
}
`, "json")
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080", nil)
if err != nil {
t.Fail()
return
}
req.Header.Set("X-Caddy-Upstream-Dial", "localhost:8080")
tester.AssertResponse(req, 200, "Hello, World!")
}
func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "static_response",
"body": "Hello, World!"
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
}
},
"srv1": {
"listen": [
":9080"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "tcp/{http.request.header.X-Caddy-Upstream-Dial}:8080"
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
}
}
}
}
}
}
`, "json")
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080", nil)
if err != nil {
t.Fail()
return
}
req.Header.Set("X-Caddy-Upstream-Dial", "localhost")
tester.AssertResponse(req, 200, "Hello, World!")
}
func TestSRVWithActiveHealthcheck(t *testing.T) {
caddytest.AssertLoadError(t, `
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"health_checks": {
"active": {
"path": "/ok"
}
},
"upstreams": [
{
"lookup_srv": "srv.host.service.consul"
}
]
}
]
}
]
}
}
}
}
}
`, "json", `upstream: lookup_srv is incompatible with active health checks: 0: {\"dial\": \"\", \"lookup_srv\": \"srv.host.service.consul\"}`)
}
func TestReverseProxyHealthCheck(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
}
http://localhost:2020 {
respond "Hello, World!"
}
http://localhost:2021 {
respond "ok"
}
http://localhost:9080 {
reverse_proxy {
to localhost:2020
health_path /health
health_port 2021
health_interval 2s
health_timeout 5s
}
}
`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
}
func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
if runtime.GOOS == "windows" {
t.SkipNow()
}
tester := caddytest.NewTester(t)
f, err := ioutil.TempFile("", "*.sock")
if err != nil {
t.Errorf("failed to create TempFile: %s", err)
return
}
// a hack to get a file name within a valid path to use as socket
socketName := f.Name()
os.Remove(f.Name())
server := http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if strings.HasPrefix(req.URL.Path, "/health") {
w.Write([]byte("ok"))
return
}
w.Write([]byte("Hello, World!"))
}),
}
unixListener, err := net.Listen("unix", socketName)
if err != nil {
t.Errorf("failed to listen on the socket: %s", err)
return
}
go server.Serve(unixListener)
t.Cleanup(func() {
server.Close()
})
runtime.Gosched() // Allow other goroutines to run
tester.InitServer(fmt.Sprintf(`
{
http_port 9080
https_port 9443
}
http://localhost:9080 {
reverse_proxy {
to unix/%s
health_path /health
health_port 2021
health_interval 2s
health_timeout 5s
}
}
`, socketName), "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
}
+2 -3
View File
@@ -99,7 +99,7 @@ func TestDefaultSNI(t *testing.T) {
// act and assert // act and assert
// makes a request with no sni // makes a request with no sni
tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a") tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a.caddy.localhost")
} }
func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) { func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
@@ -204,7 +204,6 @@ func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
} }
func TestDefaultSNIWithPortMappingOnly(t *testing.T) { func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
@@ -273,7 +272,7 @@ func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
// act and assert // act and assert
// makes a request with no sni // makes a request with no sni
tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a") tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a.caddy.localhost")
} }
func TestHttpOnlyOnDomainWithSNI(t *testing.T) { func TestHttpOnlyOnDomainWithSNI(t *testing.T) {
+437
View File
@@ -0,0 +1,437 @@
package integration
import (
"compress/gzip"
"context"
"crypto/rand"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"testing"
"time"
"github.com/caddyserver/caddy/v2/caddytest"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
// (see https://github.com/caddyserver/caddy/issues/3556 for use case)
func TestH2ToH2CStream(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http",
"compression": false,
"versions": [
"h2c",
"2"
]
},
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
],
"match": [
{
"path": [
"/tov2ray"
]
}
]
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
expectedBody := "some data to be echoed"
// start the server
server := testH2ToH2CStreamServeH2C(t)
go server.ListenAndServe()
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
defer cancel()
server.Shutdown(ctx)
}()
r, w := io.Pipe()
req := &http.Request{
Method: "PUT",
Body: ioutil.NopCloser(r),
URL: &url.URL{
Scheme: "https",
Host: "127.0.0.1:9443",
Path: "/tov2ray",
},
Proto: "HTTP/2",
ProtoMajor: 2,
ProtoMinor: 0,
Header: make(http.Header),
}
// Disable any compression method from server.
req.Header.Set("Accept-Encoding", "identity")
resp := tester.AssertResponseCode(req, 200)
if 200 != resp.StatusCode {
return
}
go func() {
fmt.Fprint(w, expectedBody)
w.Close()
}()
defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("unable to read the response body %s", err)
}
body := string(bytes)
if !strings.Contains(body, expectedBody) {
t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
}
return
}
func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
h2s := &http2.Server{}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rstring, err := httputil.DumpRequest(r, false)
if err == nil {
t.Logf("h2c server received req: %s", rstring)
}
// We only accept HTTP/2!
if r.ProtoMajor != 2 {
t.Error("Not a HTTP/2 request, rejected!")
w.WriteHeader(http.StatusInternalServerError)
return
}
if r.Host != "127.0.0.1:9443" {
t.Errorf("r.Host doesn't match, %v!", r.Host)
w.WriteHeader(http.StatusNotFound)
return
}
if !strings.HasPrefix(r.URL.Path, "/tov2ray") {
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(200)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
buf := make([]byte, 4*1024)
for {
n, err := r.Body.Read(buf)
if n > 0 {
w.Write(buf[:n])
}
if err != nil {
if err == io.EOF {
r.Body.Close()
}
break
}
}
})
server := &http.Server{
Addr: "127.0.0.1:54321",
Handler: h2c.NewHandler(handler, h2s),
}
return server
}
// (see https://github.com/caddyserver/caddy/issues/3606 for use case)
func TestH2ToH1ChunkedResponse(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"logging": {
"logs": {
"default": {
"level": "DEBUG"
}
}
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"encodings": {
"gzip": {}
},
"handler": "encode"
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
],
"match": [
{
"path": [
"/tov2ray"
]
}
]
}
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": [
"cert0"
]
},
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
}
}
}
`, "json")
// need a large body here to trigger caddy's compression, larger than gzip.miniLength
expectedBody, err := GenerateRandomString(1024)
if err != nil {
t.Fatalf("generate expected body failed, err: %s", err)
}
// start the server
server := testH2ToH1ChunkedResponseServeH1(t)
go server.ListenAndServe()
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
defer cancel()
server.Shutdown(ctx)
}()
r, w := io.Pipe()
req := &http.Request{
Method: "PUT",
Body: ioutil.NopCloser(r),
URL: &url.URL{
Scheme: "https",
Host: "127.0.0.1:9443",
Path: "/tov2ray",
},
Proto: "HTTP/2",
ProtoMajor: 2,
ProtoMinor: 0,
Header: make(http.Header),
}
// underlying transport will automaticlly add gzip
// req.Header.Set("Accept-Encoding", "gzip")
go func() {
fmt.Fprint(w, expectedBody)
w.Close()
}()
resp := tester.AssertResponseCode(req, 200)
if 200 != resp.StatusCode {
return
}
defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("unable to read the response body %s", err)
}
body := string(bytes)
if body != expectedBody {
t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
}
return
}
func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Host != "127.0.0.1:9443" {
t.Errorf("r.Host doesn't match, %v!", r.Host)
w.WriteHeader(http.StatusNotFound)
return
}
if !strings.HasPrefix(r.URL.Path, "/tov2ray") {
w.WriteHeader(http.StatusNotFound)
return
}
defer r.Body.Close()
bytes, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Fatalf("unable to read the response body %s", err)
}
n := len(bytes)
var writer io.Writer
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
gw, err := gzip.NewWriterLevel(w, 5)
if err != nil {
t.Error("can't return gzip data")
w.WriteHeader(http.StatusInternalServerError)
return
}
defer gw.Close()
writer = gw
w.Header().Set("Content-Encoding", "gzip")
w.Header().Del("Content-Length")
w.WriteHeader(200)
} else {
writer = w
}
if n > 0 {
writer.Write(bytes[:])
}
})
server := &http.Server{
Addr: "127.0.0.1:54321",
Handler: handler,
}
return server
}
// GenerateRandomBytes returns securely generated random bytes.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
return nil, err
}
return b, nil
}
// GenerateRandomString returns a securely generated random string.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomString(n int) (string, error) {
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
bytes, err := GenerateRandomBytes(n)
if err != nil {
return "", err
}
for i, b := range bytes {
bytes[i] = letters[b%byte(len(letters))]
}
return string(bytes), nil
}
+14 -5
View File
@@ -96,7 +96,7 @@ func cmdStart(fl Flags) (int, error) {
// started yet, and writing synchronously would result // started yet, and writing synchronously would result
// in a deadlock // in a deadlock
go func() { go func() {
stdinpipe.Write(expect) _, _ = stdinpipe.Write(expect)
stdinpipe.Close() stdinpipe.Close()
}() }()
@@ -533,7 +533,17 @@ func cmdFmt(fl Flags) (int, error) {
if formatCmdConfigFile == "" { if formatCmdConfigFile == "" {
formatCmdConfigFile = "Caddyfile" formatCmdConfigFile = "Caddyfile"
} }
overwrite := fl.Bool("overwrite")
// as a special case, read from stdin if the file name is "-"
if formatCmdConfigFile == "-" {
input, err := ioutil.ReadAll(os.Stdin)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading stdin: %v", err)
}
fmt.Print(string(caddyfile.Format(input)))
return caddy.ExitCodeSuccess, nil
}
input, err := ioutil.ReadFile(formatCmdConfigFile) input, err := ioutil.ReadFile(formatCmdConfigFile)
if err != nil { if err != nil {
@@ -543,9 +553,8 @@ func cmdFmt(fl Flags) (int, error) {
output := caddyfile.Format(input) output := caddyfile.Format(input)
if overwrite { if fl.Bool("overwrite") {
err = ioutil.WriteFile(formatCmdConfigFile, output, 0644) if err := ioutil.WriteFile(formatCmdConfigFile, output, 0600); err != nil {
if err != nil {
return caddy.ExitCodeFailedStartup, nil return caddy.ExitCodeFailedStartup, nil
} }
} else { } else {
+6 -2
View File
@@ -263,8 +263,12 @@ provisioning stages.`,
Formats the Caddyfile by adding proper indentation and spaces to improve Formats the Caddyfile by adding proper indentation and spaces to improve
human readability. It prints the result to stdout. human readability. It prints the result to stdout.
If --write is specified, the output will be written to the config file If --overwrite is specified, the output will be written to the config file
directly instead of printing it.`, directly instead of printing it.
If you wish you use stdin instead of a regular file, use - as the path.
When reading from stdin, the --overwrite flag has no effect: the result
is always printed to stdout.`,
Flags: func() *flag.FlagSet { Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("format", flag.ExitOnError) fs := flag.NewFlagSet("format", flag.ExitOnError)
fs.Bool("overwrite", false, "Overwrite the input file with the results") fs.Bool("overwrite", false, "Overwrite the input file with the results")
+7 -1
View File
@@ -123,7 +123,11 @@ func loadConfig(configFile, adapterName string) ([]byte, string, error) {
var cfgAdapter caddyconfig.Adapter var cfgAdapter caddyconfig.Adapter
var err error var err error
if configFile != "" { if configFile != "" {
config, err = ioutil.ReadFile(configFile) if configFile == "-" {
config, err = ioutil.ReadAll(os.Stdin)
} else {
config, err = ioutil.ReadFile(configFile)
}
if err != nil { if err != nil {
return nil, "", fmt.Errorf("reading config file: %v", err) return nil, "", fmt.Errorf("reading config file: %v", err)
} }
@@ -236,6 +240,7 @@ func watchConfigFile(filename, adapterName string) {
} }
// begin poller // begin poller
//nolint:staticcheck
for range time.Tick(1 * time.Second) { for range time.Tick(1 * time.Second) {
// get the file info // get the file info
info, err := os.Stat(filename) info, err := os.Stat(filename)
@@ -410,6 +415,7 @@ func printEnvironment() {
fmt.Printf("caddy.AppDataDir=%s\n", caddy.AppDataDir()) fmt.Printf("caddy.AppDataDir=%s\n", caddy.AppDataDir())
fmt.Printf("caddy.AppConfigDir=%s\n", caddy.AppConfigDir()) fmt.Printf("caddy.AppConfigDir=%s\n", caddy.AppConfigDir())
fmt.Printf("caddy.ConfigAutosavePath=%s\n", caddy.ConfigAutosavePath) fmt.Printf("caddy.ConfigAutosavePath=%s\n", caddy.ConfigAutosavePath)
fmt.Printf("caddy.Version=%s\n", caddy.GoModule().Version)
fmt.Printf("runtime.GOOS=%s\n", runtime.GOOS) fmt.Printf("runtime.GOOS=%s\n", runtime.GOOS)
fmt.Printf("runtime.GOARCH=%s\n", runtime.GOARCH) fmt.Printf("runtime.GOARCH=%s\n", runtime.GOARCH)
fmt.Printf("runtime.Compiler=%s\n", runtime.Compiler) fmt.Printf("runtime.Compiler=%s\n", runtime.Compiler)
-37
View File
@@ -1,37 +0,0 @@
// 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.
// +build !windows
package caddycmd
import (
"fmt"
"os"
"path/filepath"
"syscall"
)
func gracefullyStopProcess(pid int) error {
fmt.Print("Graceful stop... ")
err := syscall.Kill(pid, syscall.SIGINT)
if err != nil {
return fmt.Errorf("kill: %v", err)
}
return nil
}
func getProcessName() string {
return filepath.Base(os.Args[0])
}
-44
View File
@@ -1,44 +0,0 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddycmd
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
)
func gracefullyStopProcess(pid int) error {
fmt.Print("Forceful stop... ")
// process on windows will not stop unless forced with /f
cmd := exec.Command("taskkill", "/pid", strconv.Itoa(pid), "/f")
if err := cmd.Run(); err != nil {
return fmt.Errorf("taskkill: %v", err)
}
return nil
}
// On Windows the app name passed in os.Args[0] will match how
// caddy was started eg will match caddy or caddy.exe.
// So return appname with .exe for consistency
func getProcessName() string {
base := filepath.Base(os.Args[0])
if filepath.Ext(base) == "" {
return base + ".exe"
}
return base
}
+18 -18
View File
@@ -4,31 +4,31 @@ go 1.14
require ( require (
github.com/Masterminds/sprig/v3 v3.1.0 github.com/Masterminds/sprig/v3 v3.1.0
github.com/alecthomas/chroma v0.7.4-0.20200517063913-500529fd43c1 github.com/alecthomas/chroma v0.8.2
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a
github.com/caddyserver/certmagic v0.11.2 github.com/caddyserver/certmagic v0.12.1-0.20201215190346-201f83a06067
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
github.com/go-acme/lego/v3 v3.7.0
github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/chi v4.1.2+incompatible
github.com/google/cel-go v0.5.1 github.com/google/cel-go v0.6.0
github.com/jsternberg/zap-logfmt v1.2.0 github.com/jsternberg/zap-logfmt v1.2.0
github.com/klauspost/compress v1.10.10 github.com/klauspost/compress v1.11.3
github.com/klauspost/cpuid v1.3.0 github.com/klauspost/cpuid/v2 v2.0.1
github.com/libdns/libdns v0.0.0-20200501023120-186724ffc821 github.com/lucas-clemente/quic-go v0.19.3
github.com/lucas-clemente/quic-go v0.17.1 github.com/mholt/acmez v0.1.1
github.com/naoina/go-stringutil v0.1.0 // indirect github.com/naoina/go-stringutil v0.1.0 // indirect
github.com/naoina/toml v0.1.1 github.com/naoina/toml v0.1.1
github.com/smallstep/certificates v0.15.0-rc.1.0.20200506212953-e855707dc274 github.com/prometheus/client_golang v1.9.0
github.com/smallstep/cli v0.14.4 github.com/smallstep/certificates v0.15.4
github.com/smallstep/nosql v0.3.0 github.com/smallstep/cli v0.15.2
github.com/smallstep/truststore v0.9.5 github.com/smallstep/nosql v0.3.0 // cannot upgrade from v0.3.0 until protobuf warning is fixed
github.com/yuin/goldmark v1.1.32 github.com/smallstep/truststore v0.9.6
github.com/yuin/goldmark v1.2.1
github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691 github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691
go.uber.org/zap v1.15.0 go.uber.org/zap v1.16.0
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
golang.org/x/net v0.0.0-20200625001655-4c5254603344 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98
google.golang.org/protobuf v1.25.0 google.golang.org/protobuf v1.24.0 // cannot upgrade until warning is fixed
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.3.0 gopkg.in/yaml.v2 v2.3.0
) )
+232 -240
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -129,9 +129,9 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
if *fcl.deadline { if *fcl.deadline {
switch ln := fcl.Listener.(type) { switch ln := fcl.Listener.(type) {
case *net.TCPListener: case *net.TCPListener:
ln.SetDeadline(time.Time{}) _ = ln.SetDeadline(time.Time{})
case *net.UnixListener: case *net.UnixListener:
ln.SetDeadline(time.Time{}) _ = ln.SetDeadline(time.Time{})
} }
*fcl.deadline = false *fcl.deadline = false
} }
@@ -167,9 +167,9 @@ func (fcl *fakeCloseListener) Close() error {
if !*fcl.deadline { if !*fcl.deadline {
switch ln := fcl.Listener.(type) { switch ln := fcl.Listener.(type) {
case *net.TCPListener: case *net.TCPListener:
ln.SetDeadline(time.Now().Add(-1 * time.Minute)) _ = ln.SetDeadline(time.Now().Add(-1 * time.Minute))
case *net.UnixListener: case *net.UnixListener:
ln.SetDeadline(time.Now().Add(-1 * time.Minute)) _ = ln.SetDeadline(time.Now().Add(-1 * time.Minute))
} }
*fcl.deadline = true *fcl.deadline = true
} }
+1 -1
View File
@@ -458,7 +458,7 @@ func (cl *CustomLog) buildCore() {
if cl.Sampling.Thereafter == 0 { if cl.Sampling.Thereafter == 0 {
cl.Sampling.Thereafter = 100 cl.Sampling.Thereafter = 100
} }
c = zapcore.NewSampler(c, cl.Sampling.Interval, c = zapcore.NewSamplerWithOptions(c, cl.Sampling.Interval,
cl.Sampling.First, cl.Sampling.Thereafter) cl.Sampling.First, cl.Sampling.Thereafter)
} }
cl.core = c cl.core = c
+77
View File
@@ -0,0 +1,77 @@
package caddy
import (
"net/http"
"strconv"
"strings"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
// define and register the metrics used in this package.
func init() {
prometheus.MustRegister(prometheus.NewBuildInfoCollector())
const ns, sub = "caddy", "admin"
adminMetrics.requestCount = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: ns,
Subsystem: sub,
Name: "http_requests_total",
Help: "Counter of requests made to the Admin API's HTTP endpoints.",
}, []string{"handler", "path", "code", "method"})
adminMetrics.requestErrors = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: ns,
Subsystem: sub,
Name: "http_request_errors_total",
Help: "Number of requests resulting in middleware errors.",
}, []string{"handler", "path", "method"})
}
// adminMetrics is a collection of metrics that can be tracked for the admin API.
var adminMetrics = struct {
requestCount *prometheus.CounterVec
requestErrors *prometheus.CounterVec
}{}
// Similar to promhttp.InstrumentHandlerCounter, but upper-cases method names
// instead of lower-casing them.
//
// Unlike promhttp.InstrumentHandlerCounter, this assumes a "code" and "method"
// label is present, and will panic otherwise.
func instrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
d := newDelegator(w)
next.ServeHTTP(d, r)
counter.With(prometheus.Labels{
"code": sanitizeCode(d.status),
"method": strings.ToUpper(r.Method),
}).Inc()
})
}
func newDelegator(w http.ResponseWriter) *delegator {
return &delegator{
ResponseWriter: w,
}
}
type delegator struct {
http.ResponseWriter
status int
}
func (d *delegator) WriteHeader(code int) {
d.status = code
d.ResponseWriter.WriteHeader(code)
}
func sanitizeCode(s int) string {
switch s {
case 0, 200:
return "200"
default:
return strconv.Itoa(s)
}
}
+27 -1
View File
@@ -47,6 +47,7 @@ func init() {
// //
// Placeholder | Description // Placeholder | Description
// ------------|--------------- // ------------|---------------
// `{http.request.body}` | The request body (⚠️ inefficient; use only for debugging)
// `{http.request.cookie.*}` | HTTP request cookie // `{http.request.cookie.*}` | HTTP request cookie
// `{http.request.header.*}` | Specific request header field // `{http.request.header.*}` | Specific request header field
// `{http.request.host.labels.*}` | Request host labels (0-based from right); e.g. for foo.example.com: 0=com, 1=example, 2=foo // `{http.request.host.labels.*}` | Request host labels (0-based from right); e.g. for foo.example.com: 0=com, 1=example, 2=foo
@@ -74,6 +75,7 @@ func init() {
// `{http.request.tls.client.fingerprint}` | The SHA256 checksum of the client certificate // `{http.request.tls.client.fingerprint}` | The SHA256 checksum of the client certificate
// `{http.request.tls.client.public_key}` | The public key of the client certificate. // `{http.request.tls.client.public_key}` | The public key of the client certificate.
// `{http.request.tls.client.public_key_sha256}` | The SHA256 checksum of the client's public key. // `{http.request.tls.client.public_key_sha256}` | The SHA256 checksum of the client's public key.
// `{http.request.tls.client.certificate_pem}` | The PEM-encoded value of the certificate.
// `{http.request.tls.client.issuer}` | The issuer DN of the client certificate // `{http.request.tls.client.issuer}` | The issuer DN of the client certificate
// `{http.request.tls.client.serial}` | The serial number of the client certificate // `{http.request.tls.client.serial}` | The serial number of the client certificate
// `{http.request.tls.client.subject}` | The subject DN of the client certificate // `{http.request.tls.client.subject}` | The subject DN of the client certificate
@@ -154,6 +156,7 @@ func (app *App) Provision(ctx caddy.Context) error {
// prepare each server // prepare each server
for srvName, srv := range app.Servers { for srvName, srv := range app.Servers {
srv.name = srvName
srv.tlsApp = app.tlsApp srv.tlsApp = app.tlsApp
srv.logger = app.logger.Named("log") srv.logger = app.logger.Named("log")
srv.errorLogger = app.logger.Named("log.error") srv.errorLogger = app.logger.Named("log.error")
@@ -247,6 +250,13 @@ func (app *App) Provision(ctx caddy.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("server %s: setting up TLS connection policies: %v", srvName, err) return fmt.Errorf("server %s: setting up TLS connection policies: %v", srvName, err)
} }
// if there is no idle timeout, set a sane default; users have complained
// before that aggressive CDNs leave connections open until the server
// closes them, so if we don't close them it leads to resource exhaustion
if srv.IdleTimeout == 0 {
srv.IdleTimeout = defaultIdleTimeout
}
} }
return nil return nil
@@ -280,6 +290,12 @@ func (app *App) Validate() error {
// Start runs the app. It finishes automatic HTTPS if enabled, // Start runs the app. It finishes automatic HTTPS if enabled,
// including management of certificates. // including management of certificates.
func (app *App) Start() error { func (app *App) Start() error {
// get a logger compatible with http.Server
serverLogger, err := zap.NewStdLogAt(app.logger.Named("stdlib"), zap.DebugLevel)
if err != nil {
return fmt.Errorf("failed to set up server logger: %v", err)
}
for srvName, srv := range app.Servers { for srvName, srv := range app.Servers {
s := &http.Server{ s := &http.Server{
ReadTimeout: time.Duration(srv.ReadTimeout), ReadTimeout: time.Duration(srv.ReadTimeout),
@@ -288,6 +304,7 @@ func (app *App) Start() error {
IdleTimeout: time.Duration(srv.IdleTimeout), IdleTimeout: time.Duration(srv.IdleTimeout),
MaxHeaderBytes: srv.MaxHeaderBytes, MaxHeaderBytes: srv.MaxHeaderBytes,
Handler: srv, Handler: srv,
ErrorLog: serverLogger,
} }
// enable h2c if configured // enable h2c if configured
@@ -343,8 +360,10 @@ func (app *App) Start() error {
Addr: hostport, Addr: hostport,
Handler: srv, Handler: srv,
TLSConfig: tlsCfg, TLSConfig: tlsCfg,
ErrorLog: serverLogger,
}, },
} }
//nolint:errcheck
go h3srv.Serve(h3ln) go h3srv.Serve(h3ln)
app.h3servers = append(app.h3servers, h3srv) app.h3servers = append(app.h3servers, h3srv)
app.h3listeners = append(app.h3listeners, h3ln) app.h3listeners = append(app.h3listeners, h3ln)
@@ -373,6 +392,7 @@ func (app *App) Start() error {
zap.Bool("tls", useTLS), zap.Bool("tls", useTLS),
) )
//nolint:errcheck
go s.Serve(ln) go s.Serve(ln)
app.servers = append(app.servers, s) app.servers = append(app.servers, s)
} }
@@ -381,7 +401,7 @@ func (app *App) Start() error {
// finish automatic HTTPS by finally beginning // finish automatic HTTPS by finally beginning
// certificate management // certificate management
err := app.automaticHTTPSPhase2() err = app.automaticHTTPSPhase2()
if err != nil { if err != nil {
return fmt.Errorf("finalizing automatic HTTPS: %v", err) return fmt.Errorf("finalizing automatic HTTPS: %v", err)
} }
@@ -447,6 +467,12 @@ func (app *App) httpsPort() int {
return app.HTTPSPort return app.HTTPSPort
} }
// defaultIdleTimeout is the default HTTP server timeout
// for closing idle connections; useful to avoid resource
// exhaustion behind hungry CDNs, for example (we've had
// several complaints without this).
const defaultIdleTimeout = caddy.Duration(5 * time.Minute)
// Interface guards // Interface guards
var ( var (
_ caddy.App = (*App)(nil) _ caddy.App = (*App)(nil)
+85 -61
View File
@@ -241,7 +241,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// we now have a list of all the unique names for which we need certs; // we now have a list of all the unique names for which we need certs;
// turn the set into a slice so that phase 2 can use it // turn the set into a slice so that phase 2 can use it
app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts)) app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts))
var internal, external []string var internal []string
uniqueDomainsLoop: uniqueDomainsLoop:
for d := range uniqueDomainsForCerts { for d := range uniqueDomainsForCerts {
// whether or not there is already an automation policy for this // whether or not there is already an automation policy for this
@@ -264,15 +264,13 @@ uniqueDomainsLoop:
// if no automation policy exists for the name yet, we // if no automation policy exists for the name yet, we
// will associate it with an implicit one // will associate it with an implicit one
if certmagic.SubjectQualifiesForPublicCert(d) { if !certmagic.SubjectQualifiesForPublicCert(d) {
external = append(external, d)
} else {
internal = append(internal, d) internal = append(internal, d)
} }
} }
// ensure there is an automation policy to handle these certs // ensure there is an automation policy to handle these certs
err := app.createAutomationPolicies(ctx, external, internal) err := app.createAutomationPolicies(ctx, internal)
if err != nil { if err != nil {
return err return err
} }
@@ -305,31 +303,11 @@ uniqueDomainsLoop:
matcherSet = append(matcherSet, MatchHost(domains)) matcherSet = append(matcherSet, MatchHost(domains))
} }
// build the address to which to redirect
addr, err := caddy.ParseNetworkAddress(addrStr) addr, err := caddy.ParseNetworkAddress(addrStr)
if err != nil { if err != nil {
return err return err
} }
redirTo := "https://{http.request.host}" redirRoute := app.makeRedirRoute(addr.StartPort, matcherSet)
if addr.StartPort != uint(app.httpsPort()) {
redirTo += ":" + strconv.Itoa(int(addr.StartPort))
}
redirTo += "{http.request.uri}"
// build the route
redirRoute := Route{
MatcherSets: []MatcherSet{matcherSet},
Handlers: []MiddlewareHandler{
StaticResponse{
StatusCode: WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
Headers: http.Header{
"Location": []string{redirTo},
"Connection": []string{"close"},
},
Close: true,
},
},
}
// use the network/host information from the address, // use the network/host information from the address,
// but change the port to the HTTP port then rebuild // but change the port to the HTTP port then rebuild
@@ -357,25 +335,7 @@ uniqueDomainsLoop:
// it's not something that should be relied on. We can change this // it's not something that should be relied on. We can change this
// if we want to. // if we want to.
appendCatchAll := func(routes []Route) []Route { appendCatchAll := func(routes []Route) []Route {
redirTo := "https://{http.request.host}" return append(routes, app.makeRedirRoute(uint(app.httpsPort()), MatcherSet{MatchProtocol("http")}))
if app.httpsPort() != DefaultHTTPSPort {
redirTo += ":" + strconv.Itoa(app.httpsPort())
}
redirTo += "{http.request.uri}"
routes = append(routes, Route{
MatcherSets: []MatcherSet{{MatchProtocol("http")}},
Handlers: []MiddlewareHandler{
StaticResponse{
StatusCode: WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
Headers: http.Header{
"Location": []string{redirTo},
"Connection": []string{"close"},
},
Close: true,
},
},
})
return routes
} }
redirServersLoop: redirServersLoop:
@@ -424,13 +384,47 @@ redirServersLoop:
return nil return nil
} }
func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
redirTo := "https://{http.request.host}"
// since this is an external redirect, we should only append an explicit
// port if we know it is not the officially standardized HTTPS port, and,
// notably, also not the port that Caddy thinks is the HTTPS port (the
// configurable HTTPSPort parameter) - we can't change the standard HTTPS
// port externally, so that config parameter is for internal use only;
// we also do not append the port if it happens to be the HTTP port as
// well, obviously (for example, user defines the HTTP port explicitly
// in the list of listen addresses for a server)
if redirToPort != uint(app.httpPort()) &&
redirToPort != uint(app.httpsPort()) &&
redirToPort != DefaultHTTPPort &&
redirToPort != DefaultHTTPSPort {
redirTo += ":" + strconv.Itoa(int(redirToPort))
}
redirTo += "{http.request.uri}"
return Route{
MatcherSets: []MatcherSet{matcherSet},
Handlers: []MiddlewareHandler{
StaticResponse{
StatusCode: WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
Headers: http.Header{
"Location": []string{redirTo},
"Connection": []string{"close"},
},
Close: true,
},
},
}
}
// createAutomationPolicy ensures that automated certificates for this // createAutomationPolicy ensures that automated certificates for this
// app are managed properly. This adds up to two automation policies: // app are managed properly. This adds up to two automation policies:
// one for the public names, and one for the internal names. If a catch-all // one for the public names, and one for the internal names. If a catch-all
// automation policy exists, it will be shallow-copied and used as the // automation policy exists, it will be shallow-copied and used as the
// base for the new ones (this is important for preserving behavior the // base for the new ones (this is important for preserving behavior the
// user intends to be "defaults"). // user intends to be "defaults").
func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, internalNames []string) error { func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []string) error {
// before we begin, loop through the existing automation policies // before we begin, loop through the existing automation policies
// and, for any ACMEIssuers we find, make sure they're filled in // and, for any ACMEIssuers we find, make sure they're filled in
// with default values that might be specified in our HTTP app; also // with default values that might be specified in our HTTP app; also
@@ -447,16 +441,23 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna
// set up default issuer -- honestly, this is only // set up default issuer -- honestly, this is only
// really necessary because the HTTP app is opinionated // really necessary because the HTTP app is opinionated
// and has settings which could be inferred as new // and has settings which could be inferred as new
// defaults for the ACMEIssuer in the TLS app // defaults for the ACMEIssuer in the TLS app (such as
if ap.Issuer == nil { // what the HTTP and HTTPS ports are)
ap.Issuer = new(caddytls.ACMEIssuer) if ap.Issuers == nil {
} var err error
if acmeIssuer, ok := ap.Issuer.(*caddytls.ACMEIssuer); ok { ap.Issuers, err = caddytls.DefaultIssuers(ctx)
err := app.fillInACMEIssuer(acmeIssuer)
if err != nil { if err != nil {
return err return err
} }
} }
for _, iss := range ap.Issuers {
if acmeIssuer, ok := iss.(acmeCapable); ok {
err := app.fillInACMEIssuer(acmeIssuer.GetACMEIssuer())
if err != nil {
return err
}
}
}
// while we're here, is this the catch-all/base policy? // while we're here, is this the catch-all/base policy?
if !foundBasePolicy && len(ap.Subjects) == 0 { if !foundBasePolicy && len(ap.Subjects) == 0 {
@@ -470,9 +471,16 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna
basePolicy = new(caddytls.AutomationPolicy) basePolicy = new(caddytls.AutomationPolicy)
} }
// if the basePolicy has an existing ACMEIssuer, let's // if the basePolicy has an existing ACMEIssuer (particularly to
// use it, otherwise we'll make one // include any type that embeds/wraps an ACMEIssuer), let's use it
baseACMEIssuer, _ := basePolicy.Issuer.(*caddytls.ACMEIssuer) // (I guess we just use the first one?), otherwise we'll make one
var baseACMEIssuer *caddytls.ACMEIssuer
for _, iss := range basePolicy.Issuers {
if acmeWrapper, ok := iss.(acmeCapable); ok {
baseACMEIssuer = acmeWrapper.GetACMEIssuer()
break
}
}
if baseACMEIssuer == nil { if baseACMEIssuer == nil {
// note that this happens if basePolicy.Issuer is nil // note that this happens if basePolicy.Issuer is nil
// OR if it is not nil but is not an ACMEIssuer // OR if it is not nil but is not an ACMEIssuer
@@ -481,7 +489,7 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna
// if there was a base policy to begin with, we already // if there was a base policy to begin with, we already
// filled in its issuer's defaults; if there wasn't, we // filled in its issuer's defaults; if there wasn't, we
// stil need to do that // still need to do that
if !foundBasePolicy { if !foundBasePolicy {
err := app.fillInACMEIssuer(baseACMEIssuer) err := app.fillInACMEIssuer(baseACMEIssuer)
if err != nil { if err != nil {
@@ -490,8 +498,20 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna
} }
// never overwrite any other issuer that might already be configured // never overwrite any other issuer that might already be configured
if basePolicy.Issuer == nil { if basePolicy.Issuers == nil {
basePolicy.Issuer = baseACMEIssuer var err error
basePolicy.Issuers, err = caddytls.DefaultIssuers(ctx)
if err != nil {
return err
}
for _, iss := range basePolicy.Issuers {
if acmeIssuer, ok := iss.(acmeCapable); ok {
err := app.fillInACMEIssuer(acmeIssuer.GetACMEIssuer())
if err != nil {
return err
}
}
}
} }
if !foundBasePolicy { if !foundBasePolicy {
@@ -499,7 +519,10 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna
// our base/catch-all policy - this will serve the // our base/catch-all policy - this will serve the
// public-looking names as well as any other names // public-looking names as well as any other names
// that don't match any other policy // that don't match any other policy
app.tlsApp.AddAutomationPolicy(basePolicy) err := app.tlsApp.AddAutomationPolicy(basePolicy)
if err != nil {
return err
}
} else { } else {
// a base policy already existed; we might have // a base policy already existed; we might have
// changed it, so re-provision it // changed it, so re-provision it
@@ -545,8 +568,7 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna
// of names that would normally use the production API; // of names that would normally use the production API;
// anyway, that gets into the weeds a bit... // anyway, that gets into the weeds a bit...
newPolicy.Subjects = internalNames newPolicy.Subjects = internalNames
newPolicy.Issuer = internalIssuer newPolicy.Issuers = []certmagic.Issuer{internalIssuer}
err := app.tlsApp.AddAutomationPolicy(newPolicy) err := app.tlsApp.AddAutomationPolicy(newPolicy)
if err != nil { if err != nil {
return err return err
@@ -630,3 +652,5 @@ func (app *App) automaticHTTPSPhase2() error {
app.allCertDomains = nil // no longer needed; allow GC to deallocate app.allCertDomains = nil // no longer needed; allow GC to deallocate
return nil return nil
} }
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
+39 -12
View File
@@ -52,11 +52,19 @@ type HTTPBasicAuth struct {
// memory for a longer time (this should not be a problem // memory for a longer time (this should not be a problem
// as long as your machine is not compromised, at which point // as long as your machine is not compromised, at which point
// all bets are off, since basicauth necessitates plaintext // all bets are off, since basicauth necessitates plaintext
// passwords being received over the wire anyway). // passwords being received over the wire anyway). Note that
// a cache hit does not mean it is a valid password.
HashCache *Cache `json:"hash_cache,omitempty"` HashCache *Cache `json:"hash_cache,omitempty"`
Accounts map[string]Account `json:"-"` Accounts map[string]Account `json:"-"`
Hash Comparer `json:"-"` Hash Comparer `json:"-"`
// fakePassword is used when a given user is not found,
// so that timing side-channels can be mitigated: it gives
// us something to hash and compare even if the user does
// not exist, which should have similar timing as a user
// account that does exist.
fakePassword []byte
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
@@ -84,6 +92,14 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
return fmt.Errorf("hash is required") return fmt.Errorf("hash is required")
} }
// if supported, generate a fake password we can compare against if needed
if hasher, ok := hba.Hash.(Hasher); ok {
hba.fakePassword, err = hasher.Hash([]byte("antitiming"), []byte("fakesalt"))
if err != nil {
return fmt.Errorf("generating anti-timing password hash: %v", err)
}
}
repl := caddy.NewReplacer() repl := caddy.NewReplacer()
// load account list // load account list
@@ -118,7 +134,7 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
if hba.HashCache != nil { if hba.HashCache != nil {
hba.HashCache.cache = make(map[string]bool) hba.HashCache.cache = make(map[string]bool)
hba.HashCache.mu = new(sync.Mutex) hba.HashCache.mu = new(sync.RWMutex)
} }
return nil return nil
@@ -132,16 +148,17 @@ func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request)
} }
account, accountExists := hba.Accounts[username] account, accountExists := hba.Accounts[username]
// don't return early if account does not exist; we want if !accountExists {
// to try to avoid side-channels that leak existence // don't return early if account does not exist; we want
// to try to avoid side-channels that leak existence, so
// we use a fake password to simulate realistic CPU cycles
account.password = hba.fakePassword
}
same, err := hba.correctPassword(account, []byte(plaintextPasswordStr)) same, err := hba.correctPassword(account, []byte(plaintextPasswordStr))
if err != nil { if err != nil || !same || !accountExists {
return hba.promptForCredentials(w, err) return hba.promptForCredentials(w, err)
} }
if !same || !accountExists {
return hba.promptForCredentials(w, nil)
}
return User{ID: username}, true, nil return User{ID: username}, true, nil
} }
@@ -160,13 +177,12 @@ func (hba HTTPBasicAuth) correctPassword(account Account, plaintextPassword []by
cacheKey := hex.EncodeToString(append(append(account.password, account.salt...), plaintextPassword...)) cacheKey := hex.EncodeToString(append(append(account.password, account.salt...), plaintextPassword...))
// fast track: if the result of the input is already cached, use it // fast track: if the result of the input is already cached, use it
hba.HashCache.mu.Lock() hba.HashCache.mu.RLock()
same, ok := hba.HashCache.cache[cacheKey] same, ok := hba.HashCache.cache[cacheKey]
hba.HashCache.mu.RUnlock()
if ok { if ok {
hba.HashCache.mu.Unlock()
return same, nil return same, nil
} }
hba.HashCache.mu.Unlock()
// slow track: do the expensive op, then add it to the cache // slow track: do the expensive op, then add it to the cache
same, err := compare() same, err := compare()
@@ -199,7 +215,7 @@ func (hba HTTPBasicAuth) promptForCredentials(w http.ResponseWriter, err error)
// helpful for secure password hashes which can be expensive to // helpful for secure password hashes which can be expensive to
// compute on every HTTP request. // compute on every HTTP request.
type Cache struct { type Cache struct {
mu *sync.Mutex mu *sync.RWMutex
// map of concatenated hashed password + plaintext password + salt, to result // map of concatenated hashed password + plaintext password + salt, to result
cache map[string]bool cache map[string]bool
@@ -224,6 +240,7 @@ func (c *Cache) makeRoom() {
// map with less code, this is a heavily skewed eviction // map with less code, this is a heavily skewed eviction
// strategy; generating random numbers is cheap and // strategy; generating random numbers is cheap and
// ensures a much better distribution. // ensures a much better distribution.
//nolint:gosec
rnd := weakrand.Intn(len(c.cache)) rnd := weakrand.Intn(len(c.cache))
i := 0 i := 0
for key := range c.cache { for key := range c.cache {
@@ -249,6 +266,16 @@ type Comparer interface {
Compare(hashedPassword, plaintextPassword, salt []byte) (bool, error) Compare(hashedPassword, plaintextPassword, salt []byte) (bool, error)
} }
// Hasher is a type that can generate a secure hash
// given a plaintext and optional salt (for algorithms
// that require a salt). Hashing modules which implement
// this interface can be used with the hash-password
// subcommand as well as benefitting from anti-timing
// features.
type Hasher interface {
Hash(plaintext, salt []byte) ([]byte, error)
}
// Account contains a username, password, and salt (if applicable). // Account contains a username, password, and salt (if applicable).
type Account struct { type Account struct {
// A user's username. // A user's username.
+12 -2
View File
@@ -16,11 +16,11 @@ package caddyauth
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"go.uber.org/zap"
) )
func init() { func init() {
@@ -30,6 +30,11 @@ func init() {
// Authentication is a middleware which provides user authentication. // Authentication is a middleware which provides user authentication.
// Rejects requests with HTTP 401 if the request is not authenticated. // Rejects requests with HTTP 401 if the request is not authenticated.
// //
// After a successful authentication, the placeholder
// `{http.auth.user.id}` will be set to the username, and also
// `{http.auth.user.*}` placeholders may be set for any authentication
// modules that provide user metadata.
//
// Its API is still experimental and may be subject to change. // Its API is still experimental and may be subject to change.
type Authentication struct { type Authentication struct {
// A set of authentication providers. If none are specified, // A set of authentication providers. If none are specified,
@@ -37,6 +42,8 @@ type Authentication struct {
ProvidersRaw caddy.ModuleMap `json:"providers,omitempty" caddy:"namespace=http.authentication.providers"` ProvidersRaw caddy.ModuleMap `json:"providers,omitempty" caddy:"namespace=http.authentication.providers"`
Providers map[string]Authenticator `json:"-"` Providers map[string]Authenticator `json:"-"`
logger *zap.Logger
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
@@ -49,6 +56,7 @@ func (Authentication) CaddyModule() caddy.ModuleInfo {
// Provision sets up a. // Provision sets up a.
func (a *Authentication) Provision(ctx caddy.Context) error { func (a *Authentication) Provision(ctx caddy.Context) error {
a.logger = ctx.Logger(a)
a.Providers = make(map[string]Authenticator) a.Providers = make(map[string]Authenticator)
mods, err := ctx.LoadModule(a, "ProvidersRaw") mods, err := ctx.LoadModule(a, "ProvidersRaw")
if err != nil { if err != nil {
@@ -67,7 +75,9 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
for provName, prov := range a.Providers { for provName, prov := range a.Providers {
user, authed, err = prov.Authenticate(w, r) user, authed, err = prov.Authenticate(w, r)
if err != nil { if err != nil {
log.Printf("[ERROR] Authenticating with %s: %v", provName, err) a.logger.Error("auth provider returned error",
zap.String("provider", provName),
zap.Error(err))
continue continue
} }
if authed { if authed {
+3 -5
View File
@@ -25,8 +25,6 @@ import (
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
caddycmd "github.com/caddyserver/caddy/v2/cmd" caddycmd "github.com/caddyserver/caddy/v2/cmd"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/scrypt"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
) )
@@ -72,7 +70,7 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
if terminal.IsTerminal(fd) { if terminal.IsTerminal(fd) {
// ensure the terminal state is restored on SIGINT // ensure the terminal state is restored on SIGINT
state, _ := terminal.GetState(fd) state, _ := terminal.GetState(fd)
c := make(chan os.Signal) c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt) signal.Notify(c, os.Interrupt)
go func() { go func() {
<-c <-c
@@ -116,11 +114,11 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
var hash []byte var hash []byte
switch algorithm { switch algorithm {
case "bcrypt": case "bcrypt":
hash, err = bcrypt.GenerateFromPassword(plaintext, bcrypt.DefaultCost) hash, err = BcryptHash{}.Hash(plaintext, nil)
case "scrypt": case "scrypt":
def := ScryptHash{} def := ScryptHash{}
def.SetDefaults() def.SetDefaults()
hash, err = scrypt.Key(plaintext, salt, def.N, def.R, def.P, def.KeyLength) hash, err = def.Hash(plaintext, salt)
default: default:
return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm) return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm)
} }
+12
View File
@@ -50,6 +50,11 @@ func (BcryptHash) Compare(hashed, plaintext, _ []byte) (bool, error) {
return true, nil return true, nil
} }
// Hash hashes plaintext using a random salt.
func (BcryptHash) Hash(plaintext, _ []byte) ([]byte, error) {
return bcrypt.GenerateFromPassword(plaintext, 14)
}
// ScryptHash implements the scrypt KDF as a hash. // ScryptHash implements the scrypt KDF as a hash.
type ScryptHash struct { type ScryptHash struct {
// scrypt's N parameter. If unset or 0, a safe default is used. // scrypt's N parameter. If unset or 0, a safe default is used.
@@ -113,6 +118,11 @@ func (s ScryptHash) Compare(hashed, plaintext, salt []byte) (bool, error) {
return false, nil return false, nil
} }
// Hash hashes plaintext using the given salt.
func (s ScryptHash) Hash(plaintext, salt []byte) ([]byte, error) {
return scrypt.Key(plaintext, salt, s.N, s.R, s.P, s.KeyLength)
}
func hashesMatch(pwdHash1, pwdHash2 []byte) bool { func hashesMatch(pwdHash1, pwdHash2 []byte) bool {
return subtle.ConstantTimeCompare(pwdHash1, pwdHash2) == 1 return subtle.ConstantTimeCompare(pwdHash1, pwdHash2) == 1
} }
@@ -121,5 +131,7 @@ func hashesMatch(pwdHash1, pwdHash2 []byte) bool {
var ( var (
_ Comparer = (*BcryptHash)(nil) _ Comparer = (*BcryptHash)(nil)
_ Comparer = (*ScryptHash)(nil) _ Comparer = (*ScryptHash)(nil)
_ Hasher = (*BcryptHash)(nil)
_ Hasher = (*ScryptHash)(nil)
_ caddy.Provisioner = (*ScryptHash)(nil) _ caddy.Provisioner = (*ScryptHash)(nil)
) )
+4 -4
View File
@@ -18,18 +18,15 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"io" "io"
weakrand "math/rand"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
"time"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
) )
func init() { func init() {
weakrand.Seed(time.Now().UnixNano())
caddy.RegisterModule(tlsPlaceholderWrapper{}) caddy.RegisterModule(tlsPlaceholderWrapper{})
} }
@@ -235,6 +232,8 @@ func (tlsPlaceholderWrapper) CaddyModule() caddy.ModuleInfo {
func (tlsPlaceholderWrapper) WrapListener(ln net.Listener) net.Listener { return ln } func (tlsPlaceholderWrapper) WrapListener(ln net.Listener) net.Listener { return ln }
func (tlsPlaceholderWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil }
const ( const (
// DefaultHTTPPort is the default port for HTTP. // DefaultHTTPPort is the default port for HTTP.
DefaultHTTPPort = 80 DefaultHTTPPort = 80
@@ -245,3 +244,4 @@ const (
// Interface guard // Interface guard
var _ caddy.ListenerWrapper = (*tlsPlaceholderWrapper)(nil) var _ caddy.ListenerWrapper = (*tlsPlaceholderWrapper)(nil)
var _ caddyfile.Unmarshaler = (*tlsPlaceholderWrapper)(nil)
+24
View File
@@ -15,6 +15,7 @@
package caddyhttp package caddyhttp
import ( import (
"crypto/x509/pkix"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
@@ -199,6 +200,27 @@ func (cr celHTTPRequest) Equal(other ref.Val) ref.Val {
func (celHTTPRequest) Type() ref.Type { return httpRequestCELType } func (celHTTPRequest) Type() ref.Type { return httpRequestCELType }
func (cr celHTTPRequest) Value() interface{} { return cr } func (cr celHTTPRequest) Value() interface{} { return cr }
var pkixNameCELType = types.NewTypeValue("pkix.Name", traits.ReceiverType)
// celPkixName wraps an pkix.Name with
// methods to satisfy the ref.Val interface.
type celPkixName struct{ *pkix.Name }
func (pn celPkixName) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
return pn.Name, nil
}
func (celPkixName) ConvertToType(typeVal ref.Type) ref.Val {
panic("not implemented")
}
func (pn celPkixName) Equal(other ref.Val) ref.Val {
if o, ok := other.Value().(string); ok {
return types.Bool(pn.Name.String() == o)
}
return types.ValOrErr(other, "%v is not comparable type", other)
}
func (celPkixName) Type() ref.Type { return pkixNameCELType }
func (pn celPkixName) Value() interface{} { return pn }
// celTypeAdapter can adapt our custom types to a CEL value. // celTypeAdapter can adapt our custom types to a CEL value.
type celTypeAdapter struct{} type celTypeAdapter struct{}
@@ -206,6 +228,8 @@ func (celTypeAdapter) NativeToValue(value interface{}) ref.Val {
switch v := value.(type) { switch v := value.(type) {
case celHTTPRequest: case celHTTPRequest:
return v return v
case pkix.Name:
return celPkixName{&v}
case time.Time: case time.Time:
// TODO: eliminate direct protobuf dependency, sigh -- just wrap stdlib time.Time instead... // TODO: eliminate direct protobuf dependency, sigh -- just wrap stdlib time.Time instead...
return types.Timestamp{Timestamp: &timestamp.Timestamp{Seconds: v.Unix(), Nanos: int32(v.Nanosecond())}} return types.Timestamp{Timestamp: &timestamp.Timestamp{Seconds: v.Unix(), Nanos: int32(v.Nanosecond())}}
+74 -1
View File
@@ -15,6 +15,11 @@
package caddyhttp package caddyhttp
import ( import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"net/http/httptest"
"testing" "testing"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
@@ -27,7 +32,7 @@ func TestMatchExpressionProvision(t *testing.T) {
wantErr bool wantErr bool
}{ }{
{ {
name: "boolean mtaches succeed", name: "boolean matches succeed",
expression: &MatchExpression{ expression: &MatchExpression{
Expr: "{http.request.uri.query} != ''", Expr: "{http.request.uri.query} != ''",
}, },
@@ -49,3 +54,71 @@ func TestMatchExpressionProvision(t *testing.T) {
}) })
} }
} }
func TestMatchExpressionMatch(t *testing.T) {
clientCert := []byte(`-----BEGIN CERTIFICATE-----
MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk
eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG
A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF
z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+
fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ
BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A
AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+
eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH
9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g=
-----END CERTIFICATE-----`)
tests := []struct {
name string
expression *MatchExpression
wantErr bool
wantResult bool
clientCertificate []byte
}{
{
name: "boolean matches succeed for placeholder http.request.tls.client.subject",
expression: &MatchExpression{
Expr: "{http.request.tls.client.subject} == 'CN=client.localdomain'",
},
clientCertificate: clientCert,
wantResult: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.expression.Provision(caddy.Context{}); (err != nil) != tt.wantErr {
t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tt.wantErr)
}
req := httptest.NewRequest("GET", "https://example.com/foo", nil)
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
if tt.clientCertificate != nil {
block, _ := pem.Decode(clientCert)
if block == nil {
t.Fatalf("failed to decode PEM certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("failed to decode PEM certificate: %v", err)
}
req.TLS = &tls.ConnectionState{
PeerCertificates: []*x509.Certificate{cert},
}
}
if tt.expression.Match(req) != tt.wantResult {
t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tt.wantResult, tt.expression)
}
})
}
}
+1 -1
View File
@@ -262,7 +262,7 @@ func acceptedEncodings(r *http.Request) []string {
return []string{} return []string{}
} }
var prefs []encodingPreference prefs := []encodingPreference{}
for _, accepted := range strings.Split(acceptEncHeader, ",") { for _, accepted := range strings.Split(acceptEncHeader, ",") {
parts := strings.Split(accepted, ";") parts := strings.Split(accepted, ";")
+8 -2
View File
@@ -16,14 +16,19 @@ package caddyhttp
import ( import (
"fmt" "fmt"
mathrand "math/rand" weakrand "math/rand"
"path" "path"
"runtime" "runtime"
"strings" "strings"
"time"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
) )
func init() {
weakrand.Seed(time.Now().UnixNano())
}
// Error is a convenient way for a Handler to populate the // Error is a convenient way for a Handler to populate the
// essential fields of a HandlerError. If err is itself a // essential fields of a HandlerError. If err is itself a
// HandlerError, then any essential fields that are not // HandlerError, then any essential fields that are not
@@ -92,7 +97,8 @@ func randString(n int, sameCase bool) string {
} }
b := make([]byte, n) b := make([]byte, n)
for i := range b { for i := range b {
b[i] = dict[mathrand.Int63()%int64(len(dict))] //nolint:gosec
b[i] = dict[weakrand.Int63()%int64(len(dict))]
} }
return string(b) return string(b)
} }
+15 -9
View File
@@ -25,6 +25,7 @@ import (
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"go.uber.org/zap"
) )
// Browse configures directory browsing. // Browse configures directory browsing.
@@ -35,12 +36,17 @@ type Browse struct {
template *template.Template template *template.Template
} }
func (fsrv *FileServer) serveBrowse(dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
fsrv.logger.Debug("browse enabled; listing directory contents",
zap.String("path", dirPath),
zap.String("root", root))
// navigation on the client-side gets messed up if the // navigation on the client-side gets messed up if the
// URL doesn't end in a trailing slash because hrefs like // URL doesn't end in a trailing slash because hrefs like
// "/b/c" on a path like "/a" end up going to "/b/c" instead // "/b/c" on a path like "/a" end up going to "/b/c" instead
// of "/a/b/c" - so we have to redirect in this case // of "/a/b/c" - so we have to redirect in this case
if !strings.HasSuffix(r.URL.Path, "/") { if !strings.HasSuffix(r.URL.Path, "/") {
fsrv.logger.Debug("redirecting to trailing slash to preserve hrefs", zap.String("request_path", r.URL.Path))
r.URL.Path += "/" r.URL.Path += "/"
http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently) http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently)
return nil return nil
@@ -55,7 +61,7 @@ func (fsrv *FileServer) serveBrowse(dirPath string, w http.ResponseWriter, r *ht
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
// calling path.Clean here prevents weird breadcrumbs when URL paths are sketchy like /%2e%2e%2f // calling path.Clean here prevents weird breadcrumbs when URL paths are sketchy like /%2e%2e%2f
listing, err := fsrv.loadDirectoryContents(dir, path.Clean(r.URL.Path), repl) listing, err := fsrv.loadDirectoryContents(dir, root, path.Clean(r.URL.Path), repl)
switch { switch {
case os.IsPermission(err): case os.IsPermission(err):
return caddyhttp.Error(http.StatusForbidden, err) return caddyhttp.Error(http.StatusForbidden, err)
@@ -82,22 +88,21 @@ func (fsrv *FileServer) serveBrowse(dirPath string, w http.ResponseWriter, r *ht
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
} }
buf.WriteTo(w) _, _ = buf.WriteTo(w)
return nil return nil
} }
func (fsrv *FileServer) loadDirectoryContents(dir *os.File, urlPath string, repl *caddy.Replacer) (browseListing, error) { func (fsrv *FileServer) loadDirectoryContents(dir *os.File, root, urlPath string, repl *caddy.Replacer) (browseListing, error) {
files, err := dir.Readdir(-1) files, err := dir.Readdir(-1)
if err != nil { if err != nil {
return browseListing{}, err return browseListing{}, err
} }
// determine if user can browse up another folder // user can presumably browse "up" to parent folder if path is longer than "/"
curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/")) canGoUp := len(urlPath) > 1
canGoUp := strings.HasPrefix(curPathDir, fsrv.Root)
return fsrv.directoryListing(files, canGoUp, urlPath, repl), nil return fsrv.directoryListing(files, canGoUp, root, urlPath, repl), nil
} }
// browseApplyQueryParams applies query parameters to the listing. // browseApplyQueryParams applies query parameters to the listing.
@@ -106,6 +111,7 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
sortParam := r.URL.Query().Get("sort") sortParam := r.URL.Query().Get("sort")
orderParam := r.URL.Query().Get("order") orderParam := r.URL.Query().Get("order")
limitParam := r.URL.Query().Get("limit") limitParam := r.URL.Query().Get("limit")
offsetParam := r.URL.Query().Get("offset")
// first figure out what to sort by // first figure out what to sort by
switch sortParam { switch sortParam {
@@ -130,7 +136,7 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
} }
// finally, apply the sorting and limiting // finally, apply the sorting and limiting
listing.applySortAndLimit(sortParam, orderParam, limitParam) listing.applySortAndLimit(sortParam, orderParam, limitParam, offsetParam)
} }
func (fsrv *FileServer) browseWriteJSON(listing browseListing) (*bytes.Buffer, error) { func (fsrv *FileServer) browseWriteJSON(listing browseListing) (*bytes.Buffer, error) {
+18 -18
View File
@@ -11,15 +11,15 @@ func BenchmarkBrowseWriteJSON(b *testing.B) {
fsrv := new(FileServer) fsrv := new(FileServer)
fsrv.Provision(caddy.Context{}) fsrv.Provision(caddy.Context{})
listing := browseListing{ listing := browseListing{
Name: "test", Name: "test",
Path: "test", Path: "test",
CanGoUp: false, CanGoUp: false,
Items: make([]fileInfo, 100), Items: make([]fileInfo, 100),
NumDirs: 42, NumDirs: 42,
NumFiles: 420, NumFiles: 420,
Sort: "", Sort: "",
Order: "", Order: "",
ItemsLimitedTo: 42, Limit: 42,
} }
b.ResetTimer() b.ResetTimer()
@@ -36,15 +36,15 @@ func BenchmarkBrowseWriteHTML(b *testing.B) {
template: template.New("test"), template: template.New("test"),
} }
listing := browseListing{ listing := browseListing{
Name: "test", Name: "test",
Path: "test", Path: "test",
CanGoUp: false, CanGoUp: false,
Items: make([]fileInfo, 100), Items: make([]fileInfo, 100),
NumDirs: 42, NumDirs: 42,
NumFiles: 420, NumFiles: 420,
Sort: "", Sort: "",
Order: "", Order: "",
ItemsLimitedTo: 42, Limit: 42,
} }
b.ResetTimer() b.ResetTimer()
+40 -32
View File
@@ -27,13 +27,11 @@ import (
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
) )
func (fsrv *FileServer) directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, repl *caddy.Replacer) browseListing { func (fsrv *FileServer) directoryListing(files []os.FileInfo, canGoUp bool, root, urlPath string, repl *caddy.Replacer) browseListing {
filesToHide := fsrv.transformHidePaths(repl) filesToHide := fsrv.transformHidePaths(repl)
var ( var dirCount, fileCount int
fileInfos []fileInfo fileInfos := []fileInfo{}
dirCount, fileCount int
)
for _, f := range files { for _, f := range files {
name := f.Name() name := f.Name()
@@ -42,7 +40,7 @@ func (fsrv *FileServer) directoryListing(files []os.FileInfo, canGoUp bool, urlP
continue continue
} }
isDir := f.IsDir() || isSymlinkTargetDir(f, fsrv.Root, urlPath) isDir := f.IsDir() || isSymlinkTargetDir(f, root, urlPath)
if isDir { if isDir {
name += "/" name += "/"
@@ -76,40 +74,41 @@ func (fsrv *FileServer) directoryListing(files []os.FileInfo, canGoUp bool, urlP
type browseListing struct { type browseListing struct {
// The name of the directory (the last element of the path). // The name of the directory (the last element of the path).
Name string Name string `json:"name"`
// The full path of the request. // The full path of the request.
Path string Path string `json:"path"`
// Whether the parent directory is browseable. // Whether the parent directory is browseable.
CanGoUp bool CanGoUp bool `json:"can_go_up"`
// The items (files and folders) in the path. // The items (files and folders) in the path.
Items []fileInfo Items []fileInfo `json:"items,omitempty"`
// The number of directories in the listing. // If ≠0 then Items starting from that many elements.
NumDirs int Offset int `json:"offset,omitempty"`
// The number of files (items that aren't directories) in the listing.
NumFiles int
// Sort column used
Sort string
// Sorting order
Order string
// If ≠0 then Items have been limited to that many elements. // If ≠0 then Items have been limited to that many elements.
ItemsLimitedTo int Limit int `json:"limit,omitempty"`
// The number of directories in the listing.
NumDirs int `json:"num_dirs"`
// The number of files (items that aren't directories) in the listing.
NumFiles int `json:"num_files"`
// Sort column used
Sort string `json:"sort,omitempty"`
// Sorting order
Order string `json:"order,omitempty"`
} }
// Breadcrumbs returns l.Path where every element maps // Breadcrumbs returns l.Path where every element maps
// the link to the text to display. // the link to the text to display.
func (l browseListing) Breadcrumbs() []crumb { func (l browseListing) Breadcrumbs() []crumb {
var result []crumb
if len(l.Path) == 0 { if len(l.Path) == 0 {
return result return []crumb{}
} }
// skip trailing slash // skip trailing slash
@@ -119,19 +118,19 @@ func (l browseListing) Breadcrumbs() []crumb {
} }
parts := strings.Split(lpath, "/") parts := strings.Split(lpath, "/")
for i := range parts { result := make([]crumb, len(parts))
txt := parts[i] for i, p := range parts {
if i == 0 && parts[i] == "" { if i == 0 && p == "" {
txt = "/" p = "/"
} }
lnk := strings.Repeat("../", len(parts)-i-1) lnk := strings.Repeat("../", len(parts)-i-1)
result = append(result, crumb{Link: lnk, Text: txt}) result[i] = crumb{Link: lnk, Text: p}
} }
return result return result
} }
func (l *browseListing) applySortAndLimit(sortParam, orderParam, limitParam string) { func (l *browseListing) applySortAndLimit(sortParam, orderParam, limitParam string, offsetParam string) {
l.Sort = sortParam l.Sort = sortParam
l.Order = orderParam l.Order = orderParam
@@ -159,11 +158,20 @@ func (l *browseListing) applySortAndLimit(sortParam, orderParam, limitParam stri
} }
} }
if offsetParam != "" {
offset, _ := strconv.Atoi(offsetParam)
if offset > 0 && offset <= len(l.Items) {
l.Items = l.Items[offset:]
l.Offset = offset
}
}
if limitParam != "" { if limitParam != "" {
limit, _ := strconv.Atoi(limitParam) limit, _ := strconv.Atoi(limitParam)
if limit > 0 && limit <= len(l.Items) { if limit > 0 && limit <= len(l.Items) {
l.Items = l.Items[:limit] l.Items = l.Items[:limit]
l.ItemsLimitedTo = limit l.Limit = limit
} }
} }
} }
@@ -0,0 +1,40 @@
package fileserver
import (
"testing"
)
func TestBreadcrumbs(t *testing.T) {
testdata := []struct {
path string
expected []crumb
}{
{"", []crumb{}},
{"/", []crumb{{Text: "/"}}},
{"foo/bar/baz", []crumb{
{Link: "../../", Text: "foo"},
{Link: "../", Text: "bar"},
{Link: "", Text: "baz"},
}},
{"/qux/quux/corge/", []crumb{
{Link: "../../../", Text: "/"},
{Link: "../../", Text: "qux"},
{Link: "../", Text: "quux"},
{Link: "", Text: "corge"},
}},
}
for _, d := range testdata {
l := browseListing{Path: d.path}
actual := l.Breadcrumbs()
if len(actual) != len(d.expected) {
t.Errorf("wrong size output, got %d elements but expected %d", len(actual), len(d.expected))
continue
}
for i, c := range actual {
if c != d.expected[i] {
t.Errorf("got %#v but expected %#v at index %d", c, d.expected[i], i)
}
}
}
}
+14 -14
View File
@@ -283,8 +283,8 @@ footer {
<div id="summary"> <div id="summary">
<span class="meta-item"><b>{{.NumDirs}}</b> director{{if eq 1 .NumDirs}}y{{else}}ies{{end}}</span> <span class="meta-item"><b>{{.NumDirs}}</b> director{{if eq 1 .NumDirs}}y{{else}}ies{{end}}</span>
<span class="meta-item"><b>{{.NumFiles}}</b> file{{if ne 1 .NumFiles}}s{{end}}</span> <span class="meta-item"><b>{{.NumFiles}}</b> file{{if ne 1 .NumFiles}}s{{end}}</span>
{{- if ne 0 .ItemsLimitedTo}} {{- if ne 0 .Limit}}
<span class="meta-item">(of which only <b>{{.ItemsLimitedTo}}</b> are displayed)</span> <span class="meta-item">(of which only <b>{{.Limit}}</b> are displayed)</span>
{{- end}} {{- end}}
<span class="meta-item"><input type="text" placeholder="filter" id="filter" onkeyup='filter()'></span> <span class="meta-item"><input type="text" placeholder="filter" id="filter" onkeyup='filter()'></span>
</div> </div>
@@ -296,37 +296,37 @@ footer {
<th></th> <th></th>
<th> <th>
{{- if and (eq .Sort "namedirfirst") (ne .Order "desc")}} {{- if and (eq .Sort "namedirfirst") (ne .Order "desc")}}
<a href="?sort=namedirfirst&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}" class="icon"><svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a> <a href="?sort=namedirfirst&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}" class="icon"><svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
{{- else if and (eq .Sort "namedirfirst") (ne .Order "asc")}} {{- else if and (eq .Sort "namedirfirst") (ne .Order "asc")}}
<a href="?sort=namedirfirst&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}" class="icon"><svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a> <a href="?sort=namedirfirst&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}" class="icon"><svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
{{- else}} {{- else}}
<a href="?sort=namedirfirst&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}" class="icon sort"><svg class="top" width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg><svg class="bottom" width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a> <a href="?sort=namedirfirst&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}" class="icon sort"><svg class="top" width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg><svg class="bottom" width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
{{- end}} {{- end}}
{{- if and (eq .Sort "name") (ne .Order "desc")}} {{- if and (eq .Sort "name") (ne .Order "desc")}}
<a href="?sort=name&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Name <svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a> <a href="?sort=name&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">Name <svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
{{- else if and (eq .Sort "name") (ne .Order "asc")}} {{- else if and (eq .Sort "name") (ne .Order "asc")}}
<a href="?sort=name&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Name <svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a> <a href="?sort=name&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">Name <svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
{{- else}} {{- else}}
<a href="?sort=name&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Name</a> <a href="?sort=name&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">Name</a>
{{- end}} {{- end}}
</th> </th>
<th> <th>
{{- if and (eq .Sort "size") (ne .Order "desc")}} {{- if and (eq .Sort "size") (ne .Order "desc")}}
<a href="?sort=size&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Size <svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a> <a href="?sort=size&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">Size <svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
{{- else if and (eq .Sort "size") (ne .Order "asc")}} {{- else if and (eq .Sort "size") (ne .Order "asc")}}
<a href="?sort=size&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Size <svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a> <a href="?sort=size&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">Size <svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
{{- else}} {{- else}}
<a href="?sort=size&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Size</a> <a href="?sort=size&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">Size</a>
{{- end}} {{- end}}
</th> </th>
<th class="hideable"> <th class="hideable">
{{- if and (eq .Sort "time") (ne .Order "desc")}} {{- if and (eq .Sort "time") (ne .Order "desc")}}
<a href="?sort=time&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified <svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a> <a href="?sort=time&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">Modified <svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
{{- else if and (eq .Sort "time") (ne .Order "asc")}} {{- else if and (eq .Sort "time") (ne .Order "asc")}}
<a href="?sort=time&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified <svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a> <a href="?sort=time&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">Modified <svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
{{- else}} {{- else}}
<a href="?sort=time&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified</a> <a href="?sort=time&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">Modified</a>
{{- end}} {{- end}}
</th> </th>
<th class="hideable"></th> <th class="hideable"></th>
@@ -15,6 +15,7 @@
package fileserver package fileserver
import ( import (
"path/filepath"
"strings" "strings"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
@@ -85,7 +86,14 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// hide the Caddyfile (and any imported Caddyfiles) // hide the Caddyfile (and any imported Caddyfiles)
if configFiles := h.Caddyfiles(); len(configFiles) > 0 { if configFiles := h.Caddyfiles(); len(configFiles) > 0 {
for _, file := range configFiles { for _, file := range configFiles {
file = filepath.Clean(file)
if !fileHidden(file, fsrv.Hide) { if !fileHidden(file, fsrv.Hide) {
// if there's no path separator, the file server module will hide all
// files by that name, rather than a specific one; but we want to hide
// only this specific file, so ensure there's always a path separator
if !strings.Contains(file, separator) {
file = "." + separator + file
}
fsrv.Hide = append(fsrv.Hide, file) fsrv.Hide = append(fsrv.Hide, file)
} }
} }
+86 -41
View File
@@ -34,7 +34,7 @@ func init() {
// MatchFile is an HTTP request matcher that can match // MatchFile is an HTTP request matcher that can match
// requests based upon file existence. // requests based upon file existence.
// //
// Upon matching, two new placeholders will be made // Upon matching, three new placeholders will be made
// available: // available:
// //
// - `{http.matchers.file.relative}` The root-relative // - `{http.matchers.file.relative}` The root-relative
@@ -42,6 +42,10 @@ func init() {
// requests. // requests.
// - `{http.matchers.file.absolute}` The absolute path // - `{http.matchers.file.absolute}` The absolute path
// of the matched file. // of the matched file.
// - `{http.matchers.file.type}` Set to "directory" if
// the matched file is a directory, "file" otherwise.
// - `{http.matchers.file.remainder}` Set to the remainder
// of the path if the path was split by `split_path`.
type MatchFile struct { type MatchFile struct {
// The root directory, used for creating absolute // The root directory, used for creating absolute
// file paths, and required when working with // file paths, and required when working with
@@ -117,11 +121,13 @@ func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.ArgErr() return d.ArgErr()
} }
m.TryPolicy = d.Val() m.TryPolicy = d.Val()
case "split": case "split_path":
m.SplitPath = d.RemainingArgs() m.SplitPath = d.RemainingArgs()
if len(m.SplitPath) == 0 { if len(m.SplitPath) == 0 {
return d.ArgErr() return d.ArgErr()
} }
default:
return d.Errf("unrecognized subdirective: %s", d.Val())
} }
} }
} }
@@ -151,25 +157,19 @@ func (m MatchFile) Validate() error {
} }
// Match returns true if r matches m. Returns true // Match returns true if r matches m. Returns true
// if a file was matched. If so, two placeholders // if a file was matched. If so, four placeholders
// will be available: // will be available:
// - http.matchers.file.relative // - http.matchers.file.relative
// - http.matchers.file.absolute // - http.matchers.file.absolute
// - http.matchers.file.type
// - http.matchers.file.remainder
func (m MatchFile) Match(r *http.Request) bool { func (m MatchFile) Match(r *http.Request) bool {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) return m.selectFile(r)
rel, abs, matched := m.selectFile(r)
if matched {
repl.Set("http.matchers.file.relative", rel)
repl.Set("http.matchers.file.absolute", abs)
}
return matched
} }
// selectFile chooses a file according to m.TryPolicy by appending // selectFile chooses a file according to m.TryPolicy by appending
// the paths in m.TryFiles to m.Root, with placeholder replacements. // the paths in m.TryFiles to m.Root, with placeholder replacements.
// It returns the root-relative path to the matched file, the full func (m MatchFile) selectFile(r *http.Request) (matched bool) {
// or absolute path, and whether a match was made.
func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
root := repl.ReplaceAll(m.Root, ".") root := repl.ReplaceAll(m.Root, ".")
@@ -181,13 +181,36 @@ func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) {
m.TryFiles = []string{r.URL.Path} m.TryFiles = []string{r.URL.Path}
} }
// common preparation of the file into parts
prepareFilePath := func(file string) (suffix, fullpath, remainder string) {
suffix, remainder = m.firstSplit(path.Clean(repl.ReplaceAll(file, "")))
if strings.HasSuffix(file, "/") {
suffix += "/"
}
fullpath = sanitizedPathJoin(root, suffix)
return
}
// sets up the placeholders for the matched file
setPlaceholders := func(info os.FileInfo, rel string, abs string, remainder string) {
repl.Set("http.matchers.file.relative", rel)
repl.Set("http.matchers.file.absolute", abs)
repl.Set("http.matchers.file.remainder", remainder)
fileType := "file"
if info.IsDir() {
fileType = "directory"
}
repl.Set("http.matchers.file.type", fileType)
}
switch m.TryPolicy { switch m.TryPolicy {
case "", tryPolicyFirstExist: case "", tryPolicyFirstExist:
for _, f := range m.TryFiles { for _, f := range m.TryFiles {
suffix := m.firstSplit(path.Clean(repl.ReplaceAll(f, ""))) suffix, fullpath, remainder := prepareFilePath(f)
fullpath := sanitizedPathJoin(root, suffix) if info, exists := strictFileExists(fullpath); exists {
if strictFileExists(fullpath) { setPlaceholders(info, suffix, fullpath, remainder)
return suffix, fullpath, true return true
} }
} }
@@ -195,50 +218,59 @@ func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) {
var largestSize int64 var largestSize int64
var largestFilename string var largestFilename string
var largestSuffix string var largestSuffix string
var remainder string
var info os.FileInfo
for _, f := range m.TryFiles { for _, f := range m.TryFiles {
suffix := m.firstSplit(path.Clean(repl.ReplaceAll(f, ""))) suffix, fullpath, splitRemainder := prepareFilePath(f)
fullpath := sanitizedPathJoin(root, suffix)
info, err := os.Stat(fullpath) info, err := os.Stat(fullpath)
if err == nil && info.Size() > largestSize { if err == nil && info.Size() > largestSize {
largestSize = info.Size() largestSize = info.Size()
largestFilename = fullpath largestFilename = fullpath
largestSuffix = suffix largestSuffix = suffix
remainder = splitRemainder
} }
} }
return largestSuffix, largestFilename, true setPlaceholders(info, largestSuffix, largestFilename, remainder)
return true
case tryPolicySmallestSize: case tryPolicySmallestSize:
var smallestSize int64 var smallestSize int64
var smallestFilename string var smallestFilename string
var smallestSuffix string var smallestSuffix string
var remainder string
var info os.FileInfo
for _, f := range m.TryFiles { for _, f := range m.TryFiles {
suffix := m.firstSplit(path.Clean(repl.ReplaceAll(f, ""))) suffix, fullpath, splitRemainder := prepareFilePath(f)
fullpath := sanitizedPathJoin(root, suffix)
info, err := os.Stat(fullpath) info, err := os.Stat(fullpath)
if err == nil && (smallestSize == 0 || info.Size() < smallestSize) { if err == nil && (smallestSize == 0 || info.Size() < smallestSize) {
smallestSize = info.Size() smallestSize = info.Size()
smallestFilename = fullpath smallestFilename = fullpath
smallestSuffix = suffix smallestSuffix = suffix
remainder = splitRemainder
} }
} }
return smallestSuffix, smallestFilename, true setPlaceholders(info, smallestSuffix, smallestFilename, remainder)
return true
case tryPolicyMostRecentlyMod: case tryPolicyMostRecentlyMod:
var recentDate time.Time var recentDate time.Time
var recentFilename string var recentFilename string
var recentSuffix string var recentSuffix string
var remainder string
var info os.FileInfo
for _, f := range m.TryFiles { for _, f := range m.TryFiles {
suffix := m.firstSplit(path.Clean(repl.ReplaceAll(f, ""))) suffix, fullpath, splitRemainder := prepareFilePath(f)
fullpath := sanitizedPathJoin(root, suffix)
info, err := os.Stat(fullpath) info, err := os.Stat(fullpath)
if err == nil && if err == nil &&
(recentDate.IsZero() || info.ModTime().After(recentDate)) { (recentDate.IsZero() || info.ModTime().After(recentDate)) {
recentDate = info.ModTime() recentDate = info.ModTime()
recentFilename = fullpath recentFilename = fullpath
recentSuffix = suffix recentSuffix = suffix
remainder = splitRemainder
} }
} }
return recentSuffix, recentFilename, true setPlaceholders(info, recentSuffix, recentFilename, remainder)
return true
} }
return return
@@ -250,7 +282,7 @@ func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) {
// the file must also be a directory; if it does // the file must also be a directory; if it does
// NOT end in a forward slash, the file must NOT // NOT end in a forward slash, the file must NOT
// be a directory. // be a directory.
func strictFileExists(file string) bool { func strictFileExists(file string) (os.FileInfo, bool) {
stat, err := os.Stat(file) stat, err := os.Stat(file)
if err != nil { if err != nil {
// in reality, this can be any error // in reality, this can be any error
@@ -261,36 +293,49 @@ func strictFileExists(file string) bool {
// the file exists, so we just treat any // the file exists, so we just treat any
// error as if it does not exist; see // error as if it does not exist; see
// https://stackoverflow.com/a/12518877/1048862 // https://stackoverflow.com/a/12518877/1048862
return false return nil, false
} }
if strings.HasSuffix(file, "/") { if strings.HasSuffix(file, separator) {
// by convention, file paths ending // by convention, file paths ending
// in a slash must be a directory // in a path separator must be a directory
return stat.IsDir() return stat, stat.IsDir()
} }
// by convention, file paths NOT ending // by convention, file paths NOT ending
// in a slash must NOT be a directory // in a path separator must NOT be a directory
return !stat.IsDir() return stat, !stat.IsDir()
} }
// firstSplit returns the first result where the path // firstSplit returns the first result where the path
// can be split in two by a value in m.SplitPath. The // can be split in two by a value in m.SplitPath. The
// result is the first piece of the path that ends with // return values are the first piece of the path that
// in the split value. Returns the path as-is if the // ends with the split substring and the remainder.
// path cannot be split. // If the path cannot be split, the path is returned
func (m MatchFile) firstSplit(path string) string { // as-is (with no remainder).
lowerPath := strings.ToLower(path) func (m MatchFile) firstSplit(path string) (splitPart, remainder string) {
for _, split := range m.SplitPath { for _, split := range m.SplitPath {
if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 { if idx := indexFold(path, split); idx > -1 {
pos := idx + len(split) pos := idx + len(split)
// skip the split if it's not the final part of the filename // skip the split if it's not the final part of the filename
if pos != len(path) && !strings.HasPrefix(path[pos:], "/") { if pos != len(path) && !strings.HasPrefix(path[pos:], "/") {
continue continue
} }
return path[:pos] return path[:pos], path[pos:]
} }
} }
return path return path, ""
}
// There is no strings.IndexFold() function like there is strings.EqualFold(),
// but we can use strings.EqualFold() to build our own case-insensitive
// substring search (as of Go 1.14).
func indexFold(haystack, needle string) int {
nlen := len(needle)
for i := 0; i+nlen < len(haystack); i++ {
if strings.EqualFold(haystack[i:i+nlen], needle) {
return i
}
}
return -1
} }
const ( const (
+157 -41
View File
@@ -22,69 +22,64 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
) )
func TestPhpFileMatcher(t *testing.T) { func TestFileMatcher(t *testing.T) {
for i, tc := range []struct { for i, tc := range []struct {
path string path string
expectedPath string expectedPath string
matched bool expectedType string
matched bool
}{ }{
{ {
path: "/index.php", path: "/foo.txt",
expectedPath: "/index.php", expectedPath: "/foo.txt",
matched: true, expectedType: "file",
matched: true,
}, },
{ {
path: "/index.php/somewhere", path: "/foo.txt/",
expectedPath: "/index.php", expectedPath: "/foo.txt",
matched: true, expectedType: "file",
matched: true,
}, },
{ {
path: "/remote.php", path: "/foodir",
expectedPath: "/remote.php", expectedPath: "/foodir/",
matched: true, expectedType: "directory",
matched: true,
}, },
{ {
path: "/remote.php/somewhere", path: "/foodir/",
expectedPath: "/remote.php", expectedPath: "/foodir/",
matched: true, expectedType: "directory",
matched: true,
}, },
{ {
path: "/missingfile.php", path: "/foodir/foo.txt",
expectedPath: "/foodir/foo.txt",
expectedType: "file",
matched: true,
},
{
path: "/missingfile.php",
matched: false, matched: false,
}, },
{
path: "/notphp.php.txt",
expectedPath: "/notphp.php.txt",
matched: true,
},
{
path: "/notphp.php.txt/",
expectedPath: "/notphp.php.txt",
matched: true,
},
{
path: "/notphp.php.txt.suffixed",
matched: false,
},
{
path: "/foo.php.php/index.php",
expectedPath: "/foo.php.php/index.php",
matched: true,
},
} { } {
m := &MatchFile{ m := &MatchFile{
Root: "./testdata", Root: "./testdata",
TryFiles: []string{"{http.request.uri.path}"}, TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"},
SplitPath: []string{".php"},
} }
req := &http.Request{URL: &url.URL{Path: tc.path}} u, err := url.Parse(tc.path)
if err != nil {
t.Fatalf("Test %d: parsing path: %v", i, err)
}
req := &http.Request{URL: u}
repl := caddyhttp.NewTestReplacer(req) repl := caddyhttp.NewTestReplacer(req)
result := m.Match(req) result := m.Match(req)
if result != tc.matched { if result != tc.matched {
t.Fatalf("Test %d: match bool result: %v, expected: %v", i, result, tc.matched) t.Fatalf("Test %d: expected match=%t, got %t", i, tc.matched, result)
} }
rel, ok := repl.Get("http.matchers.file.relative") rel, ok := repl.Get("http.matchers.file.relative")
@@ -98,5 +93,126 @@ func TestPhpFileMatcher(t *testing.T) {
if rel != tc.expectedPath { if rel != tc.expectedPath {
t.Fatalf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath) t.Fatalf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath)
} }
fileType, ok := repl.Get("http.matchers.file.type")
if fileType != tc.expectedType {
t.Fatalf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType)
}
}
}
func TestPHPFileMatcher(t *testing.T) {
for i, tc := range []struct {
path string
expectedPath string
expectedType string
matched bool
}{
{
path: "/index.php",
expectedPath: "/index.php",
expectedType: "file",
matched: true,
},
{
path: "/index.php/somewhere",
expectedPath: "/index.php",
expectedType: "file",
matched: true,
},
{
path: "/remote.php",
expectedPath: "/remote.php",
expectedType: "file",
matched: true,
},
{
path: "/remote.php/somewhere",
expectedPath: "/remote.php",
expectedType: "file",
matched: true,
},
{
path: "/missingfile.php",
matched: false,
},
{
path: "/notphp.php.txt",
expectedPath: "/notphp.php.txt",
expectedType: "file",
matched: true,
},
{
path: "/notphp.php.txt/",
expectedPath: "/notphp.php.txt",
expectedType: "file",
matched: true,
},
{
path: "/notphp.php.txt.suffixed",
matched: false,
},
{
path: "/foo.php.php/index.php",
expectedPath: "/foo.php.php/index.php",
expectedType: "file",
matched: true,
},
{
// See https://github.com/caddyserver/caddy/issues/3623
path: "/%E2%C3",
expectedPath: "/%E2%C3",
expectedType: "file",
matched: false,
},
} {
m := &MatchFile{
Root: "./testdata",
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"},
SplitPath: []string{".php"},
}
u, err := url.Parse(tc.path)
if err != nil {
t.Fatalf("Test %d: parsing path: %v", i, err)
}
req := &http.Request{URL: u}
repl := caddyhttp.NewTestReplacer(req)
result := m.Match(req)
if result != tc.matched {
t.Fatalf("Test %d: expected match=%t, got %t", i, tc.matched, result)
}
rel, ok := repl.Get("http.matchers.file.relative")
if !ok && result {
t.Fatalf("Test %d: expected replacer value", i)
}
if !result {
continue
}
if rel != tc.expectedPath {
t.Fatalf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath)
}
fileType, ok := repl.Get("http.matchers.file.type")
if fileType != tc.expectedType {
t.Fatalf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType)
}
}
}
func TestFirstSplit(t *testing.T) {
m := MatchFile{SplitPath: []string{".php"}}
actual, remainder := m.firstSplit("index.PHP/somewhere")
expected := "index.PHP"
expectedRemainder := "/somewhere"
if actual != expected {
t.Errorf("Expected split %s but got %s", expected, actual)
}
if remainder != expectedRemainder {
t.Errorf("Expected remainder %s but got %s", expectedRemainder, remainder)
} }
} }
+117 -30
View File
@@ -23,7 +23,6 @@ import (
"mime" "mime"
"net/http" "net/http"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@@ -32,6 +31,7 @@ import (
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"go.uber.org/zap"
) )
func init() { func init() {
@@ -47,7 +47,20 @@ type FileServer struct {
Root string `json:"root,omitempty"` Root string `json:"root,omitempty"`
// A list of files or folders to hide; the file server will pretend as if // A list of files or folders to hide; the file server will pretend as if
// they don't exist. Accepts globular patterns like "*.hidden" or "/foo/*/bar". // they don't exist. Accepts globular patterns like "*.ext" or "/foo/*/bar"
// as well as placeholders. Because site roots can be dynamic, this list
// uses file system paths, not request paths. To clarify, the base of
// relative paths is the current working directory, NOT the site root.
//
// Entries without a path separator (`/` or `\` depending on OS) will match
// any file or directory of that name regardless of its path. To hide only a
// specific file with a name that may not be unique, always use a path
// separator. For example, to hide all files or folder trees named "hidden",
// put "hidden" in the list. To hide only ./hidden, put "./hidden" in the list.
//
// When possible, all paths are resolved to their absolute form before
// comparisons are made. For maximum clarity and explictness, use complete,
// absolute paths; or, for greater portability, use relative paths instead.
Hide []string `json:"hide,omitempty"` Hide []string `json:"hide,omitempty"`
// The names of files to try as index files if a folder is requested. // The names of files to try as index files if a folder is requested.
@@ -65,6 +78,8 @@ type FileServer struct {
// it will invoke the next handler in the chain instead of returning // it will invoke the next handler in the chain instead of returning
// a 404 error. By default, this is false (disabled). // a 404 error. By default, this is false (disabled).
PassThru bool `json:"pass_thru,omitempty"` PassThru bool `json:"pass_thru,omitempty"`
logger *zap.Logger
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
@@ -77,6 +92,8 @@ func (FileServer) CaddyModule() caddy.ModuleInfo {
// Provision sets up the static files responder. // Provision sets up the static files responder.
func (fsrv *FileServer) Provision(ctx caddy.Context) error { func (fsrv *FileServer) Provision(ctx caddy.Context) error {
fsrv.logger = ctx.Logger(fsrv)
if fsrv.Root == "" { if fsrv.Root == "" {
fsrv.Root = "{http.vars.root}" fsrv.Root = "{http.vars.root}"
} }
@@ -102,6 +119,16 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error {
fsrv.Browse.template = tpl fsrv.Browse.template = tpl
} }
// for hide paths that are static (i.e. no placeholders), we can transform them into
// absolute paths before the server starts for very slight performance improvement
for i, h := range fsrv.Hide {
if !strings.Contains(h, "{") && strings.Contains(h, separator) {
if abs, err := filepath.Abs(h); err == nil {
fsrv.Hide[i] = abs
}
}
}
return nil return nil
} }
@@ -114,6 +141,11 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
suffix := repl.ReplaceAll(r.URL.Path, "") suffix := repl.ReplaceAll(r.URL.Path, "")
filename := sanitizedPathJoin(root, suffix) filename := sanitizedPathJoin(root, suffix)
fsrv.logger.Debug("sanitized path join",
zap.String("site_root", root),
zap.String("request_path", suffix),
zap.String("result", filename))
// get information about the file // get information about the file
info, err := os.Stat(filename) info, err := os.Stat(filename)
if err != nil { if err != nil {
@@ -135,6 +167,9 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
indexPath := sanitizedPathJoin(filename, indexPage) indexPath := sanitizedPathJoin(filename, indexPage)
if fileHidden(indexPath, filesToHide) { if fileHidden(indexPath, filesToHide) {
// pretend this file doesn't exist // pretend this file doesn't exist
fsrv.logger.Debug("hiding index file",
zap.String("filename", indexPath),
zap.Strings("files_to_hide", filesToHide))
continue continue
} }
@@ -154,6 +189,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
info = indexInfo info = indexInfo
filename = indexPath filename = indexPath
implicitIndexFile = true implicitIndexFile = true
fsrv.logger.Debug("located index file", zap.String("filename", filename))
break break
} }
} }
@@ -161,8 +197,11 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
// if still referencing a directory, delegate // if still referencing a directory, delegate
// to browse or return an error // to browse or return an error
if info.IsDir() { if info.IsDir() {
fsrv.logger.Debug("no index file in directory",
zap.String("path", filename),
zap.Strings("index_filenames", fsrv.IndexNames))
if fsrv.Browse != nil && !fileHidden(filename, filesToHide) { if fsrv.Browse != nil && !fileHidden(filename, filesToHide) {
return fsrv.serveBrowse(filename, w, r, next) return fsrv.serveBrowse(root, filename, w, r, next)
} }
return fsrv.notFound(w, r, next) return fsrv.notFound(w, r, next)
} }
@@ -172,6 +211,9 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
// one last check to ensure the file isn't hidden (we might // one last check to ensure the file isn't hidden (we might
// have changed the filename from when we last checked) // have changed the filename from when we last checked)
if fileHidden(filename, filesToHide) { if fileHidden(filename, filesToHide) {
fsrv.logger.Debug("hiding file",
zap.String("filename", filename),
zap.Strings("files_to_hide", filesToHide))
return fsrv.notFound(w, r, next) return fsrv.notFound(w, r, next)
} }
@@ -181,12 +223,16 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
// in HTML (see https://github.com/caddyserver/caddy/issues/2741) // in HTML (see https://github.com/caddyserver/caddy/issues/2741)
if fsrv.CanonicalURIs == nil || *fsrv.CanonicalURIs { if fsrv.CanonicalURIs == nil || *fsrv.CanonicalURIs {
if implicitIndexFile && !strings.HasSuffix(r.URL.Path, "/") { if implicitIndexFile && !strings.HasSuffix(r.URL.Path, "/") {
fsrv.logger.Debug("redirecting to canonical URI (adding trailing slash for directory)", zap.String("path", r.URL.Path))
return redirect(w, r, r.URL.Path+"/") return redirect(w, r, r.URL.Path+"/")
} else if !implicitIndexFile && strings.HasSuffix(r.URL.Path, "/") { } else if !implicitIndexFile && strings.HasSuffix(r.URL.Path, "/") {
fsrv.logger.Debug("redirecting to canonical URI (removing trailing slash for file)", zap.String("path", r.URL.Path))
return redirect(w, r, r.URL.Path[:len(r.URL.Path)-1]) return redirect(w, r, r.URL.Path[:len(r.URL.Path)-1])
} }
} }
fsrv.logger.Debug("opening file", zap.String("filename", filename))
// open the file // open the file
file, err := fsrv.openFile(filename, w) file, err := fsrv.openFile(filename, w)
if err != nil { if err != nil {
@@ -226,8 +272,8 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
} }
} }
w.WriteHeader(statusCode) w.WriteHeader(statusCode)
if r.Method != "HEAD" { if r.Method != http.MethodHead {
io.Copy(w, file) _, _ = io.Copy(w, file)
} }
return nil return nil
} }
@@ -256,6 +302,7 @@ func (fsrv *FileServer) openFile(filename string, w http.ResponseWriter) (*os.Fi
} }
// maybe the server is under load and ran out of file descriptors? // maybe the server is under load and ran out of file descriptors?
// have client wait arbitrary seconds to help prevent a stampede // have client wait arbitrary seconds to help prevent a stampede
//nolint:gosec
backoff := weakrand.Intn(maxBackoff-minBackoff) + minBackoff backoff := weakrand.Intn(maxBackoff-minBackoff) + minBackoff
w.Header().Set("Retry-After", strconv.Itoa(backoff)) w.Header().Set("Retry-After", strconv.Itoa(backoff))
return nil, caddyhttp.Error(http.StatusServiceUnavailable, err) return nil, caddyhttp.Error(http.StatusServiceUnavailable, err)
@@ -274,12 +321,12 @@ func mapDirOpenError(originalErr error, name string) error {
return originalErr return originalErr
} }
parts := strings.Split(name, string(filepath.Separator)) parts := strings.Split(name, separator)
for i := range parts { for i := range parts {
if parts[i] == "" { if parts[i] == "" {
continue continue
} }
fi, err := os.Stat(strings.Join(parts[:i+1], string(filepath.Separator))) fi, err := os.Stat(strings.Join(parts[:i+1], separator))
if err != nil { if err != nil {
return originalErr return originalErr
} }
@@ -291,12 +338,19 @@ func mapDirOpenError(originalErr error, name string) error {
return originalErr return originalErr
} }
// transformHidePaths performs replacements for all the elements of // transformHidePaths performs replacements for all the elements of fsrv.Hide and
// fsrv.Hide and returns a new list of the transformed values. // makes them absolute paths (if they contain a path separator), then returns a
// new list of the transformed values.
func (fsrv *FileServer) transformHidePaths(repl *caddy.Replacer) []string { func (fsrv *FileServer) transformHidePaths(repl *caddy.Replacer) []string {
hide := make([]string, len(fsrv.Hide)) hide := make([]string, len(fsrv.Hide))
for i := range fsrv.Hide { for i := range fsrv.Hide {
hide[i] = repl.ReplaceAll(fsrv.Hide[i], "") hide[i] = repl.ReplaceAll(fsrv.Hide[i], "")
if strings.Contains(hide[i], separator) {
abs, err := filepath.Abs(hide[i])
if err == nil {
hide[i] = abs
}
}
} }
return hide return hide
} }
@@ -323,34 +377,64 @@ func sanitizedPathJoin(root, reqPath string) string {
if root == "" { if root == "" {
root = "." root = "."
} }
return filepath.Join(root, filepath.FromSlash(path.Clean("/"+reqPath)))
path := filepath.Join(root, filepath.Clean("/"+reqPath))
// filepath.Join also cleans the path, and cleaning strips
// the trailing slash, so we need to re-add it afterwards.
// if the length is 1, then it's a path to the root,
// and that should return ".", so we don't append the separator.
if strings.HasSuffix(reqPath, "/") && len(reqPath) > 1 {
path += separator
}
return path
} }
// fileHidden returns true if filename is hidden // fileHidden returns true if filename is hidden according to the hide list.
// according to the hide list. // filename must be a relative or absolute file system path, not a request
// URI path. It is expected that all the paths in the hide list are absolute
// paths or are singular filenames (without a path separator).
func fileHidden(filename string, hide []string) bool { func fileHidden(filename string, hide []string) bool {
nameOnly := filepath.Base(filename) if len(hide) == 0 {
sep := string(filepath.Separator) return false
}
// all path comparisons use the complete absolute path if possible
filenameAbs, err := filepath.Abs(filename)
if err == nil {
filename = filenameAbs
}
var components []string
for _, h := range hide { for _, h := range hide {
// assuming h is a glob/shell-like pattern, if !strings.Contains(h, separator) {
// use it to compare the whole file path; // if there is no separator in h, then we assume the user
// but if there is no separator in h, then // wants to hide any files or folders that match that
// just compare against the file's name // name; thus we have to compare against each component
compare := filename // of the filename, e.g. hiding "bar" would hide "/bar"
if !strings.Contains(h, sep) { // as well as "/foo/bar/baz" but not "/barstool".
compare = nameOnly if len(components) == 0 {
} components = strings.Split(filename, separator)
}
hidden, err := filepath.Match(h, compare) for _, c := range components {
if err != nil { if hidden, _ := filepath.Match(h, c); hidden {
// malformed pattern; fallback by checking prefix return true
if strings.HasPrefix(filename, h) { }
}
} else if strings.HasPrefix(filename, h) {
// if there is a separator in h, and filename is exactly
// prefixed with h, then we can do a prefix match so that
// "/foo" matches "/foo/bar" but not "/foobar".
withoutPrefix := strings.TrimPrefix(filename, h)
if strings.HasPrefix(withoutPrefix, separator) {
return true return true
} }
} }
if hidden {
// file name or path matches hide pattern // in the general case, a glob match will suffice
if hidden, _ := filepath.Match(h, filename); hidden {
return true return true
} }
} }
@@ -395,7 +479,10 @@ var bufPool = sync.Pool{
}, },
} }
const minBackoff, maxBackoff = 2, 5 const (
minBackoff, maxBackoff = 2, 5
separator = string(filepath.Separator)
)
// Interface guards // Interface guards
var ( var (
@@ -17,6 +17,8 @@ package fileserver
import ( import (
"net/url" "net/url"
"path/filepath" "path/filepath"
"runtime"
"strings"
"testing" "testing"
) )
@@ -42,6 +44,10 @@ func TestSanitizedPathJoin(t *testing.T) {
inputPath: "/foo", inputPath: "/foo",
expect: "foo", expect: "foo",
}, },
{
inputPath: "/foo/",
expect: "foo" + separator,
},
{ {
inputPath: "/foo/bar", inputPath: "/foo/bar",
expect: filepath.Join("foo", "bar"), expect: filepath.Join("foo", "bar"),
@@ -73,7 +79,7 @@ func TestSanitizedPathJoin(t *testing.T) {
{ {
inputRoot: "/a/b", inputRoot: "/a/b",
inputPath: "/%2e%2e%2f%2e%2e%2f", inputPath: "/%2e%2e%2f%2e%2e%2f",
expect: filepath.Join("/", "a", "b"), expect: filepath.Join("/", "a", "b") + separator,
}, },
{ {
inputRoot: "C:\\www", inputRoot: "C:\\www",
@@ -93,9 +99,116 @@ func TestSanitizedPathJoin(t *testing.T) {
} }
actual := sanitizedPathJoin(tc.inputRoot, u.Path) actual := sanitizedPathJoin(tc.inputRoot, u.Path)
if actual != tc.expect { if actual != tc.expect {
t.Errorf("Test %d: [%s %s] => %s (expected %s)", i, tc.inputRoot, tc.inputPath, actual, tc.expect) t.Errorf("Test %d: [%s %s] => %s (expected %s)",
i, tc.inputRoot, tc.inputPath, actual, tc.expect)
} }
} }
} }
// TODO: test fileHidden func TestFileHidden(t *testing.T) {
for i, tc := range []struct {
inputHide []string
inputPath string
expect bool
}{
{
inputHide: nil,
inputPath: "",
expect: false,
},
{
inputHide: []string{".gitignore"},
inputPath: "/.gitignore",
expect: true,
},
{
inputHide: []string{".git"},
inputPath: "/.gitignore",
expect: false,
},
{
inputHide: []string{"/.git"},
inputPath: "/.gitignore",
expect: false,
},
{
inputHide: []string{".git"},
inputPath: "/.git",
expect: true,
},
{
inputHide: []string{".git"},
inputPath: "/.git/foo",
expect: true,
},
{
inputHide: []string{".git"},
inputPath: "/foo/.git/bar",
expect: true,
},
{
inputHide: []string{"/prefix"},
inputPath: "/prefix/foo",
expect: true,
},
{
inputHide: []string{"/foo/*/bar"},
inputPath: "/foo/asdf/bar",
expect: true,
},
{
inputHide: []string{"*.txt"},
inputPath: "/foo/bar.txt",
expect: true,
},
{
inputHide: []string{"/foo/bar/*.txt"},
inputPath: "/foo/bar/baz.txt",
expect: true,
},
{
inputHide: []string{"/foo/bar/*.txt"},
inputPath: "/foo/bar.txt",
expect: false,
},
{
inputHide: []string{"/foo/bar/*.txt"},
inputPath: "/foo/bar/index.html",
expect: false,
},
{
inputHide: []string{"/foo"},
inputPath: "/foo",
expect: true,
},
{
inputHide: []string{"/foo"},
inputPath: "/foobar",
expect: false,
},
{
inputHide: []string{"first", "second"},
inputPath: "/second",
expect: true,
},
} {
if runtime.GOOS == "windows" {
if strings.HasPrefix(tc.inputPath, "/") {
tc.inputPath, _ = filepath.Abs(tc.inputPath)
}
tc.inputPath = filepath.FromSlash(tc.inputPath)
for i := range tc.inputHide {
if strings.HasPrefix(tc.inputHide[i], "/") {
tc.inputHide[i], _ = filepath.Abs(tc.inputHide[i])
}
tc.inputHide[i] = filepath.FromSlash(tc.inputHide[i])
}
}
actual := fileHidden(tc.inputPath, tc.inputHide)
if actual != tc.expect {
t.Errorf("Test %d: Does %v hide %s? Got %t but expected %t",
i, tc.inputHide, tc.inputPath, actual, tc.expect)
}
}
}
+1
View File
@@ -0,0 +1 @@
foo.txt
+1
View File
@@ -0,0 +1 @@
foodir/foo.txt
+135 -48
View File
@@ -15,7 +15,9 @@
package headers package headers
import ( import (
"fmt"
"net/http" "net/http"
"reflect"
"strings" "strings"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
@@ -23,15 +25,16 @@ import (
) )
func init() { func init() {
httpcaddyfile.RegisterHandlerDirective("header", parseCaddyfile) httpcaddyfile.RegisterDirective("header", parseCaddyfile)
httpcaddyfile.RegisterHandlerDirective("request_header", parseReqHdrCaddyfile) httpcaddyfile.RegisterDirective("request_header", parseReqHdrCaddyfile)
} }
// parseCaddyfile sets up the handler for response headers from // parseCaddyfile sets up the handler for response headers from
// Caddyfile tokens. Syntax: // Caddyfile tokens. Syntax:
// //
// header [<matcher>] [[+|-]<field> [<value|regexp>] [<replacement>]] { // header [<matcher>] [[+|-|?]<field> [<value|regexp>] [<replacement>]] {
// [+]<field> [<value|regexp> [<replacement>]] // [+]<field> [<value|regexp> [<replacement>]]
// ?<field> <default_value>
// -<field> // -<field>
// [defer] // [defer]
// } // }
@@ -39,17 +42,27 @@ func init() {
// Either a block can be opened or a single header field can be configured // Either a block can be opened or a single header field can be configured
// in the first line, but not both in the same directive. Header operations // in the first line, but not both in the same directive. Header operations
// are deferred to write-time if any headers are being deleted or if the // are deferred to write-time if any headers are being deleted or if the
// 'defer' subdirective is used. // 'defer' subdirective is used. + appends a header value, - deletes a field,
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { // and ? conditionally sets a value only if the header field is not already
hdr := new(Handler) // set.
func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
if !h.Next() {
return nil, h.ArgErr()
}
makeResponseOps := func() { matcherSet, err := h.ExtractMatcherSet()
if hdr.Response == nil { if err != nil {
hdr.Response = &RespHeaderOps{ return nil, err
HeaderOps: new(HeaderOps), }
}
makeHandler := func() Handler {
return Handler{
Response: &RespHeaderOps{
HeaderOps: &HeaderOps{},
},
} }
} }
handler, handlerWithRequire := makeHandler(), makeHandler()
for h.Next() { for h.Next() {
// first see if headers are in the initial line // first see if headers are in the initial line
@@ -64,10 +77,18 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
if h.NextArg() { if h.NextArg() {
replacement = h.Val() replacement = h.Val()
} }
makeResponseOps() err := applyHeaderOp(
CaddyfileHeaderOp(hdr.Response.HeaderOps, field, value, replacement) handler.Response.HeaderOps,
if len(hdr.Response.HeaderOps.Delete) > 0 { handler.Response,
hdr.Response.Deferred = true field,
value,
replacement,
)
if err != nil {
return nil, h.Err(err.Error())
}
if len(handler.Response.HeaderOps.Delete) > 0 {
handler.Response.Deferred = true
} }
} }
@@ -75,12 +96,18 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
for h.NextBlock(0) { for h.NextBlock(0) {
field := h.Val() field := h.Val()
if field == "defer" { if field == "defer" {
hdr.Response.Deferred = true handler.Response.Deferred = true
continue continue
} }
if hasArgs { if hasArgs {
return nil, h.Err("cannot specify headers in both arguments and block") return nil, h.Err("cannot specify headers in both arguments and block") // because it would be weird
} }
// sometimes it is habitual for users to suffix a field name with a colon,
// as if they were writing a curl command or something; see
// https://caddy.community/t/v2-reverse-proxy-please-add-cors-example-to-the-docs/7349/19
field = strings.TrimSuffix(field, ":")
var value, replacement string var value, replacement string
if h.NextArg() { if h.NextArg() {
value = h.Val() value = h.Val()
@@ -88,15 +115,34 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
if h.NextArg() { if h.NextArg() {
replacement = h.Val() replacement = h.Val()
} }
makeResponseOps()
CaddyfileHeaderOp(hdr.Response.HeaderOps, field, value, replacement) handlerToUse := handler
if len(hdr.Response.HeaderOps.Delete) > 0 { if strings.HasPrefix(field, "?") {
hdr.Response.Deferred = true handlerToUse = handlerWithRequire
}
err := applyHeaderOp(
handlerToUse.Response.HeaderOps,
handlerToUse.Response,
field,
value,
replacement,
)
if err != nil {
return nil, h.Err(err.Error())
} }
} }
} }
return hdr, nil var configValues []httpcaddyfile.ConfigValue
if !reflect.DeepEqual(handler, makeHandler()) {
configValues = append(configValues, h.NewRoute(matcherSet, handler)...)
}
if !reflect.DeepEqual(handlerWithRequire, makeHandler()) {
configValues = append(configValues, h.NewRoute(matcherSet, handlerWithRequire)...)
}
return configValues, nil
} }
// parseReqHdrCaddyfile sets up the handler for request headers // parseReqHdrCaddyfile sets up the handler for request headers
@@ -104,17 +150,27 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// //
// request_header [<matcher>] [[+|-]<field> [<value|regexp>] [<replacement>]] // request_header [<matcher>] [[+|-]<field> [<value|regexp>] [<replacement>]]
// //
func parseReqHdrCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { func parseReqHdrCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
hdr := new(Handler) matcherSet, err := h.ExtractMatcherSet()
if err != nil {
return nil, err
}
configValues := []httpcaddyfile.ConfigValue{}
for h.Next() { for h.Next() {
if !h.NextArg() { if !h.NextArg() {
return nil, h.ArgErr() return nil, h.ArgErr()
} }
field := h.Val() field := h.Val()
hdr := Handler{
Request: &HeaderOps{},
}
// sometimes it is habitual for users to suffix a field name with a colon, // sometimes it is habitual for users to suffix a field name with a colon,
// as if they were writing a curl command or something; see // as if they were writing a curl command or something; see
// https://caddy.community/t/v2-reverse-proxy-please-add-cors-example-to-the-docs/7349 // https://caddy.community/t/v2-reverse-proxy-please-add-cors-example-to-the-docs/7349/19
field = strings.TrimSuffix(field, ":") field = strings.TrimSuffix(field, ":")
var value, replacement string var value, replacement string
@@ -131,13 +187,17 @@ func parseReqHdrCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler,
if hdr.Request == nil { if hdr.Request == nil {
hdr.Request = new(HeaderOps) hdr.Request = new(HeaderOps)
} }
CaddyfileHeaderOp(hdr.Request, field, value, replacement) if err := CaddyfileHeaderOp(hdr.Request, field, value, replacement); err != nil {
return nil, h.Err(err.Error())
}
configValues = append(configValues, h.NewRoute(matcherSet, hdr)...)
if h.NextArg() { if h.NextArg() {
return nil, h.ArgErr() return nil, h.ArgErr()
} }
} }
return hdr, nil return configValues, nil
} }
// CaddyfileHeaderOp applies a new header operation according to // CaddyfileHeaderOp applies a new header operation according to
@@ -148,32 +208,59 @@ func parseReqHdrCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler,
// will be used to search and then replacement will be used to // will be used to search and then replacement will be used to
// complete the substring replacement; in that case, any + or - // complete the substring replacement; in that case, any + or -
// prefix to field will be ignored. // prefix to field will be ignored.
func CaddyfileHeaderOp(ops *HeaderOps, field, value, replacement string) { func CaddyfileHeaderOp(ops *HeaderOps, field, value, replacement string) error {
if strings.HasPrefix(field, "+") { return applyHeaderOp(ops, nil, field, value, replacement)
}
func applyHeaderOp(ops *HeaderOps, respHeaderOps *RespHeaderOps, field, value, replacement string) error {
switch {
case strings.HasPrefix(field, "+"): // append
if ops.Add == nil { if ops.Add == nil {
ops.Add = make(http.Header) ops.Add = make(http.Header)
} }
ops.Add.Set(field[1:], value) ops.Add.Set(field[1:], value)
} else if strings.HasPrefix(field, "-") {
case strings.HasPrefix(field, "-"): // delete
ops.Delete = append(ops.Delete, field[1:]) ops.Delete = append(ops.Delete, field[1:])
} else { if respHeaderOps != nil {
if replacement == "" { respHeaderOps.Deferred = true
if ops.Set == nil {
ops.Set = make(http.Header)
}
ops.Set.Set(field, value)
} else {
if ops.Replace == nil {
ops.Replace = make(map[string][]Replacement)
}
field = strings.TrimLeft(field, "+-")
ops.Replace[field] = append(
ops.Replace[field],
Replacement{
SearchRegexp: value,
Replace: replacement,
},
)
} }
case strings.HasPrefix(field, "?"): // default (conditional on not existing) - response headers only
if respHeaderOps == nil {
return fmt.Errorf("%v: the default header modifier ('?') can only be used on response headers; for conditional manipulation of request headers, use matchers", field)
}
if respHeaderOps.Require == nil {
respHeaderOps.Require = &caddyhttp.ResponseMatcher{
Headers: make(http.Header),
}
}
field = strings.TrimPrefix(field, "?")
respHeaderOps.Require.Headers[field] = nil
if respHeaderOps.Set == nil {
respHeaderOps.Set = make(http.Header)
}
respHeaderOps.Set.Set(field, value)
case replacement != "": // replace
if ops.Replace == nil {
ops.Replace = make(map[string][]Replacement)
}
field = strings.TrimLeft(field, "+-?")
ops.Replace[field] = append(
ops.Replace[field],
Replacement{
SearchRegexp: value,
Replace: replacement,
},
)
default: // set (overwrite)
if ops.Set == nil {
ops.Set = make(http.Header)
}
ops.Set.Set(field, value)
} }
return nil
} }
+5 -4
View File
@@ -54,15 +54,15 @@ func (Handler) CaddyModule() caddy.ModuleInfo {
} }
// Provision sets up h's configuration. // Provision sets up h's configuration.
func (h *Handler) Provision(_ caddy.Context) error { func (h *Handler) Provision(ctx caddy.Context) error {
if h.Request != nil { if h.Request != nil {
err := h.Request.provision() err := h.Request.Provision(ctx)
if err != nil { if err != nil {
return err return err
} }
} }
if h.Response != nil { if h.Response != nil {
err := h.Response.provision() err := h.Response.Provision(ctx)
if err != nil { if err != nil {
return err return err
} }
@@ -125,7 +125,8 @@ type HeaderOps struct {
Replace map[string][]Replacement `json:"replace,omitempty"` Replace map[string][]Replacement `json:"replace,omitempty"`
} }
func (ops *HeaderOps) provision() error { // Provision sets up the header operations.
func (ops *HeaderOps) Provision(_ caddy.Context) error {
for fieldName, replacements := range ops.Replace { for fieldName, replacements := range ops.Replace {
for i, r := range replacements { for i, r := range replacements {
if r.SearchRegexp != "" { if r.SearchRegexp != "" {
+192 -3
View File
@@ -14,8 +14,197 @@
package headers package headers
import "testing" import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"testing"
func TestReqHeaders(t *testing.T) { "github.com/caddyserver/caddy/v2"
// TODO: write tests "github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func TestHandler(t *testing.T) {
for i, tc := range []struct {
handler Handler
reqHeader http.Header
respHeader http.Header
respStatusCode int
expectedReqHeader http.Header
expectedRespHeader http.Header
}{
{
handler: Handler{
Request: &HeaderOps{
Add: http.Header{
"Expose-Secrets": []string{"always"},
},
},
},
reqHeader: http.Header{
"Expose-Secrets": []string{"i'm serious"},
},
expectedReqHeader: http.Header{
"Expose-Secrets": []string{"i'm serious", "always"},
},
},
{
handler: Handler{
Request: &HeaderOps{
Set: http.Header{
"Who-Wins": []string{"batman"},
},
},
},
reqHeader: http.Header{
"Who-Wins": []string{"joker"},
},
expectedReqHeader: http.Header{
"Who-Wins": []string{"batman"},
},
},
{
handler: Handler{
Request: &HeaderOps{
Delete: []string{"Kick-Me"},
},
},
reqHeader: http.Header{
"Kick-Me": []string{"if you can"},
"Keep-Me": []string{"i swear i'm innocent"},
},
expectedReqHeader: http.Header{
"Keep-Me": []string{"i swear i'm innocent"},
},
},
{
handler: Handler{
Request: &HeaderOps{
Replace: map[string][]Replacement{
"Best-Server": {
Replacement{
Search: "NGINX",
Replace: "the Caddy web server",
},
Replacement{
SearchRegexp: `Apache(\d+)`,
Replace: "Caddy",
},
},
},
},
},
reqHeader: http.Header{
"Best-Server": []string{"it's NGINX, undoubtedly", "I love Apache2"},
},
expectedReqHeader: http.Header{
"Best-Server": []string{"it's the Caddy web server, undoubtedly", "I love Caddy"},
},
},
{
handler: Handler{
Response: &RespHeaderOps{
Require: &caddyhttp.ResponseMatcher{
Headers: http.Header{
"Cache-Control": nil,
},
},
HeaderOps: &HeaderOps{
Add: http.Header{
"Cache-Control": []string{"no-cache"},
},
},
},
},
respHeader: http.Header{},
expectedRespHeader: http.Header{
"Cache-Control": []string{"no-cache"},
},
},
{
handler: Handler{
Response: &RespHeaderOps{
Require: &caddyhttp.ResponseMatcher{
Headers: http.Header{
"Cache-Control": []string{"no-cache"},
},
},
HeaderOps: &HeaderOps{
Delete: []string{"Cache-Control"},
},
},
},
respHeader: http.Header{
"Cache-Control": []string{"no-cache"},
},
expectedRespHeader: http.Header{},
},
{
handler: Handler{
Response: &RespHeaderOps{
Require: &caddyhttp.ResponseMatcher{
StatusCode: []int{5},
},
HeaderOps: &HeaderOps{
Add: http.Header{
"Fail-5xx": []string{"true"},
},
},
},
},
respStatusCode: 503,
respHeader: http.Header{},
expectedRespHeader: http.Header{
"Fail-5xx": []string{"true"},
},
},
} {
rr := httptest.NewRecorder()
req := &http.Request{Header: tc.reqHeader}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
tc.handler.Provision(caddy.Context{})
next := nextHandler(func(w http.ResponseWriter, r *http.Request) error {
for k, hdrs := range tc.respHeader {
for _, v := range hdrs {
w.Header().Add(k, v)
}
}
status := 200
if tc.respStatusCode != 0 {
status = tc.respStatusCode
}
w.WriteHeader(status)
if tc.expectedReqHeader != nil && !reflect.DeepEqual(r.Header, tc.expectedReqHeader) {
return fmt.Errorf("expected request header %v, got %v", tc.expectedReqHeader, r.Header)
}
return nil
})
if err := tc.handler.ServeHTTP(rr, req, next); err != nil {
t.Errorf("Test %d: %w", i, err)
continue
}
actual := rr.Header()
if tc.expectedRespHeader != nil && !reflect.DeepEqual(actual, tc.expectedRespHeader) {
t.Errorf("Test %d: expected response header %v, got %v", i, tc.expectedRespHeader, actual)
continue
}
}
}
type nextHandler func(http.ResponseWriter, *http.Request) error
func (f nextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
return f(w, r)
} }
+67 -29
View File
@@ -15,6 +15,8 @@
package maphandler package maphandler
import ( import (
"strings"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
) )
@@ -23,49 +25,85 @@ func init() {
httpcaddyfile.RegisterHandlerDirective("map", parseCaddyfile) httpcaddyfile.RegisterHandlerDirective("map", parseCaddyfile)
} }
// parseCaddyfile sets up the handler for a map from Caddyfile tokens. Syntax: // parseCaddyfile sets up the map handler from Caddyfile tokens. Syntax:
// //
// map <source> <dest> { // map [<matcher>] <source> <destinations...> {
// [default <default>] - used if not match is found // [~]<input> <outputs...>
// [<regexp> <replacement>] - regular expression to match against the source find and the matching replacement value // default <defaults...>
// ...
// } // }
// //
// The map takes a source variable and maps it into the dest variable. The mapping process // If the input value is prefixed with a tilde (~), then the input will be parsed as a
// will check the source variable for the first successful match against a list of regular expressions. // regular expression.
// If a successful match is found the dest variable will contain the replacement value.
// If no successful match is found and the default is specified then the dest will contain the default value.
// //
// The Caddyfile adapter treats outputs that are a literal hyphen (-) as a null/nil
// value. This is useful if you want to fall back to default for that particular output.
//
// The number of outputs for each mapping must not be more than the number of destinations.
// However, for convenience, there may be fewer outputs than destinations and any missing
// outputs will be filled in implicitly.
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
m := new(Handler) var handler Handler
for h.Next() { for h.Next() {
// first see if source and dest are configured // source
if h.NextArg() { if !h.NextArg() {
m.Source = h.Val() return nil, h.ArgErr()
if h.NextArg() { }
m.Destination = h.Val() handler.Source = h.Val()
}
// destinations
handler.Destinations = h.RemainingArgs()
if len(handler.Destinations) == 0 {
return nil, h.Err("missing destination argument(s)")
} }
// load the rules // mappings
for h.NextBlock(0) { for h.NextBlock(0) {
expression := h.Val() // defaults are a special case
if expression == "default" { if h.Val() == "default" {
args := h.RemainingArgs() if len(handler.Defaults) > 0 {
if len(args) != 1 { return nil, h.Err("defaults already defined")
return m, h.ArgErr()
} }
m.Default = args[0] handler.Defaults = h.RemainingArgs()
} else { for len(handler.Defaults) < len(handler.Destinations) {
args := h.RemainingArgs() handler.Defaults = append(handler.Defaults, "")
if len(args) != 1 {
return m, h.ArgErr()
} }
m.Items = append(m.Items, Item{Expression: expression, Value: args[0]}) continue
} }
// every other line maps one input to one or more outputs
in := h.Val()
var outs []interface{}
for _, out := range h.RemainingArgs() {
if out == "-" {
outs = append(outs, nil)
} else {
outs = append(outs, out)
}
}
// cannot have more outputs than destinations
if len(outs) > len(handler.Destinations) {
return nil, h.Err("too many outputs")
}
// for convenience, can have fewer outputs than destinations, but the
// underlying handler won't accept that, so we fill in nil values
for len(outs) < len(handler.Destinations) {
outs = append(outs, nil)
}
// create the mapping
mapping := Mapping{Outputs: outs}
if strings.HasPrefix(in, "~") {
mapping.InputRegexp = in[1:]
} else {
mapping.Input = in
}
handler.Mappings = append(handler.Mappings, mapping)
} }
} }
return m, nil return handler, nil
} }
+134 -42
View File
@@ -15,8 +15,10 @@
package maphandler package maphandler
import ( import (
"fmt"
"net/http" "net/http"
"regexp" "regexp"
"strings"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -26,27 +28,27 @@ func init() {
caddy.RegisterModule(Handler{}) caddy.RegisterModule(Handler{})
} }
// Handler is a middleware that maps a source placeholder to a destination // Handler implements a middleware that maps inputs to outputs. Specifically, it
// placeholder. // compares a source value against the map inputs, and for one that matches, it
// // applies the output values to each destination. Destinations become placeholder
// The mapping process happens early in the request handling lifecycle so that // names.
// the Destination placeholder is calculated and available for substitution.
// The Items array contains pairs of regex expressions and values, the
// Source is matched against the expression, if they match then the destination
// placeholder is set to the value.
//
// The Default is optional, if no Item expression is matched then the value of
// the Default will be used.
// //
// Mapped placeholders are not evaluated until they are used, so even for very
// large mappings, this handler is quite efficient.
type Handler struct { type Handler struct {
// Source is a placeholder // Source is the placeholder from which to get the input value.
Source string `json:"source,omitempty"` Source string `json:"source,omitempty"`
// Destination is a new placeholder
Destination string `json:"destination,omitempty"` // Destinations are the names of placeholders in which to store the outputs.
// Default is an optional value to use if no other was found Destinations []string `json:"destinations,omitempty"`
Default string `json:"default,omitempty"`
// Items is an array of regex expressions and values // Mappings from source values (inputs) to destination values (outputs).
Items []Item `json:"items,omitempty"` // The first matching, non-nil mapping will be applied.
Mappings []Mapping `json:"mappings,omitempty"`
// If no mappings match or if the mapped output is null/nil, the associated
// default output will be applied (optional).
Defaults []string
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
@@ -57,10 +59,58 @@ func (Handler) CaddyModule() caddy.ModuleInfo {
} }
} }
// Provision will compile all regular expressions // Provision sets up h.
func (h *Handler) Provision(_ caddy.Context) error { func (h *Handler) Provision(_ caddy.Context) error {
for i := 0; i < len(h.Items); i++ { for j, dest := range h.Destinations {
h.Items[i].compiled = regexp.MustCompile(h.Items[i].Expression) h.Destinations[j] = strings.Trim(dest, "{}")
}
for i, m := range h.Mappings {
if m.InputRegexp == "" {
continue
}
var err error
h.Mappings[i].re, err = regexp.Compile(m.InputRegexp)
if err != nil {
return fmt.Errorf("compiling regexp for mapping %d: %v", i, err)
}
}
// TODO: improve efficiency even further by using an actual map type
// for the non-regexp mappings, OR sort them and do a binary search
return nil
}
// Validate ensures that h is configured properly.
func (h *Handler) Validate() error {
nDest, nDef := len(h.Destinations), len(h.Defaults)
if nDef > 0 && nDef != nDest {
return fmt.Errorf("%d destinations != %d defaults", nDest, nDef)
}
seen := make(map[string]int)
for i, m := range h.Mappings {
// prevent confusing/ambiguous mappings
if m.Input != "" && m.InputRegexp != "" {
return fmt.Errorf("mapping %d has both input and input_regexp fields specified, which is confusing", i)
}
// prevent duplicate mappings
input := m.Input
if m.InputRegexp != "" {
input = m.InputRegexp
}
if prev, ok := seen[input]; ok {
return fmt.Errorf("mapping %d has a duplicate input '%s' previously used with mapping %d", i, input, prev)
}
seen[input] = i
// ensure mappings have 1:1 output-to-destination correspondence
nOut := len(m.Outputs)
if nOut != nDest {
return fmt.Errorf("mapping %d has %d outputs but there are %d destinations defined", i, nOut, nDest)
}
} }
return nil return nil
} }
@@ -68,38 +118,80 @@ func (h *Handler) Provision(_ caddy.Context) error {
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
// get the source value, if the source value was not found do no // defer work until a variable is actually evaluated by using replacer's Map callback
// replacement. repl.Map(func(key string) (interface{}, bool) {
val, ok := repl.GetString(h.Source) // return early if the variable is not even a configured destination
if ok { destIdx := h.destinationIndex(key)
found := false if destIdx < 0 {
for i := 0; i < len(h.Items); i++ { return nil, false
if h.Items[i].compiled.MatchString(val) { }
found = true
repl.Set(h.Destination, h.Items[i].Value) input := repl.ReplaceAll(h.Source, "")
break
// find the first mapping matching the input and return
// the requested destination/output value
for _, m := range h.Mappings {
if m.re != nil {
if m.re.MatchString(input) {
if output := m.Outputs[destIdx]; output == nil {
continue
} else {
return output, true
}
}
continue
}
if input == m.Input {
if output := m.Outputs[destIdx]; output == nil {
continue
} else {
return output, true
}
} }
} }
if !found && h.Default != "" { // fall back to default if no match or if matched nil value
repl.Set(h.Destination, h.Default) if len(h.Defaults) > destIdx {
return h.Defaults[destIdx], true
} }
}
return nil, true
})
return next.ServeHTTP(w, r) return next.ServeHTTP(w, r)
} }
// Item defines each entry in the map // destinationIndex returns the positional index of the destination
type Item struct { // is name is a known destination; otherwise it returns -1.
// Expression is the regular expression searched for func (h Handler) destinationIndex(name string) int {
Expression string `json:"expression,omitempty"` for i, dest := range h.Destinations {
// Value to use once the expression has been found if dest == name {
Value string `json:"value,omitempty"` return i
// compiled expression, internal use }
compiled *regexp.Regexp }
return -1
}
// Mapping describes a mapping from input to outputs.
type Mapping struct {
// The input value to match. Must be distinct from other mappings.
// Mutually exclusive to input_regexp.
Input string `json:"input,omitempty"`
// The input regular expression to match. Mutually exclusive to input.
InputRegexp string `json:"input_regexp,omitempty"`
// Upon a match with the input, each output is positionally correlated
// with each destination of the parent handler. An output that is null
// (nil) will be treated as if it was not mapped at all.
Outputs []interface{} `json:"outputs,omitempty"`
re *regexp.Regexp
} }
// Interface guards // Interface guards
var ( var (
_ caddy.Provisioner = (*Handler)(nil) _ caddy.Provisioner = (*Handler)(nil)
_ caddy.Validator = (*Handler)(nil)
_ caddyhttp.MiddlewareHandler = (*Handler)(nil) _ caddyhttp.MiddlewareHandler = (*Handler)(nil)
) )
+10 -5
View File
@@ -26,11 +26,11 @@ type LoggableHTTPRequest struct{ *http.Request }
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface. // MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error { func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
enc.AddString("method", r.Method)
enc.AddString("uri", r.RequestURI)
enc.AddString("proto", r.Proto)
enc.AddString("remote_addr", r.RemoteAddr) enc.AddString("remote_addr", r.RemoteAddr)
enc.AddString("proto", r.Proto)
enc.AddString("method", r.Method)
enc.AddString("host", r.Host) enc.AddString("host", r.Host)
enc.AddString("uri", r.RequestURI)
enc.AddObject("headers", LoggableHTTPHeader(r.Header)) enc.AddObject("headers", LoggableHTTPHeader(r.Header))
if r.TLS != nil { if r.TLS != nil {
enc.AddObject("tls", LoggableTLSConnState(*r.TLS)) enc.AddObject("tls", LoggableTLSConnState(*r.TLS))
@@ -73,10 +73,15 @@ type LoggableTLSConnState tls.ConnectionState
func (t LoggableTLSConnState) MarshalLogObject(enc zapcore.ObjectEncoder) error { func (t LoggableTLSConnState) MarshalLogObject(enc zapcore.ObjectEncoder) error {
enc.AddBool("resumed", t.DidResume) enc.AddBool("resumed", t.DidResume)
enc.AddUint16("version", t.Version) enc.AddUint16("version", t.Version)
enc.AddUint16("ciphersuite", t.CipherSuite) enc.AddUint16("cipher_suite", t.CipherSuite)
enc.AddString("proto", t.NegotiatedProtocol) enc.AddString("proto", t.NegotiatedProtocol)
enc.AddBool("proto_mutual", t.NegotiatedProtocolIsMutual) // NegotiatedProtocolIsMutual is deprecated - it's always true
enc.AddBool("proto_mutual", true)
enc.AddString("server_name", t.ServerName) enc.AddString("server_name", t.ServerName)
if len(t.PeerCertificates) > 0 {
enc.AddString("client_common_name", t.PeerCertificates[0].Subject.CommonName)
enc.AddString("client_serial", t.PeerCertificates[0].SerialNumber.String())
}
return nil return nil
} }
+131 -24
View File
@@ -23,6 +23,7 @@ import (
"net/url" "net/url"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort"
"strings" "strings"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
@@ -51,6 +52,8 @@ type (
// //
// The wildcard can be useful for matching all subdomains, for example: // The wildcard can be useful for matching all subdomains, for example:
// `*.example.com` matches `foo.example.com` but not `foo.bar.example.com`. // `*.example.com` matches `foo.example.com` but not `foo.bar.example.com`.
//
// Duplicate entries will return an error.
MatchHost []string MatchHost []string
// MatchPath matches requests by the URI's path (case-insensitive). Path // MatchPath matches requests by the URI's path (case-insensitive). Path
@@ -103,8 +106,15 @@ type (
// MatchRemoteIP matches requests by client IP (or CIDR range). // MatchRemoteIP matches requests by client IP (or CIDR range).
MatchRemoteIP struct { MatchRemoteIP struct {
// The IPs or CIDR ranges to match.
Ranges []string `json:"ranges,omitempty"` Ranges []string `json:"ranges,omitempty"`
// If true, prefer the first IP in the request's X-Forwarded-For
// header, if present, rather than the immediate peer's IP, as
// the reference IP against which to match. Note that it is easy
// to spoof request headers. Default: false
Forwarded bool `json:"forwarded,omitempty"`
cidrs []*net.IPNet cidrs []*net.IPNet
logger *zap.Logger logger *zap.Logger
} }
@@ -167,6 +177,40 @@ func (m *MatchHost) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return nil return nil
} }
// Provision sets up and validates m, including making it more efficient for large lists.
func (m MatchHost) Provision(_ caddy.Context) error {
// check for duplicates; they are nonsensical and reduce efficiency
// (we could just remove them, but the user should know their config is erroneous)
seen := make(map[string]int)
for i, h := range m {
h = strings.ToLower(h)
if firstI, ok := seen[h]; ok {
return fmt.Errorf("host at index %d is repeated at index %d: %s", firstI, i, h)
}
seen[h] = i
}
if m.large() {
// sort the slice lexicographically, grouping "fuzzy" entries (wildcards and placeholders)
// at the front of the list; this allows us to use binary search for exact matches, which
// we have seen from experience is the most common kind of value in large lists; and any
// other kinds of values (wildcards and placeholders) are grouped in front so the linear
// search should find a match fairly quickly
sort.Slice(m, func(i, j int) bool {
iInexact, jInexact := m.fuzzy(m[i]), m.fuzzy(m[j])
if iInexact && !jInexact {
return true
}
if !iInexact && jInexact {
return false
}
return m[i] < m[j]
})
}
return nil
}
// Match returns true if r matches m. // Match returns true if r matches m.
func (m MatchHost) Match(r *http.Request) bool { func (m MatchHost) Match(r *http.Request) bool {
reqHost, _, err := net.SplitHostPort(r.Host) reqHost, _, err := net.SplitHostPort(r.Host)
@@ -179,10 +223,31 @@ func (m MatchHost) Match(r *http.Request) bool {
reqHost = strings.TrimSuffix(reqHost, "]") reqHost = strings.TrimSuffix(reqHost, "]")
} }
if m.large() {
// fast path: locate exact match using binary search (about 100-1000x faster for large lists)
pos := sort.Search(len(m), func(i int) bool {
if m.fuzzy(m[i]) {
return false
}
return m[i] >= reqHost
})
if pos < len(m) && m[pos] == reqHost {
return true
}
}
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
outer: outer:
for _, host := range m { for _, host := range m {
// fast path: if matcher is large, we already know we don't have an exact
// match, so we're only looking for fuzzy match now, which should be at the
// front of the list; if we have reached a value that is not fuzzy, there
// will be no match and we can short-circuit for efficiency
if m.large() && !m.fuzzy(host) {
break
}
host = repl.ReplaceAll(host, "") host = repl.ReplaceAll(host, "")
if strings.Contains(host, "*") { if strings.Contains(host, "*") {
patternParts := strings.Split(host, ".") patternParts := strings.Split(host, ".")
@@ -207,6 +272,15 @@ outer:
return false return false
} }
// fuzzy returns true if the given hostname h is not a specific
// hostname, e.g. has placeholders or wildcards.
func (MatchHost) fuzzy(h string) bool { return strings.ContainsAny(h, "{*") }
// large returns true if m is considered to be large. Optimizing
// the matcher for smaller lists has diminishing returns.
// See related benchmark function in test file to conduct experiments.
func (m MatchHost) large() bool { return len(m) > 100 }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (MatchPath) CaddyModule() caddy.ModuleInfo { func (MatchPath) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
@@ -353,18 +427,16 @@ func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
*m = make(map[string][]string) *m = make(map[string][]string)
} }
for d.Next() { for d.Next() {
var query string for _, query := range d.RemainingArgs() {
if !d.Args(&query) { if query == "" {
return d.ArgErr() continue
}
parts := strings.SplitN(query, "=", 2)
if len(parts) != 2 {
return d.Errf("malformed query matcher token: %s; must be in param=val format", d.Val())
}
url.Values(*m).Add(parts[0], parts[1])
} }
if query == "" {
continue
}
parts := strings.SplitN(query, "=", 2)
if len(parts) != 2 {
return d.Errf("malformed query matcher token: %s; must be in param=val format", d.Val())
}
url.Values(*m).Set(parts[0], parts[1])
if d.NextBlock(0) { if d.NextBlock(0) {
return d.Err("malformed query matcher: blocks are not supported") return d.Err("malformed query matcher: blocks are not supported")
} }
@@ -405,10 +477,33 @@ func (m *MatchHeader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
} }
for d.Next() { for d.Next() {
var field, val string var field, val string
if !d.Args(&field, &val) { if !d.Args(&field) {
return d.Errf("malformed header matcher: expected both field and value") return d.Errf("malformed header matcher: expected field")
} }
http.Header(*m).Set(field, val)
if strings.HasPrefix(field, "!") {
if len(field) == 1 {
return d.Errf("malformed header matcher: must have field name following ! character")
}
field = field[1:]
headers := *m
headers[field] = nil
m = &headers
if d.NextArg() {
return d.Errf("malformed header matcher: null matching headers cannot have a field value")
}
} else {
if !d.NextArg() {
return d.Errf("malformed header matcher: expected both field and value")
}
// If multiple header matchers with the same header field are defined,
// we want to add the existing to the list of headers (will be OR'ed)
val = d.Val()
http.Header(*m).Add(field, val)
}
if d.NextBlock(0) { if d.NextBlock(0) {
return d.Err("malformed header matcher: blocks are not supported") return d.Err("malformed header matcher: blocks are not supported")
} }
@@ -443,6 +538,10 @@ func matchHeaders(input, against http.Header, host string) bool {
// match if the header field exists at all // match if the header field exists at all
continue continue
} }
if allowedFieldVals == nil && actualFieldVals == nil {
// a nil list means match if the header does not exist at all
continue
}
var match bool var match bool
fieldVals: fieldVals:
for _, actualFieldVal := range actualFieldVals { for _, actualFieldVal := range actualFieldVals {
@@ -700,7 +799,16 @@ func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo {
// UnmarshalCaddyfile implements caddyfile.Unmarshaler. // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() { for d.Next() {
m.Ranges = append(m.Ranges, d.RemainingArgs()...) for d.NextArg() {
if d.Val() == "forwarded" {
if len(m.Ranges) > 0 {
return d.Err("if used, 'forwarded' must be first argument")
}
m.Forwarded = true
continue
}
m.Ranges = append(m.Ranges, d.Val())
}
if d.NextBlock(0) { if d.NextBlock(0) {
return d.Err("malformed remote_ip matcher: blocks are not supported") return d.Err("malformed remote_ip matcher: blocks are not supported")
} }
@@ -734,24 +842,20 @@ func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
} }
func (m MatchRemoteIP) getClientIP(r *http.Request) (net.IP, error) { func (m MatchRemoteIP) getClientIP(r *http.Request) (net.IP, error) {
var remote string remote := r.RemoteAddr
if fwdFor := r.Header.Get("X-Forwarded-For"); fwdFor != "" { if m.Forwarded {
remote = strings.TrimSpace(strings.Split(fwdFor, ",")[0]) if fwdFor := r.Header.Get("X-Forwarded-For"); fwdFor != "" {
remote = strings.TrimSpace(strings.Split(fwdFor, ",")[0])
}
} }
if remote == "" {
remote = r.RemoteAddr
}
ipStr, _, err := net.SplitHostPort(remote) ipStr, _, err := net.SplitHostPort(remote)
if err != nil { if err != nil {
ipStr = remote // OK; probably didn't have a port ipStr = remote // OK; probably didn't have a port
} }
ip := net.ParseIP(ipStr) ip := net.ParseIP(ipStr)
if ip == nil { if ip == nil {
return nil, fmt.Errorf("invalid client IP address: %s", ipStr) return nil, fmt.Errorf("invalid client IP address: %s", ipStr)
} }
return ip, nil return ip, nil
} }
@@ -903,6 +1007,7 @@ const regexpPlaceholderPrefix = "http.regexp"
// Interface guards // Interface guards
var ( var (
_ RequestMatcher = (*MatchHost)(nil) _ RequestMatcher = (*MatchHost)(nil)
_ caddy.Provisioner = (*MatchHost)(nil)
_ RequestMatcher = (*MatchPath)(nil) _ RequestMatcher = (*MatchPath)(nil)
_ RequestMatcher = (*MatchPathRE)(nil) _ RequestMatcher = (*MatchPathRE)(nil)
_ caddy.Provisioner = (*MatchPathRE)(nil) _ caddy.Provisioner = (*MatchPathRE)(nil)
@@ -927,6 +1032,8 @@ var (
_ caddyfile.Unmarshaler = (*MatchHeaderRE)(nil) _ caddyfile.Unmarshaler = (*MatchHeaderRE)(nil)
_ caddyfile.Unmarshaler = (*MatchProtocol)(nil) _ caddyfile.Unmarshaler = (*MatchProtocol)(nil)
_ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil) _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
_ caddyfile.Unmarshaler = (*VarsMatcher)(nil)
_ caddyfile.Unmarshaler = (*MatchVarsRE)(nil)
_ json.Marshaler = (*MatchNot)(nil) _ json.Marshaler = (*MatchNot)(nil)
_ json.Unmarshaler = (*MatchNot)(nil) _ json.Unmarshaler = (*MatchNot)(nil)
+42
View File
@@ -122,6 +122,11 @@ func TestHostMatcher(t *testing.T) {
input: "sub.foo.example.net", input: "sub.foo.example.net",
expect: false, expect: false,
}, },
{
match: MatchHost{"www.*.*"},
input: "www.example.com",
expect: true,
},
{ {
match: MatchHost{"example.com"}, match: MatchHost{"example.com"},
input: "example.com:5555", input: "example.com:5555",
@@ -475,6 +480,16 @@ func TestHeaderMatcher(t *testing.T) {
host: "caddyserver.com", host: "caddyserver.com",
expect: false, expect: false,
}, },
{
match: MatchHeader{"Must-Not-Exist": nil},
input: http.Header{},
expect: true,
},
{
match: MatchHeader{"Must-Not-Exist": nil},
input: http.Header{"Must-Not-Exist": []string{"do not match"}},
expect: false,
},
} { } {
req := &http.Request{Header: tc.input, Host: tc.host} req := &http.Request{Header: tc.input, Host: tc.host}
actual := tc.match.Match(req) actual := tc.match.Match(req)
@@ -1003,6 +1018,33 @@ func TestNotMatcher(t *testing.T) {
} }
} }
} }
func BenchmarkLargeHostMatcher(b *testing.B) {
// this benchmark simulates a large host matcher (thousands of entries) where each
// value is an exact hostname (not a placeholder or wildcard) - compare the results
// of this with and without the binary search (comment out the various fast path
// sections in Match) to conduct experiments
const n = 10000
lastHost := fmt.Sprintf("%d.example.com", n-1)
req := &http.Request{Host: lastHost}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
matcher := make(MatchHost, n)
for i := 0; i < n; i++ {
matcher[i] = fmt.Sprintf("%d.example.com", i)
}
err := matcher.Provision(caddy.Context{})
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
matcher.Match(req)
}
}
func BenchmarkHostMatcherWithoutPlaceholder(b *testing.B) { func BenchmarkHostMatcherWithoutPlaceholder(b *testing.B) {
req := &http.Request{Host: "localhost"} req := &http.Request{Host: "localhost"}
+186
View File
@@ -0,0 +1,186 @@
package caddyhttp
import (
"context"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var httpMetrics = struct {
init sync.Once
requestInFlight *prometheus.GaugeVec
requestCount *prometheus.CounterVec
requestErrors *prometheus.CounterVec
requestDuration *prometheus.HistogramVec
requestSize *prometheus.HistogramVec
responseSize *prometheus.HistogramVec
responseDuration *prometheus.HistogramVec
}{
init: sync.Once{},
}
func initHTTPMetrics() {
const ns, sub = "caddy", "http"
basicLabels := []string{"server", "handler"}
httpMetrics.requestInFlight = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: ns,
Subsystem: sub,
Name: "requests_in_flight",
Help: "Number of requests currently handled by this server.",
}, basicLabels)
httpMetrics.requestErrors = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: ns,
Subsystem: sub,
Name: "request_errors_total",
Help: "Number of requests resulting in middleware errors.",
}, basicLabels)
httpMetrics.requestCount = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: ns,
Subsystem: sub,
Name: "requests_total",
Help: "Counter of HTTP(S) requests made.",
}, basicLabels)
// TODO: allow these to be customized in the config
durationBuckets := prometheus.DefBuckets
sizeBuckets := prometheus.ExponentialBuckets(256, 4, 8)
httpLabels := []string{"server", "handler", "code", "method"}
httpMetrics.requestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: ns,
Subsystem: sub,
Name: "request_duration_seconds",
Help: "Histogram of round-trip request durations.",
Buckets: durationBuckets,
}, httpLabels)
httpMetrics.requestSize = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: ns,
Subsystem: sub,
Name: "request_size_bytes",
Help: "Total size of the request. Includes body",
Buckets: sizeBuckets,
}, httpLabels)
httpMetrics.responseSize = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: ns,
Subsystem: sub,
Name: "response_size_bytes",
Help: "Size of the returned response.",
Buckets: sizeBuckets,
}, httpLabels)
httpMetrics.responseDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: ns,
Subsystem: sub,
Name: "response_duration_seconds",
Help: "Histogram of times to first byte in response bodies.",
Buckets: durationBuckets,
}, httpLabels)
}
// serverNameFromContext extracts the current server name from the context.
// Returns "UNKNOWN" if none is available (should probably never happen).
func serverNameFromContext(ctx context.Context) string {
srv, ok := ctx.Value(ServerCtxKey).(*Server)
if !ok || srv == nil || srv.name == "" {
return "UNKNOWN"
}
return srv.name
}
type metricsInstrumentedHandler struct {
handler string
mh MiddlewareHandler
}
func newMetricsInstrumentedHandler(handler string, mh MiddlewareHandler) *metricsInstrumentedHandler {
httpMetrics.init.Do(func() {
initHTTPMetrics()
})
return &metricsInstrumentedHandler{handler, mh}
}
func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error {
server := serverNameFromContext(r.Context())
labels := prometheus.Labels{"server": server, "handler": h.handler}
method := strings.ToUpper(r.Method)
// the "code" value is set later, but initialized here to eliminate the possibility
// of a panic
statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""}
inFlight := httpMetrics.requestInFlight.With(labels)
inFlight.Inc()
defer inFlight.Dec()
start := time.Now()
// This is a _bit_ of a hack - it depends on the ShouldBufferFunc always
// being called when the headers are written.
// Effectively the same behaviour as promhttp.InstrumentHandlerTimeToWriteHeader.
writeHeaderRecorder := ShouldBufferFunc(func(status int, header http.Header) bool {
statusLabels["code"] = sanitizeCode(status)
ttfb := time.Since(start).Seconds()
httpMetrics.responseDuration.With(statusLabels).Observe(ttfb)
return false
})
wrec := NewResponseRecorder(w, nil, writeHeaderRecorder)
err := h.mh.ServeHTTP(wrec, r, next)
dur := time.Since(start).Seconds()
httpMetrics.requestCount.With(labels).Inc()
if err != nil {
httpMetrics.requestErrors.With(labels).Inc()
return err
}
// If the code hasn't been set yet, and we didn't encounter an error, we're
// probably falling through with an empty handler.
if statusLabels["code"] == "" {
// we still sanitize it, even though it's likely to be 0. A 200 is
// returned on fallthrough so we want to reflect that.
statusLabels["code"] = sanitizeCode(wrec.Status())
}
httpMetrics.requestDuration.With(statusLabels).Observe(dur)
httpMetrics.requestSize.With(statusLabels).Observe(float64(computeApproximateRequestSize(r)))
httpMetrics.responseSize.With(statusLabels).Observe(float64(wrec.Size()))
return nil
}
func sanitizeCode(code int) string {
if code == 0 {
return "200"
}
return strconv.Itoa(code)
}
// taken from https://github.com/prometheus/client_golang/blob/6007b2b5cae01203111de55f753e76d8dac1f529/prometheus/promhttp/instrument_server.go#L298
func computeApproximateRequestSize(r *http.Request) int {
s := 0
if r.URL != nil {
s += len(r.URL.String())
}
s += len(r.Method)
s += len(r.Proto)
for name, values := range r.Header {
s += len(name)
for _, value := range values {
s += len(value)
}
}
s += len(r.Host)
// N.B. r.Form and r.MultipartForm are assumed to be included in r.URL.
if r.ContentLength != -1 {
s += int(r.ContentLength)
}
return s
}

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