Compare commits

...

75 Commits

Author SHA1 Message Date
Francis Lavoie 5968ebd0f4 reverseproxy: Add support for specifying IDs in Caddyfile 2021-09-13 00:21:54 -04:00
Francis Lavoie a5f4fae145 reverseproxy: Add ID field for upstreams 2021-09-12 23:50:04 -04:00
Francis Lavoie a779e1b383 fastcgi: Fix Caddyfile parsing when handle_response is used (#4342) 2021-09-11 14:12:21 -06:00
Matthew Holt 46ab93be51 go.mod: Update CertMagic
Adds one more debug log
2021-09-03 11:42:13 -06:00
Mohammed Al Sahaf e0fc46a911 ci: revert workaround implemented in #4306 (#4328) 2021-09-03 10:05:04 -04:00
peymaneh 9f6393c64c cmd: export CaddyVersion(), Commands() (#4316)
* cmd: Export CaddyVersion()

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

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

* Update modules/caddyhttp/encode/encode.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2021-08-31 13:36:36 -06:00
Matthew Holt f43fd6f388 go.mod: Upgrade CertMagic to v0.14.4
Adds more debug logging
2021-08-30 13:14:42 -06:00
Matthew Holt 84b906a248 go.mod: Upgrade some dependencies 2021-08-26 15:00:25 -06:00
Francis Lavoie 403732c433 httpcaddyfile: Reorder some directives (#4311)
We realized we made some mistakes with the directive ordering, so we're making some minor adjustments.

`abort` and `error` don't really make sense to be after other handler directives, because you would expect to be able to "fail-fast" and throw an error before falling through to some `file_server` or `respond` typically. So we're moving them up to just before `respond`, i.e. before the common handler directives. 

This is also more consistent with our existing examples in the docs, which actually didn't work due to the directive ordering. See https://caddyserver.com/docs/caddyfile/directives/error#examples

Also, `push` doesn't quite make sense to be after `handle`/`route`, since its job is to read from response headers to push additional resources if necessary, and `handle`/`route` may be terminal so push would not be reached if it was declared outside those. And also, it would make sense to be _before_ `templates` because a template _could_ add a `Link` header to the response dynamically.
2021-08-26 14:31:55 -06:00
Francis Lavoie f6d5ec2fd6 chore: Upgrade smallstep libs (#4307)
See https://github.com/smallstep/nosql/issues/12 for context.
2021-08-25 12:16:55 -06:00
Mohammed Al Sahaf 19a55d6aeb chore: promote creating 'caddy-build' to the release action (#4306)
The commit goreleaser/goreleaser@013bd69126 of GoReleaser is now checking the `go version` prior to executing any of the pre-hooks, which involves setting the current dir of the command to the `build.dir` of the build config. At the time of version check, the buil dir does not exist. It's created in the pre-hook. As a workaround, the build-dir is now created in the Github Action prior to executing goreleaser action.
2021-08-25 17:30:24 +00:00
Matthew Holt bfbc459c0a httpcaddyfile: Improve unrecognized directive errors 2021-08-25 10:30:39 -06:00
Francis Lavoie f70a7578fa reverseproxy: Remove redundant flushing (#4299)
From reading through the code, I think this code path is now obsoleted by the changes made in https://github.com/caddyserver/caddy/pull/4266.

Basically, `h.flushInterval()` will set the flush interval to `-1` if we're in a bi-directional stream, and the recent PR ensured that `h.copyResponse()` properly flushes headers immediately when the flush interval is non-zero. So now there should be no need to call Flush before calling `h.copyResponse()`.
2021-08-23 11:54:28 -06:00
Francis Lavoie 51f125bd44 caddyfile: Better error message for missing site block braces (#4301)
Some new users mistakenly try to define two sites without braces around each. Doing this can yield a confusing error message saying that their site address is an "unknown directive".

We can do better by keeping track of whether the current site block was parsed with or without a brace, then changing the error message later based on that.

For example, now this invalid config:

```
foo.example.com
respond "foo"

bar.example.com
respond "bar"
```

Will yield this error message:

```
$ caddy adapt
2021/08/22 19:21:31.028 INFO    using adjacent Caddyfile
adapt: Caddyfile:4: unrecognized directive: bar.example.com
Did you mean to define a second site? If so, you must use curly braces around each site to separate their configurations.
```
2021-08-23 11:53:27 -06:00
Francis Lavoie d74913f871 caddyfile: Error on invalid site addresses containing comma (#4302)
Some users forget to use a comma between their site addresses. This is invalid (commas aren't a valid character in domains) and later parts of the code like certificate automation will try to use this otherwise, which doesn't make sense. Best to error as early as possible.

Example thread on the forums where this happened: https://caddy.community/t/simplify-caddyfile/13281/9
2021-08-23 11:26:07 -06:00
Pascal Zarrad ce5a45db45 cmd: Fix paths when using an env file (#4296)
* core: Fix paths when using an env file

* refactor: move path logic to loadFromEnv
2021-08-20 15:51:31 -06:00
Adam Weinberger e0a6a1efff chore: Update quic-go for go 1.17 support (#4297)
* Update quic-go for go 1.17 support

* Complete quic-go update (go mod tidy)
2021-08-20 10:19:16 -06:00
Scott Mebberson c1cd192ee7 caddyhttp: Updated the documentation for MatchQuery (#4295) 2021-08-19 22:44:28 -06:00
Francis Lavoie a056fcd7ba chore: Upgrade smallstep libs (#4291)
See https://github.com/smallstep/nosql/issues/12 for context.
2021-08-19 16:08:19 -06:00
M. Ángel Jimeno 9e333c39da cmd: use net.ErrClosed for matching returned error (#4289)
Implements #3805
2021-08-18 12:58:19 -06:00
Matthew Holt 8a974a4f8f logging: Warn for deprecated single_field encoder 2021-08-17 10:51:26 -06:00
Francis Lavoie 6bc87ea2ff ci: Start testing on Go 1.17, drop 1.15 (#4283) 2021-08-16 21:56:20 -06:00
Rainer Borene 1b1e625c20 core: Unix ns and Unix ms time placeholders (#4280) 2021-08-16 15:06:44 -06:00
Steven Angles a10910f398 admin: Sync server variables (fix #4260) (#4274)
* Synchronize server assignment/references to avoid data race

* only hold lock during var reassignment
2021-08-16 15:04:47 -06:00
Francis Lavoie ab32440b21 httpcaddyfile: Add shortcut for proxy hostport placeholder (#4263)
* httpcaddyfile: Add shortcut for proxy hostport placeholder

I've noticed that it's a pretty common pattern to write a proxy like this, when needing to proxy over HTTPS:

```
reverse_proxy https://example.com {
	header_up Host {http.reverse_proxy.upstream.hostport}
}
```

I find it pretty hard to remember the exact placeholder to use for this, and I continually need to refer to the docs when I need it. I think a simple fix for this is to add another Caddyfile placeholder for this one to shorten it:

```
reverse_proxy https://example.com {
	header_up Host {proxy_hostport}
}
```

* Switch the shortcut name
2021-08-12 12:08:37 -06:00
Francis Lavoie e6c29ce081 reverseproxy: Incorporate latest proxy changes from stdlib (#4266)
I went through the commits that touched stdlib's `reverseproxy.go` file, and copied over all the changes that are to code that was copied into Caddy.

The commits I pulled changes from:

- https://github.com/golang/go/commit/2cc347382f4df3fb40d8d81ec9331f0748b1c394
- https://github.com/golang/go/commit/a5cea062b305c8502bdc959c0eec279dbcd4391f
- https://github.com/golang/go/commit/ecdbffd4ec68b509998792f120868fec319de59b
- https://github.com/golang/go/commit/21898524f66c075d7cfb64a38f17684140e57675
-https://github.com/golang/go/commit/ca3c0df1f8e07337ba4048b191bf905118ebe251
- https://github.com/golang/go/commit/9c017ff30dd21bbdcdb11f39458d3944db530d7e

This may also fix https://github.com/caddyserver/caddy/issues/4247 because of the change to `copyResponse` to set `mlw.flushPending = true` right away.
2021-08-12 10:48:24 -06:00
Oleg 68c5c71659 cmd: New add-package and remove-package commands (#4226)
* adding package command

* add-package command name

* refactoring duplicate code

* fixed by review

* fixed by review

* remove-package command

* commands in different files, common utils

* fix add, remove, upgrade packages in 1 file

* copyright and downloadPath moved

* refactor

* downloadPath do no export

* adding/removing multiple packages

* addPackages/removePackages, comments, command-desc

* add-package, process case len(args) == 0

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2021-08-11 17:31:41 -06:00
Frederik Ring 569ecdbd02 httpcaddyfile: Ensure hosts to skip for logs can always be collected (#4258)
* httpcaddyfile: ensure hosts to skip can always be collected

Previously, some hosts that should be skipped in logging would
be missed as the current logic would only collect them after
encountering the first server that would log. This change makes sure
the ServerLogConfig is initialized before iterating over the server
blocks.

* httpcaddyfile: add test case for skip hosts behavior
2021-08-02 14:15:27 -06:00
王清雨 c131339c5c admin: Implement load_interval to pull config on a timer (#4246)
* feat: implement a simple timer to pull config

mostly referenced to the issue

re #4106

* Update admin.go

use `caddy.Duration`

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

* Update caddy.go

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

* Update admin.go

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

* fix: sync load config when no pull interval provided

try not to make break change

* fix: change PullInterval to LoadInterval

* fix: change pull_interval to load_interval

* Update caddy.go

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

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2021-07-28 15:39:08 -06:00
Ggicci b6f51254ea caddyfile: keep error chain info in Dispenser.Errf (#4233)
* caddyfile: Errf enable error chain unwrapping

* refactor: remove parseError
2021-07-19 08:35:14 -06:00
Francis Lavoie 124ba1ba71 logging: Prep for common_log removal (#4149)
See https://github.com/caddyserver/caddy/issues/4148#issuecomment-833207811
2021-07-14 11:07:38 -06:00
Francis Lavoie 1c6c7714a3 caddyhttp: Fix edgecase with auto HTTP->HTTPS logic (#4243) 2021-07-14 10:49:34 -06:00
Leo Di Donato 46d99aba85 logging: Add missing interface guards for replace filter (#4244)
Signed-off-by: Leonardo Di Donato <leodidonato@gmail.com>
2021-07-12 11:13:01 -04:00
diamondburned 9e16e80f3c fileserver: Fix browse name_dir_first sorting (#4218)
This commit fixes the `sortByNameDirFirst` variable inside fileserver to
match what browse's default template has.

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2021-07-07 02:54:54 -04:00
elnoro d882211080 reverseproxy: Keep path to unix socket as dial address (#4232) 2021-07-06 23:43:45 -04:00
hmol233 42e140b1b2 caddyhttp: Fix incorrect determination of gRPC protocol (#4236) 2021-07-06 12:09:44 -04:00
mritd 4245ceb67d fileserver: Add disable_canonical_uris Caddyfile subdirective (#4222)
* feat(fileserver): add 'canonical_uris' parameter to caddyfile

add 'canonical_uris' parameter to caddyfile

reference #2741

Signed-off-by: mritd <mritd@linux.com>

* feat(file_server): rename subdirective canonical_uris to disable_canonical_uris

rename subdirective canonical_uris to disable_canonical_uris

Signed-off-by: mritd <mritd@linux.com>

* test(caddyfile_adapt): add disable_canonical_uris subdirective test file

add disable_canonical_uris subdirective test file

Signed-off-by: mritd <mritd@linux.com>
2021-07-01 17:22:16 -06:00
Matthew Holt 0bdb8aa82d acmeserver: Don't set host for directory links by default
This makes the server more easily proxied.
2021-07-01 17:20:51 -06:00
Matthew Holt 191dc86f9e fileserver: Clarify docs about canonicalization
Related to https://github.com/caddyserver/caddy/issues/4205.
2021-06-25 11:33:18 -06:00
Matthew Holt 81e5318021 caddytls: Remove "IssuerRaw" field
Has been deprecated and printing warnings for about 8 months now.
Replaced by "IssuersRaw" field in v2.3.0.
2021-06-25 11:29:56 -06:00
Matthew Holt b3d35a4995 httpcaddyfile: Don't put localhost in public APs (fix #4220)
If an email is specified in global options, a site called 'localhost' shouldn't be bunched together with public DNS names in the automation policies, which get the default, public-CA issuers. Fix old test that did this.

I also noticed that these two:

    localhost {
    }
    example.com {
    }

and

    localhost, example.com {
    }

produce slightly different TLS automation policies. The former is what the new test case covers, and we have logic that removes the empty automation policy for localhost so that auto-HTTPS can implicitly create one. (We prefer that whenever possible.) But the latter case produces two automation policies, with the second one being for localhost, with an explicit internal issuer. It's not wrong, just more explicit than it needs to be.

I'd really like to completely rewrite the code from scratch that generates automation policies, hopefully there is a simpler, more correct algorithm.
2021-06-25 11:28:32 -06:00
Matthew Holt 2de7e14e1c acmeserver: Trim slashes from path prefix
See https://caddy.community/t/mtls-tls-internal-error/12807
2021-06-21 11:56:41 -06:00
Matthew Holt 885a9aaf48 go.mod: Update dependencies (close #4216) 2021-06-18 12:02:47 -06:00
Klaus Post 69c914483d encode: Tweak compression settings (#4215)
* Tweak compression settings

zstd: Limit window sizes to 128K to keep memory in control both server and client size.
zstd: Write 0 length frames. This may be needed for compatibility.
zstd: Create fewer encoders. Small memory improvement.
gzip: Allow -2 (Huffman only) and -3 (stateless) compression modes.

* Update modules/caddyhttp/encode/zstd/zstd.go

Update docs.

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

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2021-06-18 11:49:49 -06:00
Matt Holt 9d4ed3a323 caddyhttp: Refactor and export SanitizedPathJoin for use in fastcgi (#4207) 2021-06-17 09:59:08 -06:00
Matthew Holt fbd6560976 fileserver: Only redirect if filename not rewritten (fix #4205)
This is the more correct implementation of  23dadc0d86 (#4179)... I think. This commit effectively undoes the revert in 8848df9c5d, but with corrections to the logic.

We *do* need to use the original request path (the path the browser knows) for redirects, since they are external, and rewrites are only internal.

However, if the path was rewritten to a non-canonical path, we should not redirect to canonicalize that, since rewrites are intentional by the site owner. Canonicalizing the path involves modifying only the suffix (base element, or filename) of the path. Thus, if a rewrite involves only the prefix (like how handle_path strips a path prefix), then we can (hopefully!) safely redirect using the original URI since the filename was not rewritten.

So basically, if rewrites modify the filename, we should not canonicalize those requests. If rewrites only modify another part of the path (commonly a prefix), we should be OK to redirect.
2021-06-17 09:55:49 -06:00
Matthew Holt 238914d70b Some misc. cleanup
The fastcgi changes came from v1 which don't make sense in v2.

Fix comment about default value in reverse proxy keep alive.
2021-06-16 14:29:42 -06:00
Matthew Holt e8ae80adca fileserver: Don't persist parsed template (fix #4202)
Templates are parsed at request-time (like they are in the templates middleware) to allow live changes to the template while the server is running. Fixes race condition.

Also refactored use of a buffer so a buffer put back in the pool will not continue to be used (written to client) in the meantime.

A couple of benchmarks removed due to refactor, which is fine, since we know pooling helps here.
2021-06-16 14:28:34 -06:00
Matthew Holt 32c284b54a reverseproxy: Adjust test related to #4201
Commit 7c68809f4e
2021-06-15 15:02:22 -06:00
Matthew Holt 7c68809f4e reverseproxy: Fix overwriting of max_idle_conns_per_host (closes #4201)
Also split the Caddyfile subdirective keepalive_idle_conns into two properties so the conns and conns_per_host can be set separately.

This is technically a breaking change, but probably anyone who this breaks already had a broken config anyway, and silently fixing it won't help them fix their configs.
2021-06-15 14:54:48 -06:00
Matthew Holt 6d25261c22 Expand and clarify security policy
While the Caddy project has had very few valid security bug reports over the years, we have a low signal-to-noise ratio with them (lots of invalid reports). Most are out of scope, and it can take too much valuable time for us to determine that. We would prefer researchers do this first. Hopefully these paragraphs spell out much more clearly what we do and don't accept.
2021-06-14 14:00:43 -06:00
Matthew Holt 8848df9c5d Revert "fileserver: Redirect within the original URL (#4179)"
This reverts commit f9b54454a1.
/cc @diamondburned (see #4205)
2021-06-14 09:04:30 -06:00
Matt Holt 89aa3a5ef3 go.mod: Use CertMagic v0.14.0 (fix #4191)
* Force auto-renew for OCSP revoked status (maybe) (fix #4191)

* Use latest commit

* go.mod: Use CertMagic v0.14.0 (fix #4191)

Correctly replaces revoked certificates
2021-06-12 14:44:32 -06:00
Matthew Holt 05656a60b3 httpcaddyfile: Don't add HTTP hosts to TLS APs (fix #4176 and fix #4198)
In the Caddyfile, hosts specified for HTTP sockets (either scheme is "http" or it is on the HTTP port) should not be used as subjects in TLS automation policies (APs).
2021-06-09 14:35:09 -06:00
Klooven 1e92258dd6 httpcaddyfile: Add preferred_chains global option and issuer subdirective (#4192)
* Added preferred_chains option to Caddyfile

* Caddyfile adapt tests for preferred_chains
2021-06-08 14:10:37 -06:00
diamondburned 76913b19ff fileserver: Fix browse not redirecting query parameters (#4196)
This commit is a follow up to PR #4179 that introduced a bug where
browse redirections to the right URL would not preserve query
parameters.
2021-06-07 17:33:54 -06:00
Peter Magnusson 4c2da18841 caddytls: Add Caddyfile support for propagation_timeout (#4178)
* add propagation_timeout to UnmarshalCaddyfile

- Closes #4177

* added caddyfile_adapt test
2021-06-07 12:25:12 -06:00
diamondburned f9b54454a1 fileserver: Redirect within the original URL (#4179)
This commit changes the file_server directive to redirect using the
original request's URL instead of the possibly trimmed URL. This should
make file_server work with handle_path.

This fix is taken from mholt's comment in
https://caddy.community/t/file-servers-on-different-paths-not-working/11698/11.
2021-06-07 12:20:08 -06:00
Francis Lavoie 658772ff24 httpcaddyfile: Add skip_install_trust global option (#4153)
Fixes https://github.com/caddyserver/caddy/issues/4002
2021-06-07 12:18:49 -06:00
Matthew Holt 323ffd2076 admin: Replace admin cert cache when reloading (fix #4184) 2021-06-05 11:47:44 -06:00
Matthew Holt 2a8109468c reverseproxy: Always remove hop-by-hop headers
See golang/go#46313

Based on https://github.com/golang/go/commit/950fa11c4cb01a145bb07eeb167d90a1846061b3
2021-06-04 15:21:16 -06:00
Francis Lavoie 94b712009a logging: Actually use level_key (#4189) 2021-06-04 14:15:43 -06:00
Dave Henderson 7b500e74b4 metrics: use buildinfo collector from new collectors pkg (#4187)
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
2021-06-04 00:19:16 -04:00
Matthew Holt ecd5eeab38 go.mod: Update direct dependencies 2021-06-03 12:18:25 -06:00
Matt Holt b4cef492cc Update .goreleaser.yml
Ubuntu's package updater doesn't show the name of the package, so just adding "Caddy" to the description helps a lot
2021-05-24 16:21:53 -06:00
Matt Holt e3c369d452 logging: Implement dial timeout for net writer (fix #4083) (#4172)
* logging: Implement dial timeout for net writer (fix #4083)

* Limit how often redials are attempted

This should cause dial blocking to occur only once every 10 seconds at most, but it also means the logger connection might be down for up to 10 seconds after it comes back online; oh well. We shouldn't block for DialTimeout at every single log emission.

* Clarify offline behavior
2021-05-19 15:14:03 -06:00
Matthew Holt c052162203 Merge branch '2.4' 2021-05-19 10:47:06 -06:00
Matthew Holt 7f26a6b3e5 admin: Reinstate internal redirect for /id/ requests
Fix regression from ab80ff4fd2 (probably a mistake when rebasing)

See https://caddy.community/t/id-selector-is-not-working-after-upgrade-to-2-4-0/12513?u=matt
2021-05-19 10:27:25 -06:00
Francis Lavoie b82db994f3 caddyfile: Add parse error on site address with trailing { (#4163)
* caddyfile: Add parse error on site address in `{`

This is an incredibly common mistake made by users, so we should catch it earlier in the parser and give a more friendly message. Often it ends up adapting but with mistakes, or erroring out later due to other site addresses being read as directives.

There's not really ever a situation where a lone '{' is valid at the end of a site address (but I suppose there are edgecases where the user wants to use a path matcher where it ends specifically in `{`, but... why?), so this should be fine.

* Update caddyconfig/caddyfile/parse.go
2021-05-12 16:18:44 -06:00
Francis Lavoie aef8d4decc reverseproxy: Set the headers in the replacer before handle_response (#4165)
Turns out this was an oversight, we assumed we could use `{http.response.header.*}` but that doesn't work because those are grabbed from the response writer, and we haven't copied any headers into the response writer yet.

So the fix is to set all the response headers into the replacer at a new namespace before running the handlers.

This adds the `{http.reverse_proxy.header.*}` replacer.

See https://caddy.community/t/empty-http-response-header-x-accel-redirect/12447
2021-05-12 14:19:08 -06:00
Francis Lavoie 37718560c1 ci: Run CI on PRs targeting minor version branches (#4164)
We decided that we'll use branches like `2.4` as the target for any changes that we might want to release in a `2.4.x` version like `2.4.1`, so that we can continue to merge changes targeting the next minor release (e.g. `2.5.0`) on master.

Our CI config wasn't set up for this to work properly though, since it was only running checks on PRs targeting master. This should fix it.

I couldn't find a way to do a pattern to only match digits for the branch names from Github's docs, it just looks like a pretty generic glob syntax. But this should do until we get to 3.0
2021-05-12 00:26:16 -04:00
Mohammed Al Sahaf 2aefe15686 cmd: upgrade: inherit the permissions of the original executable (#4160) 2021-05-11 16:11:27 -06:00
Matthew Holt dbe164d98a httpcaddyfile: Fix automation policy consolidation again (fix #4161)
Also fix a previous test that asserted incorrect behavior.
2021-05-11 15:26:07 -06:00
75 changed files with 2974 additions and 1001 deletions
+37 -5
View File
@@ -2,9 +2,6 @@
The Caddy project would like to make sure that it stays on top of all practically-exploitable vulnerabilities. The Caddy project would like to make sure that it stays on top of all practically-exploitable vulnerabilities.
Some security problems are more the result of interplay between different components of the Web, rather than a vulnerability in the web server itself. Please report only vulnerabilities in the web server itself, as we cannot coerce the rest of the Web to be fixed (for example, we do not consider IP spoofing or BGP hijacks a vulnerability in the Caddy web server).
Please note that we consider publicly-registered domain names to be public information. This necessary in order to maintain the integrity of certificate transparency, public DNS, and other public trust systems.
## Supported Versions ## Supported Versions
@@ -14,11 +11,46 @@ Please note that we consider publicly-registered domain names to be public infor
| 1.x | :x: | | 1.x | :x: |
| < 1.x | :x: | | < 1.x | :x: |
## Acceptable Scope
A security report must demonstrate a security bug in the source code from this repository.
Some security problems are the result of interplay between different components of the Web, rather than a vulnerability in the web server itself. Please only report vulnerabilities in the web server itself, as we cannot coerce the rest of the Web to be fixed (for example, we do not consider IP spoofing, BGP hijacks, or missing/misconfigured HTTP headers a vulnerability in the Caddy web server).
Vulnerabilities caused by misconfigurations are out of scope. Yes, it is entirely possible to craft and use a configuration that is unsafe, just like with every other web server; we recommend against doing that.
We do not accept reports if the steps imply or require a compromised system or third-party software, as we cannot control those. We expect that users secure their own systems and keep all their software patched. For example, if untrusted users are able to upload/write/host arbitrary files in the web root directory, it is NOT a security bug in Caddy if those files get served to clients; however, it _would_ be a valid report if a bug in Caddy's source code unintentionally gave unauthorized users the ability to upload unsafe files or delete files without relying on an unpatched system or piece of software.
Client-side exploits are out of scope. In other words, it is not a bug in Caddy if the web browser does something unsafe, even if the downloaded content was served by Caddy. (Those kinds of exploits can generally be mitigated by proper configuration of HTTP headers.) As a general rule, the content served by Caddy is not considered in scope because content is configurable by the site owner or the associated web application.
Security bugs in code dependencies are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code.
## Reporting a Vulnerability ## Reporting a Vulnerability
Please email Matt Holt (the author) directly: matt [at] lightcodelabs [dot com]. We get a lot of difficult reports that turn out to be invalid. Clear, obvious reports tend to be the most credible (but are also rare).
We'll need enough information to verify the bug and make a patch. It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, resources permitting. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. Thank you for understanding. First please ensure your report falls within the accepted scope of security bugs (above).
We'll need enough information to verify the bug and make a patch. To speed things up, please include:
- Most minimal possible config (without redactions!)
- Command(s)
- Precise HTTP requests (`curl -v` and its output please)
- Full log output (please enable debug mode)
- Specific minimal steps to reproduce the issue from scratch
- A working patch
Please DO NOT use containers, VMs, cloud instances or services, or any other complex infrastructure in your steps. Always prefer `curl` instead of web browsers.
We consider publicly-registered domain names to be public information. This necessary in order to maintain the integrity of certificate transparency, public DNS, and other public trust systems. Do not redact domain names from your reports. The actual content of your domain name affects Caddy's behavior, so we need the exact domain name(s) to reproduce with, or your report will be ignored.
It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, depending on available resources. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. (We get a lot of invalid reports.) Thank you for understanding.
When you are ready, please email Matt Holt (the author) directly: matt [at] lightcodelabs [dot com].
Please don't encrypt the email body. It only makes the process more complicated.
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.
+4 -1
View File
@@ -6,9 +6,11 @@ on:
push: push:
branches: branches:
- master - master
- 2.*
pull_request: pull_request:
branches: branches:
- master - master
- 2.*
jobs: jobs:
test: test:
@@ -17,7 +19,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: [ '1.15', '1.16' ] go: [ '1.16', '1.17' ]
# 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
@@ -154,6 +156,7 @@ jobs:
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: goreleaser/goreleaser-action@v2 - uses: goreleaser/goreleaser-action@v2
with: with:
version: latest version: latest
+3 -1
View File
@@ -4,9 +4,11 @@ on:
push: push:
branches: branches:
- master - master
- 2.*
pull_request: pull_request:
branches: branches:
- master - master
- 2.*
jobs: jobs:
cross-build-test: cross-build-test:
@@ -14,7 +16,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd'] goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
go: [ '1.15', '1.16' ] go: [ '1.17' ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
steps: steps:
+2
View File
@@ -4,9 +4,11 @@ on:
push: push:
branches: branches:
- master - master
- 2.*
pull_request: pull_request:
branches: branches:
- master - master
- 2.*
jobs: jobs:
# From https://github.com/golangci/golangci-lint-action # From https://github.com/golangci/golangci-lint-action
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ ubuntu-latest ] os: [ ubuntu-latest ]
go: [ '1.16' ] go: [ '1.17' ]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
+1 -1
View File
@@ -79,7 +79,7 @@ nfpms:
homepage: https://caddyserver.com homepage: https://caddyserver.com
maintainer: Matthew Holt <mholt@users.noreply.github.com> maintainer: Matthew Holt <mholt@users.noreply.github.com>
description: | description: |
Powerful, enterprise-ready, open source web server with automatic HTTPS written in Go Caddy - Powerful, enterprise-ready, open source web server with automatic HTTPS written in Go
license: Apache 2.0 license: Apache 2.0
formats: formats:
+1 -1
View File
@@ -75,7 +75,7 @@ For other install options, see https://caddyserver.com/docs/install.
Requirements: Requirements:
- [Go 1.15 or newer](https://golang.org/dl/) - [Go 1.16 or newer](https://golang.org/dl/)
### For development ### For development
+41 -13
View File
@@ -109,6 +109,12 @@ type ConfigSettings struct {
// //
// EXPERIMENTAL: Subject to change. // EXPERIMENTAL: Subject to change.
LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"` LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"`
// The interval to pull config. With a non-zero value, will pull config
// from config loader (eg. a http loader) with given interval.
//
// EXPERIMENTAL: Subject to change.
LoadInterval Duration `json:"load_interval,omitempty"`
} }
// IdentityConfig configures management of this server's identity. An identity // IdentityConfig configures management of this server's identity. An identity
@@ -329,6 +335,7 @@ func replaceLocalAdminServer(cfg *Config) error {
return err return err
} }
serverMu.Lock()
localAdminServer = &http.Server{ localAdminServer = &http.Server{
Addr: addr.String(), // for logging purposes only Addr: addr.String(), // for logging purposes only
Handler: handler, Handler: handler,
@@ -337,10 +344,14 @@ func replaceLocalAdminServer(cfg *Config) error {
IdleTimeout: 60 * time.Second, IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1024 * 64, MaxHeaderBytes: 1024 * 64,
} }
serverMu.Unlock()
adminLogger := Log().Named("admin") adminLogger := Log().Named("admin")
go func() { go func() {
if err := localAdminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) { serverMu.Lock()
server := localAdminServer
serverMu.Unlock()
if err := server.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err)) adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
} }
}() }()
@@ -364,11 +375,6 @@ func manageIdentity(ctx Context, cfg *Config) error {
return nil return nil
} }
oldIdentityCertCache := identityCertCache
if oldIdentityCertCache != nil {
defer oldIdentityCertCache.Stop()
}
// set default issuers; this is pretty hacky because we can't // set default issuers; this is pretty hacky because we can't
// import the caddytls package -- but it works // import the caddytls package -- but it works
if cfg.Admin.Identity.IssuersRaw == nil { if cfg.Admin.Identity.IssuersRaw == nil {
@@ -389,8 +395,13 @@ func manageIdentity(ctx Context, cfg *Config) error {
} }
} }
// we'll make a new cache when we make the CertMagic config, so stop any previous cache
if identityCertCache != nil {
identityCertCache.Stop()
}
logger := Log().Named("admin.identity") logger := Log().Named("admin.identity")
cmCfg := cfg.Admin.Identity.certmagicConfig(logger) cmCfg := cfg.Admin.Identity.certmagicConfig(logger, true)
// issuers have circular dependencies with the configs because, // issuers have circular dependencies with the configs because,
// as explained in the caddytls package, they need access to the // as explained in the caddytls package, they need access to the
@@ -456,7 +467,7 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
} }
// create TLS config that will enforce mutual authentication // create TLS config that will enforce mutual authentication
cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger) cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger, false)
tlsConfig := cmCfg.TLSConfig() tlsConfig := cmCfg.TLSConfig()
tlsConfig.NextProtos = nil // this server does not solve ACME challenges tlsConfig.NextProtos = nil // this server does not solve ACME challenges
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
@@ -468,6 +479,7 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
return err return err
} }
serverMu.Lock()
// create secure HTTP server // create secure HTTP server
remoteAdminServer = &http.Server{ remoteAdminServer = &http.Server{
Addr: addr.String(), // for logging purposes only Addr: addr.String(), // for logging purposes only
@@ -479,6 +491,7 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
MaxHeaderBytes: 1024 * 64, MaxHeaderBytes: 1024 * 64,
ErrorLog: serverLogger, ErrorLog: serverLogger,
} }
serverMu.Unlock()
// start listener // start listener
ln, err := Listen(addr.Network, addr.JoinHostPort(0)) ln, err := Listen(addr.Network, addr.JoinHostPort(0))
@@ -488,7 +501,10 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
ln = tls.NewListener(ln, tlsConfig) ln = tls.NewListener(ln, tlsConfig)
go func() { go func() {
if err := remoteAdminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) { serverMu.Lock()
server := remoteAdminServer
serverMu.Unlock()
if err := server.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
remoteLogger.Error("admin remote server shutdown for unknown reason", zap.Error(err)) remoteLogger.Error("admin remote server shutdown for unknown reason", zap.Error(err))
} }
}() }()
@@ -499,7 +515,7 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
return nil return nil
} }
func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger) *certmagic.Config { func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool) *certmagic.Config {
if ident == nil { if ident == nil {
// user might not have configured identity; that's OK, we can still make a // user might not have configured identity; that's OK, we can still make a
// certmagic config, although it'll be mostly useless for remote management // certmagic config, although it'll be mostly useless for remote management
@@ -510,7 +526,7 @@ func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger) *certmagic.Conf
Logger: logger, Logger: logger,
Issuers: ident.issuers, Issuers: ident.issuers,
} }
if identityCertCache == nil { if makeCache {
identityCertCache = certmagic.NewCache(certmagic.CacheOptions{ identityCertCache = certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) { GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
return cmCfg, nil return cmCfg, nil
@@ -533,7 +549,7 @@ func (ctx Context) IdentityCredentials(logger *zap.Logger) ([]tls.Certificate, e
if logger == nil { if logger == nil {
logger = Log() logger = Log()
} }
magic := ident.certmagicConfig(logger) magic := ident.certmagicConfig(logger, false)
return magic.ClientCredentials(ctx, ident.Identifiers) return magic.ClientCredentials(ctx, ident.Identifiers)
} }
@@ -717,6 +733,10 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
if err == nil { if err == nil {
return return
} }
if err == errInternalRedir {
h.serveHTTP(w, r)
return
}
apiErr, ok := err.(APIError) apiErr, ok := err.(APIError)
if !ok { if !ok {
@@ -896,7 +916,7 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
parts = append([]string{expanded}, parts[3:]...) parts = append([]string{expanded}, parts[3:]...)
r.URL.Path = path.Join(parts...) r.URL.Path = path.Join(parts...)
return nil return errInternalRedir
} }
func handleStop(w http.ResponseWriter, r *http.Request) error { func handleStop(w http.ResponseWriter, r *http.Request) error {
@@ -1199,6 +1219,13 @@ var idRegexp = regexp.MustCompile(`(?m),?\s*"` + idKey + `"\s*:\s*(-?[0-9]+(\.[0
// pidfile is the name of the pidfile, if any. // pidfile is the name of the pidfile, if any.
var pidfile string var pidfile string
// errInternalRedir indicates an internal redirect
// and is useful when admin API handlers rewrite
// the request; in that case, authentication and
// authorization needs to happen again for the
// rewritten request.
var errInternalRedir = fmt.Errorf("internal redirect; re-authorization required")
const ( const (
rawConfigKey = "config" rawConfigKey = "config"
idKey = "@id" idKey = "@id"
@@ -1212,6 +1239,7 @@ var bufPool = sync.Pool{
// keep a reference to admin endpoint singletons while they're active // keep a reference to admin endpoint singletons while they're active
var ( var (
serverMu sync.Mutex
localAdminServer, remoteAdminServer *http.Server localAdminServer, remoteAdminServer *http.Server
identityCertCache *certmagic.Cache identityCertCache *certmagic.Cache
) )
+36 -18
View File
@@ -17,9 +17,28 @@ package caddy
import ( import (
"encoding/json" "encoding/json"
"reflect" "reflect"
"sync"
"testing" "testing"
) )
var testCfg = []byte(`{
"apps": {
"http": {
"servers": {
"myserver": {
"listen": ["tcp/localhost:8080-8084"],
"read_timeout": "30s"
},
"yourserver": {
"listen": ["127.0.0.1:5000"],
"read_header_timeout": "15s"
}
}
}
}
}
`)
func TestUnsyncedConfigAccess(t *testing.T) { func TestUnsyncedConfigAccess(t *testing.T) {
// each test is performed in sequence, so // each test is performed in sequence, so
// each change builds on the previous ones; // each change builds on the previous ones;
@@ -108,25 +127,24 @@ func TestUnsyncedConfigAccess(t *testing.T) {
} }
} }
// TestLoadConcurrent exercises Load under concurrent conditions
// and is most useful under test with `-race` enabled.
func TestLoadConcurrent(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
_ = Load(testCfg, true)
wg.Done()
}()
}
wg.Wait()
}
func BenchmarkLoad(b *testing.B) { func BenchmarkLoad(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
cfg := []byte(`{ Load(testCfg, true)
"apps": {
"http": {
"servers": {
"myserver": {
"listen": ["tcp/localhost:8080-8084"],
"read_timeout": "30s"
},
"yourserver": {
"listen": ["127.0.0.1:5000"],
"read_header_timeout": "15s"
}
}
}
}
}
`)
Load(cfg, true)
} }
} }
+32 -12
View File
@@ -268,8 +268,9 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
newCfg != nil && newCfg != nil &&
newCfg.Admin != nil && newCfg.Admin != nil &&
newCfg.Admin.Config != nil && newCfg.Admin.Config != nil &&
newCfg.Admin.Config.LoadRaw != nil { newCfg.Admin.Config.LoadRaw != nil &&
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs") newCfg.Admin.Config.LoadInterval <= 0 {
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_interval")
} }
// run the new config and start all its apps // run the new config and start all its apps
@@ -480,23 +481,42 @@ func finishSettingUp(ctx Context, cfg *Config) error {
if err != nil { if err != nil {
return fmt.Errorf("loading config loader module: %s", err) return fmt.Errorf("loading config loader module: %s", err)
} }
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx) runLoadedConfig := func(config []byte) {
if err != nil { Log().Info("applying dynamically-loaded config", zap.String("loader_module", val.(Module).CaddyModule().ID.Name()), zap.Int("pull_interval", int(cfg.Admin.Config.LoadInterval)))
return fmt.Errorf("loading dynamic config from %T: %v", val, err)
}
// do this in a goroutine so current config can finish being loaded; otherwise deadlock
go func() {
Log().Info("applying dynamically-loaded config", zap.String("loader_module", val.(Module).CaddyModule().ID.Name()))
currentCfgMu.Lock() currentCfgMu.Lock()
err := unsyncedDecodeAndRun(loadedConfig, false) err := unsyncedDecodeAndRun(config, false)
currentCfgMu.Unlock() currentCfgMu.Unlock()
if err == nil { if err == nil {
Log().Info("dynamically-loaded config applied successfully") Log().Info("dynamically-loaded config applied successfully")
} else { } else {
Log().Error("running dynamically-loaded config failed", zap.Error(err)) Log().Error("running dynamically-loaded config failed", zap.Error(err))
} }
}() }
if cfg.Admin.Config.LoadInterval > 0 {
go func() {
select {
// if LoadInterval is positive, will wait for the interval and then run with new config
case <-time.After(time.Duration(cfg.Admin.Config.LoadInterval)):
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
if err != nil {
Log().Error("loading dynamic config failed", zap.Error(err))
return
}
runLoadedConfig(loadedConfig)
case <-ctx.Done():
return
}
}()
} else {
// if no LoadInterval is provided, will load config synchronously
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
if err != nil {
return fmt.Errorf("loading dynamic config from %T: %v", val, err)
}
// do this in a goroutine so current config can finish being loaded; otherwise deadlock
go runLoadedConfig(loadedConfig)
}
} }
return nil return nil
+3 -3
View File
@@ -345,13 +345,13 @@ func (d *Dispenser) EOFErr() error {
// Err generates a custom parse-time error with a message of msg. // Err generates a custom parse-time error with a message of msg.
func (d *Dispenser) Err(msg string) error { func (d *Dispenser) Err(msg string) error {
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg) return d.Errf(msg)
return errors.New(msg)
} }
// Errf is like Err, but for formatted error messages // Errf is like Err, but for formatted error messages
func (d *Dispenser) Errf(format string, args ...interface{}) error { func (d *Dispenser) Errf(format string, args ...interface{}) error {
return d.Err(fmt.Sprintf(format, args...)) err := fmt.Errorf(format, args...)
return fmt.Errorf("%s:%d - Error during parsing: %w", d.File(), d.Line(), err)
} }
// Delete deletes the current token and returns the updated slice // Delete deletes the current token and returns the updated slice
+7
View File
@@ -15,6 +15,7 @@
package caddyfile package caddyfile
import ( import (
"errors"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
@@ -303,4 +304,10 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
if !strings.Contains(err.Error(), "foobar") { if !strings.Contains(err.Error(), "foobar") {
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err) t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
} }
var ErrBarIsFull = errors.New("bar is full")
bookingError := d.Errf("unable to reserve: %w", ErrBarIsFull)
if !errors.Is(bookingError, ErrBarIsFull) {
t.Errorf("Errf(): should be able to unwrap the error chain")
}
} }
+21 -2
View File
@@ -211,9 +211,20 @@ func (p *parser) addresses() error {
if expectingAnother { if expectingAnother {
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn) return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
} }
// Mark this server block as being defined with braces.
// This is used to provide a better error message when
// the user may have tried to define two server blocks
// without having used braces, which are required in
// that case.
p.block.HasBraces = true
break break
} }
// Users commonly forget to place a space between the address and the '{'
if strings.HasSuffix(tkn, "{") {
return p.Errf("Site addresses cannot end with a curly brace: '%s' - put a space between the token and the brace", tkn)
}
if tkn != "" { // empty token possible if user typed "" if tkn != "" { // empty token possible if user typed ""
// Trailing comma indicates another address will follow, which // Trailing comma indicates another address will follow, which
// may possibly be on the next line // may possibly be on the next line
@@ -224,6 +235,13 @@ func (p *parser) addresses() error {
expectingAnother = false // but we may still see another one on this line expectingAnother = false // but we may still see another one on this line
} }
// If there's a comma here, it's probably because they didn't use a space
// between their two domains, e.g. "foo.com,bar.com", which would not be
// parsed as two separate site addresses.
if strings.Contains(tkn, ",") {
return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", tkn)
}
p.block.Keys = append(p.block.Keys, tkn) p.block.Keys = append(p.block.Keys, tkn)
} }
@@ -559,8 +577,9 @@ func (p *parser) snippetTokens() ([]Token, error) {
// head of the server block with tokens, which are // head of the server block with tokens, which are
// grouped by segments. // grouped by segments.
type ServerBlock struct { type ServerBlock struct {
Keys []string HasBraces bool
Segments []Segment Keys []string
Segments []Segment
} }
// DispenseDirective returns a dispenser that contains // DispenseDirective returns a dispenser that contains
+4
View File
@@ -160,6 +160,10 @@ func TestParseOneAndImport(t *testing.T) {
"localhost", "localhost",
}, []int{}}, }, []int{}},
{`localhost{
dir1
}`, true, []string{}, []int{}},
{`localhost {`localhost
dir1 { dir1 {
nested { nested {
+26 -5
View File
@@ -44,9 +44,9 @@ var directiveOrder = []string{
"request_body", "request_body",
"redir", "redir",
"rewrite",
// URI manipulation // URI manipulation
"rewrite",
"uri", "uri",
"try_files", "try_files",
@@ -54,23 +54,23 @@ var directiveOrder = []string{
"basicauth", "basicauth",
"request_header", "request_header",
"encode", "encode",
"push",
"templates", "templates",
// special routing & dispatching 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
"abort",
"error",
"respond", "respond",
"metrics", "metrics",
"reverse_proxy", "reverse_proxy",
"php_fastcgi", "php_fastcgi",
"file_server", "file_server",
"acme_server", "acme_server",
"abort",
"error",
} }
// directiveIsOrdered returns true if dir is // directiveIsOrdered returns true if dir is
@@ -329,7 +329,7 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
dir := seg.Directive() dir := seg.Directive()
dirFunc, ok := registeredDirectives[dir] dirFunc, ok := registeredDirectives[dir]
if !ok { if !ok {
return nil, h.Errf("unrecognized directive: %s", dir) return nil, h.Errf("unrecognized directive: %s - are you sure your Caddyfile structure (nesting and braces) is correct?", dir)
} }
subHelper := h subHelper := h
@@ -478,6 +478,27 @@ func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
return sblockHosts return sblockHosts
} }
func (sb serverBlock) hostsFromKeysNotHTTP(httpPort string) []string {
// ensure each entry in our list is unique
hostMap := make(map[string]struct{})
for _, addr := range sb.keys {
if addr.Host == "" {
continue
}
if addr.Scheme != "http" && addr.Port != httpPort {
hostMap[addr.Host] = struct{}{}
}
}
// convert map to slice
sblockHosts := make([]string, 0, len(hostMap))
for host := range hostMap {
sblockHosts = append(sblockHosts, host)
}
return sblockHosts
}
// hasHostCatchAllKey returns true if sb has a key that // hasHostCatchAllKey returns true if sb has a key that
// omits a host portion, i.e. it "catches all" hosts. // omits a host portion, i.e. it "catches all" hosts.
func (sb serverBlock) hasHostCatchAllKey() bool { func (sb serverBlock) hasHostCatchAllKey() bool {
+16 -4
View File
@@ -113,6 +113,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
"{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}", "{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
) )
// these are placeholders that allow a user-defined final // these are placeholders that allow a user-defined final
@@ -169,7 +170,11 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
dirFunc, ok := registeredDirectives[dir] dirFunc, ok := registeredDirectives[dir]
if !ok { if !ok {
tkn := segment[0] tkn := segment[0]
return nil, warnings, fmt.Errorf("%s:%d: unrecognized directive: %s", tkn.File, tkn.Line, dir) message := "%s:%d: unrecognized directive: %s"
if !sb.block.HasBraces {
message += "\nDid you mean to define a second site? If so, you must use curly braces around each site to separate their configurations."
}
return nil, warnings, fmt.Errorf(message, tkn.File, tkn.Line, dir)
} }
h := Helper{ h := Helper{
@@ -522,6 +527,16 @@ func (st *ServerType) serversFromPairings(
} }
} }
// if needed, the ServerLogConfig is initialized beforehand so
// that all server blocks can populate it with data, even when not
// coming with a log directive
for _, sblock := range p.serverBlocks {
if len(sblock.pile["custom_log"]) != 0 {
srv.Logs = new(caddyhttp.ServerLogConfig)
break
}
}
// 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)
@@ -636,9 +651,6 @@ func (st *ServerType) serversFromPairings(
sblockLogHosts := sblock.hostsFromKeys(true) sblockLogHosts := sblock.hostsFromKeys(true)
for _, cval := range sblock.pile["custom_log"] { for _, cval := range sblock.pile["custom_log"] {
ncl := cval.Value.(namedCustomLog) ncl := cval.Value.(namedCustomLog)
if srv.Logs == nil {
srv.Logs = new(caddyhttp.ServerLogConfig)
}
if sblock.hasHostCatchAllKey() { if sblock.hasHostCatchAllKey() {
// all requests for hosts not able to be listed should use // all requests for hosts not able to be listed should use
// this log because it's a catch-all-hosts server block // this log because it's a catch-all-hosts server block
+7
View File
@@ -39,6 +39,7 @@ func init() {
RegisterGlobalOption("acme_dns", parseOptACMEDNS) RegisterGlobalOption("acme_dns", parseOptACMEDNS)
RegisterGlobalOption("acme_eab", parseOptACMEEAB) RegisterGlobalOption("acme_eab", parseOptACMEEAB)
RegisterGlobalOption("cert_issuer", parseOptCertIssuer) RegisterGlobalOption("cert_issuer", parseOptCertIssuer)
RegisterGlobalOption("skip_install_trust", parseOptTrue)
RegisterGlobalOption("email", parseOptSingleString) RegisterGlobalOption("email", parseOptSingleString)
RegisterGlobalOption("admin", parseOptAdmin) RegisterGlobalOption("admin", parseOptAdmin)
RegisterGlobalOption("on_demand_tls", parseOptOnDemand) RegisterGlobalOption("on_demand_tls", parseOptOnDemand)
@@ -48,6 +49,7 @@ func init() {
RegisterGlobalOption("servers", parseServerOptions) RegisterGlobalOption("servers", parseServerOptions)
RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions) RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
RegisterGlobalOption("log", parseLogOptions) RegisterGlobalOption("log", parseLogOptions)
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
} }
func parseOptTrue(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { return true, nil } func parseOptTrue(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { return true, nil }
@@ -451,3 +453,8 @@ func parseLogOptions(d *caddyfile.Dispenser, existingVal interface{}) (interface
return configValues, nil return configValues, nil
} }
func parseOptPreferredChains(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next()
return caddytls.ParseCaddyfilePreferredChainsOptions(d)
}
+20
View File
@@ -27,15 +27,35 @@ func (st ServerType) buildPKIApp(
pkiApp := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)} pkiApp := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
skipInstallTrust := false
if _, ok := options["skip_install_trust"]; ok {
skipInstallTrust = true
}
falseBool := false
for _, p := range pairings { for _, p := range pairings {
for _, sblock := range p.serverBlocks { for _, sblock := range p.serverBlocks {
// find all the CAs that were defined and add them to the app config // find all the CAs that were defined and add them to the app config
// i.e. from any "acme_server" directives
for _, caCfgValue := range sblock.pile["pki.ca"] { for _, caCfgValue := range sblock.pile["pki.ca"] {
ca := caCfgValue.Value.(*caddypki.CA) ca := caCfgValue.Value.(*caddypki.CA)
if skipInstallTrust {
ca.InstallTrust = &falseBool
}
pkiApp.CAs[ca.ID] = ca pkiApp.CAs[ca.ID] = ca
} }
} }
} }
// if there was no CAs defined in any of the servers,
// and we were requested to not install trust, then
// add one for the default/local CA to do so
if len(pkiApp.CAs) == 0 && skipInstallTrust {
ca := new(caddypki.CA)
ca.ID = caddypki.DefaultCAID
ca.InstallTrust = &falseBool
pkiApp.CAs[ca.ID] = ca
}
return pkiApp, warnings, nil return pkiApp, warnings, nil
} }
+44 -7
View File
@@ -189,7 +189,7 @@ func (st ServerType) buildTLSApp(
} }
// associate our new automation policy with this server block's hosts // associate our new automation policy with this server block's hosts
ap.Subjects = sblockHosts ap.Subjects = sblock.hostsFromKeysNotHTTP(httpPort)
sort.Strings(ap.Subjects) // solely for deterministic test results sort.Strings(ap.Subjects) // solely for deterministic test results
// if a combination of public and internal names were given // if a combination of public and internal names were given
@@ -211,7 +211,7 @@ func (st ServerType) buildTLSApp(
// it that we would need to check here) since the hostname is known at handshake; // 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 // 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. // regular certificates on-demand for a class of certs like *.*.tld.
if !certmagic.SubjectIsIP(s) && !certmagic.SubjectIsInternal(s) && (strings.Count(s, "*.") < 2 || ap.OnDemand) { if subjectQualifiesForPublicCert(ap, s) {
external = append(external, s) external = append(external, s)
} else { } else {
internal = append(internal, s) internal = append(internal, s)
@@ -321,10 +321,15 @@ func (st ServerType) buildTLSApp(
globalACMECARoot := options["acme_ca_root"] globalACMECARoot := options["acme_ca_root"]
globalACMEDNS := options["acme_dns"] globalACMEDNS := options["acme_dns"]
globalACMEEAB := options["acme_eab"] globalACMEEAB := options["acme_eab"]
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil globalPreferredChains := options["preferred_chains"]
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil
if hasGlobalACMEDefaults { if hasGlobalACMEDefaults {
for _, ap := range tlsApp.Automation.Policies { // for _, ap := range tlsApp.Automation.Policies {
if len(ap.Issuers) == 0 { for i := 0; i < len(tlsApp.Automation.Policies); i++ {
ap := tlsApp.Automation.Policies[i]
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
// for public names, create default issuers which will later be filled in with configured global defaults
// (internal names will implicitly use the internal issuer at auto-https time)
ap.Issuers = caddytls.DefaultIssuers() ap.Issuers = caddytls.DefaultIssuers()
// if a specific endpoint is configured, can't use multiple default issuers // if a specific endpoint is configured, can't use multiple default issuers
@@ -405,6 +410,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf
globalACMECARoot := options["acme_ca_root"] globalACMECARoot := options["acme_ca_root"]
globalACMEDNS := options["acme_dns"] globalACMEDNS := options["acme_dns"]
globalACMEEAB := options["acme_eab"] globalACMEEAB := options["acme_eab"]
globalPreferredChains := options["preferred_chains"]
if globalEmail != nil && acmeIssuer.Email == "" { if globalEmail != nil && acmeIssuer.Email == "" {
acmeIssuer.Email = globalEmail.(string) acmeIssuer.Email = globalEmail.(string)
@@ -425,6 +431,9 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf
if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil { if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB) acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB)
} }
if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil {
acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference)
}
return nil return nil
} }
@@ -489,16 +498,23 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
}) })
emptyAPCount := 0 emptyAPCount := 0
origLenAPs := len(aps)
// compute the number of empty policies (disregarding subjects) - see #4128 // compute the number of empty policies (disregarding subjects) - see #4128
emptyAP := new(caddytls.AutomationPolicy) emptyAP := new(caddytls.AutomationPolicy)
for i := 0; i < len(aps); i++ { for i := 0; i < len(aps); i++ {
emptyAP.Subjects = aps[i].Subjects emptyAP.Subjects = aps[i].Subjects
if reflect.DeepEqual(aps[i], emptyAP) { if reflect.DeepEqual(aps[i], emptyAP) {
emptyAPCount++ emptyAPCount++
if !automationPolicyHasAllPublicNames(aps[i]) {
// if this automation policy has internal names, we might as well remove it
// so auto-https can implicitly use the internal issuer
aps = append(aps[:i], aps[i+1:]...)
i--
}
} }
} }
// If all policies are empty, we can return nil, as there is no need to set any policy // If all policies are empty, we can return nil, as there is no need to set any policy
if emptyAPCount == len(aps) { if emptyAPCount == origLenAPs {
return nil return nil
} }
@@ -510,7 +526,10 @@ outer:
// 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:]...)
break // must re-evaluate current i against next j; can't skip it!
// even if i decrements to -1, will be incremented to 0 immediately
i--
continue outer
} }
// if the policy is the same, we can keep just one, but we have // if the policy is the same, we can keep just one, but we have
@@ -593,3 +612,21 @@ func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int {
} }
return -1 return -1
} }
// subjectQualifiesForPublicCert is like certmagic.SubjectQualifiesForPublicCert() except
// that this allows domains with multiple wildcard levels like '*.*.example.com' to qualify
// if the automation policy has OnDemand enabled (i.e. this function is more lenient).
func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) bool {
return !certmagic.SubjectIsIP(subj) &&
!certmagic.SubjectIsInternal(subj) &&
(strings.Count(subj, "*.") < 2 || ap.OnDemand)
}
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
for _, subj := range ap.Subjects {
if !subjectQualifiesForPublicCert(ap, subj) {
return false
}
}
return true
}
+20
View File
@@ -103,3 +103,23 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) {
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo") tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Baz") tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Baz")
} }
func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSite(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
local_certs
}
http://:9080 {
respond "Foo"
}
bar.localhost {
respond "Bar"
}
`, "caddyfile")
tester.AssertRedirect("http://bar.localhost:9080/", "https://bar.localhost/", http.StatusPermanentRedirect)
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Foo")
}
@@ -0,0 +1,138 @@
example.com {
root * /srv
# Trigger errors for certain paths
error /private* "Unauthorized" 403
error /hidden* "Not found" 404
# Handle the error by serving an HTML page
handle_errors {
rewrite * /{http.error.status_code}.html
file_server
}
file_server
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"handle": [
{
"error": "Unauthorized",
"handler": "error",
"status_code": 403
}
],
"match": [
{
"path": [
"/private*"
]
}
]
},
{
"handle": [
{
"error": "Not found",
"handler": "error",
"status_code": 404
}
],
"match": [
{
"path": [
"/hidden*"
]
}
]
},
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
]
}
]
}
]
}
],
"terminal": true
}
],
"errors": {
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"group": "group0",
"handle": [
{
"handler": "rewrite",
"uri": "/{http.error.status_code}.html"
}
]
},
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
}
@@ -0,0 +1,32 @@
:80
file_server {
disable_canonical_uris
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"canonical_uris": false,
"handler": "file_server",
"hide": [
"./Caddyfile"
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,56 @@
{
preferred_chains smallest
}
example.com
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"example.com"
],
"issuers": [
{
"module": "acme",
"preferred_chains": {
"smallest": true
}
},
{
"module": "zerossl",
"preferred_chains": {
"smallest": true
}
}
]
}
]
}
}
}
}
@@ -0,0 +1,56 @@
{
skip_install_trust
}
a.example.com {
tls internal
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.example.com"
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"a.example.com"
],
"issuers": [
{
"module": "internal"
}
]
}
]
}
}
}
}
@@ -0,0 +1,75 @@
one.example.com {
log
}
two.example.com {
}
three.example.com {
}
example.com {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"three.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"one.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"two.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
],
"logs": {
"skip_hosts": [
"three.example.com",
"two.example.com",
"example.com"
]
}
}
}
}
}
}
@@ -0,0 +1,145 @@
:8881 {
php_fastcgi app:9000 {
env FOO bar
@error status 4xx
handle_response @error {
root * /errors
rewrite * /{http.reverse_proxy.status_code}.html
file_server
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"routes": [
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
],
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.uri.path}/"
]
},
"status_code": 308
}
]
},
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
],
"split_path": [
".php"
]
}
}
],
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
]
},
{
"match": [
{
"path": [
"*.php"
]
}
],
"handle": [
{
"handle_response": [
{
"match": {
"status_code": [
4
]
},
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/errors"
}
]
},
{
"group": "group0",
"handle": [
{
"handler": "rewrite",
"uri": "/{http.reverse_proxy.status_code}.html"
}
]
},
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
]
}
]
}
]
}
],
"handler": "reverse_proxy",
"transport": {
"env": {
"FOO": "bar"
},
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{
"dial": "app:9000"
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,46 @@
:8884
reverse_proxy one|http://localhost two|http://localhost {
to three|srv+http://localhost four|srv+http://localhost
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "localhost:80",
"id": "one"
},
{
"dial": "localhost:80",
"id": "two"
},
{
"id": "three",
"lookup_srv": "localhost"
},
{
"id": "four",
"lookup_srv": "localhost"
}
]
}
]
}
]
}
}
}
}
}
@@ -21,7 +21,7 @@ https://example.com {
versions h2c 2 versions h2c 2
compression off compression off
max_conns_per_host 5 max_conns_per_host 5
max_idle_conns_per_host 2 keepalive_idle_conns_per_host 2
} }
} }
} }
@@ -79,8 +79,10 @@ https://example.com {
"dial_fallback_delay": 5000000000, "dial_fallback_delay": 5000000000,
"dial_timeout": 3000000000, "dial_timeout": 3000000000,
"expect_continue_timeout": 9000000000, "expect_continue_timeout": 9000000000,
"keep_alive": {
"max_idle_conns_per_host": 2
},
"max_conns_per_host": 5, "max_conns_per_host": 5,
"max_idle_conns_per_host": 2,
"max_response_header_size": 30000000, "max_response_header_size": 30000000,
"protocol": "http", "protocol": "http",
"read_buffer_size": 10000000, "read_buffer_size": 10000000,
@@ -0,0 +1,57 @@
localhost
tls {
issuer acme {
preferred_chains {
any_common_name "Generic CA 1" "Generic CA 2"
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"localhost"
],
"issuers": [
{
"module": "acme",
"preferred_chains": {
"any_common_name": [
"Generic CA 1",
"Generic CA 2"
]
}
}
]
}
]
}
}
}
}
@@ -124,18 +124,6 @@ abc.de {
"tls": { "tls": {
"automation": { "automation": {
"policies": [ "policies": [
{
"issuers": [
{
"email": "my.email@example.com",
"module": "acme"
},
{
"email": "my.email@example.com",
"module": "zerossl"
}
]
},
{ {
"issuers": [ "issuers": [
{ {
@@ -0,0 +1,120 @@
# (this Caddyfile is contrived, but based on issue #4161)
example.com {
tls {
ca https://foobar
}
}
example.com:8443 {
tls {
ca https://foobar
}
}
example.com:8444 {
tls {
ca https://foobar
}
}
example.com:8445 {
tls {
ca https://foobar
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
":8443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
},
"srv2": {
"listen": [
":8444"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
},
"srv3": {
"listen": [
":8445"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"example.com"
],
"issuers": [
{
"ca": "https://foobar",
"module": "acme"
}
]
}
]
}
}
}
}
@@ -0,0 +1,68 @@
# (this Caddyfile is contrived, but based on issues #4176 and #4198)
http://example.com {
}
https://example.com {
tls internal
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"example.com"
],
"issuers": [
{
"module": "internal"
}
]
}
]
}
}
}
}
@@ -0,0 +1,66 @@
{
email foo@bar
}
localhost {
}
example.com {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"example.com"
],
"issuers": [
{
"email": "foo@bar",
"module": "acme"
},
{
"email": "foo@bar",
"module": "zerossl"
}
]
}
]
}
}
}
}
@@ -0,0 +1,70 @@
localhost
respond "hello from localhost"
tls {
issuer acme {
propagation_timeout "10m0s"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"localhost"
],
"issuers": [
{
"challenges": {
"dns": {
"propagation_timeout": 600000000000
}
},
"module": "acme"
}
]
}
]
}
}
}
}
+56 -2
View File
@@ -371,7 +371,7 @@ func TestReverseProxyHealthCheck(t *testing.T) {
reverse_proxy { reverse_proxy {
to localhost:2020 to localhost:2020
health_path /health health_uri /health
health_port 2021 health_port 2021
health_interval 2s health_interval 2s
health_timeout 5s health_timeout 5s
@@ -426,7 +426,7 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
reverse_proxy { reverse_proxy {
to unix/%s to unix/%s
health_path /health health_uri /health
health_port 2021 health_port 2021
health_interval 2s health_interval 2s
health_timeout 5s health_timeout 5s
@@ -436,3 +436,57 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!") tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
} }
func TestReverseProxyHealthCheckUnixSocketWithoutPort(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_uri /health
health_interval 2s
health_timeout 5s
}
}
`, socketName), "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
}
+3 -195
View File
@@ -19,16 +19,15 @@ import (
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
"net/url"
"os" "os"
"os/exec" "os/exec"
"reflect"
"runtime" "runtime"
"runtime/debug" "runtime/debug"
"sort" "sort"
@@ -121,7 +120,7 @@ func cmdStart(fl Flags) (int, error) {
for { for {
conn, err := ln.Accept() conn, err := ln.Accept()
if err != nil { if err != nil {
if !strings.Contains(err.Error(), "use of closed network connection") { if !errors.Is(err, net.ErrClosed) {
log.Println(err) log.Println(err)
} }
break break
@@ -332,7 +331,7 @@ func cmdReload(fl Flags) (int, error) {
} }
func cmdVersion(_ Flags) (int, error) { func cmdVersion(_ Flags) (int, error) {
fmt.Println(caddyVersion()) fmt.Println(CaddyVersion())
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
@@ -570,147 +569,6 @@ func cmdFmt(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
func cmdUpgrade(_ Flags) (int, error) {
l := caddy.Log()
thisExecPath, err := os.Executable()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("determining current executable path: %v", err)
}
l.Info("this executable will be replaced", zap.String("path", thisExecPath))
// get the list of nonstandard plugins
_, nonstandard, _, err := getModules()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
}
pluginPkgs := make(map[string]struct{})
for _, mod := range nonstandard {
if mod.goModule.Replace != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s",
mod.goModule.Path, mod.goModule.Replace.Path)
}
l.Info("found non-standard module",
zap.String("id", mod.caddyModuleID),
zap.String("package", mod.goModule.Path))
pluginPkgs[mod.goModule.Path] = struct{}{}
}
// build the request URL to download this custom build
qs := url.Values{
"os": {runtime.GOOS},
"arch": {runtime.GOARCH},
}
for pkg := range pluginPkgs {
qs.Add("p", pkg)
}
urlStr := fmt.Sprintf("https://caddyserver.com/api/download?%s", qs.Encode())
// initiate the build
l.Info("requesting build",
zap.String("os", qs.Get("os")),
zap.String("arch", qs.Get("arch")),
zap.Strings("packages", qs["p"]))
resp, err := http.Get(urlStr)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("secure request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
var details struct {
StatusCode int `json:"status_code"`
Error struct {
Message string `json:"message"`
ID string `json:"id"`
} `json:"error"`
}
err2 := json.NewDecoder(resp.Body).Decode(&details)
if err2 != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download and error decoding failed: HTTP %d: %v", resp.StatusCode, err2)
}
return caddy.ExitCodeFailedStartup, fmt.Errorf("download failed: HTTP %d: %s (id=%s)", resp.StatusCode, details.Error.Message, details.Error.ID)
}
// back up the current binary, in case something goes wrong we can replace it
backupExecPath := thisExecPath + ".tmp"
l.Info("build acquired; backing up current executable",
zap.String("current_path", thisExecPath),
zap.String("backup_path", backupExecPath))
err = os.Rename(thisExecPath, backupExecPath)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("backing up current binary: %v", err)
}
defer func() {
if err != nil {
err2 := os.Rename(backupExecPath, thisExecPath)
if err2 != nil {
l.Error("restoring original executable failed; will need to be restored manually",
zap.String("backup_path", backupExecPath),
zap.String("original_path", thisExecPath),
zap.Error(err2))
}
}
}()
// download the file; do this in a closure to close reliably before we execute it
writeFile := func() error {
destFile, err := os.OpenFile(thisExecPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0770)
if err != nil {
return fmt.Errorf("unable to open destination file: %v", err)
}
defer destFile.Close()
l.Info("downloading binary", zap.String("source", urlStr), zap.String("destination", thisExecPath))
_, err = io.Copy(destFile, resp.Body)
if err != nil {
return fmt.Errorf("unable to download file: %v", err)
}
err = destFile.Sync()
if err != nil {
return fmt.Errorf("syncing downloaded file to device: %v", err)
}
return nil
}
err = writeFile()
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
l.Info("download successful; displaying new binary details", zap.String("location", thisExecPath))
// use the new binary to print out version and module info
fmt.Print("\nModule versions:\n\n")
cmd := exec.Command(thisExecPath, "list-modules", "--versions")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
}
fmt.Println("\nVersion:")
cmd = exec.Command(thisExecPath, "version")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
}
fmt.Println()
// clean up the backup file
err = os.Remove(backupExecPath)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to clean up backup binary: %v", err)
}
l.Info("upgrade successful; please restart any running Caddy instances", zap.String("executable", thisExecPath))
return caddy.ExitCodeSuccess, nil
}
func cmdHelp(fl Flags) (int, error) { func cmdHelp(fl Flags) (int, error) {
const fullDocs = `Full documentation is available at: const fullDocs = `Full documentation is available at:
https://caddyserver.com/docs/command-line` https://caddyserver.com/docs/command-line`
@@ -775,56 +633,6 @@ commands:
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
bi, ok := debug.ReadBuildInfo()
if !ok {
err = fmt.Errorf("no build info")
return
}
for _, modID := range caddy.Modules() {
modInfo, err := caddy.GetModule(modID)
if err != nil {
// that's weird, shouldn't happen
unknown = append(unknown, moduleInfo{caddyModuleID: modID, err: err})
continue
}
// to get the Caddy plugin's version info, we need to know
// the package that the Caddy module's value comes from; we
// can use reflection but we need a non-pointer value (I'm
// not sure why), and since New() should return a pointer
// value, we need to dereference it first
iface := interface{}(modInfo.New())
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
}
modPkgPath := reflect.TypeOf(iface).PkgPath()
// now we find the Go module that the Caddy module's package
// belongs to; we assume the Caddy module package path will
// be prefixed by its Go module path, and we will choose the
// longest matching prefix in case there are nested modules
var matched *debug.Module
for _, dep := range bi.Deps {
if strings.HasPrefix(modPkgPath, dep.Path) {
if matched == nil || len(dep.Path) > len(matched.Path) {
matched = dep
}
}
}
caddyModGoMod := moduleInfo{caddyModuleID: modID, goModule: matched}
if strings.HasPrefix(modPkgPath, caddy.ImportPath) {
standard = append(standard, caddyModGoMod)
} else {
nonstandard = append(nonstandard, caddyModGoMod)
}
}
return
}
// apiRequest makes an API request to the endpoint adminAddr with the // apiRequest makes an API request to the endpoint adminAddr with the
// given HTTP method and request URI. If body is non-nil, it will be // given HTTP method and request URI. If body is non-nil, it will be
// assumed to be Content-Type application/json. // assumed to be Content-Type application/json.
+30
View File
@@ -61,6 +61,12 @@ type Command struct {
// any error that occurred. // any error that occurred.
type CommandFunc func(Flags) (int, error) type CommandFunc func(Flags) (int, error)
// Commands returns a list of commands initialised by
// RegisterCommand
func Commands() map[string]Command {
return commands
}
var commands = make(map[string]Command) var commands = make(map[string]Command)
func init() { func init() {
@@ -291,6 +297,30 @@ Downloads an updated Caddy binary with the same modules/plugins at the
latest versions. EXPERIMENTAL: May be changed or removed.`, latest versions. EXPERIMENTAL: May be changed or removed.`,
}) })
RegisterCommand(Command{
Name: "add-package",
Func: cmdAddPackage,
Usage: "<packages...>",
Short: "Adds Caddy packages (EXPERIMENTAL)",
Long: `
Downloads an updated Caddy binary with the specified packages (module/plugin)
added. Retains existing packages. Returns an error if the any of packages are
already included. EXPERIMENTAL: May be changed or removed.
`,
})
RegisterCommand(Command{
Name: "remove-package",
Func: cmdRemovePackage,
Usage: "<packages...>",
Short: "Removes Caddy packages (EXPERIMENTAL)",
Long: `
Downloads an updated Caddy binaries without the specified packages (module/plugin).
Returns an error if any of the packages are not included.
EXPERIMENTAL: May be changed or removed.
`,
})
} }
// RegisterCommand registers the command cmd. // RegisterCommand registers the command cmd.
+8 -3
View File
@@ -361,6 +361,11 @@ func loadEnvFromFile(envFile string) error {
} }
} }
// Update the storage paths to ensure they have the proper
// value after loading a specified env file.
caddy.ConfigAutosavePath = filepath.Join(caddy.AppConfigDir(), "autosave.json")
caddy.DefaultStorage = &certmagic.FileStorage{Path: caddy.AppDataDir()}
return nil return nil
} }
@@ -415,7 +420,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", caddyVersion()) fmt.Printf("caddy.Version=%s\n", CaddyVersion())
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)
@@ -432,8 +437,8 @@ func printEnvironment() {
} }
} }
// caddyVersion returns a detailed version string, if available. // CaddyVersion returns a detailed version string, if available.
func caddyVersion() string { func CaddyVersion() string {
goModule := caddy.GoModule() goModule := caddy.GoModule()
ver := goModule.Version ver := goModule.Version
if goModule.Sum != "" { if goModule.Sum != "" {
+306
View File
@@ -0,0 +1,306 @@
// 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 (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"reflect"
"runtime"
"runtime/debug"
"strings"
"github.com/caddyserver/caddy/v2"
"go.uber.org/zap"
)
func cmdUpgrade(_ Flags) (int, error) {
_, nonstandard, _, err := getModules()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
}
pluginPkgs, err := getPluginPackages(nonstandard)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
return upgradeBuild(pluginPkgs)
}
func cmdAddPackage(fl Flags) (int, error) {
if len(fl.Args()) == 0 {
return caddy.ExitCodeFailedStartup, fmt.Errorf("at least one package name must be specified")
}
_, nonstandard, _, err := getModules()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
}
pluginPkgs, err := getPluginPackages(nonstandard)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
for _, arg := range fl.Args() {
if _, ok := pluginPkgs[arg]; ok {
return caddy.ExitCodeFailedStartup, fmt.Errorf("package is already added")
}
pluginPkgs[arg] = struct{}{}
}
return upgradeBuild(pluginPkgs)
}
func cmdRemovePackage(fl Flags) (int, error) {
if len(fl.Args()) == 0 {
return caddy.ExitCodeFailedStartup, fmt.Errorf("at least one package name must be specified")
}
_, nonstandard, _, err := getModules()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
}
pluginPkgs, err := getPluginPackages(nonstandard)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
for _, arg := range fl.Args() {
if _, ok := pluginPkgs[arg]; !ok {
// package does not exist
return caddy.ExitCodeFailedStartup, fmt.Errorf("package is not added")
}
delete(pluginPkgs, arg)
}
return upgradeBuild(pluginPkgs)
}
func upgradeBuild(pluginPkgs map[string]struct{}) (int, error) {
l := caddy.Log()
thisExecPath, err := os.Executable()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("determining current executable path: %v", err)
}
thisExecStat, err := os.Stat(thisExecPath)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err)
}
l.Info("this executable will be replaced", zap.String("path", thisExecPath))
// build the request URL to download this custom build
qs := url.Values{
"os": {runtime.GOOS},
"arch": {runtime.GOARCH},
}
for pkg := range pluginPkgs {
qs.Add("p", pkg)
}
// initiate the build
resp, err := downloadBuild(qs)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download failed: %v", err)
}
defer resp.Body.Close()
// back up the current binary, in case something goes wrong we can replace it
backupExecPath := thisExecPath + ".tmp"
l.Info("build acquired; backing up current executable",
zap.String("current_path", thisExecPath),
zap.String("backup_path", backupExecPath))
err = os.Rename(thisExecPath, backupExecPath)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("backing up current binary: %v", err)
}
defer func() {
if err != nil {
err2 := os.Rename(backupExecPath, thisExecPath)
if err2 != nil {
l.Error("restoring original executable failed; will need to be restored manually",
zap.String("backup_path", backupExecPath),
zap.String("original_path", thisExecPath),
zap.Error(err2))
}
}
}()
// download the file; do this in a closure to close reliably before we execute it
err = writeCaddyBinary(thisExecPath, &resp.Body, thisExecStat)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
l.Info("download successful; displaying new binary details", zap.String("location", thisExecPath))
// use the new binary to print out version and module info
fmt.Print("\nModule versions:\n\n")
if err = listModules(thisExecPath); err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
}
fmt.Println("\nVersion:")
if err = showVersion(thisExecPath); err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
}
fmt.Println()
// clean up the backup file
if err = os.Remove(backupExecPath); err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to clean up backup binary: %v", err)
}
l.Info("upgrade successful; please restart any running Caddy instances", zap.String("executable", thisExecPath))
return caddy.ExitCodeSuccess, nil
}
func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
bi, ok := debug.ReadBuildInfo()
if !ok {
err = fmt.Errorf("no build info")
return
}
for _, modID := range caddy.Modules() {
modInfo, err := caddy.GetModule(modID)
if err != nil {
// that's weird, shouldn't happen
unknown = append(unknown, moduleInfo{caddyModuleID: modID, err: err})
continue
}
// to get the Caddy plugin's version info, we need to know
// the package that the Caddy module's value comes from; we
// can use reflection but we need a non-pointer value (I'm
// not sure why), and since New() should return a pointer
// value, we need to dereference it first
iface := interface{}(modInfo.New())
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
}
modPkgPath := reflect.TypeOf(iface).PkgPath()
// now we find the Go module that the Caddy module's package
// belongs to; we assume the Caddy module package path will
// be prefixed by its Go module path, and we will choose the
// longest matching prefix in case there are nested modules
var matched *debug.Module
for _, dep := range bi.Deps {
if strings.HasPrefix(modPkgPath, dep.Path) {
if matched == nil || len(dep.Path) > len(matched.Path) {
matched = dep
}
}
}
caddyModGoMod := moduleInfo{caddyModuleID: modID, goModule: matched}
if strings.HasPrefix(modPkgPath, caddy.ImportPath) {
standard = append(standard, caddyModGoMod)
} else {
nonstandard = append(nonstandard, caddyModGoMod)
}
}
return
}
func listModules(path string) error {
cmd := exec.Command(path, "list-modules", "--versions")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return fmt.Errorf("download succeeded, but unable to execute: %v", err)
}
return nil
}
func showVersion(path string) error {
cmd := exec.Command(path, "version")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return fmt.Errorf("download succeeded, but unable to execute: %v", err)
}
return nil
}
func downloadBuild(qs url.Values) (*http.Response, error) {
l := caddy.Log()
l.Info("requesting build",
zap.String("os", qs.Get("os")),
zap.String("arch", qs.Get("arch")),
zap.Strings("packages", qs["p"]))
resp, err := http.Get(fmt.Sprintf("%s?%s", downloadPath, qs.Encode()))
if err != nil {
return nil, fmt.Errorf("secure request failed: %v", err)
}
if resp.StatusCode >= 400 {
var details struct {
StatusCode int `json:"status_code"`
Error struct {
Message string `json:"message"`
ID string `json:"id"`
} `json:"error"`
}
err2 := json.NewDecoder(resp.Body).Decode(&details)
if err2 != nil {
return nil, fmt.Errorf("download and error decoding failed: HTTP %d: %v", resp.StatusCode, err2)
}
return nil, fmt.Errorf("download failed: HTTP %d: %s (id=%s)", resp.StatusCode, details.Error.Message, details.Error.ID)
}
return resp, nil
}
func getPluginPackages(modules []moduleInfo) (map[string]struct{}, error) {
pluginPkgs := make(map[string]struct{})
for _, mod := range modules {
if mod.goModule.Replace != nil {
return nil, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s",
mod.goModule.Path, mod.goModule.Replace.Path)
}
pluginPkgs[mod.goModule.Path] = struct{}{}
}
return pluginPkgs, nil
}
func writeCaddyBinary(path string, body *io.ReadCloser, fileInfo os.FileInfo) error {
l := caddy.Log()
destFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileInfo.Mode())
if err != nil {
return fmt.Errorf("unable to open destination file: %v", err)
}
defer destFile.Close()
l.Info("downloading binary", zap.String("destination", path))
_, err = io.Copy(destFile, *body)
if err != nil {
return fmt.Errorf("unable to download file: %v", err)
}
err = destFile.Sync()
if err != nil {
return fmt.Errorf("syncing downloaded file to device: %v", err)
}
return nil
}
const downloadPath = "https://caddyserver.com/api/download"
+24 -24
View File
@@ -1,35 +1,35 @@
module github.com/caddyserver/caddy/v2 module github.com/caddyserver/caddy/v2
go 1.15 go 1.16
require ( require (
github.com/Masterminds/sprig/v3 v3.1.0 github.com/Masterminds/sprig/v3 v3.2.2
github.com/alecthomas/chroma v0.8.2 github.com/alecthomas/chroma v0.9.2
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
github.com/caddyserver/certmagic v0.13.1 github.com/caddyserver/certmagic v0.14.5
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/chi v4.1.2+incompatible
github.com/google/cel-go v0.6.0 github.com/google/cel-go v0.7.3
github.com/google/uuid v1.2.0 github.com/google/uuid v1.3.0
github.com/klauspost/compress v1.11.3 github.com/klauspost/compress v1.13.4
github.com/klauspost/cpuid/v2 v2.0.6 github.com/klauspost/cpuid/v2 v2.0.9
github.com/lucas-clemente/quic-go v0.20.1 github.com/lucas-clemente/quic-go v0.23.0
github.com/mholt/acmez v0.1.3 github.com/mholt/acmez v1.0.0
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/prometheus/client_golang v1.9.0 github.com/prometheus/client_golang v1.11.0
github.com/smallstep/certificates v0.15.4 github.com/smallstep/certificates v0.16.4
github.com/smallstep/cli v0.15.2 github.com/smallstep/cli v0.16.1
github.com/smallstep/nosql v0.3.0 // cannot upgrade from v0.3.0 until protobuf warning is fixed github.com/smallstep/nosql v0.3.8
github.com/smallstep/truststore v0.9.6 github.com/smallstep/truststore v0.9.6
github.com/yuin/goldmark v1.2.1 github.com/yuin/goldmark v1.4.0
github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691 github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01
go.uber.org/zap v1.16.0 go.uber.org/zap v1.19.0
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b golang.org/x/net v0.0.0-20210614182718-04defd469f4e
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 golang.org/x/term v0.0.0-20210503060354-a79de5458b56
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08
google.golang.org/protobuf v1.24.0 // cannot upgrade until warning is fixed google.golang.org/protobuf v1.27.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.3.0 gopkg.in/yaml.v2 v2.4.0
) )
+755 -304
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -6,12 +6,13 @@ import (
"strings" "strings"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
) )
// define and register the metrics used in this package. // define and register the metrics used in this package.
func init() { func init() {
prometheus.MustRegister(prometheus.NewBuildInfoCollector()) prometheus.MustRegister(collectors.NewBuildInfoCollector())
const ns, sub = "caddy", "admin" const ns, sub = "caddy", "admin"
+29
View File
@@ -20,7 +20,9 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"path/filepath"
"strconv" "strconv"
"strings"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -217,6 +219,31 @@ func StatusCodeMatches(actual, configured int) bool {
return false return false
} }
// SanitizedPathJoin performs filepath.Join(root, reqPath) that
// is safe against directory traversal attacks. It uses logic
// similar to that in the Go standard library, specifically
// in the implementation of http.Dir. The root is assumed to
// be a trusted path, but reqPath is not; and the output will
// never be outside of root. The resulting path can be used
// with the local file system.
func SanitizedPathJoin(root, reqPath string) string {
if root == "" {
root = "."
}
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
}
// tlsPlaceholderWrapper is a no-op listener wrapper that marks // tlsPlaceholderWrapper is a no-op listener wrapper that marks
// where the TLS listener should be in a chain of listener wrappers. // where the TLS listener should be in a chain of listener wrappers.
// It should only be used if another listener wrapper must be placed // It should only be used if another listener wrapper must be placed
@@ -242,6 +269,8 @@ const (
DefaultHTTPSPort = 443 DefaultHTTPSPort = 443
) )
const separator = string(filepath.Separator)
// Interface guard // Interface guard
var _ caddy.ListenerWrapper = (*tlsPlaceholderWrapper)(nil) var _ caddy.ListenerWrapper = (*tlsPlaceholderWrapper)(nil)
var _ caddyfile.Unmarshaler = (*tlsPlaceholderWrapper)(nil) var _ caddyfile.Unmarshaler = (*tlsPlaceholderWrapper)(nil)
+94
View File
@@ -0,0 +1,94 @@
package caddyhttp
import (
"net/url"
"path/filepath"
"testing"
)
func TestSanitizedPathJoin(t *testing.T) {
// For reference:
// %2e = .
// %2f = /
// %5c = \
for i, tc := range []struct {
inputRoot string
inputPath string
expect string
}{
{
inputPath: "",
expect: ".",
},
{
inputPath: "/",
expect: ".",
},
{
inputPath: "/foo",
expect: "foo",
},
{
inputPath: "/foo/",
expect: "foo" + separator,
},
{
inputPath: "/foo/bar",
expect: filepath.Join("foo", "bar"),
},
{
inputRoot: "/a",
inputPath: "/foo/bar",
expect: filepath.Join("/", "a", "foo", "bar"),
},
{
inputPath: "/foo/../bar",
expect: "bar",
},
{
inputRoot: "/a/b",
inputPath: "/foo/../bar",
expect: filepath.Join("/", "a", "b", "bar"),
},
{
inputRoot: "/a/b",
inputPath: "/..%2fbar",
expect: filepath.Join("/", "a", "b", "bar"),
},
{
inputRoot: "/a/b",
inputPath: "/%2e%2e%2fbar",
expect: filepath.Join("/", "a", "b", "bar"),
},
{
inputRoot: "/a/b",
inputPath: "/%2e%2e%2f%2e%2e%2f",
expect: filepath.Join("/", "a", "b") + separator,
},
{
inputRoot: "C:\\www",
inputPath: "/foo/bar",
expect: filepath.Join("C:\\www", "foo", "bar"),
},
{
inputRoot: "C:\\www",
inputPath: "/D:\\foo\\bar",
expect: filepath.Join("C:\\www", "D:\\foo\\bar"),
},
} {
// we don't *need* to use an actual parsed URL, but it
// adds some authenticity to the tests since real-world
// values will be coming in from URLs; thus, the test
// corpus can contain paths as encoded by clients, which
// more closely emulates the actual attack vector
u, err := url.Parse("http://test:9999" + tc.inputPath)
if err != nil {
t.Fatalf("Test %d: invalid URL: %v", i, err)
}
actual := SanitizedPathJoin(tc.inputRoot, u.Path)
if actual != tc.expect {
t.Errorf("Test %d: SanitizedPathJoin('%s', '%s') => %s (expected '%s')",
i, tc.inputRoot, tc.inputPath, actual, tc.expect)
}
}
}
+1 -3
View File
@@ -35,7 +35,6 @@ import (
"github.com/google/cel-go/interpreter/functions" "github.com/google/cel-go/interpreter/functions"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
timestamp "google.golang.org/protobuf/types/known/timestamppb"
) )
func init() { func init() {
@@ -231,8 +230,7 @@ func (celTypeAdapter) NativeToValue(value interface{}) ref.Val {
case pkix.Name: case pkix.Name:
return celPkixName{&v} return celPkixName{&v}
case time.Time: case time.Time:
// TODO: eliminate direct protobuf dependency, sigh -- just wrap stdlib time.Time instead... return types.Timestamp{Time: v}
return types.Timestamp{Timestamp: &timestamp.Timestamp{Seconds: v.Unix(), Nanos: int32(v.Nanosecond())}}
case error: case error:
types.NewErr(v.Error()) types.NewErr(v.Error())
} }
+16
View File
@@ -182,6 +182,7 @@ type responseWriter struct {
buf *bytes.Buffer buf *bytes.Buffer
config *Encode config *Encode
statusCode int statusCode int
wroteHeader bool
} }
// WriteHeader stores the status to write when the time comes // WriteHeader stores the status to write when the time comes
@@ -195,6 +196,19 @@ func (enc *Encode) Match(rw *responseWriter) bool {
return enc.Matcher.Match(rw.statusCode, rw.Header()) return enc.Matcher.Match(rw.statusCode, rw.Header())
} }
// Flush implements http.Flusher. It delays the actual Flush of the underlying ResponseWriterWrapper
// until headers were written.
func (rw *responseWriter) Flush() {
if !rw.wroteHeader {
// flushing the underlying ResponseWriter will write header and status code,
// but we need to delay that until we can determine if we must encode and
// therefore add the Content-Encoding header; this happens in the first call
// to rw.Write (see bug in #4314)
return
}
rw.ResponseWriterWrapper.Flush()
}
// Write writes to the response. If the response qualifies, // Write writes to the response. If the response qualifies,
// it is encoded using the encoder, which is initialized // it is encoded using the encoder, which is initialized
// if not done so already. // if not done so already.
@@ -225,6 +239,7 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
if rw.statusCode > 0 { if rw.statusCode > 0 {
rw.ResponseWriter.WriteHeader(rw.statusCode) rw.ResponseWriter.WriteHeader(rw.statusCode)
rw.statusCode = 0 rw.statusCode = 0
rw.wroteHeader = true
} }
switch { switch {
@@ -271,6 +286,7 @@ func (rw *responseWriter) Close() error {
// that rely on If-None-Match, for example // that rely on If-None-Match, for example
rw.ResponseWriter.WriteHeader(rw.statusCode) rw.ResponseWriter.WriteHeader(rw.statusCode)
rw.statusCode = 0 rw.statusCode = 0
rw.wroteHeader = true
} }
if rw.w != nil { if rw.w != nil {
err2 := rw.w.Close() err2 := rw.w.Close()
+4 -5
View File
@@ -15,7 +15,6 @@
package caddygzip package caddygzip
import ( import (
"compress/flate"
"fmt" "fmt"
"strconv" "strconv"
@@ -68,11 +67,11 @@ func (g *Gzip) Provision(ctx caddy.Context) error {
// Validate validates g's configuration. // Validate validates g's configuration.
func (g Gzip) Validate() error { func (g Gzip) Validate() error {
if g.Level < flate.NoCompression { if g.Level < gzip.StatelessCompression {
return fmt.Errorf("quality too low; must be >= %d", flate.NoCompression) return fmt.Errorf("quality too low; must be >= %d", gzip.StatelessCompression)
} }
if g.Level > flate.BestCompression { if g.Level > gzip.BestCompression {
return fmt.Errorf("quality too high; must be <= %d", flate.BestCompression) return fmt.Errorf("quality too high; must be <= %d", gzip.BestCompression)
} }
return nil return nil
} }
+4 -1
View File
@@ -47,7 +47,10 @@ func (Zstd) AcceptEncoding() string { return "zstd" }
// NewEncoder returns a new gzip writer. // NewEncoder returns a new gzip writer.
func (z Zstd) NewEncoder() encode.Encoder { func (z Zstd) NewEncoder() encode.Encoder {
writer, _ := zstd.NewWriter(nil) // The default of 8MB for the window is
// too large for many clients, so we limit
// it to 128K to lighten their load.
writer, _ := zstd.NewWriter(nil, zstd.WithWindowSize(128<<10), zstd.WithEncoderConcurrency(1), zstd.WithZeroFrames(true))
return writer return writer
} }
+42 -37
View File
@@ -22,6 +22,7 @@ import (
"os" "os"
"path" "path"
"strings" "strings"
"sync"
"text/template" "text/template"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
@@ -34,8 +35,6 @@ import (
type Browse struct { type Browse struct {
// Use this template file instead of the default browse template. // Use this template file instead of the default browse template.
TemplateFile string `json:"template_file,omitempty"` TemplateFile string `json:"template_file,omitempty"`
template *template.Template
} }
func (fsrv *FileServer) serveBrowse(root, 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 {
@@ -43,15 +42,28 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
zap.String("path", dirPath), zap.String("path", dirPath),
zap.String("root", root)) 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 to
// "/b/c" on a path like "/a" end up going to "/b/c" instead // "b/c" at path "/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, "/") { // so that the path is "/a/" and the client constructs
fsrv.logger.Debug("redirecting to trailing slash to preserve hrefs", zap.String("request_path", r.URL.Path)) // relative hrefs "b/c" to be "/a/b/c".
r.URL.Path += "/" //
http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently) // Only redirect if the last element of the path (the filename) was not
return nil // rewritten; if the admin wanted to rewrite to the canonical path, they
// would have, and we have to be very careful not to introduce unwanted
// redirects and especially redirect loops! (Redirecting using the
// original URI is necessary because that's the URI the browser knows,
// we don't want to redirect from internally-rewritten URIs.)
// See https://github.com/caddyserver/caddy/issues/4205.
origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
if path.Base(origReq.URL.Path) == path.Base(r.URL.Path) {
if !strings.HasSuffix(origReq.URL.Path, "/") {
fsrv.logger.Debug("redirecting to trailing slash to preserve hrefs", zap.String("request_path", r.URL.Path))
origReq.URL.Path += "/"
http.Redirect(w, r, origReq.URL.String(), http.StatusMovedPermanently)
return nil
}
} }
dir, err := fsrv.openFile(dirPath, w) dir, err := fsrv.openFile(dirPath, w)
@@ -75,11 +87,14 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
fsrv.browseApplyQueryParams(w, r, &listing) fsrv.browseApplyQueryParams(w, r, &listing)
// write response as either JSON or HTML buf := bufPool.Get().(*bytes.Buffer)
var buf *bytes.Buffer defer bufPool.Put(buf)
acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ",")) acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ","))
// write response as either JSON or HTML
if strings.Contains(acceptHeader, "application/json") { if strings.Contains(acceptHeader, "application/json") {
if buf, err = fsrv.browseWriteJSON(listing); err != nil { if err := json.NewEncoder(buf).Encode(listing.Items); err != nil {
return caddyhttp.Error(http.StatusInternalServerError, err) return caddyhttp.Error(http.StatusInternalServerError, err)
} }
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
@@ -98,12 +113,11 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
browseTemplateContext: listing, browseTemplateContext: listing,
} }
err = fsrv.makeBrowseTemplate(tplCtx) tpl, err := fsrv.makeBrowseTemplate(tplCtx)
if err != nil { if err != nil {
return fmt.Errorf("parsing browse template: %v", err) return fmt.Errorf("parsing browse template: %v", err)
} }
if err := tpl.Execute(buf, tplCtx); err != nil {
if buf, err = fsrv.browseWriteHTML(tplCtx); err != nil {
return caddyhttp.Error(http.StatusInternalServerError, err) return caddyhttp.Error(http.StatusInternalServerError, err)
} }
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -161,7 +175,7 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
} }
// makeBrowseTemplate creates the template to be used for directory listings. // makeBrowseTemplate creates the template to be used for directory listings.
func (fsrv *FileServer) makeBrowseTemplate(tplCtx *templateContext) error { func (fsrv *FileServer) makeBrowseTemplate(tplCtx *templateContext) (*template.Template, error) {
var tpl *template.Template var tpl *template.Template
var err error var err error
@@ -169,33 +183,17 @@ func (fsrv *FileServer) makeBrowseTemplate(tplCtx *templateContext) error {
tpl = tplCtx.NewTemplate(path.Base(fsrv.Browse.TemplateFile)) tpl = tplCtx.NewTemplate(path.Base(fsrv.Browse.TemplateFile))
tpl, err = tpl.ParseFiles(fsrv.Browse.TemplateFile) tpl, err = tpl.ParseFiles(fsrv.Browse.TemplateFile)
if err != nil { if err != nil {
return fmt.Errorf("parsing browse template file: %v", err) return nil, fmt.Errorf("parsing browse template file: %v", err)
} }
} else { } else {
tpl = tplCtx.NewTemplate("default_listing") tpl = tplCtx.NewTemplate("default_listing")
tpl, err = tpl.Parse(defaultBrowseTemplate) tpl, err = tpl.Parse(defaultBrowseTemplate)
if err != nil { if err != nil {
return fmt.Errorf("parsing default browse template: %v", err) return nil, fmt.Errorf("parsing default browse template: %v", err)
} }
} }
fsrv.Browse.template = tpl return tpl, nil
return nil
}
func (fsrv *FileServer) browseWriteJSON(listing browseTemplateContext) (*bytes.Buffer, error) {
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)
err := json.NewEncoder(buf).Encode(listing.Items)
return buf, err
}
func (fsrv *FileServer) browseWriteHTML(tplCtx *templateContext) (*bytes.Buffer, error) {
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)
err := fsrv.Browse.template.Execute(buf, tplCtx)
return buf, err
} }
// isSymlink return true if f is a symbolic link // isSymlink return true if f is a symbolic link
@@ -209,7 +207,7 @@ func isSymlinkTargetDir(f os.FileInfo, root, urlPath string) bool {
if !isSymlink(f) { if !isSymlink(f) {
return false return false
} }
target := sanitizedPathJoin(root, path.Join(urlPath, f.Name())) target := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, f.Name()))
targetInfo, err := os.Stat(target) targetInfo, err := os.Stat(target)
if err != nil { if err != nil {
return false return false
@@ -224,3 +222,10 @@ type templateContext struct {
templates.TemplateContext templates.TemplateContext
browseTemplateContext browseTemplateContext
} }
// bufPool is used to increase the efficiency of file listings.
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
@@ -1,68 +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 fileserver
import (
"testing"
"text/template"
)
func BenchmarkBrowseWriteJSON(b *testing.B) {
fsrv := new(FileServer)
listing := browseTemplateContext{
Name: "test",
Path: "test",
CanGoUp: false,
Items: make([]fileInfo, 100),
NumDirs: 42,
NumFiles: 420,
Sort: "",
Order: "",
Limit: 42,
}
b.ResetTimer()
for n := 0; n < b.N; n++ {
fsrv.browseWriteJSON(listing)
}
}
func BenchmarkBrowseWriteHTML(b *testing.B) {
fsrv := new(FileServer)
fsrv.Browse = &Browse{
TemplateFile: "",
template: template.New("test"),
}
listing := browseTemplateContext{
Name: "test",
Path: "test",
CanGoUp: false,
Items: make([]fileInfo, 100),
NumDirs: 42,
NumFiles: 420,
Sort: "",
Order: "",
Limit: 42,
}
tplCtx := &templateContext{
browseTemplateContext: listing,
}
fsrv.makeBrowseTemplate(tplCtx)
b.ResetTimer()
for n := 0; n < b.N; n++ {
fsrv.browseWriteHTML(tplCtx)
}
}
@@ -264,7 +264,7 @@ func (l byTime) Less(i, j int) bool { return l.Items[i].ModTime.Before(l.Items[j
const ( const (
sortByName = "name" sortByName = "name"
sortByNameDirFirst = "name_dir_first" sortByNameDirFirst = "namedirfirst"
sortBySize = "size" sortBySize = "size"
sortByTime = "time" sortByTime = "time"
) )
@@ -41,6 +41,7 @@ func init() {
// browse [<template_file>] // browse [<template_file>]
// precompressed <formats...> // precompressed <formats...>
// status <status> // status <status>
// disable_canonical_uris
// } // }
// //
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
@@ -112,6 +113,13 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
} }
fsrv.StatusCode = caddyhttp.WeakString(h.Val()) fsrv.StatusCode = caddyhttp.WeakString(h.Val())
case "disable_canonical_uris":
if h.NextArg() {
return nil, h.ArgErr()
}
falseBool := false
fsrv.CanonicalURIs = &falseBool
default: default:
return nil, h.Errf("unknown subdirective '%s'", h.Val()) return nil, h.Errf("unknown subdirective '%s'", h.Val())
} }
+1 -1
View File
@@ -185,7 +185,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
if strings.HasSuffix(file, "/") { if strings.HasSuffix(file, "/") {
suffix += "/" suffix += "/"
} }
fullpath = sanitizedPathJoin(root, suffix) fullpath = caddyhttp.SanitizedPathJoin(root, suffix)
return return
} }
+2 -2
View File
@@ -94,7 +94,7 @@ func TestFileMatcher(t *testing.T) {
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") fileType, _ := repl.Get("http.matchers.file.type")
if fileType != tc.expectedType { if fileType != tc.expectedType {
t.Fatalf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType) t.Fatalf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType)
} }
@@ -197,7 +197,7 @@ func TestPHPFileMatcher(t *testing.T) {
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") fileType, _ := repl.Get("http.matchers.file.type")
if fileType != tc.expectedType { if fileType != tc.expectedType {
t.Fatalf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType) t.Fatalf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType)
} }
+27 -53
View File
@@ -15,16 +15,15 @@
package fileserver package fileserver
import ( import (
"bytes"
"fmt" "fmt"
weakrand "math/rand" weakrand "math/rand"
"mime" "mime"
"net/http" "net/http"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
@@ -71,6 +70,10 @@ type FileServer struct {
// Use redirects to enforce trailing slashes for directories, or to // Use redirects to enforce trailing slashes for directories, or to
// remove trailing slash from URIs for files. Default is true. // remove trailing slash from URIs for files. Default is true.
//
// Canonicalization will not happen if the last element of the request's
// path (the filename) is changed in an internal rewrite, to avoid
// clobbering the explicit rewrite with implicit behavior.
CanonicalURIs *bool `json:"canonical_uris,omitempty"` CanonicalURIs *bool `json:"canonical_uris,omitempty"`
// Override the status code written when successfully serving a file. // Override the status code written when successfully serving a file.
@@ -162,7 +165,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
filesToHide := fsrv.transformHidePaths(repl) filesToHide := fsrv.transformHidePaths(repl)
root := repl.ReplaceAll(fsrv.Root, ".") root := repl.ReplaceAll(fsrv.Root, ".")
filename := sanitizedPathJoin(root, r.URL.Path) filename := caddyhttp.SanitizedPathJoin(root, r.URL.Path)
fsrv.logger.Debug("sanitized path join", fsrv.logger.Debug("sanitized path join",
zap.String("site_root", root), zap.String("site_root", root),
@@ -178,7 +181,6 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
} else if os.IsPermission(err) { } else if os.IsPermission(err) {
return caddyhttp.Error(http.StatusForbidden, err) return caddyhttp.Error(http.StatusForbidden, err)
} }
// TODO: treat this as resource exhaustion like with os.Open? Or unnecessary here?
return caddyhttp.Error(http.StatusInternalServerError, err) return caddyhttp.Error(http.StatusInternalServerError, err)
} }
@@ -187,7 +189,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
var implicitIndexFile bool var implicitIndexFile bool
if info.IsDir() && len(fsrv.IndexNames) > 0 { if info.IsDir() && len(fsrv.IndexNames) > 0 {
for _, indexPage := range fsrv.IndexNames { for _, indexPage := range fsrv.IndexNames {
indexPath := sanitizedPathJoin(filename, indexPage) indexPath := caddyhttp.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", fsrv.logger.Debug("hiding index file",
@@ -243,12 +245,26 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
// trailing slash - not enforcing this can break relative hrefs // trailing slash - not enforcing this can break relative hrefs
// 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, "/") { // Only redirect if the last element of the path (the filename) was not
fsrv.logger.Debug("redirecting to canonical URI (adding trailing slash for directory)", zap.String("path", r.URL.Path)) // rewritten; if the admin wanted to rewrite to the canonical path, they
return redirect(w, r, r.URL.Path+"/") // would have, and we have to be very careful not to introduce unwanted
} else if !implicitIndexFile && strings.HasSuffix(r.URL.Path, "/") { // redirects and especially redirect loops!
fsrv.logger.Debug("redirecting to canonical URI (removing trailing slash for file)", zap.String("path", r.URL.Path)) // See https://github.com/caddyserver/caddy/issues/4205.
return redirect(w, r, r.URL.Path[:len(r.URL.Path)-1]) origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
if path.Base(origReq.URL.Path) == path.Base(r.URL.Path) {
if implicitIndexFile && !strings.HasSuffix(origReq.URL.Path, "/") {
to := origReq.URL.Path + "/"
fsrv.logger.Debug("redirecting to canonical URI (adding trailing slash for directory)",
zap.String("from_path", origReq.URL.Path),
zap.String("to_path", to))
return redirect(w, r, to)
} else if !implicitIndexFile && strings.HasSuffix(origReq.URL.Path, "/") {
to := origReq.URL.Path[:len(origReq.URL.Path)-1]
fsrv.logger.Debug("redirecting to canonical URI (removing trailing slash for file)",
zap.String("from_path", origReq.URL.Path),
zap.String("to_path", to))
return redirect(w, r, to)
}
} }
} }
@@ -423,42 +439,6 @@ func (fsrv *FileServer) transformHidePaths(repl *caddy.Replacer) []string {
return hide return hide
} }
// sanitizedPathJoin performs filepath.Join(root, reqPath) that
// is safe against directory traversal attacks. It uses logic
// similar to that in the Go standard library, specifically
// in the implementation of http.Dir. The root is assumed to
// be a trusted path, but reqPath is not.
func sanitizedPathJoin(root, reqPath string) string {
// TODO: Caddy 1 uses this:
// prevent absolute path access on Windows, e.g. http://localhost:5000/C:\Windows\notepad.exe
// if runtime.GOOS == "windows" && len(reqPath) > 0 && filepath.IsAbs(reqPath[1:]) {
// TODO.
// }
// TODO: whereas std lib's http.Dir.Open() uses this:
// if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) {
// return nil, errors.New("http: invalid character in file path")
// }
// TODO: see https://play.golang.org/p/oh77BiVQFti for another thing to consider
if root == "" {
root = "."
}
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 according to the hide list. // fileHidden returns true if filename is hidden according to the hide list.
// filename must be a relative or absolute file system path, not a request // 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 // URI path. It is expected that all the paths in the hide list are absolute
@@ -555,12 +535,6 @@ func (wr statusOverrideResponseWriter) WriteHeader(int) {
var defaultIndexNames = []string{"index.html", "index.txt"} var defaultIndexNames = []string{"index.html", "index.txt"}
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
const ( const (
minBackoff, maxBackoff = 2, 5 minBackoff, maxBackoff = 2, 5
separator = string(filepath.Separator) separator = string(filepath.Separator)
@@ -15,96 +15,12 @@
package fileserver package fileserver
import ( import (
"net/url"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"testing" "testing"
) )
func TestSanitizedPathJoin(t *testing.T) {
// For easy reference:
// %2e = .
// %2f = /
// %5c = \
for i, tc := range []struct {
inputRoot string
inputPath string
expect string
}{
{
inputPath: "",
expect: ".",
},
{
inputPath: "/",
expect: ".",
},
{
inputPath: "/foo",
expect: "foo",
},
{
inputPath: "/foo/",
expect: "foo" + separator,
},
{
inputPath: "/foo/bar",
expect: filepath.Join("foo", "bar"),
},
{
inputRoot: "/a",
inputPath: "/foo/bar",
expect: filepath.Join("/", "a", "foo", "bar"),
},
{
inputPath: "/foo/../bar",
expect: "bar",
},
{
inputRoot: "/a/b",
inputPath: "/foo/../bar",
expect: filepath.Join("/", "a", "b", "bar"),
},
{
inputRoot: "/a/b",
inputPath: "/..%2fbar",
expect: filepath.Join("/", "a", "b", "bar"),
},
{
inputRoot: "/a/b",
inputPath: "/%2e%2e%2fbar",
expect: filepath.Join("/", "a", "b", "bar"),
},
{
inputRoot: "/a/b",
inputPath: "/%2e%2e%2f%2e%2e%2f",
expect: filepath.Join("/", "a", "b") + separator,
},
{
inputRoot: "C:\\www",
inputPath: "/foo/bar",
expect: filepath.Join("C:\\www", "foo", "bar"),
},
// TODO: test more windows paths... on windows... sigh.
} {
// we don't *need* to use an actual parsed URL, but it
// adds some authenticity to the tests since real-world
// values will be coming in from URLs; thus, the test
// corpus can contain paths as encoded by clients, which
// more closely emulates the actual attack vector
u, err := url.Parse("http://test:9999" + tc.inputPath)
if err != nil {
t.Fatalf("Test %d: invalid URL: %v", i, err)
}
actual := sanitizedPathJoin(tc.inputRoot, u.Path)
if actual != tc.expect {
t.Errorf("Test %d: [%s %s] => %s (expected %s)",
i, tc.inputRoot, tc.inputPath, actual, tc.expect)
}
}
}
func TestFileHidden(t *testing.T) { func TestFileHidden(t *testing.T) {
for i, tc := range []struct { for i, tc := range []struct {
inputHide []string inputHide []string
+14 -2
View File
@@ -82,7 +82,19 @@ type (
// MatchMethod matches requests by the method. // MatchMethod matches requests by the method.
MatchMethod []string MatchMethod []string
// MatchQuery matches requests by URI's query string. // MatchQuery matches requests by the URI's query string. It takes a JSON object
// keyed by the query keys, with an array of string values to match for that key.
// Query key matches are exact, but wildcards may be used for value matches. Both
// keys and values may be placeholders.
// An example of the structure to match `?key=value&topic=api&query=something` is:
//
// ```json
// {
// "key": ["value"],
// "topic": ["api"],
// "query": ["*"]
// }
// ```
MatchQuery url.Values MatchQuery url.Values
// MatchHeader matches requests by header fields. It performs fast, // MatchHeader matches requests by header fields. It performs fast,
@@ -667,7 +679,7 @@ func (MatchProtocol) CaddyModule() caddy.ModuleInfo {
func (m MatchProtocol) Match(r *http.Request) bool { func (m MatchProtocol) Match(r *http.Request) bool {
switch string(m) { switch string(m) {
case "grpc": case "grpc":
return r.Header.Get("content-type") == "application/grpc" return strings.HasPrefix(r.Header.Get("content-type"), "application/grpc")
case "https": case "https":
return r.TLS != nil return r.TLS != nil
case "http": case "http":
+5 -3
View File
@@ -34,7 +34,8 @@ type adminUpstreams struct{}
// upstreamResults holds the status of a particular upstream // upstreamResults holds the status of a particular upstream
type upstreamStatus struct { type upstreamStatus struct {
Address string `json:"address"` ID string `json:"id"`
Address string `json:"address"` // Address is deprecated, should be removed in a future release.
Healthy bool `json:"healthy"` Healthy bool `json:"healthy"`
NumRequests int `json:"num_requests"` NumRequests int `json:"num_requests"`
Fails int `json:"fails"` Fails int `json:"fails"`
@@ -78,7 +79,7 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er
// Iterate over the upstream pool (needs to be fast) // Iterate over the upstream pool (needs to be fast)
var rangeErr error var rangeErr error
hosts.Range(func(key, val interface{}) bool { hosts.Range(func(key, val interface{}) bool {
address, ok := key.(string) id, ok := key.(string)
if !ok { if !ok {
rangeErr = caddy.APIError{ rangeErr = caddy.APIError{
HTTPStatus: http.StatusInternalServerError, HTTPStatus: http.StatusInternalServerError,
@@ -97,7 +98,8 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er
} }
results = append(results, upstreamStatus{ results = append(results, upstreamStatus{
Address: address, ID: id,
Address: id,
Healthy: !upstream.Unhealthy(), Healthy: !upstream.Unhealthy(),
NumRequests: upstream.NumRequests(), NumRequests: upstream.NumRequests(),
Fails: upstream.Fails(), Fails: upstream.Fails(),
+21 -13
View File
@@ -61,7 +61,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// lb_try_interval <interval> // lb_try_interval <interval>
// //
// # active health checking // # active health checking
// health_path <path> // health_uri <uri>
// health_port <port> // health_port <port>
// health_interval <interval> // health_interval <interval>
// health_timeout <duration> // health_timeout <duration>
@@ -219,6 +219,12 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// treated as a SRV-based upstream, and any port will be // treated as a SRV-based upstream, and any port will be
// dropped. // dropped.
appendUpstream := func(address string) error { appendUpstream := func(address string) error {
var id string
if strings.Contains(address, "|") {
parts := strings.SplitN(address, "|", 2)
id = parts[0]
address = parts[1]
}
isSRV := strings.HasPrefix(address, "srv+") isSRV := strings.HasPrefix(address, "srv+")
if isSRV { if isSRV {
address = strings.TrimPrefix(address, "srv+") address = strings.TrimPrefix(address, "srv+")
@@ -231,9 +237,9 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if host, _, err := net.SplitHostPort(dialAddr); err == nil { if host, _, err := net.SplitHostPort(dialAddr); err == nil {
dialAddr = host dialAddr = host
} }
h.Upstreams = append(h.Upstreams, &Upstream{LookupSRV: dialAddr}) h.Upstreams = append(h.Upstreams, &Upstream{ID: id, LookupSRV: dialAddr})
} else { } else {
h.Upstreams = append(h.Upstreams, &Upstream{Dial: dialAddr}) h.Upstreams = append(h.Upstreams, &Upstream{ID: id, Dial: dialAddr})
} }
return nil return nil
} }
@@ -978,6 +984,18 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
h.KeepAlive = new(KeepAlive) h.KeepAlive = new(KeepAlive)
} }
h.KeepAlive.MaxIdleConns = num h.KeepAlive.MaxIdleConns = num
case "keepalive_idle_conns_per_host":
if !d.NextArg() {
return d.ArgErr()
}
num, err := strconv.Atoi(d.Val())
if err != nil {
return d.Errf("bad integer value '%s': %v", d.Val(), err)
}
if h.KeepAlive == nil {
h.KeepAlive = new(KeepAlive)
}
h.KeepAlive.MaxIdleConnsPerHost = num h.KeepAlive.MaxIdleConnsPerHost = num
case "versions": case "versions":
@@ -1004,16 +1022,6 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
} }
h.MaxConnsPerHost = num h.MaxConnsPerHost = num
case "max_idle_conns_per_host":
if !d.NextArg() {
return d.ArgErr()
}
num, err := strconv.Atoi(d.Val())
if err != nil {
return d.Errf("bad integer value '%s': %v", d.Val(), err)
}
h.MaxIdleConnsPerHost = num
default: default:
return d.Errf("unrecognized subdirective %s", d.Val()) return d.Errf("unrecognized subdirective %s", d.Val())
} }
@@ -192,7 +192,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
// NOTE: we delete the tokens as we go so that the reverse_proxy // NOTE: we delete the tokens as we go so that the reverse_proxy
// unmarshal doesn't see these subdirectives which it cannot handle // unmarshal doesn't see these subdirectives which it cannot handle
for dispenser.Next() { for dispenser.Next() {
for dispenser.NextBlock(0) { for dispenser.NextBlock(0) && dispenser.Nesting() == 1 {
switch dispenser.Val() { switch dispenser.Val() {
case "root": case "root":
if !dispenser.NextArg() { if !dispenser.NextArg() {
@@ -20,7 +20,6 @@ import (
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@@ -218,12 +217,7 @@ func (t Transport) buildEnv(r *http.Request) (map[string]string, error) {
} }
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME // SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
scriptFilename := filepath.Join(root, scriptName) scriptFilename := caddyhttp.SanitizedPathJoin(root, scriptName)
// Add vhost path prefix to scriptName. Otherwise, some PHP software will
// have difficulty discovering its URL.
pathPrefix, _ := r.Context().Value(caddy.CtxKey("path_prefix")).(string)
scriptName = path.Join(pathPrefix, scriptName)
// Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875 // Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875
// Info: https://tools.ietf.org/html/rfc3875#section-4.1.13 // Info: https://tools.ietf.org/html/rfc3875#section-4.1.13
@@ -236,13 +230,7 @@ func (t Transport) buildEnv(r *http.Request) (map[string]string, error) {
// original URI in as the value of REQUEST_URI (the user can overwrite this // original URI in as the value of REQUEST_URI (the user can overwrite this
// if desired). Most PHP apps seem to want the original URI. Besides, this is // if desired). Most PHP apps seem to want the original URI. Besides, this is
// how nginx defaults: http://stackoverflow.com/a/12485156/1048862 // how nginx defaults: http://stackoverflow.com/a/12485156/1048862
origReq, ok := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request) origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
if !ok {
// some requests, like active health checks, don't add this to
// the request context, so we can just use the current URL
origReq = *r
}
reqURL := origReq.URL
requestScheme := "http" requestScheme := "http"
if r.TLS != nil { if r.TLS != nil {
@@ -285,7 +273,7 @@ func (t Transport) buildEnv(r *http.Request) (map[string]string, error) {
"DOCUMENT_ROOT": root, "DOCUMENT_ROOT": root,
"DOCUMENT_URI": docURI, "DOCUMENT_URI": docURI,
"HTTP_HOST": r.Host, // added here, since not always part of headers "HTTP_HOST": r.Host, // added here, since not always part of headers
"REQUEST_URI": reqURL.RequestURI(), "REQUEST_URI": origReq.URL.RequestURI(),
"SCRIPT_FILENAME": scriptFilename, "SCRIPT_FILENAME": scriptFilename,
"SCRIPT_NAME": scriptName, "SCRIPT_NAME": scriptName,
} }
@@ -294,7 +282,7 @@ func (t Transport) buildEnv(r *http.Request) (map[string]string, error) {
// PATH_TRANSLATED should only exist if PATH_INFO is defined. // PATH_TRANSLATED should only exist if PATH_INFO is defined.
// Info: https://www.ietf.org/rfc/rfc3875 Page 14 // Info: https://www.ietf.org/rfc/rfc3875 Page 14
if env["PATH_INFO"] != "" { if env["PATH_INFO"] != "" {
env["PATH_TRANSLATED"] = filepath.Join(root, pathInfo) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html env["PATH_TRANSLATED"] = caddyhttp.SanitizedPathJoin(root, pathInfo) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
} }
// compliance with the CGI specification requires that // compliance with the CGI specification requires that
@@ -189,13 +189,14 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
return return
} }
hostAddr := addr.JoinHostPort(0) hostAddr := addr.JoinHostPort(0)
dialAddr := hostAddr
if addr.IsUnixNetwork() { if addr.IsUnixNetwork() {
// this will be used as the Host portion of a http.Request URL, and // this will be used as the Host portion of a http.Request URL, and
// paths to socket files would produce an error when creating URL, // paths to socket files would produce an error when creating URL,
// so use a fake Host value instead; unix sockets are usually local // so use a fake Host value instead; unix sockets are usually local
hostAddr = "localhost" hostAddr = "localhost"
} }
err = h.doActiveHealthCheck(DialInfo{Network: addr.Network, Address: hostAddr}, hostAddr, upstream.Host) err = h.doActiveHealthCheck(DialInfo{Network: addr.Network, Address: dialAddr}, hostAddr, upstream.Host)
if err != nil { if err != nil {
h.HealthChecks.Active.logger.Error("active health check failed", h.HealthChecks.Active.logger.Error("active health check failed",
zap.String("address", hostAddr), zap.String("address", hostAddr),
+9
View File
@@ -65,6 +65,12 @@ type UpstreamPool []*Upstream
type Upstream struct { type Upstream struct {
Host `json:"-"` Host `json:"-"`
// The unique ID for this upstream, to disambiguate multiple
// upstreams with the same Dial address. This is optional,
// and only necessary if the upstream states need to be
// separate, such as having different health checking policies.
ID string `json:"id,omitempty"`
// The [network address](/docs/conventions#network-addresses) // The [network address](/docs/conventions#network-addresses)
// to dial to connect to the upstream. Must represent precisely // to dial to connect to the upstream. Must represent precisely
// one socket (i.e. no port ranges). A valid network address // one socket (i.e. no port ranges). A valid network address
@@ -98,6 +104,9 @@ type Upstream struct {
} }
func (u Upstream) String() string { func (u Upstream) String() string {
if u.ID != "" {
return u.ID
}
if u.LookupSRV != "" { if u.LookupSRV != "" {
return u.LookupSRV return u.LookupSRV
} }
@@ -62,9 +62,6 @@ type HTTPTransport struct {
// Maximum number of connections per host. Default: 0 (no limit) // Maximum number of connections per host. Default: 0 (no limit)
MaxConnsPerHost int `json:"max_conns_per_host,omitempty"` MaxConnsPerHost int `json:"max_conns_per_host,omitempty"`
// Maximum number of idle connections per host. Default: 0 (uses Go's default of 2)
MaxIdleConnsPerHost int `json:"max_idle_conns_per_host,omitempty"`
// How long to wait before timing out trying to connect to // How long to wait before timing out trying to connect to
// an upstream. // an upstream.
DialTimeout caddy.Duration `json:"dial_timeout,omitempty"` DialTimeout caddy.Duration `json:"dial_timeout,omitempty"`
@@ -197,7 +194,6 @@ func (h *HTTPTransport) NewTransport(ctx caddy.Context) (*http.Transport, error)
return conn, nil return conn, nil
}, },
MaxConnsPerHost: h.MaxConnsPerHost, MaxConnsPerHost: h.MaxConnsPerHost,
MaxIdleConnsPerHost: h.MaxIdleConnsPerHost,
ResponseHeaderTimeout: time.Duration(h.ResponseHeaderTimeout), ResponseHeaderTimeout: time.Duration(h.ResponseHeaderTimeout),
ExpectContinueTimeout: time.Duration(h.ExpectContinueTimeout), ExpectContinueTimeout: time.Duration(h.ExpectContinueTimeout),
MaxResponseHeaderBytes: h.MaxResponseHeaderSize, MaxResponseHeaderBytes: h.MaxResponseHeaderSize,
@@ -412,13 +408,13 @@ type KeepAlive struct {
// How often to probe for liveness. // How often to probe for liveness.
ProbeInterval caddy.Duration `json:"probe_interval,omitempty"` ProbeInterval caddy.Duration `json:"probe_interval,omitempty"`
// Maximum number of idle connections. // Maximum number of idle connections. Default: 0, which means no limit.
MaxIdleConns int `json:"max_idle_conns,omitempty"` MaxIdleConns int `json:"max_idle_conns,omitempty"`
// Maximum number of idle connections per upstream host. // Maximum number of idle connections per host. Default: 32.
MaxIdleConnsPerHost int `json:"max_idle_conns_per_host,omitempty"` MaxIdleConnsPerHost int `json:"max_idle_conns_per_host,omitempty"`
// How long connections should be kept alive when idle. // How long connections should be kept alive when idle. Default: 0, which means no timeout.
IdleConnTimeout caddy.Duration `json:"idle_timeout,omitempty"` IdleConnTimeout caddy.Duration `json:"idle_timeout,omitempty"`
} }
+41 -35
View File
@@ -23,6 +23,7 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/textproto"
"net/url" "net/url"
"regexp" "regexp"
"strconv" "strconv"
@@ -80,10 +81,13 @@ type Handler struct {
// Upstreams is the list of backends to proxy to. // Upstreams is the list of backends to proxy to.
Upstreams UpstreamPool `json:"upstreams,omitempty"` Upstreams UpstreamPool `json:"upstreams,omitempty"`
// Adjusts how often to flush the response buffer. A // Adjusts how often to flush the response buffer. By default,
// negative value disables response buffering. // no periodic flushing is done. A negative value disables
// TODO: figure out good defaults and write docs for this // response buffering, and flushes immediately after each
// (see https://github.com/caddyserver/caddy/issues/1460) // write to the client. This option is ignored when the upstream's
// response is recognized as a streaming response, or if its
// content length is -1; for such responses, writes are flushed
// to the client immediately.
FlushInterval caddy.Duration `json:"flush_interval,omitempty"` FlushInterval caddy.Duration `json:"flush_interval,omitempty"`
// Headers manipulates headers between Caddy and the backend. // Headers manipulates headers between Caddy and the backend.
@@ -120,9 +124,10 @@ type Handler struct {
// handler chain will not affect the health status of the // handler chain will not affect the health status of the
// backend. // backend.
// //
// Two new placeholders are available in this handler chain: // Three new placeholders are available in this handler chain:
// - `{http.reverse_proxy.status_code}` The status code // - `{http.reverse_proxy.status_code}` The status code from the response
// - `{http.reverse_proxy.status_text}` The status text // - `{http.reverse_proxy.status_text}` The status text from the response
// - `{http.reverse_proxy.header.*}` The headers from the response
HandleResponse []caddyhttp.ResponseHandler `json:"handle_response,omitempty"` HandleResponse []caddyhttp.ResponseHandler `json:"handle_response,omitempty"`
Transport http.RoundTripper `json:"-"` Transport http.RoundTripper `json:"-"`
@@ -203,7 +208,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
KeepAlive: &KeepAlive{ KeepAlive: &KeepAlive{
ProbeInterval: caddy.Duration(30 * time.Second), ProbeInterval: caddy.Duration(30 * time.Second),
IdleConnTimeout: caddy.Duration(2 * time.Minute), IdleConnTimeout: caddy.Duration(2 * time.Minute),
MaxIdleConnsPerHost: 32, MaxIdleConnsPerHost: 32, // seems about optimal, see #2805
}, },
DialTimeout: caddy.Duration(10 * time.Second), DialTimeout: caddy.Duration(10 * time.Second),
} }
@@ -503,18 +508,14 @@ func (h Handler) prepareRequest(req *http.Request) error {
// Remove hop-by-hop headers to the backend. Especially // Remove hop-by-hop headers to the backend. Especially
// important is "Connection" because we want a persistent // important is "Connection" because we want a persistent
// connection, regardless of what the client sent to us. // connection, regardless of what the client sent to us.
// Issue golang/go#46313: don't skip if field is empty.
for _, h := range hopHeaders { for _, h := range hopHeaders {
hv := req.Header.Get(h) // Issue golang/go#21096: tell backend applications that care about trailer support
if hv == "" { // that we support trailers. (We do, but we don't go out of our way to
continue // advertise that unless the incoming client request thought it was worth
} // mentioning.)
if h == "Te" && hv == "trailers" { if h == "Te" && httpguts.HeaderValuesContainsToken(req.Header["Te"], "trailers") {
// Issue golang/go#21096: tell backend applications that req.Header.Set("Te", "trailers")
// care about trailer support that we support
// trailers. (We do, but we don't go out of
// our way to advertise that unless the
// incoming client request thought it was
// worth mentioning)
continue continue
} }
req.Header.Del(h) req.Header.Del(h)
@@ -531,13 +532,19 @@ func (h Handler) prepareRequest(req *http.Request) error {
// If we aren't the first proxy retain prior // If we aren't the first proxy retain prior
// X-Forwarded-For information as a comma+space // X-Forwarded-For information as a comma+space
// separated list and fold multiple headers into one. // separated list and fold multiple headers into one.
if prior, ok := req.Header["X-Forwarded-For"]; ok { prior, ok := req.Header["X-Forwarded-For"]
omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
if len(prior) > 0 {
clientIP = strings.Join(prior, ", ") + ", " + clientIP clientIP = strings.Join(prior, ", ") + ", " + clientIP
} }
req.Header.Set("X-Forwarded-For", clientIP) if !omit {
req.Header.Set("X-Forwarded-For", clientIP)
}
} }
if req.Header.Get("X-Forwarded-Proto") == "" { prior, ok := req.Header["X-Forwarded-Proto"]
omit := ok && prior == nil
if len(prior) == 0 && !omit {
// set X-Forwarded-Proto; many backend apps expect this too // set X-Forwarded-Proto; many backend apps expect this too
proto := "https" proto := "https"
if req.TLS == nil { if req.TLS == nil {
@@ -631,9 +638,17 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
if len(rh.Routes) == 0 { if len(rh.Routes) == 0 {
continue continue
} }
res.Body.Close() res.Body.Close()
// set up the replacer so that parts of the original response can be
// used for routing decisions
for field, value := range res.Header {
repl.Set("http.reverse_proxy.header."+field, strings.Join(value, ","))
}
repl.Set("http.reverse_proxy.status_code", res.StatusCode) repl.Set("http.reverse_proxy.status_code", res.StatusCode)
repl.Set("http.reverse_proxy.status_text", res.Status) repl.Set("http.reverse_proxy.status_text", res.Status)
h.logger.Debug("handling response", zap.Int("handler", i)) h.logger.Debug("handling response", zap.Int("handler", i))
if routeErr := rh.Routes.Compile(next).ServeHTTP(rw, req); routeErr != nil { if routeErr := rh.Routes.Compile(next).ServeHTTP(rw, req); routeErr != nil {
// wrap error in roundtripSucceeded so caller knows that // wrap error in roundtripSucceeded so caller knows that
@@ -676,15 +691,6 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
} }
rw.WriteHeader(res.StatusCode) rw.WriteHeader(res.StatusCode)
// some apps need the response headers before starting to stream content with http2,
// so it's important to explicitly flush the headers to the client before streaming the data.
// (see https://github.com/caddyserver/caddy/issues/3556 for use case and nuances)
if h.isBidirectionalStream(req, res) {
if wf, ok := rw.(http.Flusher); ok {
wf.Flush()
}
}
err = h.copyResponse(rw, res.Body, h.flushInterval(req, res)) err = h.copyResponse(rw, res.Body, h.flushInterval(req, res))
res.Body.Close() // close now, instead of defer, to populate res.Trailer res.Body.Close() // close now, instead of defer, to populate res.Trailer
if err != nil { if err != nil {
@@ -822,10 +828,10 @@ func upgradeType(h http.Header) string {
// removeConnectionHeaders removes hop-by-hop headers listed in the "Connection" header of h. // removeConnectionHeaders removes hop-by-hop headers listed in the "Connection" header of h.
// See RFC 7230, section 6.1 // See RFC 7230, section 6.1
func removeConnectionHeaders(h http.Header) { func removeConnectionHeaders(h http.Header) {
if c := h.Get("Connection"); c != "" { for _, f := range h["Connection"] {
for _, f := range strings.Split(c, ",") { for _, sf := range strings.Split(f, ",") {
if f = strings.TrimSpace(f); f != "" { if sf = textproto.TrimString(sf); sf != "" {
h.Del(f) h.Del(sf)
} }
} }
} }
+16 -4
View File
@@ -32,6 +32,9 @@ import (
func (h Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWriter, req *http.Request, res *http.Response) { func (h Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWriter, req *http.Request, res *http.Response) {
reqUpType := upgradeType(req.Header) reqUpType := upgradeType(req.Header)
resUpType := upgradeType(res.Header) resUpType := upgradeType(res.Header)
// TODO: Update to use "net/http/internal/ascii" once we bumped
// the minimum Go version to 1.17.
// See https://github.com/golang/go/commit/5c489514bc5e61ad9b5b07bd7d8ec65d66a0512a
if reqUpType != resUpType { if reqUpType != resUpType {
h.logger.Debug("backend tried to switch to unexpected protocol via Upgrade header", h.logger.Debug("backend tried to switch to unexpected protocol via Upgrade header",
zap.String("backend_upgrade", resUpType), zap.String("backend_upgrade", resUpType),
@@ -39,8 +42,6 @@ func (h Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWrite
return return
} }
copyHeader(res.Header, rw.Header())
hj, ok := rw.(http.Hijacker) hj, ok := rw.(http.Hijacker)
if !ok { if !ok {
h.logger.Sugar().Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw) h.logger.Sugar().Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw)
@@ -78,6 +79,9 @@ func (h Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWrite
logger.Debug("connection closed", zap.Duration("duration", time.Since(start))) logger.Debug("connection closed", zap.Duration("duration", time.Since(start)))
}() }()
copyHeader(rw.Header(), res.Header)
res.Header = rw.Header()
res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above
if err := res.Write(brw); err != nil { if err := res.Write(brw); err != nil {
h.logger.Debug("response write", zap.Error(err)) h.logger.Debug("response write", zap.Error(err))
@@ -107,13 +111,16 @@ func (h Handler) flushInterval(req *http.Request, res *http.Response) time.Durat
return -1 // negative means immediately return -1 // negative means immediately
} }
// We might have the case of streaming for which Content-Length might be unset.
if res.ContentLength == -1 {
return -1
}
// for h2 and h2c upstream streaming data to client (issues #3556 and #3606) // for h2 and h2c upstream streaming data to client (issues #3556 and #3606)
if h.isBidirectionalStream(req, res) { if h.isBidirectionalStream(req, res) {
return -1 return -1
} }
// TODO: more specific cases? e.g. res.ContentLength == -1? (this TODO is from the std lib, but
// strangely similar to our isBidirectionalStream function that we implemented ourselves)
return time.Duration(h.FlushInterval) return time.Duration(h.FlushInterval)
} }
@@ -142,6 +149,11 @@ func (h Handler) copyResponse(dst io.Writer, src io.Reader, flushInterval time.D
latency: flushInterval, latency: flushInterval,
} }
defer mlw.stop() defer mlw.stop()
// set up initial timer so headers get flushed even if body writes are delayed
mlw.flushPending = true
mlw.t = time.AfterFunc(flushInterval, mlw.delayedFlush)
dst = mlw dst = mlw
} }
} }
+14
View File
@@ -184,8 +184,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log = logger.Error log = logger.Error
} }
userID, _ := repl.GetString("http.auth.user.id")
log("handled request", log("handled request",
zap.String("common_log", repl.ReplaceAll(commonLogFormat, commonLogEmptyValue)), zap.String("common_log", repl.ReplaceAll(commonLogFormat, commonLogEmptyValue)),
zap.String("user_id", userID),
zap.Duration("duration", duration), zap.Duration("duration", duration),
zap.Int("size", wrec.Size()), zap.Int("size", wrec.Size()),
zap.Int("status", wrec.Status()), zap.Int("status", wrec.Status()),
@@ -379,7 +382,9 @@ func (s *Server) hasTLSClientAuth() bool {
// that it is after any other host matcher but before any "catch-all" // that it is after any other host matcher but before any "catch-all"
// route without a host matcher. // route without a host matcher.
func (s *Server) findLastRouteWithHostMatcher() int { func (s *Server) findLastRouteWithHostMatcher() int {
foundHostMatcher := false
lastIndex := len(s.Routes) lastIndex := len(s.Routes)
for i, route := range s.Routes { for i, route := range s.Routes {
// since we want to break out of an inner loop, use a closure // since we want to break out of an inner loop, use a closure
// to allow us to use 'return' when we found a host matcher // to allow us to use 'return' when we found a host matcher
@@ -388,6 +393,7 @@ func (s *Server) findLastRouteWithHostMatcher() int {
for _, matcher := range sets { for _, matcher := range sets {
switch matcher.(type) { switch matcher.(type) {
case *MatchHost: case *MatchHost:
foundHostMatcher = true
return true return true
} }
} }
@@ -401,6 +407,14 @@ func (s *Server) findLastRouteWithHostMatcher() int {
lastIndex = i + 1 lastIndex = i + 1
} }
} }
// If we didn't actually find a host matcher, return 0
// because that means every defined route was a "catch-all".
// See https://caddy.community/t/how-to-set-priority-in-caddyfile/13002/8
if !foundHostMatcher {
return 0
}
return lastIndex return lastIndex
} }
+20 -20
View File
@@ -29,6 +29,7 @@ import (
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/acme"
acmeAPI "github.com/smallstep/certificates/acme/api" acmeAPI "github.com/smallstep/certificates/acme/api"
acmeNoSQL "github.com/smallstep/certificates/acme/db/nosql"
"github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/db" "github.com/smallstep/certificates/db"
@@ -49,17 +50,16 @@ type Handler struct {
// The hostname or IP address by which ACME clients // The hostname or IP address by which ACME clients
// will access the server. This is used to populate // will access the server. This is used to populate
// the ACME directory endpoint. Default: localhost. // the ACME directory endpoint. If not set, the Host
// header of the request will be used.
// COMPATIBILITY NOTE / TODO: This property may go away in the // COMPATIBILITY NOTE / TODO: This property may go away in the
// future, as it is currently only required due to // future. Do not rely on this property long-term; check release notes.
// limitations in the underlying library. Do not rely
// on this property long-term; check release notes.
Host string `json:"host,omitempty"` Host string `json:"host,omitempty"`
// The path prefix under which to serve all ACME // The path prefix under which to serve all ACME
// endpoints. All other requests will not be served // endpoints. All other requests will not be served
// by this handler and will be passed through to // by this handler and will be passed through to
// the next one. Default: "/acme/" // the next one. Default: "/acme/".
// COMPATIBILITY NOTE / TODO: This property may go away in the // COMPATIBILITY NOTE / TODO: This property may go away in the
// future, as it is currently only required due to // future, as it is currently only required due to
// limitations in the underlying library. Do not rely // limitations in the underlying library. Do not rely
@@ -92,9 +92,6 @@ func (ash *Handler) Provision(ctx caddy.Context) error {
if ash.CA == "" { if ash.CA == "" {
ash.CA = caddypki.DefaultCAID ash.CA = caddypki.DefaultCAID
} }
if ash.Host == "" {
ash.Host = defaultHost
}
if ash.PathPrefix == "" { if ash.PathPrefix == "" {
ash.PathPrefix = defaultPathPrefix ash.PathPrefix = defaultPathPrefix
} }
@@ -138,17 +135,23 @@ func (ash *Handler) Provision(ctx caddy.Context) error {
return err return err
} }
acmeAuth, err := acme.New(auth, acme.AuthorityOptions{ var acmeDB acme.DB
DB: auth.GetDatabase().(nosql.DB), // stores all the server state if authorityConfig.DB != nil {
DNS: ash.Host, // used for directory links; TODO: not needed acmeDB, err = acmeNoSQL.New(auth.GetDatabase().(nosql.DB))
Prefix: strings.Trim(ash.PathPrefix, "/"), // used for directory links if err != nil {
}) return fmt.Errorf("configuring ACME DB: %v", err)
if err != nil { }
return err
} }
// create the router for the ACME endpoints // create the router for the ACME endpoints
acmeRouterHandler := acmeAPI.New(acmeAuth) acmeRouterHandler := acmeAPI.NewHandler(acmeAPI.HandlerOptions{
CA: auth,
DB: acmeDB, // stores all the server state
DNS: ash.Host, // used for directory links
Prefix: strings.Trim(ash.PathPrefix, "/"), // used for directory links
})
// extract its http.Handler so we can use it directly
r := chi.NewRouter() r := chi.NewRouter()
r.Route(ash.PathPrefix, func(r chi.Router) { r.Route(ash.PathPrefix, func(r chi.Router) {
acmeRouterHandler.Route(r) acmeRouterHandler.Route(r)
@@ -212,10 +215,7 @@ func (ash Handler) openDatabase() (*db.AuthDB, error) {
return database.(databaseCloser).DB, err return database.(databaseCloser).DB, err
} }
const ( const defaultPathPrefix = "/acme/"
defaultHost = "localhost"
defaultPathPrefix = "/acme/"
)
var keyCleaner = regexp.MustCompile(`[^\w.-_]`) var keyCleaner = regexp.MustCompile(`[^\w.-_]`)
var databasePool = caddy.NewUsagePool() var databasePool = caddy.NewUsagePool()
+78
View File
@@ -265,6 +265,10 @@ func (iss *ACMEIssuer) GetACMEIssuer() *ACMEIssuer { return iss }
// trusted_roots <pem_files...> // trusted_roots <pem_files...>
// dns <provider_name> [<options>] // dns <provider_name> [<options>]
// resolvers <dns_servers...> // resolvers <dns_servers...>
// preferred_chains [smallest] {
// root_common_name <common_names...>
// any_common_name <common_names...>
// }
// } // }
// //
func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
@@ -387,6 +391,22 @@ func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return err return err
} }
iss.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil) iss.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil)
case "propagation_timeout":
if !d.NextArg() {
return d.ArgErr()
}
timeoutStr := d.Val()
timeout, err := caddy.ParseDuration(timeoutStr)
if err != nil {
return d.Errf("invalid propagation_timeout duration %s: %v", timeoutStr, err)
}
if iss.Challenges == nil {
iss.Challenges = new(ChallengesConfig)
}
if iss.Challenges.DNS == nil {
iss.Challenges.DNS = new(DNSChallengeConfig)
}
iss.Challenges.DNS.PropagationTimeout = caddy.Duration(timeout)
case "resolvers": case "resolvers":
if iss.Challenges == nil { if iss.Challenges == nil {
@@ -400,6 +420,13 @@ func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.ArgErr() return d.ArgErr()
} }
case "preferred_chains":
chainPref, err := ParseCaddyfilePreferredChainsOptions(d)
if err != nil {
return err
}
iss.PreferredChains = chainPref
default: default:
return d.Errf("unrecognized ACME issuer property: %s", d.Val()) return d.Errf("unrecognized ACME issuer property: %s", d.Val())
} }
@@ -436,6 +463,57 @@ func onDemandAskRequest(ask string, name string) error {
return nil return nil
} }
func ParseCaddyfilePreferredChainsOptions(d *caddyfile.Dispenser) (*ChainPreference, error) {
chainPref := new(ChainPreference)
if d.NextArg() {
smallestOpt := d.Val()
if smallestOpt == "smallest" {
trueBool := true
chainPref.Smallest = &trueBool
if d.NextArg() { // Only one argument allowed
return nil, d.ArgErr()
}
if d.NextBlock(d.Nesting()) { // Don't allow other options when smallest == true
return nil, d.Err("No more options are accepted when using the 'smallest' option")
}
} else { // Smallest option should always be 'smallest' or unset
return nil, d.Errf("Invalid argument '%s'", smallestOpt)
}
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "root_common_name":
rootCommonNameOpt := d.RemainingArgs()
chainPref.RootCommonName = rootCommonNameOpt
if rootCommonNameOpt == nil {
return nil, d.ArgErr()
}
if chainPref.AnyCommonName != nil {
return nil, d.Err("Can't set root_common_name when any_common_name is already set")
}
case "any_common_name":
anyCommonNameOpt := d.RemainingArgs()
chainPref.AnyCommonName = anyCommonNameOpt
if anyCommonNameOpt == nil {
return nil, d.ArgErr()
}
if chainPref.RootCommonName != nil {
return nil, d.Err("Can't set any_common_name when root_common_name is already set")
}
default:
return nil, d.Errf("Received unrecognized parameter '%s'", d.Val())
}
}
if chainPref.Smallest == nil && chainPref.RootCommonName == nil && chainPref.AnyCommonName == nil {
return nil, d.Err("No options for preferred_chains received")
}
return chainPref, nil
}
// ChainPreference describes the client's preferred certificate chain, // ChainPreference describes the client's preferred certificate chain,
// useful if the CA offers alternate chains. The first matching chain // useful if the CA offers alternate chains. The first matching chain
// will be selected. // will be selected.
-10
View File
@@ -89,10 +89,6 @@ type AutomationPolicy struct {
// zerossl. // zerossl.
IssuersRaw []json.RawMessage `json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module"` IssuersRaw []json.RawMessage `json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module"`
// DEPRECATED: Use `issuers` instead (November 2020). This field will
// be removed in the future.
IssuerRaw json.RawMessage `json:"issuer,omitempty" caddy:"namespace=tls.issuance inline_key=module"`
// If true, certificates will be requested with MustStaple. Not all // If true, certificates will be requested with MustStaple. Not all
// CAs support this, and there are potentially serious consequences // CAs support this, and there are potentially serious consequences
// of enabling this feature without proper threat modeling. // of enabling this feature without proper threat modeling.
@@ -180,12 +176,6 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
} }
} }
// TODO: IssuerRaw field deprecated as of November 2020 - remove this shim after deprecation is complete
if ap.IssuerRaw != nil {
tlsApp.logger.Warn("the 'issuer' field is deprecated and will be removed in the future; use 'issuers' instead; your issuer has been appended automatically for now")
ap.IssuersRaw = append(ap.IssuersRaw, ap.IssuerRaw)
}
// load and provision any explicitly-configured issuer modules // load and provision any explicitly-configured issuer modules
if ap.IssuersRaw != nil { if ap.IssuersRaw != nil {
val, err := tlsApp.ctx.LoadModule(ap, "IssuersRaw") val, err := tlsApp.ctx.LoadModule(ap, "IssuersRaw")
+1 -3
View File
@@ -175,9 +175,7 @@ func (d customCertLifetime) Modify(cert *x509.Certificate, _ provisioner.SignOpt
return nil return nil
} }
const ( const defaultInternalCertLifetime = 12 * time.Hour
defaultInternalCertLifetime = 12 * time.Hour
)
// Interface guards // Interface guards
var ( var (
+6
View File
@@ -135,6 +135,7 @@ func (SingleFieldEncoder) CaddyModule() caddy.ModuleInfo {
// Provision sets up the encoder. // Provision sets up the encoder.
func (se *SingleFieldEncoder) Provision(ctx caddy.Context) error { func (se *SingleFieldEncoder) Provision(ctx caddy.Context) error {
caddy.Log().Named("caddy.logging.encoders.single_field").Warn("the 'single_field' encoder is deprecated and will be removed soon!")
if se.FallbackRaw != nil { if se.FallbackRaw != nil {
val, err := ctx.LoadModule(se, "FallbackRaw") val, err := ctx.LoadModule(se, "FallbackRaw")
if err != nil { if err != nil {
@@ -264,6 +265,9 @@ func (lec *LogEncoderConfig) ZapcoreEncoderConfig() zapcore.EncoderConfig {
if lec.MessageKey != nil { if lec.MessageKey != nil {
cfg.MessageKey = *lec.MessageKey cfg.MessageKey = *lec.MessageKey
} }
if lec.LevelKey != nil {
cfg.LevelKey = *lec.LevelKey
}
if lec.TimeKey != nil { if lec.TimeKey != nil {
cfg.TimeKey = *lec.TimeKey cfg.TimeKey = *lec.TimeKey
} }
@@ -304,6 +308,8 @@ func (lec *LogEncoderConfig) ZapcoreEncoderConfig() zapcore.EncoderConfig {
timeFormat = "2006/01/02 15:04:05.000" timeFormat = "2006/01/02 15:04:05.000"
case "wall_nano": case "wall_nano":
timeFormat = "2006/01/02 15:04:05.000000000" timeFormat = "2006/01/02 15:04:05.000000000"
case "common_log":
timeFormat = "02/Jan/2006:15:04:05 -0700"
} }
timeFormatter = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) { timeFormatter = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(ts.UTC().Format(timeFormat)) encoder.AppendString(ts.UTC().Format(timeFormat))
+2
View File
@@ -188,9 +188,11 @@ func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
// Interface guards // Interface guards
var ( var (
_ LogFieldFilter = (*DeleteFilter)(nil) _ LogFieldFilter = (*DeleteFilter)(nil)
_ LogFieldFilter = (*ReplaceFilter)(nil)
_ LogFieldFilter = (*IPMaskFilter)(nil) _ LogFieldFilter = (*IPMaskFilter)(nil)
_ caddyfile.Unmarshaler = (*DeleteFilter)(nil) _ caddyfile.Unmarshaler = (*DeleteFilter)(nil)
_ caddyfile.Unmarshaler = (*ReplaceFilter)(nil)
_ caddyfile.Unmarshaler = (*IPMaskFilter)(nil) _ caddyfile.Unmarshaler = (*IPMaskFilter)(nil)
_ caddy.Provisioner = (*IPMaskFilter)(nil) _ caddy.Provisioner = (*IPMaskFilter)(nil)
+61 -14
View File
@@ -18,7 +18,9 @@ import (
"fmt" "fmt"
"io" "io"
"net" "net"
"os"
"sync" "sync"
"time"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -28,10 +30,16 @@ func init() {
caddy.RegisterModule(NetWriter{}) caddy.RegisterModule(NetWriter{})
} }
// NetWriter implements a log writer that outputs to a network socket. // NetWriter implements a log writer that outputs to a network socket. If
// the socket goes down, it will dump logs to stderr while it attempts to
// reconnect.
type NetWriter struct { type NetWriter struct {
// The address of the network socket to which to connect.
Address string `json:"address,omitempty"` Address string `json:"address,omitempty"`
// The timeout to wait while connecting to the socket.
DialTimeout caddy.Duration `json:"dial_timeout,omitempty"`
addr caddy.NetworkAddress addr caddy.NetworkAddress
} }
@@ -60,6 +68,10 @@ func (nw *NetWriter) Provision(ctx caddy.Context) error {
return fmt.Errorf("multiple ports not supported") return fmt.Errorf("multiple ports not supported")
} }
if nw.DialTimeout < 0 {
return fmt.Errorf("timeout cannot be less than 0")
}
return nil return nil
} }
@@ -74,7 +86,10 @@ func (nw NetWriter) WriterKey() string {
// OpenWriter opens a new network connection. // OpenWriter opens a new network connection.
func (nw NetWriter) OpenWriter() (io.WriteCloser, error) { func (nw NetWriter) OpenWriter() (io.WriteCloser, error) {
reconn := &redialerConn{nw: nw} reconn := &redialerConn{
nw: nw,
timeout: time.Duration(nw.DialTimeout),
}
conn, err := reconn.dial() conn, err := reconn.dial()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -87,7 +102,9 @@ func (nw NetWriter) OpenWriter() (io.WriteCloser, error) {
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
// //
// net <address> // net <address> {
// dial_timeout <duration>
// }
// //
func (nw *NetWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { func (nw *NetWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() { for d.Next() {
@@ -98,6 +115,22 @@ func (nw *NetWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if d.NextArg() { if d.NextArg() {
return d.ArgErr() return d.ArgErr()
} }
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "dial_timeout":
if !d.NextArg() {
return d.ArgErr()
}
timeout, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("invalid duration: %s", d.Val())
}
if d.NextArg() {
return d.ArgErr()
}
nw.DialTimeout = caddy.Duration(timeout)
}
}
} }
return nil return nil
} }
@@ -107,8 +140,10 @@ func (nw *NetWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// is retried. // is retried.
type redialerConn struct { type redialerConn struct {
net.Conn net.Conn
connMu sync.RWMutex connMu sync.RWMutex
nw NetWriter nw NetWriter
timeout time.Duration
lastRedial time.Time
} }
// Write wraps the underlying Conn.Write method, but if that fails, // Write wraps the underlying Conn.Write method, but if that fails,
@@ -131,20 +166,32 @@ func (reconn *redialerConn) Write(b []byte) (n int, err error) {
return return
} }
// we're the lucky first goroutine to re-dial the connection // there's still a problem, so try to re-attempt dialing the socket
conn2, err2 := reconn.dial() // if some time has passed in which the issue could have potentially
if err2 != nil { // been resolved - we don't want to block at every single log
return // emission (!) - see discussion in #4111
} if time.Since(reconn.lastRedial) > 10*time.Second {
if n, err = conn2.Write(b); err == nil { reconn.lastRedial = time.Now()
reconn.Conn.Close() conn2, err2 := reconn.dial()
reconn.Conn = conn2 if err2 != nil {
// logger socket still offline; instead of discarding the log, dump it to stderr
os.Stderr.Write(b)
return
}
if n, err = conn2.Write(b); err == nil {
reconn.Conn.Close()
reconn.Conn = conn2
}
} else {
// last redial attempt was too recent; just dump to stderr for now
os.Stderr.Write(b)
} }
return return
} }
func (reconn *redialerConn) dial() (net.Conn, error) { func (reconn *redialerConn) dial() (net.Conn, error) {
return net.Dial(reconn.nw.addr.Network, reconn.nw.addr.JoinHostPort(0)) return net.DialTimeout(reconn.nw.addr.Network, reconn.nw.addr.JoinHostPort(0), reconn.timeout)
} }
// Interface guards // Interface guards
+4
View File
@@ -301,6 +301,10 @@ func globalDefaultReplacements(key string) (interface{}, bool) {
return nowFunc().Format("02/Jan/2006:15:04:05 -0700"), true return nowFunc().Format("02/Jan/2006:15:04:05 -0700"), true
case "time.now.year": case "time.now.year":
return strconv.Itoa(nowFunc().Year()), true return strconv.Itoa(nowFunc().Year()), true
case "time.now.unix":
return strconv.FormatInt(nowFunc().Unix(), 10), true
case "time.now.unix_ms":
return strconv.FormatInt(nowFunc().UnixNano()/int64(time.Millisecond), 10), true
} }
return nil, false return nil, false