Compare commits

..

278 Commits

Author SHA1 Message Date
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
Matt Holt 385adf5d87 caddyhttp: Restore original request params before error handlers (#3781)
* caddyhttp: Restore original request params before error handlers

Fixes #3717

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

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

* reverseproxy: add integration tests

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

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

* reverseproxy: validate against incompatible config options with lookup_srv

* reverseproxy: add integration test cases for validations involving lookup_srv

* reverseproxy: clarify the reason for skipping an iteration

* grammar.. Oxford comma

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

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

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

* reverseproxy: replace integration test with an adapter test

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

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

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

* Apply suggestions from code review

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

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-09-25 18:00:08 -06:00
Mohammed Al Sahaf a33e4b5426 caddyfile: Add support for vars and vars_regexp matchers (#3730)
* caddyfile: support vars and vars_regexp matchers in the caddyfile

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

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

* Just use strings.ToUpper for clarity

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

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

* Minor cleanup

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

* Add integration test for auto HTTP->HTTPS redirects

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

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

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

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

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

* reverseproxy: cleanup health-check integration test

* reverseproxy: fix health-check of unix socket upstreams

* reverseproxy: skip unix socket tests on Windows

* tabs > spaces

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

* make the linter (and @francislavoie) happy

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

* One more lint fix

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

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

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

* httpcaddyfile: Add comment linking to the issue being fixed

* httpcaddyfile: Typo fix, comment clarity

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

* Update caddyconfig/httpcaddyfile/httptype.go

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

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

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

* fileserver: Review cleanup

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

* Update modules/caddyhttp/replacer.go

Change type of block and empty headers removed

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

* fixed tests

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

* logging: Add support for parsing IP masks from strings


wip

* logging: Implement Caddyfile support for ip_mask

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

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

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

* Minor adjustments

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

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

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

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

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

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

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

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

* Go 1.15 was released; let's try it

* Update to latest quic-go

* Attempt at fixing broken test

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

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

* httpcaddyfile: Generalize reverse sort directives, improve logic

* httpcaddyfile: Fix "spelling" issue

* httpcaddyfile: Turns out the second change precludes the first


httpcaddyfile: Delete test that no longer makes sense

* httpcaddyfile: Shorten logic

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

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

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

New Caddyfile global option:

    {
        cert_issuer <name> ...
    }

Or, alternatively, as a tls subdirective:

    tls {
        issuer <name> ...
    }

For example, to use ZeroSSL with an API key:

    {
        cert_issuser zerossl API_KEY
    }

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

    {
        cert_issuer acme {
            eab KEY_ID MAC_KEY
        }
    }

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

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

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

* Fix broken test

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

* Fix broken test (post-merge)

* Update modules/caddytls/acmeissuer.go

Fix godoc comment

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

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

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

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

* Wrap underlying logic rather than repeating it

Oops, duh

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

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

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

* Minor cleanup; apply review suggestions

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

* Add unit test for caddyfile admin config update

* Add caddyfile adapt test for typical admin setup

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

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

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

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

* ci: split cross-build check into separate workflow

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

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

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

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

* ci: Simplify workflow names

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-08-01 20:23:22 +00:00
Matt Holt c054a818a1 fileserver: Fix newly-introduced failing test on Linux (#3625)
* fileserver: First attempt to fix failing test on Linux

I think I updated the wrong test case before

* Make new test function

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

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

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

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

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

* Update modules/caddyhttp/templates/templates.go

updates JSON doc

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

* Update modules/caddyhttp/templates/tplcontext.go

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

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

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

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

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

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

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

* reverse proxy: adjust comments as francislavoie suggests

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

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

* Improve documentation of reverseproxy.UpstreamResolver fields

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

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

* remove support for bootstraper of resolver

* godoc and code-style changes

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

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

* Update modules/caddyhttp/reverseproxy/caddyfile.go

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

* Rename empty transport test

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

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

* Update browselisting.go

* Update browsetpl.go

* fix linter err

* Update modules/caddyhttp/fileserver/browse.go

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

* Update modules/caddyhttp/fileserver/browselisting.go

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

* Update browsetpl.go

change from -> offset

* Update browse.go

* Update browselisting.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-07-08 23:56:15 -06:00
Matthew Holt a2dae1d43f templates: Fix front matter closing fence search
This makes it choose first matching closing fence instead of last one,
which could appear in document body.
2020-07-08 16:46:56 -06:00
Matthew Holt efc0cc5e85 caddytls: Move initial storage clean op into goroutine
Sometimes this operation can take a while (we observed 7 minutes
recently, with a large, globally-distributed storage backend).
2020-07-08 10:59:49 -06:00
Matthew Holt 0bf2565c37 caddyhttp: Reorder some access log fields; add host matcher test case
This field order reads a little more naturally.
2020-07-07 08:11:35 -06:00
Matthew Holt 7bfe5b6c95 httpcaddyfile: Reorder automation policy logic (close #3550) 2020-07-07 08:10:37 -06:00
Matthew Holt 2a5599e2ad go.mod: Upgrade and downgrade smallstep, quic-go, and cpuid
Closes #3537 and fixes #3535
2020-07-06 12:10:35 -06:00
Greg Anders c35820012b templates: Disable hard wraps in Markdown rendering (#3553) 2020-07-06 11:53:40 -06:00
Francis Lavoie 2d0f8831f8 ci: Fix another oops with publish workflow (#3536) 2020-06-30 15:36:17 -04:00
Mohammed Al Sahaf d7dbf85525 cel: fix validation of expression result type (#3526)
* cel: fix validation of expression result type

The earlier code used the proto.Equals from github.com/gogo/protobuf, which failed to compare two messages of the same type for some reason. Switching to proto.Equal from the canonical github.com/golang/protobuf fixes the issue.

* deps: remove deprecated github.com/golang/protobuf in favor of google.golang.org/protobuf

* downgrade github.com/smallstep/nosql to resolve warning pb.proto warning
2020-06-30 11:53:29 -06:00
Matthew Holt 77f233a484 caddyhttp: Corrected host label index check (fix #3502) 2020-06-30 11:43:01 -06:00
James Birtles ddd690de4c caddyhttp: Support placeholders in query matcher (#3521) 2020-06-26 15:14:47 -06:00
Mark Sargent 6004d3f779 caddyhttp: Add 'map' handler (#3199)
* inital map implementation

* resolve the value during middleware execution

* use regex instead

* pr feedback

* renamed mmap to maphandler

* refactored GetString implementation

* fixed mispelling

* additional feedback
2020-06-26 15:12:37 -06:00
Francis Lavoie caca55e582 ci: Fix release publish trigger (#3524)
Looks like event payloads need to be prefixed with `github.event` to get the actual payload contents. Didn't dig deep enough.

https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#github-context
2020-06-26 16:00:54 -04:00
222 changed files with 16327 additions and 3368 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.
+2 -2
View File
@@ -11,7 +11,7 @@ Please note that we consider publicly-registered domain names to be public infor
| Version | Supported |
| ------- | ------------------ |
| 2.x | :white_check_mark: |
| 1.x | :white_check_mark: (deprecating soon) |
| 1.x | :x: |
| < 1.x | :x: |
## Reporting a Vulnerability
@@ -22,6 +22,6 @@ We'll need enough information to verify the bug and make a patch. It will speed
Please also understand that due to our nature as an open source project, we do not have a budget to award security bounties. We can only thank you.
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!
+12 -24
View File
@@ -1,6 +1,6 @@
# Used as inspiration: https://github.com/mvdan/github-actions-golang
name: Cross-Platform
name: Tests
on:
push:
@@ -17,7 +17,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
go-version: [ 1.14.x ]
go: [ '1.15', '1.16' ]
# Set some variables per OS, usable via ${{ matrix.VAR }}
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
@@ -39,9 +39,9 @@ jobs:
steps:
- name: Install Go
uses: actions/setup-go@v1
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
go-version: ${{ matrix.go }}
- name: Checkout code
uses: actions/checkout@v2
@@ -64,17 +64,18 @@ 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)"
- name: Cache the build cache
uses: actions/cache@v1
uses: actions/cache@v2
with:
path: ${{ steps.vars.outputs.go_cache }}
key: ${{ runner.os }}-go-ci-${{ hashFiles('**/go.sum') }}
key: ${{ runner.os }}-${{ matrix.go }}-go-ci-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-ci
${{ runner.os }}-${{ matrix.go }}-go-ci
- name: Get dependencies
run: |
@@ -91,7 +92,7 @@ jobs:
- name: Publish Build Artifact
uses: actions/upload-artifact@v1
with:
name: caddy_v2_${{ runner.os }}_${{ steps.vars.outputs.short_sha }}
name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
path: ${{ matrix.CADDY_BIN_PATH }}
# Commented bits below were useful to allow the job to continue
@@ -124,6 +125,7 @@ jobs:
name: test (s390x on IBM Z)
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
steps:
- name: Checkout code into the Go module directory
uses: actions/checkout@v2
@@ -136,7 +138,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
@@ -147,26 +149,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@v1
- uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: check
+60
View File
@@ -0,0 +1,60 @@
name: Cross-Build
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
cross-build-test:
strategy:
fail-fast: false
matrix:
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
go: [ '1.15', '1.16' ]
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Print Go version and environment
id: vars
run: |
printf "Using go at: $(which go)\n"
printf "Go version: $(go version)\n"
printf "\n\nGo environment:\n\n"
go env
printf "\n\nSystem environment:\n\n"
env
echo "::set-output name=go_cache::$(go env GOCACHE)"
- name: Cache the build cache
uses: actions/cache@v2
with:
path: ${{ steps.vars.outputs.go_cache }}
key: cross-build-go${{ matrix.go }}-${{ matrix.goos }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
cross-build-go${{ matrix.go }}-${{ matrix.goos }}
- name: Checkout code into the Go module directory
uses: actions/checkout@v2
- name: Run Build
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
shell: bash
continue-on-error: true
working-directory: ./cmd/caddy
run: |
GOOS=$GOOS go build -trimpath -o caddy-"$GOOS"-amd64 2> /dev/null
if [ $? -ne 0 ]; then
echo "::warning ::$GOOS Build Failed"
exit 0
fi
-73
View File
@@ -1,73 +0,0 @@
name: Fuzzing
on:
# Daily midnight fuzzing
schedule:
- cron: '0 0 * * *'
jobs:
fuzzing:
name: Fuzzing
strategy:
matrix:
os: [ ubuntu-latest ]
go-version: [ 1.14.x ]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Download go-fuzz tools and the Fuzzit CLI, move Fuzzit CLI to GOBIN
# If we decide we need to prevent this from running on forks, we can use this line:
# if: github.repository == 'caddyserver/caddy'
run: |
go get -v github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
wget -q -O fuzzit https://github.com/fuzzitdev/fuzzit/releases/download/v2.4.77/fuzzit_Linux_x86_64
chmod a+x fuzzit
mv fuzzit $(go env GOPATH)/bin
echo "::add-path::$(go env GOPATH)/bin"
- name: Generate fuzzers & submit them to Fuzzit
continue-on-error: true
env:
FUZZIT_API_KEY: ${{ secrets.FUZZIT_API_KEY }}
SYSTEM_PULLREQUEST_SOURCEBRANCH: ${{ github.ref }}
BUILD_SOURCEVERSION: ${{ github.sha }}
run: |
# debug
echo "PR Source Branch: $SYSTEM_PULLREQUEST_SOURCEBRANCH"
echo "Source version: $BUILD_SOURCEVERSION"
declare -A fuzzers_funcs=(\
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="FuzzParseAddress" \
["./listeners_fuzz.go"]="FuzzParseNetworkAddress" \
["./replacer_fuzz.go"]="FuzzReplacer" \
)
declare -A fuzzers_targets=(\
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="parse-address" \
["./listeners_fuzz.go"]="parse-network-address" \
["./replacer_fuzz.go"]="replacer" \
)
fuzz_type="fuzzing"
for f in $(find . -name \*_fuzz.go); do
FUZZER_DIRECTORY=$(dirname "$f")
echo "go-fuzz-build func ${fuzzers_funcs[$f]} residing in $f"
go-fuzz-build -func "${fuzzers_funcs[$f]}" -o "$FUZZER_DIRECTORY/${fuzzers_targets[$f]}.zip" "$FUZZER_DIRECTORY"
fuzzit create job --engine go-fuzz caddyserver/"${fuzzers_targets[$f]}" "$FUZZER_DIRECTORY"/"${fuzzers_targets[$f]}.zip" --api-key "${FUZZIT_API_KEY}" --type "${fuzz_type}" --branch "${SYSTEM_PULLREQUEST_SOURCEBRANCH}" --revision "${BUILD_SOURCEVERSION}"
echo "Completed $f"
done
+23
View File
@@ -0,0 +1,23 @@
name: Lint
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
# From https://github.com/golangci/golangci-lint-action
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.31
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true
+84 -10
View File
@@ -11,21 +11,30 @@ jobs:
strategy:
matrix:
os: [ ubuntu-latest ]
go-version: [ 1.14.x ]
go: [ '1.16' ]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v1
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
go-version: ${{ matrix.go }}
- 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,17 +65,32 @@ 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@v1
uses: actions/cache@v2
with:
path: ${{ steps.vars.outputs.go_cache }}
key: ${{ runner.os }}-go-release-${{ hashFiles('**/go.sum') }}
key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-release
${{ runner.os }}-go${{ matrix.go }}-release
# GoReleaser will take care of publishing those artifacts into the release
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v1
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --rm-dist
@@ -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
+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/dist
event-type: release-tagged
client-payload: '{"tag": "${{ github.release.tag_name }}"}'
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
- name: Trigger event on caddyserver/caddy-docker
uses: peter-evans/repository-dispatch@v1
@@ -30,5 +30,5 @@ jobs:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/caddy-docker
event-type: release-tagged
client-payload: '{"tag": "${{ github.release.tag_name }}"}'
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
+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 -7
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
@@ -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
@@ -112,5 +124,6 @@ changelog:
- '^chore:'
- '^ci:'
- '^docs?:'
- '^readme:'
- '^tests?:'
- '^\w+\s+' # a hack to remove commit messages without colons thus don't correspond to a package
+49 -15
View File
@@ -1,21 +1,25 @@
<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">
<a href="https://github.com/caddyserver/caddy/actions?query=workflow%3ACross-Platform"><img src="https://github.com/caddyserver/caddy/workflows/Cross-Platform/badge.svg"></a>
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-blue.svg"></a>
<a href="https://app.fuzzit.dev/orgs/caddyserver-gh/dashboard"><img src="https://app.fuzzit.dev/badge?org_id=caddyserver-gh"></a>
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-%23007d9c.svg"></a>
<br>
<a href="https://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" alt="@caddyserver on Twitter"></a>
<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">Download</a> ·
<a href="https://github.com/caddyserver/caddy/releases">Releases</a> ·
<a href="https://caddyserver.com/docs/">Documentation</a> ·
<a href="https://caddy.community">Community</a>
<a href="https://caddy.community">Get Help</a>
</p>
@@ -23,6 +27,7 @@
### Menu
- [Features](#features)
- [Install](#install)
- [Build from source](#build-from-source)
- [For development](#for-development)
- [With version information and/or plugins](#with-version-information-andor-plugins)
@@ -39,45 +44,68 @@
</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
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/install.
## Build from source
Requirements:
- [Go 1.14 or newer](https://golang.org/dl/)
- [Go 1.15 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
Using [our builder tool](https://github.com/caddyserver/xcaddy)...
Using [our builder tool, `xcaddy`](https://github.com/caddyserver/xcaddy)...
```
$ xcaddy build
@@ -89,8 +117,8 @@ $ 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@TAG` replacing `TAG` with a git tag or commit.
6. (Optional) Add plugins by adding their import: `_ "IMPORT_PATH"`
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`
@@ -100,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. 🙂
@@ -119,7 +147,7 @@ The primary way to configure Caddy is through [its API](https://caddyserver.com/
Caddy exposes an unprecedented level of control compared to any web server in existence. In Caddy, you are usually setting the actual values of the initialized types in memory that power everything from your HTTP handlers and TLS handshakes to your storage medium. Caddy is also ridiculously extensible, with a powerful plugin system that makes vast improvements over other web servers.
To wield the power of this design, you need to know how the config document is structured. Please see the [our documentation site](https://caddyserver.com/docs/) for details about [Caddy's config structure](https://caddyserver.com/docs/json/).
To wield the power of this design, you need to know how the config document is structured. Please see [our documentation site](https://caddyserver.com/docs/) for details about [Caddy's config structure](https://caddyserver.com/docs/json/).
Nearly all of Caddy's configuration is contained in a single config document, rather than being scattered across CLI flags and env variables and a configuration file as with other web servers. This makes managing your server config more straightforward and reduces hidden variables/factors.
@@ -138,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).
@@ -146,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.
+507 -128
View File
@@ -17,7 +17,12 @@ package caddy
import (
"bytes"
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"expvar"
"fmt"
"io"
@@ -34,11 +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 {
@@ -56,84 +62,180 @@ 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"`
}
// 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) {
labels := prometheus.Labels{"path": pattern, "handler": handlerLabel}
h = instrumentHandlerCounter(
adminMetrics.requestCount.MustCurryWith(labels),
h,
)
muxWrap.mux.Handle(pattern, h)
}
// addRoute just calls muxWrap.mux.Handle after
// wrapping the handler with error handling
addRoute := func(pattern string, h AdminHandler) {
addRoute := func(pattern string, handlerLabel string, h AdminHandler) {
wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := h.ServeHTTP(w, r)
if err != nil {
labels := prometheus.Labels{
"path": pattern,
"handler": handlerLabel,
"method": strings.ToUpper(r.Method),
}
adminMetrics.requestErrors.With(labels).Inc()
}
muxWrap.handleError(w, r, err)
})
muxWrap.mux.Handle(pattern, wrapper)
addRouteWithMetrics(pattern, handlerLabel, wrapper)
}
const handlerLabel = "admin"
// register standard config control endpoints
addRoute("/"+rawConfigKey+"/", AdminHandlerFunc(handleConfig))
addRoute("/id/", AdminHandlerFunc(handleConfigID))
addRoute("/stop", AdminHandlerFunc(handleStop))
addRoute("/"+rawConfigKey+"/", handlerLabel, AdminHandlerFunc(handleConfig))
addRoute("/id/", handlerLabel, AdminHandlerFunc(handleConfigID))
addRoute("/stop", handlerLabel, AdminHandlerFunc(handleStop))
// register debugging endpoints
muxWrap.mux.HandleFunc("/debug/pprof/", pprof.Index)
muxWrap.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
muxWrap.mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
muxWrap.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
muxWrap.mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
muxWrap.mux.Handle("/debug/vars", expvar.Handler())
addRouteWithMetrics("/debug/pprof/", handlerLabel, http.HandlerFunc(pprof.Index))
addRouteWithMetrics("/debug/pprof/cmdline", handlerLabel, http.HandlerFunc(pprof.Cmdline))
addRouteWithMetrics("/debug/pprof/profile", handlerLabel, http.HandlerFunc(pprof.Profile))
addRouteWithMetrics("/debug/pprof/symbol", handlerLabel, http.HandlerFunc(pprof.Symbol))
addRouteWithMetrics("/debug/pprof/trace", handlerLabel, http.HandlerFunc(pprof.Trace))
addRouteWithMetrics("/debug/vars", handlerLabel, expvar.Handler())
// register third-party module endpoints
for _, m := range GetModules("admin.api") {
router := m.New().(AdminRouter)
handlerLabel := m.ID.Name()
for _, route := range router.Routes() {
addRoute(route.Pattern, route.Handler)
addRoute(route.Pattern, handlerLabel, route.Handler)
}
}
@@ -176,18 +278,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
@@ -215,19 +317,20 @@ 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{
localAdminServer = &http.Server{
Addr: addr.String(), // for logging purposes only
Handler: handler,
ReadTimeout: 10 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
@@ -235,21 +338,272 @@ func replaceAdmin(cfg *Config) error {
MaxHeaderBytes: 1024 * 64,
}
go adminServer.Serve(ln)
adminLogger := Log().Named("admin")
go func() {
if err := localAdminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
}
}()
Log().Named("admin").Info("admin endpoint started",
adminLogger.Info("admin endpoint started",
zap.String("address", addr.String()),
zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
zap.Strings("origins", handler.allowedOrigins))
if !handler.enforceHost {
Log().Named("admin").Warn("admin endpoint on open interface; host checking disabled",
adminLogger.Warn("admin endpoint on open interface; host checking disabled",
zap.String("address", addr.String()))
}
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
}
oldIdentityCertCache := identityCertCache
if oldIdentityCertCache != nil {
defer oldIdentityCertCache.Stop()
}
// 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))
}
}
logger := Log().Named("admin.identity")
cmCfg := cfg.Admin.Identity.certmagicConfig(logger)
// 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)
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
}
// 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,
}
// start listener
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
if err != nil {
return err
}
ln = tls.NewListener(ln, tlsConfig)
go func() {
if err := remoteAdminServer.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) *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 identityCertCache == nil {
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)
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")
@@ -260,7 +614,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
}
@@ -276,22 +630,38 @@ 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.
// It will only be called once per request.
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Log().Named("admin.api").Info("received request",
log := Log().Named("admin.api").With(
zap.String("method", r.Method),
zap.String("host", r.Host),
zap.String("uri", r.RequestURI),
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 {
log.Info("received request")
}
h.serveHTTP(w, r)
}
@@ -299,6 +669,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
@@ -332,8 +710,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)
}
@@ -341,20 +717,16 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
if err == nil {
return
}
if err == ErrInternalRedir {
h.serveHTTP(w, r)
return
}
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()
@@ -362,12 +734,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
@@ -384,8 +759,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
@@ -399,14 +774,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
@@ -446,7 +821,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
@@ -461,8 +836,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),
}
}
@@ -473,8 +848,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()
@@ -489,8 +864,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),
}
}
@@ -521,46 +896,22 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
parts = append([]string{expanded}, parts[3:]...)
r.URL.Path = path.Join(parts...)
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 {
func handleStop(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
}
@@ -772,9 +1123,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 {
@@ -784,20 +1135,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,
}
@@ -807,7 +1182,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
}
@@ -835,4 +1210,8 @@ var bufPool = sync.Pool{
},
}
var adminServer *http.Server
// keep a reference to admin endpoint singletons while they're active
var (
localAdminServer, remoteAdminServer *http.Server
identityCertCache *certmagic.Cache
)
+194 -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,19 @@ 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 {
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs")
}
// run the new config and start all its apps
err = run(newCfg, true)
if err != nil {
@@ -259,7 +286,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 +301,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 +337,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 +374,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 +428,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 +448,64 @@ 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)
}
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 func() {
Log().Info("applying dynamically-loaded config", zap.String("loader_module", val.(Module).CaddyModule().ID.Name()))
currentCfgMu.Lock()
err := unsyncedDecodeAndRun(loadedConfig, false)
currentCfgMu.Unlock()
if err == nil {
Log().Info("dynamically-loaded config applied successfully")
} else {
Log().Error("running dynamically-loaded config failed", zap.Error(err))
}
}()
}
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 +548,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 != "" {
os.Remove(pidfile)
}
return nil
}
// Validate loads, provisions, and validates
// cfg, but does not start running it.
func Validate(cfg *Config) error {
@@ -486,6 +558,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 +674,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
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
}
+24 -3
View File
@@ -16,6 +16,7 @@ package caddyfile
import (
"bufio"
"bytes"
"io"
"unicode"
)
@@ -34,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
}
)
@@ -168,3 +171,21 @@ func (l *lexer) next() bool {
val = append(val, ch)
}
}
// Tokenize takes bytes as input and lexes it into
// a list of tokens that can be parsed as a Caddyfile.
// Also takes a filename to fill the token's File as
// the source of the tokens, which is important to
// determine relative paths for `import` directives.
func Tokenize(input []byte, filename string) ([]Token, error) {
l := lexer{}
if err := l.load(bytes.NewReader(input)); err != nil {
return nil, err
}
var tokens []Token
for l.next() {
l.token.File = filename
tokens = append(tokens, l.token)
}
return tokens, nil
}
@@ -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
}
+37 -47
View File
@@ -15,37 +15,35 @@
package caddyfile
import (
"log"
"strings"
"testing"
)
type lexerTestCase struct {
input string
input []byte
expected []Token
}
func TestLexer(t *testing.T) {
testCases := []lexerTestCase{
{
input: `host:123`,
input: []byte(`host:123`),
expected: []Token{
{Line: 1, Text: "host:123"},
},
},
{
input: `host:123
input: []byte(`host:123
directive`,
directive`),
expected: []Token{
{Line: 1, Text: "host:123"},
{Line: 3, Text: "directive"},
},
},
{
input: `host:123 {
input: []byte(`host:123 {
directive
}`,
}`),
expected: []Token{
{Line: 1, Text: "host:123"},
{Line: 1, Text: "{"},
@@ -54,7 +52,7 @@ func TestLexer(t *testing.T) {
},
},
{
input: `host:123 { directive }`,
input: []byte(`host:123 { directive }`),
expected: []Token{
{Line: 1, Text: "host:123"},
{Line: 1, Text: "{"},
@@ -63,12 +61,12 @@ func TestLexer(t *testing.T) {
},
},
{
input: `host:123 {
input: []byte(`host:123 {
#comment
directive
# comment
foobar # another comment
}`,
}`),
expected: []Token{
{Line: 1, Text: "host:123"},
{Line: 1, Text: "{"},
@@ -78,10 +76,10 @@ func TestLexer(t *testing.T) {
},
},
{
input: `host:123 {
input: []byte(`host:123 {
# hash inside string is not a comment
redir / /some/#/path
}`,
}`),
expected: []Token{
{Line: 1, Text: "host:123"},
{Line: 1, Text: "{"},
@@ -92,14 +90,14 @@ func TestLexer(t *testing.T) {
},
},
{
input: "# comment at beginning of file\n# comment at beginning of line\nhost:123",
input: []byte("# comment at beginning of file\n# comment at beginning of line\nhost:123"),
expected: []Token{
{Line: 3, Text: "host:123"},
},
},
{
input: `a "quoted value" b
foobar`,
input: []byte(`a "quoted value" b
foobar`),
expected: []Token{
{Line: 1, Text: "a"},
{Line: 1, Text: "quoted value"},
@@ -108,7 +106,7 @@ func TestLexer(t *testing.T) {
},
},
{
input: `A "quoted \"value\" inside" B`,
input: []byte(`A "quoted \"value\" inside" B`),
expected: []Token{
{Line: 1, Text: "A"},
{Line: 1, Text: `quoted "value" inside`},
@@ -116,7 +114,7 @@ func TestLexer(t *testing.T) {
},
},
{
input: "An escaped \"newline\\\ninside\" quotes",
input: []byte("An escaped \"newline\\\ninside\" quotes"),
expected: []Token{
{Line: 1, Text: "An"},
{Line: 1, Text: "escaped"},
@@ -125,7 +123,7 @@ func TestLexer(t *testing.T) {
},
},
{
input: "An escaped newline\\\noutside quotes",
input: []byte("An escaped newline\\\noutside quotes"),
expected: []Token{
{Line: 1, Text: "An"},
{Line: 1, Text: "escaped"},
@@ -135,7 +133,7 @@ func TestLexer(t *testing.T) {
},
},
{
input: "line1\\\nescaped\nline2\nline3",
input: []byte("line1\\\nescaped\nline2\nline3"),
expected: []Token{
{Line: 1, Text: "line1"},
{Line: 1, Text: "escaped"},
@@ -144,7 +142,7 @@ func TestLexer(t *testing.T) {
},
},
{
input: "line1\\\nescaped1\\\nescaped2\nline4\nline5",
input: []byte("line1\\\nescaped1\\\nescaped2\nline4\nline5"),
expected: []Token{
{Line: 1, Text: "line1"},
{Line: 1, Text: "escaped1"},
@@ -154,34 +152,34 @@ func TestLexer(t *testing.T) {
},
},
{
input: `"unescapable\ in quotes"`,
input: []byte(`"unescapable\ in quotes"`),
expected: []Token{
{Line: 1, Text: `unescapable\ in quotes`},
},
},
{
input: `"don't\escape"`,
input: []byte(`"don't\escape"`),
expected: []Token{
{Line: 1, Text: `don't\escape`},
},
},
{
input: `"don't\\escape"`,
input: []byte(`"don't\\escape"`),
expected: []Token{
{Line: 1, Text: `don't\\escape`},
},
},
{
input: `un\escapable`,
input: []byte(`un\escapable`),
expected: []Token{
{Line: 1, Text: `un\escapable`},
},
},
{
input: `A "quoted value with line
input: []byte(`A "quoted value with line
break inside" {
foobar
}`,
}`),
expected: []Token{
{Line: 1, Text: "A"},
{Line: 1, Text: "quoted value with line\n\t\t\t\t\tbreak inside"},
@@ -191,13 +189,13 @@ func TestLexer(t *testing.T) {
},
},
{
input: `"C:\php\php-cgi.exe"`,
input: []byte(`"C:\php\php-cgi.exe"`),
expected: []Token{
{Line: 1, Text: `C:\php\php-cgi.exe`},
},
},
{
input: `empty "" string`,
input: []byte(`empty "" string`),
expected: []Token{
{Line: 1, Text: `empty`},
{Line: 1, Text: ``},
@@ -205,7 +203,7 @@ func TestLexer(t *testing.T) {
},
},
{
input: "skip those\r\nCR characters",
input: []byte("skip those\r\nCR characters"),
expected: []Token{
{Line: 1, Text: "skip"},
{Line: 1, Text: "those"},
@@ -214,13 +212,13 @@ func TestLexer(t *testing.T) {
},
},
{
input: "\xEF\xBB\xBF:8080", // test with leading byte order mark
input: []byte("\xEF\xBB\xBF:8080"), // test with leading byte order mark
expected: []Token{
{Line: 1, Text: ":8080"},
},
},
{
input: "simple `backtick quoted` string",
input: []byte("simple `backtick quoted` string"),
expected: []Token{
{Line: 1, Text: `simple`},
{Line: 1, Text: `backtick quoted`},
@@ -228,7 +226,7 @@ func TestLexer(t *testing.T) {
},
},
{
input: "multiline `backtick\nquoted\n` string",
input: []byte("multiline `backtick\nquoted\n` string"),
expected: []Token{
{Line: 1, Text: `multiline`},
{Line: 1, Text: "backtick\nquoted\n"},
@@ -236,7 +234,7 @@ func TestLexer(t *testing.T) {
},
},
{
input: "nested `\"quotes inside\" backticks` string",
input: []byte("nested `\"quotes inside\" backticks` string"),
expected: []Token{
{Line: 1, Text: `nested`},
{Line: 1, Text: `"quotes inside" backticks`},
@@ -244,7 +242,7 @@ func TestLexer(t *testing.T) {
},
},
{
input: "reverse-nested \"`backticks` inside\" quotes",
input: []byte("reverse-nested \"`backticks` inside\" quotes"),
expected: []Token{
{Line: 1, Text: `reverse-nested`},
{Line: 1, Text: "`backticks` inside"},
@@ -254,22 +252,14 @@ func TestLexer(t *testing.T) {
}
for i, testCase := range testCases {
actual := tokenize(testCase.input)
actual, err := Tokenize(testCase.input, "")
if err != nil {
t.Errorf("%v", err)
}
lexerCompare(t, i, testCase.expected, actual)
}
}
func tokenize(input string) (tokens []Token) {
l := lexer{}
if err := l.load(strings.NewReader(input)); err != nil {
log.Printf("[ERROR] load failed: %v", err)
}
for l.next() {
tokens = append(tokens, l.token)
}
return
}
func lexerCompare(t *testing.T, n int, expected, actual []Token) {
if len(expected) != len(actual) {
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
+56 -16
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
}
@@ -87,16 +104,10 @@ func allTokens(filename string, input []byte) ([]Token, error) {
if err != nil {
return nil, err
}
l := new(lexer)
err = l.load(bytes.NewReader(input))
tokens, err := Tokenize(input, filename)
if err != nil {
return nil, err
}
var tokens []Token
for l.next() {
l.token.File = filename
tokens = append(tokens, l.token)
}
return tokens, nil
}
@@ -106,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) {
@@ -161,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
@@ -300,7 +321,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)
}
@@ -310,10 +331,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
@@ -349,7 +375,6 @@ func (p *parser) doImport() error {
}
// collect all the imported tokens
for _, importFile := range matches {
newTokens, err := p.doSingleImport(importFile)
if err != nil {
@@ -357,6 +382,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
@@ -554,4 +591,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 = ":"
)
+39
View File
@@ -444,6 +444,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 +500,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 +546,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
}
+255 -101
View File
@@ -22,6 +22,7 @@ import (
"io/ioutil"
"net/http"
"reflect"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2"
@@ -29,6 +30,8 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
"go.uber.org/zap/zapcore"
)
@@ -38,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)
@@ -73,8 +78,10 @@ func parseBind(h Helper) ([]ConfigValue, error) {
// load <paths...>
// ca <acme_ca_endpoint>
// ca_root <pem_file>
// dns <provider_name>
// dns <provider_name> [...]
// on_demand
// eab <key_id> <mac_key>
// issuer <module_name> [...]
// }
//
func parseTLS(h Helper) ([]ConfigValue, error) {
@@ -83,7 +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 issuers []certmagic.Issuer
var onDemand bool
for h.Next() {
@@ -117,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
@@ -262,6 +271,42 @@ 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 {
return nil, h.ArgErr()
}
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
acmeIssuer.ExternalAccount = &acme.EAB{
KeyID: arg[0],
MACKey: arg[1],
}
case "issuer":
if !h.NextArg() {
return nil, h.ArgErr()
}
modName := h.Val()
modID := "tls.issuance." + modName
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
if err != nil {
return nil, err
}
issuer, ok := unm.(certmagic.Issuer)
if !ok {
return nil, h.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm)
}
issuers = append(issuers, issuer)
case "dns":
if !h.NextArg() {
return nil, h.ArgErr()
@@ -272,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()
@@ -315,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 {
@@ -331,34 +388,64 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
})
}
// issuer
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")
// 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 {
// 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))
if acmeIssuer != nil && internalIssuer != nil {
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 len(issuers) > 0:
for _, issuer := range issuers {
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: issuer,
})
}
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: acmeIssuer,
})
} else if internalIssuer != nil {
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,
})
}
// certificate key type
if keyType != "" {
configVals = append(configVals, ConfigValue{
Class: "tls.key_type",
Value: keyType,
})
}
// on-demand TLS
if onDemand {
configVals = append(configVals, ConfigValue{
@@ -421,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>
@@ -443,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{
@@ -462,40 +558,46 @@ 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)
for h.Next() {
for nesting := h.Nesting(); h.NextBlock(nesting); {
dir := h.Val()
allResults, err := parseSegmentAsConfig(h)
if err != nil {
return nil, err
}
dirFunc, ok := registeredDirectives[dir]
if !ok {
return nil, h.Errf("unrecognized directive: %s", dir)
}
subHelper := h
subHelper.Dispenser = h.NewFromNextSegment()
results, err := dirFunc(subHelper)
if err != nil {
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
}
for _, result := range results {
switch handler := result.Value.(type) {
case caddyhttp.Route:
sr.Routes = append(sr.Routes, handler)
case caddyhttp.Subroute:
// directives which return a literal subroute instead of a route
// means they intend to keep those handlers together without
// them being reordered; we're doing that anyway since we're in
// the route directive, so just append its handlers
sr.Routes = append(sr.Routes, handler.Routes...)
default:
return nil, h.Errf("%s directive returned something other than an HTTP route or subroute: %#v (only handler directives can be used in routes)", dir, result.Value)
}
}
for _, result := range allResults {
switch handler := result.Value.(type) {
case caddyhttp.Route:
sr.Routes = append(sr.Routes, handler)
case caddyhttp.Subroute:
// directives which return a literal subroute instead of a route
// means they intend to keep those handlers together without
// them being reordered; we're doing that anyway since we're in
// the route directive, so just append its handlers
sr.Routes = append(sr.Routes, handler.Routes...)
default:
return nil, h.Errf("%s directive returned something other than an HTTP route or subroute: %#v (only handler directives can be used in routes)", result.directive, result.Value)
}
}
@@ -528,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)
@@ -558,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)
@@ -582,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)
@@ -609,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
+56 -17
View File
@@ -37,9 +37,11 @@ import (
// The header directive goes second so that headers
// can be manipulated before doing redirects.
var directiveOrder = []string{
"map",
"root",
"header",
"request_body",
"redir",
"rewrite",
@@ -54,17 +56,21 @@ var directiveOrder = []string{
"encode",
"templates",
// special routing directives
// special routing & dispatching directives
"handle",
"handle_path",
"route",
"push",
// handlers that typically respond to requests
"respond",
"metrics",
"reverse_proxy",
"php_fastcgi",
"file_server",
"acme_server",
"abort",
"error",
}
// directiveIsOrdered returns true if dir is
@@ -99,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
@@ -197,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
@@ -263,13 +265,37 @@ 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.
func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
allResults, err := parseSegmentAsConfig(h)
if err != nil {
return nil, err
}
return buildSubroute(allResults, h.groupCounter)
}
// parseSegmentAsConfig parses the segment such that its subdirectives
// are themselves treated as directives, including named matcher definitions,
// and the raw Config structs are returned.
func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
var allResults []ConfigValue
for h.Next() {
// don't allow non-matcher args on the first line
if h.NextArg() {
return nil, h.ArgErr()
}
// slice the linear list of tokens into top-level segments
var segments []caddyfile.Segment
for nesting := h.Nesting(); h.NextBlock(nesting); {
@@ -284,13 +310,17 @@ func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
}
// find and extract any embedded matcher definitions in this scope
for i, seg := range segments {
for i := 0; i < len(segments); i++ {
seg := segments[i]
if strings.HasPrefix(seg.Directive(), matcherPrefix) {
// parse, then add the matcher to matcherDefs
err := parseMatcherDefinitions(caddyfile.NewDispenser(seg), matcherDefs)
if err != nil {
return nil, err
}
// remove the matcher segment (consumed), then step back the loop
segments = append(segments[:i], segments[i+1:]...)
i--
}
}
@@ -317,7 +347,7 @@ func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
}
}
return buildSubroute(allResults, h.groupCounter)
return allResults, nil
}
// ConfigValue represents a value to be added to the final
@@ -384,6 +414,14 @@ func sortRoutes(routes []ConfigValue) {
if len(jPM) > 0 {
jPathLen = len(jPM[0])
}
// if both directives have no path matcher, use whichever one
// has any kind of matcher defined first.
if iPathLen == 0 && jPathLen == 0 {
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
}
// sort with the most-specific (longest) path first
return iPathLen > jPathLen
})
}
@@ -469,9 +507,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)
+218 -55
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,7 @@ 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}",
)
// these are placeholders that allow a user-defined final
@@ -172,6 +186,15 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
if err != nil {
return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err)
}
// As a special case, we want "handle_path" to be sorted
// at the same level as "handle", so we force them to use
// the same directive name after their parsing is complete.
// See https://github.com/caddyserver/caddy/issues/3675#issuecomment-678042377
if dir == "handle_path" {
dir = "handle"
}
for _, result := range results {
result.directive = dir
sb.pile[result.Class] = append(sb.pile[result.Class], result)
@@ -197,9 +220,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
@@ -208,30 +232,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))
}
}
}
@@ -249,24 +281,34 @@ 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",
storageCvtr.(caddy.Module).CaddyModule().ID.Name(),
&warnings)
}
if adminConfig, ok := options["admin"].(string); ok && adminConfig != "" {
if adminConfig == "off" {
cfg.Admin = &caddy.AdminConfig{Disabled: true}
} else {
cfg.Admin = &caddy.AdminConfig{Listen: adminConfig}
}
if adminConfig, ok := options["admin"].(*caddy.AdminConfig); ok && adminConfig != nil {
cfg.Admin = adminConfig
}
if len(customLogs) > 0 {
if cfg.Logging == nil {
@@ -281,7 +323,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)
@@ -305,23 +347,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
@@ -365,6 +452,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
@@ -379,7 +469,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) {
@@ -390,7 +480,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) {
@@ -400,9 +490,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 {
@@ -420,6 +513,15 @@ 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
}
}
// create a subroute for each site in the server block
for _, sblock := range p.serverBlocks {
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
@@ -429,6 +531,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
@@ -450,27 +559,31 @@ func (st *ServerType) serversFromPairings(
}
} else {
cp.DefaultSNI = defaultSNI
hasCatchAllTLSConnPolicy = true
}
// only append this policy if it actually changes something
if !cp.SettingsEmpty() {
srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
hasCatchAllTLSConnPolicy = len(hosts) == 0
}
}
}
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" ||
@@ -533,13 +646,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)
}
@@ -596,6 +709,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
}
@@ -657,9 +775,15 @@ func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock,
return nil
}
// consolidateConnPolicies removes empty TLS connection policies and combines
// equivalent ones for a cleaner overall output.
// consolidateConnPolicies sorts any catch-all policy to the end, removes empty TLS connection
// policies, and combines equivalent ones for a cleaner overall output.
func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.ConnectionPolicies, error) {
// catch-all policies (those without any matcher) should be at the
// end, otherwise it nullifies any more specific policies
sort.SliceStable(cps, func(i, j int) bool {
return cps[j].MatchersRaw == nil && cps[i].MatchersRaw != nil
})
for i := 0; i < len(cps); i++ {
// compare it to the others
for j := 0; j < len(cps); j++ {
@@ -852,7 +976,18 @@ func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subro
// root directives would overwrite previously-matched ones; they should not cascade
"root": {},
}
for meDir, info := range mutuallyExclusiveDirs {
// we need to deterministically loop over each of these directives
// in order to keep the group numbers consistent
keys := make([]string, 0, len(mutuallyExclusiveDirs))
for k := range mutuallyExclusiveDirs {
keys = append(keys, k)
}
sort.Strings(keys)
for _, meDir := range keys {
info := mutuallyExclusiveDirs[meDir]
// see how many instances of the directive there are
for _, r := range routes {
if r.directive == meDir {
@@ -1112,6 +1247,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 {
@@ -1122,6 +1265,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.
+50 -24
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,54 @@ func TestGlobalOptions(t *testing.T) {
}
:80
`,
expectWarn: false,
expectError: true,
},
{
input: `
{
admin {
enforce_origin
origins 192.168.1.1:2020 127.0.0.1:2020
}
}
:80
`,
expectError: false,
},
{
input: `
{
admin 127.0.0.1:2020 {
enforce_origin
origins 192.168.1.1:2020 127.0.0.1:2020
}
}
:80
`,
expectError: false,
},
{
input: `
{
admin 192.168.1.1:2020 127.0.0.1:2020 {
enforce_origin
origins 192.168.1.1:2020 127.0.0.1:2020
}
}
:80
`,
expectError: true,
},
{
input: `
{
admin off {
enforce_origin
origins 192.168.1.1:2020 127.0.0.1:2020
}
}
:80
`,
expectError: true,
},
} {
@@ -170,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)
+180 -38
View File
@@ -18,35 +18,41 @@ 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"
"github.com/mholt/acmez/acme"
)
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("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)
}
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
@@ -62,7 +68,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
@@ -78,7 +84,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() {
@@ -154,35 +160,60 @@ 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) {
eab := new(caddytls.ExternalAccountBinding)
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() {
return nil, d.ArgErr()
@@ -195,11 +226,11 @@ func parseOptACMEEAB(d *caddyfile.Dispenser) (interface{}, error) {
}
eab.KeyID = d.Val()
case "hmac":
case "mac_key":
if !d.NextArg() {
return nil, d.ArgErr()
}
eab.HMAC = d.Val()
eab.MACKey = d.Val()
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
@@ -209,7 +240,30 @@ func parseOptACMEEAB(d *caddyfile.Dispenser) (interface{}, error) {
return eab, nil
}
func parseOptSingleString(d *caddyfile.Dispenser) (interface{}, error) {
func parseOptCertIssuer(d *caddyfile.Dispenser, existing interface{}) (interface{}, error) {
var issuers []certmagic.Issuer
if existing != nil {
issuers = existing.([]certmagic.Issuer)
}
for d.Next() { // consume option name
if !d.Next() { // get issuer module name
return nil, d.ArgErr()
}
modID := "tls.issuance." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
iss, ok := unm.(certmagic.Issuer)
if !ok {
return nil, d.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm)
}
issuers = append(issuers, iss)
}
return issuers, nil
}
func parseOptSingleString(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() // consume parameter name
if !d.Next() {
return "", d.ArgErr()
@@ -221,21 +275,43 @@ func parseOptSingleString(d *caddyfile.Dispenser) (interface{}, error) {
return val, nil
}
func parseOptAdmin(d *caddyfile.Dispenser) (interface{}, error) {
if d.Next() {
var listenAddress string
if !d.AllArgs(&listenAddress) {
return "", d.ArgErr()
func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
adminCfg := new(caddy.AdminConfig)
for d.Next() {
if d.NextArg() {
listenAddress := d.Val()
if listenAddress == "off" {
adminCfg.Disabled = true
if d.Next() { // Do not accept any remaining options including block
return nil, d.Err("No more option is allowed after turning off admin config")
}
} else {
adminCfg.Listen = listenAddress
if d.NextArg() { // At most 1 arg is allowed
return nil, d.ArgErr()
}
}
}
if listenAddress == "" {
listenAddress = caddy.DefaultAdminListen
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "enforce_origin":
adminCfg.EnforceOrigin = true
case "origins":
adminCfg.Origins = d.RemainingArgs()
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
}
}
return listenAddress, nil
}
return "", nil
if adminCfg.Listen == "" && !adminCfg.Disabled {
adminCfg.Listen = caddy.DefaultAdminListen
}
return adminCfg, nil
}
func parseOptOnDemand(d *caddyfile.Dispenser) (interface{}, error) {
func parseOptOnDemand(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
var ond *caddytls.OnDemandConfig
for d.Next() {
if d.NextArg() {
@@ -295,7 +371,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()
@@ -304,8 +380,74 @@ 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
}
+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)
}
}
}
+41
View File
@@ -0,0 +1,41 @@
// 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)}
for _, p := range pairings {
for _, sblock := range p.serverBlocks {
// find all the CAs that were defined and add them to the app config
for _, caCfgValue := range sblock.pile["pki.ca"] {
ca := caCfgValue.Value.(*caddypki.CA)
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
}
+334 -200
View File
@@ -21,12 +21,14 @@ import (
"reflect"
"sort"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
)
func (st ServerType) buildTLSApp(
@@ -38,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)
@@ -48,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 {
@@ -64,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
@@ -74,163 +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
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()
}
if acmeIssuer == nil {
continue
}
}
// 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 {
var ok bool
if acmeIssuer, ok = ap.Issuer.(*caddytls.ACMEIssuer); !ok {
break
// 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
}
}
// proceed to configure the ACME issuer's bind host, without
// overwriting any existing settings
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
if acmeIssuer.Challenges.BindHost == "" {
// only binding to one host is supported
var bindHost string
if bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 {
bindHost = bindHosts[0]
}
acmeIssuer.Challenges.BindHost = bindHost
}
ap.Issuer = acmeIssuer // we'll encode it later
}
if ap != nil {
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 = sblockHosts
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 !certmagic.SubjectIsIP(s) && !certmagic.SubjectIsInternal(s) && (strings.Count(s, "*.") < 2 || ap.OnDemand) {
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 {
@@ -277,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
@@ -286,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)
@@ -304,23 +313,56 @@ 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"]
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil
if hasGlobalACMEDefaults {
for _, ap := range tlsApp.Automation.Policies {
if len(ap.Issuers) == 0 {
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)
// ensure automation policies don't overlap subjects (this should be
// an error at provision-time as well, but catch it in the adapt phase
// for convenience)
@@ -334,29 +376,70 @@ func (st ServerType) buildTLSApp(
}
}
// consolidate automation policies that are the exact same
tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
// 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"]
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)
}
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) {
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 := 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
@@ -368,43 +451,24 @@ func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddycon
}
ap := new(caddytls.AutomationPolicy)
if hasKeyType {
ap.KeyType = keyType.(string)
}
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.(*caddytls.ExternalAccountBinding)
}
if keyType != nil {
ap.KeyType = keyType.(string)
}
ap.Issuer = 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
@@ -413,16 +477,39 @@ func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddycon
// 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
// 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 all policies are empty, we can return nil, as there is no need to set any policy
if emptyAPCount == len(aps) {
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:]...)
i--
break
}
@@ -432,30 +519,77 @@ 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 {
aps[i].Subjects = append(aps[i].Subjects, aps[j].Subjects...)
// avoid repeated subjects
for _, subj := range aps[j].Subjects {
if !sliceContains(aps[i].Subjects, subj) {
aps[i].Subjects = append(aps[i].Subjects, subj)
}
}
aps = append(aps[:j], aps[j+1:]...)
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
}
+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)
+82 -34
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()
}
@@ -430,7 +478,7 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
body := string(bytes)
if !strings.Contains(body, expectedBody) {
if body != expectedBody {
tc.t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
}
+105
View File
@@ -0,0 +1,105 @@
package integration
import (
"net/http"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
}
localhost
respond "Yahaha! You found me!"
`, "caddyfile")
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
}
func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
}
localhost:9443
respond "Yahaha! You found me!"
`, "caddyfile")
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
}
func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
}
localhost:1234
respond "Yahaha! You found me!"
`, "caddyfile")
tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect)
}
func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"ingress_server": {
"listen": [
":9080",
":9443"
],
"routes": [
{
"match": [
{
"host": ["localhost"]
}
]
}
]
}
}
}
}
}
`, "json")
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
}
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")
}
@@ -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,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,12 @@
"automation": {
"policies": [
{
"issuer": {
"module": "internal"
}
"issuers": [
{
"module": "internal"
}
],
"key_type": "ed25519"
}
],
"on_demand": {
@@ -10,7 +10,7 @@
acme_ca https://example.com
acme_eab {
key_id 4K2scIVbBpNd-78scadB2g
hmac abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh
mac_key abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh
}
acme_ca_root /path/to/ca.crt
email test@example.com
@@ -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": {
"hmac": "abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh",
"key_id": "4K2scIVbBpNd-78scadB2g"
},
"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
}
}
}
@@ -0,0 +1,83 @@
{
debug
http_port 8080
https_port 8443
default_sni localhost
order root first
storage file_system {
root /data
}
acme_ca https://example.com
acme_ca_root /path/to/ca.crt
email test@example.com
admin {
origins localhost:2019 [::1]:2019 127.0.0.1:2019 192.168.10.128
}
on_demand_tls {
ask https://example.com
interval 30s
burst 20
}
local_certs
key_type ed25519
}
:80
----------
{
"admin": {
"listen": "localhost:2019",
"origins": [
"localhost:2019",
"[::1]:2019",
"127.0.0.1:2019",
"192.168.10.128"
]
},
"logging": {
"logs": {
"default": {
"level": "DEBUG"
}
}
},
"storage": {
"module": "file_system",
"root": "/data"
},
"apps": {
"http": {
"http_port": 8080,
"https_port": 8443,
"servers": {
"srv0": {
"listen": [
":80"
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"issuers": [
{
"module": "internal"
}
],
"key_type": "ed25519"
}
],
"on_demand": {
"rate_limit": {
"interval": 30000000000,
"burst": 20
},
"ask": "https://example.com"
}
}
}
}
}
@@ -0,0 +1,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,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"
}
]
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,105 @@
:80 {
handle /api/* {
respond "api"
}
handle_path /static/* {
respond "static"
}
handle {
respond "handle"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"group": "group3",
"match": [
{
"path": [
"/static/*"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "rewrite",
"strip_path_prefix": "/static"
}
]
},
{
"handle": [
{
"body": "static",
"handler": "static_response"
}
]
}
]
}
]
},
{
"group": "group3",
"match": [
{
"path": [
"/api/*"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "api",
"handler": "static_response"
}
]
}
]
}
]
},
{
"group": "group3",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "handle",
"handler": "static_response"
}
]
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,132 @@
:80 {
header Denis "Ritchie"
header +Edsger "Dijkstra"
header ?John "von Neumann"
header -Wolfram
header {
Grace: "Hopper" # some users habitually suffix field names with a colon
+Ray "Solomonoff"
?Tim "Berners-Lee"
defer
}
@images path /images/*
header @images {
Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/images/*"
]
}
],
"handle": [
{
"handler": "headers",
"response": {
"set": {
"Cache-Control": [
"public, max-age=3600, stale-while-revalidate=86400"
]
}
}
}
]
},
{
"handle": [
{
"handler": "headers",
"response": {
"set": {
"Denis": [
"Ritchie"
]
}
}
},
{
"handler": "headers",
"response": {
"add": {
"Edsger": [
"Dijkstra"
]
}
}
},
{
"handler": "headers",
"response": {
"require": {
"headers": {
"John": null
}
},
"set": {
"John": [
"von Neumann"
]
}
}
},
{
"handler": "headers",
"response": {
"deferred": true,
"delete": [
"Wolfram"
]
}
},
{
"handler": "headers",
"response": {
"add": {
"Ray": [
"Solomonoff"
]
},
"deferred": true,
"set": {
"Grace": [
"Hopper"
]
}
}
},
{
"handler": "headers",
"response": {
"require": {
"headers": {
"Tim": null
}
},
"set": {
"Tim": [
"Berners-Lee"
]
}
}
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,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
}
}
}
}
}
}
@@ -0,0 +1,70 @@
: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"
}
}
}
}
}
}
@@ -9,6 +9,34 @@
@matcher3 not method PUT
respond @matcher3 "not put"
@matcher4 vars "{http.request.uri}" "/vars-matcher"
respond @matcher4 "from vars matcher"
@matcher5 vars_regexp static "{http.request.uri}" `\.([a-f0-9]{6})\.(css|js)$`
respond @matcher5 "from vars_regexp matcher with name"
@matcher6 vars_regexp "{http.request.uri}" `\.([a-f0-9]{6})\.(css|js)$`
respond @matcher6 "from vars_regexp matcher without name"
@matcher7 {
header Foo bar
header Foo foobar
header Bar foo
}
respond @matcher7 "header matcher merging values of the same field"
@matcher8 {
query foo=bar foo=baz bar=foo
query bar=baz
}
respond @matcher8 "query matcher merging pairs with the same keys"
@matcher9 {
header !Foo
header Bar foo
}
respond @matcher9 "header matcher with null field matcher"
}
----------
{
@@ -68,6 +96,117 @@
"handler": "static_response"
}
]
},
{
"match": [
{
"vars": {
"{http.request.uri}": "/vars-matcher"
}
}
],
"handle": [
{
"body": "from vars matcher",
"handler": "static_response"
}
]
},
{
"match": [
{
"vars_regexp": {
"{http.request.uri}": {
"name": "static",
"pattern": "\\.([a-f0-9]{6})\\.(css|js)$"
}
}
}
],
"handle": [
{
"body": "from vars_regexp matcher with name",
"handler": "static_response"
}
]
},
{
"match": [
{
"vars_regexp": {
"{http.request.uri}": {
"pattern": "\\.([a-f0-9]{6})\\.(css|js)$"
}
}
}
],
"handle": [
{
"body": "from vars_regexp matcher without name",
"handler": "static_response"
}
]
},
{
"match": [
{
"header": {
"Bar": [
"foo"
],
"Foo": [
"bar",
"foobar"
]
}
}
],
"handle": [
{
"body": "header matcher merging values of the same field",
"handler": "static_response"
}
]
},
{
"match": [
{
"query": {
"bar": [
"foo",
"baz"
],
"foo": [
"bar",
"baz"
]
}
}
],
"handle": [
{
"body": "query matcher merging pairs with the same keys",
"handler": "static_response"
}
]
},
{
"match": [
{
"header": {
"Bar": [
"foo"
],
"Foo": null
}
}
],
"handle": [
{
"body": "header matcher with null field matcher",
"handler": "static_response"
}
]
}
]
}
@@ -0,0 +1,31 @@
:80 {
route {
# unused matchers should not panic
# see https://github.com/caddyserver/caddy/issues/3745
@matcher1 path /path1
@matcher2 path /path2
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"handler": "subroute"
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,36 @@
:80 {
metrics /metrics {
disable_openmetrics
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/metrics"
]
}
],
"handle": [
{
"disable_openmetrics": true,
"handler": "metrics"
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,33 @@
:80 {
metrics /metrics
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/metrics"
]
}
],
"handle": [
{
"handler": "metrics"
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,132 @@
:8886
route {
# Add trailing slash for directory requests
@canonicalPath {
file {
try_files {path}/index.php
}
not path */
}
redir @canonicalPath {path}/ 308
# If the requested file does not exist, try index files
@indexFiles {
file {
try_files {path} {path}/index.php index.php
split_path .php
}
}
rewrite @indexFiles {http.matchers.file.relative}
# Proxy PHP files to the FastCGI responder
@phpFiles {
path *.php
}
reverse_proxy @phpFiles 127.0.0.1:9000 {
transport fastcgi {
split .php
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8886"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{
"dial": "127.0.0.1:9000"
}
]
}
],
"match": [
{
"path": [
"*.php"
]
}
]
}
]
}
]
}
]
}
}
}
}
}
@@ -1,15 +1,18 @@
:8884
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,113 @@
whoami.example.com {
reverse_proxy whoami
}
app.example.com {
reverse_proxy app:80
}
unix.example.com {
reverse_proxy unix//path/to/socket
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"whoami.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "whoami:80"
}
]
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"unix.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "unix//path/to/socket"
}
]
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"app.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "app:80"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,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"
]
}
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,36 @@
:8884
reverse_proxy 127.0.0.1:65535 {
transport fastcgi
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi"
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,38 @@
:8884
reverse_proxy h2c://localhost:8080
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http",
"versions": [
"h2c",
"2"
]
},
"upstreams": [
{
"dial": "localhost:8080"
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,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"
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,119 @@
https://example.com {
reverse_proxy /path http://localhost:54321 {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Port {server_port}
header_up X-Forwarded-Proto "http"
buffer_requests
transport http {
read_buffer 10MB
write_buffer 20MB
max_response_header 30MB
dial_timeout 3s
dial_fallback_delay 5s
response_header_timeout 8s
expect_continue_timeout 9s
versions h2c 2
compression off
max_conns_per_host 5
max_idle_conns_per_host 2
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"buffer_requests": true,
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"Host": [
"{http.request.host}"
],
"X-Forwarded-For": [
"{http.request.remote}"
],
"X-Forwarded-Port": [
"{server_port}"
],
"X-Forwarded-Proto": [
"http"
],
"X-Real-Ip": [
"{http.request.remote}"
]
}
}
},
"transport": {
"compression": false,
"dial_fallback_delay": 5000000000,
"dial_timeout": 3000000000,
"expect_continue_timeout": 9000000000,
"max_conns_per_host": 5,
"max_idle_conns_per_host": 2,
"max_response_header_size": 30000000,
"protocol": "http",
"read_buffer_size": 10000000,
"response_header_timeout": 8000000000,
"versions": [
"h2c",
"2"
],
"write_buffer_size": 20000000
},
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
],
"match": [
{
"path": [
"/path"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,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"
]
}
}
}
}
@@ -0,0 +1,51 @@
:80
respond 200
@untrusted not remote_ip 10.1.1.0/24
respond @untrusted 401
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"not": [
{
"remote_ip": {
"ranges": [
"10.1.1.0/24"
]
}
}
]
}
],
"handle": [
{
"handler": "static_response",
"status_code": 401
}
]
},
{
"handle": [
{
"handler": "static_response",
"status_code": 200
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,86 @@
{
local_certs
}
*.tld, *.*.tld {
tls {
on_demand
}
}
foo.tld, www.foo.tld {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"foo.tld",
"www.foo.tld"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"*.tld",
"*.*.tld"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"foo.tld",
"www.foo.tld"
],
"issuers": [
{
"module": "internal"
}
]
},
{
"subjects": [
"*.*.tld",
"*.tld"
],
"issuers": [
{
"module": "internal"
}
],
"on_demand": true
},
{
"issuers": [
{
"module": "internal"
}
]
}
]
}
}
}
}
@@ -0,0 +1,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,155 @@
{
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"
}
]
},
{
"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
}
]
}
}
}
}
@@ -3,7 +3,7 @@ localhost
respond "hello from localhost"
tls {
client_auth {
mode request
mode request
trusted_ca_cert_file ../caddy.ca.cer
}
}
@@ -3,8 +3,8 @@ localhost
respond "hello from localhost"
tls {
client_auth {
mode request
trusted_ca_cert MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
mode request
trusted_ca_cert MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
}
}
----------
@@ -0,0 +1,132 @@
# https://github.com/caddyserver/caddy/issues/3906
a.a {
tls internal
respond 403
}
http://b.b https://b.b:8443 {
tls internal
respond 404
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.a"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"status_code": 403
}
]
}
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"b.b"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"status_code": 404
}
]
}
]
}
],
"terminal": true
}
]
},
"srv2": {
"listen": [
":8443"
],
"routes": [
{
"match": [
{
"host": [
"b.b"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "static_response",
"status_code": 404
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"a.a",
"b.b"
],
"issuers": [
{
"module": "internal"
}
]
}
]
}
}
}
}
+10 -2
View File
@@ -1,7 +1,10 @@
package integration
import (
jsonMod "encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"regexp"
"strings"
"testing"
@@ -32,14 +35,19 @@ func TestCaddyfileAdaptToJSON(t *testing.T) {
}
// split the Caddyfile (first) and JSON (second) parts
// (append newline to Caddyfile to match formatter expectations)
parts := strings.Split(string(data), "----------")
caddyfile, json := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
caddyfile, json := strings.TrimSpace(parts[0])+"\n", strings.TrimSpace(parts[1])
// replace windows newlines in the json with unix newlines
json = winNewlines.ReplaceAllString(json, "\n")
// replace os-specific default path for file_server's hide field
replacePath, _ := jsonMod.Marshal(fmt.Sprint(".", string(filepath.Separator), "Caddyfile"))
json = strings.ReplaceAll(json, `"./Caddyfile"`, string(replacePath))
// run the test
ok := caddytest.CompareAdapt(t, caddyfile, "caddyfile", json)
ok := caddytest.CompareAdapt(t, filename, caddyfile, "caddyfile", json)
if !ok {
t.Errorf("failed to adapt %s", filename)
}
+28
View File
@@ -0,0 +1,28 @@
package integration
import (
"net/http"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestBrowse(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
}
http://localhost:9080 {
file_server browse
}
`, "caddyfile")
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
if err != nil {
t.Fail()
return
}
tester.AssertResponseCode(req, 200)
}
+136
View File
@@ -0,0 +1,136 @@
package integration
import (
"bytes"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestMap(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
http_port 9080
https_port 9443
}
localhost:9080 {
map {http.request.method} {dest-1} {dest-2} {
default unknown1 unknown2
~G(.)(.) G${1}${2}-called
POST post-called foobar
}
respond /version 200 {
body "hello from localhost {dest-1} {dest-2}"
}
}
`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost GET-called unknown2")
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called foobar")
}
func TestMapRespondWithDefault(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
http_port 9080
https_port 9443
}
localhost:9080 {
map {http.request.method} {dest-name} {
default unknown
GET get-called
}
respond /version 200 {
body "hello from localhost {dest-name}"
}
}
`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called")
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost unknown")
}
func TestMapAsJson(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9080"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "map",
"source": "{http.request.method}",
"destinations": ["dest-name"],
"defaults": ["unknown"],
"mappings": [
{
"input": "GET",
"outputs": ["get-called"]
},
{
"input": "POST",
"outputs": ["post-called"]
}
]
}
]
},
{
"handle": [
{
"body": "hello from localhost {dest-name}",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": ["/version"]
}
]
}
]
}
],
"match": [
{
"host": ["localhost"]
}
],
"terminal": true
}
]
}
}
}
}
}`, "json")
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called")
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called")
}

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