Compare commits

..

253 Commits

Author SHA1 Message Date
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
Matthew Penner bc22102478 caddyfile: Fix caddy fmt nesting not decrementing (#4157)
* caddyfile(formatter): fix nesting not decrementing

This is an extremely weird edge-case where if you had a environment variable {}
on one line, a comment on the next line, and the closing of the block on the
following line; the rest of the Caddyfile would be indented further than it
should've been.

ref; https://github.com/matthewpi/vscode-caddyfile-support/issues/13

* run gofmt

* fmt: better way of handling edge case
2021-05-10 12:01:27 -06:00
Francis Lavoie f5db41ce1d encode: Drop prefer from Caddyfile (#4156)
Followup to #4150, #4151 /cc @ueffel @polarathene

After a bit of discussion with @mholt, we decided to remove `prefer` as a subdirective and just go with using the order implicitly always. Simpler config, simpler docs, etc.

Effectively changes 7776471 and reverts a small part of f35a7fa.
2021-05-10 11:12:59 -06:00
Francis Lavoie 77764714ad encode: Default to order the formats are enabled for prefer in Caddyfile (#4151) 2021-05-10 10:06:38 -06:00
Francis Lavoie 61642b766b caddytls: Run replacer on ask URL, for env vars (#4154)
Fixes #3922
2021-05-08 22:37:27 -06:00
Francis Lavoie 3cf443f0fe httpcaddyfile: Add grace_period global option (#4152)
See https://caddyserver.com/docs/json/apps/http/#grace_period
2021-05-07 16:18:17 -06:00
Francis Lavoie d4b2f1bcee caddyhttp: Fix fallback for the error handler chain (#4131)
* caddyhttp: Fix fallback for the error handler chain

The fix I went with in the end (after realizing some mistaken assumptions in #4131) is to just make the routes fall back to errorEmptyHandler instead of the non-error empty handler, if Terminal is true, making the routes error-aware. Ultimately this was probably just an oversight when errors was implemented at some point in the early betas of v2.

See https://caddy.community/t/problem-with-basicauth-handle-errors/12243/9 for context.

* Revert "caddyhttp: Fix fallback for the error handler chain"

This reverts commit 95b6ac44a6.

* caddyhttp: Fix via `routes.go`
2021-05-05 15:55:40 -06:00
Matthew Holt a17c3b568d reverseproxy: Minor logging improvements 2021-05-05 14:52:24 -06:00
Francis Lavoie 74f5d66c48 fileserver: Fix file matcher with empty try_files (#4147)
* fileserver: Fix `file` matcher with empty `try_files`

Fixes https://github.com/caddyserver/caddy/issues/4146

If `TryFiles` is empty, we fill it with `r.URL.Path`. In this case, this is `/`. Then later, in `prepareFilePath()`, we run the replacer (which turns `{path}` into `/` at that point) but `file` remains the original value (and the placeholder is still the placeholder there).

So then `strings.HasSuffix(file, "/")` will be `false` for the placeholder, but `true` for the empty `TryFiles` codepath, because `file` was `/` due to being set to the actual request value beforehand.

This means that `suffix` becomes `//` in that case, so after `sanitizedPathJoin`, it becomes `./`, so `strictFileExists`'s `strings.HasSuffix(file, separator)` codepath will return true.

I think we should change the `m.TryFiles == nil` codepath to `m.TryFiles = []string{"{http.request.uri.path}"}` for consistency. (And maybe consider hoisting this to `Provision` cause there's no point doing this on every request). I don't think this "optimization" of directly using `r.URL.Path` is so valuable, cause it causes this edgecase with directories.

* Update modules/caddyhttp/fileserver/matcher.go

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

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2021-05-04 09:49:13 -06:00
Matthew Holt efe84497d7 go.mod: CertMagic v0.13.1 2021-05-03 11:10:18 -06:00
Francis Lavoie e4a22de9d1 reverseproxy: Add handle_response blocks to reverse_proxy (#3710) (#4021)
* reverseproxy: Add `handle_response` blocks to `reverse_proxy` (#3710)

* reverseproxy: complete handle_response test

* reverseproxy: Change handle_response matchers to use named matchers

reverseproxy: Add support for changing status code

* fastcgi: Remove obsolete TODO

We already have d.Err("transport already specified") in the reverse_proxy parsing code which covers this case

* reverseproxy: Fix support for "4xx" type status codes

* Apply suggestions from code review

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

* caddyhttp: Reorganize response matchers

* reverseproxy: Reintroduce caddyfile.Unmarshaler

* reverseproxy: Add comment mentioning Finalize should be called

Co-authored-by: Maxime Soulé <btik-git@scoubidou.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2021-05-02 12:39:06 -06:00
Jesse e6f6d3a476 cmd: Add --envfile flag to start command (#4141)
* add envfile in start cmd

* fix commandfuncs

* fix commandfuncs

* fix cmdStart envfile from bool to string

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2021-05-02 12:38:16 -06:00
Francis Lavoie ef7f15f3a4 httpcaddyfile: Add auto_https ignore_loaded_certs (#4077) 2021-05-02 12:11:27 -06:00
Francis Lavoie 6e0e3e1537 httpcaddyfile: Add global option for storage_clean_interval (#4134)
Followup to https://github.com/caddyserver/caddy/commit/42b7134ffa3bf3e9e86514c82407979c2627a5ab
2021-05-02 11:57:28 -06:00
Calvin Xiao 53ececda21 caddyhttp: performance improvement in HeaderRE Matcher (#4143)
Below is the report using `benchstat` and cmd:

`go test -run=BenchmarkHeaderREMatcher -bench=BenchmarkHeaderREMatcher -benchmem -count=10`

```
name                old time/op    new time/op    delta
HeaderREMatcher-16     869ns ± 1%     658ns ± 0%  -24.29%  (p=0.000 n=10+10)

name                old alloc/op   new alloc/op   delta
HeaderREMatcher-16      144B ± 0%      112B ± 0%  -22.22%  (p=0.000 n=10+10)

name                old allocs/op  new allocs/op  delta
HeaderREMatcher-16      7.00 ± 0%      5.00 ± 0%  -28.57%  (p=0.000 n=10+10)
```
2021-05-02 10:35:28 -06:00
Jason Du 637fd8f67b fileserver: Share template logic for both templates and file_server browse (#4093)
Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2021-04-30 22:17:23 -04:00
Matt Holt 956f01163d caddytls: Implement remote IP connection matcher (#4123)
* caddytls: Implement remote IP connection matcher

* Implement IP range negation

If both Ranges and NotRanges are specified, both must match.
2021-04-30 10:14:52 -06:00
Alban Lecocq ff6ca577ec httpcaddyfile: Fix unexpectedly removed policy (#4128)
* httpcaddyfile: Fix unexpectedly removed policy

When user set on_demand tls option in a catch-all (:443) policy,
we expect other policies to not have the on_demand enabled
See ex in tls_automation_policies_5.txt

Btw, we can remove policies if they are **all** empty.

* Update caddyconfig/httpcaddyfile/tlsapp.go

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

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2021-04-29 10:56:01 -06:00
Simão Gomes Viana 9017557169 reverseproxy: fix hash selection policy (#4137)
* caddyhttp: reverseproxy: fix hash selection policy

Fixes: #4135
Test: go test './...' -count=1

* caddyhttp: reverseproxy: add test to catch #4135

If you revert the last commit, the test will fail.
2021-04-29 10:52:22 -06:00
Francis Lavoie 3a1e81dbf6 fileserver: Better handling of HTTP status override (#4132) 2021-04-29 02:01:48 -04:00
Francis Lavoie a8d45277ca caddyfile: Fix import replacing unrelated placeholders (#4129)
* caddyfile: Fix `import` replacing unrelated placeholders

See https://caddy.community/t/snippet-issue-works-outside-snippet/12231

So it turns out that `NewReplacer()` gives a replacer with some global defaults (like `{env.*}` and some system and time placeholders), which is not ideal when running `import` because we just want to replace `{args.*}` only, and nothing else.

* caddyfile: Add test
2021-04-22 18:29:04 -06:00
Francis Lavoie 1e218e1d2e caddytls: Add load_storage module (#4055)
An idea that came up in https://caddy.community/t/save-internally-issued-wildcard-certificate-in-consul/11740, this a simple module that might be useful for anyone who uses storage modules that aren't filesystem, to let them load certs/keys externally issued for use by Caddy.

Bit goofy, since we need to fetch the certmagic.Storage during provisioning, it needs a wrapping struct instead of just being an array like `load_files`.

Future work might involve adding Caddyfile support via a subdirective of the `tls` directive maybe?
2021-04-21 17:05:55 -06:00
Francis Lavoie 4d0474e3b8 reverseproxy: Admin endpoint for reporting upstream statuses (#4125) 2021-04-21 13:43:34 -06:00
Francis Lavoie d789596bc0 caddyhttp: Implement better logic for inserting the HTTP->HTTPS redirs (#4033)
* caddyhttp: Implement better logic for inserting the HTTP->HTTPS redirs

* caddyhttp: Add integration test
2021-04-19 19:54:12 -06:00
Matthew Holt 96bb365929 httpcaddyfile: Take into account host scheme/port (fix #4113) 2021-04-16 11:17:22 -06:00
Mohammed Al Sahaf 00e12aa918 fuzz: fix the FuzzFormat comparison (#4117) 2021-04-15 23:03:54 +00:00
Matthew Holt 2250920e1d caddytls: Disable OCSP stapling for manual certs (#4064) 2021-04-12 16:09:02 -06:00
Matthew Holt 42b7134ffa caddytls: Configurable storage clean interval
Can drastically reduce costs on storage backends where scans are expensive.

Also reduced default interval to 24h.

See https://github.com/silinternational/certmagic-storage-dynamodb/issues/18
2021-04-12 15:41:22 -06:00
Mohammed Al Sahaf 3903642aa7 caddyfile: reject cyclic imports (#4022)
* caddyfile: reject recursive self-imports

* caddyfile: detect and reject cyclic imports of snippets and files

* caddyfile: do not be stickler about connected nodes not being connected already

* caddyfile: include missing test artifacts of cyclic imports

* address review comments
2021-04-09 12:06:25 -06:00
Mohammed Al Sahaf 03b5debd95 ci: fuzz: add 4 more fuzzing targets (#4105) 2021-04-08 11:45:19 -06:00
Francis Lavoie 3f6283b385 fileserver: Add status code override (#4076)
After reading a question about the `handle_response` feature of `reverse_proxy`, I realized that we didn't have a way of serving an arbitrary file with a status code other than 200. This is an issue in situations where you want to serve a custom error page in routes that are not errors, like the aforementioned `handle_response`, where you may want to retain the status code returned by the proxy but write a response with content from a file.

This feature is super simple, basically if a status code is configured (can be a status code number, or a placeholder string) then that status will be written out before serving the file - if we write the status code first, then the stdlib won't write its own (only the first HTTP status header wins).
2021-04-08 11:09:12 -06:00
Carl George 45fb7202ac notify: Send all sd_notify signals from main caddy process (#4060)
Initial sd_notify support was added in #3963, but that sent signals from
both cmdRun and cmdReload.  This approach has two drawbacks:

- Reloads initiated via the API do not send signals.
- The signals are sent from different processes, which requires the
  `NotifyAccess=exec` directive in the unit file.

This change moves the NotifyReloading and NotifyReadiness invocations to
Load, which address both of those drawbacks.  It also adds a
complimentary NotifyStopping method which is invoked from handleStop.
All the notify methods are defined in a notify package to avoid an
import loop.
2021-04-05 14:01:20 -06:00
Marten Seemann 66783eb4d9 go.mod: Update quic-go to v0.20.1 (#4075) 2021-04-05 13:09:30 -06:00
Francis Lavoie 1455d6bb69 httpcaddyfile: Fix panic in automation policy consolidation (#4104)
* httpcaddyfile: Add reproduce test

* httpcaddyfile: Don't allow `i` to go below zero
2021-04-02 16:47:04 -06:00
Francis Lavoie 3401f91dbe caddyfile: Normalize line endings before comparing fmt result (#4103) 2021-04-02 11:55:34 -06:00
Mohammed Al Sahaf eb3955a960 ci: accommodate go1.16 changes to go mod (#4102)
As of go1.16, the `go` commands will no longer make automatic changes to go.{mod,sum} files (see: https://blog.golang.org/go116-module-changes). This broke the release script which relied on `go mod download` and/or `go build` to automatically generate the go.sum file. This commit explicitly invokes `go mod tidy` to have the go.sum file generated.
2021-04-01 16:07:32 -06:00
Matthew Holt d21e88ae3a Minor tweaks 2021-04-01 12:49:51 -06:00
Matthew Holt a0a7c60cb9 go.mod: Use latest CertMagic 2021-04-01 12:48:59 -06:00
Matthew Holt 7da9241fd7 Use 600 instead of 644 for UUID file
Are you happy, linter?
2021-04-01 12:12:28 -06:00
Matthew Holt e68dbe9cf8 Change os to ioutil for now
My editor automatically changed ioutil.ReadFile() to os.ReadFile() in accordance
with Go 1.16 changes. I didn't notice this until pushing.

But we still have to support Go 1.15 for a little while.
2021-03-30 15:42:04 -06:00
Dimitri Masson bd357bf005 reverseproxy: Set cookie path to / when using cookie lb_policy (#4096) 2021-03-30 15:29:00 -06:00
Matthew Holt aac1ccf12d caddy: Add InstanceID() method
Caddy can now generate and persist its own instance ID, a UUID that is stored in
the data directory.

This makes it possible to differentiate it from other instances in a cluster.
2021-03-30 14:15:20 -06:00
Steffen Brüheim f35a7fa466 encode,staticfiles: Content negotiation, precompressed files (#4045)
* encode: implement prefer setting

* encode: minimum_length configurable via caddyfile

* encode: configurable content-types which to encode

* file_server: support precompressed files

* encode: use ReponseMatcher for conditional encoding of content

* linting error & documentation of encode.PrecompressedOrder

* encode: allow just one response matcher

also change the namespace of the encoders back, I accidently changed to precompressed >.>
default matchers include a *  to match to any charset, that may be appended

* rounding of the PR

* added integration tests for new caddyfile directives
* improved various doc strings (punctuation and typos)
* added json tag for file_server precompress order and encode matcher

* file_server: add vary header, remove accept-ranges when serving precompressed files

* encode: move Suffix implementation to precompressed modules
2021-03-29 18:47:19 -06:00
Francis Lavoie 75f797debd reverseproxy: Implement health_uri, deprecate health_path, supports query (#4050)
* reverseproxy: Implement health_uri, replaces health_path, supports query

Also fixes a bug with `health_status` Caddyfile parsing , it would always only take the first character of the status code even if it didn't end with "xx".

* reverseproxy: Rename to URI, named logger, warn in Provision (for JSON)
2021-03-29 18:36:40 -06:00
Simão Gomes Viana 1c8ea00828 go.mod: Migrate to golang.org/x/term (#4073)
golang.org/x/crypto/ssh/terminal is deprecated in favor of golang.org/x/term

See https://github.com/caddyserver/caddy/pull/4073/checks?check_run_id=2152150495
Error: SA1019: package golang.org/x/crypto/ssh/terminal is deprecated: this package moved to golang.org/x/term.  (staticcheck)

See https://github.com/caddyserver/caddy/pull/4073/checks?check_run_id=2152228516
Error: SA1019: package golang.org/x/crypto/ssh/terminal is deprecated: this package moved to golang.org/x/term.  (staticcheck)

Test: go test -count=1 './...'
2021-03-29 12:39:08 -06:00
Simão Gomes Viana d63d5ae1ce caddyhttp: improve grammar of comment for AllowH2C (#4072) 2021-03-29 12:04:25 -06:00
Simão Gomes Viana a6bc58153b sigtrap_posix: add missing comma to SIGTERM info (#4078)
Was missing a comma, so added it
2021-03-29 11:04:22 -06:00
Francis Lavoie 911c8a371a cmd: Use formatted logger for config adapter warnings (#4080) 2021-03-29 11:03:50 -06:00
Simão Gomes Viana 87fbc0783a cmd: main: fix minor doc typos (#4082)
Fixed typos in the documentation
2021-03-29 11:02:21 -06:00
Francis Lavoie f1c36680fc headers: Fix Caddyfile parsing for request_header with matchers (#4085) 2021-03-29 10:55:29 -06:00
Simão Gomes Viana a87f757fcc .gitignore: add IDE files (#4087)
These tend to clutter up repositories.
2021-03-29 10:54:38 -06:00
Francis Lavoie 0018b9be0d fileserver: Add a few more debug lines (#4063) 2021-03-19 11:42:26 -06:00
rai a48c6205b7 fileserver: Browse listing supports dark mode (#4066)
* Add dark color scheme media query

* Theme search box, make everything less contrasting

* Further contrast tweaks
2021-03-19 11:41:02 -06:00
Simão Gomes Viana 28a4159933 CONTRIBUTING: fix spelling (#4070)
Minor spelling fixes to make this document even better
2021-03-19 07:37:43 -04:00
Francis Lavoie 0d7fe36007 httpcaddyfile: Add error directive for the existing handler (#4034)
* httpcaddyfile: Add `error` directive for the existing handler

* httpcaddyfile: Move `error` to the end of the order
2021-03-12 13:25:49 -07:00
Aaron Taylor f137b82227 logging: add replace filter for static value replacement (#4029)
This filter is intended to be useful in scenarios where you may want to
redact a value with a static string, giving you information that the
field did previously exist and was present, but not revealing the value
itself in the logs.

This was inspired by work on adding more complete support for removing
sensitive values from logs [1]. An example use case would be the
Authorization header in request log output, for which the value should
usually not be logged, but it may be quite useful for debugging to
confirm that the header was present in the request.

[1] https://github.com/caddyserver/caddy/issues/3958
2021-03-12 13:01:34 -07:00
Aaron Taylor 2a127ac3d1 caddyconfig: add global option for configuring loggers (#4028)
This change is aimed at enhancing the logging module within the
Caddyfile directive to allow users to configure logs other than the HTTP
access log stream, which is the current capability of the Caddyfile [1].
The intent here is to leverage the same syntax as the server log
directive at a global level, so that similar customizations can be added
without needing to resort to a JSON-based configuration.

Discussion for this approach happened in the referenced issue.

Closes https://github.com/caddyserver/caddy/issues/3958

[1] https://caddyserver.com/docs/caddyfile/directives/log
2021-03-12 13:00:02 -07:00
Rajat Jain 802f80c382 map: Accept regex substitution in outputs (#3991)
* Replace placeholders with regex groups

* using Matcher methods

* test added

* linting fix

* Revert "linting fix"

This reverts commit cafd7296f4.

* Revert "test added"

This reverts commit 3a76cc7b0b.

* Revert "using Matcher methods"

This reverts commit cc34337b8e.

* tests added
2021-03-10 14:22:33 -07:00
Francis Lavoie 51f35ba03f reverseproxy: Fix upstreams with placeholders with no port (#4046) 2021-03-03 10:12:31 -07:00
Matthew Holt ad8d01cb66 rewrite: Implement regex path replacements
https://caddy.community/t/collapsing-multiple-forward-slashes-in-path-only/11626
2021-03-01 18:27:59 -07:00
Matthew Holt 5bf0a55df4 fileserver: Don't replace in request paths (fix #4027) 2021-03-01 13:49:13 -07:00
Matthew Holt ec309c6d52 caddypki: Add SignWithRoot option for ACME server
See https://caddy.community/t/setting-up-a-caddy-pki-based-on-a-windows-
root-ca-was-getting-pki-config/11616/7

Also improved a godoc comment in the caddytls package.
2021-02-26 19:27:58 -07:00
Matthew Holt ce5a0934a8 reverseproxy: Fix round robin data race (#4038) 2021-02-25 09:41:52 -07:00
Matthew Holt b54fa41239 Update docs; commit setcap.sh 2021-02-24 11:55:56 -07:00
Matthew Holt 427bbe99d0 go.mod: Latest CertMagic (updated libdns conventions) 2021-02-24 11:50:23 -07:00
Matthew Holt a8fdc0a998 core: Initialize logging before admin
We'll see how this goes.

https://caddy.community/t/calling-the-admin-api-from-within-a-route-route-causes-a-deadlock/11319
2021-02-23 12:57:10 -07:00
Matthew Holt f6bb02b303 caddytls: Remove old asset migration code (close #3894) 2021-02-22 15:19:35 -07:00
Matt Holt 6722ae3a83 reverseproxy: Add duration/latency placeholders (close #4012) (#4013)
* reverseproxy: Add duration/latency placeholders (close #4012) (and #2268)

Adds 4 placeholders, one is actually outside reverse proxy though:

{http.request.duration} is how long since the server decoded the HTTP request (headers).
{http.reverse_proxy.upstream.latency} is how long it took a proxy upstream to write the response header.
{http.reverse_proxy.upstream.duration} is total time proxying to the upstream, including writing response body to client.
{http.reverse_proxy.duration} is total time spent proxying, including selecting an upstream and retries.

Obviously, most of these are only useful at the end of a request, like when writing response headers or logs.

See also: https://caddy.community/t/any-equivalent-of-request-time-and-upstream-header-time-from-nginx/11418

* Add new placeholders to documentation
2021-02-22 11:57:21 -07:00
Matthew Holt edb362aa96 httpcaddyfile: Fix catch-all site block sorting
A site block that has a catch-all and the shortest address is now sorted better.

https://caddy.community/t/caddy-suddenly-directs-my-site-to-the-wrong-directive/11597/2
2021-02-22 11:15:11 -07:00
Francis Lavoie 5376e5113e ci: Build and test on Go 1.16, bump minimum to 1.15 (#4024)
* ci: Build and test on Go 1.16

* ci: Drop Go 1.14 support
2021-02-18 07:09:49 -05:00
Matthew Holt ec3ac840cf caddy: Support SetReadBuffer and SyscallConn for QUIC (fix #3998)
Supersedes #3999
2021-02-16 14:05:31 -07:00
Matthew Holt fbd00e4b53 Improve security warnings 2021-02-16 14:05:31 -07:00
Francis Lavoie bafb562991 httpcaddyfile: Configure other apps from global options (#3990) 2021-02-15 20:10:27 -07:00
Matthew Holt ed678235a4 cmd: Clean up build-info and upgrade output 2021-02-15 12:07:55 -07:00
Matthew Holt cc63c5805e caddyhttp: Support placeholders in header matcher values (close #3916) 2021-02-11 16:27:09 -07:00
Matthew Holt 51e3fdba77 caddytls: Save email with account if not already specified
I'm pretty sure this fixes a bug when the default email is used...
2021-02-10 19:49:23 -07:00
Matthew Holt 5ef76ff3e6 reverseproxy: Response buffering & configurable buffer size
Proxy response bodies can now be buffered, and the size of the request body and
response body buffer can be limited. Any remaining content that doesn't fit in the
buffer will remain on the wire until it can be read; i.e. bodies are not truncated,
even if the buffer is not big enough.

This fulfills a customer requirement. This was made possible by their sponsorship!
2021-02-09 14:15:04 -07:00
Matthew Holt 653a0d3f6b httpcaddyfile: Fix automation policies
Fixes a bug introduced in #3862
2021-02-08 11:06:19 -07:00
Mohammed Al Sahaf 0aefa7b047 ci: deflake integration tests (#3966)
* ci: deflake integration tests by pulling Caddy for the running config until new config is loaded
2021-02-05 15:36:52 +00:00
Matthew Holt 8c291298c9 httpcaddyfile: Add resolvers subdir of tls (close #4008)
Allows conveniently setting the resolvers for the DNS challenge using a TLS subdirective, which applies to default issuers, rather than having to explicitly define the issuers and overwrite the defaults.
2021-02-02 23:07:50 -07:00
Matthew Holt bf50d7010a acmeserver: Support custom CAs from Caddyfile
The HTTP Caddyfile adapter can now configure the PKI app, and the acme_server directive can now be used to specify a custom CA used for issuing certificates. More customization options can follow later as needed.
2021-02-02 17:23:52 -07:00
Matthew Holt 8ec90f1c40 caddyhttp: Check for invalid subdirectives of static_response
Ref: https://caddy.community/t/acme-server-implementation/11256/
2021-02-02 16:19:58 -07:00
Matthew Holt 90284e8017 httpcaddyfile: Fix default issuers when email provided
If `tls <email>` is used, we should apply that to all applicable default issuers, not drop them. This refactoring applies implicit ACME issuer settings from the tls directive to all default ACME issuers, like ZeroSSL.

We also consolidate some annoying logic and improve config validity checks.

Ref: https://caddy.community/t/error-obtaining-certificate-after-caddy-restart/11335/8
2021-02-02 16:17:26 -07:00
Matthew Holt 2772ede43c cmd: Add --force flag to reload command (close #4005)
Can be useful if user wants to reload manual certificates, for example.
2021-02-01 18:14:03 -07:00
Matthew Holt c986110678 httpcaddyfile: Warn if site address uses unspecified IP (close #4004) 2021-02-01 17:02:01 -07:00
Matthew Holt 55e49ff5c8 httpcaddyfile: Sort catch-all site blocks properly (fix #4003) 2021-02-01 11:45:28 -07:00
Mohammed Al Sahaf e2940c8c03 ci: update the command to run tests on the s390x machine (#3995) 2021-01-28 22:40:36 +00:00
Tyler Kropp bef80cd806 caddyhttp: Fix redir html status code, improve flow (#3987)
* Fix html redir code, improve flow

* Fix integer check error and add tests
2021-01-28 12:59:50 -07:00
Matt Holt e2c5c28597 caddyhttp: Implement handler abort; new 'abort' directive (close #3871) (#3983)
* caddyhttp: Implement handler abort; new 'abort' directive (close #3871)

* Move abort directive ordering; clean up redirects

Seems logical for the end-all of handlers to go at the... end.

The Connection header no longer needs to be set there, since Close is
true, and the static_response handler now does that.
2021-01-28 12:54:55 -07:00
Matt Holt ab80ff4fd2 admin: Identity management, remote admin, config loaders (#3994)
This commits dds 3 separate, but very related features:

1. Automated server identity management

How do you know you're connecting to the server you think you are? How do you know the server connecting to you is the server instance you think it is? Mutually-authenticated TLS (mTLS) answers both of these questions. Using TLS to authenticate requires a public/private key pair (and the peer must trust the certificate you present to it).

Fortunately, Caddy is really good at managing certificates by now. We tap into that power to make it possible for Caddy to obtain and renew its own identity credentials, or in other words, a certificate that can be used for both server verification when clients connect to it, and client verification when it connects to other servers. Its associated private key is essentially its identity, and TLS takes care of possession proofs.

This configuration is simply a list of identifiers and an optional list of custom certificate issuers. Identifiers are things like IP addresses or DNS names that can be used to access the Caddy instance. The default issuers are ZeroSSL and Let's Encrypt, but these are public CAs, so they won't issue certs for private identifiers. Caddy will simply manage credentials for these, which other parts of Caddy can use, for example: remote administration or dynamic config loading (described below).

2. Remote administration over secure connection

This feature adds generic remote admin functionality that is safe to expose on a public interface.

- The "remote" (or "secure") endpoint is optional. It does not affect the standard/local/plaintext endpoint.
- It's the same as the [API endpoint on localhost:2019](https://caddyserver.com/docs/api), but over TLS.
- TLS cannot be disabled on this endpoint.
- TLS mutual auth is required, and cannot be disabled.
- The server's certificate _must_ be obtained and renewed via automated means, such as ACME. It cannot be manually loaded.
- The TLS server takes care of verifying the client.
- The admin handler takes care of application-layer permissions (methods and paths that each client is allowed to use).\
- Sensible defaults are still WIP.
- Config fields subject to change/renaming.

3. Dyanmic config loading at startup

Since this feature was planned in tandem with remote admin, and depends on its changes, I am combining them into one PR.

Dynamic config loading is where you tell Caddy how to load its config, and then it loads and runs that. First, it will load the config you give it (and persist that so it can be optionally resumed later). Then, it will try pulling its _actual_ config using the module you've specified (dynamically loaded configs are _not_ persisted to storage, since resuming them doesn't make sense).

This PR comes with a standard config loader module called `caddy.config_loaders.http`.

Caddyfile config for all of this can probably be added later.

COMMITS:

* admin: Secure socket for remote management

Functional, but still WIP.

Optional secure socket for the admin endpoint is designed
for remote management, i.e. to be exposed on a public
port. It enforces TLS mutual authentication which cannot
be disabled. The default port for this is :2021. The server
certificate cannot be specified manually, it MUST be
obtained from a certificate issuer (i.e. ACME).

More polish and sensible defaults are still in development.

Also cleaned up and consolidated the code related to
quitting the process.

* Happy lint

* Implement dynamic config loading; HTTP config loader module

This allows Caddy to load a dynamic config when it starts.

Dynamically-loaded configs are intentionally not persisted to storage.

Includes an implementation of the standard config loader, HTTPLoader.
Can be used to download configs over HTTP(S).

* Refactor and cleanup; prevent recursive config pulls

Identity management is now separated from remote administration.

There is no need to enable remote administration if all you want is identity
management, but you will need to configure identity management
if you want remote administration.

* Fix lint warnings

* Rename identities->identifiers for consistency
2021-01-27 16:16:04 -07:00
Matt Holt 3366384d93 caddycmd: Add upgrade command (#3972)
Replaces the current Caddy executable with a new one from the build server. Honors custom builds, as long as plugins are registered on the Caddy website. Requires permissions to replace current executable, of course.

This is an experimental command that may get changed or removed later.
2021-01-19 18:45:49 -07:00
Matthew Holt 1ac6351705 Revert "requestbody: Allow overwriting remote address"
This reverts commit 0bf2046da7.

No actual use case.
2021-01-19 18:43:01 -07:00
Matthew Holt 160d199999 caddytest: Update Caddyfile tests for formatting, HTTP-only blocks
Previous commit improved the Caddyfile adapter so it doesn't unnecessarily add names to "skip" in "auto_https" when the server is already HTTP-only.

This commit updates the tests to reflect that change, while also fixing the Caddyfile formatting in many of the tests.

We also print the line number of the divergence between input and formatted version in Caddyfile adapt warnings - very useful for finding initial formatting problems.
2021-01-19 14:21:11 -07:00
Matthew Holt d68cff8eb6 httpcaddyfile: Skip TLS APs for HTTP-only hosts (fix #3977)
This is probably an invasive change, but existing tests continue to pass.
It seems to make sense this way. There is likely an edge case I haven't
considered.
2021-01-19 14:16:06 -07:00
Matthew Holt 8f6f9865d4 cmd: Print more detailed version with --environ 2021-01-16 12:52:33 -07:00
Matthew Holt 58e83a811b map: Add missing json struct tag 2021-01-16 09:56:06 -07:00
Sven Dowideit f0c0f38ba5 tests: use actual admin port value in error message (#3973)
Signed-off-by: Sven Dowideit <sven.dowideit@csiro.au>
2021-01-13 18:30:49 -07:00
Stefan Tatschner 59071ea15d cmd: Implement sd_notify() to notify systemd about readiness (#3963)
Issue: #3786
Based on Gaurav Dhameeja's work in #3908.
2021-01-12 14:38:53 -07:00
Matthew Holt 14f50d9dfb templates: Add fileExists and httpError template actions
The httpError function isn't particularly useful until https://github.com/golang/go/issues/34201 is fixed in the Go standard lib.
2021-01-11 13:49:20 -07:00
Matthew Holt 0bf2046da7 requestbody: Allow overwriting remote address
An experimental feature, let's see if it's useful.
2021-01-11 13:35:12 -07:00
go-d 88a38bd00d rewrite: Use RawPath instead of Path (fix #3596) (#3918)
Prevent information loss, i.e. the encoded form that was sent by the
client, when using URL strip/replace.
2021-01-11 09:18:53 -07:00
Matthew Holt 4f64105fbb Update docs 2021-01-08 16:00:36 -07:00
Matthew Holt 09432ba64d caddytls: Configurable OCSP stapling; global option (closes #3714)
Allows user to disable OCSP stapling (including support in the Caddyfile via the ocsp_stapling global option) or overriding responder URLs. Useful in environments where responders are not reachable due to firewalls.
2021-01-07 15:52:58 -07:00
Matthew Holt ef54483249 logging: Remove logfmt encoder (close #3575)
Has been deprecated for about 6 months now because it is broken.
2021-01-07 14:29:19 -07:00
Matthew Holt c2b91dbd65 httpcaddyfile: Support repeated use of cert_issuer global option
This changes the signature of UnmarshalGlobalFunc but this is probably OK since it's only used by this repo as far as we know.

We need this change in order to "remember" the previous value in case a global option appears more than once, which is now a possibility with the cert_issuer option since Caddy now supports multiple issuers in the order defined by the user.

Bonus: the issuer subdirective of tls now supports one-liner for "acme" when all you need to set is the directory:

issuer acme <dir>
2021-01-07 11:02:06 -07:00
Jordi Masip 8b6fdc04da caddytls: add 'key_type' subdirective (#3956)
* caddytls: add 'key_type' subdirective

* Suggested change

* *string -> string

* test
2021-01-06 12:02:58 -07:00
Matthew Holt f0216967dc caddyfile: Refactor unmarshaling of module tokens
Eliminates a fair amount of repeated code
2021-01-05 14:39:30 -07:00
Matthew Holt b1bec8c899 go.mod: Update CertMagic and acmez (improved IDN support) 2021-01-04 15:59:48 -07:00
yaxin 3c9256a1be reverseproxy: Caddyfile health check headers, host header support (#3948)
* reverse_proxy: 1.health check headers can be set through Caddyfile using health_headers directive; 2.health check header host can be set properly

* reverse_proxy:
replace example with syntax definition
inline health_headers directive parse function

* bugfix: change caddyfile_adapt testcase file from space to tab

* reverseproxy: modify health_header value document as optional and add more test cases
2021-01-04 11:26:18 -07:00
Matthew Holt 7846bc1e06 httpcaddyfile: Adjust iterator when removing AP (fix #3953) 2021-01-04 11:25:41 -07:00
Matt Holt 144b65cf99 cmd: Organize list-modules output; --packages flag (#3925) 2021-01-04 11:11:56 -07:00
Matt Holt c8557dc00b caddyfile: Introduce basic linting and fmt check (#3923)
* caddyfile: Introduce basic linting and fmt check

This will help encourage people to keep their Caddyfiles tidy.

* Remove unrelated tests

I am not sure that testing the output of warnings here is quite the
right idea; these tests are just for syntax and parsing success.
2021-01-04 11:11:36 -07:00
Mohammed Al Sahaf 1b453dd4fb ci: force fetch the upstream tags (#3947) 2020-12-30 21:02:54 +00:00
Dave Henderson ebc278ec98 metrics: allow disabling OpenMetrics negotiation (#3944)
* metrics: allow disabling OpenMetrics negotiation

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

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

* ci: Update comments, remove env TODO

* ci: Fix Cloudsmith installation by setting PATH

* docs: Add Cloudsmith attribution to README

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

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

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

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

* implement null header matcher syntax

* avoid repeating magic !

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

* fastcgi: Avoid changing scriptName when not necessary

* Stylistic tweaks

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

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

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

- Also add error handling for StatusGatewayTimeout

* revert(caddy): Revert 504 handling

- This will potentially break load balancing and health checks

* Handle client cancellation as different error

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

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

* Implement UsagePool

* Remove unused context

* Fix initializing non-ACME CA

This will handle cases where a DB is not provided

* Sanitize acme db path and clean debug logs

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

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

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

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

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

* httpcaddyfile: Add listener wrapper support

* httpcaddyfile: Sort sbaddrs to make adapt output more deterministic

* httpcaddyfile: Add server options adapt tests

* httpcaddyfile: Windows line endings lol

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

* Update caddyconfig/httpcaddyfile/serveroptions.go

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

* httpcaddyfile: Reword listener address "matcher"

* Apply suggestions from code review

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

* httpcaddyfile: Deprecate experimental_http3 option (moved to servers)

* httpcaddyfile: Remove validation step, no longer needed

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

* reverseproxy: Use parentheses for condition

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

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

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

* Fix most of the staticcheck lint errors

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

* Fix the prealloc lint errors

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

* Fix the misspell lint errors

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

* Fix the varcheck lint errors

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

* Fix the errcheck lint errors

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

* Fix the bodyclose lint errors

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

* Fix the deadcode lint errors

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

* Fix the unused lint errors

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

* Fix the gosec lint errors

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

* Fix the gosimple lint errors

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

* Fix the ineffassign lint errors

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

* Fix the staticcheck lint errors

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

* Revert the misspell change, use a neutral English

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

* Remove broken golangci-lint CI job

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

* Re-add errantly-removed weakrand initialization

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

* don't break the loop and return

* Removing extra handling for null rootKey

* unignore RegisterModule/RegisterAdapter

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

* single-line log message

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

* Fix lint after a1808b0dbf209c615e438a496d257ce5e3acdce2 was merged

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

* Revert ticker change, ignore it instead

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

* Ignore some of the write errors

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

* Remove blank line

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

* Use lifetime

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

* close immediately

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

* Preallocate configVals

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

* Update modules/caddytls/distributedstek/distributedstek.go

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

* reverseproxy: Change some of the error levels, unsugar

* Use unsugared log in one spot

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

* add CookieHashSelection for session affinity

* register module

* reverse_proxy: Add and fix cookie lb_policy

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

* reverse_proxy: fix some comments

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

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

* reverse_proxy: Set a default secret is indeed useless

* reverse_proxy: add configuration syntax for cookie lb_policy

* reverse_proxy: doc typo and improvement

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

closes #3804

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

This has the following advantages over the previous attempt:

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

* add integration test for header directive in caddyfile

* bubble up errors when parsing caddyfile header directive

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

* fix response headers not passed in blocks

* caddyfile: fix clash when using default header in block

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

* caddyhttp: New idle_timeout default of 5m

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

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

* Refactor and cleanup with improvements

* More specific link

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

```
  request_body {
    max_size 10000000
  }
```

* Improve Caddyfile parser for request_body module

* Remove unnecessary `continue`

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

Defaults are Let's Encrypt and ZeroSSL.

There are probably bugs.

* Commit updated integration tests, d'oh

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

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

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

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

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

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

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

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

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

* Oops, forgot to run integration tests

* Make this one integration test OS-agnostic

* See if this appeases the Windows gods

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

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

* Update modules/caddyhttp/reverseproxy/caddyfile.go

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

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

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

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

This improves on that logic, but I am still not 100% satisfied with the
result since I think we need to also check if another site block is more
specific, like foo.example.tld, which might not have on-demand TLS
enabled, and make sure an automation policy gets created before the
more general policy with on-demand...
2020-10-22 12:40:23 -06:00
Matthew Holt 97caf368ee readme: Add zerossl 2020-10-19 10:44:46 -06:00
218 changed files with 13867 additions and 3689 deletions
+17 -14
View File
@@ -23,13 +23,13 @@ Other menu items:
### Contributing code
You can have a huge impact on the project by helping with its code. To contribute code to Caddy, open a [pull request](https://github.com/caddyserver/caddy/pulls) (PR). If you're new to our community, that's okay: **we gladly welcome pull requests from anyone, regardless of your native language or coding experience.** You can get familiar with Caddy's code base by using [code search at Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy/-/search).
You can have a huge impact on the project by helping with its code. To contribute code to Caddy, first submit or comment in an issue to discuss your contribution, then open a [pull request](https://github.com/caddyserver/caddy/pulls) (PR). If you're new to our community, that's okay: **we gladly welcome pull requests from anyone, regardless of your native language or coding experience.** You can get familiar with Caddy's code base by using [code search at Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy).
We hold contributions to a high standard for quality :bowtie:, so don't be surprised if we ask for revisions&mdash;even if it seems small or insignificant. Please don't take it personally. :blue_heart: If your change is on the right track, we can guide you to make it mergable.
Here are some of the expectations we have of contributors:
- **Open an issue to propose your change first.** This way we can avoid confusion, coordinate what everyone is working on, and ensure that any changes are in-line with the project's goals and the best interests of its users. We can also discuss the best possible implementation. If there's already an issue about it, comment on the existing issue to claim it.
- **Open an issue to propose your change first.** This way we can avoid confusion, coordinate what everyone is working on, and ensure that any changes are in-line with the project's goals and the best interests of its users. We can also discuss the best possible implementation. If there's already an issue about it, comment on the existing issue to claim it. A lot of valuable time can be saved by discussing a proposal first.
- **Keep pull requests small.** Smaller PRs are more likely to be merged because they are easier to review! We might ask you to break up large PRs into smaller ones. [An example of what we want to avoid.](https://twitter.com/iamdevloper/status/397664295875805184)
@@ -45,16 +45,18 @@ Here are some of the expectations we have of contributors:
- **Use comments properly.** We expect good godoc comments for package-level functions, types, and values. Comments are also useful whenever the purpose for a line of code is not obvious.
- **Pull requests may still get closed.** The longer a PR stays open and idle, the more likely it is to be closed. If we haven't reviewed it in a while, it probably means the change is not a priority. Please don't take this personally, we're trying to balance a lot of tasks! If nobody else has commented or reacted to the PR, it likely means your change is useful only to you. The reality is this happens quite a bit. We don't tend to accept PRs that aren't generally helpful. For these reasons or others, the PR may get closed even after a review. We are not obligated to accept all proposed changes, even if the best justification we can give is something vague like, "It doesn't sit right." Sometimes PRs are just the wrong thing or the wrong time. Because it is open source, you can always build your own modified version of Caddy with a change you need, even if we reject it in the official repo.
We often grant [collaborator status](#collaborator-instructions) to contributors who author one or more significant, high-quality PRs that are merged into the code base!
#### HOW TO MAKE A PULL REQUEST TO CADDY
Contributing to Go projects on GitHub is fun and easy. We recommend the following workflow:
Contributing to Go projects on GitHub is fun and easy. After you have proposed your change in an issue, we recommend the following workflow:
1. [Fork this repo](https://github.com/caddyserver/caddy). This makes a copy of the code you can write to.
2. If you don't already have this repo (caddyserver/caddy.git) repo on your computer, get it with `go get github.com/caddyserver/caddy/v2`.
2. If you don't already have this repo (caddyserver/caddy.git) repo on your computer, clone it down: `git clone https://github.com/caddyserver/caddy.git`
3. Tell git that it can push the caddyserver/caddy.git repo to your fork by adding a remote: `git remote add myfork https://github.com/<your-username>/caddy.git`
@@ -85,9 +87,9 @@ Many people on the forums could benefit from your experience and expertise, too.
Like every software, Caddy has its flaws. If you find one, [search the issues](https://github.com/caddyserver/caddy/issues) to see if it has already been reported. If not, [open a new issue](https://github.com/caddyserver/caddy/issues/new) and describe the bug, and somebody will look into it! (This repository is only for Caddy and its standard modules.)
**You can help stop bugs in their tracks!** Speed up the patch by identifying the bug in the code. This can sometimes be done by adding `fmt.Println()` statements (or similar) in relevant code paths to narrow down where the problem may be. It's a good way to [introduce yourself to the Go language](https://tour.golang.org), too.
**You can help us fix bugs!** Speed up the patch by identifying the bug in the code. This can sometimes be done by adding `fmt.Println()` statements (or similar) in relevant code paths to narrow down where the problem may be. It's a good way to [introduce yourself to the Go language](https://tour.golang.org), too.
Please follow the issue template so we have all the needed information. Unredacted&mdash;yes, actual values matter. We need to be able to repeat the bug using your instructions. Please simplify the issue as much as possible. The burden is on you to convince us that it is actually a bug in Caddy. This is easiest to do when you write clear, concise instructions so we can reproduce the behavior (even if it seems obvious). The more detailed and specific you are, the faster we will be able to help you!
We may reply with an issue template. Please follow the template so we have all the needed information. Unredacted&mdash;yes, actual values matter. We need to be able to repeat the bug using your instructions. Please simplify the issue as much as possible. If you don't, we might close your report. The burden is on you to make it easily reproducible and to convince us that it is actually a bug in Caddy. This is easiest to do when you write clear, concise instructions so we can reproduce the behavior (even if it seems obvious). The more detailed and specific you are, the faster we will be able to help you!
We suggest reading [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
@@ -98,11 +100,12 @@ Please be kind. :smile: Remember that Caddy comes at no cost to you, and you're
Maintainers---or more generally, developers---need three things to act on bugs:
1. To agree or be convinced that it's a bug (reporter's responsibility).
- A bug is undesired or surprising behavior which violates documentation or the spec.
- A bug is unintentional, undesired, or surprising behavior which violates documentation or relevant spec. It might be either a mistake in the documentation or a bug in the code.
- This project usually does not work around bugs in other software, systems, and dependencies; instead, we recommend that those bugs are fixed at their source. This sometimes means we close issues or reject PRs that attempt to fix, workaround, or hide bugs in other projects.
2. To be able to understand what is happening (mostly reporter's responsibility).
- If the reporter can provide satisfactory instructions such that a developer can reproduce the bug, the developer will likely be able to understand the bug, write a test case, and implement a fix.
- Otherwise, the burden is on the reporter to test possible solutions. This is discouraged because it loosens the feedback loop, slows down debugging efforts, obscures the true nature of the problem from the developers, and is unlikely to result in new test cases.
- If the reporter can provide satisfactory instructions such that a developer can reproduce the bug, the developer will likely be able to understand the bug, write a test case, and implement a fix. This is the least amount of work for everyone and path to the fastest resolution.
- Otherwise, the burden is on the reporter to test possible solutions. This is less preferable because it loosens the feedback loop, slows down debugging efforts, obscures the true nature of the problem from the developers, and is unlikely to result in new test cases.
3. A solution, or ideas toward a solution (mostly maintainer's responsibility).
- Sometimes the best solution is a documentation change.
@@ -112,7 +115,7 @@ Maintainers---or more generally, developers---need three things to act on bugs:
Thus, at the very least, the reporter is expected to:
1. Convince the reader that it's a bug (if it's not obvious).
1. Convince the reader that it's a bug in Caddy (if it's not obvious).
2. Reduce the problem down to the minimum specific steps required to reproduce it.
The maintainer is usually able to do the rest; but of course the reporter may invest additional effort to speed up the process.
@@ -123,7 +126,7 @@ The maintainer is usually able to do the rest; but of course the reporter may in
First, [search to see if your feature has already been requested](https://github.com/caddyserver/caddy/issues). If it has, you can add a :+1: reaction to vote for it. If your feature idea is new, open an issue to request the feature. Please describe your idea thoroughly so that we know how to implement it! Really vague requests may not be helpful or actionable and, without clarification, will have to be closed.
While we really do value your requests and implement many of them, not all features are a good fit for Caddy. Most of those [make good modules](#writing-a-caddy-module), which can be made by anyone! But if a feature is not in the best interest of the Caddy project or its users in general, we may politely decline to implement it into Caddy core.
While we really do value your requests and implement many of them, not all features are a good fit for Caddy. Most of those [make good modules](#writing-a-caddy-module), which can be made by anyone! But if a feature is not in the best interest of the Caddy project or its users in general, we may politely decline to implement it into Caddy core. Additionally, some features are bad ideas altogether (for either obvious or non-obvious reasons) which may be rejected. We'll try to explain why we reject a feature, but sometimes the best we can do is, "It's not a good fit for the project."
### Improving documentation
@@ -132,11 +135,11 @@ Caddy's documentation is available at [https://caddyserver.com/docs](https://cad
Note that third-party module documentation is not hosted by the Caddy website, other than basic usage examples. They are managed by the individual module authors, and you will have to contact them to change their documentation.
Our documentation is scoped to the Caddy project only: it is not for describing how other software or systems work, even if they relate to Caddy or web servers. That kind of content [can be found in our community wiki](https://caddy.community/c/wiki/13), however.
## Collaborator Instructions
Collaborators have push rights to the repository. We grant this permission after one or more successful, high-quality PRs are merged! We thank them for their help.The expectations we have of collaborators are:
Collaborators have push rights to the repository. We grant this permission after one or more successful, high-quality PRs are merged! We thank them for their help. The expectations we have of collaborators are:
- **Help review pull requests.** Be meticulous, but also kind. We love our contributors, but we critique the contribution to make it better. Multiple, thorough reviews make for the best contributions! Here are some questions to consider:
- Can the change be made more elegant?
@@ -167,7 +170,7 @@ Collaborators have push rights to the repository. We grant this permission after
## Values
## Values (WIP)
- A person is always more important than code. People don't like being handled "efficiently". But we can still process issues and pull requests efficiently while being kind, patient, and considerate.
+38 -6
View File
@@ -2,9 +2,6 @@
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
@@ -14,14 +11,49 @@ Please note that we consider publicly-registered domain names to be public infor
| 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
Please email Matt Holt (the author) directly: matt [at] ardanlabs [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.
If your report is valid and a patch is released, we will not reveal your identity by default. If you wish to be credited, please give us the name to use.
If your report is valid and a patch is released, we will not reveal your identity by default. If you wish to be credited, please give us the name to use and/or your GitHub username. If you don't provide this we can't credit you.
Thanks for responsibly helping Caddy&mdash;and thousands of websites&mdash;be more secure!
+6 -16
View File
@@ -6,9 +6,11 @@ on:
push:
branches:
- master
- 2.*
pull_request:
branches:
- master
- 2.*
jobs:
test:
@@ -17,7 +19,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
go: [ '1.14', '1.15' ]
go: [ '1.16', '1.17' ]
# Set some variables per OS, usable via ${{ matrix.VAR }}
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
@@ -64,6 +66,7 @@ jobs:
go env
printf "\n\nSystem environment:\n\n"
env
printf "Git version: $(git version)\n\n"
# Calculate the short SHA1 hash of the git commit
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
echo "::set-output name=go_cache::$(go env GOCACHE)"
@@ -137,7 +140,7 @@ jobs:
# The environment is fresh, so there's no point in keeping accepting and adding the key.
rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . caddy-ci@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t caddy-ci@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; CGO_ENABLED=0 /usr/local/go/bin/go test -v ./..."
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t caddy-ci@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -v ./..."
test_result=$?
# There's no need leaving the files around
@@ -148,25 +151,12 @@ jobs:
env:
SSH_KEY: ${{ secrets.S390X_SSH_KEY }}
# From https://github.com/reviewdog/action-golangci-lint
golangci-lint:
name: runner / golangci-lint
runs-on: ubuntu-latest
steps:
- name: Checkout code into the Go module directory
uses: actions/checkout@v2
- name: Run golangci-lint
uses: reviewdog/action-golangci-lint@v1
# uses: docker://reviewdog/action-golangci-lint:v1 # pre-build docker image
with:
github_token: ${{ secrets.github_token }}
goreleaser-check:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
- uses: goreleaser/goreleaser-action@v2
with:
version: latest
+3 -1
View File
@@ -4,9 +4,11 @@ on:
push:
branches:
- master
- 2.*
pull_request:
branches:
- master
- 2.*
jobs:
cross-build-test:
@@ -14,7 +16,7 @@ jobs:
fail-fast: false
matrix:
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
go: [ '1.14', '1.15' ]
go: [ '1.17' ]
runs-on: ubuntu-latest
continue-on-error: true
steps:
-73
View File
@@ -1,73 +0,0 @@
name: Fuzzing
on:
# Daily midnight fuzzing
schedule:
- cron: '0 0 * * *'
jobs:
fuzzing:
name: Fuzzing
strategy:
matrix:
os: [ ubuntu-latest ]
go: [ '1.14' ]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Checkout code
uses: actions/checkout@v2
- name: Download go-fuzz tools and the Fuzzit CLI, move Fuzzit CLI to GOBIN
# If we decide we need to prevent this from running on forks, we can use this line:
# if: github.repository == 'caddyserver/caddy'
run: |
go get -v github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
wget -q -O fuzzit https://github.com/fuzzitdev/fuzzit/releases/download/v2.4.77/fuzzit_Linux_x86_64
chmod a+x fuzzit
mv fuzzit $(go env GOPATH)/bin
echo "::add-path::$(go env GOPATH)/bin"
- name: Generate fuzzers & submit them to Fuzzit
continue-on-error: true
env:
FUZZIT_API_KEY: ${{ secrets.FUZZIT_API_KEY }}
SYSTEM_PULLREQUEST_SOURCEBRANCH: ${{ github.ref }}
BUILD_SOURCEVERSION: ${{ github.sha }}
run: |
# debug
echo "PR Source Branch: $SYSTEM_PULLREQUEST_SOURCEBRANCH"
echo "Source version: $BUILD_SOURCEVERSION"
declare -A fuzzers_funcs=(\
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="FuzzParseAddress" \
["./listeners_fuzz.go"]="FuzzParseNetworkAddress" \
["./replacer_fuzz.go"]="FuzzReplacer" \
)
declare -A fuzzers_targets=(\
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="parse-address" \
["./listeners_fuzz.go"]="parse-network-address" \
["./replacer_fuzz.go"]="replacer" \
)
fuzz_type="fuzzing"
for f in $(find . -name \*_fuzz.go); do
FUZZER_DIRECTORY=$(dirname "$f")
echo "go-fuzz-build func ${fuzzers_funcs[$f]} residing in $f"
go-fuzz-build -func "${fuzzers_funcs[$f]}" -o "$FUZZER_DIRECTORY/${fuzzers_targets[$f]}.zip" "$FUZZER_DIRECTORY"
fuzzit create job --engine go-fuzz caddyserver/"${fuzzers_targets[$f]}" "$FUZZER_DIRECTORY"/"${fuzzers_targets[$f]}.zip" --api-key "${FUZZIT_API_KEY}" --type "${fuzz_type}" --branch "${SYSTEM_PULLREQUEST_SOURCEBRANCH}" --revision "${BUILD_SOURCEVERSION}"
echo "Completed $f"
done
+25
View File
@@ -0,0 +1,25 @@
name: Lint
on:
push:
branches:
- master
- 2.*
pull_request:
branches:
- master
- 2.*
jobs:
# From https://github.com/golangci/golangci-lint-action
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.31
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true
+78 -4
View File
@@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
os: [ ubuntu-latest ]
go: [ '1.15' ]
go: [ '1.17' ]
runs-on: ${{ matrix.os }}
steps:
@@ -22,10 +22,19 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
# So GoReleaser can generate the changelog properly
- name: Unshallowify the repo clone
run: git fetch --prune --unshallow
# Force fetch upstream tags -- because 65 minutes
# tl;dr: actions/checkout@v2 runs this line:
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
# git fetch --prune --unshallow
# which doesn't overwrite that tag because that would be destructive.
# Credit to @francislavoie for the investigation.
# https://github.com/actions/checkout/issues/290#issuecomment-680260080
- name: Force fetch upstream tags
run: git fetch --tags --force
# https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
- name: Print Go version and environment
@@ -41,6 +50,9 @@ jobs:
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
echo "::set-output name=go_cache::$(go env GOCACHE)"
# Add "pip install" CLI tools to PATH
echo ~/.local/bin >> $GITHUB_PATH
# Parse semver
TAG=${GITHUB_REF/refs\/tags\//}
SEMVER_RE='[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z\.-]*\)'
@@ -53,6 +65,21 @@ jobs:
echo "::set-output name=tag_patch::${TAG_PATCH}"
echo "::set-output name=tag_special::${TAG_SPECIAL}"
# Cloudsmith CLI tooling for pushing releases
# See https://help.cloudsmith.io/docs/cli
- name: Install Cloudsmith CLI
run: pip install --upgrade cloudsmith-cli
- name: Validate commits and tag signatures
run: |
# Import Matt Holt's key
curl 'https://github.com/mholt.gpg' | gpg --import
echo "Verifying the tag: ${{ steps.vars.outputs.version_tag }}"
# tags are only accepted if signed by Matt's key
git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1
- name: Cache the build cache
uses: actions/cache@v2
with:
@@ -72,12 +99,59 @@ jobs:
TAG: ${{ steps.vars.outputs.version_tag }}
# Only publish on non-special tags (e.g. non-beta)
# We will continue to push to Gemfury for the forseeable future, although
# Cloudsmith is probably better, to not break things for existing users of Gemfury.
# See https://gemfury.com/caddy/deb:caddy
- name: Publish .deb to Gemfury
if: ${{ steps.vars.outputs.tag_special == '' }}
env:
GEMFURY_PUSH_TOKEN: ${{ secrets.GEMFURY_PUSH_TOKEN }}
run: |
for filename in dist/*.deb; do
# armv6 and armv7 are both "armhf" so we can skip the duplicate
if [[ "$filename" == *"armv6"* ]]; then
echo "Skipping $filename"
continue
fi
curl -F package=@"$filename" https://${GEMFURY_PUSH_TOKEN}:@push.fury.io/caddy/
done
# Publish only special tags (unstable/beta/rc) to the "testing" repo
# See https://cloudsmith.io/~caddy/repos/testing/
- name: Publish .deb to Cloudsmith (special tags)
if: ${{ steps.vars.outputs.tag_special != '' }}
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
run: |
for filename in dist/*.deb; do
# armv6 and armv7 are both "armhf" so we can skip the duplicate
if [[ "$filename" == *"armv6"* ]]; then
echo "Skipping $filename"
continue
fi
echo "Pushing $filename to 'testing'"
cloudsmith push deb caddy/testing/any-distro/any-version $filename
done
# Publish stable tags to Cloudsmith to both repos, "stable" and "testing"
# See https://cloudsmith.io/~caddy/repos/stable/
- name: Publish .deb to Cloudsmith (stable tags)
if: ${{ steps.vars.outputs.tag_special == '' }}
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
run: |
for filename in dist/*.deb; do
# armv6 and armv7 are both "armhf" so we can skip the duplicate
if [[ "$filename" == *"armv6"* ]]; then
echo "Skipping $filename"
continue
fi
echo "Pushing $filename to 'stable'"
cloudsmith push deb caddy/stable/any-distro/any-version $filename
echo "Pushing $filename to 'testing'"
cloudsmith push deb caddy/testing/any-distro/any-version $filename
done
+5 -1
View File
@@ -7,7 +7,7 @@ Caddyfile
*.prof
*.test
# build artifacts
# build artifacts and helpers
cmd/caddy/caddy
cmd/caddy/caddy.exe
@@ -21,3 +21,7 @@ vendor
dist
caddy-build
caddy-dist
# IDE files
.idea/
.vscode/
+52 -5
View File
@@ -1,21 +1,68 @@
linters-settings:
errcheck:
ignore: fmt:.*,io/ioutil:^Read.*,github.com/caddyserver/caddy/v2/caddyconfig:RegisterAdapter,github.com/caddyserver/caddy/v2:RegisterModule
ignore: fmt:.*,io/ioutil:^Read.*,go.uber.org/zap/zapcore:^Add.*
ignoretests: true
misspell:
locale: US
linters:
disable-all: true
enable:
- bodyclose
- prealloc
- unconvert
- deadcode
- errcheck
- gofmt
- goimports
- gosec
- gosimple
- govet
- ineffassign
- misspell
- prealloc
- staticcheck
- structcheck
- typecheck
- unconvert
- unused
- varcheck
# these are implicitly disabled:
# - asciicheck
# - depguard
# - dogsled
# - dupl
# - exhaustive
# - exportloopref
# - funlen
# - gci
# - gochecknoglobals
# - gochecknoinits
# - gocognit
# - goconst
# - gocritic
# - gocyclo
# - godot
# - godox
# - goerr113
# - gofumpt
# - goheader
# - golint
# - gomnd
# - gomodguard
# - goprintffuncname
# - interfacer
# - lll
# - maligned
# - nakedret
# - nestif
# - nlreturn
# - noctx
# - nolintlint
# - rowserrcheck
# - scopelint
# - sqlclosecheck
# - stylecheck
# - testpackage
# - unparam
# - whitespace
# - wsl
run:
# default concurrency is a available CPU number.
+20 -8
View File
@@ -11,6 +11,9 @@ before:
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
- go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod
# as of Go 1.16, `go` commands no longer automatically change go.{mod,sum}. We now have to explicitly
# run `go mod tidy`. The `/bin/sh -c '...'` is because goreleaser can't find cd in PATH without shell invocation.
- /bin/sh -c 'cd ./caddy-build && go mod tidy'
- git clone --depth 1 https://github.com/caddyserver/dist caddy-dist
- go mod download
@@ -76,7 +79,7 @@ nfpms:
homepage: https://caddyserver.com
maintainer: Matthew Holt <mholt@users.noreply.github.com>
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
formats:
@@ -84,13 +87,22 @@ nfpms:
# - rpm
bindir: /usr/bin
files:
./caddy-dist/init/caddy.service: /lib/systemd/system/caddy.service
./caddy-dist/init/caddy-api.service: /lib/systemd/system/caddy-api.service
./caddy-dist/welcome/index.html: /usr/share/caddy/index.html
./caddy-dist/scripts/completions/bash-completion: /etc/bash_completion.d/caddy
config_files:
./caddy-dist/config/Caddyfile: /etc/caddy/Caddyfile
contents:
- src: ./caddy-dist/init/caddy.service
dst: /lib/systemd/system/caddy.service
- src: ./caddy-dist/init/caddy-api.service
dst: /lib/systemd/system/caddy-api.service
- src: ./caddy-dist/welcome/index.html
dst: /usr/share/caddy/index.html
- src: ./caddy-dist/scripts/completions/bash-completion
dst: /etc/bash_completion.d/caddy
- src: ./caddy-dist/config/Caddyfile
dst: /etc/caddy/Caddyfile
type: config
scripts:
postinstall: ./caddy-dist/scripts/postinstall.sh
+40 -10
View File
@@ -1,6 +1,9 @@
<p align="center">
<a href="https://caddyserver.com"><img src="https://user-images.githubusercontent.com/1128849/36338535-05fb646a-136f-11e8-987b-e6901e717d5a.png" alt="Caddy" width="450"></a>
<br>
<h3 align="center">a <a href="https://zerossl.com"><img src="https://caddyserver.com/resources/images/zerossl-logo.svg" height="28" valign="middle"></a> project</h3>
</p>
<hr>
<h3 align="center">Every site on HTTPS</h3>
<p align="center">Caddy is an extensible server platform that uses TLS by default.</p>
<p align="center">
@@ -9,7 +12,9 @@
<br>
<a href="https://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" alt="@caddyserver on Twitter"></a>
<a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
<br>
<a href="https://sourcegraph.com/github.com/caddyserver/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/caddyserver/caddy/-/badge.svg" alt="Caddy on Sourcegraph"></a>
<a href="https://cloudsmith.io/~caddy/repos/"><img src="https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith" alt="Cloudsmith"></a>
</p>
<p align="center">
<a href="https://github.com/caddyserver/caddy/releases">Releases</a> ·
@@ -39,45 +44,64 @@
</p>
## Features
## [Features](https://caddyserver.com/v2)
- **Easy configuration** with the [Caddyfile](https://caddyserver.com/docs/caddyfile)
- **Powerful configuration** with its [native JSON config](https://caddyserver.com/docs/json/)
- **Dynamic configuration** with the [JSON API](https://caddyserver.com/docs/api)
- [**Config adapters**](https://caddyserver.com/docs/config-adapters) if you don't like JSON
- **Automatic HTTPS** by default
- [Let's Encrypt](https://letsencrypt.org) for public sites
- [ZeroSSL](https://zerossl.com) and [Let's Encrypt](https://letsencrypt.org) for public names
- Fully-managed local CA for internal names & IPs
- Can coordinate with other Caddy instances in a cluster
- Multi-issuer fallback
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
- **Production-ready** after serving trillions of requests and managing millions of TLS certificates
- **Scales to tens of thousands of sites** ... and probably more
- **HTTP/1.1, HTTP/2, and experimental HTTP/3** support
- **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat
- **Runs anywhere** with **no external dependencies** (not even libc)
- Written in Go, a language with higher **memory safety guarantees** than other servers
- Actually **fun to use**
- So, so much more to discover
- So, so much more to [discover](https://caddyserver.com/v2)
## Install
You can install Caddy by downloading from the Github Releases and placing it in your PATH.
The simplest, cross-platform way is to download from [GitHub Releases](https://github.com/caddyserver/caddy/releases) and place the executable file in your PATH.
For other install options, see https://caddyserver.com/docs/download.
For other install options, see https://caddyserver.com/docs/install.
## Build from source
Requirements:
- [Go 1.14 or newer](https://golang.org/dl/)
- [Go 1.16 or newer](https://golang.org/dl/)
### For development
_**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions in the next section._
```bash
$ git clone "https://github.com/caddyserver/caddy.git"
$ cd caddy/cmd/caddy/
$ go build
```
_**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions below._
When you run Caddy, it may try to bind to low ports unless otherwise specified in your config. If your OS requires elevated privileges for this, you will need to give your new binary permission to do so. On Linux, this can be done easily with: `sudo setcap cap_net_bind_service=+ep ./caddy`
If you prefer to use `go run` which only creates temporary binaries, you can still do this with the included `setcap.sh` like so:
```bash
$ go run -exec ./setcap.sh main.go
```
If you don't want to type your password for `setcap`, use `sudo visudo` to edit your sudoers file and allow your user account to run that command without a password, for example:
```
username ALL=(ALL:ALL) NOPASSWD: /usr/sbin/setcap
```
replacing `username` with your actual username. Please be careful and only do this if you know what you are doing! We are only qualified to document how to use Caddy, not Go tooling or your computer, and we are providing these instructions for convenience only; please learn how to use your own computer at your own risk and make any needful adjustments.
### With version information and/or plugins
@@ -93,7 +117,7 @@ $ xcaddy build
2. Change into it: `cd caddy`
3. Copy [Caddy's main.go](https://github.com/caddyserver/caddy/blob/master/cmd/caddy/main.go) into the empty folder. Add imports for any custom plugins you want to add.
4. Initialize a Go module: `go mod init caddy`
5. (Optional) Pin Caddy version: `go get github.com/caddyserver/caddy/v2@version` replacing `version` with a git tag or commit.
5. (Optional) Pin Caddy version: `go get github.com/caddyserver/caddy/v2@version` replacing `version` with a git tag, commit, or branch name.
6. (Optional) Add plugins by adding their import: `_ "import/path/here"`
7. Compile: `go build`
@@ -104,7 +128,7 @@ $ xcaddy build
The [Caddy website](https://caddyserver.com/docs/) has documentation that includes tutorials, quick-start guides, reference, and more.
**We recommend that all users do our [Getting Started](https://caddyserver.com/docs/getting-started) guide to become familiar with using Caddy.**
**We recommend that all users -- regardless of experience level -- do our [Getting Started](https://caddyserver.com/docs/getting-started) guide to become familiar with using Caddy.**
If you've only got a minute, [the website has several quick-start tutorials](https://caddyserver.com/docs/quick-starts) to choose from! However, after finishing a quick-start tutorial, please read more documentation to understand how the software works. 🙂
@@ -142,6 +166,8 @@ The docs are also open source. You can contribute to them here: https://github.c
- We **strongly recommend** that all professionals or companies using Caddy get a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed.
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way! If Caddy is benefitting your company, please consider a sponsorship! This not only helps fund full-time work to ensure the longevity of the project, it's also a great look for your company to your customers and potential customers!
- Individuals can exchange help for free on our community forum at https://caddy.community. Remember that people give help out of their spare time and good will. The best way to get help is to give it first!
Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only for bug reports and feature requests, i.e. actionable development items (support questions will usually be referred to the forums).
@@ -150,7 +176,11 @@ Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only
## About
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Light Code Labs, LLC.
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH.
- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_
- _Author on Twitter: [@mholt6](https://twitter.com/mholt6)_
Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company.
Debian package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that enables your organization to create, store and share packages in any format, to any place, with total confidence.
+486 -110
View File
@@ -17,6 +17,10 @@ package caddy
import (
"bytes"
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"expvar"
@@ -35,12 +39,12 @@ import (
"sync"
"time"
"github.com/caddyserver/caddy/v2/notify"
"github.com/caddyserver/certmagic"
"github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap"
)
// TODO: is there a way to make the admin endpoint so that it can be plugged into the HTTP app? see issue #2833
// AdminConfig configures Caddy's API endpoint, which is used
// to manage Caddy while it is running.
type AdminConfig struct {
@@ -58,54 +62,137 @@ type AdminConfig struct {
// If true, CORS headers will be emitted, and requests to the
// API will be rejected if their `Host` and `Origin` headers
// do not match the expected value(s). Use `origins` to
// customize which origins/hosts are allowed.If `origins` is
// customize which origins/hosts are allowed. If `origins` is
// not set, the listen address is the only value allowed by
// default.
// default. Enforced only on local (plaintext) endpoint.
EnforceOrigin bool `json:"enforce_origin,omitempty"`
// The list of allowed origins/hosts for API requests. Only needed
// if accessing the admin endpoint from a host different from the
// socket's network interface or if `enforce_origin` is true. If not
// set, the listener address will be the default value. If set but
// empty, no origins will be allowed.
// empty, no origins will be allowed. Enforced only on local
// (plaintext) endpoint.
Origins []string `json:"origins,omitempty"`
// Options related to configuration management.
// Options pertaining to configuration management.
Config *ConfigSettings `json:"config,omitempty"`
// Options that establish this server's identity. Identity refers to
// credentials which can be used to uniquely identify and authenticate
// this server instance. This is required if remote administration is
// enabled (but does not require remote administration to be enabled).
// Default: no identity management.
Identity *IdentityConfig `json:"identity,omitempty"`
// Options pertaining to remote administration. By default, remote
// administration is disabled. If enabled, identity management must
// also be configured, as that is how the endpoint is secured.
// See the neighboring "identity" object.
//
// EXPERIMENTAL: This feature is subject to change.
Remote *RemoteAdmin `json:"remote,omitempty"`
}
// ConfigSettings configures the, uh, configuration... and
// management thereof.
// ConfigSettings configures the management of configuration.
type ConfigSettings struct {
// Whether to keep a copy of the active config on disk. Default is true.
// Note that "pulled" dynamic configs (using the neighboring "load" module)
// are not persisted; only configs that are pushed to Caddy get persisted.
Persist *bool `json:"persist,omitempty"`
// Loads a configuration to use. This is helpful if your configs are
// managed elsewhere, and you want Caddy to pull its config dynamically
// when it starts. The pulled config completely replaces the current
// one, just like any other config load. It is an error if a pulled
// config is configured to pull another config.
//
// EXPERIMENTAL: Subject to change.
LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"`
// The interval to pull config. With a non-zero value, will pull config
// from config loader (eg. a http loader) with given interval.
//
// EXPERIMENTAL: Subject to change.
LoadInterval Duration `json:"load_interval,omitempty"`
}
// listenAddr extracts a singular listen address from ac.Listen,
// returning the network and the address of the listener.
func (admin AdminConfig) listenAddr() (NetworkAddress, error) {
input := admin.Listen
if input == "" {
input = DefaultAdminListen
}
listenAddr, err := ParseNetworkAddress(input)
if err != nil {
return NetworkAddress{}, fmt.Errorf("parsing admin listener address: %v", err)
}
if listenAddr.PortRangeSize() != 1 {
return NetworkAddress{}, fmt.Errorf("admin endpoint must have exactly one address; cannot listen on %v", listenAddr)
}
return listenAddr, nil
// IdentityConfig configures management of this server's identity. An identity
// consists of credentials that uniquely verify this instance; for example,
// TLS certificates (public + private key pairs).
type IdentityConfig struct {
// List of names or IP addresses which refer to this server.
// Certificates will be obtained for these identifiers so
// secure TLS connections can be made using them.
Identifiers []string `json:"identifiers,omitempty"`
// Issuers that can provide this admin endpoint its identity
// certificate(s). Default: ACME issuers configured for
// ZeroSSL and Let's Encrypt. Be sure to change this if you
// require credentials for private identifiers.
IssuersRaw []json.RawMessage `json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module"`
issuers []certmagic.Issuer
}
// RemoteAdmin enables and configures remote administration. If enabled,
// a secure listener enforcing mutual TLS authentication will be started
// on a different port from the standard plaintext admin server.
//
// This endpoint is secured using identity management, which must be
// configured separately (because identity management does not depend
// on remote administration). See the admin/identity config struct.
//
// EXPERIMENTAL: Subject to change.
type RemoteAdmin struct {
// The address on which to start the secure listener.
// Default: :2021
Listen string `json:"listen,omitempty"`
// List of access controls for this secure admin endpoint.
// This configures TLS mutual authentication (i.e. authorized
// client certificates), but also application-layer permissions
// like which paths and methods each identity is authorized for.
AccessControl []*AdminAccess `json:"access_control,omitempty"`
}
// AdminAccess specifies what permissions an identity or group
// of identities are granted.
type AdminAccess struct {
// Base64-encoded DER certificates containing public keys to accept.
// (The contents of PEM certificate blocks are base64-encoded DER.)
// Any of these public keys can appear in any part of a verified chain.
PublicKeys []string `json:"public_keys,omitempty"`
// Limits what the associated identities are allowed to do.
// If unspecified, all permissions are granted.
Permissions []AdminPermissions `json:"permissions,omitempty"`
publicKeys []crypto.PublicKey
}
// AdminPermissions specifies what kinds of requests are allowed
// to be made to the admin endpoint.
type AdminPermissions struct {
// The API paths allowed. Paths are simple prefix matches.
// Any subpath of the specified paths will be allowed.
Paths []string `json:"paths,omitempty"`
// The HTTP methods allowed for the given paths.
Methods []string `json:"methods,omitempty"`
}
// newAdminHandler reads admin's config and returns an http.Handler suitable
// for use in an admin endpoint server, which will be listening on listenAddr.
func (admin AdminConfig) newAdminHandler(addr NetworkAddress) adminHandler {
muxWrap := adminHandler{
enforceOrigin: admin.EnforceOrigin,
enforceHost: !addr.isWildcardInterface(),
allowedOrigins: admin.allowedOrigins(addr),
mux: http.NewServeMux(),
func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler {
muxWrap := adminHandler{mux: http.NewServeMux()}
// secure the local or remote endpoint respectively
if remote {
muxWrap.remoteControl = admin.Remote
} else {
muxWrap.enforceHost = !addr.isWildcardInterface()
muxWrap.allowedOrigins = admin.allowedOrigins(addr)
}
addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
@@ -197,18 +284,18 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
return allowed
}
// replaceAdmin replaces the running admin server according
// to the relevant configuration in cfg. If no configuration
// for the admin endpoint exists in cfg, a default one is
// used, so that there is always an admin server (unless it
// is explicitly configured to be disabled).
func replaceAdmin(cfg *Config) error {
// replaceLocalAdminServer replaces the running local admin server
// according to the relevant configuration in cfg. If no configuration
// for the admin endpoint exists in cfg, a default one is used, so
// that there is always an admin server (unless it is explicitly
// configured to be disabled).
func replaceLocalAdminServer(cfg *Config) error {
// always be sure to close down the old admin endpoint
// as gracefully as possible, even if the new one is
// disabled -- careful to use reference to the current
// (old) admin endpoint since it will be different
// when the function returns
oldAdminServer := adminServer
oldAdminServer := localAdminServer
defer func() {
// do the shutdown asynchronously so that any
// current API request gets a response; this
@@ -236,29 +323,35 @@ func replaceAdmin(cfg *Config) error {
}
// extract a singular listener address
addr, err := adminConfig.listenAddr()
addr, err := parseAdminListenAddr(adminConfig.Listen, DefaultAdminListen)
if err != nil {
return err
}
handler := adminConfig.newAdminHandler(addr)
handler := adminConfig.newAdminHandler(addr, false)
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
if err != nil {
return err
}
adminServer = &http.Server{
serverMu.Lock()
localAdminServer = &http.Server{
Addr: addr.String(), // for logging purposes only
Handler: handler,
ReadTimeout: 10 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1024 * 64,
}
serverMu.Unlock()
adminLogger := Log().Named("admin")
go func() {
if err := adminServer.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))
}
}()
@@ -276,6 +369,257 @@ func replaceAdmin(cfg *Config) error {
return nil
}
// manageIdentity sets up automated identity management for this server.
func manageIdentity(ctx Context, cfg *Config) error {
if cfg == nil || cfg.Admin == nil || cfg.Admin.Identity == nil {
return nil
}
// set default issuers; this is pretty hacky because we can't
// import the caddytls package -- but it works
if cfg.Admin.Identity.IssuersRaw == nil {
cfg.Admin.Identity.IssuersRaw = []json.RawMessage{
json.RawMessage(`{"module": "zerossl"}`),
json.RawMessage(`{"module": "acme"}`),
}
}
// load and provision issuer modules
if cfg.Admin.Identity.IssuersRaw != nil {
val, err := ctx.LoadModule(cfg.Admin.Identity, "IssuersRaw")
if err != nil {
return fmt.Errorf("loading identity issuer modules: %s", err)
}
for _, issVal := range val.([]interface{}) {
cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer))
}
}
// 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")
cmCfg := cfg.Admin.Identity.certmagicConfig(logger, true)
// issuers have circular dependencies with the configs because,
// as explained in the caddytls package, they need access to the
// correct storage and cache to solve ACME challenges
for _, issuer := range cfg.Admin.Identity.issuers {
// avoid import cycle with caddytls package, so manually duplicate the interface here, yuck
if annoying, ok := issuer.(interface{ SetConfig(cfg *certmagic.Config) }); ok {
annoying.SetConfig(cmCfg)
}
}
// obtain and renew server identity certificate(s)
return cmCfg.ManageAsync(ctx, cfg.Admin.Identity.Identifiers)
}
// replaceRemoteAdminServer replaces the running remote admin server
// according to the relevant configuration in cfg. It stops any previous
// remote admin server and only starts a new one if configured.
func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
if cfg == nil {
return nil
}
remoteLogger := Log().Named("admin.remote")
oldAdminServer := remoteAdminServer
defer func() {
if oldAdminServer != nil {
go func(oldAdminServer *http.Server) {
err := stopAdminServer(oldAdminServer)
if err != nil {
Log().Named("admin").Error("stopping current secure admin endpoint", zap.Error(err))
}
}(oldAdminServer)
}
}()
if cfg.Admin == nil || cfg.Admin.Remote == nil {
return nil
}
addr, err := parseAdminListenAddr(cfg.Admin.Remote.Listen, DefaultRemoteAdminListen)
if err != nil {
return err
}
// make the HTTP handler but disable Host/Origin enforcement
// because we are using TLS authentication instead
handler := cfg.Admin.newAdminHandler(addr, true)
// create client certificate pool for TLS mutual auth, and extract public keys
// so that we can enforce access controls at the application layer
clientCertPool := x509.NewCertPool()
for i, accessControl := range cfg.Admin.Remote.AccessControl {
for j, certBase64 := range accessControl.PublicKeys {
cert, err := decodeBase64DERCert(certBase64)
if err != nil {
return fmt.Errorf("access control %d public key %d: parsing base64 certificate DER: %v", i, j, err)
}
accessControl.publicKeys = append(accessControl.publicKeys, cert.PublicKey)
clientCertPool.AddCert(cert)
}
}
// create TLS config that will enforce mutual authentication
cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger, false)
tlsConfig := cmCfg.TLSConfig()
tlsConfig.NextProtos = nil // this server does not solve ACME challenges
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
tlsConfig.ClientCAs = clientCertPool
// convert logger to stdlib so it can be used by HTTP server
serverLogger, err := zap.NewStdLogAt(remoteLogger, zap.DebugLevel)
if err != nil {
return err
}
serverMu.Lock()
// create secure HTTP server
remoteAdminServer = &http.Server{
Addr: addr.String(), // for logging purposes only
Handler: handler,
TLSConfig: tlsConfig,
ReadTimeout: 10 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1024 * 64,
ErrorLog: serverLogger,
}
serverMu.Unlock()
// start listener
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
if err != nil {
return err
}
ln = tls.NewListener(ln, tlsConfig)
go func() {
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.Info("secure admin remote control endpoint started",
zap.String("address", addr.String()))
return nil
}
func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool) *certmagic.Config {
if ident == nil {
// user might not have configured identity; that's OK, we can still make a
// certmagic config, although it'll be mostly useless for remote management
ident = new(IdentityConfig)
}
cmCfg := &certmagic.Config{
Storage: DefaultStorage, // do not act as part of a cluster (this is for the server's local identity)
Logger: logger,
Issuers: ident.issuers,
}
if makeCache {
identityCertCache = certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
return cmCfg, nil
},
})
}
return certmagic.New(identityCertCache, *cmCfg)
}
// IdentityCredentials returns this instance's configured, managed identity credentials
// that can be used in TLS client authentication.
func (ctx Context) IdentityCredentials(logger *zap.Logger) ([]tls.Certificate, error) {
if ctx.cfg == nil || ctx.cfg.Admin == nil || ctx.cfg.Admin.Identity == nil {
return nil, fmt.Errorf("no server identity configured")
}
ident := ctx.cfg.Admin.Identity
if len(ident.Identifiers) == 0 {
return nil, fmt.Errorf("no identifiers configured")
}
if logger == nil {
logger = Log()
}
magic := ident.certmagicConfig(logger, false)
return magic.ClientCredentials(ctx, ident.Identifiers)
}
// enforceAccessControls enforces application-layer access controls for r based on remote.
// It expects that the TLS server has already established at least one verified chain of
// trust, and then looks for a matching, authorized public key that is allowed to access
// the defined path(s) using the defined method(s).
func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
for _, chain := range r.TLS.VerifiedChains {
for _, peerCert := range chain {
for _, adminAccess := range remote.AccessControl {
for _, allowedKey := range adminAccess.publicKeys {
// see if we found a matching public key; the TLS server already verified the chain
// so we know the client possesses the associated private key; this handy interface
// doesn't appear to be defined anywhere in the std lib, but was implemented here:
// https://github.com/golang/go/commit/b5f2c0f50297fa5cd14af668ddd7fd923626cf8c
comparer, ok := peerCert.PublicKey.(interface{ Equal(crypto.PublicKey) bool })
if !ok || !comparer.Equal(allowedKey) {
continue
}
// key recognized; make sure its HTTP request is permitted
for _, accessPerm := range adminAccess.Permissions {
// verify method
methodFound := accessPerm.Methods == nil
for _, method := range accessPerm.Methods {
if method == r.Method {
methodFound = true
break
}
}
if !methodFound {
return APIError{
HTTPStatus: http.StatusForbidden,
Message: "not authorized to use this method",
}
}
// verify path
pathFound := accessPerm.Paths == nil
for _, allowedPath := range accessPerm.Paths {
if strings.HasPrefix(r.URL.Path, allowedPath) {
pathFound = true
break
}
}
if !pathFound {
return APIError{
HTTPStatus: http.StatusForbidden,
Message: "not authorized to access this path",
}
}
}
// public key authorized, method and path allowed
return nil
}
}
}
}
// in theory, this should never happen; with an unverified chain, the TLS server
// should not accept the connection in the first place, and the acceptable cert
// pool is configured using the same list of public keys we verify against
return APIError{
HTTPStatus: http.StatusUnauthorized,
Message: "client identity not authorized",
}
}
func stopAdminServer(srv *http.Server) error {
if srv == nil {
return fmt.Errorf("no admin server")
@@ -286,7 +630,7 @@ func stopAdminServer(srv *http.Server) error {
if err != nil {
return fmt.Errorf("shutting down admin server: %v", err)
}
Log().Named("admin").Info("stopped previous server")
Log().Named("admin").Info("stopped previous server", zap.String("address", srv.Addr))
return nil
}
@@ -302,10 +646,15 @@ type AdminRoute struct {
}
type adminHandler struct {
mux *http.ServeMux
// security for local/plaintext) endpoint, on by default
enforceOrigin bool
enforceHost bool
allowedOrigins []string
mux *http.ServeMux
// security for remote/encrypted endpoint
remoteControl *RemoteAdmin
}
// ServeHTTP is the external entry point for API requests.
@@ -318,6 +667,12 @@ func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
zap.String("remote_addr", r.RemoteAddr),
zap.Reflect("headers", r.Header),
)
if r.TLS != nil {
log = log.With(
zap.Bool("secure", true),
zap.Int("verified_chains", len(r.TLS.VerifiedChains)),
)
}
if r.RequestURI == "/metrics" {
log.Debug("received request")
} else {
@@ -330,6 +685,14 @@ func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// be called more than once per request, for example if a request
// is rewritten (i.e. internal redirect).
func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
if h.remoteControl != nil {
// enforce access controls on secure endpoint
if err := h.remoteControl.enforceAccessControls(r); err != nil {
h.handleError(w, r, err)
return
}
}
if strings.Contains(r.Header.Get("Upgrade"), "websocket") {
// I've never been able demonstrate a vulnerability myself, but apparently
// WebSocket connections originating from browsers aren't subject to CORS
@@ -363,8 +726,6 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
// TODO: authentication & authorization, if configured
h.mux.ServeHTTP(w, r)
}
@@ -372,7 +733,7 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
if err == nil {
return
}
if err == ErrInternalRedir {
if err == errInternalRedir {
h.serveHTTP(w, r)
return
}
@@ -380,12 +741,12 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
apiErr, ok := err.(APIError)
if !ok {
apiErr = APIError{
Code: http.StatusInternalServerError,
Err: err,
HTTPStatus: http.StatusInternalServerError,
Err: err,
}
}
if apiErr.Code == 0 {
apiErr.Code = http.StatusInternalServerError
if apiErr.HTTPStatus == 0 {
apiErr.HTTPStatus = http.StatusInternalServerError
}
if apiErr.Message == "" && apiErr.Err != nil {
apiErr.Message = apiErr.Err.Error()
@@ -393,12 +754,15 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
Log().Named("admin.api").Error("request error",
zap.Error(err),
zap.Int("status_code", apiErr.Code),
zap.Int("status_code", apiErr.HTTPStatus),
)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(apiErr.Code)
json.NewEncoder(w).Encode(apiErr)
w.WriteHeader(apiErr.HTTPStatus)
encErr := json.NewEncoder(w).Encode(apiErr)
if encErr != nil {
Log().Named("admin.api").Error("failed to encode error response", zap.Error(encErr))
}
}
// checkHost returns a handler that wraps next such that
@@ -415,8 +779,8 @@ func (h adminHandler) checkHost(r *http.Request) error {
}
if !allowed {
return APIError{
Code: http.StatusForbidden,
Err: fmt.Errorf("host not allowed: %s", r.Host),
HTTPStatus: http.StatusForbidden,
Err: fmt.Errorf("host not allowed: %s", r.Host),
}
}
return nil
@@ -430,14 +794,14 @@ func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
origin := h.getOriginHost(r)
if origin == "" {
return origin, APIError{
Code: http.StatusForbidden,
Err: fmt.Errorf("missing required Origin header"),
HTTPStatus: http.StatusForbidden,
Err: fmt.Errorf("missing required Origin header"),
}
}
if !h.originAllowed(origin) {
return origin, APIError{
Code: http.StatusForbidden,
Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
HTTPStatus: http.StatusForbidden,
Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
}
}
return origin, nil
@@ -477,7 +841,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
err := readConfig(r.URL.Path, w)
if err != nil {
return APIError{Code: http.StatusBadRequest, Err: err}
return APIError{HTTPStatus: http.StatusBadRequest, Err: err}
}
return nil
@@ -492,8 +856,8 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodDelete {
if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
return APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
}
}
@@ -504,8 +868,8 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
_, err := io.Copy(buf, r.Body)
if err != nil {
return APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("reading request body: %v", err),
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("reading request body: %v", err),
}
}
body = buf.Bytes()
@@ -520,8 +884,8 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
default:
return APIError{
Code: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method %s not allowed", r.Method),
HTTPStatus: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method %s not allowed", r.Method),
}
}
@@ -552,46 +916,22 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
parts = append([]string{expanded}, parts[3:]...)
r.URL.Path = path.Join(parts...)
return ErrInternalRedir
return errInternalRedir
}
func handleStop(w http.ResponseWriter, r *http.Request) error {
err := handleUnload(w, r)
if err != nil {
Log().Named("admin.api").Error("unload error", zap.Error(err))
}
if adminServer != nil {
// use goroutine so that we can finish responding to API request
go func() {
err := stopAdminServer(adminServer)
var exitCode int
if err != nil {
exitCode = ExitCodeFailedQuit
Log().Named("admin.api").Error("failed to stop admin server gracefully", zap.Error(err))
}
Log().Named("admin.api").Info("stopping now, bye!! 👋")
os.Exit(exitCode)
}()
}
return nil
}
// handleUnload stops the current configuration that is running.
// Note that doing this can also be accomplished with DELETE /config/
// but we leave this function because handleStop uses it.
func handleUnload(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodPost {
return APIError{
Code: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed"),
HTTPStatus: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed"),
}
}
Log().Named("admin.api").Info("unloading")
if err := stopAndCleanup(); err != nil {
Log().Named("admin.api").Error("error unloading", zap.Error(err))
} else {
Log().Named("admin.api").Info("unloading completed")
if err := notify.NotifyStopping(); err != nil {
Log().Error("unable to notify stopping to service manager", zap.Error(err))
}
exitProcess(Log().Named("admin.api"))
return nil
}
@@ -803,9 +1143,9 @@ func (f AdminHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) erro
// and client responses. If Message is unset, then
// Err.Error() will be serialized in its place.
type APIError struct {
Code int `json:"-"`
Err error `json:"-"`
Message string `json:"error"`
HTTPStatus int `json:"-"`
Err error `json:"-"`
Message string `json:"error"`
}
func (e APIError) Error() string {
@@ -815,20 +1155,44 @@ func (e APIError) Error() string {
return e.Message
}
// parseAdminListenAddr extracts a singular listen address from either addr
// or defaultAddr, returning the network and the address of the listener.
func parseAdminListenAddr(addr string, defaultAddr string) (NetworkAddress, error) {
input := addr
if input == "" {
input = defaultAddr
}
listenAddr, err := ParseNetworkAddress(input)
if err != nil {
return NetworkAddress{}, fmt.Errorf("parsing listener address: %v", err)
}
if listenAddr.PortRangeSize() != 1 {
return NetworkAddress{}, fmt.Errorf("must be exactly one listener address; cannot listen on: %s", listenAddr)
}
return listenAddr, nil
}
// decodeBase64DERCert base64-decodes, then DER-decodes, certStr.
func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
derBytes, err := base64.StdEncoding.DecodeString(certStr)
if err != nil {
return nil, err
}
return x509.ParseCertificate(derBytes)
}
var (
// DefaultAdminListen is the address for the admin
// DefaultAdminListen is the address for the local admin
// listener, if none is specified at startup.
DefaultAdminListen = "localhost:2019"
// 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.
ErrInternalRedir = fmt.Errorf("internal redirect; re-authorization required")
// DefaultRemoteAdminListen is the address for the remote
// (TLS-authenticated) admin listener, if enabled and not
// specified otherwise.
DefaultRemoteAdminListen = ":2021"
// DefaultAdminConfig is the default configuration
// for the administration endpoint.
// for the local administration endpoint.
DefaultAdminConfig = &AdminConfig{
Listen: DefaultAdminListen,
}
@@ -838,7 +1202,7 @@ var (
// will get deleted before the process gracefully exits.
func PIDFile(filename string) error {
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
err := ioutil.WriteFile(filename, pid, 0644)
err := ioutil.WriteFile(filename, pid, 0600)
if err != nil {
return err
}
@@ -855,6 +1219,13 @@ var idRegexp = regexp.MustCompile(`(?m),?\s*"` + idKey + `"\s*:\s*(-?[0-9]+(\.[0
// pidfile is the name of the pidfile, if any.
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 (
rawConfigKey = "config"
idKey = "@id"
@@ -866,4 +1237,9 @@ var bufPool = sync.Pool{
},
}
var adminServer *http.Server
// keep a reference to admin endpoint singletons while they're active
var (
serverMu sync.Mutex
localAdminServer, remoteAdminServer *http.Server
identityCertCache *certmagic.Cache
)
+36 -18
View File
@@ -17,9 +17,28 @@ package caddy
import (
"encoding/json"
"reflect"
"sync"
"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) {
// each test is performed in sequence, so
// 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) {
for i := 0; i < b.N; i++ {
cfg := []byte(`{
"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)
Load(testCfg, true)
}
}
+214 -36
View File
@@ -32,7 +32,9 @@ import (
"sync"
"time"
"github.com/caddyserver/caddy/v2/notify"
"github.com/caddyserver/certmagic"
"github.com/google/uuid"
"go.uber.org/zap"
)
@@ -99,6 +101,16 @@ func Run(cfg *Config) error {
// if it is different from the current config or
// forceReload is true.
func Load(cfgJSON []byte, forceReload bool) error {
if err := notify.NotifyReloading(); err != nil {
Log().Error("unable to notify reloading to service manager", zap.Error(err))
}
defer func() {
if err := notify.NotifyReadiness(); err != nil {
Log().Error("unable to notify readiness to service manager", zap.Error(err))
}
}()
return changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
}
@@ -130,8 +142,8 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
newCfg, err := json.Marshal(rawCfg[rawConfigKey])
if err != nil {
return APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("encoding new config: %v", err),
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("encoding new config: %v", err),
}
}
@@ -146,14 +158,14 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
if err != nil {
return APIError{
Code: http.StatusInternalServerError,
Err: fmt.Errorf("indexing config: %v", err),
HTTPStatus: http.StatusInternalServerError,
Err: fmt.Errorf("indexing config: %v", err),
}
}
// load this new config; if it fails, we need to revert to
// our old representation of caddy's actual config
err = unsyncedDecodeAndRun(newCfg)
err = unsyncedDecodeAndRun(newCfg, true)
if err != nil {
if len(rawCfgJSON) > 0 {
// restore old config state to keep it consistent
@@ -233,8 +245,10 @@ func indexConfigObjects(ptr interface{}, configPath string, index map[string]str
// it as the new config, replacing any other current config.
// It does NOT update the raw config state, as this is a
// lower-level function; most callers will want to use Load
// instead. A write lock on currentCfgMu is required!
func unsyncedDecodeAndRun(cfgJSON []byte) error {
// instead. A write lock on currentCfgMu is required! If
// allowPersist is false, it will not be persisted to disk,
// even if it is configured to.
func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
// remove any @id fields from the JSON, which would cause
// loading to break since the field wouldn't be recognized
strippedCfgJSON := RemoveMetaFields(cfgJSON)
@@ -245,6 +259,20 @@ func unsyncedDecodeAndRun(cfgJSON []byte) error {
return err
}
// prevent recursive config loads; that is a user error, and
// although frequent config loads should be safe, we cannot
// guarantee that in the presence of third party plugins, nor
// do we want this error to go unnoticed (we assume it was a
// pulled config if we're not allowed to persist it)
if !allowPersist &&
newCfg != nil &&
newCfg.Admin != nil &&
newCfg.Admin.Config != nil &&
newCfg.Admin.Config.LoadRaw != nil &&
newCfg.Admin.Config.LoadInterval <= 0 {
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_interval")
}
// run the new config and start all its apps
err = run(newCfg, true)
if err != nil {
@@ -259,7 +287,8 @@ func unsyncedDecodeAndRun(cfgJSON []byte) error {
unsyncedStop(oldCfg)
// autosave a non-nil config, if not disabled
if newCfg != nil &&
if allowPersist &&
newCfg != nil &&
(newCfg.Admin == nil ||
newCfg.Admin.Config == nil ||
newCfg.Admin.Config.Persist == nil ||
@@ -273,7 +302,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte) error {
} else {
err := ioutil.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
if err == nil {
Log().Info("autosaved config", zap.String("file", ConfigAutosavePath))
Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath))
} else {
Log().Error("unable to autosave config",
zap.String("file", ConfigAutosavePath),
@@ -309,21 +338,10 @@ func run(newCfg *Config, start bool) error {
// been set by a short assignment
var err error
// start the admin endpoint (and stop any prior one)
if start {
err = replaceAdmin(newCfg)
if err != nil {
return fmt.Errorf("starting caddy administration endpoint: %v", err)
}
}
if newCfg == nil {
return nil
newCfg = new(Config)
}
// prepare the new config for use
newCfg.apps = make(map[string]App)
// create a context within which to load
// modules - essentially our new config's
// execution environment; be sure that
@@ -357,6 +375,17 @@ func run(newCfg *Config, start bool) error {
return err
}
// start the admin endpoint (and stop any prior one)
if start {
err = replaceLocalAdminServer(newCfg)
if err != nil {
return fmt.Errorf("starting caddy administration endpoint: %v", err)
}
}
// prepare the new config for use
newCfg.apps = make(map[string]App)
// set up global storage and make it CertMagic's default storage, too
err = func() error {
if newCfg.StorageRaw != nil {
@@ -400,7 +429,7 @@ func run(newCfg *Config, start bool) error {
}
// Start
return func() error {
err = func() error {
var started []string
for name, a := range newCfg.apps {
err := a.Start()
@@ -420,6 +449,83 @@ func run(newCfg *Config, start bool) error {
}
return nil
}()
if err != nil {
return err
}
// now that the user's config is running, finish setting up anything else,
// such as remote admin endpoint, config loader, etc.
return finishSettingUp(ctx, newCfg)
}
// finishSettingUp should be run after all apps have successfully started.
func finishSettingUp(ctx Context, cfg *Config) error {
// establish this server's identity (only after apps are loaded
// so that cert management of this endpoint doesn't prevent user's
// servers from starting which likely also use HTTP/HTTPS ports;
// but before remote management which may depend on these creds)
err := manageIdentity(ctx, cfg)
if err != nil {
return fmt.Errorf("provisioning remote admin endpoint: %v", err)
}
// replace any remote admin endpoint
err = replaceRemoteAdminServer(ctx, cfg)
if err != nil {
return fmt.Errorf("provisioning remote admin endpoint: %v", err)
}
// if dynamic config is requested, set that up and run it
if cfg != nil && cfg.Admin != nil && cfg.Admin.Config != nil && cfg.Admin.Config.LoadRaw != nil {
val, err := ctx.LoadModule(cfg.Admin.Config, "LoadRaw")
if err != nil {
return fmt.Errorf("loading config loader module: %s", err)
}
runLoadedConfig := func(config []byte) {
Log().Info("applying dynamically-loaded config", zap.String("loader_module", val.(Module).CaddyModule().ID.Name()), zap.Int("pull_interval", int(cfg.Admin.Config.LoadInterval)))
currentCfgMu.Lock()
err := unsyncedDecodeAndRun(config, false)
currentCfgMu.Unlock()
if err == nil {
Log().Info("dynamically-loaded config applied successfully")
} else {
Log().Error("running dynamically-loaded config failed", zap.Error(err))
}
}
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
}
// ConfigLoader is a type that can load a Caddy config. The
// returned config must be valid Caddy JSON.
type ConfigLoader interface {
LoadConfig(Context) ([]byte, error)
}
// Stop stops running the current configuration.
@@ -462,20 +568,6 @@ func unsyncedStop(cfg *Config) {
cfg.cancelFunc()
}
// stopAndCleanup calls stop and cleans up anything
// else that is expedient. This should only be used
// when stopping and not replacing with a new config.
func stopAndCleanup() error {
if err := Stop(); err != nil {
return err
}
certmagic.CleanUpOwnLocks()
if pidfile != "" {
return os.Remove(pidfile)
}
return nil
}
// Validate loads, provisions, and validates
// cfg, but does not start running it.
func Validate(cfg *Config) error {
@@ -486,6 +578,72 @@ func Validate(cfg *Config) error {
return err
}
// exitProcess exits the process as gracefully as possible,
// but it always exits, even if there are errors doing so.
// It stops all apps, cleans up external locks, removes any
// PID file, and shuts down admin endpoint(s) in a goroutine.
// Errors are logged along the way, and an appropriate exit
// code is emitted.
func exitProcess(logger *zap.Logger) {
if logger == nil {
logger = Log()
}
logger.Warn("exiting; byeee!! 👋")
exitCode := ExitCodeSuccess
// stop all apps
if err := Stop(); err != nil {
logger.Error("failed to stop apps", zap.Error(err))
exitCode = ExitCodeFailedQuit
}
// clean up certmagic locks
certmagic.CleanUpOwnLocks(logger)
// remove pidfile
if pidfile != "" {
err := os.Remove(pidfile)
if err != nil {
logger.Error("cleaning up PID file:",
zap.String("pidfile", pidfile),
zap.Error(err))
exitCode = ExitCodeFailedQuit
}
}
// shut down admin endpoint(s) in goroutines so that
// if this function was called from an admin handler,
// it has a chance to return gracefully
// use goroutine so that we can finish responding to API request
go func() {
defer func() {
logger = logger.With(zap.Int("exit_code", exitCode))
if exitCode == ExitCodeSuccess {
logger.Info("shutdown complete")
} else {
logger.Error("unclean shutdown")
}
os.Exit(exitCode)
}()
if remoteAdminServer != nil {
err := stopAdminServer(remoteAdminServer)
if err != nil {
exitCode = ExitCodeFailedQuit
logger.Error("failed to stop remote admin server gracefully", zap.Error(err))
}
}
if localAdminServer != nil {
err := stopAdminServer(localAdminServer)
if err != nil {
exitCode = ExitCodeFailedQuit
logger.Error("failed to stop local admin server gracefully", zap.Error(err))
}
}
}()
}
// Duration can be an integer or a string. An integer is
// interpreted as nanoseconds. If a string, it is a Go
// time.Duration value such as `300ms`, `1.5h`, or `2h45m`;
@@ -536,6 +694,26 @@ func ParseDuration(s string) (time.Duration, error) {
return time.ParseDuration(s)
}
// InstanceID returns the UUID for this instance, and generates one if it
// does not already exist. The UUID is stored in the local data directory,
// regardless of storage configuration, since each instance is intended to
// have its own unique ID.
func InstanceID() (uuid.UUID, error) {
uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid")
uuidFileBytes, err := ioutil.ReadFile(uuidFilePath)
if os.IsNotExist(err) {
uuid, err := uuid.NewRandom()
if err != nil {
return uuid, err
}
err = ioutil.WriteFile(uuidFilePath, []byte(uuid.String()), 0600)
return uuid, err
} else if err != nil {
return [16]byte{}, err
}
return uuid.ParseBytes(uuidFileBytes)
}
// GoModule returns the build info of this Caddy
// build from debug.BuildInfo (requires Go modules).
// If no version information is available, a non-nil
+62 -4
View File
@@ -15,6 +15,7 @@
package caddyfile
import (
"bytes"
"encoding/json"
"fmt"
@@ -51,15 +52,46 @@ func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []c
return nil, warnings, err
}
marshalFunc := json.Marshal
if options["pretty"] == "true" {
marshalFunc = caddyconfig.JSONIndent
// lint check: see if input was properly formatted; sometimes messy files files parse
// successfully but result in logical errors (the Caddyfile is a bad format, I'm sorry)
if warning, different := formattingDifference(filename, body); different {
warnings = append(warnings, warning)
}
result, err := marshalFunc(cfg)
result, err := json.Marshal(cfg)
return result, warnings, err
}
// formattingDifference returns a warning and true if the formatted version
// is any different from the input; empty warning and false otherwise.
// TODO: also perform this check on imported files
func formattingDifference(filename string, body []byte) (caddyconfig.Warning, bool) {
// replace windows-style newlines to normalize comparison
normalizedBody := bytes.Replace(body, []byte("\r\n"), []byte("\n"), -1)
formatted := Format(normalizedBody)
if bytes.Equal(formatted, normalizedBody) {
return caddyconfig.Warning{}, false
}
// find where the difference is
line := 1
for i, ch := range normalizedBody {
if i >= len(formatted) || ch != formatted[i] {
break
}
if ch == '\n' {
line++
}
}
return caddyconfig.Warning{
File: filename,
Line: line,
Message: "input is not formatted with 'caddy fmt'",
}, true
}
// Unmarshaler is a type that can unmarshal
// Caddyfile tokens to set itself up for a
// JSON encoding. The goal of an unmarshaler
@@ -87,5 +119,31 @@ type ServerType interface {
Setup([]ServerBlock, map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error)
}
// UnmarshalModule instantiates a module with the given ID and invokes
// UnmarshalCaddyfile on the new value using the immediate next segment
// of d as input. In other words, d's next token should be the first
// token of the module's Caddyfile input.
//
// This function is used when the next segment of Caddyfile tokens
// belongs to another Caddy module. The returned value is often
// type-asserted to the module's associated type for practical use
// when setting up a config.
func UnmarshalModule(d *Dispenser, moduleID string) (Unmarshaler, error) {
mod, err := caddy.GetModule(moduleID)
if err != nil {
return nil, d.Errf("getting module named '%s': %v", moduleID, err)
}
inst := mod.New()
unm, ok := inst.(Unmarshaler)
if !ok {
return nil, d.Errf("module %s is not a Caddyfile unmarshaler; is %T", mod.ID, inst)
}
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
if err != nil {
return nil, err
}
return unm, nil
}
// Interface guard
var _ caddyconfig.Adapter = (*Adapter)(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.
func (d *Dispenser) Err(msg string) error {
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
return errors.New(msg)
return d.Errf(msg)
}
// Errf is like Err, but for formatted error messages
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
+7
View File
@@ -15,6 +15,7 @@
package caddyfile
import (
"errors"
"reflect"
"strings"
"testing"
@@ -303,4 +304,10 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
if !strings.Contains(err.Error(), "foobar") {
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")
}
}
+3
View File
@@ -78,6 +78,9 @@ func Format(input []byte) []byte {
if comment {
if ch == '\n' {
comment = false
space = true
nextLine()
continue
} else {
write(ch)
continue
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build gofuzz
package caddyfile
import "bytes"
func FuzzFormat(input []byte) int {
formatted := Format(input)
if bytes.Equal(formatted, Format(formatted)) {
return 1
}
return 0
}
+49
View File
@@ -310,6 +310,55 @@ baz`,
input: `redir / /some/#/path`,
expect: `redir / /some/#/path`,
},
{
description: "brace does not fold into comment above",
input: `# comment
{
foo
}`,
expect: `# comment
{
foo
}`,
},
{
description: "matthewpi/vscode-caddyfile-support#13",
input: `{
email {$ACMEEMAIL}
#debug
}
block {
}
`,
expect: `{
email {$ACMEEMAIL}
#debug
}
block {
}
`,
},
{
description: "matthewpi/vscode-caddyfile-support#13 - bad formatting",
input: `{
email {$ACMEEMAIL}
#debug
}
block {
}
`,
expect: `{
email {$ACMEEMAIL}
#debug
}
block {
}
`,
},
} {
// the formatter should output a trailing newline,
// even if the tests aren't written to expect that
+127
View File
@@ -0,0 +1,127 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"fmt"
)
type adjacency map[string][]string
type importGraph struct {
nodes map[string]bool
edges adjacency
}
func (i *importGraph) addNode(name string) {
if i.nodes == nil {
i.nodes = make(map[string]bool)
}
if _, exists := i.nodes[name]; exists {
return
}
i.nodes[name] = true
}
func (i *importGraph) addNodes(names []string) {
for _, name := range names {
i.addNode(name)
}
}
func (i *importGraph) removeNode(name string) {
delete(i.nodes, name)
}
func (i *importGraph) removeNodes(names []string) {
for _, name := range names {
i.removeNode(name)
}
}
func (i *importGraph) addEdge(from, to string) error {
if !i.exists(from) || !i.exists(to) {
return fmt.Errorf("one of the nodes does not exist")
}
if i.willCycle(to, from) {
return fmt.Errorf("a cycle of imports exists between %s and %s", from, to)
}
if i.areConnected(from, to) {
// if connected, there's nothing to do
return nil
}
if i.nodes == nil {
i.nodes = make(map[string]bool)
}
if i.edges == nil {
i.edges = make(adjacency)
}
i.edges[from] = append(i.edges[from], to)
return nil
}
func (i *importGraph) addEdges(from string, tos []string) error {
for _, to := range tos {
err := i.addEdge(from, to)
if err != nil {
return err
}
}
return nil
}
func (i *importGraph) areConnected(from, to string) bool {
al, ok := i.edges[from]
if !ok {
return false
}
for _, v := range al {
if v == to {
return true
}
}
return false
}
func (i *importGraph) willCycle(from, to string) bool {
collector := make(map[string]bool)
var visit func(string)
visit = func(start string) {
if !collector[start] {
collector[start] = true
for _, v := range i.edges[start] {
visit(v)
}
}
}
for _, v := range i.edges[from] {
visit(v)
}
for k := range collector {
if to == k {
return true
}
}
return false
}
func (i *importGraph) exists(key string) bool {
_, exists := i.nodes[key]
return exists
}
+5 -3
View File
@@ -35,9 +35,11 @@ type (
// Token represents a single parsable unit.
Token struct {
File string
Line int
Text string
File string
Line int
Text string
inSnippet bool
snippetName string
}
)
@@ -12,26 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// +build !windows
// +build gofuzz
package caddycmd
package caddyfile
import (
"fmt"
"os"
"path/filepath"
"syscall"
)
func gracefullyStopProcess(pid int) error {
fmt.Print("Graceful stop... ")
err := syscall.Kill(pid, syscall.SIGINT)
func FuzzTokenize(input []byte) int {
tokens, err := Tokenize(input, "Caddyfile")
if err != nil {
return fmt.Errorf("kill: %v", err)
return 0
}
return nil
}
func getProcessName() string {
return filepath.Base(os.Args[0])
if len(tokens) == 0 {
return -1
}
return 1
}
+76 -11
View File
@@ -16,6 +16,7 @@ package caddyfile
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
@@ -40,7 +41,13 @@ func Parse(filename string, input []byte) ([]ServerBlock, error) {
if err != nil {
return nil, err
}
p := parser{Dispenser: NewDispenser(tokens)}
p := parser{
Dispenser: NewDispenser(tokens),
importGraph: importGraph{
nodes: make(map[string]bool),
edges: make(adjacency),
},
}
return p.parseAll()
}
@@ -60,21 +67,31 @@ func replaceEnvVars(input []byte) ([]byte, error) {
end += begin + len(spanOpen) // make end relative to input, not begin
// get the name; if there is no name, skip it
envVarName := input[begin+len(spanOpen) : end]
if len(envVarName) == 0 {
envString := input[begin+len(spanOpen) : end]
if len(envString) == 0 {
offset = end + len(spanClose)
continue
}
// split the string into a key and an optional default
envParts := strings.SplitN(string(envString), envVarDefaultDelimiter, 2)
// do a lookup for the env var, replace with the default if not found
envVarValue, found := os.LookupEnv(envParts[0])
if !found && len(envParts) == 2 {
envVarValue = envParts[1]
}
// get the value of the environment variable
envVarValue := []byte(os.ExpandEnv(os.Getenv(string(envVarName))))
// note that this causes one-level deep chaining
envVarBytes := []byte(envVarValue)
// splice in the value
input = append(input[:begin],
append(envVarValue, input[end+len(spanClose):]...)...)
append(envVarBytes, input[end+len(spanClose):]...)...)
// continue at the end of the replacement
offset = begin + len(envVarValue)
offset = begin + len(envVarBytes)
}
return input, nil
}
@@ -100,6 +117,7 @@ type parser struct {
eof bool // if we encounter a valid EOF in a hard place
definedSnippets map[string][]Token
nesting int
importGraph importGraph
}
func (p *parser) parseAll() ([]ServerBlock, error) {
@@ -155,6 +173,15 @@ func (p *parser) begin() error {
if err != nil {
return err
}
// Just as we need to track which file the token comes from, we need to
// keep track of which snippets do the tokens come from. This is helpful
// in tracking import cycles across files/snippets by namespacing them. Without
// this we end up with false-positives in cycle-detection.
for k, v := range tokens {
v.inSnippet = true
v.snippetName = name
tokens[k] = v
}
p.definedSnippets[name] = tokens
// empty block keys so we don't save this block as a real server.
p.block.Keys = nil
@@ -184,9 +211,20 @@ func (p *parser) addresses() error {
if expectingAnother {
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
}
// 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 ""
// Trailing comma indicates another address will follow, which
// may possibly be on the next line
@@ -197,6 +235,13 @@ func (p *parser) addresses() error {
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)
}
@@ -294,7 +339,7 @@ func (p *parser) doImport() error {
args := p.RemainingArgs()
// add args to the replacer
repl := caddy.NewReplacer()
repl := caddy.NewEmptyReplacer()
for index, arg := range args {
repl.Set("args."+strconv.Itoa(index), arg)
}
@@ -304,10 +349,15 @@ func (p *parser) doImport() error {
tokensBefore := p.tokens[:p.cursor-1-len(args)]
tokensAfter := p.tokens[p.cursor+1:]
var importedTokens []Token
var nodes []string
// first check snippets. That is a simple, non-recursive replacement
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
importedTokens = p.definedSnippets[importPattern]
if len(importedTokens) > 0 {
// just grab the first one
nodes = append(nodes, fmt.Sprintf("%s:%s", importedTokens[0].File, importedTokens[0].snippetName))
}
} else {
// make path relative to the file of the _token_ being processed rather
// than current working directory (issue #867) and then use glob to get
@@ -343,7 +393,6 @@ func (p *parser) doImport() error {
}
// collect all the imported tokens
for _, importFile := range matches {
newTokens, err := p.doSingleImport(importFile)
if err != nil {
@@ -351,6 +400,18 @@ func (p *parser) doImport() error {
}
importedTokens = append(importedTokens, newTokens...)
}
nodes = matches
}
nodeName := p.File()
if p.Token().inSnippet {
nodeName += fmt.Sprintf(":%s", p.Token().snippetName)
}
p.importGraph.addNode(nodeName)
p.importGraph.addNodes(nodes)
if err := p.importGraph.addEdges(nodeName, nodes); err != nil {
p.importGraph.removeNodes(nodes)
return err
}
// copy the tokens so we don't overwrite p.definedSnippets
@@ -516,8 +577,9 @@ func (p *parser) snippetTokens() ([]Token, error) {
// head of the server block with tokens, which are
// grouped by segments.
type ServerBlock struct {
Keys []string
Segments []Segment
HasBraces bool
Keys []string
Segments []Segment
}
// DispenseDirective returns a dispenser that contains
@@ -548,4 +610,7 @@ func (s Segment) Directive() string {
// spanOpen and spanClose are used to bound spans that
// contain the name of an environment variable.
var spanOpen, spanClose = []byte{'{', '$'}, []byte{'}'}
var (
spanOpen, spanClose = []byte{'{', '$'}, []byte{'}'}
envVarDefaultDelimiter = ":"
)
+43
View File
@@ -160,6 +160,10 @@ func TestParseOneAndImport(t *testing.T) {
"localhost",
}, []int{}},
{`localhost{
dir1
}`, true, []string{}, []int{}},
{`localhost
dir1 {
nested {
@@ -444,6 +448,28 @@ func TestParseAll(t *testing.T) {
{`import notfound/*`, false, [][]string{}}, // glob needn't error with no matches
{`import notfound/file.conf`, true, [][]string{}}, // but a specific file should
// recursive self-import
{`import testdata/import_recursive0.txt`, true, [][]string{}},
{`import testdata/import_recursive3.txt
import testdata/import_recursive1.txt`, true, [][]string{}},
// cyclic imports
{`(A) {
import A
}
:80
import A
`, true, [][]string{}},
{`(A) {
import B
}
(B) {
import A
}
:80
import A
`, true, [][]string{}},
} {
p := testParser(test.input)
blocks, err := p.parseAll()
@@ -478,6 +504,7 @@ func TestParseAll(t *testing.T) {
func TestEnvironmentReplacement(t *testing.T) {
os.Setenv("FOOBAR", "foobar")
os.Setenv("CHAINED", "$FOOBAR")
for i, test := range []struct {
input string
@@ -523,6 +550,22 @@ func TestEnvironmentReplacement(t *testing.T) {
input: "{$FOOBAR}{$FOOBAR}",
expect: "foobarfoobar",
},
{
input: "{$CHAINED}",
expect: "$FOOBAR", // should not chain env expands
},
{
input: "{$FOO:default}",
expect: "default",
},
{
input: "foo{$BAR:bar}baz",
expect: "foobarbaz",
},
{
input: "foo{$BAR:$FOOBAR}baz",
expect: "foo$FOOBARbaz", // should not chain env expands
},
{
input: "{$FOOBAR",
expect: "{$FOOBAR",
+1
View File
@@ -0,0 +1 @@
import import_recursive0.txt
+1
View File
@@ -0,0 +1 @@
import import_recursive2.txt
+1
View File
@@ -0,0 +1 @@
import import_recursive3.txt
+1
View File
@@ -0,0 +1 @@
import import_recursive1.txt
+8 -6
View File
@@ -35,6 +35,14 @@ type Warning struct {
Message string `json:"message,omitempty"`
}
func (w Warning) String() string {
var directive string
if w.Directive != "" {
directive = fmt.Sprintf(" (%s)", w.Directive)
}
return fmt.Sprintf("%s:%d%s: %s", w.File, w.Line, directive, w.Message)
}
// JSON encodes val as JSON, returning it as a json.RawMessage. Any
// marshaling errors (which are highly unlikely with correct code)
// are converted to warnings. This is convenient when filling config
@@ -93,12 +101,6 @@ func JSONModuleObject(val interface{}, fieldName, fieldVal string, warnings *[]W
return result
}
// JSONIndent is used to JSON-marshal the final resulting Caddy
// configuration in a consistent, human-readable way.
func JSONIndent(val interface{}) ([]byte, error) {
return json.MarshalIndent(val, "", "\t")
}
// RegisterAdapter registers a config adapter with the given name.
// This should usually be done at init-time. It panics if the
// adapter cannot be registered successfully.
+8
View File
@@ -18,6 +18,7 @@ import (
"fmt"
"net"
"reflect"
"sort"
"strconv"
"strings"
"unicode"
@@ -163,6 +164,13 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
sbaddrs = append(sbaddrs, a)
}
// sort them by their first address (we know there will always be at least one)
// to avoid problems with non-deterministic ordering (makes tests flaky)
sort.Slice(sbaddrs, func(i, j int) bool {
return sbaddrs[i].addresses[0] < sbaddrs[j].addresses[0]
})
return sbaddrs
}
+205 -89
View File
@@ -22,6 +22,7 @@ import (
"io/ioutil"
"net/http"
"reflect"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2"
@@ -40,6 +41,8 @@ func init() {
RegisterHandlerDirective("root", parseRoot)
RegisterHandlerDirective("redir", parseRedir)
RegisterHandlerDirective("respond", parseRespond)
RegisterHandlerDirective("abort", parseAbort)
RegisterHandlerDirective("error", parseError)
RegisterHandlerDirective("route", parseRoute)
RegisterHandlerDirective("handle", parseHandle)
RegisterDirective("handle_errors", parseHandleErrors)
@@ -87,8 +90,9 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
var folderLoader caddytls.FolderLoader
var certSelector caddytls.CustomCertSelectionPolicy
var acmeIssuer *caddytls.ACMEIssuer
var keyType string
var internalIssuer *caddytls.InternalIssuer
var issuer certmagic.Issuer
var issuers []certmagic.Issuer
var onDemand bool
for h.Next() {
@@ -122,10 +126,10 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
// must load each cert only once; otherwise, they each get a
// different tag... since a cert loaded twice has the same
// bytes, it will overwrite the first one in the cache, and
// only the last cert (and its tag) will survive, so a any conn
// policy that is looking for any tag but the last one to be
// loaded won't find it, and TLS handshakes will fail (see end)
// of issue #3004)
// only the last cert (and its tag) will survive, so any conn
// policy that is looking for any tag other than the last one
// to be loaded won't find it, and TLS handshakes will fail
// (see end of issue #3004)
//
// tlsCertTags maps certificate filenames to their tag.
// This is used to remember which tag is used for each
@@ -267,6 +271,13 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
}
acmeIssuer.CA = arg[0]
case "key_type":
arg := h.RemainingArgs()
if len(arg) != 1 {
return nil, h.ArgErr()
}
keyType = arg[0]
case "eab":
arg := h.RemainingArgs()
if len(arg) != 2 {
@@ -285,22 +296,16 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
return nil, h.ArgErr()
}
modName := h.Val()
mod, err := caddy.GetModule("tls.issuance." + modName)
if err != nil {
return nil, h.Errf("getting issuer module '%s': %v", modName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, h.Errf("issuer module '%s' is not a Caddyfile unmarshaler", mod.ID)
}
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
modID := "tls.issuance." + modName
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
if err != nil {
return nil, err
}
issuer, ok = unm.(certmagic.Issuer)
issuer, ok := unm.(certmagic.Issuer)
if !ok {
return nil, h.Errf("module %s is not a certmagic.Issuer", mod.ID)
return nil, h.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm)
}
issuers = append(issuers, issuer)
case "dns":
if !h.NextArg() {
@@ -312,20 +317,32 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
}
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.DNS == nil {
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
}
dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
modID := "dns.providers." + provName
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
if err != nil {
return nil, h.Errf("getting DNS provider module named '%s': %v", provName, err)
return nil, err
}
dnsProvModuleInstance := dnsProvModule.New()
if unm, ok := dnsProvModuleInstance.(caddyfile.Unmarshaler); ok {
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
if err != nil {
return nil, err
}
acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, h.warnings)
case "resolvers":
args := h.RemainingArgs()
if len(args) == 0 {
return nil, h.ArgErr()
}
acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(dnsProvModuleInstance, "name", provName, h.warnings)
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.DNS == nil {
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
}
acmeIssuer.Challenges.DNS.Resolvers = args
case "ca_root":
arg := h.RemainingArgs()
@@ -355,7 +372,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
}
// begin building the final config values
var configVals []ConfigValue
configVals := []ConfigValue{}
// certificate loaders
if len(fileLoader) > 0 {
@@ -371,42 +388,61 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
})
}
// issuer
// some tls subdirectives are shortcuts that implicitly configure issuers, and the
// user can also configure issuers explicitly using the issuer subdirective; the
// logic to support both would likely be complex, or at least unintuitive
if len(issuers) > 0 && (acmeIssuer != nil || internalIssuer != nil) {
return nil, h.Err("cannot mix issuer subdirective (explicit issuers) with other issuer-specific subdirectives (implicit issuers)")
}
if acmeIssuer != nil && internalIssuer != nil {
// the logic to support this would be complex
return nil, h.Err("cannot use both ACME and internal issuers in same server block")
}
if issuer != nil && (acmeIssuer != nil || internalIssuer != nil) {
// similarly, the logic to support this would be complex
return nil, h.Err("when defining an issuer, all its config must be in its block, rather than from separate tls subdirectives")
return nil, h.Err("cannot create both ACME and internal certificate issuers")
}
// now we should either have: explicitly-created issuers, or an implicitly-created
// ACME or internal issuer, or no issuers at all
switch {
case issuer != nil:
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: issuer,
})
case len(issuers) > 0:
for _, issuer := range issuers {
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: issuer,
})
}
case acmeIssuer != nil:
// implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one
defaultIssuers := caddytls.DefaultIssuers()
// if a CA endpoint was set, override multiple implicit issuers since it's a specific one
if acmeIssuer.CA != "" {
defaultIssuers = []certmagic.Issuer{acmeIssuer}
}
for _, issuer := range defaultIssuers {
switch iss := issuer.(type) {
case *caddytls.ACMEIssuer:
issuer = acmeIssuer
case *caddytls.ZeroSSLIssuer:
iss.ACMEIssuer = acmeIssuer
}
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: issuer,
})
}
case internalIssuer != nil:
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: internalIssuer,
})
}
case acmeIssuer != nil:
// fill in global defaults, if configured
if email := h.Option("email"); email != nil && acmeIssuer.Email == "" {
acmeIssuer.Email = email.(string)
}
if acmeCA := h.Option("acme_ca"); acmeCA != nil && acmeIssuer.CA == "" {
acmeIssuer.CA = acmeCA.(string)
}
if caPemFile := h.Option("acme_ca_root"); caPemFile != nil {
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, caPemFile.(string))
}
// certificate key type
if keyType != "" {
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: disambiguateACMEIssuer(acmeIssuer),
Class: "tls.key_type",
Value: keyType,
})
}
@@ -472,14 +508,14 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
if h.NextArg() {
code = h.Val()
}
if code == "permanent" {
code = "301"
}
if code == "temporary" || code == "" {
code = "302"
}
var body string
if code == "html" {
switch code {
case "permanent":
code = "301"
case "temporary", "":
code = "302"
case "html":
// Script tag comes first since that will better imitate a redirect in the browser's
// history, but the meta tag is a fallback for most non-JS clients.
const metaRedir = `<!DOCTYPE html>
@@ -494,6 +530,15 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
`
safeTo := html.EscapeString(to)
body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo)
code = "302"
default:
codeInt, err := strconv.Atoi(code)
if err != nil {
return nil, h.Errf("Not a supported redir code type or not valid integer: '%s'", code)
}
if codeInt < 300 || codeInt > 399 {
return nil, h.Errf("Redir code not in the 3xx range: '%v'", codeInt)
}
}
return caddyhttp.StaticResponse{
@@ -513,6 +558,25 @@ func parseRespond(h Helper) (caddyhttp.MiddlewareHandler, error) {
return sr, nil
}
// parseAbort parses the abort directive.
func parseAbort(h Helper) (caddyhttp.MiddlewareHandler, error) {
h.Next() // consume directive
for h.Next() || h.NextBlock(0) {
return nil, h.ArgErr()
}
return &caddyhttp.StaticResponse{Abort: true}, nil
}
// parseError parses the error directive.
func parseError(h Helper) (caddyhttp.MiddlewareHandler, error) {
se := new(caddyhttp.StaticError)
err := se.UnmarshalCaddyfile(h.Dispenser)
if err != nil {
return nil, err
}
return se, nil
}
// parseRoute parses the route directive.
func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) {
sr := new(caddyhttp.Subroute)
@@ -566,11 +630,50 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {
// }
//
func parseLog(h Helper) ([]ConfigValue, error) {
return parseLogHelper(h, nil)
}
// parseLogHelper is used both for the parseLog directive within Server Blocks,
// as well as the global "log" option for configuring loggers at the global
// level. The parseAsGlobalOption parameter is used to distinguish any differing logic
// between the two.
func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue, error) {
// When the globalLogNames parameter is passed in, we make
// modifications to the parsing behavior.
parseAsGlobalOption := globalLogNames != nil
var configValues []ConfigValue
for h.Next() {
// log does not currently support any arguments
if h.NextArg() {
return nil, h.ArgErr()
// Logic below expects that a name is always present when a
// global option is being parsed.
var globalLogName string
if parseAsGlobalOption {
if h.NextArg() {
globalLogName = h.Val()
// Only a single argument is supported.
if h.NextArg() {
return nil, h.ArgErr()
}
} else {
// If there is no log name specified, we
// reference the default logger. See the
// setupNewDefault function in the logging
// package for where this is configured.
globalLogName = "default"
}
// Verify this name is unused.
_, used := globalLogNames[globalLogName]
if used {
return nil, h.Err("duplicate global log option for: " + globalLogName)
}
globalLogNames[globalLogName] = struct{}{}
} else {
// No arguments are supported for the server block log directive
if h.NextArg() {
return nil, h.ArgErr()
}
}
cl := new(caddy.CustomLog)
@@ -596,21 +699,15 @@ func parseLog(h Helper) ([]ConfigValue, error) {
case "discard":
wo = caddy.DiscardWriter{}
default:
mod, err := caddy.GetModule("caddy.logging.writers." + moduleName)
if err != nil {
return nil, h.Errf("getting log writer module named '%s': %v", moduleName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, h.Errf("log writer module '%s' is not a Caddyfile unmarshaler", mod)
}
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
modID := "caddy.logging.writers." + moduleName
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
if err != nil {
return nil, err
}
var ok bool
wo, ok = unm.(caddy.WriterOpener)
if !ok {
return nil, h.Errf("module %s is not a WriterOpener", mod)
return nil, h.Errf("module %s (%T) is not a WriterOpener", modID, unm)
}
}
cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings)
@@ -620,21 +717,14 @@ func parseLog(h Helper) ([]ConfigValue, error) {
return nil, h.ArgErr()
}
moduleName := h.Val()
mod, err := caddy.GetModule("caddy.logging.encoders." + moduleName)
if err != nil {
return nil, h.Errf("getting log encoder module named '%s': %v", moduleName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, h.Errf("log encoder module '%s' is not a Caddyfile unmarshaler", mod)
}
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
moduleID := "caddy.logging.encoders." + moduleName
unm, err := caddyfile.UnmarshalModule(h.Dispenser, moduleID)
if err != nil {
return nil, err
}
enc, ok := unm.(zapcore.Encoder)
if !ok {
return nil, h.Errf("module %s is not a zapcore.Encoder", mod)
return nil, h.Errf("module %s (%T) is not a zapcore.Encoder", moduleID, unm)
}
cl.EncoderRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, h.warnings)
@@ -647,22 +737,48 @@ func parseLog(h Helper) ([]ConfigValue, error) {
return nil, h.ArgErr()
}
case "include":
// This configuration is only allowed in the global options
if !parseAsGlobalOption {
return nil, h.ArgErr()
}
for h.NextArg() {
cl.Include = append(cl.Include, h.Val())
}
case "exclude":
// This configuration is only allowed in the global options
if !parseAsGlobalOption {
return nil, h.ArgErr()
}
for h.NextArg() {
cl.Exclude = append(cl.Exclude, h.Val())
}
default:
return nil, h.Errf("unrecognized subdirective: %s", h.Val())
}
}
var val namedCustomLog
// Skip handling of empty logging configs
if !reflect.DeepEqual(cl, new(caddy.CustomLog)) {
logCounter, ok := h.State["logCounter"].(int)
if !ok {
logCounter = 0
if parseAsGlobalOption {
// Use indicated name for global log options
val.name = globalLogName
val.log = cl
} else {
// Construct a log name for server log streams
logCounter, ok := h.State["logCounter"].(int)
if !ok {
logCounter = 0
}
val.name = fmt.Sprintf("log%d", logCounter)
cl.Include = []string{"http.log.access." + val.name}
val.log = cl
logCounter++
h.State["logCounter"] = logCounter
}
val.name = fmt.Sprintf("log%d", logCounter)
cl.Include = []string{"http.log.access." + val.name}
val.log = cl
logCounter++
h.State["logCounter"] = logCounter
}
configValues = append(configValues, ConfigValue{
Class: "custom_log",
+147 -8
View File
@@ -10,7 +10,7 @@ import (
func TestLogDirectiveSyntax(t *testing.T) {
for i, tc := range []struct {
input string
expectWarn bool
output string
expectError bool
}{
{
@@ -18,7 +18,7 @@ func TestLogDirectiveSyntax(t *testing.T) {
log
}
`,
expectWarn: false,
output: `{"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{}}}}}}`,
expectError: false,
},
{
@@ -28,17 +28,35 @@ func TestLogDirectiveSyntax(t *testing.T) {
}
}
`,
expectWarn: false,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"writer":{"filename":"foo.log","output":"file"},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
expectError: false,
},
{
input: `:8080 {
log /foo {
log {
format filter {
wrap console
fields {
common_log delete
request>remote_addr ip_mask {
ipv4 24
ipv6 32
}
}
}
}
}
`,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"encoder":{"fields":{"common_log":{"filter":"delete"},"request\u003eremote_addr":{"filter":"ip_mask","ipv4_cidr":24,"ipv6_cidr":32}},"format":"filter","wrap":{"format":"console"}},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
expectError: false,
},
{
input: `:8080 {
log invalid {
output file foo.log
}
}
`,
expectWarn: false,
expectError: true,
},
} {
@@ -47,13 +65,134 @@ func TestLogDirectiveSyntax(t *testing.T) {
ServerType: ServerType{},
}
_, warnings, err := adapter.Adapt([]byte(tc.input), nil)
out, _, err := adapter.Adapt([]byte(tc.input), nil)
if len(warnings) > 0 != tc.expectWarn {
t.Errorf("Test %d warning expectation failed Expected: %v, got %v", i, tc.expectWarn, warnings)
if err != nil != tc.expectError {
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
continue
}
if string(out) != tc.output {
t.Errorf("Test %d error output mismatch Expected: %s, got %s", i, tc.output, out)
}
}
}
func TestRedirDirectiveSyntax(t *testing.T) {
for i, tc := range []struct {
input string
expectError bool
}{
{
input: `:8080 {
redir :8081
}`,
expectError: false,
},
{
input: `:8080 {
redir * :8081
}`,
expectError: false,
},
{
input: `:8080 {
redir /api/* :8081 300
}`,
expectError: false,
},
{
input: `:8080 {
redir :8081 300
}`,
expectError: false,
},
{
input: `:8080 {
redir /api/* :8081 399
}`,
expectError: false,
},
{
input: `:8080 {
redir :8081 399
}`,
expectError: false,
},
{
input: `:8080 {
redir /old.html /new.html
}`,
expectError: false,
},
{
input: `:8080 {
redir /old.html /new.html temporary
}`,
expectError: false,
},
{
input: `:8080 {
redir https://example.com{uri} permanent
}`,
expectError: false,
},
{
input: `:8080 {
redir /old.html /new.html permanent
}`,
expectError: false,
},
{
input: `:8080 {
redir /old.html /new.html html
}`,
expectError: false,
},
{
input: `:8080 {
redir /old.html /new.html htlm
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 200
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 400
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 temp
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 perm
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 php
}`,
expectError: true,
},
} {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
_, _, err := adapter.Adapt([]byte(tc.input), nil)
if err != nil != tc.expectError {
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
continue
+45 -17
View File
@@ -41,11 +41,12 @@ var directiveOrder = []string{
"root",
"header",
"request_body",
"redir",
"rewrite",
// URI manipulation
"rewrite",
"uri",
"try_files",
@@ -53,15 +54,17 @@ var directiveOrder = []string{
"basicauth",
"request_header",
"encode",
"push",
"templates",
// special routing & dispatching directives
"handle",
"handle_path",
"route",
"push",
// handlers that typically respond to requests
"abort",
"error",
"respond",
"metrics",
"reverse_proxy",
@@ -102,20 +105,11 @@ func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
return nil, h.ArgErr()
}
matcherSet, ok, err := h.MatcherToken()
matcherSet, err := h.ExtractMatcherSet()
if err != nil {
return nil, err
}
if ok {
// strip matcher token; we don't need to
// use the return value here because a
// new dispenser should have been made
// solely for this directive's tokens,
// with no other uses of same slice
h.Dispenser.Delete()
}
h.Dispenser.Reset() // pretend this lookahead never happened
val, err := setupFunc(h)
if err != nil {
return nil, err
@@ -200,7 +194,12 @@ func (h Helper) ExtractMatcherSet() (caddy.ModuleMap, error) {
return nil, err
}
if hasMatcher {
h.Dispenser.Delete() // strip matcher token
// strip matcher token; we don't need to
// use the return value here because a
// new dispenser should have been made
// solely for this directive's tokens,
// with no other uses of same slice
h.Dispenser.Delete()
}
h.Dispenser.Reset() // pretend this lookahead never happened
return matcherSet, nil
@@ -266,6 +265,13 @@ func (h Helper) NewBindAddresses(addrs []string) []ConfigValue {
return []ConfigValue{{Class: "bind", Value: addrs}}
}
// WithDispenser returns a new instance based on d. All others Helper
// fields are copied, so typically maps are shared with this new instance.
func (h Helper) WithDispenser(d *caddyfile.Dispenser) Helper {
h.Dispenser = d
return h
}
// ParseSegmentAsSubroute parses the segment such that its subdirectives
// are themselves treated as directives, from which a subroute is built
// and returned.
@@ -323,7 +329,7 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
dir := seg.Directive()
dirFunc, ok := registeredDirectives[dir]
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
@@ -472,6 +478,27 @@ func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
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
// omits a host portion, i.e. it "catches all" hosts.
func (sb serverBlock) hasHostCatchAllKey() bool {
@@ -501,9 +528,10 @@ type (
UnmarshalHandlerFunc func(h Helper) (caddyhttp.MiddlewareHandler, error)
// UnmarshalGlobalFunc is a function which can unmarshal Caddyfile
// tokens into a global option config value using a Helper type.
// These are passed in a call to RegisterGlobalOption.
UnmarshalGlobalFunc func(d *caddyfile.Dispenser) (interface{}, error)
// tokens from a global option. It is passed the tokens to parse and
// existing value from the previous instance of this global option
// (if any). It returns the value to associate with this global option.
UnmarshalGlobalFunc func(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error)
)
var registeredDirectives = make(map[string]UnmarshalFunc)
+202 -49
View File
@@ -17,6 +17,7 @@ package httpcaddyfile
import (
"encoding/json"
"fmt"
"log"
"reflect"
"regexp"
"sort"
@@ -27,6 +28,7 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddypki"
"github.com/caddyserver/caddy/v2/modules/caddytls"
)
@@ -34,6 +36,17 @@ func init() {
caddyconfig.RegisterAdapter("caddyfile", caddyfile.Adapter{ServerType: ServerType{}})
}
// App represents the configuration for a non-standard
// Caddy app module (e.g. third-party plugin) which was
// parsed from a global options block.
type App struct {
// The JSON key for the app being configured
Name string
// The raw app config as JSON
Value json.RawMessage
}
// ServerType can set up a config from an HTTP Caddyfile.
type ServerType struct {
}
@@ -99,6 +112,8 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
"{tls_client_serial}", "{http.request.tls.client.serial}",
"{tls_client_subject}", "{http.request.tls.client.subject}",
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
)
// these are placeholders that allow a user-defined final
@@ -155,7 +170,11 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
dirFunc, ok := registeredDirectives[dir]
if !ok {
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{
@@ -206,9 +225,10 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
// now that each server is configured, make the HTTP app
httpApp := caddyhttp.App{
HTTPPort: tryInt(options["http_port"], &warnings),
HTTPSPort: tryInt(options["https_port"], &warnings),
Servers: servers,
HTTPPort: tryInt(options["http_port"], &warnings),
HTTPSPort: tryInt(options["https_port"], &warnings),
GracePeriod: tryDuration(options["grace_period"], &warnings),
Servers: servers,
}
// then make the TLS app
@@ -217,30 +237,38 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
return nil, warnings, err
}
// if experimental HTTP/3 is enabled, enable it on each server
if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
for _, srv := range httpApp.Servers {
srv.ExperimentalHTTP3 = true
}
// then make the PKI app
pkiApp, warnings, err := st.buildPKIApp(pairings, options, warnings)
if err != nil {
return nil, warnings, err
}
// extract any custom logs, and enforce configured levels
var customLogs []namedCustomLog
var hasDefaultLog bool
addCustomLog := func(ncl namedCustomLog) {
if ncl.name == "" {
return
}
if ncl.name == "default" {
hasDefaultLog = true
}
if _, ok := options["debug"]; ok && ncl.log.Level == "" {
ncl.log.Level = "DEBUG"
}
customLogs = append(customLogs, ncl)
}
// Apply global log options, when set
if options["log"] != nil {
for _, logValue := range options["log"].([]ConfigValue) {
addCustomLog(logValue.Value.(namedCustomLog))
}
}
// Apply server-specific log options
for _, p := range pairings {
for _, sb := range p.serverBlocks {
for _, clVal := range sb.pile["custom_log"] {
ncl := clVal.Value.(namedCustomLog)
if ncl.name == "" {
continue
}
if ncl.name == "default" {
hasDefaultLog = true
}
if _, ok := options["debug"]; ok && ncl.log.Level == "" {
ncl.log.Level = "DEBUG"
}
customLogs = append(customLogs, ncl)
addCustomLog(clVal.Value.(namedCustomLog))
}
}
}
@@ -258,12 +286,26 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
// annnd the top-level config, then we're done!
cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)}
// loop through the configured options, and if any of
// them are an httpcaddyfile App, then we insert them
// into the config as raw Caddy apps
for _, opt := range options {
if app, ok := opt.(App); ok {
cfg.AppsRaw[app.Name] = app.Value
}
}
// insert the standard Caddy apps into the config
if len(httpApp.Servers) > 0 {
cfg.AppsRaw["http"] = caddyconfig.JSON(httpApp, &warnings)
}
if !reflect.DeepEqual(tlsApp, &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}) {
cfg.AppsRaw["tls"] = caddyconfig.JSON(tlsApp, &warnings)
}
if !reflect.DeepEqual(pkiApp, &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}) {
cfg.AppsRaw["pki"] = caddyconfig.JSON(pkiApp, &warnings)
}
if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok {
cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr,
"module",
@@ -286,7 +328,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
// most users seem to prefer not writing access logs
// to the default log when they are directed to a
// file or have any other special customization
if len(ncl.log.Include) > 0 {
if ncl.name != "default" && len(ncl.log.Include) > 0 {
defaultLog, ok := cfg.Logging.Logs["default"]
if !ok {
defaultLog = new(caddy.CustomLog)
@@ -310,23 +352,68 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
}
for _, segment := range serverBlocks[0].block.Segments {
dir := segment.Directive()
opt := segment.Directive()
var val interface{}
var err error
disp := caddyfile.NewDispenser(segment)
dirFunc, ok := registeredGlobalOptions[dir]
optFunc, ok := registeredGlobalOptions[opt]
if !ok {
tkn := segment[0]
return nil, fmt.Errorf("%s:%d: unrecognized global option: %s", tkn.File, tkn.Line, dir)
return nil, fmt.Errorf("%s:%d: unrecognized global option: %s", tkn.File, tkn.Line, opt)
}
val, err = dirFunc(disp)
val, err = optFunc(disp, options[opt])
if err != nil {
return nil, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err)
return nil, fmt.Errorf("parsing caddyfile tokens for '%s': %v", opt, err)
}
options[dir] = val
// As a special case, fold multiple "servers" options together
// in an array instead of overwriting a possible existing value
if opt == "servers" {
existingOpts, ok := options[opt].([]serverOptions)
if !ok {
existingOpts = []serverOptions{}
}
serverOpts, ok := val.(serverOptions)
if !ok {
return nil, fmt.Errorf("unexpected type from 'servers' global options: %T", val)
}
options[opt] = append(existingOpts, serverOpts)
continue
}
// Additionally, fold multiple "log" options together into an
// array so that multiple loggers can be configured.
if opt == "log" {
existingOpts, ok := options[opt].([]ConfigValue)
if !ok {
existingOpts = []ConfigValue{}
}
logOpts, ok := val.([]ConfigValue)
if !ok {
return nil, fmt.Errorf("unexpected type from 'log' global options: %T", val)
}
options[opt] = append(existingOpts, logOpts...)
continue
}
options[opt] = val
}
// If we got "servers" options, we'll sort them by their listener address
if serverOpts, ok := options["servers"].([]serverOptions); ok {
sort.Slice(serverOpts, func(i, j int) bool {
return len(serverOpts[i].ListenerAddress) > len(serverOpts[j].ListenerAddress)
})
// Reject the config if there are duplicate listener address
seen := make(map[string]bool)
for _, entry := range serverOpts {
if _, alreadySeen := seen[entry.ListenerAddress]; alreadySeen {
return nil, fmt.Errorf("cannot have 'servers' global options with duplicate listener addresses: %s", entry.ListenerAddress)
}
seen[entry.ListenerAddress] = true
}
}
return serverBlocks[1:], nil
@@ -370,6 +457,9 @@ func (st *ServerType) serversFromPairings(
if autoHTTPS == "disable_redirects" {
srv.AutoHTTPS.DisableRedir = true
}
if autoHTTPS == "ignore_loaded_certs" {
srv.AutoHTTPS.IgnoreLoadedCerts = true
}
}
// sort server blocks by their keys; this is important because
@@ -384,7 +474,7 @@ func (st *ServerType) serversFromPairings(
var iLongestHost, jLongestHost string
var iWildcardHost, jWildcardHost bool
for _, addr := range p.serverBlocks[i].keys {
if strings.Contains(addr.Host, "*.") {
if strings.Contains(addr.Host, "*") || addr.Host == "" {
iWildcardHost = true
}
if specificity(addr.Host) > specificity(iLongestHost) {
@@ -395,7 +485,7 @@ func (st *ServerType) serversFromPairings(
}
}
for _, addr := range p.serverBlocks[j].keys {
if strings.Contains(addr.Host, "*.") {
if strings.Contains(addr.Host, "*") || addr.Host == "" {
jWildcardHost = true
}
if specificity(addr.Host) > specificity(jLongestHost) {
@@ -405,9 +495,12 @@ func (st *ServerType) serversFromPairings(
jLongestPath = addr.Path
}
}
// catch-all blocks (blocks with no hostname) should always go
// last, even after blocks with wildcard hosts
if specificity(iLongestHost) == 0 {
return false
}
if specificity(jLongestHost) == 0 {
// catch-all blocks (blocks with no hostname) should always go
// last, even after blocks with wildcard hosts
return true
}
if iWildcardHost != jWildcardHost {
@@ -425,6 +518,25 @@ func (st *ServerType) serversFromPairings(
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
autoHTTPSWillAddConnPolicy := autoHTTPS != "off"
// if a catch-all server block (one which accepts all hostnames) exists in this pairing,
// we need to know that so that we can configure logs properly (see #3878)
var catchAllSblockExists bool
for _, sblock := range p.serverBlocks {
if len(sblock.hostsFromKeys(false)) == 0 {
catchAllSblockExists = true
}
}
// if needed, the ServerLogConfig is initialized beforehand so
// that all server blocks can populate it with data, even when not
// coming with a log directive
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
for _, sblock := range p.serverBlocks {
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
@@ -434,6 +546,13 @@ func (st *ServerType) serversFromPairings(
hosts := sblock.hostsFromKeys(false)
// emit warnings if user put unspecified IP addresses; they probably want the bind directive
for _, h := range hosts {
if h == "0.0.0.0" || h == "::" {
log.Printf("[WARNING] Site block has unspecified IP address %s which only matches requests having that Host header; you probably want the 'bind' directive to configure the socket", h)
}
}
// tls: connection policies
if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
// tls connection policies
@@ -466,16 +585,20 @@ func (st *ServerType) serversFromPairings(
}
for _, addr := range sblock.keys {
// exclude any hosts that were defined explicitly with "http://"
// in the key from automated cert management (issue #2998)
if addr.Scheme == "http" && addr.Host != "" {
if srv.AutoHTTPS == nil {
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
}
if !sliceContains(srv.AutoHTTPS.Skip, addr.Host) {
srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, addr.Host)
// if server only uses HTTPS port, auto-HTTPS will not apply
if listenersUseAnyPortOtherThan(srv.Listen, httpPort) {
// exclude any hosts that were defined explicitly with "http://"
// in the key from automated cert management (issue #2998)
if addr.Scheme == "http" && addr.Host != "" {
if srv.AutoHTTPS == nil {
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
}
if !sliceContains(srv.AutoHTTPS.Skip, addr.Host) {
srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, addr.Host)
}
}
}
// we'll need to remember if the address qualifies for auto-HTTPS, so we
// can add a TLS conn policy if necessary
if addr.Scheme == "https" ||
@@ -528,9 +651,6 @@ func (st *ServerType) serversFromPairings(
sblockLogHosts := sblock.hostsFromKeys(true)
for _, cval := range sblock.pile["custom_log"] {
ncl := cval.Value.(namedCustomLog)
if srv.Logs == nil {
srv.Logs = new(caddyhttp.ServerLogConfig)
}
if sblock.hasHostCatchAllKey() {
// all requests for hosts not able to be listed should use
// this log because it's a catch-all-hosts server block
@@ -538,13 +658,13 @@ func (st *ServerType) serversFromPairings(
} else {
// map each host to the user's desired logger name
for _, h := range sblockLogHosts {
// if the custom logger name is non-empty, add it to
// the map; otherwise, only map to an empty logger
// name if the server block has a catch-all host (in
// which case only requests with mapped hostnames will
// be access-logged, so it'll be necessary to add them
// to the map even if they use default logger)
if ncl.name != "" || len(hosts) == 0 {
// if the custom logger name is non-empty, add it to the map;
// otherwise, only map to an empty logger name if this or
// another site block on this server has a catch-all host (in
// which case only requests with mapped hostnames will be
// access-logged, so it'll be necessary to add them to the
// map even if they use default logger)
if ncl.name != "" || catchAllSblockExists {
if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string)
}
@@ -601,6 +721,11 @@ func (st *ServerType) serversFromPairings(
servers[fmt.Sprintf("srv%d", i)] = srv
}
err := applyServerOptions(servers, options, warnings)
if err != nil {
return nil, err
}
return servers, nil
}
@@ -1134,6 +1259,14 @@ func tryString(val interface{}, warnings *[]caddyconfig.Warning) string {
return stringVal
}
func tryDuration(val interface{}, warnings *[]caddyconfig.Warning) caddy.Duration {
durationVal, ok := val.(caddy.Duration)
if val != nil && !ok && warnings != nil {
*warnings = append(*warnings, caddyconfig.Warning{Message: "not a duration type"})
}
return durationVal
}
// sliceContains returns true if needle is in haystack.
func sliceContains(haystack []string, needle string) bool {
for _, s := range haystack {
@@ -1144,6 +1277,26 @@ func sliceContains(haystack []string, needle string) bool {
return false
}
// listenersUseAnyPortOtherThan returns true if there are any
// listeners in addresses that use a port which is not otherPort.
// Mostly borrowed from unexported method in caddyhttp package.
func listenersUseAnyPortOtherThan(addresses []string, otherPort string) bool {
otherPortInt, err := strconv.Atoi(otherPort)
if err != nil {
return false
}
for _, lnAddr := range addresses {
laddrs, err := caddy.ParseNetworkAddress(lnAddr)
if err != nil {
continue
}
if uint(otherPortInt) > laddrs.EndPort || uint(otherPortInt) < laddrs.StartPort {
return true
}
}
return false
}
// specificity returns len(s) minus any wildcards (*) and
// placeholders ({...}). Basically, it's a length count
// that penalizes the use of wildcards and placeholders.
+2 -28
View File
@@ -9,7 +9,6 @@ import (
func TestMatcherSyntax(t *testing.T) {
for i, tc := range []struct {
input string
expectWarn bool
expectError bool
}{
{
@@ -18,7 +17,6 @@ func TestMatcherSyntax(t *testing.T) {
query showdebug=1
}
`,
expectWarn: false,
expectError: false,
},
{
@@ -27,7 +25,6 @@ func TestMatcherSyntax(t *testing.T) {
query bad format
}
`,
expectWarn: false,
expectError: true,
},
{
@@ -38,7 +35,6 @@ func TestMatcherSyntax(t *testing.T) {
}
}
`,
expectWarn: false,
expectError: false,
},
{
@@ -47,14 +43,12 @@ func TestMatcherSyntax(t *testing.T) {
not path /somepath*
}
`,
expectWarn: false,
expectError: false,
},
{
input: `http://localhost
@debug not path /somepath*
`,
expectWarn: false,
expectError: false,
},
{
@@ -63,7 +57,6 @@ func TestMatcherSyntax(t *testing.T) {
}
http://localhost
`,
expectWarn: false,
expectError: true,
},
} {
@@ -72,12 +65,7 @@ func TestMatcherSyntax(t *testing.T) {
ServerType: ServerType{},
}
_, warnings, err := adapter.Adapt([]byte(tc.input), nil)
if len(warnings) > 0 != tc.expectWarn {
t.Errorf("Test %d warning expectation failed Expected: %v, got %v", i, tc.expectWarn, warnings)
continue
}
_, _, err := adapter.Adapt([]byte(tc.input), nil)
if err != nil != tc.expectError {
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
@@ -119,7 +107,6 @@ func TestSpecificity(t *testing.T) {
func TestGlobalOptions(t *testing.T) {
for i, tc := range []struct {
input string
expectWarn bool
expectError bool
}{
{
@@ -129,7 +116,6 @@ func TestGlobalOptions(t *testing.T) {
}
:80
`,
expectWarn: false,
expectError: false,
},
{
@@ -139,7 +125,6 @@ func TestGlobalOptions(t *testing.T) {
}
:80
`,
expectWarn: false,
expectError: false,
},
{
@@ -149,7 +134,6 @@ func TestGlobalOptions(t *testing.T) {
}
:80
`,
expectWarn: false,
expectError: false,
},
{
@@ -161,7 +145,6 @@ func TestGlobalOptions(t *testing.T) {
}
:80
`,
expectWarn: false,
expectError: true,
},
{
@@ -174,7 +157,6 @@ func TestGlobalOptions(t *testing.T) {
}
:80
`,
expectWarn: false,
expectError: false,
},
{
@@ -187,7 +169,6 @@ func TestGlobalOptions(t *testing.T) {
}
:80
`,
expectWarn: false,
expectError: false,
},
{
@@ -200,7 +181,6 @@ func TestGlobalOptions(t *testing.T) {
}
:80
`,
expectWarn: false,
expectError: true,
},
{
@@ -213,7 +193,6 @@ func TestGlobalOptions(t *testing.T) {
}
:80
`,
expectWarn: false,
expectError: true,
},
} {
@@ -222,12 +201,7 @@ func TestGlobalOptions(t *testing.T) {
ServerType: ServerType{},
}
_, warnings, err := adapter.Adapt([]byte(tc.input), nil)
if len(warnings) > 0 != tc.expectWarn {
t.Errorf("Test %d warning expectation failed Expected: %v, got %v", i, tc.expectWarn, warnings)
continue
}
_, _, err := adapter.Adapt([]byte(tc.input), nil)
if err != nil != tc.expectError {
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
+147 -50
View File
@@ -18,6 +18,7 @@ import (
"strconv"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
@@ -28,28 +29,32 @@ func init() {
RegisterGlobalOption("debug", parseOptTrue)
RegisterGlobalOption("http_port", parseOptHTTPPort)
RegisterGlobalOption("https_port", parseOptHTTPSPort)
RegisterGlobalOption("grace_period", parseOptDuration)
RegisterGlobalOption("default_sni", parseOptSingleString)
RegisterGlobalOption("order", parseOptOrder)
RegisterGlobalOption("experimental_http3", parseOptTrue)
RegisterGlobalOption("storage", parseOptStorage)
RegisterGlobalOption("storage_clean_interval", parseOptDuration)
RegisterGlobalOption("acme_ca", parseOptSingleString)
RegisterGlobalOption("acme_ca_root", parseOptSingleString)
RegisterGlobalOption("acme_dns", parseOptSingleString)
RegisterGlobalOption("acme_dns", parseOptACMEDNS)
RegisterGlobalOption("acme_eab", parseOptACMEEAB)
RegisterGlobalOption("cert_issuer", parseOptCertIssuer)
RegisterGlobalOption("skip_install_trust", parseOptTrue)
RegisterGlobalOption("email", parseOptSingleString)
RegisterGlobalOption("admin", parseOptAdmin)
RegisterGlobalOption("on_demand_tls", parseOptOnDemand)
RegisterGlobalOption("local_certs", parseOptTrue)
RegisterGlobalOption("key_type", parseOptSingleString)
RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
RegisterGlobalOption("servers", parseServerOptions)
RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
RegisterGlobalOption("log", parseLogOptions)
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
}
func parseOptTrue(d *caddyfile.Dispenser) (interface{}, error) {
return true, nil
}
func parseOptTrue(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { return true, nil }
func parseOptHTTPPort(d *caddyfile.Dispenser) (interface{}, error) {
func parseOptHTTPPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
var httpPort int
for d.Next() {
var httpPortStr string
@@ -65,7 +70,7 @@ func parseOptHTTPPort(d *caddyfile.Dispenser) (interface{}, error) {
return httpPort, nil
}
func parseOptHTTPSPort(d *caddyfile.Dispenser) (interface{}, error) {
func parseOptHTTPSPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
var httpsPort int
for d.Next() {
var httpsPortStr string
@@ -81,7 +86,7 @@ func parseOptHTTPSPort(d *caddyfile.Dispenser) (interface{}, error) {
return httpsPort, nil
}
func parseOptOrder(d *caddyfile.Dispenser) (interface{}, error) {
func parseOptOrder(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
newOrder := directiveOrder
for d.Next() {
@@ -157,34 +162,59 @@ func parseOptOrder(d *caddyfile.Dispenser) (interface{}, error) {
return newOrder, nil
}
func parseOptStorage(d *caddyfile.Dispenser) (interface{}, error) {
func parseOptStorage(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
if !d.Next() { // get storage module name
return nil, d.ArgErr()
}
modName := d.Val()
mod, err := caddy.GetModule("caddy.storage." + modName)
if err != nil {
return nil, d.Errf("getting storage module '%s': %v", modName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, d.Errf("storage module '%s' is not a Caddyfile unmarshaler", mod.ID)
}
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
modID := "caddy.storage." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
storage, ok := unm.(caddy.StorageConverter)
if !ok {
return nil, d.Errf("module %s is not a StorageConverter", mod.ID)
return nil, d.Errf("module %s is not a caddy.StorageConverter", modID)
}
return storage, nil
}
func parseOptACMEEAB(d *caddyfile.Dispenser) (interface{}, error) {
func parseOptDuration(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
if !d.Next() { // get duration value
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, err
}
return caddy.Duration(dur), nil
}
func parseOptACMEDNS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
if !d.Next() { // get DNS module name
return nil, d.ArgErr()
}
modID := "dns.providers." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
prov, ok := unm.(certmagic.ACMEDNSProvider)
if !ok {
return nil, d.Errf("module %s (%T) is not a certmagic.ACMEDNSProvider", modID, unm)
}
return prov, nil
}
func parseOptACMEEAB(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
eab := new(acme.EAB)
for d.Next() {
if d.NextArg() {
@@ -212,34 +242,30 @@ func parseOptACMEEAB(d *caddyfile.Dispenser) (interface{}, error) {
return eab, nil
}
func parseOptCertIssuer(d *caddyfile.Dispenser) (interface{}, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
func parseOptCertIssuer(d *caddyfile.Dispenser, existing interface{}) (interface{}, error) {
var issuers []certmagic.Issuer
if existing != nil {
issuers = existing.([]certmagic.Issuer)
}
if !d.Next() { // get issuer module name
return nil, d.ArgErr()
for d.Next() { // consume option name
if !d.Next() { // get issuer module name
return nil, d.ArgErr()
}
modID := "tls.issuance." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
iss, ok := unm.(certmagic.Issuer)
if !ok {
return nil, d.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm)
}
issuers = append(issuers, iss)
}
modName := d.Val()
mod, err := caddy.GetModule("tls.issuance." + modName)
if err != nil {
return nil, d.Errf("getting issuer module '%s': %v", modName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, d.Errf("issuer module '%s' is not a Caddyfile unmarshaler", mod.ID)
}
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
if err != nil {
return nil, err
}
iss, ok := unm.(certmagic.Issuer)
if !ok {
return nil, d.Errf("module %s is not a certmagic.Issuer", mod.ID)
}
return iss, nil
return issuers, nil
}
func parseOptSingleString(d *caddyfile.Dispenser) (interface{}, error) {
func parseOptSingleString(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() // consume parameter name
if !d.Next() {
return "", d.ArgErr()
@@ -251,7 +277,7 @@ func parseOptSingleString(d *caddyfile.Dispenser) (interface{}, error) {
return val, nil
}
func parseOptAdmin(d *caddyfile.Dispenser) (interface{}, error) {
func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
adminCfg := new(caddy.AdminConfig)
for d.Next() {
if d.NextArg() {
@@ -287,7 +313,7 @@ func parseOptAdmin(d *caddyfile.Dispenser) (interface{}, error) {
return adminCfg, nil
}
func parseOptOnDemand(d *caddyfile.Dispenser) (interface{}, error) {
func parseOptOnDemand(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
var ond *caddytls.OnDemandConfig
for d.Next() {
if d.NextArg() {
@@ -347,7 +373,7 @@ func parseOptOnDemand(d *caddyfile.Dispenser) (interface{}, error) {
return ond, nil
}
func parseOptAutoHTTPS(d *caddyfile.Dispenser) (interface{}, error) {
func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() // consume parameter name
if !d.Next() {
return "", d.ArgErr()
@@ -356,8 +382,79 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser) (interface{}, error) {
if d.Next() {
return "", d.ArgErr()
}
if val != "off" && val != "disable_redirects" {
return "", d.Errf("auto_https must be either 'off' or 'disable_redirects'")
if val != "off" && val != "disable_redirects" && val != "ignore_loaded_certs" {
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects' or 'ignore_loaded_certs'")
}
return val, nil
}
func parseServerOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
return unmarshalCaddyfileServerOptions(d)
}
func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() // consume option name
var val string
if !d.AllArgs(&val) {
return nil, d.ArgErr()
}
if val != "off" {
return nil, d.Errf("invalid argument '%s'", val)
}
return certmagic.OCSPConfig{
DisableStapling: val == "off",
}, nil
}
// parseLogOptions parses the global log option. Syntax:
//
// log [name] {
// output <writer_module> ...
// format <encoder_module> ...
// level <level>
// include <namespaces...>
// exclude <namespaces...>
// }
//
// When the name argument is unspecified, this directive modifies the default
// logger.
//
func parseLogOptions(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) {
currentNames := make(map[string]struct{})
if existingVal != nil {
innerVals, ok := existingVal.([]ConfigValue)
if !ok {
return nil, d.Errf("existing log values of unexpected type: %T", existingVal)
}
for _, rawVal := range innerVals {
val, ok := rawVal.Value.(namedCustomLog)
if !ok {
return nil, d.Errf("existing log value of unexpected type: %T", existingVal)
}
currentNames[val.name] = struct{}{}
}
}
var warnings []caddyconfig.Warning
// Call out the same parser that handles server-specific log configuration.
configValues, err := parseLogHelper(
Helper{
Dispenser: d,
warnings: &warnings,
},
currentNames,
)
if err != nil {
return nil, err
}
if len(warnings) > 0 {
return nil, d.Errf("warnings found in parsing global log options: %+v", warnings)
}
return configValues, nil
}
func parseOptPreferredChains(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next()
return caddytls.ParseCaddyfilePreferredChainsOptions(d)
}
+64
View File
@@ -0,0 +1,64 @@
package httpcaddyfile
import (
"testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
_ "github.com/caddyserver/caddy/v2/modules/logging"
)
func TestGlobalLogOptionSyntax(t *testing.T) {
for i, tc := range []struct {
input string
output string
expectError bool
}{
// NOTE: Additional test cases of successful Caddyfile parsing
// are present in: caddytest/integration/caddyfile_adapt/
{
input: `{
log default
}
`,
output: `{}`,
expectError: false,
},
{
input: `{
log example {
output file foo.log
}
log example {
format json
}
}
`,
expectError: true,
},
{
input: `{
log example /foo {
output file foo.log
}
}
`,
expectError: true,
},
} {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
out, _, err := adapter.Adapt([]byte(tc.input), nil)
if err != nil != tc.expectError {
t.Errorf("Test %d error expectation failed Expected: %v, got %v", i, tc.expectError, err)
continue
}
if string(out) != tc.output {
t.Errorf("Test %d error output mismatch Expected: %s, got %s", i, tc.output, out)
}
}
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpcaddyfile
import (
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddypki"
)
func (st ServerType) buildPKIApp(
pairings []sbAddrAssociation,
options map[string]interface{},
warnings []caddyconfig.Warning,
) (*caddypki.PKI, []caddyconfig.Warning, error) {
pkiApp := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
skipInstallTrust := false
if _, ok := options["skip_install_trust"]; ok {
skipInstallTrust = true
}
falseBool := false
for _, p := range pairings {
for _, sblock := range p.serverBlocks {
// 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"] {
ca := caCfgValue.Value.(*caddypki.CA)
if skipInstallTrust {
ca.InstallTrust = &falseBool
}
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
}
+228
View File
@@ -0,0 +1,228 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpcaddyfile
import (
"encoding/json"
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/dustin/go-humanize"
)
// serverOptions collects server config overrides parsed from Caddyfile global options
type serverOptions struct {
// If set, will only apply these options to servers that contain a
// listener address that matches exactly. If empty, will apply to all
// servers that were not already matched by another serverOptions.
ListenerAddress string
// These will all map 1:1 to the caddyhttp.Server struct
ListenerWrappersRaw []json.RawMessage
ReadTimeout caddy.Duration
ReadHeaderTimeout caddy.Duration
WriteTimeout caddy.Duration
IdleTimeout caddy.Duration
MaxHeaderBytes int
AllowH2C bool
ExperimentalHTTP3 bool
StrictSNIHost *bool
}
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
serverOpts := serverOptions{}
for d.Next() {
if d.NextArg() {
serverOpts.ListenerAddress = d.Val()
if d.NextArg() {
return nil, d.ArgErr()
}
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "listener_wrappers":
for nesting := d.Nesting(); d.NextBlock(nesting); {
modID := "caddy.listeners." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
listenerWrapper, ok := unm.(caddy.ListenerWrapper)
if !ok {
return nil, fmt.Errorf("module %s (%T) is not a listener wrapper", modID, unm)
}
jsonListenerWrapper := caddyconfig.JSONModuleObject(
listenerWrapper,
"wrapper",
listenerWrapper.(caddy.Module).CaddyModule().ID.Name(),
nil,
)
serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper)
}
case "timeouts":
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "read_body":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing read_body timeout duration: %v", err)
}
serverOpts.ReadTimeout = caddy.Duration(dur)
case "read_header":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing read_header timeout duration: %v", err)
}
serverOpts.ReadHeaderTimeout = caddy.Duration(dur)
case "write":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing write timeout duration: %v", err)
}
serverOpts.WriteTimeout = caddy.Duration(dur)
case "idle":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing idle timeout duration: %v", err)
}
serverOpts.IdleTimeout = caddy.Duration(dur)
default:
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
}
}
case "max_header_size":
var sizeStr string
if !d.AllArgs(&sizeStr) {
return nil, d.ArgErr()
}
size, err := humanize.ParseBytes(sizeStr)
if err != nil {
return nil, d.Errf("parsing max_header_size: %v", err)
}
serverOpts.MaxHeaderBytes = int(size)
case "protocol":
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "allow_h2c":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.AllowH2C = true
case "experimental_http3":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.ExperimentalHTTP3 = true
case "strict_sni_host":
if d.NextArg() {
return nil, d.ArgErr()
}
trueBool := true
serverOpts.StrictSNIHost = &trueBool
default:
return nil, d.Errf("unrecognized protocol option '%s'", d.Val())
}
}
default:
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
}
}
}
return serverOpts, nil
}
// applyServerOptions sets the server options on the appropriate servers
func applyServerOptions(
servers map[string]*caddyhttp.Server,
options map[string]interface{},
warnings *[]caddyconfig.Warning,
) error {
// If experimental HTTP/3 is enabled, enable it on each server.
// We already know there won't be a conflict with serverOptions because
// we validated earlier that "experimental_http3" cannot be set at the same
// time as "servers"
if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
*warnings = append(*warnings, caddyconfig.Warning{Message: "the 'experimental_http3' global option is deprecated, please use the 'servers > protocol > experimental_http3' option instead"})
for _, srv := range servers {
srv.ExperimentalHTTP3 = true
}
}
serverOpts, ok := options["servers"].([]serverOptions)
if !ok {
return nil
}
for _, server := range servers {
// find the options that apply to this server
opts := func() *serverOptions {
for _, entry := range serverOpts {
if entry.ListenerAddress == "" {
return &entry
}
for _, listener := range server.Listen {
if entry.ListenerAddress == listener {
return &entry
}
}
}
return nil
}()
// if none apply, then move to the next server
if opts == nil {
continue
}
// set all the options
server.ListenerWrappersRaw = opts.ListenerWrappersRaw
server.ReadTimeout = opts.ReadTimeout
server.ReadHeaderTimeout = opts.ReadHeaderTimeout
server.WriteTimeout = opts.WriteTimeout
server.IdleTimeout = opts.IdleTimeout
server.MaxHeaderBytes = opts.MaxHeaderBytes
server.AllowH2C = opts.AllowH2C
server.ExperimentalHTTP3 = opts.ExperimentalHTTP3
server.StrictSNIHost = opts.StrictSNIHost
}
return nil
}
+358 -217
View File
@@ -40,6 +40,10 @@ func (st ServerType) buildTLSApp(
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
var certLoaders []caddytls.CertificateLoader
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp)
}
httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
if hsp, ok := options["https_port"].(int); ok {
httpsPort = strconv.Itoa(hsp)
@@ -50,7 +54,7 @@ func (st ServerType) buildTLSApp(
// a hostless key, so that they don't get forgotten/omitted
// by auto-HTTPS (since they won't appear in route matchers)
var serverBlocksWithTLSHostlessKey int
hostsSharedWithHostlessKey := make(map[string]struct{})
httpsHostsSharedWithHostlessKey := make(map[string]struct{})
for _, pair := range pairings {
for _, sb := range pair.serverBlocks {
for _, addr := range sb.keys {
@@ -66,8 +70,8 @@ func (st ServerType) buildTLSApp(
if otherAddr.Original == addr.Original {
continue
}
if otherAddr.Host != "" {
hostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
}
}
break
@@ -76,166 +80,158 @@ func (st ServerType) buildTLSApp(
}
}
// a catch-all automation policy is used as a "default" for all subjects that
// don't have custom configuration explicitly associated with them; this
// is only to add if the global settings or defaults are non-empty
catchAllAP, err := newBaseAutomationPolicy(options, warnings, false)
if err != nil {
return nil, warnings, err
}
if catchAllAP != nil {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
}
for _, p := range pairings {
// avoid setting up TLS automation policies for a server that is HTTP-only
if !listenersUseAnyPortOtherThan(p.addresses, httpPort) {
continue
}
for _, sblock := range p.serverBlocks {
// get values that populate an automation policy for this block
var ap *caddytls.AutomationPolicy
ap, err := newBaseAutomationPolicy(options, warnings, true)
if err != nil {
return nil, warnings, err
}
sblockHosts := sblock.hostsFromKeys(false)
if len(sblockHosts) == 0 {
if len(sblockHosts) == 0 && catchAllAP != nil {
ap = catchAllAP
}
// on-demand tls
if _, ok := sblock.pile["tls.on_demand"]; ok {
if ap == nil {
var err error
ap, err = newBaseAutomationPolicy(options, warnings, true)
if err != nil {
return nil, warnings, err
}
}
ap.OnDemand = true
}
if keyTypeVals, ok := sblock.pile["tls.key_type"]; ok {
ap.KeyType = keyTypeVals[0].Value.(string)
}
// certificate issuers
if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
var issuers []certmagic.Issuer
for _, issuerVal := range issuerVals {
issuer := issuerVal.Value.(certmagic.Issuer)
if ap == nil {
var err error
ap, err = newBaseAutomationPolicy(options, warnings, true)
if err != nil {
return nil, warnings, err
}
}
if ap == catchAllAP && !reflect.DeepEqual(ap.Issuer, issuer) {
return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuer, issuer)
}
ap.Issuer = issuer
issuers = append(issuers, issuerVal.Value.(certmagic.Issuer))
}
if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) {
return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers)
}
ap.Issuers = issuers
}
// custom bind host
for _, cfgVal := range sblock.pile["bind"] {
// either an existing issuer is already configured (and thus, ap is not
// nil), or we need to configure an issuer, so we need ap to be non-nil
if ap == nil {
ap, err = newBaseAutomationPolicy(options, warnings, true)
if err != nil {
return nil, warnings, err
}
}
// if an issuer was already configured and it is NOT an ACME
// issuer, skip, since we intend to adjust only ACME issuers
var acmeIssuer *caddytls.ACMEIssuer
if ap.Issuer != nil {
// ensure we include any issuer that embeds/wraps an underlying ACME issuer
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
if acmeWrapper, ok := ap.Issuer.(acmeCapable); ok {
for _, iss := range ap.Issuers {
// if an issuer was already configured and it is NOT an ACME issuer,
// skip, since we intend to adjust only ACME issuers; ensure we
// include any issuer that embeds/wraps an underlying ACME issuer
var acmeIssuer *caddytls.ACMEIssuer
if acmeWrapper, ok := iss.(acmeCapable); ok {
acmeIssuer = acmeWrapper.GetACMEIssuer()
} else {
break
}
}
if acmeIssuer == nil {
continue
}
// proceed to configure the ACME issuer's bind host, without
// overwriting any existing settings
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.BindHost == "" {
// only binding to one host is supported
var bindHost string
if bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 {
bindHost = bindHosts[0]
// proceed to configure the ACME issuer's bind host, without
// overwriting any existing settings
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.BindHost == "" {
// only binding to one host is supported
var bindHost string
if bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 {
bindHost = bindHosts[0]
}
acmeIssuer.Challenges.BindHost = bindHost
}
acmeIssuer.Challenges.BindHost = bindHost
}
ap.Issuer = acmeIssuer // we'll encode it later
}
if ap != nil {
if ap.Issuer != nil {
// encode issuer now that it's all set up
issuerName := ap.Issuer.(caddy.Module).CaddyModule().ID.Name()
ap.IssuerRaw = caddyconfig.JSONModuleObject(ap.Issuer, "module", issuerName, &warnings)
// first make sure this block is allowed to create an automation policy;
// doing so is forbidden if it has a key with no host (i.e. ":443")
// and if there is a different server block that also has a key with no
// host -- since a key with no host matches any host, we need its
// associated automation policy to have an empty Subjects list, i.e. no
// host filter, which is indistinguishable between the two server blocks
// because automation is not done in the context of a particular server...
// this is an example of a poor mapping from Caddyfile to JSON but that's
// the least-leaky abstraction I could figure out
if len(sblockHosts) == 0 {
if serverBlocksWithTLSHostlessKey > 1 {
// this server block and at least one other has a key with no host,
// making the two indistinguishable; it is misleading to define such
// a policy within one server block since it actually will apply to
// others as well
return nil, warnings, fmt.Errorf("cannot make a TLS automation policy from a server block that has a host-less address when there are other TLS-enabled server block addresses lacking a host")
}
if catchAllAP == nil {
// this server block has a key with no hosts, but there is not yet
// a catch-all automation policy (probably because no global options
// were set), so this one becomes it
catchAllAP = ap
}
}
// first make sure this block is allowed to create an automation policy;
// doing so is forbidden if it has a key with no host (i.e. ":443")
// and if there is a different server block that also has a key with no
// host -- since a key with no host matches any host, we need its
// associated automation policy to have an empty Subjects list, i.e. no
// host filter, which is indistinguishable between the two server blocks
// because automation is not done in the context of a particular server...
// this is an example of a poor mapping from Caddyfile to JSON but that's
// the least-leaky abstraction I could figure out
if len(sblockHosts) == 0 {
if serverBlocksWithTLSHostlessKey > 1 {
// this server block and at least one other has a key with no host,
// making the two indistinguishable; it is misleading to define such
// a policy within one server block since it actually will apply to
// others as well
return nil, warnings, fmt.Errorf("cannot make a TLS automation policy from a server block that has a host-less address when there are other TLS-enabled server block addresses lacking a host")
// associate our new automation policy with this server block's hosts
ap.Subjects = sblock.hostsFromKeysNotHTTP(httpPort)
sort.Strings(ap.Subjects) // solely for deterministic test results
// if a combination of public and internal names were given
// for this same server block and no issuer was specified, we
// need to separate them out in the automation policies so
// that the internal names can use the internal issuer and
// the other names can use the default/public/ACME issuer
var ap2 *caddytls.AutomationPolicy
if len(ap.Issuers) == 0 {
var internal, external []string
for _, s := range ap.Subjects {
if !certmagic.SubjectQualifiesForCert(s) {
return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s)
}
if catchAllAP == nil {
// this server block has a key with no hosts, but there is not yet
// a catch-all automation policy (probably because no global options
// were set), so this one becomes it
catchAllAP = ap
// we don't use certmagic.SubjectQualifiesForPublicCert() because of one nuance:
// names like *.*.tld that may not qualify for a public certificate are actually
// fine when used with OnDemand, since OnDemand (currently) does not obtain
// wildcards (if it ever does, there will be a separate config option to enable
// it that we would need to check here) since the hostname is known at handshake;
// and it is unexpected to switch to internal issuer when the user wants to get
// regular certificates on-demand for a class of certs like *.*.tld.
if subjectQualifiesForPublicCert(ap, s) {
external = append(external, s)
} else {
internal = append(internal, s)
}
}
// associate our new automation policy with this server block's hosts,
// unless, of course, the server block has a key with no hosts, in which
// case its automation policy becomes or blends with the default/global
// automation policy because, of necessity, it applies to all hostnames
// (i.e. it has no Subjects filter) -- in that case, we'll append it last
if ap != catchAllAP {
ap.Subjects = sblockHosts
// if a combination of public and internal names were given
// for this same server block and no issuer was specified, we
// need to separate them out in the automation policies so
// that the internal names can use the internal issuer and
// the other names can use the default/public/ACME issuer
var ap2 *caddytls.AutomationPolicy
if ap.Issuer == nil {
var internal, external []string
for _, s := range ap.Subjects {
if certmagic.SubjectQualifiesForPublicCert(s) {
external = append(external, s)
} else {
internal = append(internal, s)
}
}
if len(external) > 0 && len(internal) > 0 {
ap.Subjects = external
apCopy := *ap
ap2 = &apCopy
ap2.Subjects = internal
ap2.IssuerRaw = caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)
}
}
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
if ap2 != nil {
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap2)
}
if len(external) > 0 && len(internal) > 0 {
ap.Subjects = external
apCopy := *ap
ap2 = &apCopy
ap2.Subjects = internal
ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
}
}
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
if ap2 != nil {
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap2)
}
// certificate loaders
if clVals, ok := sblock.pile["tls.cert_loader"]; ok {
@@ -282,6 +278,14 @@ func (st ServerType) buildTLSApp(
tlsApp.Automation.OnDemand = onDemand
}
// set the storage clean interval if configured
if storageCleanInterval, ok := options["storage_clean_interval"].(caddy.Duration); ok {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.StorageCleanInterval = storageCleanInterval
}
// if any hostnames appear on the same server block as a key with
// no host, they will not be used with route matchers because the
// hostless key matches all hosts, therefore, it wouldn't be
@@ -291,9 +295,9 @@ func (st ServerType) buildTLSApp(
// get internal certificates by default rather than ACME
var al caddytls.AutomateLoader
internalAP := &caddytls.AutomationPolicy{
IssuerRaw: json.RawMessage(`{"module":"internal"}`),
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
}
for h := range hostsSharedWithHostlessKey {
for h := range httpsHostsSharedWithHostlessKey {
al = append(al, h)
if !certmagic.SubjectQualifiesForPublicCert(h) {
internalAP.Subjects = append(internalAP.Subjects, h)
@@ -309,23 +313,58 @@ func (st ServerType) buildTLSApp(
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, internalAP)
}
// if there is a global/catch-all automation policy, ensure it goes last
if catchAllAP != nil {
// first, encode its issuer, if there is one
if catchAllAP.Issuer != nil {
issuerName := catchAllAP.Issuer.(caddy.Module).CaddyModule().ID.Name()
catchAllAP.IssuerRaw = caddyconfig.JSONModuleObject(catchAllAP.Issuer, "module", issuerName, &warnings)
}
// if there are any global options set for issuers (ACME ones in particular), make sure they
// take effect in every automation policy that does not have any issuers
if tlsApp.Automation != nil {
globalEmail := options["email"]
globalACMECA := options["acme_ca"]
globalACMECARoot := options["acme_ca_root"]
globalACMEDNS := options["acme_dns"]
globalACMEEAB := options["acme_eab"]
globalPreferredChains := options["preferred_chains"]
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil
if hasGlobalACMEDefaults {
// for _, ap := range tlsApp.Automation.Policies {
for i := 0; i < len(tlsApp.Automation.Policies); i++ {
ap := tlsApp.Automation.Policies[i]
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
// for public names, create default issuers which will later be filled in with configured global defaults
// (internal names will implicitly use the internal issuer at auto-https time)
ap.Issuers = caddytls.DefaultIssuers()
// then append it to the end of the policies list
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
// if a specific endpoint is configured, can't use multiple default issuers
if globalACMECA != nil {
if strings.Contains(globalACMECA.(string), "zerossl") {
ap.Issuers = []certmagic.Issuer{&caddytls.ZeroSSLIssuer{ACMEIssuer: new(caddytls.ACMEIssuer)}}
} else {
ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
}
}
}
}
}
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
}
// do a little verification & cleanup
// finalize and verify policies; do cleanup
if tlsApp.Automation != nil {
for i, ap := range tlsApp.Automation.Policies {
// ensure all issuers have global defaults filled in
for j, issuer := range ap.Issuers {
err := fillInGlobalACMEDefaults(issuer, options)
if err != nil {
return nil, warnings, fmt.Errorf("filling in global issuer defaults for AP %d, issuer %d: %v", i, j, err)
}
}
// encode all issuer values we created, so they will be rendered in the output
if len(ap.Issuers) > 0 && ap.IssuersRaw == nil {
for _, iss := range ap.Issuers {
issuerName := iss.(caddy.Module).CaddyModule().ID.Name()
ap.IssuersRaw = append(ap.IssuersRaw, caddyconfig.JSONModuleObject(iss, "module", issuerName, &warnings))
}
}
}
// consolidate automation policies that are the exact same
tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
@@ -341,29 +380,75 @@ func (st ServerType) buildTLSApp(
automationHostSet[s] = struct{}{}
}
}
// if nothing remains, remove any excess values to clean up the resulting config
if len(tlsApp.Automation.Policies) == 0 {
tlsApp.Automation.Policies = nil
}
if reflect.DeepEqual(tlsApp.Automation, new(caddytls.AutomationConfig)) {
tlsApp.Automation = nil
}
}
return tlsApp, warnings, nil
}
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interface{}) error {
acmeWrapper, ok := issuer.(acmeCapable)
if !ok {
return nil
}
acmeIssuer := acmeWrapper.GetACMEIssuer()
if acmeIssuer == nil {
return nil
}
globalEmail := options["email"]
globalACMECA := options["acme_ca"]
globalACMECARoot := options["acme_ca_root"]
globalACMEDNS := options["acme_dns"]
globalACMEEAB := options["acme_eab"]
globalPreferredChains := options["preferred_chains"]
if globalEmail != nil && acmeIssuer.Email == "" {
acmeIssuer.Email = globalEmail.(string)
}
if globalACMECA != nil && acmeIssuer.CA == "" {
acmeIssuer.CA = globalACMECA.(string)
}
if globalACMECARoot != nil && !sliceContains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
}
if globalACMEDNS != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) {
acmeIssuer.Challenges = &caddytls.ChallengesConfig{
DNS: &caddytls.DNSChallengeConfig{
ProviderRaw: caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil),
},
}
}
if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB)
}
if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil {
acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference)
}
return nil
}
// newBaseAutomationPolicy returns a new TLS automation policy that gets
// its values from the global options map. It should be used as the base
// for any other automation policies. A nil policy (and no error) will be
// returned if there are no default/global options. However, if always is
// true, a non-nil value will always be returned (unless there is an error).
func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
issuer, hasIssuer := options["cert_issuer"]
acmeCA, hasACMECA := options["acme_ca"]
acmeCARoot, hasACMECARoot := options["acme_ca_root"]
acmeDNS, hasACMEDNS := options["acme_dns"]
acmeEAB, hasACMEEAB := options["acme_eab"]
email, hasEmail := options["email"]
localCerts, hasLocalCerts := options["local_certs"]
issuers, hasIssuers := options["cert_issuer"]
_, hasLocalCerts := options["local_certs"]
keyType, hasKeyType := options["key_type"]
ocspStapling, hasOCSPStapling := options["ocsp_stapling"]
hasGlobalAutomationOpts := hasIssuer || hasACMECA || hasACMECARoot || hasACMEDNS || hasACMEEAB || hasEmail || hasLocalCerts || hasKeyType
hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling
// if there are no global options related to automation policies
// set, then we can just return right away
@@ -375,80 +460,76 @@ func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddycon
}
ap := new(caddytls.AutomationPolicy)
if keyType != nil {
if hasKeyType {
ap.KeyType = keyType.(string)
}
if hasIssuer {
if hasACMECA || hasACMEDNS || hasACMEEAB || hasEmail || hasLocalCerts {
return nil, fmt.Errorf("global options are ambiguous: cert_issuer is confusing when combined with acme_*, email, or local_certs options")
}
ap.Issuer = issuer.(certmagic.Issuer)
} else if localCerts != nil {
// internal issuer enabled trumps any ACME configurations; useful in testing
ap.Issuer = new(caddytls.InternalIssuer) // we'll encode it later
} else {
if acmeCA == nil {
acmeCA = ""
}
if email == nil {
email = ""
}
mgr := &caddytls.ACMEIssuer{
CA: acmeCA.(string),
Email: email.(string),
}
if acmeDNS != nil {
provName := acmeDNS.(string)
dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
if err != nil {
return nil, fmt.Errorf("getting DNS provider module named '%s': %v", provName, err)
}
mgr.Challenges = &caddytls.ChallengesConfig{
DNS: &caddytls.DNSChallengeConfig{
ProviderRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "name", provName, &warnings),
},
}
}
if acmeCARoot != nil {
mgr.TrustedRootsPEMFiles = []string{acmeCARoot.(string)}
}
if acmeEAB != nil {
mgr.ExternalAccount = acmeEAB.(*acme.EAB)
}
ap.Issuer = disambiguateACMEIssuer(mgr) // we'll encode it later
if hasIssuers && hasLocalCerts {
return nil, fmt.Errorf("global options are ambiguous: local_certs is confusing when combined with cert_issuer, because local_certs is also a specific kind of issuer")
}
if hasIssuers {
ap.Issuers = issuers.([]certmagic.Issuer)
} else if hasLocalCerts {
ap.Issuers = []certmagic.Issuer{new(caddytls.InternalIssuer)}
}
if hasOCSPStapling {
ocspConfig := ocspStapling.(certmagic.OCSPConfig)
ap.DisableOCSPStapling = ocspConfig.DisableStapling
ap.OCSPOverrides = ocspConfig.ResponderOverrides
}
return ap, nil
}
// disambiguateACMEIssuer returns an issuer based on the properties of acmeIssuer.
// If acmeIssuer implicitly configures a certain kind of ACMEIssuer (for example,
// ZeroSSL), the proper wrapper over acmeIssuer will be returned instead.
func disambiguateACMEIssuer(acmeIssuer *caddytls.ACMEIssuer) certmagic.Issuer {
// as a special case, we integrate with ZeroSSL's ACME endpoint if it looks like an
// implicit ZeroSSL configuration (this requires a wrapper type over ACMEIssuer
// because of the EAB generation; if EAB is provided, we can use plain ACMEIssuer)
if strings.Contains(acmeIssuer.CA, "acme.zerossl.com") && acmeIssuer.ExternalAccount == nil {
return &caddytls.ZeroSSLIssuer{ACMEIssuer: acmeIssuer}
}
return acmeIssuer
}
// consolidateAutomationPolicies combines automation policies that are the same,
// for a cleaner overall output.
func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
for i := 0; i < len(aps); i++ {
for j := 0; j < len(aps); j++ {
if j == i {
continue
}
// sort from most specific to least specific; we depend on this ordering
sort.SliceStable(aps, func(i, j int) bool {
if automationPolicyIsSubset(aps[i], aps[j]) {
return true
}
if automationPolicyIsSubset(aps[j], aps[i]) {
return false
}
return len(aps[i].Subjects) > len(aps[j].Subjects)
})
emptyAPCount := 0
origLenAPs := len(aps)
// compute the number of empty policies (disregarding subjects) - see #4128
emptyAP := new(caddytls.AutomationPolicy)
for i := 0; i < len(aps); i++ {
emptyAP.Subjects = aps[i].Subjects
if reflect.DeepEqual(aps[i], emptyAP) {
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 emptyAPCount == origLenAPs {
return nil
}
// remove or combine duplicate policies
outer:
for i := 0; i < len(aps); i++ {
// compare only with next policies; we sorted by specificity so we must not delete earlier policies
for j := i + 1; j < len(aps); j++ {
// if they're exactly equal in every way, just keep one of them
if reflect.DeepEqual(aps[i], aps[j]) {
aps = append(aps[:j], aps[j+1:]...)
// 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--
break
continue outer
}
// if the policy is the same, we can keep just one, but we have
@@ -457,16 +538,25 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
// otherwise the one without any subjects (a catch-all) would be
// eaten up by the one with subjects; and if both have subjects, we
// need to combine their lists
if bytes.Equal(aps[i].IssuerRaw, aps[j].IssuerRaw) &&
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
aps[i].MustStaple == aps[j].MustStaple &&
aps[i].KeyType == aps[j].KeyType &&
aps[i].OnDemand == aps[j].OnDemand &&
aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio {
if len(aps[i].Subjects) == 0 && len(aps[j].Subjects) > 0 {
aps = append(aps[:j], aps[j+1:]...)
} else if len(aps[i].Subjects) > 0 && len(aps[j].Subjects) == 0 {
aps = append(aps[:i], aps[i+1:]...)
if len(aps[i].Subjects) > 0 && len(aps[j].Subjects) == 0 {
// later policy (at j) has no subjects ("catch-all"), so we can
// remove the identical-but-more-specific policy that comes first
// AS LONG AS it is not shadowed by another policy before it; e.g.
// if policy i is for example.com, policy i+1 is '*.com', and policy
// j is catch-all, we cannot remove policy i because that would
// cause example.com to be served by the less specific policy for
// '*.com', which might be different (yes we've seen this happen)
if automationPolicyShadows(i, aps) >= j {
aps = append(aps[:i], aps[i+1:]...)
i--
continue outer
}
} else {
// avoid repeated subjects
for _, subj := range aps[j].Subjects {
@@ -475,17 +565,68 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
}
}
aps = append(aps[:j], aps[j+1:]...)
j--
}
i--
break
}
}
}
// ensure any catch-all policies go last
sort.SliceStable(aps, func(i, j int) bool {
return len(aps[i].Subjects) > len(aps[j].Subjects)
})
return aps
}
// automationPolicyIsSubset returns true if a's subjects are a subset
// of b's subjects.
func automationPolicyIsSubset(a, b *caddytls.AutomationPolicy) bool {
if len(b.Subjects) == 0 {
return true
}
if len(a.Subjects) == 0 {
return false
}
for _, aSubj := range a.Subjects {
var inSuperset bool
for _, bSubj := range b.Subjects {
if certmagic.MatchWildcard(aSubj, bSubj) {
inSuperset = true
break
}
}
if !inSuperset {
return false
}
}
return true
}
// automationPolicyShadows returns the index of a policy that aps[i] shadows;
// in other words, for all policies after position i, if that policy covers
// the same subjects but is less specific, that policy's position is returned,
// or -1 if no shadowing is found. For example, if policy i is for
// "foo.example.com" and policy i+2 is for "*.example.com", then i+2 will be
// returned, since that policy is shadowed by i, which is in front.
func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int {
for j := i + 1; j < len(aps); j++ {
if automationPolicyIsSubset(aps[i], aps[j]) {
return j
}
}
return -1
}
// 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
}
+56
View File
@@ -0,0 +1,56 @@
package httpcaddyfile
import (
"testing"
"github.com/caddyserver/caddy/v2/modules/caddytls"
)
func TestAutomationPolicyIsSubset(t *testing.T) {
for i, test := range []struct {
a, b []string
expect bool
}{
{
a: []string{"example.com"},
b: []string{},
expect: true,
},
{
a: []string{},
b: []string{"example.com"},
expect: false,
},
{
a: []string{"foo.example.com"},
b: []string{"*.example.com"},
expect: true,
},
{
a: []string{"foo.example.com"},
b: []string{"foo.example.com"},
expect: true,
},
{
a: []string{"foo.example.com"},
b: []string{"example.com"},
expect: false,
},
{
a: []string{"example.com", "foo.example.com"},
b: []string{"*.com", "*.*.com"},
expect: true,
},
{
a: []string{"example.com", "foo.example.com"},
b: []string{"*.com"},
expect: false,
},
} {
apA := &caddytls.AutomationPolicy{Subjects: test.a}
apB := &caddytls.AutomationPolicy{Subjects: test.b}
if actual := automationPolicyIsSubset(apA, apB); actual != test.expect {
t.Errorf("Test %d: Expected %t but got %t (A: %v B: %v)", i, test.expect, actual, test.a, test.b)
}
}
}
+151
View File
@@ -0,0 +1,151 @@
package caddyconfig
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/caddyserver/caddy/v2"
)
func init() {
caddy.RegisterModule(HTTPLoader{})
}
// HTTPLoader can load Caddy configs over HTTP(S). It can adapt the config
// based on the Content-Type header of the HTTP response.
type HTTPLoader struct {
// The method for the request. Default: GET
Method string `json:"method,omitempty"`
// The URL of the request.
URL string `json:"url,omitempty"`
// HTTP headers to add to the request.
Headers http.Header `json:"header,omitempty"`
// Maximum time allowed for a complete connection and request.
Timeout caddy.Duration `json:"timeout,omitempty"`
TLS *struct {
// Present this instance's managed remote identity credentials to the server.
UseServerIdentity bool `json:"use_server_identity,omitempty"`
// PEM-encoded client certificate filename to present to the server.
ClientCertificateFile string `json:"client_certificate_file,omitempty"`
// PEM-encoded key to use with the client certificate.
ClientCertificateKeyFile string `json:"client_certificate_key_file,omitempty"`
// List of PEM-encoded CA certificate files to add to the same trust
// store as RootCAPool (or root_ca_pool in the JSON).
RootCAPEMFiles []string `json:"root_ca_pem_files,omitempty"`
} `json:"tls,omitempty"`
}
// CaddyModule returns the Caddy module information.
func (HTTPLoader) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "caddy.config_loaders.http",
New: func() caddy.Module { return new(HTTPLoader) },
}
}
// LoadConfig loads a Caddy config.
func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
client, err := hl.makeClient(ctx)
if err != nil {
return nil, err
}
method := hl.Method
if method == "" {
method = http.MethodGet
}
req, err := http.NewRequest(method, hl.URL, nil)
if err != nil {
return nil, err
}
req.Header = hl.Headers
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("server responded with HTTP %d", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
result, warnings, err := adaptByContentType(resp.Header.Get("Content-Type"), body)
if err != nil {
return nil, err
}
for _, warn := range warnings {
ctx.Logger(hl).Warn(warn.String())
}
return result, nil
}
func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
client := &http.Client{
Timeout: time.Duration(hl.Timeout),
}
if hl.TLS != nil {
var tlsConfig *tls.Config
// client authentication
if hl.TLS.UseServerIdentity {
certs, err := ctx.IdentityCredentials(ctx.Logger(hl))
if err != nil {
return nil, fmt.Errorf("getting server identity credentials: %v", err)
}
if tlsConfig == nil {
tlsConfig = new(tls.Config)
}
tlsConfig.Certificates = certs
} else if hl.TLS.ClientCertificateFile != "" && hl.TLS.ClientCertificateKeyFile != "" {
cert, err := tls.LoadX509KeyPair(hl.TLS.ClientCertificateFile, hl.TLS.ClientCertificateKeyFile)
if err != nil {
return nil, err
}
if tlsConfig == nil {
tlsConfig = new(tls.Config)
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
// trusted server certs
if len(hl.TLS.RootCAPEMFiles) > 0 {
rootPool := x509.NewCertPool()
for _, pemFile := range hl.TLS.RootCAPEMFiles {
pemData, err := ioutil.ReadFile(pemFile)
if err != nil {
return nil, fmt.Errorf("failed reading ca cert: %v", err)
}
rootPool.AppendCertsFromPEM(pemData)
}
if tlsConfig == nil {
tlsConfig = new(tls.Config)
}
tlsConfig.RootCAs = rootPool
}
client.Transport = &http.Transport{TLSClientConfig: tlsConfig}
}
return client, nil
}
var _ caddy.ConfigLoader = (*HTTPLoader)(nil)
+55 -38
View File
@@ -69,8 +69,8 @@ func (al adminLoad) Routes() []caddy.AdminRoute {
func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodPost {
return caddy.APIError{
Code: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed"),
HTTPStatus: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed"),
}
}
@@ -81,8 +81,8 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
_, err := io.Copy(buf, r.Body)
if err != nil {
return caddy.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("reading request body: %v", err),
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("reading request body: %v", err),
}
}
body := buf.Bytes()
@@ -90,45 +90,21 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
// if the config is formatted other than Caddy's native
// JSON, we need to adapt it before loading it
if ctHeader := r.Header.Get("Content-Type"); ctHeader != "" {
ct, _, err := mime.ParseMediaType(ctHeader)
result, warnings, err := adaptByContentType(ctHeader, body)
if err != nil {
return caddy.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("invalid Content-Type: %v", err),
HTTPStatus: http.StatusBadRequest,
Err: err,
}
}
if !strings.HasSuffix(ct, "/json") {
slashIdx := strings.Index(ct, "/")
if slashIdx < 0 {
return caddy.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("malformed Content-Type"),
}
}
adapterName := ct[slashIdx+1:]
cfgAdapter := GetAdapter(adapterName)
if cfgAdapter == nil {
return caddy.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("unrecognized config adapter '%s'", adapterName),
}
}
result, warnings, err := cfgAdapter.Adapt(body, nil)
if len(warnings) > 0 {
respBody, err := json.Marshal(warnings)
if err != nil {
return caddy.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("adapting config using %s adapter: %v", adapterName, err),
}
caddy.Log().Named("admin.api.load").Error(err.Error())
}
if len(warnings) > 0 {
respBody, err := json.Marshal(warnings)
if err != nil {
caddy.Log().Named("admin.api.load").Error(err.Error())
}
_, _ = w.Write(respBody)
}
body = result
_, _ = w.Write(respBody)
}
body = result
}
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
@@ -136,8 +112,8 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
err = caddy.Load(body, forceReload)
if err != nil {
return caddy.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("loading config: %v", err),
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("loading config: %v", err),
}
}
@@ -146,6 +122,47 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
return nil
}
// adaptByContentType adapts body to Caddy JSON using the adapter specified by contenType.
// If contentType is empty or ends with "/json", the input will be returned, as a no-op.
func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, error) {
// assume JSON as the default
if contentType == "" {
return body, nil, nil
}
ct, _, err := mime.ParseMediaType(contentType)
if err != nil {
return nil, nil, caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("invalid Content-Type: %v", err),
}
}
// if already JSON, no need to adapt
if strings.HasSuffix(ct, "/json") {
return body, nil, nil
}
// adapter name should be suffix of MIME type
slashIdx := strings.Index(ct, "/")
if slashIdx < 0 {
return nil, nil, fmt.Errorf("malformed Content-Type")
}
adapterName := ct[slashIdx+1:]
cfgAdapter := GetAdapter(adapterName)
if cfgAdapter == nil {
return nil, nil, fmt.Errorf("unrecognized config adapter '%s'", adapterName)
}
result, warnings, err := cfgAdapter.Adapt(body, nil)
if err != nil {
return nil, nil, fmt.Errorf("adapting config using %s adapter: %v", adapterName, err)
}
return result, warnings, nil
}
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
+81 -33
View File
@@ -14,6 +14,7 @@ import (
"net/http/cookiejar"
"os"
"path"
"reflect"
"regexp"
"runtime"
"strings"
@@ -98,6 +99,10 @@ func (tc *Tester) InitServer(rawConfig string, configType string) {
tc.t.Logf("failed to load config: %s", err)
tc.t.Fail()
}
if err := tc.ensureConfigRunning(rawConfig, configType); err != nil {
tc.t.Logf("failed ensurng config is running: %s", err)
tc.t.Fail()
}
}
// InitServer this will configure the server with a configurion of a specific
@@ -124,10 +129,10 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
body, _ := ioutil.ReadAll(res.Body)
var out bytes.Buffer
json.Indent(&out, body, "", " ")
_ = json.Indent(&out, body, "", " ")
tc.t.Logf("----------- failed with config -----------\n%s", out.String())
}
})
@@ -171,20 +176,57 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
return nil
}
var hasValidated bool
var arePrerequisitesValid bool
func validateTestPrerequisites() error {
if hasValidated {
if !arePrerequisitesValid {
return errors.New("caddy integration prerequisites failed. see first error")
func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error {
expectedBytes := []byte(prependCaddyFilePath(rawConfig))
if configType != "json" {
adapter := caddyconfig.GetAdapter(configType)
if adapter == nil {
return fmt.Errorf("adapter of config type is missing: %s", configType)
}
return nil
expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil)
}
hasValidated = true
arePrerequisitesValid = false
var expected interface{}
err := json.Unmarshal(expectedBytes, &expected)
if err != nil {
return err
}
client := &http.Client{
Timeout: Default.LoadRequestTimeout,
}
fetchConfig := func(client *http.Client) interface{} {
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
return nil
}
defer resp.Body.Close()
actualBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil
}
var actual interface{}
err = json.Unmarshal(actualBytes, &actual)
if err != nil {
return nil
}
return actual
}
for retries := 4; retries > 0; retries-- {
if reflect.DeepEqual(expected, fetchConfig(client)) {
return nil
}
time.Sleep(10 * time.Millisecond)
}
tc.t.Errorf("POSTed configuration isn't active")
return errors.New("EnsureConfigRunning: POSTed configuration isn't active")
}
// validateTestPrerequisites ensures the certificates are available in the
// designated path and Caddy sub-process is running.
func validateTestPrerequisites() error {
// check certificates are found
for _, certName := range Default.Certifcates {
@@ -200,20 +242,14 @@ func validateTestPrerequisites() error {
caddycmd.Main()
}()
// wait for caddy to start
retries := 4
for ; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
// wait for caddy to start serving the initial config
for retries := 4; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
time.Sleep(10 * time.Millisecond)
}
}
// assert that caddy is running
if err := isCaddyAdminRunning(); err != nil {
return err
}
arePrerequisitesValid = true
return nil
// one more time to return the error
return isCaddyAdminRunning()
}
func isCaddyAdminRunning() error {
@@ -221,10 +257,11 @@ func isCaddyAdminRunning() error {
client := &http.Client{
Timeout: Default.LoadRequestTimeout,
}
_, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
return errors.New("caddy integration test caddy server not running. Expected to be listening on localhost:2019")
return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", Default.AdminPort)
}
resp.Body.Close()
return nil
}
@@ -272,7 +309,7 @@ func CreateTestingTransport() *http.Transport {
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
}
}
@@ -313,16 +350,20 @@ func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, e
if err != nil {
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err)
}
if expectedToLocation != loc.String() {
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String())
if loc == nil && expectedToLocation != "" {
tc.t.Errorf("requesting \"%s\" expected a Location header, but didn't get one", requestURI)
}
if loc != nil {
if expectedToLocation != loc.String() {
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String())
}
}
return resp
}
// CompareAdapt adapts a config and then compares it against an expected result
func CompareAdapt(t *testing.T, rawConfig string, adapterName string, expectedResponse string) bool {
func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string, expectedResponse string) bool {
cfgAdapter := caddyconfig.GetAdapter(adapterName)
if cfgAdapter == nil {
@@ -331,7 +372,6 @@ func CompareAdapt(t *testing.T, rawConfig string, adapterName string, expectedRe
}
options := make(map[string]interface{})
options["pretty"] = "true"
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
if err != nil {
@@ -339,9 +379,17 @@ func CompareAdapt(t *testing.T, rawConfig string, adapterName string, expectedRe
return false
}
// prettify results to keep tests human-manageable
var prettyBuf bytes.Buffer
err = json.Indent(&prettyBuf, result, "", "\t")
if err != nil {
return false
}
result = prettyBuf.Bytes()
if len(warnings) > 0 {
for _, w := range warnings {
t.Logf("warning: directive: %s : %s", w.Directive, w.Message)
t.Logf("warning: %s:%d: %s: %s", filename, w.Line, w.Directive, w.Message)
}
}
@@ -376,7 +424,7 @@ func CompareAdapt(t *testing.T, rawConfig string, adapterName string, expectedRe
// AssertAdapt adapts a config and then tests it against an expected result
func AssertAdapt(t *testing.T, rawConfig string, adapterName string, expectedResponse string) {
ok := CompareAdapt(t, rawConfig, adapterName, expectedResponse)
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
if !ok {
t.Fail()
}
+104 -1
View File
@@ -7,7 +7,21 @@ import (
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestAutoHTTPtoHTTPSRedirects(t *testing.T) {
func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
}
localhost
respond "Yahaha! You found me!"
`, "caddyfile")
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
}
func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
@@ -20,3 +34,92 @@ func TestAutoHTTPtoHTTPSRedirects(t *testing.T) {
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
}
func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
}
localhost:1234
respond "Yahaha! You found me!"
`, "caddyfile")
tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect)
}
func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"ingress_server": {
"listen": [
":9080",
":9443"
],
"routes": [
{
"match": [
{
"host": ["localhost"]
}
]
}
]
}
}
}
}
}
`, "json")
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
}
func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
local_certs
}
http://:9080 {
respond "Foo"
}
http://baz.localhost:9080 {
respond "Baz"
}
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, "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,34 @@
{
auto_https ignore_loaded_certs
}
localhost
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
],
"automatic_https": {
"ignore_loaded_certificates": true
}
}
}
}
}
}
@@ -0,0 +1,85 @@
:80
# All the options
encode gzip zstd {
minimum_length 256
match {
status 2xx 4xx 500
header Content-Type text/*
header Content-Type application/json*
header Content-Type application/javascript*
header Content-Type application/xhtml+xml*
header Content-Type application/atom+xml*
header Content-Type application/rss+xml*
header Content-Type image/svg+xml*
}
}
# Long way with a block for each encoding
encode {
zstd
gzip 5
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"encodings": {
"gzip": {},
"zstd": {}
},
"handler": "encode",
"match": {
"headers": {
"Content-Type": [
"text/*",
"application/json*",
"application/javascript*",
"application/xhtml+xml*",
"application/atom+xml*",
"application/rss+xml*",
"image/svg+xml*"
]
},
"status_code": [
2,
4,
500
]
},
"minimum_length": 256,
"prefer": [
"gzip",
"zstd"
]
},
{
"encodings": {
"gzip": {
"level": 5
},
"zstd": {}
},
"handler": "encode",
"prefer": [
"zstd",
"gzip"
]
}
]
}
]
}
}
}
}
}
@@ -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,41 @@
:80
file_server {
precompressed zstd br gzip
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
],
"precompressed": {
"br": {},
"gzip": {},
"zstd": {}
},
"precompressed_order": [
"zstd",
"br",
"gzip"
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,112 @@
localhost
root * /srv
handle /nope* {
file_server {
status 403
}
}
handle /custom-status* {
file_server {
status {env.CUSTOM_STATUS}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
],
"status_code": "{env.CUSTOM_STATUS}"
}
]
}
]
}
],
"match": [
{
"path": [
"/custom-status*"
]
}
]
},
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
],
"status_code": 403
}
]
}
]
}
],
"match": [
{
"path": [
"/nope*"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -2,6 +2,7 @@
debug
http_port 8080
https_port 8443
grace_period 5s
default_sni localhost
order root first
storage file_system {
@@ -42,6 +43,7 @@
"http": {
"http_port": 8080,
"https_port": 8443,
"grace_period": 5000000000,
"servers": {
"srv0": {
"listen": [
@@ -54,9 +56,11 @@
"automation": {
"policies": [
{
"issuer": {
"module": "internal"
},
"issuers": [
{
"module": "internal"
}
],
"key_type": "ed25519"
}
],
@@ -9,7 +9,7 @@
}
acme_ca https://example.com
acme_eab {
key_id 4K2scIVbBpNd-78scadB2g
key_id 4K2scIVbBpNd-78scadB2g
mac_key abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh
}
acme_ca_root /path/to/ca.crt
@@ -20,6 +20,7 @@
interval 30s
burst 20
}
storage_clean_interval 7d
key_type ed25519
}
@@ -57,18 +58,20 @@
"automation": {
"policies": [
{
"issuer": {
"ca": "https://example.com",
"email": "test@example.com",
"external_account": {
"key_id": "4K2scIVbBpNd-78scadB2g",
"mac_key": "abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh"
},
"module": "acme",
"trusted_roots_pem_files": [
"/path/to/ca.crt"
]
},
"issuers": [
{
"ca": "https://example.com",
"email": "test@example.com",
"external_account": {
"key_id": "4K2scIVbBpNd-78scadB2g",
"mac_key": "abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh"
},
"module": "acme",
"trusted_roots_pem_files": [
"/path/to/ca.crt"
]
}
],
"key_type": "ed25519"
}
],
@@ -78,7 +81,8 @@
"burst": 20
},
"ask": "https://example.com"
}
},
"storage_clean_interval": 604800000000000
}
}
}
@@ -62,9 +62,11 @@
"automation": {
"policies": [
{
"issuer": {
"module": "internal"
},
"issuers": [
{
"module": "internal"
}
],
"key_type": "ed25519"
}
],
@@ -0,0 +1,77 @@
{
log {
output file caddy.log
include some-log-source
exclude admin.api admin2.api
}
log custom-logger {
output file caddy.log
level WARN
include custom-log-source
}
}
:8884 {
log {
format json
output file access.log
}
}
----------
{
"logging": {
"logs": {
"custom-logger": {
"writer": {
"filename": "caddy.log",
"output": "file"
},
"level": "WARN",
"include": [
"custom-log-source"
]
},
"default": {
"writer": {
"filename": "caddy.log",
"output": "file"
},
"include": [
"some-log-source"
],
"exclude": [
"admin.api",
"admin2.api",
"custom-log-source",
"http.log.access.log0"
]
},
"log0": {
"writer": {
"filename": "access.log",
"output": "file"
},
"encoder": {
"format": "json"
},
"include": [
"http.log.access.log0"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"logs": {
"default_logger_name": "log0"
}
}
}
}
}
}
@@ -0,0 +1,18 @@
{
log {
output file foo.log
}
}
----------
{
"logging": {
"logs": {
"default": {
"writer": {
"filename": "foo.log",
"output": "file"
}
}
}
}
}
@@ -0,0 +1,39 @@
{
log custom-logger {
format filter {
wrap console
fields {
common_log delete
request>remote_addr ip_mask {
ipv4 24
ipv6 32
}
}
}
}
}
----------
{
"logging": {
"logs": {
"custom-logger": {
"encoder": {
"fields": {
"common_log": {
"filter": "delete"
},
"request\u003eremote_addr": {
"filter": "ip_mask",
"ipv4_cidr": 24,
"ipv6_cidr": 32
}
},
"format": "filter",
"wrap": {
"format": "console"
}
}
}
}
}
}
@@ -0,0 +1,26 @@
{
log first {
output file foo.log
}
log second {
format json
}
}
----------
{
"logging": {
"logs": {
"first": {
"writer": {
"filename": "foo.log",
"output": "file"
}
},
"second": {
"encoder": {
"format": "json"
}
}
}
}
}
@@ -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,78 @@
{
servers {
timeouts {
idle 90s
}
}
servers :80 {
timeouts {
idle 60s
}
}
servers :443 {
timeouts {
idle 30s
}
}
}
foo.com {
}
http://bar.com {
}
:8080 {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"idle_timeout": 30000000000,
"routes": [
{
"match": [
{
"host": [
"foo.com"
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
":80"
],
"idle_timeout": 60000000000,
"routes": [
{
"match": [
{
"host": [
"bar.com"
]
}
],
"terminal": true
}
]
},
"srv2": {
"listen": [
":8080"
],
"idle_timeout": 90000000000
}
}
}
}
}
@@ -0,0 +1,62 @@
{
servers {
listener_wrappers {
tls
}
timeouts {
read_body 30s
read_header 30s
write 30s
idle 30s
}
max_header_size 100MB
protocol {
allow_h2c
experimental_http3
strict_sni_host
}
}
}
foo.com {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"listener_wrappers": [
{
"wrapper": "tls"
}
],
"read_timeout": 30000000000,
"read_header_timeout": 30000000000,
"write_timeout": 30000000000,
"idle_timeout": 30000000000,
"max_header_bytes": 100000000,
"routes": [
{
"match": [
{
"host": [
"foo.com"
]
}
],
"terminal": true
}
],
"strict_sni_host": true,
"experimental_http3": true,
"allow_h2c": true
}
}
}
}
}
@@ -1,52 +1,52 @@
:80
handle_path /api/v1/* {
respond "API v1"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/api/v1/*"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "rewrite",
"strip_path_prefix": "/api/v1"
}
]
},
{
"handle": [
{
"body": "API v1",
"handler": "static_response"
}
]
}
]
}
]
}
]
}
}
}
}
:80
handle_path /api/v1/* {
respond "API v1"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/api/v1/*"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "rewrite",
"strip_path_prefix": "/api/v1"
}
]
},
{
"handle": [
{
"body": "API v1",
"handler": "static_response"
}
]
}
]
}
]
}
]
}
}
}
}
}
@@ -1,105 +1,105 @@
:80 {
handle /api/* {
respond "api"
}
handle_path /static/* {
respond "static"
}
handle {
respond "handle"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"group": "group3",
"match": [
{
"path": [
"/static/*"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "rewrite",
"strip_path_prefix": "/static"
}
]
},
{
"handle": [
{
"body": "static",
"handler": "static_response"
}
]
}
]
}
]
},
{
"group": "group3",
"match": [
{
"path": [
"/api/*"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "api",
"handler": "static_response"
}
]
}
]
}
]
},
{
"group": "group3",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "handle",
"handler": "static_response"
}
]
}
]
}
]
}
]
}
}
}
}
:80 {
handle /api/* {
respond "api"
}
handle_path /static/* {
respond "static"
}
handle {
respond "handle"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"group": "group3",
"match": [
{
"path": [
"/static/*"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "rewrite",
"strip_path_prefix": "/static"
}
]
},
{
"handle": [
{
"body": "static",
"handler": "static_response"
}
]
}
]
}
]
},
{
"group": "group3",
"match": [
{
"path": [
"/api/*"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "api",
"handler": "static_response"
}
]
}
]
}
]
},
{
"group": "group3",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "handle",
"handler": "static_response"
}
]
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,132 @@
:80 {
header Denis "Ritchie"
header +Edsger "Dijkstra"
header ?John "von Neumann"
header -Wolfram
header {
Grace: "Hopper" # some users habitually suffix field names with a colon
+Ray "Solomonoff"
?Tim "Berners-Lee"
defer
}
@images path /images/*
header @images {
Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/images/*"
]
}
],
"handle": [
{
"handler": "headers",
"response": {
"set": {
"Cache-Control": [
"public, max-age=3600, stale-while-revalidate=86400"
]
}
}
}
]
},
{
"handle": [
{
"handler": "headers",
"response": {
"set": {
"Denis": [
"Ritchie"
]
}
}
},
{
"handler": "headers",
"response": {
"add": {
"Edsger": [
"Dijkstra"
]
}
}
},
{
"handler": "headers",
"response": {
"require": {
"headers": {
"John": null
}
},
"set": {
"John": [
"von Neumann"
]
}
}
},
{
"handler": "headers",
"response": {
"deferred": true,
"delete": [
"Wolfram"
]
}
},
{
"handler": "headers",
"response": {
"add": {
"Ray": [
"Solomonoff"
]
},
"deferred": true,
"set": {
"Grace": [
"Hopper"
]
}
}
},
{
"handler": "headers",
"response": {
"require": {
"headers": {
"Tim": null
}
},
"set": {
"Tim": [
"Berners-Lee"
]
}
}
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,45 @@
# https://github.com/caddyserver/caddy/issues/3977
http://* {
respond "Hello, world!"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"*"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Hello, world!",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -46,12 +46,7 @@ http://a.caddy.localhost {
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"a.caddy.localhost"
]
}
]
}
}
}
@@ -0,0 +1,28 @@
# Issue #4113
:80, http://example.com {
respond "foo"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"body": "foo",
"handler": "static_response"
}
]
}
]
}
}
}
}
}
@@ -1,49 +1,49 @@
example.com
import testdata/import_respond.txt Groot Rocket
import testdata/import_respond.txt you "the confused man"
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "'I am Groot', hears Rocket",
"handler": "static_response"
},
{
"body": "'I am you', hears the confused man",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
example.com
import testdata/import_respond.txt Groot Rocket
import testdata/import_respond.txt you "the confused man"
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "'I am Groot', hears Rocket",
"handler": "static_response"
},
{
"body": "'I am you', hears the confused man",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -1,83 +1,83 @@
(logging) {
log {
output file /var/log/caddy/{args.0}.access.log
}
}
a.example.com {
import logging a.example.com
}
b.example.com {
import logging b.example.com
}
----------
{
"logging": {
"logs": {
"default": {
"exclude": [
"http.log.access.log0",
"http.log.access.log1"
]
},
"log0": {
"writer": {
"filename": "/var/log/caddy/a.example.com.access.log",
"output": "file"
},
"include": [
"http.log.access.log0"
]
},
"log1": {
"writer": {
"filename": "/var/log/caddy/b.example.com.access.log",
"output": "file"
},
"include": [
"http.log.access.log1"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"b.example.com"
]
}
],
"terminal": true
}
],
"logs": {
"logger_names": {
"a.example.com": "log0",
"b.example.com": "log1"
}
}
}
}
}
}
(logging) {
log {
output file /var/log/caddy/{args.0}.access.log
}
}
a.example.com {
import logging a.example.com
}
b.example.com {
import logging b.example.com
}
----------
{
"logging": {
"logs": {
"default": {
"exclude": [
"http.log.access.log0",
"http.log.access.log1"
]
},
"log0": {
"writer": {
"filename": "/var/log/caddy/a.example.com.access.log",
"output": "file"
},
"include": [
"http.log.access.log0"
]
},
"log1": {
"writer": {
"filename": "/var/log/caddy/b.example.com.access.log",
"output": "file"
},
"include": [
"http.log.access.log1"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"b.example.com"
]
}
],
"terminal": true
}
],
"logs": {
"logger_names": {
"a.example.com": "log0",
"b.example.com": "log1"
}
}
}
}
}
}
}
@@ -0,0 +1,31 @@
(foo) {
respond {env.FOO}
}
:80 {
import foo
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"body": "{env.FOO}",
"handler": "static_response"
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,78 @@
http://localhost:2020 {
log
respond 200
}
:2020 {
respond 418
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":2020"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"status_code": 200
}
]
}
]
}
],
"terminal": true
},
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"status_code": 418
}
]
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
},
"logs": {
"logger_names": {
"localhost:2020": ""
},
"skip_unmapped_hosts": true
}
}
}
}
}
}
@@ -1,69 +1,70 @@
:80
log {
output stdout
format filter {
wrap console
fields {
request>headers>Authorization delete
request>headers>Server delete
request>remote_addr ip_mask {
ipv4 24
ipv6 32
}
}
}
}
----------
{
"logging": {
"logs": {
"default": {
"exclude": [
"http.log.access.log0"
]
},
"log0": {
"writer": {
"output": "stdout"
},
"encoder": {
"fields": {
"request\u003eheaders\u003eAuthorization": {
"filter": "delete"
},
"request\u003eheaders\u003eServer": {
"filter": "delete"
},
"request\u003eremote_addr": {
"filter": "ip_mask",
"ipv4_cidr": 24,
"ipv6_cidr": 32
}
},
"format": "filter",
"wrap": {
"format": "console"
}
},
"include": [
"http.log.access.log0"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"logs": {
"default_logger_name": "log0"
}
}
}
}
}
}
:80
log {
output stdout
format filter {
wrap console
fields {
request>headers>Authorization replace REDACTED
request>headers>Server delete
request>remote_addr ip_mask {
ipv4 24
ipv6 32
}
}
}
}
----------
{
"logging": {
"logs": {
"default": {
"exclude": [
"http.log.access.log0"
]
},
"log0": {
"writer": {
"output": "stdout"
},
"encoder": {
"fields": {
"request\u003eheaders\u003eAuthorization": {
"filter": "replace",
"value": "REDACTED"
},
"request\u003eheaders\u003eServer": {
"filter": "delete"
},
"request\u003eremote_addr": {
"filter": "ip_mask",
"ipv4_cidr": 24,
"ipv6_cidr": 32
}
},
"format": "filter",
"wrap": {
"format": "console"
}
},
"include": [
"http.log.access.log0"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"logs": {
"default_logger_name": "log0"
}
}
}
}
}
}
@@ -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"
]
}
}
}
}
}
}
@@ -18,6 +18,25 @@
@matcher6 vars_regexp "{http.request.uri}" `\.([a-f0-9]{6})\.(css|js)$`
respond @matcher6 "from vars_regexp matcher without name"
@matcher7 {
header Foo bar
header Foo foobar
header Bar foo
}
respond @matcher7 "header matcher merging values of the same field"
@matcher8 {
query foo=bar foo=baz bar=foo
query bar=baz
}
respond @matcher8 "query matcher merging pairs with the same keys"
@matcher9 {
header !Foo
header Bar foo
}
respond @matcher9 "header matcher with null field matcher"
}
----------
{
@@ -127,6 +146,67 @@
"handler": "static_response"
}
]
},
{
"match": [
{
"header": {
"Bar": [
"foo"
],
"Foo": [
"bar",
"foobar"
]
}
}
],
"handle": [
{
"body": "header matcher merging values of the same field",
"handler": "static_response"
}
]
},
{
"match": [
{
"query": {
"bar": [
"foo",
"baz"
],
"foo": [
"bar",
"baz"
]
}
}
],
"handle": [
{
"body": "query matcher merging pairs with the same keys",
"handler": "static_response"
}
]
},
{
"match": [
{
"header": {
"Bar": [
"foo"
],
"Foo": null
}
}
],
"handle": [
{
"body": "header matcher with null field matcher",
"handler": "static_response"
}
]
}
]
}
@@ -1,10 +1,10 @@
:80 {
route {
# unused matchers should not panic
# see https://github.com/caddyserver/caddy/issues/3745
@matcher1 path /path1
@matcher2 path /path2
}
route {
# unused matchers should not panic
# see https://github.com/caddyserver/caddy/issues/3745
@matcher1 path /path1
@matcher2 path /path2
}
}
----------
{
@@ -0,0 +1,36 @@
:80 {
metrics /metrics {
disable_openmetrics
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/metrics"
]
}
],
"handle": [
{
"disable_openmetrics": true,
"handler": "metrics"
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,33 @@
:80 {
metrics /metrics
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/metrics"
]
}
],
"handle": [
{
"handler": "metrics"
}
]
}
]
}
}
}
}
}
@@ -1,132 +1,132 @@
:8886
route {
# Add trailing slash for directory requests
@canonicalPath {
file {
try_files {path}/index.php
}
not path */
}
redir @canonicalPath {path}/ 308
# If the requested file does not exist, try index files
@indexFiles {
file {
try_files {path} {path}/index.php index.php
split_path .php
}
}
rewrite @indexFiles {http.matchers.file.relative}
# Proxy PHP files to the FastCGI responder
@phpFiles {
path *.php
}
reverse_proxy @phpFiles 127.0.0.1:9000 {
transport fastcgi {
split .php
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8886"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{
"dial": "127.0.0.1:9000"
}
]
}
],
"match": [
{
"path": [
"*.php"
]
}
]
}
]
}
]
}
]
}
}
}
}
:8886
route {
# Add trailing slash for directory requests
@canonicalPath {
file {
try_files {path}/index.php
}
not path */
}
redir @canonicalPath {path}/ 308
# If the requested file does not exist, try index files
@indexFiles {
file {
try_files {path} {path}/index.php index.php
split_path .php
}
}
rewrite @indexFiles {http.matchers.file.relative}
# Proxy PHP files to the FastCGI responder
@phpFiles {
path *.php
}
reverse_proxy @phpFiles 127.0.0.1:9000 {
transport fastcgi {
split .php
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8886"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{
"dial": "127.0.0.1:9000"
}
]
}
],
"match": [
{
"path": [
"*.php"
]
}
]
}
]
}
]
}
]
}
}
}
}
}
@@ -1,15 +1,18 @@
:8884
php_fastcgi localhost:9000 {
# some php_fastcgi-specific subdirectives
split .php .php5
env VAR1 value1
env VAR2 value2
root /var/www
index off
# some php_fastcgi-specific subdirectives
split .php .php5
env VAR1 value1
env VAR2 value2
root /var/www
index off
dial_timeout 3s
read_timeout 10s
write_timeout 20s
# passed through to reverse_proxy (directive order doesn't matter!)
lb_policy random
# passed through to reverse_proxy (directive order doesn't matter!)
lb_policy random
}
----------
{
@@ -39,16 +42,19 @@ php_fastcgi localhost:9000 {
}
},
"transport": {
"dial_timeout": 3000000000,
"env": {
"VAR1": "value1",
"VAR2": "value2"
},
"protocol": "fastcgi",
"read_timeout": 10000000000,
"root": "/var/www",
"split_path": [
".php",
".php5"
]
],
"write_timeout": 20000000000
},
"upstreams": [
{
@@ -63,4 +69,4 @@ php_fastcgi localhost:9000 {
}
}
}
}
}
@@ -1,112 +1,112 @@
:8884
@api host example.com
php_fastcgi @api localhost:9000
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
],
"match": [
{
"path": [
"*.php"
]
}
]
}
]
}
]
}
]
}
}
}
}
:8884
@api host example.com
php_fastcgi @api localhost:9000
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
],
"match": [
{
"path": [
"*.php"
]
}
]
}
]
}
]
}
]
}
}
}
}
}
@@ -1,15 +1,15 @@
:8884
php_fastcgi localhost:9000 {
# some php_fastcgi-specific subdirectives
split .php .php5
env VAR1 value1
env VAR2 value2
root /var/www
index index.php5
# some php_fastcgi-specific subdirectives
split .php .php5
env VAR1 value1
env VAR2 value2
root /var/www
index index.php5
# passed through to reverse_proxy (directive order doesn't matter!)
lb_policy random
# passed through to reverse_proxy (directive order doesn't matter!)
lb_policy random
}
----------
{
@@ -0,0 +1,46 @@
localhost
request_body {
max_size 1MB
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "request_body",
"max_size": 1000000
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,90 @@
:80
@matcher path /something*
request_header @matcher Denis "Ritchie"
request_header +Edsger "Dijkstra"
request_header -Wolfram
@images path /images/*
request_header @images Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/something*"
]
}
],
"handle": [
{
"handler": "headers",
"request": {
"set": {
"Denis": [
"Ritchie"
]
}
}
}
]
},
{
"match": [
{
"path": [
"/images/*"
]
}
],
"handle": [
{
"handler": "headers",
"request": {
"set": {
"Cache-Control": [
"public, max-age=3600, stale-while-revalidate=86400"
]
}
}
}
]
},
{
"handle": [
{
"handler": "headers",
"request": {
"add": {
"Edsger": [
"Dijkstra"
]
}
}
},
{
"handler": "headers",
"request": {
"delete": [
"Wolfram"
]
}
}
]
}
]
}
}
}
}
}
@@ -1,7 +1,7 @@
:8884
reverse_proxy 127.0.0.1:65535 {
transport fastcgi
transport fastcgi
}
----------
{
@@ -0,0 +1,38 @@
:8884
reverse_proxy h2c://localhost:8080
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http",
"versions": [
"h2c",
"2"
]
},
"upstreams": [
{
"dial": "localhost:8080"
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,193 @@
:8884
reverse_proxy 127.0.0.1:65535 {
@accel header X-Accel-Redirect *
handle_response @accel {
respond "Header X-Accel-Redirect!"
}
@another {
header X-Another *
}
handle_response @another {
respond "Header X-Another!"
}
@401 status 401
handle_response @401 {
respond "Status 401!"
}
handle_response {
respond "Any! This should be last in the JSON!"
}
@403 {
status 403
}
handle_response @403 {
respond "Status 403!"
}
@multi {
status 401 403
status 404
header Foo *
header Bar *
}
handle_response @multi {
respond "Headers Foo, Bar AND statuses 401, 403 and 404!"
}
@changeStatus status 500
handle_response @changeStatus 400
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handle_response": [
{
"match": {
"headers": {
"X-Accel-Redirect": [
"*"
]
}
},
"routes": [
{
"handle": [
{
"body": "Header X-Accel-Redirect!",
"handler": "static_response"
}
]
}
]
},
{
"match": {
"headers": {
"X-Another": [
"*"
]
}
},
"routes": [
{
"handle": [
{
"body": "Header X-Another!",
"handler": "static_response"
}
]
}
]
},
{
"match": {
"status_code": [
401
]
},
"routes": [
{
"handle": [
{
"body": "Status 401!",
"handler": "static_response"
}
]
}
]
},
{
"match": {
"status_code": [
403
]
},
"routes": [
{
"handle": [
{
"body": "Status 403!",
"handler": "static_response"
}
]
}
]
},
{
"match": {
"headers": {
"Bar": [
"*"
],
"Foo": [
"*"
]
},
"status_code": [
401,
403,
404
]
},
"routes": [
{
"handle": [
{
"body": "Headers Foo, Bar AND statuses 401, 403 and 404!",
"handler": "static_response"
}
]
}
]
},
{
"match": {
"status_code": [
500
]
},
"status_code": 400
},
{
"routes": [
{
"handle": [
{
"body": "Any! This should be last in the JSON!",
"handler": "static_response"
}
]
}
]
}
],
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,57 @@
:8884
reverse_proxy 127.0.0.1:65535 {
health_headers {
Host example.com
X-Header-Key 95ca39e3cbe7
X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO
X-Empty-Value
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"health_checks": {
"active": {
"headers": {
"Host": [
"example.com"
],
"X-Empty-Value": [
""
],
"X-Header-Key": [
"95ca39e3cbe7"
],
"X-Header-Keys": [
"VbG4NZwWnipo",
"335Q9/MhqcNU3s2TO"
]
}
}
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,75 @@
# Health with query in the uri
:8443 {
reverse_proxy localhost:54321 {
health_uri /health?ready=1
health_status 2xx
}
}
# Health without query in the uri
:8444 {
reverse_proxy localhost:54321 {
health_uri /health
health_status 200
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8443"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"health_checks": {
"active": {
"expect_status": 2,
"uri": "/health?ready=1"
}
},
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
]
}
]
},
"srv1": {
"listen": [
":8444"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"health_checks": {
"active": {
"expect_status": 200,
"uri": "/health"
}
},
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
]
}
]
}
}
}
}
}
@@ -6,11 +6,23 @@ https://example.com {
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Port {server_port}
header_up X-Forwarded-Proto "http"
buffer_requests
transport http {
read_buffer 10MB
write_buffer 20MB
max_response_header 30MB
dial_timeout 3s
dial_fallback_delay 5s
response_header_timeout 8s
expect_continue_timeout 9s
versions h2c 2
compression off
max_conns_per_host 5
keepalive_idle_conns_per_host 2
}
buffer_requests
}
}
@@ -64,11 +76,22 @@ https://example.com {
},
"transport": {
"compression": false,
"dial_fallback_delay": 5000000000,
"dial_timeout": 3000000000,
"expect_continue_timeout": 9000000000,
"keep_alive": {
"max_idle_conns_per_host": 2
},
"max_conns_per_host": 5,
"max_response_header_size": 30000000,
"protocol": "http",
"read_buffer_size": 10000000,
"response_header_timeout": 8000000000,
"versions": [
"h2c",
"2"
]
],
"write_buffer_size": 20000000
},
"upstreams": [
{
@@ -0,0 +1,102 @@
:8884 {
map {host} {upstream} {
foo.example.com 1.2.3.4
default 2.3.4.5
}
# Upstream placeholder with a port should retain the port
reverse_proxy {upstream}:80
}
:8885 {
map {host} {upstream} {
foo.example.com 1.2.3.4:8080
default 2.3.4.5:8080
}
# Upstream placeholder with no port should not have a port joined
reverse_proxy {upstream}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"defaults": [
"2.3.4.5"
],
"destinations": [
"{upstream}"
],
"handler": "map",
"mappings": [
{
"input": "foo.example.com",
"outputs": [
"1.2.3.4"
]
}
],
"source": "{http.request.host}"
},
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "{upstream}:80"
}
]
}
]
}
]
},
"srv1": {
"listen": [
":8885"
],
"routes": [
{
"handle": [
{
"defaults": [
"2.3.4.5:8080"
],
"destinations": [
"{upstream}"
],
"handler": "map",
"mappings": [
{
"input": "foo.example.com",
"outputs": [
"1.2.3.4:8080"
]
}
],
"source": "{http.request.host}"
},
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "{upstream}"
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,193 @@
# https://caddy.community/t/caddy-suddenly-directs-my-site-to-the-wrong-directive/11597/2
abcdef {
respond "abcdef"
}
abcdefg {
respond "abcdefg"
}
abc {
respond "abc"
}
abcde, http://abcde {
respond "abcde"
}
:443, ab {
respond "443 or ab"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"abcdefg"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "abcdefg",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"abcdef"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "abcdef",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"abcde"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "abcde",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"abc"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "abc",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "443 or ab",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"abcde"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "abcde",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"certificates": {
"automate": [
"ab"
]
}
}
}
}
@@ -1,55 +1,51 @@
:80
file_server
@untrusted not remote_ip 10.1.1.0/24
file_server @untrusted
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"not": [
{
"remote_ip": {
"ranges": [
"10.1.1.0/24"
]
}
}
]
}
],
"handle": [
{
"handler": "file_server",
"hide": [
"Caddyfile"
]
}
]
},
{
"handle": [
{
"handler": "file_server",
"hide": [
"Caddyfile"
]
}
]
}
]
}
}
}
}
:80
respond 200
@untrusted not remote_ip 10.1.1.0/24
respond @untrusted 401
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"not": [
{
"remote_ip": {
"ranges": [
"10.1.1.0/24"
]
}
}
]
}
],
"handle": [
{
"handler": "static_response",
"status_code": 401
}
]
},
{
"handle": [
{
"handler": "static_response",
"status_code": 200
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,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"
]
}
}
]
}
]
}
}
}
}
@@ -0,0 +1,86 @@
{
local_certs
}
*.tld, *.*.tld {
tls {
on_demand
}
}
foo.tld, www.foo.tld {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"foo.tld",
"www.foo.tld"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"*.tld",
"*.*.tld"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"foo.tld",
"www.foo.tld"
],
"issuers": [
{
"module": "internal"
}
]
},
{
"subjects": [
"*.*.tld",
"*.tld"
],
"issuers": [
{
"module": "internal"
}
],
"on_demand": true
},
{
"issuers": [
{
"module": "internal"
}
]
}
]
}
}
}
}
@@ -0,0 +1,92 @@
# issue #3953
{
cert_issuer zerossl api_key
}
example.com {
tls {
on_demand
key_type rsa2048
}
}
http://example.net {
}
:1234 {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":1234"
]
},
"srv1": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
},
"srv2": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"example.net"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"example.com"
],
"issuers": [
{
"api_key": "api_key",
"module": "zerossl"
}
],
"key_type": "rsa2048",
"on_demand": true
},
{
"issuers": [
{
"api_key": "api_key",
"module": "zerossl"
}
]
}
]
}
}
}
}
@@ -0,0 +1,89 @@
# https://caddy.community/t/caddyfile-having-individual-sites-differ-from-global-options/11297
{
local_certs
}
a.example.com {
tls internal
}
b.example.com {
tls abc@example.com
}
c.example.com {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"b.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"c.example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"b.example.com"
],
"issuers": [
{
"email": "abc@example.com",
"module": "acme"
},
{
"email": "abc@example.com",
"module": "zerossl"
}
]
},
{
"issuers": [
{
"module": "internal"
}
]
}
]
}
}
}
}
@@ -0,0 +1,143 @@
{
email my.email@example.com
}
:82 {
redir https://example.com{uri}
}
:83 {
redir https://example.com{uri}
}
:84 {
redir https://example.com{uri}
}
abc.de {
redir https://example.com{uri}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"abc.de"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"https://example.com{http.request.uri}"
]
},
"status_code": 302
}
]
}
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
":82"
],
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"https://example.com{http.request.uri}"
]
},
"status_code": 302
}
]
}
]
},
"srv2": {
"listen": [
":83"
],
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"https://example.com{http.request.uri}"
]
},
"status_code": 302
}
]
}
]
},
"srv3": {
"listen": [
":84"
],
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"https://example.com{http.request.uri}"
]
},
"status_code": 302
}
]
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"issuers": [
{
"email": "my.email@example.com",
"module": "acme"
},
{
"email": "my.email@example.com",
"module": "zerossl"
}
]
}
]
}
}
}
}
@@ -0,0 +1,62 @@
a.example.com {
}
b.example.com {
}
:443 {
tls {
on_demand
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"b.example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"a.example.com",
"b.example.com"
]
},
{
"on_demand": true
}
]
}
}
}
}

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