Compare commits

..

392 Commits

Author SHA1 Message Date
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
Matthew Holt c9049bdc24 go.mod: Minor dependency updates 2020-06-26 12:19:51 -06:00
Matt Holt 21c00a3cd2 caddyhttp: Better host matching for logger names (fix #3488) (#3522)
First try an exact lookup like before, but if it fails, strip the port
and try again. example.com:1234 should still use a logger keyed for
example.com if there is no key example.com:1234.
2020-06-26 12:01:50 -06:00
Francis Lavoie 61b7002d26 ci: Apparently only single-quote strings are supported (#3523)
https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#literals

https://github.com/caddyserver/caddy/actions/runs/147953515
2020-06-25 21:58:44 +00:00
Francis Lavoie b1480eb52f fastcgi: Fix php_fastcgi matcher regression (#3512) 2020-06-22 11:45:18 -06:00
Xiuming Chen 5bc4777be9 chore: Fix typo in reverse-proxy subcommand help message (#3513) 2020-06-22 00:40:54 -04:00
Matthew Holt 3af15c0725 caddyhttp: Empty, not nil, query matcher matches empty query string 2020-06-16 12:02:23 -06:00
Matthew Holt 6db3615547 caddyhttp: Enable matching empty query string
Caddyfile syntax: query ""

Or a nil matcher in the JSON should also match an empty query string.

See https://caddy.community/t/v2-match-empty-query/8708?u=matt
2020-06-16 10:41:37 -06:00
Matthew Holt 32cafbb630 httpcaddyfile: Fix ordering of catch-all site blocks
Catch-alls should always go last. Normally this is the case, but we have
a special case for comparing one wildcard-host site block to another
non-wildcard host site block; and a catch-all site block is also a
non-wildcard host site block, so now we have to special-case the
catch-all site block. Sigh.

This could be reproduced with a Caddyfile that has two site blocks:
":80" and "*.example.com", in that order.
2020-06-16 10:02:06 -06:00
Francis Lavoie 003403ecbc templates: Add support for dots to close yaml frontmatter (#3498)
* templates: Add support for dots to close yaml frontmatter

* templates: Fix regression in body output
2020-06-15 12:38:51 -06:00
Mohammed Al Sahaf 5b48f784ae ci: don't run s390x tests on PRs of forks (#3494)
* ci: don't run s390x tests on PRs of forks

* ci: check if fork by matchinging name from event against name of repo
2020-06-12 19:51:04 +00:00
Chris Ortman d84a5d8427 httpcaddyfile: New acme_eab option (#3492)
* Adds global options for external account bindings

* Maybe other people use ctags too?

* Use nested block to configure external account

* go format files

* Restore acme_ca directive in test file

* Change Caddyfile config syntax for acme_eab

* Update test

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-06-12 13:37:56 -06:00
Mohammed Al Sahaf 7da32f493a ci: skip s390x tests on forks (#3493) 2020-06-12 18:03:29 +00:00
Matthew Holt cb0d9838cb go.mod: Update quic-go to 0.17.1 (draft 29) and certmagic 0.11.2 (eab) 2020-06-12 11:52:12 -06:00
Matthew Holt d81a69ef16 Merge branch 'eab-fix' 2020-06-12 11:49:45 -06:00
Mohammed Al Sahaf 99dcc10f31 ci: add CI on s390x (#3463)
* ci: lay out foundation for s390x tests

* ci: uncomment the s390x test script & replace placeholders with real values

* ci: amend the s390x test job name to be more consistent with others
2020-06-12 17:11:46 +00:00
Wynn Wolf Arbor fa4cdde7d8 fastcgi: Make sure splitPos handles empty SplitPath correctly (#3491)
In commit f2ce81c, support for multiple path splitters was added. The
type of SplitPath changed from string to []string, and splitPos was
changed to loop through all values in SplitPath.

Before that commit, if SplitPath was empty, strings.Index returned 0 and
PATH_INFO was set correctly in buildEnv.

Currently, however, splitPos returns -1 for empty values of SplitPath,
behaving as if a split position could not be found at all. PATH_INFO is
then never set in buildEnv and remains empty.

Restore the old behaviour by explicitly checking whether SplitPath is
empty and returning 0 in splitPos.

Closes #3490
2020-06-12 10:07:59 -06:00
Matthew Holt d55c3b31eb caddyhttp: Add client cert SAN placeholders 2020-06-11 16:19:07 -06:00
Matthew Holt 6d03fb48f9 caddytls: Don't decode HMAC
https://caddy.community/t/trouble-with-external-account-hmac/8600?u=matt
2020-06-11 15:33:27 -06:00
Matthew Holt b3bff13f7d reverseproxy: Close websocket conn if req context cancels
This is a recent patch in the Go standard library
2020-06-11 15:25:26 -06:00
Francis Lavoie 7211101c52 ci: Fix gemfury upload condition, move triggers to publish event (#3483) 2020-06-08 12:21:20 -06:00
Mohammed Al Sahaf 90dba172cb ci: fix an oopsie in the release script (#3482) 2020-06-08 11:10:28 -06:00
Matthew Holt 4b10ae5ce6 reverseproxy: Add Caddyfile support for ClientCertificateAutomate 2020-06-08 10:30:26 -06:00
NWHirschfeld 1dfb11486e httpcaddyfile: Add client_auth options to tls directive (#3335)
* reading client certificate config from Caddyfile

Signed-off-by: NWHirschfeld <Niclas@NWHirschfeld.de>

* Update caddyconfig/httpcaddyfile/builtins.go

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

* added adapt test for parsing client certificate configuration from Caddyfile

Signed-off-by: NWHirschfeld <Niclas@NWHirschfeld.de>

* read client ca and leaf certificates from file https://github.com/caddyserver/caddy/pull/3335#discussion_r421633844

Signed-off-by: NWHirschfeld <Niclas@NWHirschfeld.de>

* Update modules/caddytls/connpolicy.go

* Make review adjustments

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-06-05 12:19:36 -06:00
Matthew Holt 11a132d48b caddytls: Configurable cache size limit 2020-06-05 11:14:39 -06:00
Matthew Holt 9dafa63933 go.mod: Update dependencies 2020-06-05 11:14:09 -06:00
Francis Lavoie 21c1da101c ci: Disable publishing .deb on beta tags (#3473) 2020-06-05 10:23:15 -06:00
Matthew Holt 7a99835dab reverseproxy: Enable changing only the status code (close #2920) 2020-06-04 12:06:38 -06:00
Matthew Holt 7b0962ba4d caddyhttp: Default to error status if found in context
This is just a convenience if using a static_response handler in an
error route, by setting the default status code to the same one as
the error status.
2020-06-04 10:32:01 -06:00
Matthew Holt 2d1f7b9da8 caddyhttp: Auto-redirects from all bind addresses (fix #3443) 2020-06-03 10:56:26 -06:00
Matthew Holt a285fe4129 caddypki: Add 'acme_server' Caddyfile directive 2020-06-03 09:59:36 -06:00
Matthew Holt 97e61c16a3 httpcaddyfile: Sort site blocks with wildcards last (fix #3410) 2020-06-03 09:35:13 -06:00
Matthew Holt 83551edf3e cmd: Only stop admin server on signal if it exists (fix #3470) 2020-06-03 07:31:31 -06:00
Matthew Holt e18c373064 caddytls: Actually use configured test CA 2020-06-02 11:13:44 -06:00
Matt Holt 9a7756c6e4 caddyauth: Cache basicauth results (fixes #3462) (#3465)
Cache capacity is currently hard-coded at 1000 with random eviction.
It is enabled by default from Caddyfile configurations because I assume
this is the most common preference.
2020-06-01 23:56:47 -06:00
Francis Lavoie fdf2a77feb caddyfile: Add args on imports (#3423)
* caddyfile: Add support for args on imports

* caddyfile: Add more import args tests
2020-06-01 10:43:06 -06:00
Georges Haidar a496308f6e httpcaddyfile: Let modules add listener wrappers (#3397)
* httpcaddyfile: allow modules to customize listener wrappers

* Update caddyconfig/httpcaddyfile/httptype.go

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>

* Update caddyconfig/httpcaddyfile/httptype.go

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>

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-06-01 09:50:00 -06:00
Matthew Holt d5d7fb5954 go.mod: Update dependencies 2020-06-01 09:31:08 -06:00
Matt Holt 996af0915d cmd: Support admin endpoint on unix socket (#3320) 2020-05-29 14:21:55 -06:00
Matthew Holt 6c051cd27d caddyconfig: Minor internal and godoc tweaks 2020-05-29 11:49:25 -06:00
Matt Holt 9415feca7c logging: Net writer redials if write fails (#3453)
* logging: Net writer redials if write fails

https://caddy.community/t/v2-log-output-net-does-not-reconnect-after-lost-connection/8386?u=matt

* Only replace connection if redial succeeds

* Fix error handling
2020-05-28 10:40:14 -06:00
Matthew Holt 881b826fb5 reverseproxy: Pool copy buffers (minor optimization) 2020-05-27 11:42:19 -06:00
Matthew Holt 538ddb8587 reverseproxy: Enable response interception (#1447, #2920)
It's a raw, low-level implementation for now, but it's very flexible.
More sugar-coating can be added after error handling is more developed.
2020-05-27 10:17:45 -06:00
Francis Lavoie 69b5643130 chore: Fix typo in dispenser.go (#3456) 2020-05-27 08:13:57 -06:00
Matthew Holt e5bbed1046 caddyhttp: Refactor header matching
This allows response matchers to benefit from the same matching logic
as the request header matchers (mainly prefix/suffix wildcards).
2020-05-26 17:35:27 -06:00
Matthew Holt 294910c68c caddyhttp: Add client.public_key(_sha256) placeholders 2020-05-26 15:52:53 -06:00
Francis Lavoie 8c5d00b2bc httpcaddyfile: New handle_path directive (#3281)
* caddyconfig: WIP implementation of handle_path

* caddyconfig: Complete the implementation - h.NewRoute was key

* caddyconfig: Add handle_path integration test

* caddyhttp: Use the path matcher as-is, strip the trailing *, update test
2020-05-26 15:27:51 -06:00
Rui Lopes aa20878887 cmd: file-server: add --access-log flag (#3454) 2020-05-26 15:04:04 -06:00
Francis Lavoie c1e5c09294 reverseproxy: Improve error message when using scheme+placeholder (#3393)
* reverseproxy: Improve error message when using scheme+placeholder

* reverseproxy: Simplify error message

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

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-05-26 14:13:15 -06:00
Francis Lavoie ffc125d6f5 caddyfile: Move NewTestDispenser into non-test file (#3439) 2020-05-26 13:45:22 -06:00
AndyBan 22055c5e0f reverseproxy: Fix https active health checks #3450 (#3451) 2020-05-26 12:40:57 -06:00
Mohammed Al Sahaf dfe802aed3 chore: forego the use of deprecated cel func NewIdent in favor of NewVar (#3444) 2020-05-25 03:59:38 +00:00
Mohammed Al Sahaf 7a365af5df chore: simplify goreleaser flow, add bash completions to .deb (#3436) 2020-05-22 15:13:31 -04:00
Matthew Holt 0cbf467b3f caddyhttp: Add time.now placeholder and update cel-go (closes #2594) 2020-05-21 18:19:01 -06:00
Francis Lavoie bb67e19d7b cmd: hash-password: Fix broken terminal state on SIGINT (#3416)
* caddyauth: Fix hash-password broken terminal state on SIGINT

* caddycmd: Move TrapSignals calls to only subcommands that run long
2020-05-21 13:09:49 -06:00
Matthew Holt 1dc4ec2d77 admin: Disallow websockets
No currently-known exploit here, just being conservative
2020-05-21 12:29:19 -06:00
Matt Holt 452d4726f7 Update SECURITY.md 2020-05-20 14:24:47 -06:00
Matthew Holt 2a8a198568 reverseproxy: Don't overwrite existing X-Forwarded-Proto header
Correct behavior is not well defined because this is a non-standard
header field. This could be a "hop-by-hop" field much like
X-Forwarded-For is, but even our X-Forwarded-For implementation
preserves prior entries. Or, it could be best to preserve the original
value from the first hop, representing the protocol as facing the
client.

Let's try it the other way for a bit and see how it goes.

See https://caddy.community/t/caddy2-w-wordpress-behind-nginx-reverse-proxy/8174/3?u=matt
2020-05-20 11:33:17 -06:00
Francis Lavoie cc8fb488d3 httpcaddyfile: Improve error on matcher declared outside site block (#3431) 2020-05-20 10:37:48 -06:00
Francis Lavoie fae064262d httpcaddyfile: Add auto_https global option (#3284) 2020-05-19 16:59:51 -06:00
Matthew Holt 9ee01dceac reverseproxy: Make debug log safe if error occurs 2020-05-18 14:08:11 -06:00
Matthew Holt 812278acd8 reverseproxy: Emit debug log before checking error (#3425)
This way the upstream request will always be available even if it failed
2020-05-18 13:50:46 -06:00
Matthew Holt c47ddbeffb pki: Add docs to some struct fields 2020-05-18 13:50:46 -06:00
Thorkild Gregersen 483e31b978 templates: trim windows whitespace in SplitFrontMatter; fix #3386 (#3387)
* add test case for SplitFrontMatter showing issue with windows newline

* fix issue with windows newline when using SplitFrontMatter

* Update modules/caddyhttp/templates/frontmatter.go

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

* make it mere explicit what is trimmed from firstLine

* Update modules/caddyhttp/templates/frontmatter.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-05-18 13:01:04 -06:00
Francis Lavoie 41a682ddde caddyauth: Add realm to basicauth Caddyfile directive (#3315) 2020-05-18 12:19:28 -06:00
Francis Lavoie 7243454a96 fastcgi: php_fastcgi subdirectives to override shortcut behaviour (#3255)
* fastcgi: Add new php_fastcgi subdirectives to override the shortcut

* fastcgi: Support "index off" to disable redir and try_files

* fastcgi: Remove whitespace to satisfy linter

* fastcgi: Run gofmt

* fastcgi: Make a new dispenser instead of using rewind

* fastcgi: Some fmt

* fastcgi: Add a couple adapt tests

* fastcgi: Clean up for loops

* fastcgi: Move adapt tests to separate files
2020-05-18 12:15:38 -06:00
Matthew Holt 3fb2c394d1 go.mod: Update dependencies
Notably, this adds Caddyfile syntax highlighting in markdown rendering
2020-05-17 17:12:34 -06:00
Francis Lavoie 21de227fe9 httpcaddyfile: Be stricter about log syntax (#3419) 2020-05-15 15:57:16 -06:00
elcore 62c9f2cf3e cmd: Add --envfile flag to run command (#3278)
* run: Add the possibility to load an env file

* run: change envfile flag var

* run: do not ignore err values

* 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-05-15 15:49:51 -06:00
Francis Lavoie bde3823b76 caddytest: Refactor Caddyfile adapt tests to separate files (#3398) 2020-05-14 17:53:28 -04:00
Matthew Holt 4df56c77e3 cmd: Add pidfile support (closes #3235) 2020-05-13 11:28:15 -06:00
Mohammed Al Sahaf cee5589b98 docs: link to CEL standard definitions (#3407)
* docs: link to CEL standard definitions

* Rephrase the anchor to CEL standard definitions

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

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-05-13 17:11:31 +00:00
Matt Holt 90c7b4b0a1 reverseproxy: Apply response header ops before copying it (fix #3382) (#3401) 2020-05-13 09:52:20 -06:00
Matthew Holt aef560c7fc all: Recover from panics in goroutines 2020-05-12 11:36:20 -06:00
linquize 44536a7594 cmd: reverse-proxy: add --insecure flag (with warning) (#3389) 2020-05-12 10:43:18 -06:00
Francis Lavoie ea7e4b4024 httpcaddyfile: Shorthands for parameterized placeholders (#3305)
* httpcaddyfile: Add shorthands for parameterized placeholders


httpcaddyfile: Now with regexp instead


httpcaddyfile: Allow dashes, gofmt


httpcaddyfile: Compile regexp only once


httpcaddyfile: Cleanup struct


httpcaddyfile: Optimize the replacers, pull out of the loop


httpcaddyfile: Add `{port}` shorthand

* httpcaddyfile: Switch `r.` to `re.`
2020-05-11 16:50:49 -06:00
Francis Lavoie ef6e53bb5f core: Add support for d duration unit (#3323)
* caddy: Add support for `d` duration unit

* Improvements to ParseDuration; add unit tests

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2020-05-11 16:41:11 -06:00
Francis Lavoie 35e1d92d58 ci: Delete .travis.yml (#3396)
Too flaky. We'll explore different avenues to testing s390x and ppc64le.

See discussion here: https://github.com/caddyserver/caddy/pull/3355

/cc @grooverdan, @Mohammed90 said he'll reach out to Elizabeth as you suggested.
2020-05-11 15:07:02 -06:00
Francis Lavoie dc9f4f13fc httpcaddyfile: Make global options pluggable (#3265)
* httpcaddyfile: Make global options pluggable

* httpcaddyfile: Add a global options adapt test

* httpcaddyfile: Wrap err

Co-Authored-By: Dave Henderson <dhenderson@gmail.com>

* httpcaddyfile: Revert wrap err

Co-authored-by: Dave Henderson <dhenderson@gmail.com>
2020-05-11 15:00:35 -06:00
Francis Lavoie 4c55d26f11 caddyhttp: Fix merging of Caddyfile matchers in not blocks (#3379) 2020-05-11 14:38:33 -06:00
Gregory Dosh d534162556 caddyhttp: Match hostnames with wildcards to loggers (#3378)
* adding wildcard matching of logger names

* reordering precedence for more specific loggers to match first

* removing dependence on certmagic and extra loop

Co-authored-by: GregoryDosh <GregoryDosh@users.noreply.github.com>
2020-05-11 14:17:59 -06:00
Andrew Zhou 5bde8d705b cmd: hash-password: Support reading from stdin (#3373)
Closes #3365 

* http: Add support in hash-password for reading from terminals/stdin

* FIXUP: Run gofmt -s

* FIXUP

* FIXUP: Apply suggestions from code review

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

* FIXUP

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-05-11 14:10:47 -06:00
Matthew Holt 7960b4259d caddyhttp: Minor refactoring for preparing requests
While building a layer4 app for Caddy, I discovered that we need the
ability to fill a request's context just like the HTTP server does,
hence this exported function PrepareRequest().
2020-05-11 12:14:47 -06:00
Mark Sargent 2c91688f39 fix testharness, dumps the current config, only if the config was successfully loaded (#3385) 2020-05-10 08:11:35 +12:00
Chandler Swift 513e0240fd docs: Fix TOC/section header mismatch (#3380) 2020-05-08 19:46:40 -04:00
Jeremy Lin bf8c3c25c1 log: improve rounding logic for log rolling directives (#3367)
* For `roll_size` and `roll_keep_for` directives, round up instead of down.
  For example, if a user wants to be able to look back on 36 hours of logs,
  but you must round to a 24-hour multiple, then it's better to round up to
  48 hours (which includes the desired 36 hours) instead of down to 24 hours.

* `roll_size` had an off-by-one error that caused the size to be as much as
  1 MB larger than requested. For example, requests of `1MB` and `1.1MB`
  both became 2 MB. Now `1MB` means 1 MB, and `1.1MB` is rounded up to 2 MB.
2020-05-07 13:06:00 -06:00
Matthew Holt c8da8ca673 Update readme 2020-05-07 13:01:33 -06:00
Jose Donizetti 43fba378d6 docs: Fix command.Func documentation (#3371) 2020-05-07 09:31:58 -06:00
Matthew Holt cd9317e5df httpcaddyfile: Fix route ordering bug
https://caddy.community/t/cant-get-simple-alias-to-work/7911/8?u=matt

This removes an optimization where we amortized path matcher decoding.
The decoded matchers were index by... position... which obviously
changes during sorting. Duh.

Anyway, sorting is sliiightly slower now but the Caddyfile is not
really CPU-sensitive, so this is fine.
2020-05-06 19:41:37 -06:00
Matthew Holt 8dbc5f70a5 Update dependencies and get rid of placeholder hacks in CA code
With the latest commit on smallstep/certificates, placeholders in config
are no longer needed.
2020-05-06 16:02:21 -06:00
Francis Lavoie 07c6076ea0 ci: Add release tagged event triggers to sister repos (#3321) 2020-05-06 16:42:55 -04:00
Matthew Holt 28ab0bfb13 core: Support loading modules from [][]json.RawMessage fields 2020-05-06 13:18:56 -06:00
Matthew Holt 1c17e6c6bb reverseproxy: Allow using TLS for port 80 upstreams (see #3361)
An upstream like https://localhost:80 is still forbidden, but an addr of
localhost:80 can be used while explicitly enabling TLS as an override;
we just don't allow the implicit behavior to be ambiguous.
2020-05-06 12:37:44 -06:00
Karol Będkowski b814c0af9c tls/client auth: verify first certificates in client request (#3344)
When client certificate is enabled Caddy check only last certificate from
request. When this cert is not in list of trusted leaf certificates,
connection is rejected. According to RFC TLS1.x the sender's certificate
must come first in the list.  Each following certificate must directly
certify the one preceding it.

This patch fix this problem - first certificate is checked instead of last.
2020-05-06 10:07:13 -06:00
Dave Henderson 9e5d9e2530 ci: Add linux-armv5 builds (#3356)
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
2020-05-05 19:13:56 -06:00
Dave Henderson 9408dacc27 Fixing goreleaser syntax error (#3355)
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
2020-05-06 00:50:30 +00:00
Daniel Black 12cfc19487 ci: add s390x and ppc64le builds for linux (#3325) 2020-05-05 12:41:08 -06:00
Francis Lavoie afecd90a6c reverseproxy: Add tls_server_name option to Caddyfile (#3322) 2020-05-05 12:39:39 -06:00
Matt Holt 2f59467ac3 httpcaddyfile: Only append TLS conn policy if it's non-empty (#3319)
This can lead to nicer, smaller JSON output for Caddyfiles like this:

	a {
		tls internal
	}
	b {
		tls foo@bar.com
	}

i.e. where the tls directive only configures automation policies, and
is merely meant to enable TLS on a server block (if it wasn't implied).
This helps keeps implicit config implicit.

Needs a little more testing to ensure it doesn't break anything
important.
2020-05-05 12:37:52 -06:00
Matt Holt 184e8e9f71 pki: Embedded ACME server (#3198)
* pki: Initial commit of embedded ACME server (#3021)

* reverseproxy: Support auto-managed TLS client certificates (#3021)

* A little cleanup after today's review session
2020-05-05 12:35:32 -06:00
Matt Holt 1e8c9764df file_server: Accept files args in one-liner of Caddyfile matcher (#3298)
Previously, matching by trying files other than the actual path of the
URI was:

    file {
        try_files <files...>
    }

Now, the same can be done in one line:

    file <files...>

As before, an empty file matcher:

    file

still matches if the request URI exists as a file in the site root.
2020-05-05 12:34:58 -06:00
Matt Holt 41c7bd27b4 httpserver: Add experimental H2C support (#3289)
* reverse_proxy: Initial attempt at H2C transport/client support (#3218)

I have not tested this yet

* Experimentally enabling H2C server support (closes #3227)

See also #3218

I have not tested this

* reverseproxy: Clean up H2C transport a bit

* caddyhttp: Update godoc for h2c server; clarify experimental status

* caddyhttp: Fix trailers when recording responses (fixes #3236)

* caddyhttp: Tweak h2c config settings and docs
2020-05-05 12:33:21 -06:00
Francis Lavoie 96d6d277a4 caddyconfig: Don't start comments in middle of tokens (#3267)
* caddyconfig: Only parse # as start of comment if preceded by space

* caddyconfig: Simplify # logic using len(val), add a test
2020-05-05 12:32:12 -06:00
Francis Lavoie 26e559662d httpcaddyfile: Support single-line matchers (#3263)
* httpcaddyfile: Support single-line matchers

* httpcaddyfile: Add single-line matcher test

* httpcaddyfile: Add a matcher syntax adapt test
2020-05-05 12:29:21 -06:00
Matt Holt 52305618df caddyfile: Support backticks as quotes (closes #2591) (#3242) 2020-05-05 12:27:49 -06:00
Mohammed Al Sahaf e051e119d1 ci: add tests on s390x and ppc64le (#3328)
* ci: add tests on s390x and ppc64le

* ci: use Travis as CI for ppc64le and s390x

* ci: cache Go builds on Travis

* ci: avoid Travis duplicate builds
2020-05-02 17:24:54 -06:00
Matthew Holt 8e42661060 caddytls: Finish upgrading to libdns DNS providers for ACME challenges
Until we finish the migration to the new acme library, we have to bring
the solver type in-house. It's small and temporary.
2020-05-02 17:23:36 -06:00
Matthew Holt 86a4f2c9f4 caddytls: Fix namespace tls.dns -> dns.providers
Coulda sworn I did this already but I think I messed up my git commands
2020-05-02 16:28:10 -06:00
Matthew Holt a507a5bbc7 reverseproxy: Remove circuitbreaker module (see #3331)
Moving to https://github.com/caddyserver/circuitbreaker

Nobody was using it anyway -- it works well, but something got fumbled
in a refactoring *months* ago. Turns out that we forgot the interface
guards AND botched a method name (my bad) - Ok() should have been OK().
So it would always have thrown a runtime panic if it tried to be loaded.
The module itself works well, but obviously nobody used it because
nobody reported the error. Fixing this while we move it to the new repo.

Removing this removes the last Bazaar/Launchpad dependency (I think).
2020-05-01 19:47:46 -06:00
Mark Sargent d0770dbbb3 expose caddytest timeouts (#3329) 2020-05-02 10:24:35 +12:00
Matthew Holt a77bd1d887 httpcaddyfile: Update tls parsing for DNS providers 2020-05-01 10:41:08 -06:00
Matthew Holt bca610fbde httpcaddyfile: Minor fixes to parsing storage options 2020-05-01 09:34:32 -06:00
Matthew Holt 1fa8c185a8 go.mod: Remove DNSProviderMaker interface; update to lego 3.6 2020-04-30 18:17:39 -06:00
Matthew Holt a1796c2f14 caddytls: Adjust DNS challenge structure; clarify some docs 2020-04-30 16:15:20 -06:00
Matthew Holt f931c26f68 caddyhttp: Better duration logging
Also un-nest all the error handling, that was unnecessary indentation
2020-04-28 15:38:45 -06:00
Matt Holt 10db57027d caddyhttp: General improvements to access logging (#3301)
* httpcaddyfile: Exclude access logs written to files from default log

Even though any logs can just be ignored, most users don't seem to like
configuring an access log to go to a file only to have it doubly appear
in the default log.

Related to:
- #3294
- https://caddy.community/t/v2-logging-format/7642/4?u=matt
- https://caddy.community/t/caddyfile-questions/7651/3?u=matt

* caddyhttp: General improvements to access log controls (fixes #3310)

* caddyhttp: Move log config nil check higher

* Rename LoggerName -> DefaultLoggerName
2020-04-28 08:32:04 -06:00
Matthew Holt c11d0e47a3 cmd: Clean up, simplify reverse proxy command; fix some edge cases
Now we take advantage of the address parsing capabilities of the HTTP
caddyfile.
2020-04-27 15:53:38 -06:00
Matthew Holt 9770ce7c9f Minor comment fix 2020-04-27 14:49:27 -06:00
Francis Lavoie 5ae1a5617c caddyhttp: Add split_path to file matcher (used by php_fastcgi) (#3302)
* matcher: Add `split_path` option to file matcher; used in php_fastcgi

* matcher: Skip try_files split if not the final part of the filename

* matcher: Add MatchFile tests

* matcher: Clarify SplitPath godoc
2020-04-27 14:46:46 -06:00
Matthew Holt 83c85c53f5 caddyhttp: Fix listener overlap detection on Linux
Sigh, apparently Linux is incapable of distinguishing host interfaces
in socket addresses, even though it works fine on Mac. I suppose we just
have to assume that any listeners with the same port are the same
address, completely ignoring the host interface on Linux... oh well.
2020-04-26 22:28:49 -06:00
Francis Lavoie 768383a610 ci: Enable GoReleaser .deb support (#3309)
* ci: Enable GoReleaser .deb support

* ci: Test .deb build

* ci: Fix typo

* ci: Turn off snapshot (breaks due to go mod edit)

* ci: Force the tag to rc3 for now

* ci: Let's try to publish the .debs

* ci: Attempt to enable build cache, rebuild after fixed line endings

* ci: Fix yml dupe ID issue, add caddy-api.service

* ci: Split cache keys between files so they're separate

* ci: Fix bindir

* ci: Update the script files

* ci: Retrigger

* ci: Push to gemfury

* ci: Use loop, fix bad env var

* ci: Retrigger

* ci: Try to force blank password?

* ci: Check if the token is actually present

* ci: Cleanup, remove debugging stuff

* ci: Remove useless comment
2020-04-26 20:20:14 -06:00
Mark Sargent 570d84f7d3 refactored caddytest helpers (#3285)
* refactored caddytest helpers
* added cookie jar support. Added support for more http verbs
2020-04-27 13:23:46 +12:00
Christoffer Andersson a6761153cb Fix misspelling in onDemandAskRequest error (#3308) 2020-04-25 10:34:56 -06:00
Matthew Holt 02845bc9fd docs: Improve template documentation slightly; use const, not literal 2020-04-24 21:05:09 -06:00
Matthew Holt 97ed9e111d httpcaddyfile: Add nil check to prevent panic, fix validation logic
Panic would happen if an automation policy was specified in a singular
server block that had no hostnames in its address. Definitely an edge
case.

Fixed a bug related to checking for server blocks with a host-less key
that tried to make an automation policy. Previously if you had only two
server blocks like ":443" and another one at ":80", the one at ":443"
could not create a TLS automation policy because it thought it would
interfere with TLS automation for the block at ":80", but obviously that
key doesn't enable TLS because it is on the HTTP port. So now we are a
little smarter and count only non-HTTP-empty-hostname keys.

Also fixed a bug so that a key like "https://:1234" is sure to have TLS
enabled by giving it a TLS connection policy. (Relaxed conditions
slightly; the previous conditions were too strict, requiring there to be
a TLS conn policy already or a default SNI to be non-empty.)

Also clarified a comment thanks to feedback from @Mohammed90
2020-04-24 20:57:51 -06:00
Matthew Holt 100d19e3af dangit, of course I would bork my git commit 2020-04-24 17:48:33 -06:00
Matthew Holt ebf07f853b caddyhttp: Fix auto redirects for catch-all HTTPS sites
Prior logic was not setting up redirects for the case when domain names
are not known, but the server still clearly has TLS enabled.
2020-04-24 17:36:52 -06:00
Matthew Holt 1b061815b2 reverseproxy: Don't forget to provision embedded headers handler
https://caddy.community/t/set-cookie-manipulation-in-reverse-proxy/7666?u=matt
2020-04-22 19:57:06 -06:00
Matthew Holt 026937fab5 caddyhttp: Fix trailers when recording responses (fixes #3236) 2020-04-22 11:10:13 -06:00
Matthew Holt 295604d6df httpcaddyfile: Why was this code repeated?? 2020-04-22 09:20:39 -06:00
Francis Lavoie bacf50a59e caddyhttp: Fix common_log format's user ID placeholder (#3300) 2020-04-22 09:05:26 -06:00
westwin da8686c4b9 reverseproxy: always set req.URL.Host with upstream (#3297) 2020-04-21 20:34:00 -06:00
Matthew Holt e3a8f72f1c docs: Minor improvements 2020-04-21 19:30:04 -06:00
Mohammed Al Sahaf bae4f15fad ci: fuzz: remove the fuzzer of the Caddyfile parser (#3288) 2020-04-20 15:21:19 -06:00
Francis Lavoie 0798459e44 readme: Fix broken links (#3283)
Credit to @kanagawa41 for spotting these!

Fixes #3282
2020-04-19 17:37:03 -06:00
Matthew Holt f980170909 doc: Improve comment 2020-04-17 12:03:57 -06:00
Francis Lavoie 6963a72a63 ci: Cache the GOCACHE directory to speed up builds and tests (#3273)
* ci: Let's see if caching GOCACHE helps...

* ci: Use GOCACHE env instead (fixes windows), remove build -a

* ci: Hack to pull the GOCACHE env up to CI vars

* ci: Change cache key (mainly to wipe cache now)
2020-04-17 11:54:35 -06:00
Matt Holt 76bbb473a5 reverseproxy: Set X-Forwarded-Proto (closes #3275) (#3276) 2020-04-17 09:53:06 -06:00
Francis Lavoie 3c70950fa1 docs: Pull contributing document from v1 branch (#3270)
* docs: Pull contributing document from v1 branch

* Update .github/CONTRIBUTING.md

Co-Authored-By: Matt Holt <mholt@users.noreply.github.com>

* docs: [Responsible -> Coordinated] Disclosure

* docs: Link to the new security policy page

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-04-16 18:32:42 -06:00
Matthew Holt 7c171542ed Add security policy 2020-04-16 17:20:03 -06:00
Matthew Holt 9a572635f5 admin: Close admin endpoint when shutting down (fixes #3269) 2020-04-16 12:34:28 -06:00
Matthew Holt f5ccb904a3 admin: Disable host checking if wildcard interface is specified
To clarify, listening on wildcard interfaces is NOT the default and
should only be done under certain circumstances and when you know
what you're doing. Emits a warning in the log.

Fixes https://github.com/caddyserver/caddy-docker/issues/71
2020-04-16 11:41:32 -06:00
Matthew Holt 829e36d535 httpcaddyfile: Don't lowercase placeholder contents (fixes #3264) 2020-04-14 16:11:46 -06:00
Matthew Holt 2609a72893 go.mod: Update dependencies including CertMagic (fixes #3202) 2020-04-14 11:28:41 -06:00
Matthew Holt ec456811bb core: Don't return error on RegisterModule() and RegisterAdapter()
These functions are called at init-time, and their inputs are hard-coded
so there are no environmental or user factors that could make it fail
or succeed; the error return values are often ignored, and when they're
not, they are usually a fatal error anyway. To ensure that a programmer
mistake is not missed, we now panic instead.

Last breaking change 🤞
2020-04-13 09:48:54 -06:00
Matthew Holt 68cebb28d0 Fix some godocs 2020-04-11 09:01:40 -06:00
Matthew Holt a3bdc22234 admin: Always enforce Host header checks
With a simple heuristic for loopback addresses, we can enable this by
default without adding unnecessary inconvenience.
2020-04-10 17:31:38 -06:00
Matthew Holt d3383ced2a Update link in readme 2020-04-10 09:19:03 -06:00
Matthew Holt c024ae096d tests: Clean up redundant type declarations 2020-04-10 08:48:21 -06:00
Matthew Holt 3bee569a8a httpcaddyfile: Don't remove empty TLS conn policies (fix #3249)
Not sure why I thought that would be a good idea
2020-04-10 08:24:12 -06:00
Matthew Holt 999ab22b8c caddyhttp: Add nil check (fixes #3248 and fixes #3250) 2020-04-10 08:12:42 -06:00
Matthew Holt 9991fdc495 Update readme 2020-04-10 08:10:35 -06:00
Matthew Holt f29023bf8f reverseproxy: Minor tweaks
We'll need that context in v2.1 when the transport can manage its own
client certificates; see #3198
2020-04-09 13:22:05 -06:00
Matthew Holt 85f5f47f31 caddytls: Don't initialize default internal issuer unless necessary
Otherwise, a password prompt can occur unnecessarily.
2020-04-09 13:09:48 -06:00
Matthew Holt 6e4132eb89 logging: Colorize output in all cases of stdout/stderr 2020-04-09 13:06:06 -06:00
Matt Holt d89ad2fd5b caddytls: Fix for TLS conn policy being applied to HTTP-only servers (#3243)
* httpcaddyfile: Don't add TLS policy to HTTP-only server (#3193, #3223)

* Account for HTTP port

* Add integration test written by @sarge
2020-04-09 12:39:05 -06:00
Matthew Holt d33926b63f go.mod: Update certmagic 2020-04-09 12:32:57 -06:00
Matthew Holt c5f9227a48 go.mod: Try smallstep again
See if the broken dependency cycle has been... well, broken
2020-04-09 12:10:52 -06:00
Matthew Holt 88d391c1f5 go.mod: Update smallstep/cli 2020-04-09 11:16:47 -06:00
Matthew Holt b4a7d6267f go.mod: Update dependencies
Should fix the builds with GOPROXY=direct!
2020-04-09 10:57:23 -06:00
Matthew Holt e5dc76b054 caddyhttp: CEL matcher checks return type; slight refactor
As per https://github.com/caddyserver/caddy/issues/3051#issuecomment-611200414
2020-04-08 15:39:30 -06:00
Mohammed Al Sahaf 7dfd69cdc5 chore: make the linter happier (#3245)
* chore: make the linter happier

* chore: remove reference to maligned linter in .golangci.yml
2020-04-08 15:31:51 -06:00
Matthew Holt 28fdf64dc5 httpcaddyfile, caddytls: Multiple edge case fixes; add tests
- Create two default automation policies; if the TLS app is used in
  isolation with the 'automate' certificate loader, it will now use
  an internal issuer for internal-only names, and an ACME issuer for
  all other names by default.
- If the HTTP Caddyfile adds an 'automate' loader, it now also adds an
  automation policy for any names in that loader that do not qualify
  for public certificates so that they will be issued internally. (It
  might be nice if this wasn't necessary, but the alternative is to
  either make auto-HTTPS logic way more complex by scanning the names in
  the 'automate' loader, or to have an automation policy without an
  issuer switch between default issuer based on the name being issued
  a certificate - I think I like the latter option better, right now we
  do something kind of like that but at a level above each individual
  automation policies, we do that switch only when no automation
  policies match, rather than when a policy without an issuer does
  match.)
- Set the default LoggerName rather than a LoggerNames with an empty
  host value, which is now taken literally rather than as a catch-all.
- hostsFromKeys, the function that gets a list of hosts from server
  block keys, no longer returns an empty string in its resulting slice,
  ever.
2020-04-08 14:46:44 -06:00
Matthew Holt 0fe98038b6 caddyhttp: Fix logging name associations by adding a default 2020-04-08 14:39:20 -06:00
Matthew Holt 6e4c688ea7 logging: Only colorize console output 2020-04-08 14:37:37 -06:00
Francis Lavoie 5110643201 httpcaddyfile: Add key_type global option (#3231) 2020-04-08 11:09:38 -06:00
Matthew Holt 4d9b63d909 cel: Leverage DefaultAdapter to extend CEL's type system
Thanks to @TristonianJones for the tip!
https://github.com/caddyserver/caddy/commit/105acfa08664c97460a6fe3fb49635618be5bcb2#r38358983
2020-04-08 10:44:40 -06:00
Matthew Holt e30deedcc1 caddyhttp: Return port placeholders as ints 2020-04-08 10:44:40 -06:00
Matt Holt fbd9515d35 basicauth: Re-prompt after invalid credentials (fix #3239) (#3240) 2020-04-07 20:39:13 -06:00
Matthew Holt 95f6bd7e5c templates: Update docs 2020-04-07 12:29:09 -06:00
Matthew Holt b1ce9d4db7 templates: Add env function (closes #3237) 2020-04-07 12:26:08 -06:00
Matthew Holt 61679b74f5 Merge branch 'remove-ntlm' 2020-04-07 11:41:49 -06:00
Matthew Holt 2c1b663156 reverseproxy: Remove NTLM transport; refactor and improve docs 2020-04-07 11:39:14 -06:00
Matthew Holt 8b2dbc52ec core: Rename ParsedAddress -> NetworkAddress 2020-04-07 08:33:45 -06:00
Matthew Holt 657f0cab17 docs: Clarify "not" matcher structure (see #3233) 2020-04-06 18:44:12 -06:00
Francis Lavoie 7be747fbe9 caddyhttp: Add missing LB policy Caddyfile unmarshalers (#3230) 2020-04-06 13:08:42 -06:00
Francis Lavoie 5b355cbed0 caddyhttp: Strictly forbid unnecessary blocks on matchers (#3229) 2020-04-06 13:07:07 -06:00
Francis Lavoie a3cfe437b1 caddyhttp: Support single-line not matcher (#3228)
* caddyhttp: Support single-line not matcher shortcut

* caddyhttp: Some tests, I guess
2020-04-06 13:05:49 -06:00
Matthew Holt 437d5095a6 templates: Use text/template; add experimental notice to docs
Using html/template.HTML like we were doing before caused nested include
to be HTML-escaped, which breaks sites. Now we do not escape any of the
output; template input is usually trusted, and if it's not, users should
employ escaping actions within their templates to keep it safe. The docs
already said this.
2020-04-06 12:51:53 -06:00
Matthew Holt 145aebbba5 httpcaddyfile: Carry bind setting through to ACME issuer (fixes #3232) 2020-04-06 12:24:35 -06:00
Matthew Holt 6a32daa225 caddytls: Support custom bind host for challenges (#3232) 2020-04-06 11:22:06 -06:00
Matthew Holt 81cdebf648 tests: Remove noisy logs 2020-04-06 10:41:42 -06:00
Matthew Holt 84c729e96a ci: Tweak commit prefixes to ignore 2020-04-04 13:29:48 -06:00
Matthew Holt 346c33b4d5 cmd: Log warning if --resume and --config used together
There's nothing actually risky/dangerous in this situation, it's mostly
an attempt to get the user's attention
2020-04-04 13:29:48 -06:00
Mark Sargent 78717ce5b0 chore: add adapt tests. fix load failure not failing tests (#3222)
* add adaption tests. fix load failure not failing tests

* removed unnecessary assignment
2020-04-03 21:02:46 -06:00
Matthew Holt 3d6fc1e1b7 httpcaddyfile: Yield cleaner JSON when conn policy or log name is empty 2020-04-03 20:19:46 -06:00
Matthew Holt c7ac7de38a go.mod: Update CertMagic (again) v0.10.10 2020-04-03 17:46:43 -06:00
Matthew Holt 05164c895a go.mod: Use latest Certmagic (v0.10.9) 2020-04-03 16:16:22 -06:00
Matthew Holt 1e8af27329 fastcgi: Account for lack of split path configuration (fix #3221) 2020-04-03 10:25:25 -06:00
Matthew Holt b6482e53c1 go.mod: Update CertMagic to v0.10.8
Fixes occasional panic due to closing closed channel
2020-04-03 09:33:04 -06:00
Matt Holt 20f6795413 Create FUNDING.yml
I guess this got left in the v1 branch when we switched, oops
2020-04-03 09:07:14 -06:00
Matt Holt 84f16852ab ci: goreleaser: Drop some platforms and replacements (#3217)
Based on download stats, demand for 32-bit binaries these days is
extremely low. Also unify some of the filename conventions; just a
few bikeshedding changes :)
2020-04-02 18:07:57 -06:00
Matthew Holt 1456f15f9a readme: So much more ... what? Fixed cliffhanger 2020-04-02 16:46:52 -06:00
Mohammed Al Sahaf fdfe2ae53b chore: ci: fix release action script (#3216)
* chore: ci: fixing the step name that captures the pushed tag

* chrore: ci: exclude commits prefixed with `ci:` from changelog
2020-04-02 16:44:44 -06:00
Matthew Holt 1c190b001b httpcaddyfile: Refactor site key parsing; detect conflicting schemes
We now store the parsed site/server block keys with the server block,
rather than parsing the addresses every time we read them.

Also detect conflicting schemes, i.e. TLS and non-TLS cannot be served
from the same server (natively -- modules could be built for it).

Also do not add site subroutes (subroutes generated specifically from
site blocks in the Caddyfile) that are empty.
2020-04-02 14:24:53 -06:00
Mohammed Al Sahaf 3634c4593f ci: fuzz: skip fuzz data that contains import (#3214)
Thus far the fuzzers have found a few crashers in the Caddyfile parser. However, the fuzzer have been stuck at import glob expansion after import glob expansion, which aren't reproducible.
2020-04-02 10:40:21 -06:00
Matthew Holt 7ca15861dd caddytls: Encode big.Int as string with JSON 2020-04-02 09:43:33 -06:00
Matthew Holt 8ff330c555 Update readme 2020-04-02 09:43:08 -06:00
Matthew Holt 626f19a264 Fix for last commit 2020-04-01 21:07:38 -06:00
Matthew Holt 6ca5828221 caddytls: Refactor certificate selection policies (close #1575)
Certificate selection used to be a module, but this seems unnecessary,
especially since the built-in CustomSelectionPolicy allows quite complex
selection logic on a number of fields in certs. If we need to extend
that logic, we can, but I don't think there are SO many possibilities
that we need modules.

This update also allows certificate selection to choose between multiple
matching certs based on client compatibility and makes a number of other
improvements in the default cert selection logic, both here and in the
latest CertMagic.

The hardest part of this was the conn policy consolidation logic
(Caddyfile only, of course). We have to merge connection policies that
we can easily combine, because if two certs are manually loaded in a
Caddyfile site block, that produces two connection policies, and each
cert is tagged with a different tag, meaning only the first would ever
be selected. So given the same matchers, we can merge the two, but this
required improving the Tag selection logic to support multiple tags to
choose from, hence "tags" changed to "any_tag" or "all_tags" (but we
use any_tag in our Caddyfile logic).

Combining conn policies with conflicting settings is impossible, so
that should return an error if two policies with the exact same matchers
have non-empty settings that are not the same (the one exception being
any_tag which we can merge because the logic for them is to OR them).

It was a bit complicated. It seems to work in numerous tests I've
conducted, but we'll see how it pans out in the release candidates.
2020-04-01 20:49:35 -06:00
Matthew Holt 6fe04a30b1 caddyfile: Export NewTestDispenser() (close #2930)
This allows modules to test their UnmarshalCaddyfile methods.
2020-04-01 16:34:54 -06:00
Matthew Holt 19b45546a7 go.mod: Update smallstep/truststore
So that installation continues if Firefox is not installed

See https://github.com/smallstep/truststore/issues/3
2020-04-01 15:28:09 -06:00
Matthew Holt d322de6b42 gzip: Use klauspost/gzip, an optimized gzip implementation 2020-04-01 14:09:57 -06:00
Matthew Holt ce3ca541d8 caddytls: Update cipher suite names and curve names
Now using IANA-compliant names and Go 1.14's CipherSuites() function so
we don't have to maintain our own mapping of currently-secure cipher
suites.
2020-04-01 14:09:29 -06:00
Matthew Holt 581f1defcb caddyhttp: Print actual listener address in log message (closes #2992)
Needed if port is 0, thus chosen by OS
2020-04-01 12:23:07 -06:00
Matthew Holt 0d2a3511dc caddyhttp: Update host matcher docs about wildcards 2020-04-01 11:41:04 -06:00
Matt Holt 73643ea736 caddyhttp: 'not' matcher now accepts multiple matcher sets and OR's them (#3208)
See https://caddy.community/t/v2-matcher-or-in-not/7355/
2020-04-01 10:58:29 -06:00
Matthew Holt 809e72792c rewrite: Fix for rewrites with URI placeholders (#3209)
If a placeholder in the path component injects a query string such as
the {http.request.uri} placeholder is wont to do, we need to separate it
out from the path.
2020-04-01 00:43:40 -06:00
Matthew Holt 9fb0b1e838 caddytls: Add support for externalAccountBinding ACME extension 2020-03-31 21:08:02 -06:00
Matthew Holt 244b839f98 pki: Add trust subcommand to install root cert (closes #3204) 2020-03-31 17:56:36 -06:00
Matthew Holt 904d9cab39 httpcaddyfile: Include non-standard ports when mapping logger names
If a site block has a key like "http://localhost:2016", then the log for
that site must be mapped to "localhost:2016" and not just "localhost"
because "localhost:2016" will be the value of the Host header of requests.
But a key like "localhost:80" does not include the port since the Host
header will not include ":80" because it is a standard port.

Fixes https://caddy.community/t/v2-common-log-format-not-working/7352?u=matt
2020-03-30 18:39:21 -06:00
Matthew Holt ac65f690ae caddyhttp: Rename MatchNegate type to MatchNot type
This is more congruent with its module name. A change that affects only
code, not configurations.
2020-03-30 11:53:19 -06:00
Matthew Holt 37aa516a6e headers: Trim any trailing colon from field names as a courtesy 2020-03-30 11:52:11 -06:00
Matthew Holt 105acfa086 Keep type information with placeholders until replacements happen 2020-03-30 11:49:53 -06:00
Matthew Holt deba26d225 caddyfile: Minor fixes to the formatter 2020-03-29 13:53:00 -06:00
Matthew Holt 178ba024fe httpcaddyfile: Put root directive first, before redir and rewrite
See https://caddy.community/t/v2-match-any-path-but-files/7326/8?u=matt

If rewrites (or redirects, for that matter) match on file existence,
the file matcher would need to know the root of the site.

Making this change implies that root directives that depend on rewritten
URIs will not work as expected. However, I think this is very uncommon,
and am not sure I have ever seen that. Usually, dynamic roots are based
on host, not paths or query strings.

I suspect that rewrites based on file existence will be more common than
roots based on rewritten URIs, so I am moving root to be the first in
the list.

Users can always override this ordering with the 'order' global option.
2020-03-28 19:07:51 -06:00
Matthew Holt e207240f9a reverse_proxy: Upstream.String() method returns either LookupSRV or Dial
Either Dial or LookupSRV will be set, but if we rely on Dial always
being set, we could run into bugs.

Note: Health checks don't support SRV upstreams.
2020-03-27 14:29:01 -06:00
Robin Lambertz 397e04ebd9 caddyauth: Add Metadata field to caddyauth.User (#3174)
* caddyauth: Add Metadata field to caddyauth.User

* Apply gofmt

* Tidy it up a bit

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2020-03-27 11:10:51 -06:00
Mohammed Al Sahaf d2c15bea1b ci: fuzz: remove fuzzing trigger on PR (#3195) 2020-03-26 18:34:12 -06:00
Mohammed Al Sahaf 8da9eaee34 ci: fuzz: switch engine from libfuzzer to native go-fuzz (#3194) 2020-03-26 18:20:34 -06:00
Matthew Holt ea3688e1c0 caddytls: Remove ManageSync
This seems unnecessary for now and we can always add it in later if
people have a good reason to need it.
2020-03-26 14:02:29 -06:00
Matthew Holt c87f82f0ce caddytls: Match automation policies by wildcard subjects too
https://caddy.community/t/wildcard-snis-not-being-matched/7271/24?u=matt

Also use new CertMagic function for matching wildcard names
2020-03-26 14:01:38 -06:00
Pascal 5c55e5d53f caddytls: Support placeholders in key_type (#3176)
* tls: Support placeholders in key_type

* caddytls: Simplify placeholder support for ap.KeyType

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2020-03-25 23:16:12 -06:00
Matthew Holt 7ee3ab7baa caddyfile: Formatter enhancements 2020-03-25 18:45:54 -06:00
Mark Sargent ba08833b2a ci: exclude integration tests for now (#3188)
A workaround for inconsistent results on Windows
2020-03-25 08:55:14 -06:00
Matthew Holt 9eecd698da Merge branch 'v2' of https://github.com/caddyserver/caddy 2020-03-24 23:14:27 -06:00
Mohammed Al Sahaf 0fa1a3b630 ci: preliminary CD with goreleaser (#3173)
* chore: ci: preliminary CD support

* chore: ci: split release process into its own workflow

* chore: ci: cleanup the ci.yml and .goreleaser.yml

* chore: ci: unshallowify the clone before searching for the closes tag

* chore: tidy up goreleaser config & the release githubaction

* chore: add --no-tty to gpg args

* chore: more gpg args

* chore: try with default gpg args by goreleaser

* chore: gpg...

* chore: set GPG_TTY

* chore: preset gpg conf

* Apply suggestions from code review

chore: tidy up the .goreleaser.yml

Co-Authored-By: Dave Henderson <dhenderson@gmail.com>

* chore: gpg debugging

* chore: set and export the tty for gpg

* chore: gpg..

* chore: use the exact same line from goreleaser-action README for singing

* chore: remove signing stanzas from ymls

* chore: clean up the release action for final submission

* quote the arguments of echo

Co-Authored-By: Francis Lavoie <lavofr@gmail.com>

Co-authored-by: Dave Henderson <dhenderson@gmail.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2020-03-24 23:13:36 -06:00
Matthew Holt 673d3d00f2 file_server: Fix dumb error check I must have written at 1am 2020-03-24 16:48:04 -06:00
Matthew Holt 2acb208e32 caddyhttp: Specify default access log for a server (fix #3185) 2020-03-24 13:21:18 -06:00
Matt Holt e02117cb8a reverse_proxy: Add support for SRV backends (#3180)
* reverse_proxy: Begin SRV lookup support (WIP)

* reverse_proxy: Finish adding support for SRV-based backends (#3179)
2020-03-24 10:53:53 -06:00
Matthew Holt 95b2863df2 admin: Fix regex for removing @id fields (closes #3187) 2020-03-24 10:52:05 -06:00
Matthew Holt 341d4fb805 Remove some non-essential plugins from this repo (#2780)
Brotli encoder, jsonc and json5 config adapters, and the unfinished
HTTP cache handler are removed.

They will be available in separate repos.
2020-03-24 10:37:47 -06:00
Matthew Holt 745cb0e9e6 fastcgi: Add debug log (#3178) 2020-03-24 08:34:15 -06:00
Matthew Holt 9af05719bc logging: Fix off-by-one for roll size MB from Caddyfile
"10mb" now results in 10, rather than 9.
2020-03-24 08:20:49 -06:00
Mark Sargent d08cbefff8 report error on failed location response (#3184)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-03-23 21:18:53 -06:00
Matt Holt 2eede58b3a fastcgi: Ensure root is always absolute (issue #3178) (#3182) 2020-03-23 21:12:54 -06:00
Matthew Holt 235357abc8 fastcgi: Fix PATH_INFO (issue #3178) 2020-03-23 18:29:16 -06:00
Matthew Holt 4b4e16edaf cmd: Ensure certmagic defaults are set for any and all subcommands
This is really crucial and I'm surprised no one reported a problem yet
2020-03-23 14:43:42 -06:00
Matthew Holt ee64719d93 Update readme 2020-03-23 14:30:00 -06:00
Francis Lavoie 2491336c11 ci: Update branches to master (#3177)
* Update ci.yml

* Update fuzzing.yml
2020-03-23 14:26:53 -06:00
Matthew Holt 1698838685 tls: Few minor improvements/simplifications 2020-03-23 13:32:17 -06:00
Matthew Holt 4c43bf8cc8 caddyhttp: Always provision ACME issuers (fix terms agree error) 2020-03-23 12:21:39 -06:00
Matthew Holt 348cb798e2 httpcaddyfile: Allow php_fastcgi to be used in route directive
Fixes
https://caddy.community/t/v2-help-to-set-up-a-yourls-instance/7260/22
2020-03-23 09:28:29 -06:00
Matthew Holt e211491407 httpcaddyfile: Fix little typo (Next -> NextArg) 2020-03-22 23:13:08 -06:00
Matthew Holt 6e2fabb2a4 cmd: Add --watch flag to start & run commands (closes #1806)
Because, just for fun.
2020-03-22 22:58:33 -06:00
Mark Sargent 8cc60e6896 ci: test local CA and update SNI tests (#3145)
* run caddy tests in process

* call main with run args

* exclude tests - windows

* include json example

* disable caddyfile tests, include json test with non trusted local ca

* converted SNI tests to json syntax
2020-03-22 18:08:02 -06:00
Matthew Holt bea8dedfb2 httpcaddyfile: Move header before redir (fixes #3148) 2020-03-22 09:04:40 -06:00
Matthew Holt f2ce81cc8b fastcgi: Support multiple path splitters (close #1564) 2020-03-22 07:48:34 -06:00
Francis Lavoie 2cab475ba5 ci: Improve build artifact file names (#3168) 2020-03-21 17:44:51 -06:00
Francis Lavoie c32f383a01 ci: Use matrix to set per-os variables (#3166)
Simplify cross-platform
2020-03-21 16:53:42 -06:00
Mohammed Al Sahaf 37093befd5 caddyconfig: register adapters as Caddy modules (#3132)
* admin: Refactor /load endpoint out of caddy package

This eliminates the caddy package's dependency on the caddyconfig
package, which helps prevent import cycles.

* v2: adapter: register config adapters as Caddy modules

* v2: adapter: simplify adapter registration as adapters and modules

* v2: adapter: let RegisterAdapter be in charge of registering adapters as modules

* v2: adapter: remove underscrores placeholders

* v2: adapter: explicitly ignore the error of writing response of writing warnings back to client

* Implicitly wrap config adapters as modules

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2020-03-21 16:49:10 -06:00
Matthew Holt d692d503a3 tls/http: Fix auto-HTTPS logic w/rt default issuers (fixes #3164)
The comments in the code should explain the new logic thoroughly.
The basic problem for the issue was that we were overriding a catch-all
automation policy's explicitly-configured issuer with our own, for names
that we thought looked like public names. In other words, one could
configure an internal issuer for all names, but then our auto HTTPS
would create a new policy for public-looking names that uses the
default ACME issuer, because we assume public<==>ACME and
nonpublic<==>Internal, but that is not always the case. The new logic
still assumes nonpublic<==>Internal (on catch-all policies only), but
no longer assumes that public-looking names always use an ACME issuer.

Also fix a bug where HTTPPort and HTTPSPort from the HTTP app weren't
being carried through to ACME issuers properly. It required a bit of
refactoring.
2020-03-20 20:25:46 -06:00
Matthew Holt 3c1def2430 caddytls: Support wildcard matching in ServerName conn policy matcher 2020-03-20 15:51:37 -06:00
Matthew Holt b583007c49 httpcaddyfile: Simplify 'root' directive parsing
I must have written that one before the helper function
`RegisterHandlerDirective`.
2020-03-20 12:50:36 -06:00
Matthew Holt 6b60a301c0 httpcaddyfile: Append access logger name to log's includes (fix #3110) 2020-03-20 12:02:46 -06:00
Mohammed Al Sahaf d6632e2145 v2: update CI badge on README (#3162) 2020-03-20 08:54:53 -06:00
Matthew Holt 903776238e go.mod: Update some deps; add new Strings lib to CEL matcher 2020-03-20 08:53:40 -06:00
Matthew Holt f741ab3463 go.mod: Update CertMagic
Might fix mysterious hangs after certificate validation
2020-03-20 08:40:38 -06:00
Francis Lavoie 76ac28a624 ci: Switch to Github Actions (#3152)
* WIP: Trying to make a new branch

* Create fuzzing.yml

* Update ci.yml

* Try using reviewdog for golangci-lint

* Only run lint on ubuntu

* Whoops, wrong matrix variable

* Let's try just ubuntu for the moment

* Remove integration tests

* Let's see what the tree looks like (where's the binary)

* Let's plant a tree

* Let's look at another tree

* Burn the tree

* Let's build in the right dir

* Turn on publishing artifacts

* Add gobin to path

* Try running golangci-lint earlier

* Try running golangci-lint on its own, with checkout@v1

* Try moving golangci-lint back into ci.yml as a separate job

* Turn off azure-pipelines

* Remove the redundant name, see how it looks

* Trim down the naming some more

* Turn on windows and mac

* Try to fix windows build, cleanup

* Try to fix strange failure on windows

* Print our the coerce reason

* Apparently $? is 'True' on Windows, not 1 or 0

* Try setting CGO_ENABLED as an env in yml

* Try enabling/fixing the fuzzer

* Print out github event to check, fix step name

* Fuzzer needs the code

* Add GOBIN to PATH for fuzzer

* Comment out fork condition, left in-case we want it again

* Remove obsolete comment

* Comment out the coverage/test conversions for now

* Set continue-on-error: true for fuzzer, it runs out of mem

* Add some clarification to the retained commented sections
2020-03-20 08:38:44 -06:00
Mohammed Al Sahaf 61b427fa47 v2: fuzz: update function signature of caddyfile.Parse (#3160) 2020-03-20 06:56:57 -06:00
Paolo Barbolini 42a6628935 reverseproxy: Add Alt-Svc to Hop-by-hop headers list (#3159)
Adds `Alt-Svc` to the list of headers that get removed when proxying
to a backend.

This fixes the issue of having the contents of the Alt-Svc header
duplicated when proxying to another Caddy server.
2020-03-20 06:54:28 -06:00
Matt Holt 6a4d638c1e caddyhttp: Implement CEL matcher (see #3051) (#3155)
* caddyhttp: Implement CEL matcher (see #3051)

CEL (Common Expression Language) is a very fast, flexible way to express
complex logic, useful for matching requests when the conditions are not
easy to express with JSON.

This matcher may be considered experimental even after the 2.0 release.

* Improve CEL module docs
2020-03-19 15:46:22 -06:00
Matt Holt aa6c5fde07 httpcaddyfile: Unify strip_prefix, strip_suffix, uri_replace directives (#3157)
* rewrite: strip_prefix, strip_suffix, uri_replace -> uri (closes #3140)

* Add period, to satisfy @whitestrake :) and my own OCD

* Restore implied / prefix
2020-03-19 11:51:28 -06:00
Matthew Holt 31c6ac097e httpcaddyfile: 'bind' properly parses unix sockets (fixes #2999) 2020-03-19 09:43:17 -06:00
Matthew Holt 406df22a16 templates: Enable Goldmark's footnote extension (closes #3136)
Also remove Table extension, since GFM (already enabled) apparently
enables strikethrough, table, linkify, and tasklist extensions.
https://github.com/yuin/goldmark#built-in-extensions
2020-03-18 23:38:37 -06:00
Matthew Holt afb2ca27c1 caddyhttp: Minor improved Caddyfile support for some matchers
Simply allows the matcher to be specified multiple times in a set
which may be more convenient than one long line.
2020-03-18 23:36:25 -06:00
Matthew Holt ce45353e61 Little tweaky tweaks 2020-03-18 15:51:31 -06:00
Matthew Holt 89124aa570 httpcaddyfile: Prevent rewrite routes from consolidating (fix #3108)
It's hard to say whether this was actually a bug, but the linked issue
shows why the old behavior was confusing. Basically, we infer that a
rewrite handler is supposed to act as an internal redirect, which likely
means it will no longer match the matcher(s) it did before the rewrite.

So if the rewrite directive shares a matcher with any adjacent route or
directive, it can be confusing/misleading if we consolidate the rewrite
into the same route as the next handler, which shouldn't (probably) match
after the rewrite is complete.

This is kiiiind of a hacky workaround to a quirky problem.

For edge cases like these, it is probably "cleaner" to just use handle
blocks instead, to group handlers under the same matcher, nginx-style.
2020-03-18 12:18:10 -06:00
Matthew Holt ab2fc9d066 Update dependencies and readme 2020-03-17 21:03:17 -06:00
Matthew Holt fc7340e11a httpcaddyfile: Many tls-related improvements including on-demand support
Holy heck this was complicated
2020-03-17 21:00:45 -06:00
Mark Sargent 3f48a2eb45 caddyhttp: Add default SNI tests (#3146)
* added sni tests

* set the default sni when there is no host to match

* removed invalid sni test. Disabled tests that rely on host headers.

* readded SNI tests. Added logging of config load times
2020-03-17 12:39:01 -06:00
Vaibhav f192ae5ea5 cmd: fmt: Fix brace opening block indentation (#3153)
This fixes indentation for blocks starting with
a brace as:
```Caddyfile
{
    ...
}
```

Fixes #3144

Signed-off-by: Vaibhav <vrongmeal@gmail.com>
2020-03-17 09:55:36 -06:00
Matthew Holt b62f8e0582 caddyhttp: Support path matcher of "*" without panic 2020-03-16 16:08:33 -06:00
Matthew Holt ae86f6dd91 Use JSON format for logs if not interactive terminal 2020-03-16 14:22:40 -06:00
Matthew Holt b550ea433b Simplify build instructions in readme 2020-03-15 21:29:00 -06:00
Matthew Holt e42514ad4a caddyhttp: Clean up; move some code around 2020-03-15 21:28:42 -06:00
Matthew Holt f596fd77bb caddyhttp: Add support for listener wrapper modules
Wrapping listeners is useful for composing custom behavior related
to accepting, closing, reading/writing connections (etc) below the
application layer; for example, the PROXY protocol.
2020-03-15 21:26:17 -06:00
Matthew Holt 0433f9d075 caddytls: Clean up some code related to automation 2020-03-15 21:22:26 -06:00
Matthew Holt c67c8e60cc cmd: fmt: --write -> --overwrite to make it clear it's destructive 2020-03-15 21:18:31 -06:00
Matthew Holt 8f8ecd2e2a Add missing license texts 2020-03-15 21:18:00 -06:00
Matthew Holt 115b877e1a caddytls: Set Issuer properly on automation policies (fix #3150)
When using the default automation policy specifically, ap.Issuer would
be nil, so we'd end up overwriting the ap.magic.Issuer's default value
(after New()) with nil; this instead sets Issuer on the template before
New() is called, and no overwriting is done.
2020-03-15 09:24:24 -06:00
Matthew Holt 2ce3deb540 fileserver: Add --templates flag to file-server command 2020-03-14 23:31:52 -06:00
Matthew Holt acf4dde1dd pki: Don't treat cert installation failure as error
See https://caddy.community/t/fail-to-start-caddy2-not-nss-security-databases-found/7223?u=matt
2020-03-14 15:20:04 -06:00
Matthew Holt 7a4548c582 Some hotfixes for beta 16 2020-03-13 19:14:49 -06:00
Matthew Holt 6cbd93736f Minor tweaks 2020-03-13 13:04:10 -06:00
Mark Sargent c447236357 caddyhttp: Fix default SNI for default conn policy (#3141)
* add integration tests

* removed SNI test

* remove integration test condition

* minor edit

* fix sni when using static certificates

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-03-13 11:32:53 -06:00
Matt Holt 5a19db5dc2 v2: Implement 'pki' app powered by Smallstep for localhost certificates (#3125)
* pki: Initial commit of PKI app (WIP) (see #2502 and #3021)

* pki: Ability to use root/intermediates, and sign with root

* pki: Fix benign misnamings left over from copy+paste

* pki: Only install root if not already trusted

* Make HTTPS port the default; all names use auto-HTTPS; bug fixes

* Fix build - what happened to our CI tests??

* Fix go.mod
2020-03-13 11:06:08 -06:00
Bill Glover cfe85a9fe6 Fix #3130: Crash at fuzzing target replacer (#3133)
* Fix #3130: Crash at fuzzing target replacer

* Add additional test case based on fuzzer feedback
2020-03-11 16:12:00 -06:00
Francis Lavoie 90f1f7bce7 httpcaddyfile: error for wrong arg count of admin opt (#3126) (#3131) 2020-03-10 08:25:26 -06:00
Matt Holt 2762f8f058 caddyhttp: New algorithm for auto HTTP->HTTPS redirects (fix #3127) (#3128)
It's still not perfect but I think it should be more correct for
slightly more complex configs. Might still fall apart for complex
configs that use on-demand TLS or at a large scale (workarounds are
to just implement your own redirects, very easy to do anyway).
2020-03-09 15:18:19 -06:00
Matthew Holt 99d34f1c1d cmd: Use loadConfig() for validate as run, start, and reload do 2020-03-09 00:09:15 -06:00
Bill Glover 36a6c7daf0 Rework Replacer loop to handle escaped braces (#3121)
Fixes #3116

* Rework Replacer loop to ignore escaped braces

* Add benchmark tests for replacer

* Optimise handling of escaped braces

* Handle escaped closing braces

* Remove additional check for closing brace

This commit removes the additional check for input in which the closing
brace appears before the opening brace. This check has been removed for
performance reasons as it is deemed an unlikely edge case.

* Check for escaped closing braces in placeholder name
2020-03-08 15:36:59 -06:00
evtr ca6e54bbb8 caddytls: customizable client auth modes (#2913)
* ability to specify that client cert must be present in SSL

* changed the clientauthtype to string and make room for the values supported by go as in caddy1

* renamed the config parameter according to review comments and added documentation on allowed values

* missed a reference

* Minor cleanup; docs enhancements

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2020-03-08 09:48:25 -06:00
Mohammed Al Sahaf fb5168d3b4 http_ntlm: fix panic due to unintialized embedded field (#3120) 2020-03-07 17:58:44 -07:00
Matthew Holt 217419f6d9 tls: Couple of quick fixes for 4d18587192 2020-03-07 11:47:55 -07:00
Matthew Holt 4d18587192 tls: Auto-migrate cert assets to new path (details in #3124) 2020-03-07 10:42:50 -07:00
Matthew Holt b216d285df Merge branch 'certmagic-refactor' into v2 2020-03-06 23:26:13 -07:00
Matthew Holt b8cba62643 Refactor for CertMagic v0.10; prepare for PKI app
This is a breaking change primarily in two areas:
 - Storage paths for certificates have changed
 - Slight changes to JSON config parameters

Huge improvements in this commit, to be detailed more in
the release notes.

The upcoming PKI app will be powered by Smallstep libraries.
2020-03-06 23:15:25 -07:00
Matt Holt 3f5d27cd5d ci: Optimize published artifacts (#3118)
Build the published executables with CGO disabled, stripped, and with `-trimpath` for more reproducible build
2020-03-04 13:19:25 -07:00
Mark Sargent 26fb8b3efd httpcaddyfile: remove certificate tags from global state (#3111)
* remove the certificate tag tracking from global state

* refactored helper state, added log counter

* moved state initialisation close to where it is used.

* added helper state comment
2020-03-04 09:58:49 -07:00
Marten Seemann e6c6210772 update quic-go to v0.15.1 (#3109) 2020-03-02 07:13:49 -07:00
Marten Seemann 1324da2241 go.mod: update quic-go to v0.15.0 (supporting QUIC draft-27) (#3107) 2020-03-01 12:34:57 -07:00
Vaibhav 71e81d262b fmt: Add support for block nesting. (#3105)
Previously the formatter did not include support for
blocks inside other blocks. Hence the formatter could
not indent some files properly. This fixes it.

Fixes #3104

Signed-off-by: Vaibhav <vrongmeal@gmail.com>
2020-02-29 13:23:08 -07:00
Vaibhav 5fe69ac4ab cmd: Add caddy fmt command. (#3090)
This takes the config file as input and formats it.
Prints the result to stdout. Can write changes to
file if `--write` flag is passed.

Fixes #3020

Signed-off-by: Vaibhav <vrongmeal@gmail.com>
2020-02-29 10:12:16 -07:00
Mohammed Al Sahaf e717028f83 ci: publish build artifacts (#3103)
* ci: publish build artifacts (per-commit Caddy binaries)

* ci: include OS name in artifact name of *nix binaries so they don't overwrite each other
2020-02-29 20:09:50 +03:00
Matthew Holt a60da8e7ab Simplify the logic in the previous commit 2020-02-28 13:49:51 -07:00
Matthew Holt 00e99df209 httpcaddyfile: Treat no matchers as 0-len path matchers (fix #3100)
+ a couple other minor changes from linter
2020-02-28 13:38:12 -07:00
Matthew Holt c83d40ccd4 reverse_proxy, php_fastcgi: Fix upstream parsing regression (fix #3101) 2020-02-28 08:57:59 -07:00
Matthew Holt e4ec08e977 Couple of minor docs tweaks 2020-02-27 21:08:21 -07:00
Matthew Holt 03ab55b51a httpcaddyfile: Allow "admin off" option 2020-02-27 21:04:28 -07:00
Matthew Holt cef6e098bb Refactor ExtractMatcherSet() 2020-02-27 21:04:28 -07:00
Matthew Holt 260982b2df reverse_proxy: Allow use of URL to specify scheme
This makes it more convenient to configure quick proxies that use HTTPS
but also introduces a lot of logical complexity. We have to do a lot of
verification for consistency and errors.

Path and query string is not supported (i.e. no rewriting).

Scheme and port can be inferred from each other if HTTP(S)/80/443.
If omitted, defaults to HTTP.

Any explicit transport config must be consistent with the upstream
schemes, and the upstream schemes must all match too.

But, this change allows a config that used to require this:

    reverse_proxy example.com:443 {
        transport http {
            tls
        }
    }

to be reduced to this:

    reverse_proxy https://example.com

which is really nice syntactic sugar (and is reminiscent of Caddy 1).
2020-02-27 21:04:28 -07:00
Matthew Holt 0130b699df cmd/reverse_proxy: Add --change-host-header flag
"Transparent mode" is the default, just like the actual handler.
2020-02-27 21:04:28 -07:00
Success Go ca5c679880 Fix typos (#3087)
* Fix typo

* Fix typo, thanks for Spell Checker under VS Code
2020-02-27 19:30:48 -07:00
Matthew Holt e2d41ee761 Revert "reverse_proxy: Add 'transparent' Caddyfile subdirective (closes #2873)"
This reverts commit 86b785e51c.
2020-02-27 11:08:56 -07:00
Matthew Holt 86b785e51c reverse_proxy: Add 'transparent' Caddyfile subdirective (closes #2873) 2020-02-27 10:20:13 -07:00
Success Go f6ae092507 It might be HTTP->HTTPS in the comment (#3086) 2020-02-27 00:50:36 -05:00
Success Go a2a41a5bdf Fix spelling error (#3085) 2020-02-27 00:22:40 -05:00
Mohammed Al Sahaf 6fb98ba188 ci: improve CI flow (#3083)
* ci: update golangci-lint
* ci: build Caddy to catch build error
* ci: remove GO111MODULE env var
* ci: update MacOS image
2020-02-27 03:51:54 +03:00
Zaq? Wiedmann 063ed1e7f9 caddyfile: expand environment variables within caddy files (#3082)
Small expansion to the work done in https://github.com/caddyserver/caddy/pull/2963 which simply calls `os.ExpandEnv` so env vars like `{$URL}` where `$URL=$SCHEME://$HOST:$PORT` (contrived) get the expanded $SCHEME, $HOST, and $PORT variables included
2020-02-26 16:06:34 -07:00
Mark Sargent 2de0acc11f Initial implementation of global default SNI option (#3047)
* add global default sni

* fixed grammar

* httpcaddyfile: Reduce some duplicated code

* Um, re-commit already-committed commit, I guess? (sigh)

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-02-26 16:01:47 -07:00
Matt Holt 5d97522d18 v2: 'log' directive for Caddyfile, and debug mode (#3052)
* httpcaddyfile: Begin implementing log directive, and debug mode

For now, debug mode just sets the log level for all logs to DEBUG
(unless a level is specified explicitly).

* httpcaddyfile: Finish 'log' directive

Also rename StringEncoder -> SingleFieldEncoder

* Fix minor bug in replacer (when vals are empty)
2020-02-25 22:00:33 -07:00
Matthew Holt f6b9cb7122 httpcaddyfile: Matchers can now be embedded into a nested scope
This is useful in 'handle' and 'route' directives, for instance, if you
want to keep your matcher definitions by the directives that use them.
2020-02-25 21:56:43 -07:00
Matthew Holt 78760c0ddc go.mod: Bump to Go 1.14 2020-02-25 19:24:13 -07:00
Cameron Moore b0a491aec8 Expose TLS placeholders (#2982)
* caddytls: Add CipherSuiteName and ProtocolName functions

The cipher_suites.go file is derived from a commit to the Go master
branch that's slated for Go 1.14.  Once Go 1.14 is released, this file
can be removed.

* caddyhttp: Use commonLogEmptyValue in common_log replacer

* caddyhttp: Add TLS placeholders

* caddytls: update unsupportedProtocols

Don't export unsupportedProtocols and update its godoc to mention that
it's used for logging only.

* caddyhttp: simplify getRegTLSReplacement signature

getRegTLSReplacement should receive a string instead of a pointer.

* caddyhttp: Remove http.request.tls.client.cert replacer

The previous behavior of printing the raw certificate bytes was ported
from Caddy 1, but the usefulness of that approach is suspect.  Remove
the client cert replacer from v2 until a use case is presented.

* caddyhttp: Use tls.CipherSuiteName from Go 1.14

Remove ported version of CipherSuiteName in the process.
2020-02-25 19:22:50 -07:00
Success Go 45b171ff3a Make comment more readable about caddy ModuleID's Name() method. (#3080) 2020-02-25 09:11:29 -07:00
Success Go 623a1c588e Fix typo in cmdStart comment 2020-02-25 02:33:33 -05:00
Matthew Holt 7cca291d62 reverse_proxy: Health checks: Don't cross the streams
Fixes https://caddy.community/t/v2-health-checks-are-going-to-the-wrong-upstream/7084?u=matt

... I think
2020-02-23 14:31:05 -07:00
Robin Lambertz e3591009dc caddyhttp: Add handler for unhandled errors in errorChain (#3063)
* Add handler for unhandled errors in errorChain

Currently, when an error chain is defined, the default error handler is
bypassed entirely - even if the error chain doesn't handle every error.
This results in pages returning a blank 200 OK page.

For instance, it's possible for an error chain to match on the error
status code and only handle a certain subtype of errors (like 403s). In
this case, we'd want any other errors to still go through the default
handler and return an empty page with the status code.

This PR changes the "suffix handler" passed to errorChain.Compile to
set the status code of the response to the error status code.

Fixes #3053

* Move the errorHandlerChain middleware to variable

* Style fix
2020-02-20 15:00:30 -07:00
Gilbert Gilb's 30c14084ab caddyhttp: Fixes for header and header_regexp directives (#3061)
* Fix crash when specifying "*" to header directive.

Fixes #3060

* Look Host header in header and header_regexp.

Also, if more than one header is provided, header_regexp now looks for
extra headers values to reflect the behavior from header.

Fixes #3059

* Fix parsing of named header_regexp in Caddyfile.

See #3059
2020-02-20 10:55:47 -07:00
Matthew Holt 99f91c4c6f httpcaddyfile: tls: Load repeated cert files only once, with one tag
See end of issue #3004. Loading the same certificate file multiple times
with different tags will result in it being de-duplicated in the in-
memory cache, because of course they all have the same bytes. This
meant that any certs of the same filename loaded with different tags
would be overwritten by the next certificate of the same filename, and
any conn policies looking for the tags of the previous ones would never
find them, causing connections to fail.

So, now we remember cert filenames and their tags, instead of loading
them multiple times and overwriting previous ones.

A user crafting their own JSON might make this error too... maybe we
won't see it happen. But if it does, one possibility is, when loading
a duplicate cert, instead of discarding it completely, merge the tag
list into the one that's already stored in the cache, then discard.
2020-02-20 10:18:29 -07:00
Matthew Holt 0005e3acdc httpcaddyfile: Combine repeated cert loaders (fix #3004)
Also only append 1 catch-all TLS connection policy to a server, even if
multiple site blocks contribute to that server.
2020-02-20 00:15:11 -07:00
Matthew Holt 0b09b070e5 httpcaddyfile: Properly add all cert loaders across sites (fixes #3056) 2020-02-18 11:13:51 -07:00
Matthew Holt 7f9cfcc0f2 http: Close HTTP/3 servers and listeners; upstream bug irreproducible
See https://github.com/lucas-clemente/quic-go/issues/2103
and https://github.com/caddyserver/caddy/pull/2727
2020-02-18 10:39:34 -07:00
Matthew Holt 87a742c1e5 tls: Fix panic loading automation management modules (fix #3004)
When AutomationPolicy was turned into a pointer, we continued passing
a double pointer to LoadModule, oops.
2020-02-18 09:54:14 -07:00
Robin Lambertz 57c6f22684 basicauth: default hash to bcrypt (#3050)
The documentation specifies that the hash algorithm defaults to bcrypt.
However, the implementation returns an error in provision if no hash is
provided.

Fix this inconsistency by *actually* defaulting to bcrypt.
2020-02-17 12:19:59 -07:00
Marten Seemann dd103a6787 go.mod: update quic-go to v0.14.4 (#3048) 2020-02-17 08:54:03 -07:00
Matthew Holt 23cc26d585 httpcaddyfile: 'handle_errors' directive
Not sure I love the name of the directive; might change it later.
2020-02-16 22:24:20 -07:00
Matthew Holt bc2e406572 httpcaddyfile: Refactor global options parsing; prevent duplicate keys 2020-02-16 15:28:27 -07:00
Matthew Holt bf776e7de7 http: Remove redundant test file
Forgot to delete this when I moved its test into a different file
2020-02-16 15:27:53 -07:00
Matthew Holt f42b138fb1 tls: Avoid duplication AutomationPolicies for large quantities of names
This should greatly reduce memory usage at scale. Part of an overall
effort between Caddy 2 and CertMagic to optimize for large numbers of
names.
2020-02-14 11:14:52 -07:00
Matthew Holt 2cc5d2227d Minor tweaks to docs/comments 2020-02-14 11:01:09 -07:00
Matthew Holt 15bf9c196c caddyfile: Refactor; NewFromNextSegment(); fix repeated matchers
Now multiple instances of the same matcher can be used within a named
matcher without overwriting previous ones.
2020-02-14 11:01:09 -07:00
Mark Sargent eb80165583 tls: Add acme_ca_root and tls/ca_root to caddyfile (#3040) 2020-02-12 13:07:25 -07:00
193 changed files with 17591 additions and 4493 deletions
+184
View File
@@ -0,0 +1,184 @@
Contributing to Caddy
=====================
Welcome! Thank you for choosing to be a part of our community. Caddy wouldn't be great without your involvement!
For starters, we invite you to join [the Caddy forum](https://caddy.community) where you can hang out with other Caddy users and developers.
## Common Tasks
- [Contributing code](#contributing-code)
- [Writing a Caddy module](#writing-a-caddy-module)
- [Asking or answering questions for help using Caddy](#getting-help-using-caddy)
- [Reporting a bug](#reporting-bugs)
- [Suggesting an enhancement or a new feature](#suggesting-features)
- [Improving documentation](#improving-documentation)
Other menu items:
- [Values](#values)
- [Coordinated Disclosure](#coordinated-disclosure)
- [Thank You](#thank-you)
### 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).
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.
- **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)
- **Keep related commits together in a PR.** We do want pull requests to be small, but you should also keep multiple related commits in the same PR if they rely on each other.
- **Write tests.** Tests are essential! Written properly, they ensure your change works, and that other changes in the future won't break your change. CI checks should pass.
- **Benchmarks should be included for optimizations.** Optimizations sometimes make code harder to read or have changes that are less than obvious. They should be proven with benchmarks or profiling.
- **[Squash](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) insignificant commits.** Every commit should be significant. Commits which merely rewrite a comment or fix a typo can be combined into another commit that has more substance. Interactive rebase can do this, or a simpler way is `git reset --soft <diverging-commit>` then `git commit -s`.
- **Own your contributions.** Caddy is a growing project, and it's much better when individual contributors help maintain their change after it is merged.
- **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.
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:
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`.
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`
4. Make your changes in the caddyserver/caddy.git repo on your computer.
5. Push your changes to your fork: `git push myfork`
6. [Create a pull request](https://github.com/caddyserver/caddy/pull/new/master) to merge your changes into caddyserver/caddy @ master. (Click "compare across forks" and change the head fork.)
This workflow is nice because you don't have to change import paths. You can get fancier by using different branches if you want.
### Writing a Caddy module
Caddy can do more with modules! Anyone can write one. Caddy modules are Go libraries that get compiled into Caddy, extending its feature set. They can add directives to the Caddyfile, add new configuration adapters, and even implement new server types (e.g. HTTP, DNS).
[Learn how to write a module here](https://caddyserver.com/docs/extending-caddy). You should also share and discuss your module idea [on the forums](https://caddy.community) to have people test it out. We don't use the Caddy issue tracker for third-party modules.
### Getting help using Caddy
If you have a question about using Caddy, [ask on our forum](https://caddy.community)! There will be more people there who can help you than just the Caddy developers who follow our issue tracker. Issues are not the place for usage questions.
Many people on the forums could benefit from your experience and expertise, too. Once you've been helped, consider giving back by answering other people's questions and participating in other discussions.
### Reporting bugs
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.
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 suggest reading [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
Please be kind. :smile: Remember that Caddy comes at no cost to you, and you're getting free support when we fix your issues. If we helped you, please consider helping someone else!
#### Bug reporting expectations
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.
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.
3. A solution, or ideas toward a solution (mostly maintainer's responsibility).
- Sometimes the best solution is a documentation change.
- Usually the developers have the best domain knowledge for inventing a solution, but reporters may have ideas or preferences for how they would like the software to work.
- Security, correctness, and project goals/vision all take priority over a user's preferences.
- It's simply good business to yield a solution that satisfies the users, and it's even better business to leave them impressed.
Thus, at the very least, the reporter is expected to:
1. Convince the reader that it's a bug (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.
### Suggesting features
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.
### Improving documentation
Caddy's documentation is available at [https://caddyserver.com/docs](https://caddyserver.com/docs) and its source is in the [website repo](https://github.com/caddyserver/website). If you would like to make a fix to the docs, please submit an issue there describing the change to make.
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.
## 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:
- **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?
- Is this a maintenance burden?
- What assumptions does the code make?
- Is it well-tested?
- Is the change a good fit for the project?
- Does it actually fix the problem or is it creating a special case instead?
- Does the change incur any new dependencies? (Avoid these!)
- **Answer issues.** If every collaborator helped out with issues, we could count the number of open issues on two hands. This means getting involved in the discussion, investigating the code, and yes, debugging it. It's fun. Really! :smile: Please, please help with open issues. Granted, some issues need to be done before others. And of course some are larger than others: you don't have to do it all yourself. Work with other collaborators as a team!
- **Do not merge pull requests until they have been approved by one or two other collaborators.** If a project owner approves the PR, it can be merged (as long as the conversation has finished too).
- **Prefer squashed commits over a messy merge.** If there are many little commits, please [squash the commits](https://stackoverflow.com/a/11732910/1048862) so we don't clutter the commit history.
- **Don't accept new dependencies lightly.** Dependencies can make the world crash and burn, but they are sometimes necessary. Choose carefully. Extremely small dependencies (a few lines of code) can be inlined. The rest may not be needed. For those that are, Caddy uses [go modules](https://github.com/golang/go/wiki/Modules). All external dependencies must be installed as modules, and _Caddy must not export any types defined by those dependencies_. Check this diligently!
- **Be extra careful in some areas of the code.** There are some critical areas in the Caddy code base that we review extra meticulously: the `caddyhttp` and `caddytls` packages especially.
- **Make sure tests test the actual thing.** Double-check that the tests fail without the change, and pass with it. It's important that they assert what they're purported to assert.
- **Recommended reading**
- [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments) for an idea of what we look for in good, clean Go code
- [Linus Torvalds describes a good commit message](https://gist.github.com/matthewhudson/1475276)
- [Best Practices for Maintainers](https://opensource.guide/best-practices/)
- [Shrinking Code Review](https://alexgaynor.net/2015/dec/29/shrinking-code-review/)
## Values
- 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.
- The ends justify the means, if the means are good. A good tree won't produce bad fruit. But if we cut corners or are hasty in our process, the end result will not be good.
## Security Policy
If you think you've found a security vulnerability, please refer to our [Security Policy](https://github.com/caddyserver/caddy/security/policy) document.
## Thank you
Thanks for your help! Caddy would not be what it is today without your contributions.
+12
View File
@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [mholt] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+27
View File
@@ -0,0 +1,27 @@
# Security Policy
The Caddy project would like to make sure that it stays on top of all practically-exploitable vulnerabilities.
Some security problems are more the result of interplay between different components of the Web, rather than a vulnerability in the web server itself. Please report only vulnerabilities in the web server itself, as we cannot coerce the rest of the Web to be fixed (for example, we do not consider IP spoofing or BGP hijacks a vulnerability in the Caddy web server).
Please note that we consider publicly-registered domain names to be public information. This necessary in order to maintain the integrity of certificate transparency, public DNS, and other public trust systems.
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 2.x | :white_check_mark: |
| 1.x | :white_check_mark: (deprecating soon) |
| < 1.x | :x: |
## Reporting a Vulnerability
Please email Matt Holt (the author) directly: matt [at] lightcodelabs [dot com].
We'll need enough information to verify the bug and make a patch. It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, resources permitting. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. Thank you for understanding.
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.
Thanks for responsibly helping Caddy&mdash;and thousands of websites&mdash;be more secure!
+175
View File
@@ -0,0 +1,175 @@
# Used as inspiration: https://github.com/mvdan/github-actions-golang
name: Tests
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
test:
strategy:
# Default is true, cancels jobs for other platforms in the matrix if one fails
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
go-version: [ 1.14.x ]
# Set some variables per OS, usable via ${{ matrix.VAR }}
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
# SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
include:
- os: ubuntu-latest
CADDY_BIN_PATH: ./cmd/caddy/caddy
SUCCESS: 0
- os: macos-latest
CADDY_BIN_PATH: ./cmd/caddy/caddy
SUCCESS: 0
- os: windows-latest
CADDY_BIN_PATH: ./cmd/caddy/caddy.exe
SUCCESS: 'True'
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
# These tools would be useful if we later decide to reinvestigate
# publishing test/coverage reports to some tool for easier consumption
# - name: Install test and coverage analysis tools
# run: |
# go get github.com/axw/gocov/gocov
# go get github.com/AlekSi/gocov-xml
# go get -u github.com/jstemmer/go-junit-report
# echo "::add-path::$(go env GOPATH)/bin"
- 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
# 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
with:
path: ${{ steps.vars.outputs.go_cache }}
key: ${{ runner.os }}-go-ci-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-ci
- name: Get dependencies
run: |
go get -v -t -d ./...
# mkdir test-results
- name: Build Caddy
working-directory: ./cmd/caddy
env:
CGO_ENABLED: 0
run: |
go build -trimpath -ldflags="-w -s" -v
- name: Publish Build Artifact
uses: actions/upload-artifact@v1
with:
name: caddy_v2_${{ runner.os }}_${{ steps.vars.outputs.short_sha }}
path: ${{ matrix.CADDY_BIN_PATH }}
# Commented bits below were useful to allow the job to continue
# even if the tests fail, so we can publish the report separately
# For info about set-output, see https://stackoverflow.com/questions/57850553/github-actions-check-steps-status
- name: Run tests
# id: step_test
# continue-on-error: true
run: |
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
go test -v -coverprofile="cover-profile.out" -short -race ./...
# echo "::set-output name=status::$?"
# Relevant step if we reinvestigate publishing test/coverage reports
# - name: Prepare coverage reports
# run: |
# mkdir coverage
# gocov convert cover-profile.out > coverage/coverage.json
# # Because Windows doesn't work with input redirection like *nix, but output redirection works.
# (cat ./coverage/coverage.json | gocov-xml) > coverage/coverage.xml
# To return the correct result even though we set 'continue-on-error: true'
# - name: Coerce correct build result
# if: matrix.os != 'windows-latest' && steps.step_test.outputs.status != ${{ matrix.SUCCESS }}
# run: |
# echo "step_test ${{ steps.step_test.outputs.status }}\n"
# exit 1
s390x-test:
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
- name: Run Tests
run: |
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
# short sha is enough?
short_sha=$(git rev-parse --short HEAD)
# The environment is fresh, so there's no point in keeping accepting and adding the key.
rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . caddy-ci@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t caddy-ci@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; CGO_ENABLED=0 /usr/local/go/bin/go test -v ./..."
test_result=$?
# There's no need leaving the files around
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null caddy-ci@ci-s390x.caddyserver.com "rm -rf /var/tmp/'$short_sha'"
echo "Test exit code: $test_result"
exit $test_result
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
with:
version: latest
args: check
env:
TAG: ${{ steps.vars.outputs.version_tag }}
+56
View File
@@ -0,0 +1,56 @@
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-version: [ 1.14.x ]
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- 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@v1
with:
path: ${{ steps.vars.outputs.go_cache }}
key: cross-build-go-${{ matrix.goos }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
cross-build-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
@@ -0,0 +1,73 @@
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
+83
View File
@@ -0,0 +1,83 @@
name: Release
on:
push:
tags:
- 'v*.*.*'
jobs:
release:
name: Release
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
# So GoReleaser can generate the changelog properly
- name: Unshallowify the repo clone
run: git fetch --prune --unshallow
# 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
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=version_tag::${GITHUB_REF/refs\/tags\//}"
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
echo "::set-output name=go_cache::$(go env GOCACHE)"
# Parse semver
TAG=${GITHUB_REF/refs\/tags\//}
SEMVER_RE='[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z\.-]*\)'
TAG_MAJOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\1#"`
TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"`
TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"`
TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"`
echo "::set-output name=tag_major::${TAG_MAJOR}"
echo "::set-output name=tag_minor::${TAG_MINOR}"
echo "::set-output name=tag_patch::${TAG_PATCH}"
echo "::set-output name=tag_special::${TAG_SPECIAL}"
- name: Cache the build cache
uses: actions/cache@v1
with:
path: ${{ steps.vars.outputs.go_cache }}
key: ${{ runner.os }}-go-release-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-release
# GoReleaser will take care of publishing those artifacts into the release
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v1
with:
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.vars.outputs.version_tag }}
# Only publish on non-special tags (e.g. non-beta)
- 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
curl -F package=@"$filename" https://${GEMFURY_PUSH_TOKEN}:@push.fury.io/caddy/
done
+34
View File
@@ -0,0 +1,34 @@
name: Release Published
# Event payload: https://developer.github.com/webhooks/event-payloads/#release
on:
release:
types: [published]
jobs:
release:
name: Release Published
strategy:
matrix:
os: [ ubuntu-latest ]
runs-on: ${{ matrix.os }}
steps:
# See https://github.com/peter-evans/repository-dispatch
- name: Trigger event on caddyserver/dist
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/dist
event-type: release-tagged
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
- name: Trigger event on caddyserver/caddy-docker
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/caddy-docker
event-type: release-tagged
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
+5
View File
@@ -16,3 +16,8 @@ cmd/caddy/caddy.exe
# go modules
vendor
# goreleaser artifacts
dist
caddy-build
caddy-dist
+2
View File
@@ -8,6 +8,8 @@ linters-settings:
linters:
enable:
- bodyclose
- prealloc
- unconvert
- errcheck
- gofmt
- goimports
+117
View File
@@ -0,0 +1,117 @@
before:
hooks:
# The build is done in this particular way to build Caddy in a designated directory named in .gitignore.
# This is so we can run goreleaser on tag without Git complaining of being dirty. The main.go in cmd/caddy directory
# cannot be built within that directory due to changes necessary for the build causing Git to be dirty, which
# subsequently causes gorleaser to refuse running.
- mkdir -p caddy-build
- cp cmd/caddy/main.go caddy-build/main.go
- cp ./go.mod caddy-build/go.mod
- sed -i.bkp 's|github.com/caddyserver/caddy/v2|caddy|g' ./caddy-build/go.mod
# 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
- git clone --depth 1 https://github.com/caddyserver/dist caddy-dist
- go mod download
builds:
- env:
- CGO_ENABLED=0
- GO111MODULE=on
main: main.go
dir: ./caddy-build
binary: caddy
goos:
- darwin
- linux
- windows
- freebsd
goarch:
- amd64
- arm
- arm64
- s390x
- ppc64le
goarm:
- 5
- 6
- 7
ignore:
- goos: darwin
goarch: arm
- goos: darwin
goarch: ppc64le
- goos: darwin
goarch: s390x
- goos: windows
goarch: ppc64le
- goos: windows
goarch: s390x
- goos: freebsd
goarch: ppc64le
- goos: freebsd
goarch: s390x
- goos: freebsd
goarch: arm
goarm: 5
flags:
- -trimpath
ldflags:
- -s -w
archives:
- format_overrides:
- goos: windows
format: zip
replacements:
darwin: mac
checksum:
algorithm: sha512
nfpms:
- id: default
package_name: caddy
vendor: Light Code Labs
homepage: https://caddyserver.com
maintainer: Matthew Holt <mholt@users.noreply.github.com>
description: |
Powerful, enterprise-ready, open source web server with automatic HTTPS written in Go
license: Apache 2.0
formats:
- deb
# - 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
scripts:
postinstall: ./caddy-dist/scripts/postinstall.sh
preremove: ./caddy-dist/scripts/preremove.sh
postremove: ./caddy-dist/scripts/postremove.sh
release:
github:
owner: caddyserver
name: caddy
draft: true
prerelease: auto
changelog:
sort: asc
filters:
exclude:
- '^chore:'
- '^ci:'
- '^docs?:'
- '^readme:'
- '^tests?:'
- '^\w+\s+' # a hack to remove commit messages without colons thus don't correspond to a package
+49 -45
View File
@@ -1,41 +1,31 @@
Caddy 2
=======
This is the development branch for Caddy 2.
**Caddy 2 is production-ready, but there may be breaking changes before the stable 2.0 release.** Please test it and deploy it as much as you are able, and submit your feedback!
**Caddy 2 is the web server of the Go community.** We are looking for maintainers to represent the community! Please become involved with issues, PRs, [our forum](https://caddy.community), sharing on social media, etc.
---
<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>
</p>
<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://dev.azure.com/mholt-dev/Caddy/_build/latest?definitionId=5&branchName=v2"><img src="https://dev.azure.com/mholt-dev/Caddy/_apis/build/status/Multiplatform%20Tests?branchName=v2"></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://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-%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>
<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>
</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>
### Menu
- [Features](#features)
- [Build from source](#build-from-source)
- [Building with plugins](#building-with-plugins)
- [Getting started](#getting-started)
- [For development](#for-development)
- [With version information and/or plugins](#with-version-information-andor-plugins)
- [Quick start](#quick-start)
- [Overview](#overview)
- [Full documentation](#full-documentation)
- [Getting help](#getting-help)
@@ -44,49 +34,63 @@ This is the development branch for Caddy 2.
<p align="center">
<b>Powered by</b>
<br>
<a href="https://github.com/mholt/certmagic"><img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250"></a>
<a href="https://github.com/caddyserver/certmagic"><img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250"></a>
</p>
## Features
- **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
- Fully-managed local CA for internal names & IPs
- Can coordinate with other Caddy instances in a cluster
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
- **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
## Build from source
_**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions below for building with plugins (you do not have to add any plugins)._
Requirements:
- [Go 1.13 or newer](https://golang.org/dl/)
- Do NOT disable [Go modules](https://github.com/golang/go/wiki/Modules) (`export GO111MODULE=auto`)
Download the `v2` source code:
```bash
$ git clone -b v2 "https://github.com/caddyserver/caddy.git"
```
Build:
- [Go 1.14 or newer](https://golang.org/dl/)
### For development
```bash
$ git clone "https://github.com/caddyserver/caddy.git"
$ cd caddy/cmd/caddy/
$ go build
```
That will put a `caddy(.exe)` binary into the current directory.
_**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions below._
If you encounter any Go-module-related errors, try clearing your Go module cache (`$GOPATH/pkg/mod`) or Go package cache (`$GOPATH/pkg`) and read [the Go wiki page about modules for help](https://github.com/golang/go/wiki/Modules). If you have issues with Go modules, please consult the Go community for help. But if there is an actual error in Caddy, please report it to us.
### With version information and/or plugins
### Building with plugins
Using [our builder tool, `xcaddy`](https://github.com/caddyserver/xcaddy)...
Caddy is extensible with plugins. Plugins are added at compile-time, so all Caddy binaries are static (self-contained) and portable.
```
$ xcaddy build
```
Instructions for doing this are also given in comments in [cmd/caddy/main.go](https://github.com/caddyserver/caddy/blob/v2/cmd/caddy/main.go) which you can copy and use as a template.
...the following steps are automated:
1. Create a new folder: `mkdir caddy`
2. Change into it: `cd caddy`
3. Copy [Caddy's main.go](https://github.com/caddyserver/caddy/blob/v2/cmd/caddy/main.go) into the empty folder. Add imports for any plugins you want to include.
4. Run: `go mod init caddy`
5. Run: `go get github.com/caddyserver/caddy/v2@TAG` replacing `TAG` with the latest v2 tag. (Won't be necessary after stable 2.0 release.)
6. Run: `go build`
Congrats, you now have a custom Caddy build with proper version information!
3. Copy [Caddy's main.go](https://github.com/caddyserver/caddy/blob/master/cmd/caddy/main.go) into the empty folder. Add imports for any custom plugins you want to add.
4. Initialize a Go module: `go mod init caddy`
5. (Optional) Pin Caddy version: `go get github.com/caddyserver/caddy/v2@version` replacing `version` with a git tag or commit.
6. (Optional) Add plugins by adding their import: `_ "import/path/here"`
7. Compile: `go build`
@@ -97,7 +101,7 @@ The [Caddy website](https://caddyserver.com/docs/) has documentation that includ
**We recommend that all users do our [Getting Started](https://caddyserver.com/docs/getting-started) guide to become familiar with using Caddy.**
If you've only got a few minutes, [the website has several quick-start tutorials](https://caddyserver.com/docs/quick-starts) to choose from! However, after finishing a quick-start tutorial, please read more documentation to understand how the software works. 🙂
If you've only got a minute, [the website has several quick-start tutorials](https://caddyserver.com/docs/quick-starts) to choose from! However, after finishing a quick-start tutorial, please read more documentation to understand how the software works. 🙂
@@ -114,7 +118,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.
@@ -135,7 +139,7 @@ The docs are also open source. You can contribute to them here: https://github.c
- 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](/caddyserver/caddy/issues) only for bug reports and feature requests, i.e. actionable development items (support questions will usually be referred to the forums).
Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only for bug reports and feature requests, i.e. actionable development items (support questions will usually be referred to the forums).
+93 -124
View File
@@ -18,10 +18,12 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"expvar"
"fmt"
"io"
"mime"
"io/ioutil"
"net"
"net/http"
"net/http/pprof"
"net/url"
@@ -33,7 +35,6 @@ import (
"sync"
"time"
"github.com/caddyserver/caddy/v2/caddyconfig"
"go.uber.org/zap"
)
@@ -61,10 +62,11 @@ type AdminConfig struct {
// default.
EnforceOrigin bool `json:"enforce_origin,omitempty"`
// The list of allowed origins for API requests. Only used 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.
// 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.
Origins []string `json:"origins,omitempty"`
// Options related to configuration management.
@@ -80,27 +82,28 @@ type ConfigSettings struct {
// listenAddr extracts a singular listen address from ac.Listen,
// returning the network and the address of the listener.
func (admin AdminConfig) listenAddr() (string, string, error) {
func (admin AdminConfig) listenAddr() (NetworkAddress, error) {
input := admin.Listen
if input == "" {
input = DefaultAdminListen
}
listenAddr, err := ParseNetworkAddress(input)
if err != nil {
return "", "", fmt.Errorf("parsing admin listener address: %v", err)
return NetworkAddress{}, fmt.Errorf("parsing admin listener address: %v", err)
}
if listenAddr.PortRangeSize() != 1 {
return "", "", fmt.Errorf("admin endpoint must have exactly one address; cannot listen on %v", listenAddr)
return NetworkAddress{}, fmt.Errorf("admin endpoint must have exactly one address; cannot listen on %v", listenAddr)
}
return listenAddr.Network, listenAddr.JoinHostPort(0), nil
return listenAddr, nil
}
// 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(listenAddr string) adminHandler {
func (admin AdminConfig) newAdminHandler(addr NetworkAddress) adminHandler {
muxWrap := adminHandler{
enforceOrigin: admin.EnforceOrigin,
allowedOrigins: admin.allowedOrigins(listenAddr),
enforceHost: !addr.isWildcardInterface(),
allowedOrigins: admin.allowedOrigins(addr),
mux: http.NewServeMux(),
}
@@ -115,7 +118,6 @@ func (admin AdminConfig) newAdminHandler(listenAddr string) adminHandler {
}
// register standard config control endpoints
addRoute("/load", AdminHandlerFunc(handleLoad))
addRoute("/"+rawConfigKey+"/", AdminHandlerFunc(handleConfig))
addRoute("/id/", AdminHandlerFunc(handleConfigID))
addRoute("/stop", AdminHandlerFunc(handleStop))
@@ -143,16 +145,32 @@ func (admin AdminConfig) newAdminHandler(listenAddr string) adminHandler {
// If admin.Origins is nil (null), the provided listen address
// will be used as the default origin. If admin.Origins is
// empty, no origins will be allowed, effectively bricking the
// endpoint, but whatever.
func (admin AdminConfig) allowedOrigins(listen string) []string {
// endpoint for non-unix-socket endpoints, but whatever.
func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
uniqueOrigins := make(map[string]struct{})
for _, o := range admin.Origins {
uniqueOrigins[o] = struct{}{}
}
if admin.Origins == nil {
uniqueOrigins[listen] = struct{}{}
if addr.isLoopback() {
if addr.IsUnixNetwork() {
// RFC 2616, Section 14.26:
// "A client MUST include a Host header field in all HTTP/1.1 request
// messages. If the requested URI does not include an Internet host
// name for the service being requested, then the Host header field MUST
// be given with an empty value."
uniqueOrigins[""] = struct{}{}
} else {
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{}
}
}
if !addr.IsUnixNetwork() {
uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
}
}
var allowed []string
allowed := make([]string, 0, len(uniqueOrigins))
for origin := range uniqueOrigins {
allowed = append(allowed, origin)
}
@@ -198,14 +216,14 @@ func replaceAdmin(cfg *Config) error {
}
// extract a singular listener address
netw, addr, err := adminConfig.listenAddr()
addr, err := adminConfig.listenAddr()
if err != nil {
return err
}
handler := adminConfig.newAdminHandler(addr)
ln, err := Listen(netw, addr)
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
if err != nil {
return err
}
@@ -218,14 +236,22 @@ func replaceAdmin(cfg *Config) error {
MaxHeaderBytes: 1024 * 64,
}
go adminServer.Serve(ln)
adminLogger := Log().Named("admin")
go func() {
if err := adminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
}
}()
Log().Named("admin").Info(
"admin endpoint started",
zap.String("address", addr),
adminLogger.Info("admin endpoint started",
zap.String("address", addr.String()),
zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
zap.Strings("origins", handler.allowedOrigins),
)
zap.Strings("origins", handler.allowedOrigins))
if !handler.enforceHost {
adminLogger.Warn("admin endpoint on open interface; host checking disabled",
zap.String("address", addr.String()))
}
return nil
}
@@ -257,6 +283,7 @@ type AdminRoute struct {
type adminHandler struct {
enforceOrigin bool
enforceHost bool
allowedOrigins []string
mux *http.ServeMux
}
@@ -266,6 +293,7 @@ type adminHandler struct {
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Log().Named("admin.api").Info("received request",
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),
@@ -277,14 +305,24 @@ 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.enforceOrigin {
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
// restrictions, so we'll just be on the safe side
h.handleError(w, r, fmt.Errorf("websocket connections aren't allowed"))
return
}
if h.enforceHost {
// DNS rebinding mitigation
err := h.checkHost(r)
if err != nil {
h.handleError(w, r, err)
return
}
}
if h.enforceOrigin {
// cross-site mitigation
origin, err := h.checkOrigin(r)
if err != nil {
@@ -407,86 +445,6 @@ func (h adminHandler) originAllowed(origin string) bool {
return false
}
func handleLoad(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodPost {
return APIError{
Code: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed"),
}
}
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
_, err := io.Copy(buf, r.Body)
if err != nil {
return APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("reading request body: %v", err),
}
}
body := buf.Bytes()
// 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)
if err != nil {
return APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("invalid Content-Type: %v", err),
}
}
if !strings.HasSuffix(ct, "/json") {
slashIdx := strings.Index(ct, "/")
if slashIdx < 0 {
return APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("malformed Content-Type"),
}
}
adapterName := ct[slashIdx+1:]
cfgAdapter := caddyconfig.GetAdapter(adapterName)
if cfgAdapter == nil {
return APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("unrecognized config adapter '%s'", adapterName),
}
}
result, warnings, err := cfgAdapter.Adapt(body, nil)
if err != nil {
return APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("adapting config using %s adapter: %v", adapterName, err),
}
}
if len(warnings) > 0 {
respBody, err := json.Marshal(warnings)
if err != nil {
Log().Named("admin.api.load").Error(err.Error())
}
w.Write(respBody)
}
body = result
}
}
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
err = Load(body, forceReload)
if err != nil {
return APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("loading config: %v", err),
}
}
Log().Named("admin.api").Info("load complete")
return nil
}
func handleConfig(w http.ResponseWriter, r *http.Request) error {
switch r.Method {
case http.MethodGet:
@@ -577,16 +535,19 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
if err != nil {
Log().Named("admin.api").Error("unload error", zap.Error(err))
}
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)
}()
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
}
@@ -600,13 +561,6 @@ func handleUnload(w http.ResponseWriter, r *http.Request) error {
Err: fmt.Errorf("method not allowed"),
}
}
currentCfgMu.RLock()
hasCfg := currentCfg != nil
currentCfgMu.RUnlock()
if !hasCfg {
Log().Named("admin.api").Info("nothing to unload")
return nil
}
Log().Named("admin.api").Info("unloading")
if err := stopAndCleanup(); err != nil {
Log().Named("admin.api").Error("error unloading", zap.Error(err))
@@ -855,11 +809,26 @@ var (
}
)
// PIDFile writes a pidfile to the file at filename. It
// will get deleted before the process gracefully exits.
func PIDFile(filename string) error {
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
err := ioutil.WriteFile(filename, pid, 0644)
if err != nil {
return err
}
pidfile = filename
return nil
}
// idRegexp is used to match ID fields and their associated values
// in the config. It also matches adjacent commas so that syntax
// can be preserved no matter where in the object the field appears.
// It supports string and most numeric values.
var idRegexp = regexp.MustCompile(`(?m),?\s*"` + idKey + `":\s?(-?[0-9]+(\.[0-9]+)?|(?U)".*")\s*,?`)
var idRegexp = regexp.MustCompile(`(?m),?\s*"` + idKey + `"\s*:\s*(-?[0-9]+(\.[0-9]+)?|(?U)".*")\s*,?`)
// pidfile is the name of the pidfile, if any.
var pidfile string
const (
rawConfigKey = "config"
-240
View File
@@ -1,240 +0,0 @@
# Mutilated beyond recognition from the example at:
# https://docs.microsoft.com/azure/devops/pipelines/languages/go
trigger:
- v2
schedules:
- cron: "0 0 * * *"
displayName: Daily midnight fuzzing
branches:
include:
- v2
always: true
variables:
GOROOT: $(gorootDir)/go
GOPATH: $(system.defaultWorkingDirectory)/gopath
GOBIN: $(GOPATH)/bin
modulePath: '$(GOPATH)/src/github.com/$(build.repository.name)'
# TODO: Remove once it's enabled by default
GO111MODULE: on
jobs:
- job: crossPlatformTest
displayName: "Cross-Platform Tests"
strategy:
matrix:
linux:
imageName: ubuntu-16.04
gorootDir: /usr/local
mac:
imageName: macos-10.13
gorootDir: /usr/local
windows:
imageName: windows-2019
gorootDir: C:\
pool:
vmImage: $(imageName)
steps:
- bash: |
latestGo=$(curl "https://golang.org/VERSION?m=text")
echo "##vso[task.setvariable variable=LATEST_GO]$latestGo"
echo "Latest Go version: $latestGo"
displayName: "Get latest Go version"
- bash: |
sudo rm -f $(which go)
echo '##vso[task.prependpath]$(GOBIN)'
echo '##vso[task.prependpath]$(GOROOT)/bin'
mkdir -p '$(modulePath)'
shopt -s extglob
shopt -s dotglob
mv !(gopath) '$(modulePath)'
displayName: Remove old Go, set GOBIN/GOROOT, and move project into GOPATH
# Install Go (this varies by platform)
- bash: |
wget "https://dl.google.com/go/$(LATEST_GO).linux-amd64.tar.gz"
sudo tar -C $(gorootDir) -xzf "$(LATEST_GO).linux-amd64.tar.gz"
condition: eq( variables['Agent.OS'], 'Linux' )
displayName: Install Go on Linux
- bash: |
wget "https://dl.google.com/go/$(LATEST_GO).darwin-amd64.tar.gz"
sudo tar -C $(gorootDir) -xzf "$(LATEST_GO).darwin-amd64.tar.gz"
condition: eq( variables['Agent.OS'], 'Darwin' )
displayName: Install Go on macOS
# The low performance is partly due to PowerShell's attempt to update the progress bar. Disabling it speeds up the process.
# Reference: https://github.com/PowerShell/PowerShell/issues/2138
- powershell: |
$ProgressPreference = 'SilentlyContinue'
Write-Host "Downloading Go..."
(New-Object System.Net.WebClient).DownloadFile("https://dl.google.com/go/$(LATEST_GO).windows-amd64.zip", "$(LATEST_GO).windows-amd64.zip")
Write-Host "Extracting Go... (I'm slow too)"
7z x "$(LATEST_GO).windows-amd64.zip" -o"$(gorootDir)"
condition: eq( variables['Agent.OS'], 'Windows_NT' )
displayName: Install Go on Windows
- bash: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.22.2
displayName: Install golangci-lint
- script: |
go get github.com/axw/gocov/gocov
go get github.com/AlekSi/gocov-xml
go get -u github.com/jstemmer/go-junit-report
displayName: Install test and coverage analysis tools
- bash: |
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
displayName: Print Go version and environment
- script: |
go get -v -t -d ./...
mkdir test-results
workingDirectory: '$(modulePath)'
displayName: Get dependencies
# its behavior is governed by .golangci.yml
- script: |
(golangci-lint run --out-format junit-xml) > test-results/lint-result.xml
exit 0
workingDirectory: '$(modulePath)'
continueOnError: true
displayName: Run lint check
- script: |
(go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
workingDirectory: '$(modulePath)'
continueOnError: true
displayName: Run tests
- script: |
mkdir coverage
gocov convert cover-profile.out > coverage/coverage.json
# Because Windows doesn't work with input redirection like *nix, but output redirection works.
(cat ./coverage/coverage.json | gocov-xml) > coverage/coverage.xml
workingDirectory: '$(modulePath)'
displayName: Prepare coverage reports
- script: |
(cat ./test-results/test-result.out | go-junit-report) > test-results/test-result.xml
workingDirectory: '$(modulePath)'
displayName: Prepare test report
- task: PublishCodeCoverageResults@1
displayName: Publish test coverage report
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: $(modulePath)/coverage/coverage.xml
- task: PublishTestResults@2
displayName: Publish unit test
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: $(modulePath)/test-results/test-result.xml
testRunTitle: $(agent.OS) Unit Test
mergeTestResults: false
- task: PublishTestResults@2
displayName: Publish lint results
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: $(modulePath)/test-results/lint-result.xml
testRunTitle: $(agent.OS) Lint
mergeTestResults: false
- bash: |
exit 1
condition: eq(variables['Agent.JobStatus'], 'SucceededWithIssues')
displayName: Coerce correct build result
- job: fuzzing
displayName: 'Fuzzing'
# Only run this job on schedules or PRs for non-forks.
condition: or(eq(variables['System.PullRequest.IsFork'], 'False'), eq(variables['Build.Reason'], 'Schedule') )
strategy:
matrix:
linux:
imageName: ubuntu-16.04
gorootDir: /usr/local
pool:
vmImage: $(imageName)
steps:
- bash: |
latestGo=$(curl "https://golang.org/VERSION?m=text")
echo "##vso[task.setvariable variable=LATEST_GO]$latestGo"
echo "Latest Go version: $latestGo"
displayName: "Get latest Go version"
- bash: |
sudo rm -f $(which go)
echo '##vso[task.prependpath]$(GOBIN)'
echo '##vso[task.prependpath]$(GOROOT)/bin'
mkdir -p '$(modulePath)'
shopt -s extglob
shopt -s dotglob
mv !(gopath) '$(modulePath)'
displayName: Remove old Go, set GOBIN/GOROOT, and move project into GOPATH
- bash: |
wget "https://dl.google.com/go/$(LATEST_GO).linux-amd64.tar.gz"
sudo tar -C $(gorootDir) -xzf "$(LATEST_GO).linux-amd64.tar.gz"
condition: eq( variables['Agent.OS'], 'Linux' )
displayName: Install Go on Linux
- bash: |
# Install Clang-7.0 because other versions seem to be missing the file libclang_rt.fuzzer-x86_64.a
sudo add-apt-repository "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-7 main"
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
sudo apt update && sudo apt install -y clang-7 lldb-7 lld-7
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 $(GOBIN)
displayName: Download go-fuzz tools and the Fuzzit CLI, and move Fuzzit CLI to GOBIN
condition: and(eq(variables['System.PullRequest.IsFork'], 'False') , eq( variables['Agent.OS'], 'Linux' ))
- bash: |
declare -A fuzzers_funcs=(\
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="FuzzParseAddress" \
["./caddyconfig/caddyfile/parse_fuzz.go"]="FuzzParseCaddyfile" \
["./listeners_fuzz.go"]="FuzzParseNetworkAddress" \
["./replacer_fuzz.go"]="FuzzReplacer" \
)
declare -A fuzzers_targets=(\
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="parse-address" \
["./caddyconfig/caddyfile/parse_fuzz.go"]="parse-caddyfile" \
["./listeners_fuzz.go"]="parse-network-address" \
["./replacer_fuzz.go"]="replacer" \
)
fuzz_type="local-regression"
if [[ $(Build.Reason) == "Schedule" ]]; then
fuzz_type="fuzzing"
fi
echo "Fuzzing type: $fuzz_type"
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]}" -libfuzzer -o "$FUZZER_DIRECTORY/${fuzzers_targets[$f]}.a" $FUZZER_DIRECTORY
echo "Generating fuzzer binary of func ${fuzzers_funcs[$f]} which resides in $f"
clang-7 -fsanitize=fuzzer "$FUZZER_DIRECTORY/${fuzzers_targets[$f]}.a" -o "$FUZZER_DIRECTORY/${fuzzers_targets[$f]}"
fuzzit create job caddyserver/${fuzzers_targets[$f]} $FUZZER_DIRECTORY/${fuzzers_targets[$f]} --api-key ${FUZZIT_API_KEY} --type "${fuzz_type}" --branch "${SYSTEM_PULLREQUEST_SOURCEBRANCH}" --revision "${BUILD_SOURCEVERSION}"
echo "Completed $f"
done
env:
FUZZIT_API_KEY: $(FUZZIT_API_KEY)
workingDirectory: '$(modulePath)'
displayName: Generate fuzzers & submit them to Fuzzit
+41 -12
View File
@@ -32,7 +32,7 @@ import (
"sync"
"time"
"github.com/mholt/certmagic"
"github.com/caddyserver/certmagic"
"go.uber.org/zap"
)
@@ -190,7 +190,7 @@ func readConfig(path string, out io.Writer) error {
return unsyncedConfigAccess(http.MethodGet, path, nil, out)
}
// indexConfigObjects recurisvely searches ptr for object fields named
// indexConfigObjects recursively searches ptr for object fields named
// "@id" and maps that ID value to the full configPath in the index.
// This function is NOT safe for concurrent access; obtain a write lock
// on currentCfgMu.
@@ -372,7 +372,7 @@ func run(newCfg *Config, start bool) error {
}
if newCfg.storage == nil {
newCfg.storage = &certmagic.FileStorage{Path: AppDataDir()}
newCfg.storage = DefaultStorage
}
certmagic.Default.Storage = newCfg.storage
@@ -382,14 +382,12 @@ func run(newCfg *Config, start bool) error {
return err
}
// Load, Provision, Validate each app and their submodules
// Load and Provision each app and their submodules
err = func() error {
appsIface, err := ctx.LoadModule(newCfg, "AppsRaw")
if err != nil {
return fmt.Errorf("loading app modules: %v", err)
}
for appName, appIface := range appsIface.(map[string]interface{}) {
newCfg.apps[appName] = appIface.(App)
for appName := range newCfg.AppsRaw {
if _, err := ctx.App(appName); err != nil {
return err
}
}
return nil
}()
@@ -472,6 +470,9 @@ func stopAndCleanup() error {
return err
}
certmagic.CleanUpOwnLocks()
if pidfile != "" {
return os.Remove(pidfile)
}
return nil
}
@@ -488,7 +489,7 @@ func Validate(cfg *Config) error {
// 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`;
// valid units are `ns`, `us`/`µs`, `ms`, `s`, `m`, and `h`.
// valid units are `ns`, `us`/`µs`, `ms`, `s`, `m`, `h`, and `d`.
type Duration time.Duration
// UnmarshalJSON satisfies json.Unmarshaler.
@@ -499,7 +500,7 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
var dur time.Duration
var err error
if b[0] == byte('"') && b[len(b)-1] == byte('"') {
dur, err = time.ParseDuration(strings.Trim(string(b), `"`))
dur, err = ParseDuration(strings.Trim(string(b), `"`))
} else {
err = json.Unmarshal(b, &dur)
}
@@ -507,6 +508,34 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
return err
}
// ParseDuration parses a duration string, adding
// support for the "d" unit meaning number of days,
// where a day is assumed to be 24h.
func ParseDuration(s string) (time.Duration, error) {
var inNumber bool
var numStart int
for i := 0; i < len(s); i++ {
ch := s[i]
if ch == 'd' {
daysStr := s[numStart:i]
days, err := strconv.ParseFloat(daysStr, 64)
if err != nil {
return 0, err
}
hours := days * 24.0
hoursStr := strconv.FormatFloat(hours, 'f', -1, 64)
s = s[:numStart] + hoursStr + "h" + s[i+1:]
i--
continue
}
if !inNumber {
numStart = i
}
inNumber = (ch >= '0' && ch <= '9') || ch == '.' || ch == '-' || ch == '+'
}
return time.ParseDuration(s)
}
// GoModule returns the build info of this Caddy
// build from debug.BuildInfo (requires Go modules).
// If no version information is available, a non-nil
+74
View File
@@ -0,0 +1,74 @@
// 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 caddy
import (
"testing"
"time"
)
func TestParseDuration(t *testing.T) {
const day = 24 * time.Hour
for i, tc := range []struct {
input string
expect time.Duration
}{
{
input: "3h",
expect: 3 * time.Hour,
},
{
input: "1d",
expect: day,
},
{
input: "1d30m",
expect: day + 30*time.Minute,
},
{
input: "1m2d",
expect: time.Minute + day*2,
},
{
input: "1m2d30s",
expect: time.Minute + day*2 + 30*time.Second,
},
{
input: "1d2d",
expect: 3 * day,
},
{
input: "1.5d",
expect: time.Duration(1.5 * float64(day)),
},
{
input: "4m1.25d",
expect: 4*time.Minute + time.Duration(1.25*float64(day)),
},
{
input: "-1.25d12h",
expect: time.Duration(-1.25*float64(day)) - 12*time.Hour,
},
} {
actual, err := ParseDuration(tc.input)
if err != nil {
t.Errorf("Test %d ('%s'): Got error: %v", i, tc.input, err)
continue
}
if actual != tc.expect {
t.Errorf("Test %d ('%s'): Expected=%s Actual=%s", i, tc.input, tc.expect, actual)
}
}
}
+5
View File
@@ -68,6 +68,11 @@ func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []c
// into JSON. Caddyfile-unmarshaled values
// will not be used directly; they will be
// encoded as JSON and then used from that.
// Implementations must be able to support
// multiple segments (instances of their
// directive or batch of tokens); typically
// this means wrapping all token logic in
// a loop: `for d.Next() { ... }`.
type Unmarshaler interface {
UnmarshalCaddyfile(d *Dispenser) error
}
+23 -4
View File
@@ -17,6 +17,8 @@ package caddyfile
import (
"errors"
"fmt"
"io"
"log"
"strings"
)
@@ -37,6 +39,16 @@ func NewDispenser(tokens []Token) *Dispenser {
}
}
// NewTestDispenser parses input into tokens and creates a new
// Dispenser for test purposes only; any errors are fatal.
func NewTestDispenser(input string) *Dispenser {
tokens, err := allTokens("Testfile", []byte(input))
if err != nil && err != io.EOF {
log.Fatalf("getting all tokens from input: %v", err)
}
return NewDispenser(tokens)
}
// Next loads the next token. Returns true if a token
// was loaded; false otherwise. If false, all tokens
// have been consumed.
@@ -249,13 +261,20 @@ func (d *Dispenser) RemainingArgs() []string {
return args
}
// NewFromNextTokens returns a new dispenser with a copy of
// NewFromNextSegment returns a new dispenser with a copy of
// the tokens from the current token until the end of the
// "directive" whether that be to the end of the line or
// the end of a block that starts at the end of the line;
// in other words, until the end of the segment.
func (d *Dispenser) NewFromNextTokens() *Dispenser {
tkns := []Token{d.Token()}
func (d *Dispenser) NewFromNextSegment() *Dispenser {
return NewDispenser(d.NextSegment())
}
// NextSegment returns a copy of the tokens from the current
// token until the end of the line or block that starts at
// the end of the line.
func (d *Dispenser) NextSegment() Segment {
tkns := Segment{d.Token()}
for d.NextArg() {
tkns = append(tkns, d.Token())
}
@@ -282,7 +301,7 @@ func (d *Dispenser) NewFromNextTokens() *Dispenser {
// next iteration of the enclosing loop will
// call Next() and consume it
}
return NewDispenser(tkns)
return tkns
}
// Token returns the current token.
+7 -17
View File
@@ -15,8 +15,6 @@
package caddyfile
import (
"io"
"log"
"reflect"
"strings"
"testing"
@@ -27,7 +25,7 @@ func TestDispenser_Val_Next(t *testing.T) {
dir1 arg1
dir2 arg2 arg3
dir3`
d := newTestDispenser(input)
d := NewTestDispenser(input)
if val := d.Val(); val != "" {
t.Fatalf("Val(): Should return empty string when no token loaded; got '%s'", val)
@@ -65,7 +63,7 @@ func TestDispenser_NextArg(t *testing.T) {
input := `dir1 arg1
dir2 arg2 arg3
dir3`
d := newTestDispenser(input)
d := NewTestDispenser(input)
assertNext := func(shouldLoad bool, expectedVal string, expectedCursor int) {
if d.Next() != shouldLoad {
@@ -112,7 +110,7 @@ func TestDispenser_NextLine(t *testing.T) {
input := `host:port
dir1 arg1
dir2 arg2 arg3`
d := newTestDispenser(input)
d := NewTestDispenser(input)
assertNextLine := func(shouldLoad bool, expectedVal string, expectedCursor int) {
if d.NextLine() != shouldLoad {
@@ -145,7 +143,7 @@ func TestDispenser_NextBlock(t *testing.T) {
}
foobar2 {
}`
d := newTestDispenser(input)
d := NewTestDispenser(input)
assertNextBlock := func(shouldLoad bool, expectedCursor, expectedNesting int) {
if loaded := d.NextBlock(0); loaded != shouldLoad {
@@ -175,7 +173,7 @@ func TestDispenser_Args(t *testing.T) {
dir2 arg4 arg5
dir3 arg6 arg7
dir4`
d := newTestDispenser(input)
d := NewTestDispenser(input)
d.Next() // dir1
@@ -242,7 +240,7 @@ func TestDispenser_RemainingArgs(t *testing.T) {
dir2 arg4 arg5
dir3 arg6 { arg7
dir4`
d := newTestDispenser(input)
d := NewTestDispenser(input)
d.Next() // dir1
@@ -279,7 +277,7 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
input := `dir1 {
}
dir2 arg1 arg2`
d := newTestDispenser(input)
d := NewTestDispenser(input)
d.cursor = 1 // {
@@ -306,11 +304,3 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
}
}
func newTestDispenser(input string) *Dispenser {
tokens, err := allTokens("Testfile", []byte(input))
if err != nil && err != io.EOF {
log.Fatalf("getting all tokens from input: %v", err)
}
return NewDispenser(tokens)
}
+213
View File
@@ -0,0 +1,213 @@
// 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 (
"bytes"
"io"
"unicode"
)
// Format formats the input Caddyfile to a standard, nice-looking
// appearance. It works by reading each rune of the input and taking
// control over all the bracing and whitespace that is written; otherwise,
// words, comments, placeholders, and escaped characters are all treated
// literally and written as they appear in the input.
func Format(input []byte) []byte {
input = bytes.TrimSpace(input)
out := new(bytes.Buffer)
rdr := bytes.NewReader(input)
var (
last rune // the last character that was written to the result
space = true // whether current/previous character was whitespace (beginning of input counts as space)
beginningOfLine = true // whether we are at beginning of line
openBrace bool // whether current word/token is or started with open curly brace
openBraceWritten bool // if openBrace, whether that brace was written or not
openBraceSpace bool // whether there was a non-newline space before open brace
newLines int // count of newlines consumed
comment bool // whether we're in a comment
quoted bool // whether we're in a quoted segment
escaped bool // whether current char is escaped
nesting int // indentation level
)
write := func(ch rune) {
out.WriteRune(ch)
last = ch
}
indent := func() {
for tabs := nesting; tabs > 0; tabs-- {
write('\t')
}
}
nextLine := func() {
write('\n')
beginningOfLine = true
}
for {
ch, _, err := rdr.ReadRune()
if err != nil {
if err == io.EOF {
break
}
panic(err)
}
if comment {
if ch == '\n' {
comment = false
} else {
write(ch)
continue
}
}
if !escaped && ch == '\\' {
if space {
write(' ')
space = false
}
write(ch)
escaped = true
continue
}
if escaped {
write(ch)
escaped = false
continue
}
if quoted {
if ch == '"' {
quoted = false
}
write(ch)
continue
}
if space && ch == '"' {
quoted = true
}
if unicode.IsSpace(ch) {
space = true
if ch == '\n' {
newLines++
}
continue
}
spacePrior := space
space = false
//////////////////////////////////////////////////////////
// I find it helpful to think of the formatting loop in two
// main sections; by the time we reach this point, we
// know we are in a "regular" part of the file: we know
// the character is not a space, not in a literal segment
// like a comment or quoted, it's not escaped, etc.
//////////////////////////////////////////////////////////
if ch == '#' {
comment = true
}
if openBrace && spacePrior && !openBraceWritten {
if nesting == 0 && last == '}' {
nextLine()
nextLine()
}
openBrace = false
if beginningOfLine {
indent()
} else if !openBraceSpace {
write(' ')
}
write('{')
openBraceWritten = true
nextLine()
newLines = 0
nesting++
}
switch {
case ch == '{':
openBrace = true
openBraceWritten = false
openBraceSpace = spacePrior && !beginningOfLine
if openBraceSpace {
write(' ')
}
continue
case ch == '}' && (spacePrior || !openBrace):
if last != '\n' {
nextLine()
}
if nesting > 0 {
nesting--
}
indent()
write('}')
newLines = 0
continue
}
if newLines > 2 {
newLines = 2
}
for i := 0; i < newLines; i++ {
nextLine()
}
newLines = 0
if beginningOfLine {
indent()
}
if nesting == 0 && last == '}' && beginningOfLine {
nextLine()
nextLine()
}
if !beginningOfLine && spacePrior {
write(' ')
}
if openBrace && !openBraceWritten {
write('{')
openBraceWritten = true
}
write(ch)
beginningOfLine = false
}
// the Caddyfile does not need any leading or trailing spaces, but...
trimmedResult := bytes.TrimSpace(out.Bytes())
// ...Caddyfiles should, however, end with a newline because
// newlines are significant to the syntax of the file
return append(trimmedResult, '\n')
}
+327
View File
@@ -0,0 +1,327 @@
// 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 (
"strings"
"testing"
)
func TestFormatter(t *testing.T) {
for i, tc := range []struct {
description string
input string
expect string
}{
{
description: "very simple",
input: `abc def
g hi jkl
mn`,
expect: `abc def
g hi jkl
mn`,
},
{
description: "basic indentation, line breaks, and nesting",
input: ` a
b
c {
d
}
e { f
}
g {
h {
i
}
}
j { k {
l
}
}
m {
n { o
}
p { q r
s }
}
{
{ t
u
v
w
}
}`,
expect: `a
b
c {
d
}
e {
f
}
g {
h {
i
}
}
j {
k {
l
}
}
m {
n {
o
}
p {
q r
s
}
}
{
{
t
u
v
w
}
}`,
},
{
description: "block spacing",
input: `a{
b
}
c{ d
}`,
expect: `a {
b
}
c {
d
}`,
},
{
description: "advanced spacing",
input: `abc {
def
}ghi{
jkl mno
pqr}`,
expect: `abc {
def
}
ghi {
jkl mno
pqr
}`,
},
{
description: "env var placeholders",
input: `{$A}
b {
{$C}
}
d { {$E}
}
{ {$F}
}
`,
expect: `{$A}
b {
{$C}
}
d {
{$E}
}
{
{$F}
}`,
},
{
description: "comments",
input: `#a "\n"
#b {
c
}
d {
e#f
# g
}
h { # i
}`,
expect: `#a "\n"
#b {
c
}
d {
e#f
# g
}
h {
# i
}`,
},
{
description: "quotes and escaping",
input: `"a \"b\" "#c
d
e {
"f"
}
g { "h"
}
i {
"foo
bar"
}
j {
"\"k\" l m"
}`,
expect: `"a \"b\" "#c
d
e {
"f"
}
g {
"h"
}
i {
"foo
bar"
}
j {
"\"k\" l m"
}`,
},
{
description: "bad nesting (too many open)",
input: `a
{
{
}`,
expect: `a {
{
}
`,
},
{
description: "bad nesting (too many close)",
input: `a
{
{
}}}`,
expect: `a {
{
}
}
}
`,
},
{
description: "json",
input: `foo
bar "{\"key\":34}"
`,
expect: `foo
bar "{\"key\":34}"`,
},
{
description: "escaping after spaces",
input: `foo \"literal\"`,
expect: `foo \"literal\"`,
},
{
description: "simple placeholders as standalone tokens",
input: `foo {bar}`,
expect: `foo {bar}`,
},
{
description: "simple placeholders within tokens",
input: `foo{bar} foo{bar}baz`,
expect: `foo{bar} foo{bar}baz`,
},
{
description: "placeholders and malformed braces",
input: `foo{bar} foo{ bar}baz`,
expect: `foo{bar} foo {
bar
}
baz`,
},
{
description: "hash within string is not a comment",
input: `redir / /some/#/path`,
expect: `redir / /some/#/path`,
},
} {
// the formatter should output a trailing newline,
// even if the tests aren't written to expect that
if !strings.HasSuffix(tc.expect, "\n") {
tc.expect += "\n"
}
actual := Format([]byte(tc.input))
if string(actual) != tc.expect {
t.Errorf("\n[TEST %d: %s]\n====== EXPECTED ======\n%s\n====== ACTUAL ======\n%s^^^^^^^^^^^^^^^^^^^^^",
i, tc.description, string(tc.expect), string(actual))
}
}
}
+32 -6
View File
@@ -16,6 +16,7 @@ package caddyfile
import (
"bufio"
"bytes"
"io"
"unicode"
)
@@ -73,7 +74,7 @@ func (l *lexer) load(input io.Reader) error {
// a token was loaded; false otherwise.
func (l *lexer) next() bool {
var val []rune
var comment, quoted, escaped bool
var comment, quoted, btQuoted, escaped bool
makeToken := func() bool {
l.token.Text = string(val)
@@ -92,13 +93,13 @@ func (l *lexer) next() bool {
panic(err)
}
if !escaped && ch == '\\' {
if !escaped && !btQuoted && ch == '\\' {
escaped = true
continue
}
if quoted {
if escaped {
if quoted || btQuoted {
if quoted && escaped {
// all is literal in quoted area,
// so only escape quotes
if ch != '"' {
@@ -106,7 +107,10 @@ func (l *lexer) next() bool {
}
escaped = false
} else {
if ch == '"' {
if quoted && ch == '"' {
return makeToken()
}
if btQuoted && ch == '`' {
return makeToken()
}
}
@@ -138,7 +142,7 @@ func (l *lexer) next() bool {
continue
}
if ch == '#' {
if ch == '#' && len(val) == 0 {
comment = true
}
if comment {
@@ -151,6 +155,10 @@ func (l *lexer) next() bool {
quoted = true
continue
}
if ch == '`' {
btQuoted = true
continue
}
}
if escaped {
@@ -161,3 +169,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
}
+82 -40
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,8 +76,28 @@ func TestLexer(t *testing.T) {
},
},
{
input: `a "quoted value" b
foobar`,
input: []byte(`host:123 {
# hash inside string is not a comment
redir / /some/#/path
}`),
expected: []Token{
{Line: 1, Text: "host:123"},
{Line: 1, Text: "{"},
{Line: 3, Text: "redir"},
{Line: 3, Text: "/"},
{Line: 3, Text: "/some/#/path"},
{Line: 4, Text: "}"},
},
},
{
input: []byte("# comment at beginning of file\n# comment at beginning of line\nhost:123"),
expected: []Token{
{Line: 3, Text: "host:123"},
},
},
{
input: []byte(`a "quoted value" b
foobar`),
expected: []Token{
{Line: 1, Text: "a"},
{Line: 1, Text: "quoted value"},
@@ -88,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`},
@@ -96,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"},
@@ -105,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"},
@@ -115,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"},
@@ -124,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"},
@@ -134,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"},
@@ -171,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: ``},
@@ -185,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"},
@@ -194,30 +212,54 @@ 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: []byte("simple `backtick quoted` string"),
expected: []Token{
{Line: 1, Text: `simple`},
{Line: 1, Text: `backtick quoted`},
{Line: 1, Text: `string`},
},
},
{
input: []byte("multiline `backtick\nquoted\n` string"),
expected: []Token{
{Line: 1, Text: `multiline`},
{Line: 1, Text: "backtick\nquoted\n"},
{Line: 3, Text: `string`},
},
},
{
input: []byte("nested `\"quotes inside\" backticks` string"),
expected: []Token{
{Line: 1, Text: `nested`},
{Line: 1, Text: `"quotes inside" backticks`},
{Line: 1, Text: `string`},
},
},
{
input: []byte("reverse-nested \"`backticks` inside\" quotes"),
expected: []Token{
{Line: 1, Text: `reverse-nested`},
{Line: 1, Text: "`backticks` inside"},
{Line: 1, Text: `quotes`},
},
},
}
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))
+29 -14
View File
@@ -20,7 +20,10 @@ import (
"log"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2"
)
// Parse parses the input just enough to group tokens, in
@@ -64,7 +67,7 @@ func replaceEnvVars(input []byte) ([]byte, error) {
}
// get the value of the environment variable
envVarValue := []byte(os.Getenv(string(envVarName)))
envVarValue := []byte(os.ExpandEnv(os.Getenv(string(envVarName))))
// splice in the value
input = append(input[:begin],
@@ -84,16 +87,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
}
@@ -292,11 +289,19 @@ func (p *parser) doImport() error {
if importPattern == "" {
return p.Err("Import requires a non-empty filepath")
}
if p.NextArg() {
return p.Err("Import takes only one argument (glob pattern or file)")
// grab remaining args as placeholder replacements
args := p.RemainingArgs()
// add args to the replacer
repl := caddy.NewReplacer()
for index, arg := range args {
repl.Set("args."+strconv.Itoa(index), arg)
}
// splice out the import directive and its argument (2 tokens total)
tokensBefore := p.tokens[:p.cursor-1]
// splice out the import directive and its arguments
// (2 tokens, plus the length of args)
tokensBefore := p.tokens[:p.cursor-1-len(args)]
tokensAfter := p.tokens[p.cursor+1:]
var importedTokens []Token
@@ -348,10 +353,20 @@ func (p *parser) doImport() error {
}
}
// copy the tokens so we don't overwrite p.definedSnippets
tokensCopy := make([]Token, len(importedTokens))
copy(tokensCopy, importedTokens)
// run the argument replacer on the tokens
for index, token := range tokensCopy {
token.Text = repl.ReplaceKnown(token.Text, "")
tokensCopy[index] = token
}
// splice the imported tokens in the place of the import statement
// and rewind cursor so Next() will land on first imported token
p.tokens = append(tokensBefore, append(importedTokens, tokensAfter...)...)
p.cursor--
p.tokens = append(tokensBefore, append(tokensCopy, tokensAfter...)...)
p.cursor -= len(args) + 1
return nil
}
+9 -17
View File
@@ -182,14 +182,17 @@ func TestParseOneAndImport(t *testing.T) {
"host1",
}, []int{1, 2}},
{`import testdata/import_test1.txt testdata/import_test2.txt`, true, []string{}, []int{}},
{`import testdata/not_found.txt`, true, []string{}, []int{}},
{`""`, false, []string{}, []int{}},
{``, false, []string{}, []int{}},
// import with args
{`import testdata/import_args0.txt a`, false, []string{"a"}, []int{}},
{`import testdata/import_args1.txt a b`, false, []string{"a", "b"}, []int{}},
{`import testdata/import_args*.txt a b`, false, []string{"a"}, []int{2}},
// test cases found by fuzzing!
{`import }{$"`, true, []string{}, []int{}},
{`import /*/*.txt`, true, []string{}, []int{}},
@@ -210,6 +213,7 @@ func TestParseOneAndImport(t *testing.T) {
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
}
// t.Logf("%+v\n", result)
if len(result.Keys) != len(test.keys) {
t.Errorf("Test %d: Expected %d keys, got %d",
i, len(test.keys), len(result.Keys))
@@ -256,7 +260,7 @@ func TestRecursiveImport(t *testing.T) {
return false
}
if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 2 {
t.Errorf("got unexpect tokens: %v", got.Segments)
t.Errorf("got unexpected tokens: %v", got.Segments)
return false
}
return true
@@ -351,7 +355,7 @@ func TestDirectiveImport(t *testing.T) {
return false
}
if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 8 {
t.Errorf("got unexpect tokens: %v", got.Segments)
t.Errorf("got unexpected tokens: %v", got.Segments)
return false
}
return true
@@ -568,10 +572,6 @@ func TestSnippets(t *testing.T) {
if err != nil {
t.Fatal(err)
}
for _, b := range blocks {
t.Log(b.Keys)
t.Log(b.Segments)
}
if len(blocks) != 1 {
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
}
@@ -616,10 +616,6 @@ func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) {
if err != nil {
t.Fatal(err)
}
for _, b := range blocks {
t.Log(b.Keys)
t.Log(b.Segments)
}
auth := blocks[0].Segments[0]
line := auth[0].Text + " " + auth[1].Text + " " + auth[2].Text + " " + auth[3].Text
if line != "basicauth / import password" {
@@ -651,10 +647,6 @@ func TestSnippetAcrossMultipleFiles(t *testing.T) {
if err != nil {
t.Fatal(err)
}
for _, b := range blocks {
t.Log(b.Keys)
t.Log(b.Segments)
}
if len(blocks) != 1 {
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
}
@@ -670,5 +662,5 @@ func TestSnippetAcrossMultipleFiles(t *testing.T) {
}
func testParser(input string) parser {
return parser{Dispenser: newTestDispenser(input)}
return parser{Dispenser: NewTestDispenser(input)}
}
+1
View File
@@ -0,0 +1 @@
{args.0}
+1
View File
@@ -0,0 +1 @@
{args.0} {args.1}
+31 -12
View File
@@ -17,6 +17,8 @@ package caddyconfig
import (
"encoding/json"
"fmt"
"github.com/caddyserver/caddy/v2"
)
// Adapter is a type which can adapt a configuration to Caddy JSON.
@@ -49,14 +51,13 @@ func JSON(val interface{}, warnings *[]Warning) json.RawMessage {
return b
}
// JSONModuleObject is like JSON, except it marshals val into a JSON object
// and then adds a key to that object named fieldName with the value fieldVal.
// This is useful for JSON-encoding module values where the module name has to
// be described within the object by a certain key; for example,
// "responder": "file_server" for a file server HTTP responder. The val must
// encode into a map[string]interface{} (i.e. it must be a struct or map),
// and any errors are converted into warnings, so this can be conveniently
// used when filling a struct. For correct code, there should be no errors.
// JSONModuleObject is like JSON(), except it marshals val into a JSON object
// with an added key named fieldName with the value fieldVal. This is useful
// for encoding module values where the module name has to be described within
// the object by a certain key; for example, `"handler": "file_server"` for a
// file server HTTP handler (fieldName="handler" and fieldVal="file_server").
// The val parameter must encode into a map[string]interface{} (i.e. it must be
// a struct or map). Any errors are converted into warnings.
func JSONModuleObject(val interface{}, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage {
// encode to a JSON object first
enc, err := json.Marshal(val)
@@ -99,13 +100,14 @@ func JSONIndent(val interface{}) ([]byte, error) {
}
// RegisterAdapter registers a config adapter with the given name.
// This should usually be done at init-time.
func RegisterAdapter(name string, adapter Adapter) error {
// This should usually be done at init-time. It panics if the
// adapter cannot be registered successfully.
func RegisterAdapter(name string, adapter Adapter) {
if _, ok := configAdapters[name]; ok {
return fmt.Errorf("%s: already registered", name)
panic(fmt.Errorf("%s: already registered", name))
}
configAdapters[name] = adapter
return nil
caddy.RegisterModule(adapterModule{name, adapter})
}
// GetAdapter returns the adapter with the given name,
@@ -114,4 +116,21 @@ func GetAdapter(name string) Adapter {
return configAdapters[name]
}
// adapterModule is a wrapper type that can turn any config
// adapter into a Caddy module, which has the benefit of being
// counted with other modules, even though they do not
// technically extend the Caddy configuration structure.
// See caddyserver/caddy#3132.
type adapterModule struct {
name string
Adapter
}
func (am adapterModule) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: caddy.ModuleID("caddy.adapters." + am.name),
New: func() caddy.Module { return am },
}
}
var configAdapters = make(map[string]Adapter)
+58 -32
View File
@@ -20,10 +20,12 @@ import (
"reflect"
"strconv"
"strings"
"unicode"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/mholt/certmagic"
"github.com/caddyserver/certmagic"
)
// mapAddressToServerBlocks returns a map of listener address to list of server
@@ -45,7 +47,7 @@ import (
// key of its server block (specifying the host part), and each key may have
// a different port. And we definitely need to be sure that a site which is
// bound to be served on a specific interface is not served on others just
// beceause that is more convenient: it would be a potential security risk
// because that is more convenient: it would be a potential security risk
// if the difference between interfaces means private vs. public.
//
// So what this function does for the example above is iterate each server
@@ -105,12 +107,22 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBloc
// server block are only the ones which use the address; but
// the contents (tokens) are of course the same
for addr, keys := range addrToKeys {
// parse keys so that we only have to do it once
parsedKeys := make([]Address, 0, len(keys))
for _, key := range keys {
addr, err := ParseAddress(key)
if err != nil {
return nil, fmt.Errorf("parsing key '%s': %v", key, err)
}
parsedKeys = append(parsedKeys, addr.Normalize())
}
sbmap[addr] = append(sbmap[addr], serverBlock{
block: caddyfile.ServerBlock{
Keys: keys,
Segments: sblock.block.Segments,
},
pile: sblock.pile,
keys: parsedKeys,
})
}
}
@@ -127,7 +139,7 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBloc
// association from multiple addresses to multiple server blocks; i.e. each element of
// the returned slice) becomes a server definition in the output JSON.
func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]serverBlock) []sbAddrAssociation {
var sbaddrs []sbAddrAssociation
sbaddrs := make([]sbAddrAssociation, 0, len(addrToServerBlocks))
for addr, sblocks := range addrToServerBlocks {
// we start with knowing that at least this address
// maps to these server blocks
@@ -164,7 +176,7 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
// figure out the HTTP and HTTPS ports; either
// use defaults, or override with user config
httpPort, httpsPort := strconv.Itoa(certmagic.HTTPPort), strconv.Itoa(certmagic.HTTPSPort)
httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort)
if hport, ok := options["http_port"]; ok {
httpPort = strconv.Itoa(hport.(int))
}
@@ -172,20 +184,14 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
httpsPort = strconv.Itoa(hsport.(int))
}
lnPort := DefaultPort
// default port is the HTTPS port
lnPort := httpsPort
if addr.Port != "" {
// port explicitly defined
lnPort = addr.Port
} else if addr.Scheme != "" {
} else if addr.Scheme == "http" {
// port inferred from scheme
if addr.Scheme == "http" {
lnPort = httpPort
} else if addr.Scheme == "https" {
lnPort = httpsPort
}
} else if certmagic.HostQualifies(addr.Host) {
// automatic HTTPS
lnPort = httpsPort
lnPort = httpPort
}
// error if scheme and port combination violate convention
@@ -194,7 +200,7 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
}
// the bind directive specifies hosts, but is optional
var lnHosts []string
lnHosts := make([]string, 0, len(sblock.pile))
for _, cfgVal := range sblock.pile["bind"] {
lnHosts = append(lnHosts, cfgVal.Value.([]string)...)
}
@@ -205,15 +211,19 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
// use a map to prevent duplication
listeners := make(map[string]struct{})
for _, host := range lnHosts {
listeners[net.JoinHostPort(host, lnPort)] = struct{}{}
addr, err := caddy.ParseNetworkAddress(host)
if err == nil && addr.IsUnixNetwork() {
listeners[host] = struct{}{}
} else {
listeners[net.JoinHostPort(host, lnPort)] = struct{}{}
}
}
// now turn map into list
var listenersList []string
listenersList := make([]string, 0, len(listeners))
for lnStr := range listeners {
listenersList = append(listenersList, lnStr)
}
// sort.Strings(listenersList) // TODO: is sorting necessary?
return listenersList, nil
}
@@ -281,8 +291,6 @@ func ParseAddress(str string) (Address, error) {
return a, nil
}
// TODO: which of the methods on Address are even used?
// String returns a human-readable form of a. It will
// be a cleaned-up and filled-out URL string.
func (a Address) String() string {
@@ -317,20 +325,17 @@ func (a Address) String() string {
// Normalize returns a normalized version of a.
func (a Address) Normalize() Address {
path := a.Path
if !caseSensitivePath {
path = strings.ToLower(path)
}
// ensure host is normalized if it's an IP address
host := a.Host
host := strings.TrimSpace(a.Host)
if ip := net.ParseIP(host); ip != nil {
host = ip.String()
}
return Address{
Original: a.Original,
Scheme: strings.ToLower(a.Scheme),
Host: strings.ToLower(host),
Scheme: lowerExceptPlaceholders(a.Scheme),
Host: lowerExceptPlaceholders(host),
Port: a.Port,
Path: path,
}
@@ -358,9 +363,30 @@ func (a Address) Key() string {
return res
}
const (
// DefaultPort is the default port to use.
DefaultPort = "2015"
caseSensitivePath = false // TODO: Used?
)
// lowerExceptPlaceholders lowercases s except within
// placeholders (substrings in non-escaped '{ }' spans).
// See https://github.com/caddyserver/caddy/issues/3264
func lowerExceptPlaceholders(s string) string {
var sb strings.Builder
var escaped, inPlaceholder bool
for _, ch := range s {
if ch == '\\' && !escaped {
escaped = true
sb.WriteRune(ch)
continue
}
if ch == '{' && !escaped {
inPlaceholder = true
}
if ch == '}' && inPlaceholder && !escaped {
inPlaceholder = false
}
if inPlaceholder {
sb.WriteRune(ch)
} else {
sb.WriteRune(unicode.ToLower(ch))
}
escaped = false
}
return sb.String()
}
@@ -13,7 +13,6 @@
// limitations under the License.
// +build gofuzz
// +build gofuzz_libfuzzer
package httpcaddyfile
+22 -10
View File
@@ -1,7 +1,6 @@
package httpcaddyfile
import (
"strings"
"testing"
)
@@ -109,6 +108,10 @@ func TestKeyNormalization(t *testing.T) {
input string
expect string
}{
{
input: "example.com",
expect: "example.com",
},
{
input: "http://host:1234/path",
expect: "http://host:1234/path",
@@ -125,6 +128,22 @@ func TestKeyNormalization(t *testing.T) {
input: "A:2015/Path",
expect: "a:2015/Path",
},
{
input: "sub.{env.MY_DOMAIN}",
expect: "sub.{env.MY_DOMAIN}",
},
{
input: "sub.ExAmPle",
expect: "sub.example",
},
{
input: "sub.\\{env.MY_DOMAIN\\}",
expect: "sub.\\{env.my_domain\\}",
},
{
input: "sub.{env.MY_DOMAIN}.com",
expect: "sub.{env.MY_DOMAIN}.com",
},
{
input: ":80",
expect: ":80",
@@ -156,15 +175,8 @@ func TestKeyNormalization(t *testing.T) {
t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err)
continue
}
expect := tc.expect
if !caseSensitivePath {
// every other part of the address should be lowercased when normalized,
// so simply lower-case the whole thing to do case-insensitive comparison
// of the path as well
expect = strings.ToLower(expect)
}
if actual := addr.Normalize().Key(); actual != expect {
t.Errorf("Test %d: Normalized key for address '%s' was '%s' but expected '%s'", i, tc.input, actual, expect)
if actual := addr.Normalize().Key(); actual != tc.expect {
t.Errorf("Test %d: Input '%s': Expected '%s' but got '%s'", i, tc.input, tc.expect, actual)
}
}
+419 -159
View File
@@ -15,27 +15,35 @@
package httpcaddyfile
import (
"encoding/json"
"encoding/base64"
"encoding/pem"
"fmt"
"html"
"io/ioutil"
"net/http"
"reflect"
"strings"
"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/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
"go.uber.org/zap/zapcore"
)
func init() {
RegisterDirective("bind", parseBind)
RegisterDirective("root", parseRoot) // TODO: isn't this a handler directive?
RegisterDirective("tls", parseTLS)
RegisterHandlerDirective("root", parseRoot)
RegisterHandlerDirective("redir", parseRedir)
RegisterHandlerDirective("respond", parseRespond)
RegisterHandlerDirective("route", parseRoute)
RegisterHandlerDirective("handle", parseHandle)
RegisterDirective("handle_errors", parseHandleErrors)
RegisterDirective("log", parseLog)
}
// parseBind parses the bind directive. Syntax:
@@ -50,72 +58,37 @@ func parseBind(h Helper) ([]ConfigValue, error) {
return h.NewBindAddresses(lnHosts), nil
}
// parseRoot parses the root directive. Syntax:
//
// root [<matcher>] <path>
//
func parseRoot(h Helper) ([]ConfigValue, error) {
if !h.Next() {
return nil, h.ArgErr()
}
matcherSet, ok, err := h.MatcherToken()
if err != nil {
return nil, err
}
if !ok {
// no matcher token; oops
h.Dispenser.Prev()
}
if !h.NextArg() {
return nil, h.ArgErr()
}
root := h.Val()
if h.NextArg() {
return nil, h.ArgErr()
}
varsHandler := caddyhttp.VarsMiddleware{"root": root}
route := caddyhttp.Route{
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(varsHandler, "handler", "vars", nil),
},
}
if matcherSet != nil {
route.MatcherSetsRaw = []caddy.ModuleMap{matcherSet}
}
return []ConfigValue{{Class: "route", Value: route}}, nil
}
// parseTLS parses the tls directive. Syntax:
//
// tls [<email>]|[<cert_file> <key_file>] {
// tls [<email>|internal]|[<cert_file> <key_file>] {
// protocols <min> [<max>]
// ciphers <cipher_suites...>
// curves <curves...>
// client_auth {
// mode [request|require|verify_if_given|require_and_verify]
// trusted_ca_cert <base64_der>
// trusted_ca_cert_file <filename>
// trusted_leaf_cert <base64_der>
// trusted_leaf_cert_file <filename>
// }
// alpn <values...>
// load <paths...>
// ca <acme_ca_endpoint>
// ca_root <pem_file>
// dns <provider_name>
// on_demand
// issuer <module_name> ...
// }
//
func parseTLS(h Helper) ([]ConfigValue, error) {
var configVals []ConfigValue
var cp *caddytls.ConnectionPolicy
cp := new(caddytls.ConnectionPolicy)
var fileLoader caddytls.FileLoader
var folderLoader caddytls.FolderLoader
var mgr caddytls.ACMEManagerMaker
// fill in global defaults, if configured
if email := h.Option("email"); email != nil {
mgr.Email = email.(string)
}
if acmeCA := h.Option("acme_ca"); acmeCA != nil {
mgr.CA = acmeCA.(string)
}
var certSelector caddytls.CustomCertSelectionPolicy
var acmeIssuer *caddytls.ACMEIssuer
var internalIssuer *caddytls.InternalIssuer
var issuer certmagic.Issuer
var onDemand bool
for h.Next() {
// file certificate loader
@@ -123,36 +96,71 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
switch len(firstLine) {
case 0:
case 1:
if !strings.Contains(firstLine[0], "@") {
return nil, h.Err("single argument must be an email address")
if firstLine[0] == "internal" {
internalIssuer = new(caddytls.InternalIssuer)
} else if !strings.Contains(firstLine[0], "@") {
return nil, h.Err("single argument must either be 'internal' or an email address")
} else {
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
acmeIssuer.Email = firstLine[0]
}
mgr.Email = firstLine[0]
case 2:
tag := fmt.Sprintf("cert%d", tagCounter)
fileLoader = append(fileLoader, caddytls.CertKeyFilePair{
Certificate: firstLine[0],
Key: firstLine[1],
Tags: []string{tag},
})
certFilename := firstLine[0]
keyFilename := firstLine[1]
// tag this certificate so if multiple certs match, specifically
// this one that the user has provided will be used, see #2588:
// https://github.com/caddyserver/caddy/issues/2588
tagCounter++
certSelector := caddytls.CustomCertSelectionPolicy{Tag: tag}
if cp == nil {
cp = new(caddytls.ConnectionPolicy)
// https://github.com/caddyserver/caddy/issues/2588 ... but we
// must be careful about how we do this; being careless will
// lead to failed handshakes
//
// we need to remember which cert files we've seen, since we
// 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)
//
// tlsCertTags maps certificate filenames to their tag.
// This is used to remember which tag is used for each
// certificate files, since we need to avoid loading
// the same certificate files more than once, overwriting
// previous tags
tlsCertTags, ok := h.State["tlsCertTags"].(map[string]string)
if !ok {
tlsCertTags = make(map[string]string)
h.State["tlsCertTags"] = tlsCertTags
}
cp.CertSelection = caddyconfig.JSONModuleObject(certSelector, "policy", "custom", h.warnings)
tag, ok := tlsCertTags[certFilename]
if !ok {
// haven't seen this cert file yet, let's give it a tag
// and add a loader for it
tag = fmt.Sprintf("cert%d", len(tlsCertTags))
fileLoader = append(fileLoader, caddytls.CertKeyFilePair{
Certificate: certFilename,
Key: keyFilename,
Tags: []string{tag},
})
// remember this for next time we see this cert file
tlsCertTags[certFilename] = tag
}
certSelector.AnyTag = append(certSelector.AnyTag, tag)
default:
return nil, h.ArgErr()
}
var hasBlock bool
for h.NextBlock(0) {
for nesting := h.Nesting(); h.NextBlock(nesting); {
hasBlock = true
switch h.Val() {
// connection policy
case "protocols":
args := h.RemainingArgs()
if len(args) == 0 {
@@ -162,76 +170,177 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
if _, ok := caddytls.SupportedProtocols[args[0]]; !ok {
return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[0])
}
if cp == nil {
cp = new(caddytls.ConnectionPolicy)
}
cp.ProtocolMin = args[0]
}
if len(args) > 1 {
if _, ok := caddytls.SupportedProtocols[args[1]]; !ok {
return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[1])
}
if cp == nil {
cp = new(caddytls.ConnectionPolicy)
}
cp.ProtocolMax = args[1]
}
case "ciphers":
for h.NextArg() {
if _, ok := caddytls.SupportedCipherSuites[h.Val()]; !ok {
if !caddytls.CipherSuiteNameSupported(h.Val()) {
return nil, h.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", h.Val())
}
if cp == nil {
cp = new(caddytls.ConnectionPolicy)
}
cp.CipherSuites = append(cp.CipherSuites, h.Val())
}
case "curves":
for h.NextArg() {
if _, ok := caddytls.SupportedCurves[h.Val()]; !ok {
return nil, h.Errf("Wrong curve name or curve not supported: '%s'", h.Val())
}
if cp == nil {
cp = new(caddytls.ConnectionPolicy)
}
cp.Curves = append(cp.Curves, h.Val())
}
case "client_auth":
cp.ClientAuthentication = &caddytls.ClientAuthentication{}
for nesting := h.Nesting(); h.NextBlock(nesting); {
subdir := h.Val()
switch subdir {
case "mode":
if !h.Args(&cp.ClientAuthentication.Mode) {
return nil, h.ArgErr()
}
if h.NextArg() {
return nil, h.ArgErr()
}
case "trusted_ca_cert",
"trusted_leaf_cert":
if !h.NextArg() {
return nil, h.ArgErr()
}
if subdir == "trusted_ca_cert" {
cp.ClientAuthentication.TrustedCACerts = append(cp.ClientAuthentication.TrustedCACerts, h.Val())
} else {
cp.ClientAuthentication.TrustedLeafCerts = append(cp.ClientAuthentication.TrustedLeafCerts, h.Val())
}
case "trusted_ca_cert_file",
"trusted_leaf_cert_file":
if !h.NextArg() {
return nil, h.ArgErr()
}
filename := h.Val()
certDataPEM, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
block, _ := pem.Decode(certDataPEM)
if block == nil || block.Type != "CERTIFICATE" {
return nil, h.Errf("no CERTIFICATE pem block found in %s", h.Val())
}
if subdir == "trusted_ca_cert_file" {
cp.ClientAuthentication.TrustedCACerts = append(cp.ClientAuthentication.TrustedCACerts,
base64.StdEncoding.EncodeToString(block.Bytes))
} else {
cp.ClientAuthentication.TrustedLeafCerts = append(cp.ClientAuthentication.TrustedLeafCerts,
base64.StdEncoding.EncodeToString(block.Bytes))
}
default:
return nil, h.Errf("unknown subdirective for client_auth: %s", subdir)
}
}
case "alpn":
args := h.RemainingArgs()
if len(args) == 0 {
return nil, h.ArgErr()
}
if cp == nil {
cp = new(caddytls.ConnectionPolicy)
}
cp.ALPN = args
// certificate folder loader
case "load":
folderLoader = append(folderLoader, h.RemainingArgs()...)
// automation policy
case "ca":
arg := h.RemainingArgs()
if len(arg) != 1 {
return nil, h.ArgErr()
}
mgr.CA = arg[0]
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
acmeIssuer.CA = arg[0]
case "eab":
arg := h.RemainingArgs()
if len(arg) != 2 {
return nil, h.ArgErr()
}
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
acmeIssuer.ExternalAccount = &acme.EAB{
KeyID: arg[0],
MACKey: arg[1],
}
case "issuer":
if !h.NextArg() {
return nil, h.ArgErr()
}
modName := h.Val()
mod, err := caddy.GetModule("tls.issuance." + modName)
if err != nil {
return nil, h.Errf("getting issuer module '%s': %v", modName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, h.Errf("issuer module '%s' is not a Caddyfile unmarshaler", mod.ID)
}
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
if err != nil {
return nil, err
}
issuer, ok = unm.(certmagic.Issuer)
if !ok {
return nil, h.Errf("module %s is not a certmagic.Issuer", mod.ID)
}
// DNS provider for ACME DNS challenge
case "dns":
if !h.Next() {
if !h.NextArg() {
return nil, h.ArgErr()
}
provName := h.Val()
if mgr.Challenges == nil {
mgr.Challenges = new(caddytls.ChallengesConfig)
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
dnsProvModule, err := caddy.GetModule("tls.dns." + provName)
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
}
dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
if err != nil {
return nil, h.Errf("getting DNS provider module named '%s': %v", provName, err)
}
mgr.Challenges.DNSRaw = caddyconfig.JSONModuleObject(dnsProvModule.New(), "provider", provName, h.warnings)
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(dnsProvModuleInstance, "name", provName, h.warnings)
case "ca_root":
arg := h.RemainingArgs()
if len(arg) != 1 {
return nil, h.ArgErr()
}
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, arg[0])
case "on_demand":
if h.NextArg() {
return nil, h.ArgErr()
}
onDemand = true
default:
return nil, h.Errf("unknown subdirective: %s", h.Val())
@@ -244,47 +353,106 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
}
}
// begin building the final config values
var configVals []ConfigValue
// certificate loaders
if len(fileLoader) > 0 {
configVals = append(configVals, ConfigValue{
Class: "tls.certificate_loader",
Class: "tls.cert_loader",
Value: fileLoader,
})
// ensure server uses HTTPS by setting non-nil conn policy
if cp == nil {
cp = new(caddytls.ConnectionPolicy)
}
}
if len(folderLoader) > 0 {
configVals = append(configVals, ConfigValue{
Class: "tls.certificate_loader",
Class: "tls.cert_loader",
Value: folderLoader,
})
// ensure server uses HTTPS by setting non-nil conn policy
if cp == nil {
cp = new(caddytls.ConnectionPolicy)
}
// 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")
}
if issuer != nil && (acmeIssuer != nil || internalIssuer != nil) {
// similarly, the logic to support this would be complex
return nil, h.Err("when defining an issuer, all its config must be in its block, rather than from separate tls subdirectives")
}
switch {
case issuer != nil:
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: issuer,
})
case internalIssuer != nil:
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: internalIssuer,
})
case acmeIssuer != nil:
// fill in global defaults, if configured
if email := h.Option("email"); email != nil && acmeIssuer.Email == "" {
acmeIssuer.Email = email.(string)
}
if acmeCA := h.Option("acme_ca"); acmeCA != nil && acmeIssuer.CA == "" {
acmeIssuer.CA = acmeCA.(string)
}
if caPemFile := h.Option("acme_ca_root"); caPemFile != nil {
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, caPemFile.(string))
}
}
// connection policy
if cp != nil {
configVals = append(configVals, ConfigValue{
Class: "tls.connection_policy",
Value: cp,
Class: "tls.cert_issuer",
Value: disambiguateACMEIssuer(acmeIssuer),
})
}
// automation policy
if !reflect.DeepEqual(mgr, caddytls.ACMEManagerMaker{}) {
// on-demand TLS
if onDemand {
configVals = append(configVals, ConfigValue{
Class: "tls.automation_manager",
Value: mgr,
Class: "tls.on_demand",
Value: true,
})
}
// custom certificate selection
if len(certSelector.AnyTag) > 0 {
cp.CertSelection = &certSelector
}
// connection policy -- always add one, to ensure that TLS
// is enabled, because this directive was used (this is
// needed, for instance, when a site block has a key of
// just ":5000" - i.e. no hostname, and only on-demand TLS
// is enabled)
configVals = append(configVals, ConfigValue{
Class: "tls.connection_policy",
Value: cp,
})
return configVals, nil
}
// parseRoot parses the root directive. Syntax:
//
// root [<matcher>] <path>
//
func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) {
var root string
for h.Next() {
if !h.NextArg() {
return nil, h.ArgErr()
}
root = h.Val()
if h.NextArg() {
return nil, h.ArgErr()
}
}
return caddyhttp.VarsMiddleware{"root": root}, nil
}
// parseRedir parses the redir directive. Syntax:
//
// redir [<matcher>] <to> [<code>]
@@ -348,65 +516,157 @@ func parseRespond(h Helper) (caddyhttp.MiddlewareHandler, error) {
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.NewFromNextTokens()
results, err := dirFunc(subHelper)
if err != nil {
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
}
for _, result := range results {
handler, ok := result.Value.(caddyhttp.Route)
if !ok {
return nil, h.Errf("%s directive returned something other than an HTTP route: %#v (only handler directives can be used in routes)", dir, result.Value)
}
sr.Routes = append(sr.Routes, handler)
}
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)
}
}
return sr, nil
}
// parseHandle parses the route directive.
func parseHandle(h Helper) (caddyhttp.MiddlewareHandler, error) {
var allResults []ConfigValue
return ParseSegmentAsSubroute(h)
}
func parseHandleErrors(h Helper) ([]ConfigValue, error) {
subroute, err := ParseSegmentAsSubroute(h)
if err != nil {
return nil, err
}
return []ConfigValue{
{
Class: "error_route",
Value: subroute,
},
}, nil
}
// parseLog parses the log directive. Syntax:
//
// log {
// output <writer_module> ...
// format <encoder_module> ...
// level <level>
// }
//
func parseLog(h Helper) ([]ConfigValue, error) {
var configValues []ConfigValue
for h.Next() {
for nesting := h.Nesting(); h.NextBlock(nesting); {
dir := h.Val()
// log does not currently support any arguments
if h.NextArg() {
return nil, h.ArgErr()
}
dirFunc, ok := registeredDirectives[dir]
if !ok {
return nil, h.Errf("unrecognized directive: %s", dir)
}
cl := new(caddy.CustomLog)
subHelper := h
subHelper.Dispenser = h.NewFromNextTokens()
for h.NextBlock(0) {
switch h.Val() {
case "output":
if !h.NextArg() {
return nil, h.ArgErr()
}
moduleName := h.Val()
results, err := dirFunc(subHelper)
if err != nil {
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
}
for _, result := range results {
result.directive = dir
allResults = append(allResults, result)
// can't use the usual caddyfile.Unmarshaler flow with the
// standard writers because they are in the caddy package
// (because they are the default) and implementing that
// interface there would unfortunately create circular import
var wo caddy.WriterOpener
switch moduleName {
case "stdout":
wo = caddy.StdoutWriter{}
case "stderr":
wo = caddy.StderrWriter{}
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())
if err != nil {
return nil, err
}
wo, ok = unm.(caddy.WriterOpener)
if !ok {
return nil, h.Errf("module %s is not a WriterOpener", mod)
}
}
cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings)
case "format":
if !h.NextArg() {
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())
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)
}
cl.EncoderRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, h.warnings)
case "level":
if !h.NextArg() {
return nil, h.ArgErr()
}
cl.Level = h.Val()
if h.NextArg() {
return nil, h.ArgErr()
}
default:
return nil, h.Errf("unrecognized subdirective: %s", h.Val())
}
}
return buildSubroute(allResults, h.groupCounter)
var val namedCustomLog
if !reflect.DeepEqual(cl, new(caddy.CustomLog)) {
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
}
configValues = append(configValues, ConfigValue{
Class: "custom_log",
Value: val,
})
}
return nil, nil
return configValues, nil
}
var tagCounter = 0
@@ -0,0 +1,62 @@
package httpcaddyfile
import (
"testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
_ "github.com/caddyserver/caddy/v2/modules/logging"
)
func TestLogDirectiveSyntax(t *testing.T) {
for i, tc := range []struct {
input string
expectWarn bool
expectError bool
}{
{
input: `:8080 {
log
}
`,
expectWarn: false,
expectError: false,
},
{
input: `:8080 {
log {
output file foo.log
}
}
`,
expectWarn: false,
expectError: false,
},
{
input: `:8080 {
log /foo {
output file foo.log
}
}
`,
expectWarn: false,
expectError: true,
},
} {
adapter := caddyfile.Adapter{
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
}
if err != nil != tc.expectError {
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
continue
}
}
}
+222 -49
View File
@@ -16,7 +16,10 @@ package httpcaddyfile
import (
"encoding/json"
"net"
"sort"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -26,30 +29,44 @@ import (
// directiveOrder specifies the order
// to apply directives in HTTP routes.
//
// The root directive goes first in case rewrites or
// redirects depend on existence of files, i.e. the
// file matcher, which must know the root first.
//
// The header directive goes second so that headers
// can be manipulated before doing redirects.
var directiveOrder = []string{
"map",
"root",
"header",
"redir",
"rewrite",
"root",
"strip_prefix",
"strip_suffix",
"uri_replace",
// URI manipulation
"uri",
"try_files",
// middleware handlers; some wrap responses
"basicauth",
"header",
"request_header",
"encode",
"templates",
// special routing & dispatching directives
"handle",
"handle_path",
"route",
"push",
// handlers that typically respond to requests
"respond",
"reverse_proxy",
"php_fastcgi",
"file_server",
"acme_server",
}
// directiveIsOrdered returns true if dir is
@@ -107,10 +124,23 @@ func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
})
}
// RegisterGlobalOption registers a unique global option opt with
// an associated unmarshaling (setup) function. When the global
// option opt is encountered in a Caddyfile, setupFunc will be
// called to unmarshal its tokens.
func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) {
if _, ok := registeredGlobalOptions[opt]; ok {
panic("global option " + opt + " already registered")
}
registeredGlobalOptions[opt] = setupFunc
}
// Helper is a type which helps setup a value from
// Caddyfile tokens.
type Helper struct {
*caddyfile.Dispenser
// State stores intermediate variables during caddyfile adaptation.
State map[string]interface{}
options map[string]interface{}
warnings *[]caddyconfig.Warning
matcherDefs map[string]caddy.ModuleMap
@@ -158,6 +188,23 @@ func (h Helper) MatcherToken() (caddy.ModuleMap, bool, error) {
return matcherSetFromMatcherToken(h.Dispenser.Token(), h.matcherDefs, h.warnings)
}
// ExtractMatcherSet is like MatcherToken, except this is a higher-level
// method that returns the matcher set described by the matcher token,
// or nil if there is none, and deletes the matcher token from the
// dispenser and resets it as if this look-ahead never happened. Useful
// when wrapping a route (one or more handlers) in a user-defined matcher.
func (h Helper) ExtractMatcherSet() (caddy.ModuleMap, error) {
matcherSet, hasMatcher, err := h.MatcherToken()
if err != nil {
return nil, err
}
if hasMatcher {
h.Dispenser.Delete() // strip matcher token
}
h.Dispenser.Reset() // pretend this lookahead never happened
return matcherSet, nil
}
// NewRoute returns config values relevant to creating a new HTTP route.
func (h Helper) NewRoute(matcherSet caddy.ModuleMap,
handler caddyhttp.MiddlewareHandler) []ConfigValue {
@@ -218,6 +265,75 @@ func (h Helper) NewBindAddresses(addrs []string) []ConfigValue {
return []ConfigValue{{Class: "bind", Value: addrs}}
}
// 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() {
// slice the linear list of tokens into top-level segments
var segments []caddyfile.Segment
for nesting := h.Nesting(); h.NextBlock(nesting); {
segments = append(segments, h.NextSegment())
}
// copy existing matcher definitions so we can augment
// new ones that are defined only in this scope
matcherDefs := make(map[string]caddy.ModuleMap, len(h.matcherDefs))
for key, val := range h.matcherDefs {
matcherDefs[key] = val
}
// find and extract any embedded matcher definitions in this scope
for i, seg := range segments {
if strings.HasPrefix(seg.Directive(), matcherPrefix) {
err := parseMatcherDefinitions(caddyfile.NewDispenser(seg), matcherDefs)
if err != nil {
return nil, err
}
segments = append(segments[:i], segments[i+1:]...)
}
}
// with matchers ready to go, evaluate each directive's segment
for _, seg := range segments {
dir := seg.Directive()
dirFunc, ok := registeredDirectives[dir]
if !ok {
return nil, h.Errf("unrecognized directive: %s", dir)
}
subHelper := h
subHelper.Dispenser = caddyfile.NewDispenser(seg)
subHelper.matcherDefs = matcherDefs
results, err := dirFunc(subHelper)
if err != nil {
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
}
for _, result := range results {
result.directive = dir
allResults = append(allResults, result)
}
}
}
return allResults, nil
}
// ConfigValue represents a value to be added to the final
// configuration, or a value to be consulted when building
// the final configuration.
@@ -244,59 +360,109 @@ func sortRoutes(routes []ConfigValue) {
dirPositions[dir] = i
}
// while we are sorting, we will need to decode a route's path matcher
// in order to sub-sort by path length; we can amortize this operation
// for efficiency by storing the decoded matchers in a slice
decodedMatchers := make([]caddyhttp.MatchPath, len(routes))
sort.SliceStable(routes, func(i, j int) bool {
// if the directives are different, just use the established directive order
iDir, jDir := routes[i].directive, routes[j].directive
if iDir == jDir {
// directives are the same; sub-sort by path matcher length
// if there's only one matcher set and one path (common case)
iRoute, ok := routes[i].Value.(caddyhttp.Route)
if !ok {
return false
}
jRoute, ok := routes[j].Value.(caddyhttp.Route)
if !ok {
return false
}
if len(iRoute.MatcherSetsRaw) == 1 && len(jRoute.MatcherSetsRaw) == 1 {
// use already-decoded matcher, or decode if it's the first time seeing it
iPM, jPM := decodedMatchers[i], decodedMatchers[j]
if iPM == nil {
var pathMatcher caddyhttp.MatchPath
_ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &pathMatcher)
decodedMatchers[i] = pathMatcher
iPM = pathMatcher
}
if jPM == nil {
var pathMatcher caddyhttp.MatchPath
_ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &pathMatcher)
decodedMatchers[j] = pathMatcher
jPM = pathMatcher
}
// if there is only one path in the matcher, sort by
// longer path (more specific) first
if len(iPM) == 1 && len(jPM) == 1 {
return len(iPM[0]) > len(jPM[0])
}
}
if iDir != jDir {
return dirPositions[iDir] < dirPositions[jDir]
}
return dirPositions[iDir] < dirPositions[jDir]
// directives are the same; sub-sort by path matcher length if there's
// only one matcher set and one path (this is a very common case and
// usually -- but not always -- helpful/expected, oh well; user can
// always take manual control of order using handler or route blocks)
iRoute, ok := routes[i].Value.(caddyhttp.Route)
if !ok {
return false
}
jRoute, ok := routes[j].Value.(caddyhttp.Route)
if !ok {
return false
}
// decode the path matchers, if there is just one of them
var iPM, jPM caddyhttp.MatchPath
if len(iRoute.MatcherSetsRaw) == 1 {
_ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &iPM)
}
if len(jRoute.MatcherSetsRaw) == 1 {
_ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &jPM)
}
// sort by longer path (more specific) first; missing path
// matchers or multi-matchers are treated as zero-length paths
var iPathLen, jPathLen int
if len(iPM) > 0 {
iPathLen = len(iPM[0])
}
if len(jPM) > 0 {
jPathLen = len(jPM[0])
}
return iPathLen > jPathLen
})
}
// serverBlock pairs a Caddyfile server block
// with a "pile" of config values, keyed by class
// name.
// serverBlock pairs a Caddyfile server block with
// a "pile" of config values, keyed by class name,
// as well as its parsed keys for convenience.
type serverBlock struct {
block caddyfile.ServerBlock
pile map[string][]ConfigValue // config values obtained from directives
keys []Address
}
// hostsFromKeys returns a list of all the non-empty hostnames found in
// the keys of the server block sb. If logger mode is false, a key with
// an empty hostname portion will return an empty slice, since that
// server block is interpreted to effectively match all hosts. An empty
// string is never added to the slice.
//
// If loggerMode is true, then the non-standard ports of keys will be
// joined to the hostnames. This is to effectively match the Host
// header of requests that come in for that key.
//
// The resulting slice is not sorted but will never have duplicates.
func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
// ensure each entry in our list is unique
hostMap := make(map[string]struct{})
for _, addr := range sb.keys {
if addr.Host == "" {
if !loggerMode {
// server block contains a key like ":443", i.e. the host portion
// is empty / catch-all, which means to match all hosts
return []string{}
}
// never append an empty string
continue
}
if loggerMode &&
addr.Port != "" &&
addr.Port != strconv.Itoa(caddyhttp.DefaultHTTPPort) &&
addr.Port != strconv.Itoa(caddyhttp.DefaultHTTPSPort) {
hostMap[net.JoinHostPort(addr.Host, addr.Port)] = struct{}{}
} else {
hostMap[addr.Host] = struct{}{}
}
}
// convert map to slice
sblockHosts := make([]string, 0, len(hostMap))
for host := range hostMap {
sblockHosts = append(sblockHosts, host)
}
return sblockHosts
}
// hasHostCatchAllKey returns true if sb has a key that
// omits a host portion, i.e. it "catches all" hosts.
func (sb serverBlock) hasHostCatchAllKey() bool {
for _, addr := range sb.keys {
if addr.Host == "" {
return true
}
}
return false
}
type (
@@ -315,6 +481,13 @@ type (
// for you. These are passed to a call to
// RegisterHandlerDirective.
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)
)
var registeredDirectives = make(map[string]UnmarshalFunc)
var registeredGlobalOptions = make(map[string]UnmarshalGlobalFunc)
@@ -0,0 +1,94 @@
package httpcaddyfile
import (
"reflect"
"sort"
"testing"
)
func TestHostsFromKeys(t *testing.T) {
for i, tc := range []struct {
keys []Address
expectNormalMode []string
expectLoggerMode []string
}{
{
[]Address{
{Original: "foo", Host: "foo"},
},
[]string{"foo"},
[]string{"foo"},
},
{
[]Address{
{Original: "foo", Host: "foo"},
{Original: "bar", Host: "bar"},
},
[]string{"bar", "foo"},
[]string{"bar", "foo"},
},
{
[]Address{
{Original: ":2015", Port: "2015"},
},
[]string{}, []string{},
},
{
[]Address{
{Original: ":443", Port: "443"},
},
[]string{}, []string{},
},
{
[]Address{
{Original: "foo", Host: "foo"},
{Original: ":2015", Port: "2015"},
},
[]string{}, []string{"foo"},
},
{
[]Address{
{Original: "example.com:2015", Host: "example.com", Port: "2015"},
},
[]string{"example.com"},
[]string{"example.com:2015"},
},
{
[]Address{
{Original: "example.com:80", Host: "example.com", Port: "80"},
},
[]string{"example.com"},
[]string{"example.com"},
},
{
[]Address{
{Original: "https://:2015/foo", Scheme: "https", Port: "2015", Path: "/foo"},
},
[]string{},
[]string{},
},
{
[]Address{
{Original: "https://example.com:2015/foo", Scheme: "https", Host: "example.com", Port: "2015", Path: "/foo"},
},
[]string{"example.com"},
[]string{"example.com:2015"},
},
} {
sb := serverBlock{keys: tc.keys}
// test in normal mode
actual := sb.hostsFromKeys(false)
sort.Strings(actual)
if !reflect.DeepEqual(tc.expectNormalMode, actual) {
t.Errorf("Test %d (loggerMode=false): Expected: %v Actual: %v", i, tc.expectNormalMode, actual)
}
// test in logger mode
actual = sb.hostsFromKeys(true)
sort.Strings(actual)
if !reflect.DeepEqual(tc.expectLoggerMode, actual) {
t.Errorf("Test %d (loggerMode=true): Expected: %v Actual: %v", i, tc.expectLoggerMode, actual)
}
}
}
File diff suppressed because it is too large Load Diff
+205 -1
View File
@@ -1,6 +1,90 @@
package httpcaddyfile
import "testing"
import (
"testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func TestMatcherSyntax(t *testing.T) {
for i, tc := range []struct {
input string
expectWarn bool
expectError bool
}{
{
input: `http://localhost
@debug {
query showdebug=1
}
`,
expectWarn: false,
expectError: false,
},
{
input: `http://localhost
@debug {
query bad format
}
`,
expectWarn: false,
expectError: true,
},
{
input: `http://localhost
@debug {
not {
path /somepath*
}
}
`,
expectWarn: false,
expectError: false,
},
{
input: `http://localhost
@debug {
not path /somepath*
}
`,
expectWarn: false,
expectError: false,
},
{
input: `http://localhost
@debug not path /somepath*
`,
expectWarn: false,
expectError: false,
},
{
input: `@matcher {
path /matcher-not-allowed/outside-of-site-block/*
}
http://localhost
`,
expectWarn: false,
expectError: true,
},
} {
adapter := caddyfile.Adapter{
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
}
if err != nil != tc.expectError {
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
continue
}
}
}
func TestSpecificity(t *testing.T) {
for i, tc := range []struct {
@@ -31,3 +115,123 @@ func TestSpecificity(t *testing.T) {
}
}
}
func TestGlobalOptions(t *testing.T) {
for i, tc := range []struct {
input string
expectWarn bool
expectError bool
}{
{
input: `
{
email test@example.com
}
:80
`,
expectWarn: false,
expectError: false,
},
{
input: `
{
admin off
}
:80
`,
expectWarn: false,
expectError: false,
},
{
input: `
{
admin 127.0.0.1:2020
}
:80
`,
expectWarn: false,
expectError: false,
},
{
input: `
{
admin {
disabled false
}
}
:80
`,
expectWarn: false,
expectError: true,
},
{
input: `
{
admin {
enforce_origin
origins 192.168.1.1:2020 127.0.0.1:2020
}
}
:80
`,
expectWarn: false,
expectError: false,
},
{
input: `
{
admin 127.0.0.1:2020 {
enforce_origin
origins 192.168.1.1:2020 127.0.0.1:2020
}
}
:80
`,
expectWarn: false,
expectError: false,
},
{
input: `
{
admin 192.168.1.1:2020 127.0.0.1:2020 {
enforce_origin
origins 192.168.1.1:2020 127.0.0.1:2020
}
}
:80
`,
expectWarn: false,
expectError: true,
},
{
input: `
{
admin off {
enforce_origin
origins 192.168.1.1:2020 127.0.0.1:2020
}
}
:80
`,
expectWarn: false,
expectError: true,
},
} {
adapter := caddyfile.Adapter{
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
}
if err != nil != tc.expectError {
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
continue
}
}
}
+214 -52
View File
@@ -15,14 +15,41 @@
package httpcaddyfile
import (
"fmt"
"strconv"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
)
func parseOptHTTPPort(d *caddyfile.Dispenser) (int, error) {
func init() {
RegisterGlobalOption("debug", parseOptTrue)
RegisterGlobalOption("http_port", parseOptHTTPPort)
RegisterGlobalOption("https_port", parseOptHTTPSPort)
RegisterGlobalOption("default_sni", parseOptSingleString)
RegisterGlobalOption("order", parseOptOrder)
RegisterGlobalOption("experimental_http3", parseOptTrue)
RegisterGlobalOption("storage", parseOptStorage)
RegisterGlobalOption("acme_ca", parseOptSingleString)
RegisterGlobalOption("acme_ca_root", parseOptSingleString)
RegisterGlobalOption("acme_dns", parseOptSingleString)
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)
}
func parseOptTrue(d *caddyfile.Dispenser) (interface{}, error) {
return true, nil
}
func parseOptHTTPPort(d *caddyfile.Dispenser) (interface{}, error) {
var httpPort int
for d.Next() {
var httpPortStr string
@@ -38,7 +65,7 @@ func parseOptHTTPPort(d *caddyfile.Dispenser) (int, error) {
return httpPort, nil
}
func parseOptHTTPSPort(d *caddyfile.Dispenser) (int, error) {
func parseOptHTTPSPort(d *caddyfile.Dispenser) (interface{}, error) {
var httpsPort int
for d.Next() {
var httpsPortStr string
@@ -54,11 +81,7 @@ func parseOptHTTPSPort(d *caddyfile.Dispenser) (int, error) {
return httpsPort, nil
}
func parseOptExperimentalHTTP3(d *caddyfile.Dispenser) (bool, error) {
return true, nil
}
func parseOptOrder(d *caddyfile.Dispenser) ([]string, error) {
func parseOptOrder(d *caddyfile.Dispenser) (interface{}, error) {
newOrder := directiveOrder
for d.Next() {
@@ -68,7 +91,7 @@ func parseOptOrder(d *caddyfile.Dispenser) ([]string, error) {
}
dirName := d.Val()
if _, ok := registeredDirectives[dirName]; !ok {
return nil, fmt.Errorf("%s is not a registered directive", dirName)
return nil, d.Errf("%s is not a registered directive", dirName)
}
// get positional token
@@ -104,7 +127,7 @@ func parseOptOrder(d *caddyfile.Dispenser) ([]string, error) {
case "before":
case "after":
default:
return nil, fmt.Errorf("unknown positional '%s'", pos)
return nil, d.Errf("unknown positional '%s'", pos)
}
// get name of other directive
@@ -134,68 +157,207 @@ func parseOptOrder(d *caddyfile.Dispenser) ([]string, error) {
return newOrder, nil
}
func parseOptStorage(d *caddyfile.Dispenser) (caddy.StorageConverter, error) {
if !d.Next() {
func parseOptStorage(d *caddyfile.Dispenser) (interface{}, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
args := d.RemainingArgs()
if len(args) != 1 {
if !d.Next() { // get storage module name
return nil, d.ArgErr()
}
modName := args[0]
modName := d.Val()
mod, err := caddy.GetModule("caddy.storage." + modName)
if err != nil {
return nil, fmt.Errorf("getting storage module '%s': %v", modName, err)
return nil, d.Errf("getting storage module '%s': %v", modName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, fmt.Errorf("storage module '%s' is not a Caddyfile unmarshaler", mod.ID)
return nil, d.Errf("storage module '%s' is not a Caddyfile unmarshaler", mod.ID)
}
err = unm.UnmarshalCaddyfile(d.NewFromNextTokens())
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
if err != nil {
return nil, err
}
storage, ok := unm.(caddy.StorageConverter)
if !ok {
return nil, fmt.Errorf("module %s is not a StorageConverter", mod.ID)
return nil, d.Errf("module %s is not a StorageConverter", mod.ID)
}
return storage, nil
}
func parseOptACME(d *caddyfile.Dispenser) (string, error) {
d.Next() // consume parameter name
if !d.Next() {
return "", d.ArgErr()
}
val := d.Val()
if d.Next() {
return "", d.ArgErr()
}
return val, nil
}
func parseOptEmail(d *caddyfile.Dispenser) (string, error) {
d.Next() // consume parameter name
if !d.Next() {
return "", d.ArgErr()
}
val := d.Val()
if d.Next() {
return "", d.ArgErr()
}
return val, nil
}
func parseOptAdmin(d *caddyfile.Dispenser) (string, error) {
if d.Next() {
var listenAddress string
d.AllArgs(&listenAddress)
if listenAddress == "" {
listenAddress = caddy.DefaultAdminListen
func parseOptACMEEAB(d *caddyfile.Dispenser) (interface{}, error) {
eab := new(acme.EAB)
for d.Next() {
if d.NextArg() {
return nil, d.ArgErr()
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "key_id":
if !d.NextArg() {
return nil, d.ArgErr()
}
eab.KeyID = d.Val()
return listenAddress, nil
case "mac_key":
if !d.NextArg() {
return nil, d.ArgErr()
}
eab.MACKey = d.Val()
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
}
}
}
return "", nil
return eab, nil
}
func parseOptCertIssuer(d *caddyfile.Dispenser) (interface{}, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
if !d.Next() { // get issuer module name
return nil, d.ArgErr()
}
modName := d.Val()
mod, err := caddy.GetModule("tls.issuance." + modName)
if err != nil {
return nil, d.Errf("getting issuer module '%s': %v", modName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, d.Errf("issuer module '%s' is not a Caddyfile unmarshaler", mod.ID)
}
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
if err != nil {
return nil, err
}
iss, ok := unm.(certmagic.Issuer)
if !ok {
return nil, d.Errf("module %s is not a certmagic.Issuer", mod.ID)
}
return iss, nil
}
func parseOptSingleString(d *caddyfile.Dispenser) (interface{}, error) {
d.Next() // consume parameter name
if !d.Next() {
return "", d.ArgErr()
}
val := d.Val()
if d.Next() {
return "", d.ArgErr()
}
return val, nil
}
func parseOptAdmin(d *caddyfile.Dispenser) (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()
}
}
}
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())
}
}
}
if adminCfg.Listen == "" && !adminCfg.Disabled {
adminCfg.Listen = caddy.DefaultAdminListen
}
return adminCfg, nil
}
func parseOptOnDemand(d *caddyfile.Dispenser) (interface{}, error) {
var ond *caddytls.OnDemandConfig
for d.Next() {
if d.NextArg() {
return nil, d.ArgErr()
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "ask":
if !d.NextArg() {
return nil, d.ArgErr()
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
ond.Ask = d.Val()
case "interval":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, err
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
if ond.RateLimit == nil {
ond.RateLimit = new(caddytls.RateLimit)
}
ond.RateLimit.Interval = caddy.Duration(dur)
case "burst":
if !d.NextArg() {
return nil, d.ArgErr()
}
burst, err := strconv.Atoi(d.Val())
if err != nil {
return nil, err
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
if ond.RateLimit == nil {
ond.RateLimit = new(caddytls.RateLimit)
}
ond.RateLimit.Burst = burst
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
}
}
}
if ond == nil {
return nil, d.Err("expected at least one config parameter for on_demand_tls")
}
return ond, nil
}
func parseOptAutoHTTPS(d *caddyfile.Dispenser) (interface{}, error) {
d.Next() // consume parameter name
if !d.Next() {
return "", d.ArgErr()
}
val := d.Val()
if d.Next() {
return "", d.ArgErr()
}
if val != "off" && val != "disable_redirects" {
return "", d.Errf("auto_https must be either 'off' or 'disable_redirects'")
}
return val, nil
}
-66
View File
@@ -1,66 +0,0 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpcaddyfile
import (
"testing"
caddyfile "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func TestParse(t *testing.T) {
for i, tc := range []struct {
input string
expectWarn bool
expectError bool
}{
{
input: `http://localhost
@debug {
query showdebug=1
}
`,
expectWarn: false,
expectError: false,
},
{
input: `http://localhost
@debug {
query bad format
}
`,
expectWarn: false,
expectError: true,
},
} {
adapter := caddyfile.Adapter{
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
}
if err != nil != tc.expectError {
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
continue
}
}
}
+491
View File
@@ -0,0 +1,491 @@
// 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 (
"bytes"
"encoding/json"
"fmt"
"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(
pairings []sbAddrAssociation,
options map[string]interface{},
warnings []caddyconfig.Warning,
) (*caddytls.TLS, []caddyconfig.Warning, error) {
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
var certLoaders []caddytls.CertificateLoader
httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
if hsp, ok := options["https_port"].(int); ok {
httpsPort = strconv.Itoa(hsp)
}
// count how many server blocks have a TLS-enabled key with
// no host, and find all hosts that share a server block with
// a hostless key, so that they don't get forgotten/omitted
// by auto-HTTPS (since they won't appear in route matchers)
var serverBlocksWithTLSHostlessKey int
hostsSharedWithHostlessKey := make(map[string]struct{})
for _, pair := range pairings {
for _, sb := range pair.serverBlocks {
for _, addr := range sb.keys {
if addr.Host == "" {
// this address has no hostname, but if it's explicitly set
// to HTTPS, then we need to count it as being TLS-enabled
if addr.Scheme == "https" || addr.Port == httpsPort {
serverBlocksWithTLSHostlessKey++
}
// this server block has a hostless key, now
// go through and add all the hosts to the set
for _, otherAddr := range sb.keys {
if otherAddr.Original == addr.Original {
continue
}
if otherAddr.Host != "" {
hostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
}
}
break
}
}
}
}
catchAllAP, err := newBaseAutomationPolicy(options, warnings, false)
if err != nil {
return nil, warnings, err
}
for _, p := range pairings {
for _, sblock := range p.serverBlocks {
// get values that populate an automation policy for this block
var ap *caddytls.AutomationPolicy
sblockHosts := sblock.hostsFromKeys(false)
if len(sblockHosts) == 0 {
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
}
// certificate issuers
if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
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
}
}
// custom bind host
for _, cfgVal := range sblock.pile["bind"] {
// either an existing issuer is already configured (and thus, ap is not
// nil), or we need to configure an issuer, so we need ap to be non-nil
if ap == nil {
ap, err = newBaseAutomationPolicy(options, warnings, true)
if err != nil {
return nil, warnings, err
}
}
// if an issuer was already configured and it is NOT an ACME
// issuer, skip, since we intend to adjust only ACME issuers
var acmeIssuer *caddytls.ACMEIssuer
if ap.Issuer != nil {
// ensure we include any issuer that embeds/wraps an underlying ACME issuer
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
if acmeWrapper, ok := ap.Issuer.(acmeCapable); ok {
acmeIssuer = acmeWrapper.GetACMEIssuer()
} else {
break
}
}
// 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
}
}
// 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)
}
}
}
// certificate loaders
if clVals, ok := sblock.pile["tls.cert_loader"]; ok {
for _, clVal := range clVals {
certLoaders = append(certLoaders, clVal.Value.(caddytls.CertificateLoader))
}
}
}
}
// group certificate loaders by module name, then add to config
if len(certLoaders) > 0 {
loadersByName := make(map[string]caddytls.CertificateLoader)
for _, cl := range certLoaders {
name := caddy.GetModuleName(cl)
// ugh... technically, we may have multiple FileLoader and FolderLoader
// modules (because the tls directive returns one per occurrence), but
// the config structure expects only one instance of each kind of loader
// module, so we have to combine them... instead of enumerating each
// possible cert loader module in a type switch, we can use reflection,
// which works on any cert loaders that are slice types
if reflect.TypeOf(cl).Kind() == reflect.Slice {
combined := reflect.ValueOf(loadersByName[name])
if !combined.IsValid() {
combined = reflect.New(reflect.TypeOf(cl)).Elem()
}
clVal := reflect.ValueOf(cl)
for i := 0; i < clVal.Len(); i++ {
combined = reflect.Append(combined, clVal.Index(i))
}
loadersByName[name] = combined.Interface().(caddytls.CertificateLoader)
}
}
for certLoaderName, loaders := range loadersByName {
tlsApp.CertificatesRaw[certLoaderName] = caddyconfig.JSON(loaders, &warnings)
}
}
// set any of the on-demand options, for if/when on-demand TLS is enabled
if onDemand, ok := options["on_demand_tls"].(*caddytls.OnDemandConfig); ok {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.OnDemand = onDemand
}
// 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
// considered for auto-HTTPS, so we need to make sure those hosts
// are manually considered for managed certificates; we also need
// to make sure that any of these names which are internal-only
// get internal certificates by default rather than ACME
var al caddytls.AutomateLoader
internalAP := &caddytls.AutomationPolicy{
IssuerRaw: json.RawMessage(`{"module":"internal"}`),
}
for h := range hostsSharedWithHostlessKey {
al = append(al, h)
if !certmagic.SubjectQualifiesForPublicCert(h) {
internalAP.Subjects = append(internalAP.Subjects, h)
}
}
if len(al) > 0 {
tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
}
if len(internalAP.Subjects) > 0 {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
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)
}
// then append it to the end of the policies list
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
}
// do a little verification & cleanup
if tlsApp.Automation != nil {
// 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)
automationHostSet := make(map[string]struct{})
for _, ap := range tlsApp.Automation.Policies {
for _, s := range ap.Subjects {
if _, ok := automationHostSet[s]; ok {
return nil, warnings, fmt.Errorf("hostname appears in more than one automation policy, making certificate management ambiguous: %s", s)
}
automationHostSet[s] = struct{}{}
}
}
}
return tlsApp, warnings, nil
}
// newBaseAutomationPolicy returns a new TLS automation policy that gets
// its values from the global options map. It should be used as the base
// for any other automation policies. A nil policy (and no error) will be
// returned if there are no default/global options. However, if always is
// true, a non-nil value will always be returned (unless there is an error).
func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
issuer, hasIssuer := options["cert_issuer"]
acmeCA, hasACMECA := options["acme_ca"]
acmeCARoot, hasACMECARoot := options["acme_ca_root"]
acmeDNS, hasACMEDNS := options["acme_dns"]
acmeEAB, hasACMEEAB := options["acme_eab"]
email, hasEmail := options["email"]
localCerts, hasLocalCerts := options["local_certs"]
keyType, hasKeyType := options["key_type"]
hasGlobalAutomationOpts := hasIssuer || hasACMECA || hasACMECARoot || hasACMEDNS || hasACMEEAB || hasEmail || hasLocalCerts || hasKeyType
// if there are no global options related to automation policies
// set, then we can just return right away
if !hasGlobalAutomationOpts {
if always {
return new(caddytls.AutomationPolicy), nil
}
return nil, nil
}
ap := new(caddytls.AutomationPolicy)
if keyType != nil {
ap.KeyType = keyType.(string)
}
if hasIssuer {
if hasACMECA || hasACMEDNS || hasACMEEAB || hasEmail || hasLocalCerts {
return nil, fmt.Errorf("global options are ambiguous: cert_issuer is confusing when combined with acme_*, email, or local_certs options")
}
ap.Issuer = issuer.(certmagic.Issuer)
} else if localCerts != nil {
// internal issuer enabled trumps any ACME configurations; useful in testing
ap.Issuer = new(caddytls.InternalIssuer) // we'll encode it later
} else {
if acmeCA == nil {
acmeCA = ""
}
if email == nil {
email = ""
}
mgr := &caddytls.ACMEIssuer{
CA: acmeCA.(string),
Email: email.(string),
}
if acmeDNS != nil {
provName := acmeDNS.(string)
dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
if err != nil {
return nil, fmt.Errorf("getting DNS provider module named '%s': %v", provName, err)
}
mgr.Challenges = &caddytls.ChallengesConfig{
DNS: &caddytls.DNSChallengeConfig{
ProviderRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "name", provName, &warnings),
},
}
}
if acmeCARoot != nil {
mgr.TrustedRootsPEMFiles = []string{acmeCARoot.(string)}
}
if acmeEAB != nil {
mgr.ExternalAccount = acmeEAB.(*acme.EAB)
}
ap.Issuer = disambiguateACMEIssuer(mgr) // we'll encode it later
}
return ap, nil
}
// disambiguateACMEIssuer returns an issuer based on the properties of acmeIssuer.
// If acmeIssuer implicitly configures a certain kind of ACMEIssuer (for example,
// ZeroSSL), the proper wrapper over acmeIssuer will be returned instead.
func disambiguateACMEIssuer(acmeIssuer *caddytls.ACMEIssuer) certmagic.Issuer {
// as a special case, we integrate with ZeroSSL's ACME endpoint if it looks like an
// implicit ZeroSSL configuration (this requires a wrapper type over ACMEIssuer
// because of the EAB generation; if EAB is provided, we can use plain ACMEIssuer)
if strings.Contains(acmeIssuer.CA, "acme.zerossl.com") && acmeIssuer.ExternalAccount == nil {
return &caddytls.ZeroSSLIssuer{ACMEIssuer: acmeIssuer}
}
return acmeIssuer
}
// consolidateAutomationPolicies combines automation policies that are the same,
// for a cleaner overall output.
func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
for i := 0; i < len(aps); i++ {
for j := 0; j < len(aps); j++ {
if j == i {
continue
}
// 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
}
// if the policy is the same, we can keep just one, but we have
// to be careful which one we keep; if only one has any hostnames
// defined, then we need to keep the one without any hostnames,
// 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) &&
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:]...)
} else {
// 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:]...)
}
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
}
-43
View File
@@ -1,43 +0,0 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package json5adapter
import (
"encoding/json"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/ilibs/json5"
)
func init() {
caddyconfig.RegisterAdapter("json5", Adapter{})
}
// Adapter adapts JSON5 to Caddy JSON.
type Adapter struct{}
// Adapt converts the JSON5 config in body to Caddy JSON.
func (a Adapter) Adapt(body []byte, options map[string]interface{}) (result []byte, warnings []caddyconfig.Warning, err error) {
var decoded interface{}
err = json5.Unmarshal(body, &decoded)
if err != nil {
return
}
result, err = json.Marshal(decoded)
return
}
// Interface guard
var _ caddyconfig.Adapter = (*Adapter)(nil)
-49
View File
@@ -1,49 +0,0 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jsoncadapter
import (
"encoding/json"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/muhammadmuzzammil1998/jsonc"
)
func init() {
caddyconfig.RegisterAdapter("jsonc", Adapter{})
}
// Adapter adapts JSON-C to Caddy JSON.
type Adapter struct{}
// Adapt converts the JSON-C config in body to Caddy JSON.
func (a Adapter) Adapt(body []byte, options map[string]interface{}) (result []byte, warnings []caddyconfig.Warning, err error) {
result = jsonc.ToJSON(body)
// any errors in the JSON will be
// reported during config load, but
// we can at least warn here that
// it is not valid JSON
if !json.Valid(result) {
warnings = append(warnings, caddyconfig.Warning{
Message: "Resulting JSON is invalid.",
})
}
return
}
// Interface guard
var _ caddyconfig.Adapter = (*Adapter)(nil)
+153
View File
@@ -0,0 +1,153 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyconfig
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"strings"
"sync"
"github.com/caddyserver/caddy/v2"
)
func init() {
caddy.RegisterModule(adminLoad{})
}
// adminLoad is a module that provides the /load endpoint
// for the Caddy admin API. The only reason it's not baked
// into the caddy package directly is because of the import
// of the caddyconfig package for its GetAdapter function.
// If the caddy package depends on the caddyconfig package,
// then the caddyconfig package will not be able to import
// the caddy package, and it can more easily cause backward
// edges in the dependency tree (i.e. import cycle).
// Fortunately, the admin API has first-class support for
// adding endpoints from modules.
type adminLoad struct{}
// CaddyModule returns the Caddy module information.
func (adminLoad) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "admin.api.load",
New: func() caddy.Module { return new(adminLoad) },
}
}
// Routes returns a route for the /load endpoint.
func (al adminLoad) Routes() []caddy.AdminRoute {
return []caddy.AdminRoute{
{
Pattern: "/load",
Handler: caddy.AdminHandlerFunc(al.handleLoad),
},
}
}
// handleLoad replaces the entire current configuration with
// a new one provided in the response body. It supports config
// adapters through the use of the Content-Type header. A
// config that is identical to the currently-running config
// will be a no-op unless Cache-Control: must-revalidate is set.
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"),
}
}
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
_, err := io.Copy(buf, r.Body)
if err != nil {
return caddy.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("reading request body: %v", err),
}
}
body := buf.Bytes()
// 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)
if err != nil {
return caddy.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("invalid Content-Type: %v", 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 err != nil {
return caddy.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("adapting config using %s adapter: %v", adapterName, err),
}
}
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
}
}
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
err = caddy.Load(body, forceReload)
if err != nil {
return caddy.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("loading config: %v", err),
}
}
caddy.Log().Named("admin.api").Info("load complete")
return nil
}
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
+23
View File
@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIID5zCCAs8CFG4+w/pqR5AZQ+aVB330uRRRKMF0MA0GCSqGSIb3DQEBCwUAMIGv
MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZBgNVBAoMEkxvY2FsIERldmVs
b3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxvcGVtZW50MRowGAYDVQQDDBFh
LmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9jYWwgRGV2ZWxvcGVtZW50MSAw
HgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2NhbDAeFw0yMDAzMTMxODUwMTda
Fw0zMDAzMTExODUwMTdaMIGvMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZ
BgNVBAoMEkxvY2FsIERldmVsb3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxv
cGVtZW50MRowGAYDVQQDDBFhLmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9j
YWwgRGV2ZWxvcGVtZW50MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2Nh
bDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMd9pC9wF7j0459FndPs
Deud/rq41jEZFsVOVtjQgjS1A5ct6NfeMmSlq8i1F7uaTMPZjbOHzY6y6hzLc9+y
/VWNgyUC543HjXnNTnp9Xug6tBBxOxvRMw5mv2nAyzjBGDePPgN84xKhOXG2Wj3u
fOZ+VPVISefRNvjKfN87WLJ0B0HI9wplG5ASVdPQsWDY1cndrZgt2sxQ/3fjIno4
VvrgRWC9Penizgps/a0ZcFZMD/6HJoX/mSZVa1LjopwbMTXvyHCpXkth21E+rBt6
I9DMHerdioVQcX25CqPmAwePxPZSNGEQo/Qu32kzcmscmYxTtYBhDa+yLuHgGggI
j7ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAP/94KPtkpYtkWADnhtzDmgQ6Q1pH
SubTUZdCwQtm6/LrvpT+uFNsOj4L3Mv3TVUnIQDmKd5VvR42W2MRBiTN2LQptgEn
C7g9BB+UA9kjL3DPk1pJMjzxLHohh0uNLi7eh4mAj8eNvjz9Z4qMWPQoVS0y7/ZK
cCBRKh2GkIqKm34ih6pX7xmMpPEQsFoTVPRHYJfYD1SZ8Iui+EN+7WqLuJWPsPXw
JM1HuZKn7pZmJU2MZZBsrupHGUvNMbBg2mFJcxt4D1VvU+p+a67PSjpFQ6dJG2re
pZoF+N1vMGAFkxe6UqhcC/bXDX+ILVQHJ+RNhzDO6DcWf8dRrC2LaJk3WA==
-----END CERTIFICATE-----
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAx32kL3AXuPTjn0Wd0+wN653+urjWMRkWxU5W2NCCNLUDly3o
194yZKWryLUXu5pMw9mNs4fNjrLqHMtz37L9VY2DJQLnjceNec1Oen1e6Dq0EHE7
G9EzDma/acDLOMEYN48+A3zjEqE5cbZaPe585n5U9UhJ59E2+Mp83ztYsnQHQcj3
CmUbkBJV09CxYNjVyd2tmC3azFD/d+MiejhW+uBFYL096eLOCmz9rRlwVkwP/ocm
hf+ZJlVrUuOinBsxNe/IcKleS2HbUT6sG3oj0Mwd6t2KhVBxfbkKo+YDB4/E9lI0
YRCj9C7faTNyaxyZjFO1gGENr7Iu4eAaCAiPsQIDAQABAoIBAQDD/YFIBeWYlifn
e9risQDAIrp3sk7lb9O6Rwv1+Wxi4hBEABvJsYhq74VFK/3EF4UhyWR5JIvkjYyK
e6w887oGyoA05ZSe65XoO7fFidSrbbkoikZbPv3dQT7/ZCWEfdkQBNAVVyY0UGeC
e3hPbjYRsb5AOSQ694X9idqC6uhqcOrBDjITFrctUoP4S6l9A6a+mLSUIwiICcuh
mrNl+j0lzy7DMXRp/Z5Hyo5kuUlrC0dCLa1UHqtrrK7MR55AVEOihSNp1w+OC+vw
f0VjE4JUtO7RQEQUmD1tDfLXwNfMFeWaobB2W0WMvRg0IqoitiqPxsPHRm56OxfM
SRo/Q7QBAoGBAP8DapzBMuaIcJ7cE8Yl07ZGndWWf8buIKIItGF8rkEO3BXhrIke
EmpOi+ELtpbMOG0APhORZyQ58f4ZOVrqZfneNKtDiEZV4mJZaYUESm1pU+2Y6+y5
g4bpQSVKN0ow0xR+MH7qDYtSlsmBU7qAOz775L7BmMA1Bnu72aN/H1JBAoGBAMhD
OzqCSakHOjUbEd22rPwqWmcIyVyo04gaSmcVVT2dHbqR4/t0gX5a9D9U2qwyO6xi
/R+PXyMd32xIeVR2D/7SQ0x6dK68HXICLV8ofHZ5UQcHbxy5og4v/YxSZVTkN374
cEsUeyB0s/UPOHLktFU5hpIlON72/Rp7b+pNIwFxAoGAczpq+Qu/YTWzlcSh1r4O
7OT5uqI3eH7vFehTAV3iKxl4zxZa7NY+wfRd9kFhrr/2myIp6pOgBFl+hC+HoBIc
JAyIxf5M3GNAWOpH6MfojYmzV7/qktu8l8BcJGplk0t+hVsDtMUze4nFAqZCXBpH
Kw2M7bjyuZ78H/rgu6TcVUECgYEAo1M5ldE2U/VCApeuLX1TfWDpU8i1uK0zv3d5
oLKkT1i5KzTak3SEO9HgC1qf8PoS8tfUio26UICHe99rnHehOfivzEq+qNdgyF+A
M3BoeZMdgzcL5oh640k+Zte4LtDlddcWdhUhCepD7iPYrNNbQ3pkBwL2a9lRuOxc
7OC2IPECgYBH8f3OrwXjDltIG1dDvuDPNljxLZbFEFbQyVzMePYNftgZknAyGEdh
NW/LuWeTzstnmz/s6RE3jN5ZrrMa4sW77VA9+yU9QW2dkHqFyukQ4sfuNg6kDDNZ
+lqZYMCLw0M5P9fIbmnIYwey7tXkHfmzoCpnYHGQDN6hL0Bh0zGwmg==
-----END RSA PRIVATE KEY-----
+20
View File
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQEL
BQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkw
ODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU
7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl0
3WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45t
wOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNx
tdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTU
ApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAd
BgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS
2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5u
NY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkq
hkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfK
D66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEO
fG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnk
oNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZ
ks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdle
Ih6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
-----END CERTIFICATE-----
+23
View File
@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIID5zCCAs8CFFmAAFKV79uhzxc5qXbUw3oBNsYXMA0GCSqGSIb3DQEBCwUAMIGv
MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZBgNVBAoMEkxvY2FsIERldmVs
b3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxvcGVtZW50MRowGAYDVQQDDBEq
LmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9jYWwgRGV2ZWxvcGVtZW50MSAw
HgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2NhbDAeFw0yMDAzMDIwODAxMTZa
Fw0zMDAyMjgwODAxMTZaMIGvMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZ
BgNVBAoMEkxvY2FsIERldmVsb3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxv
cGVtZW50MRowGAYDVQQDDBEqLmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9j
YWwgRGV2ZWxvcGVtZW50MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2Nh
bDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJngfeirQkWaU8ihgIC5
SKpRQX/3koRjljDK/oCbhLs+wg592kIwVv06l7+mn7NSaNBloabjuA1GqyLRsNLL
ptrv0HvXa5qLx28+icsb2Ny3dJnQaj9w9PwjxQ1qZqEJfWRH1D8Vz9AmB+QSV/Gu
8e8alGFewlYZVfH1kbxoTT6QorF37TeA3bh1fgKFtzsGYKswcaZNdDBBHzLunCKZ
HU6U6L45hm+yLADj3mmDLafUeiVOt6MRLLoSD1eLRVSXGrNo+brJ87zkZntI9+W1
JxOBoXtZCwka7k2DlAtLihsrmBZA2ZC9yVeu/SQy3qb3iCNnTFTCyAnWeTCr6Tcq
6w8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAOWfXqpAmD4C3wGiMeZAeaaS4hDAR
+JmN+avPDA6F6Bq7DB4NJuIwVUlaDL2s07w5VJJtW52aZVKoBlgHR5yG/XUli6J7
YUJRmdQJvHUSu26cmKvyoOaTrEYbmvtGICWtZc8uTlMf9wQZbJA4KyxTgEQJDXsZ
B2XFe+wVdhAgEpobYDROi+l/p8TL5z3U24LpwVTcJy5sEZVv7Wfs886IyxU8ORt8
VZNcDiH6V53OIGeiufIhia/mPe6jbLntfGZfIFxtCcow4IA/lTy1ned7K5fmvNNb
ZilxOQUk+wVK8genjdrZVAnAxsYLHJIb5yf9O7rr6fWciVMF3a0k5uNK1w==
-----END CERTIFICATE-----
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAmeB96KtCRZpTyKGAgLlIqlFBf/eShGOWMMr+gJuEuz7CDn3a
QjBW/TqXv6afs1Jo0GWhpuO4DUarItGw0sum2u/Qe9drmovHbz6JyxvY3Ld0mdBq
P3D0/CPFDWpmoQl9ZEfUPxXP0CYH5BJX8a7x7xqUYV7CVhlV8fWRvGhNPpCisXft
N4DduHV+AoW3OwZgqzBxpk10MEEfMu6cIpkdTpTovjmGb7IsAOPeaYMtp9R6JU63
oxEsuhIPV4tFVJcas2j5usnzvORme0j35bUnE4Ghe1kLCRruTYOUC0uKGyuYFkDZ
kL3JV679JDLepveII2dMVMLICdZ5MKvpNyrrDwIDAQABAoIBAFcPK01zb6hfm12c
+k5aBiHOnUdgc/YRPg1XHEz5MEycQkDetZjTLrRQ7UBSbnKPgpu9lIsOtbhVLkgh
6XAqJroiCou2oruqr+hhsqZGmBiwdvj7cNF6ADGTr05az7v22YneFdinZ481pStF
sZocx+bm2+KHMV5zMSwXKyA0xtdJLxs2yklniDBxSZRppgppq1pDPprP5DkgKPfe
3ekUmbQd5bHmivhW8ItbJLuf82XSsMBZ9ZhKiKIlWlbKAgiSV3SqnUQb5fi7l8hG
yYZxbuCUIGFwKmEpUBBt/nyxrOlMiNtDh9JhrPmijTV3slq70pCLwLL/Ai2aeear
EVA5VhkCgYEAyAmxfPqc2P7BsDAp67/sA7OEPso9qM4WyuWiVdlX2gb9TLNLYbPX
Kk/UmpAIVzpoTAGY5Zp3wkvdD/ou8uUQsE8ioNn4S1a4G9XURH1wVhcEbUiAKI1S
QVBH9B/Pj3eIp5OTKwob0Wj7DNdxoH7ed/Eok0EaTWzOA8pCWADKv/MCgYEAxOzY
YsX7Nl+eyZr2+9unKyeAK/D1DCT/o99UUAHx72/xaBVP/06cfzpvKBNcF9iYc+fq
R1yIUIrDRoSmYKBq+Kb3+nOg1nrqih/NBTokbTiI4Q+/30OQt0Al1e7y9iNKqV8H
jYZItzluGNrWKedZbATwBwbVCY2jnNl6RMDnS3UCgYBxj3cwQUHLuoyQjjcuO80r
qLzZvIxWiXDNDKIk5HcIMlGYOmz/8U2kGp/SgxQJGQJeq8V2C0QTjGfaCyieAcaA
oNxCvptDgd6RBsoze5bLeNOtiqwe2WOp6n5+q5R0mOJ+Z7vzghCayGNFPgWmnH+F
TeW/+wSIkc0+v5L8TK7NWwKBgBrlWlyLO9deUfqpHqihhICBYaEexOlGuF+yZfqT
eW7BdFBJ8OYm33sFCR+JHV/oZlIWT8o1Wizd9vPPtEWoQ1P4wg/D8Si6GwSIeWEI
YudD/HX4x7T/rmlI6qIAg9CYW18sqoRq3c2gm2fro6qPfYgiWIItLbWjUcBfd7Ki
QjTtAoGARKdRv3jMWL84rlEx1nBRgL3pe9Dt+Uxzde2xT3ZeF+5Hp9NfU01qE6M6
1I6H64smqpetlsXmCEVKwBemP3pJa6avLKgIYiQvHAD/v4rs9mqgy1RTqtYyGNhR
1A/6dKkbiZ6wzePLLPasXVZxSKEviXf5gJooqumQVSVhCswyCZ0=
-----END RSA PRIVATE KEY-----
+504
View File
@@ -0,0 +1,504 @@
package caddytest
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/cookiejar"
"os"
"path"
"regexp"
"runtime"
"strings"
"testing"
"time"
"github.com/aryann/difflib"
"github.com/caddyserver/caddy/v2/caddyconfig"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
// plug in Caddy modules here
_ "github.com/caddyserver/caddy/v2/modules/standard"
)
// Defaults store any configuration required to make the tests run
type Defaults struct {
// Port we expect caddy to listening on
AdminPort int
// Certificates we expect to be loaded before attempting to run the tests
Certifcates []string
// TestRequestTimeout is the time to wait for a http request to
TestRequestTimeout time.Duration
// LoadRequestTimeout is the time to wait for the config to be loaded against the caddy server
LoadRequestTimeout time.Duration
}
// Default testing values
var Default = Defaults{
AdminPort: 2019,
Certifcates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
TestRequestTimeout: 5 * time.Second,
LoadRequestTimeout: 5 * time.Second,
}
var (
matchKey = regexp.MustCompile(`(/[\w\d\.]+\.key)`)
matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`)
)
// Tester represents an instance of a test client.
type Tester struct {
Client *http.Client
configLoaded bool
t *testing.T
}
// NewTester will create a new testing client with an attached cookie jar
func NewTester(t *testing.T) *Tester {
jar, err := cookiejar.New(nil)
if err != nil {
t.Fatalf("failed to create cookiejar: %s", err)
}
return &Tester{
Client: &http.Client{
Transport: CreateTestingTransport(),
Jar: jar,
Timeout: Default.TestRequestTimeout,
},
configLoaded: false,
t: t,
}
}
type configLoadError struct {
Response string
}
func (e configLoadError) Error() string { return e.Response }
func timeElapsed(start time.Time, name string) {
elapsed := time.Since(start)
log.Printf("%s took %s", name, elapsed)
}
// InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type.
func (tc *Tester) InitServer(rawConfig string, configType string) {
if err := tc.initServer(rawConfig, configType); err != nil {
tc.t.Logf("failed to load config: %s", err)
tc.t.Fail()
}
}
// InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type.
func (tc *Tester) initServer(rawConfig string, configType string) error {
if testing.Short() {
tc.t.SkipNow()
return nil
}
err := validateTestPrerequisites()
if err != nil {
tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err)
return nil
}
tc.t.Cleanup(func() {
if tc.t.Failed() && tc.configLoaded {
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
tc.t.Log("unable to read the current config")
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
var out bytes.Buffer
json.Indent(&out, body, "", " ")
tc.t.Logf("----------- failed with config -----------\n%s", out.String())
}
})
rawConfig = prependCaddyFilePath(rawConfig)
client := &http.Client{
Timeout: Default.LoadRequestTimeout,
}
start := time.Now()
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig))
if err != nil {
tc.t.Errorf("failed to create request. %s", err)
return err
}
if configType == "json" {
req.Header.Add("Content-Type", "application/json")
} else {
req.Header.Add("Content-Type", "text/"+configType)
}
res, err := client.Do(req)
if err != nil {
tc.t.Errorf("unable to contact caddy server. %s", err)
return err
}
timeElapsed(start, "caddytest: config load time")
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
tc.t.Errorf("unable to read response. %s", err)
return err
}
if res.StatusCode != 200 {
return configLoadError{Response: string(body)}
}
tc.configLoaded = true
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")
}
return nil
}
hasValidated = true
arePrerequisitesValid = false
// check certificates are found
for _, certName := range Default.Certifcates {
if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) {
return fmt.Errorf("caddy integration test certificates (%s) not found", certName)
}
}
if isCaddyAdminRunning() != nil {
// start inprocess caddy server
os.Args = []string{"caddy", "run"}
go func() {
caddycmd.Main()
}()
// wait for caddy to start
retries := 4
for ; 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
}
func isCaddyAdminRunning() error {
// assert that caddy is running
client := &http.Client{
Timeout: Default.LoadRequestTimeout,
}
_, 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 nil
}
func getIntegrationDir() string {
_, filename, _, ok := runtime.Caller(1)
if !ok {
panic("unable to determine the current file path")
}
return path.Dir(filename)
}
// use the convention to replace /[certificatename].[crt|key] with the full path
// this helps reduce the noise in test configurations and also allow this
// to run in any path
func prependCaddyFilePath(rawConfig string) string {
r := matchKey.ReplaceAllString(rawConfig, getIntegrationDir()+"$1")
r = matchCert.ReplaceAllString(r, getIntegrationDir()+"$1")
return r
}
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
func CreateTestingTransport() *http.Transport {
dialer := net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 5 * time.Second,
DualStack: true,
}
dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
parts := strings.Split(addr, ":")
destAddr := fmt.Sprintf("127.0.0.1:%s", parts[1])
log.Printf("caddytest: redirecting the dialer from %s to %s", addr, destAddr)
return dialer.DialContext(ctx, network, destAddr)
}
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
// AssertLoadError will load a config and expect an error
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
tc := NewTester(t)
err := tc.initServer(rawConfig, configType)
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error())
}
}
// AssertRedirect makes a request and asserts the redirection happens
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
// using the existing client, we override the check redirect policy for this test
old := tc.Client.CheckRedirect
tc.Client.CheckRedirect = redirectPolicyFunc
defer func() { tc.Client.CheckRedirect = old }()
resp, err := tc.Client.Get(requestURI)
if err != nil {
tc.t.Errorf("failed to call server %s", err)
return nil
}
if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", requestURI, expectedStatusCode, resp.StatusCode)
}
loc, err := resp.Location()
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())
}
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 {
cfgAdapter := caddyconfig.GetAdapter(adapterName)
if cfgAdapter == nil {
t.Logf("unrecognized config adapter '%s'", adapterName)
return false
}
options := make(map[string]interface{})
options["pretty"] = "true"
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
if err != nil {
t.Logf("adapting config using %s adapter: %v", adapterName, err)
return false
}
if len(warnings) > 0 {
for _, w := range warnings {
t.Logf("warning: directive: %s : %s", w.Directive, w.Message)
}
}
diff := difflib.Diff(
strings.Split(expectedResponse, "\n"),
strings.Split(string(result), "\n"))
// scan for failure
failed := false
for _, d := range diff {
if d.Delta != difflib.Common {
failed = true
break
}
}
if failed {
for _, d := range diff {
switch d.Delta {
case difflib.Common:
fmt.Printf(" %s\n", d.Payload)
case difflib.LeftOnly:
fmt.Printf(" - %s\n", d.Payload)
case difflib.RightOnly:
fmt.Printf(" + %s\n", d.Payload)
}
}
return false
}
return true
}
// 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)
if !ok {
t.Fail()
}
}
// Generic request functions
func applyHeaders(t *testing.T, req *http.Request, requestHeaders []string) {
requestContentType := ""
for _, requestHeader := range requestHeaders {
arr := strings.SplitAfterN(requestHeader, ":", 2)
k := strings.TrimRight(arr[0], ":")
v := strings.TrimSpace(arr[1])
if k == "Content-Type" {
requestContentType = v
}
t.Logf("Request header: %s => %s", k, v)
req.Header.Set(k, v)
}
if requestContentType == "" {
t.Logf("Content-Type header not provided")
}
}
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
resp, err := tc.Client.Do(req)
if err != nil {
tc.t.Fatalf("failed to call server %s", err)
}
if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.RequestURI, expectedStatusCode, resp.StatusCode)
}
return resp
}
// AssertResponse request a URI and assert the status code and the body contains a string
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
resp := tc.AssertResponseCode(req, expectedStatusCode)
defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
tc.t.Fatalf("unable to read the response body %s", err)
}
body := string(bytes)
if !strings.Contains(body, expectedBody) {
tc.t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
}
return resp, body
}
// Verb specific test functions
// AssertGetResponse GET a URI and expect a statusCode and body text
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("GET", requestURI, nil)
if err != nil {
tc.t.Fatalf("unable to create request %s", err)
}
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertDeleteResponse request a URI and expect a statusCode and body text
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("DELETE", requestURI, nil)
if err != nil {
tc.t.Fatalf("unable to create request %s", err)
}
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPostResponseBody POST to a URI and assert the response code and body
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("POST", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPutResponseBody PUT to a URI and assert the response code and body
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PUT", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PATCH", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
+33
View File
@@ -0,0 +1,33 @@
package caddytest
import (
"strings"
"testing"
)
func TestReplaceCertificatePaths(t *testing.T) {
rawConfig := `a.caddy.localhost:9443 {
tls /caddy.localhost.crt /caddy.localhost.key {
}
redir / https://b.caddy.localhost:9443/version 301
respond /version 200 {
body "hello from a.caddy.localhost"
}
}`
r := prependCaddyFilePath(rawConfig)
if !strings.Contains(r, getIntegrationDir()+"/caddy.localhost.crt") {
t.Error("expected the /caddy.localhost.crt to be expanded to include the full path")
}
if !strings.Contains(r, getIntegrationDir()+"/caddy.localhost.key") {
t.Error("expected the /caddy.localhost.crt to be expanded to include the full path")
}
if !strings.Contains(r, "https://b.caddy.localhost:9443/version") {
t.Error("expected redirect uri to be unchanged")
}
}
@@ -0,0 +1,34 @@
{
auto_https disable_redirects
}
localhost
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
],
"automatic_https": {
"disable_redirects": true
}
}
}
}
}
}
@@ -0,0 +1,37 @@
{
auto_https off
}
localhost
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{}
],
"automatic_https": {
"disable": true
}
}
}
}
}
}
@@ -0,0 +1,73 @@
{
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 off
on_demand_tls {
ask https://example.com
interval 30s
burst 20
}
local_certs
key_type ed25519
}
:80
----------
{
"admin": {
"disabled": true
},
"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": [
{
"issuer": {
"module": "internal"
},
"key_type": "ed25519"
}
],
"on_demand": {
"rate_limit": {
"interval": 30000000000,
"burst": 20
},
"ask": "https://example.com"
}
}
}
}
}
@@ -0,0 +1,85 @@
{
debug
http_port 8080
https_port 8443
default_sni localhost
order root first
storage file_system {
root /data
}
acme_ca https://example.com
acme_eab {
key_id 4K2scIVbBpNd-78scadB2g
mac_key abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh
}
acme_ca_root /path/to/ca.crt
email test@example.com
admin off
on_demand_tls {
ask https://example.com
interval 30s
burst 20
}
key_type ed25519
}
:80
----------
{
"admin": {
"disabled": true
},
"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": [
{
"issuer": {
"ca": "https://example.com",
"email": "test@example.com",
"external_account": {
"key_id": "4K2scIVbBpNd-78scadB2g",
"mac_key": "abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh"
},
"module": "acme",
"trusted_roots_pem_files": [
"/path/to/ca.crt"
]
},
"key_type": "ed25519"
}
],
"on_demand": {
"rate_limit": {
"interval": 30000000000,
"burst": 20
},
"ask": "https://example.com"
}
}
}
}
}
@@ -0,0 +1,81 @@
{
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": [
{
"issuer": {
"module": "internal"
},
"key_type": "ed25519"
}
],
"on_demand": {
"rate_limit": {
"interval": 30000000000,
"burst": 20
},
"ask": "https://example.com"
}
}
}
}
}
@@ -0,0 +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"
}
]
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,37 @@
:80 {
respond /version 200 {
body "hello from localhost"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/version"
]
}
],
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,59 @@
http://a.caddy.localhost {
respond /version 200 {
body "hello from localhost"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"a.caddy.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"a.caddy.localhost"
]
}
}
}
}
}
}
@@ -0,0 +1,54 @@
localhost:80 {
respond /version 200 {
body "hello from localhost"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,59 @@
http://a.caddy.localhost:81 {
respond /version 200 {
body "hello from localhost"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":81"
],
"routes": [
{
"match": [
{
"host": [
"a.caddy.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"a.caddy.localhost"
]
}
}
}
}
}
}
@@ -0,0 +1,54 @@
a.caddy.localhost {
respond /version 200 {
body "hello from localhost"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.caddy.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +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
}
]
}
}
}
}
}
@@ -0,0 +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"
}
}
}
}
}
}
}
@@ -0,0 +1,47 @@
:80
log {
output file /var/log/access.log {
roll_size 1gb
roll_keep 5
roll_keep_for 90d
}
}
----------
{
"logging": {
"logs": {
"default": {
"exclude": [
"http.log.access.log0"
]
},
"log0": {
"writer": {
"filename": "/var/log/access.log",
"output": "file",
"roll_keep": 5,
"roll_keep_days": 90,
"roll_size_mb": 954
},
"include": [
"http.log.access.log0"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"logs": {
"default_logger_name": "log0"
}
}
}
}
}
}
@@ -0,0 +1,77 @@
:80 {
@matcher {
method GET
}
respond @matcher "get"
@matcher2 method POST
respond @matcher2 "post"
@matcher3 not method PUT
respond @matcher3 "not put"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"method": [
"GET"
]
}
],
"handle": [
{
"body": "get",
"handler": "static_response"
}
]
},
{
"match": [
{
"method": [
"POST"
]
}
],
"handle": [
{
"body": "post",
"handler": "static_response"
}
]
},
{
"match": [
{
"not": [
{
"method": [
"PUT"
]
}
]
}
],
"handle": [
{
"body": "not put",
"handler": "static_response"
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,49 @@
:80
@test {
not {
header Abc "123"
header Bcd "123"
}
}
respond @test 403
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"not": [
{
"header": {
"Abc": [
"123"
],
"Bcd": [
"123"
]
}
}
]
}
],
"handle": [
{
"handler": "static_response",
"status_code": 403
}
]
}
]
}
}
}
}
}
@@ -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"
]
}
]
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,66 @@
:8884
php_fastcgi localhost:9000 {
# some php_fastcgi-specific subdirectives
split .php .php5
env VAR1 value1
env VAR2 value2
root /var/www
index off
# passed through to reverse_proxy (directive order doesn't matter!)
lb_policy random
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"match": [
{
"path": [
"*.php",
"*.php5"
]
}
],
"handle": [
{
"handler": "reverse_proxy",
"load_balancing": {
"selection_policy": {
"policy": "random"
}
},
"transport": {
"env": {
"VAR1": "value1",
"VAR2": "value2"
},
"protocol": "fastcgi",
"root": "/var/www",
"split_path": [
".php",
".php5"
]
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +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"
]
}
]
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,118 @@
: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
# passed through to reverse_proxy (directive order doesn't matter!)
lb_policy random
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php5"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
],
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.uri.path}/"
]
},
"status_code": 308
}
]
},
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php5",
"index.php5"
],
"split_path": [
".php",
".php5"
]
}
}
],
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
]
},
{
"match": [
{
"path": [
"*.php",
"*.php5"
]
}
],
"handle": [
{
"handler": "reverse_proxy",
"load_balancing": {
"selection_policy": {
"policy": "random"
}
},
"transport": {
"env": {
"VAR1": "value1",
"VAR2": "value2"
},
"protocol": "fastcgi",
"root": "/var/www",
"split_path": [
".php",
".php5"
]
},
"upstreams": [
{
"dial": "localhost:9000"
}
]
}
]
}
]
}
}
}
}
}
@@ -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,96 @@
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"
transport http {
versions h2c 2
compression off
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"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,
"protocol": "http",
"versions": [
"h2c",
"2"
]
},
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
],
"match": [
{
"path": [
"/path"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,43 @@
localhost:80
respond * "{header.content-type} {labels.0} {query.p} {path.0} {re.name.0}"
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "{http.request.header.content-type} {http.request.host.labels.0} {http.request.uri.query.p} {http.request.uri.path.0} {http.regexp.name.0}",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,66 @@
localhost
respond "hello from localhost"
tls {
client_auth {
mode request
trusted_ca_cert_file ../caddy.ca.cer
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"match": {
"sni": [
"localhost"
]
},
"client_authentication": {
"trusted_ca_certs": [
"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"
}
},
{}
]
}
}
}
}
}
@@ -0,0 +1,66 @@
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==
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"match": {
"sni": [
"localhost"
]
},
"client_authentication": {
"trusted_ca_certs": [
"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"
}
},
{}
]
}
}
}
}
}
@@ -0,0 +1,47 @@
package integration
import (
"io/ioutil"
"regexp"
"strings"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestCaddyfileAdaptToJSON(t *testing.T) {
// load the list of test files from the dir
files, err := ioutil.ReadDir("./caddyfile_adapt")
if err != nil {
t.Errorf("failed to read caddyfile_adapt dir: %s", err)
}
// prep a regexp to fix strings on windows
winNewlines := regexp.MustCompile(`\r?\n`)
for _, f := range files {
if f.IsDir() {
continue
}
// read the test file
filename := f.Name()
data, err := ioutil.ReadFile("./caddyfile_adapt/" + filename)
if err != nil {
t.Errorf("failed to read %s dir: %s", filename, err)
}
// split the Caddyfile (first) and JSON (second) parts
parts := strings.Split(string(data), "----------")
caddyfile, json := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
// replace windows newlines in the json with unix newlines
json = winNewlines.ReplaceAllString(json, "\n")
// run the test
ok := caddytest.CompareAdapt(t, caddyfile, "caddyfile", json)
if !ok {
t.Errorf("failed to adapt %s", filename)
}
}
}
+103
View File
@@ -0,0 +1,103 @@
package integration
import (
"net/http"
"net/url"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestRespond(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
}
localhost:9080 {
respond /version 200 {
body "hello from localhost"
}
}
`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost")
}
func TestRedirect(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
}
localhost:9080 {
redir / http://localhost:9080/hello 301
respond /hello 200 {
body "hello from localhost"
}
}
`, "caddyfile")
// act and assert
tester.AssertRedirect("http://localhost:9080/", "http://localhost:9080/hello", 301)
// follow redirect
tester.AssertGetResponse("http://localhost:9080/", 200, "hello from localhost")
}
func TestDuplicateHosts(t *testing.T) {
// act and assert
caddytest.AssertLoadError(t,
`
localhost:9080 {
}
localhost:9080 {
}
`,
"caddyfile",
"duplicate site address not allowed")
}
func TestReadCookie(t *testing.T) {
localhost, _ := url.Parse("http://localhost")
cookie := http.Cookie{
Name: "clientname",
Value: "caddytest",
}
// arrange
tester := caddytest.NewTester(t)
tester.Client.Jar.SetCookies(localhost, []*http.Cookie{&cookie})
tester.InitServer(`
{
http_port 9080
https_port 9443
}
localhost:9080 {
templates {
root testdata
}
file_server {
root testdata
}
}
`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/cookie.html", 200, "<h2>Cookie.ClientName caddytest</h2>")
}
+143
View File
@@ -0,0 +1,143 @@
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-name {
default unknown
G.T get-called
POST post-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 post-called")
}
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",
"destination": "dest-name",
"default": "unknown",
"items": [
{
"expression": "GET",
"value": "get-called"
},
{
"expression": "POST",
"value": "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")
}
+320
View File
@@ -0,0 +1,320 @@
package integration
import (
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestDefaultSNI(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from a.caddy.localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"match": [
{
"host": [
"127.0.0.1"
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"match": {
"sni": [
"127.0.0.1"
]
}
},
{
"default_sni": "*.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/caddy.localhost.crt",
"key": "/caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
// act and assert
// makes a request with no sni
tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a")
}
func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from a",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"match": [
{
"host": [
"a.caddy.localhost",
"127.0.0.1"
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost",
"match": {
"sni": [
"a.caddy.localhost",
"127.0.0.1",
""
]
}
},
{
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
// act and assert
// makes a request with no sni
tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a")
}
func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"body": "hello from a.caddy.localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
// act and assert
// makes a request with no sni
tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a")
}
func TestHttpOnlyOnDomainWithSNI(t *testing.T) {
caddytest.AssertAdapt(t, `
{
default_sni a.caddy.localhost
}
:80 {
respond /version 200 {
body "hello from localhost"
}
}
`, "caddyfile", `{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/version"
]
}
],
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
]
}
]
}
}
}
}
}`)
}
+437
View File
@@ -0,0 +1,437 @@
package integration
import (
"compress/gzip"
"context"
"crypto/rand"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"testing"
"time"
"github.com/caddyserver/caddy/v2/caddytest"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
// (see https://github.com/caddyserver/caddy/issues/3556 for use case)
func TestH2ToH2CStream(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http",
"compression": false,
"versions": [
"h2c",
"2"
]
},
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
],
"match": [
{
"path": [
"/tov2ray"
]
}
]
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
expectedBody := "some data to be echoed"
// start the server
server := testH2ToH2CStreamServeH2C(t)
go server.ListenAndServe()
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
defer cancel()
server.Shutdown(ctx)
}()
r, w := io.Pipe()
req := &http.Request{
Method: "PUT",
Body: ioutil.NopCloser(r),
URL: &url.URL{
Scheme: "https",
Host: "127.0.0.1:9443",
Path: "/tov2ray",
},
Proto: "HTTP/2",
ProtoMajor: 2,
ProtoMinor: 0,
Header: make(http.Header),
}
// Disable any compression method from server.
req.Header.Set("Accept-Encoding", "identity")
resp := tester.AssertResponseCode(req, 200)
if 200 != resp.StatusCode {
return
}
go func() {
fmt.Fprint(w, expectedBody)
w.Close()
}()
defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("unable to read the response body %s", err)
}
body := string(bytes)
if !strings.Contains(body, expectedBody) {
t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
}
return
}
func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
h2s := &http2.Server{}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rstring, err := httputil.DumpRequest(r, false)
if err == nil {
t.Logf("h2c server received req: %s", rstring)
}
// We only accept HTTP/2!
if r.ProtoMajor != 2 {
t.Error("Not a HTTP/2 request, rejected!")
w.WriteHeader(http.StatusInternalServerError)
return
}
if r.Host != "127.0.0.1:9443" {
t.Errorf("r.Host doesn't match, %v!", r.Host)
w.WriteHeader(http.StatusNotFound)
return
}
if !strings.HasPrefix(r.URL.Path, "/tov2ray") {
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(200)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
buf := make([]byte, 4*1024)
for {
n, err := r.Body.Read(buf)
if n > 0 {
w.Write(buf[:n])
}
if err != nil {
if err == io.EOF {
r.Body.Close()
}
break
}
}
})
server := &http.Server{
Addr: "127.0.0.1:54321",
Handler: h2c.NewHandler(handler, h2s),
}
return server
}
// (see https://github.com/caddyserver/caddy/issues/3606 for use case)
func TestH2ToH1ChunkedResponse(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
"logging": {
"logs": {
"default": {
"level": "DEBUG"
}
}
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"encodings": {
"gzip": {}
},
"handler": "encode"
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
],
"match": [
{
"path": [
"/tov2ray"
]
}
]
}
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": [
"cert0"
]
},
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
}
}
}
`, "json")
// need a large body here to trigger caddy's compression, larger than gzip.miniLength
expectedBody, err := GenerateRandomString(1024)
if err != nil {
t.Fatalf("generate expected body failed, err: %s", err)
}
// start the server
server := testH2ToH1ChunkedResponseServeH1(t)
go server.ListenAndServe()
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
defer cancel()
server.Shutdown(ctx)
}()
r, w := io.Pipe()
req := &http.Request{
Method: "PUT",
Body: ioutil.NopCloser(r),
URL: &url.URL{
Scheme: "https",
Host: "127.0.0.1:9443",
Path: "/tov2ray",
},
Proto: "HTTP/2",
ProtoMajor: 2,
ProtoMinor: 0,
Header: make(http.Header),
}
// underlying transport will automaticlly add gzip
// req.Header.Set("Accept-Encoding", "gzip")
go func() {
fmt.Fprint(w, expectedBody)
w.Close()
}()
resp := tester.AssertResponseCode(req, 200)
if 200 != resp.StatusCode {
return
}
defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("unable to read the response body %s", err)
}
body := string(bytes)
if body != expectedBody {
t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
}
return
}
func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Host != "127.0.0.1:9443" {
t.Errorf("r.Host doesn't match, %v!", r.Host)
w.WriteHeader(http.StatusNotFound)
return
}
if !strings.HasPrefix(r.URL.Path, "/tov2ray") {
w.WriteHeader(http.StatusNotFound)
return
}
defer r.Body.Close()
bytes, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Fatalf("unable to read the response body %s", err)
}
n := len(bytes)
var writer io.Writer
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
gw, err := gzip.NewWriterLevel(w, 5)
if err != nil {
t.Error("can't return gzip data")
w.WriteHeader(http.StatusInternalServerError)
return
}
defer gw.Close()
writer = gw
w.Header().Set("Content-Encoding", "gzip")
w.Header().Del("Content-Length")
w.WriteHeader(200)
} else {
writer = w
}
if n > 0 {
writer.Write(bytes[:])
}
})
server := &http.Server{
Addr: "127.0.0.1:54321",
Handler: handler,
}
return server
}
// GenerateRandomBytes returns securely generated random bytes.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
return nil, err
}
return b, nil
}
// GenerateRandomString returns a securely generated random string.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomString(n int) (string, error) {
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
bytes, err := GenerateRandomBytes(n)
if err != nil {
return "", err
}
for i, b := range bytes {
bytes[i] = letters[b%byte(len(letters))]
}
return string(bytes), nil
}
+1
View File
@@ -0,0 +1 @@
<h2>Cookie.ClientName {{.Cookie "clientname"}}</h2>
+1
View File
@@ -0,0 +1 @@
respond "'I am {args.0}', hears {args.1}"
+143 -72
View File
@@ -16,6 +16,7 @@ package caddycmd
import (
"bytes"
"context"
"crypto/rand"
"encoding/json"
"fmt"
@@ -34,13 +35,15 @@ import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/mholt/certmagic"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"go.uber.org/zap"
)
func cmdStart(fl Flags) (int, error) {
startCmdConfigFlag := fl.String("config")
startCmdConfigAdapterFlag := fl.String("adapter")
startCmdPidfileFlag := fl.String("pidfile")
startCmdWatchFlag := fl.Bool("watch")
// open a listener to which the child process will connect when
// it is ready to confirm that it has successfully started
@@ -67,6 +70,12 @@ func cmdStart(fl Flags) (int, error) {
if startCmdConfigAdapterFlag != "" {
cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag)
}
if startCmdWatchFlag {
cmd.Args = append(cmd.Args, "--watch")
}
if startCmdPidfileFlag != "" {
cmd.Args = append(cmd.Args, "--pidfile", startCmdPidfileFlag)
}
stdinpipe, err := cmd.StdinPipe()
if err != nil {
return caddy.ExitCodeFailedStartup,
@@ -84,7 +93,7 @@ func cmdStart(fl Flags) (int, error) {
// begin writing the confirmation bytes to the child's
// stdin; use a goroutine since the child hasn't been
// started yet, and writing sychronously would result
// started yet, and writing synchronously would result
// in a deadlock
go func() {
stdinpipe.Write(expect)
@@ -130,7 +139,7 @@ func cmdStart(fl Flags) (int, error) {
// when one of the goroutines unblocks, we're done and can exit
select {
case <-success:
fmt.Printf("Successfully started Caddy (pid=%d)\n", cmd.Process.Pid)
fmt.Printf("Successfully started Caddy (pid=%d) - Caddy is running in the background\n", cmd.Process.Pid)
case err := <-exit:
return caddy.ExitCodeFailedStartup,
fmt.Errorf("caddy process exited with error: %v", err)
@@ -140,12 +149,25 @@ func cmdStart(fl Flags) (int, error) {
}
func cmdRun(fl Flags) (int, error) {
caddy.TrapSignals()
runCmdConfigFlag := fl.String("config")
runCmdConfigAdapterFlag := fl.String("adapter")
runCmdResumeFlag := fl.Bool("resume")
runCmdLoadEnvfileFlag := fl.String("envfile")
runCmdPrintEnvFlag := fl.Bool("environ")
runCmdWatchFlag := fl.Bool("watch")
runCmdPidfileFlag := fl.String("pidfile")
runCmdPingbackFlag := fl.String("pingback")
// load all additional envs as soon as possible
if runCmdLoadEnvfileFlag != "" {
if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("loading additional environment variables: %v", err)
}
}
// if we are supposed to print the environment, do that first
if runCmdPrintEnvFlag {
printEnvironment()
@@ -166,22 +188,26 @@ func cmdRun(fl Flags) (int, error) {
} else if err != nil {
return caddy.ExitCodeFailedStartup, err
} else {
caddy.Log().Info("resuming from last configuration", zap.String("autosave_file", caddy.ConfigAutosavePath))
if runCmdConfigFlag == "" {
caddy.Log().Info("resuming from last configuration",
zap.String("autosave_file", caddy.ConfigAutosavePath))
} else {
// if they also specified a config file, user should be aware that we're not
// using it (doing so could lead to data/config loss by overwriting!)
caddy.Log().Warn("--config and --resume flags were used together; ignoring --config and resuming from last configuration",
zap.String("autosave_file", caddy.ConfigAutosavePath))
}
}
}
// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
var configFile string
if !runCmdResumeFlag {
config, _, err = loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
config, configFile, err = loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
}
// set a fitting User-Agent for ACME requests
goModule := caddy.GoModule()
cleanModVersion := strings.TrimPrefix(goModule.Version, "v")
certmagic.UserAgent = "Caddy/" + cleanModVersion
// run the initial config
err = caddy.Load(config, true)
if err != nil {
@@ -210,6 +236,22 @@ func cmdRun(fl Flags) (int, error) {
}
}
// if enabled, reload config file automatically on changes
// (this better only be used in dev!)
if runCmdWatchFlag {
go watchConfigFile(configFile, runCmdConfigAdapterFlag)
}
// create pidfile
if runCmdPidfileFlag != "" {
err := caddy.PIDFile(runCmdPidfileFlag)
if err != nil {
caddy.Log().Error("unable to write PID file",
zap.String("pidfile", runCmdPidfileFlag),
zap.Error(err))
}
}
// warn if the environment does not provide enough information about the disk
hasXDG := os.Getenv("XDG_DATA_HOME") != "" &&
os.Getenv("XDG_CONFIG_HOME") != "" &&
@@ -235,24 +277,9 @@ func cmdRun(fl Flags) (int, error) {
func cmdStop(fl Flags) (int, error) {
stopCmdAddrFlag := fl.String("address")
adminAddr := caddy.DefaultAdminListen
if stopCmdAddrFlag != "" {
adminAddr = stopCmdAddrFlag
}
stopEndpoint := fmt.Sprintf("http://%s/stop", adminAddr)
req, err := http.NewRequest(http.MethodPost, stopEndpoint, nil)
err := apiRequest(stopCmdAddrFlag, http.MethodPost, "/stop", nil)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("making request: %v", err)
}
req.Header.Set("Origin", adminAddr)
err = apiRequest(req)
if err != nil {
caddy.Log().Warn("failed using API to stop instance",
zap.String("endpoint", stopEndpoint),
zap.Error(err),
)
caddy.Log().Warn("failed using API to stop instance", zap.Error(err))
return caddy.ExitCodeFailedStartup, err
}
@@ -265,15 +292,15 @@ func cmdReload(fl Flags) (int, error) {
reloadCmdAddrFlag := fl.String("address")
// get the config in caddy's native format
config, hasConfig, err := loadConfig(reloadCmdConfigFlag, reloadCmdConfigAdapterFlag)
config, configFile, err := loadConfig(reloadCmdConfigFlag, reloadCmdConfigAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
if !hasConfig {
if configFile == "" {
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
}
// get the address of the admin listener and craft endpoint URL
// get the address of the admin listener; use flag if specified
adminAddr := reloadCmdAddrFlag
if adminAddr == "" && len(config) > 0 {
var tmpStruct struct {
@@ -286,20 +313,8 @@ func cmdReload(fl Flags) (int, error) {
}
adminAddr = tmpStruct.Admin.Listen
}
if adminAddr == "" {
adminAddr = caddy.DefaultAdminListen
}
loadEndpoint := fmt.Sprintf("http://%s/load", adminAddr)
// prepare the request to update the configuration
req, err := http.NewRequest(http.MethodPost, loadEndpoint, bytes.NewReader(config))
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("making request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Origin", adminAddr)
err = apiRequest(req)
err = apiRequest(adminAddr, http.MethodPost, "/load", bytes.NewReader(config))
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err)
}
@@ -491,35 +506,10 @@ func cmdValidateConfig(fl Flags) (int, error) {
validateCmdConfigFlag := fl.String("config")
validateCmdAdapterFlag := fl.String("adapter")
input, err := ioutil.ReadFile(validateCmdConfigFlag)
input, _, err := loadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading input file: %v", err)
return caddy.ExitCodeFailedStartup, err
}
if validateCmdAdapterFlag != "" {
cfgAdapter := caddyconfig.GetAdapter(validateCmdAdapterFlag)
if cfgAdapter == nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("unrecognized config adapter: %s", validateCmdAdapterFlag)
}
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, nil)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// print warnings to stderr
for _, warn := range warnings {
msg := warn.Message
if warn.Directive != "" {
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
}
fmt.Fprintf(os.Stderr, "[WARNING][%s] %s:%d: %s\n", validateCmdAdapterFlag, warn.File, warn.Line, msg)
}
input = adaptedConfig
}
input = caddy.RemoveMetaFields(input)
var cfg *caddy.Config
@@ -538,6 +528,33 @@ func cmdValidateConfig(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil
}
func cmdFmt(fl Flags) (int, error) {
formatCmdConfigFile := fl.Arg(0)
if formatCmdConfigFile == "" {
formatCmdConfigFile = "Caddyfile"
}
overwrite := fl.Bool("overwrite")
input, err := ioutil.ReadFile(formatCmdConfigFile)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading input file: %v", err)
}
output := caddyfile.Format(input)
if overwrite {
err = ioutil.WriteFile(formatCmdConfigFile, output, 0644)
if err != nil {
return caddy.ExitCodeFailedStartup, nil
}
} else {
fmt.Print(string(output))
}
return caddy.ExitCodeSuccess, nil
}
func cmdHelp(fl Flags) (int, error) {
const fullDocs = `Full documentation is available at:
https://caddyserver.com/docs/command-line`
@@ -602,8 +619,62 @@ commands:
return caddy.ExitCodeSuccess, nil
}
func apiRequest(req *http.Request) error {
resp, err := http.DefaultClient.Do(req)
// apiRequest makes an API request to the endpoint adminAddr with the
// given HTTP method and request URI. If body is non-nil, it will be
// assumed to be Content-Type application/json.
func apiRequest(adminAddr, method, uri string, body io.Reader) error {
// parse the admin address
if adminAddr == "" {
adminAddr = caddy.DefaultAdminListen
}
parsedAddr, err := caddy.ParseNetworkAddress(adminAddr)
if err != nil || parsedAddr.PortRangeSize() > 1 {
return fmt.Errorf("invalid admin address %s: %v", adminAddr, err)
}
origin := parsedAddr.JoinHostPort(0)
if parsedAddr.IsUnixNetwork() {
origin = "unixsocket" // hack so that http.NewRequest() is happy
}
// form the request
req, err := http.NewRequest(method, "http://"+origin+uri, body)
if err != nil {
return fmt.Errorf("making request: %v", err)
}
if parsedAddr.IsUnixNetwork() {
// When listening on a unix socket, the admin endpoint doesn't
// accept any Host header because there is no host:port for
// a unix socket's address. The server's host check is fairly
// strict for security reasons, so we don't allow just any
// Host header. For unix sockets, the Host header must be
// empty. Unfortunately, Go makes it impossible to make HTTP
// requests with an empty Host header... except with this one
// weird trick. (Hopefully they don't fix it. It's already
// hard enough to use HTTP over unix sockets.)
//
// An equivalent curl command would be something like:
// $ curl --unix-socket caddy.sock http:/:$REQUEST_URI
req.URL.Host = " "
req.Host = ""
} else {
req.Header.Set("Origin", origin)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
// make an HTTP client that dials our network type, since admin
// endpoints aren't always TCP, which is what the default transport
// expects; reuse is not of particular concern here
client := http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial(parsedAddr.Network, parsedAddr.JoinHostPort(0))
},
},
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("performing request: %v", err)
}
+37 -5
View File
@@ -27,7 +27,7 @@ type Command struct {
// Required.
Name string
// Run is a function that executes a subcommand using
// Func is a function that executes a subcommand using
// the parsed flags. It returns an exit code and any
// associated error.
// Required.
@@ -74,7 +74,7 @@ func init() {
RegisterCommand(Command{
Name: "start",
Func: cmdStart,
Usage: "[--config <path> [[--adapter <name>]]",
Usage: "[--config <path> [--adapter <name>]] [--watch] [--pidfile <file>]",
Short: "Starts the Caddy process in the background and then returns",
Long: `
Starts the Caddy process, optionally bootstrapped with an initial config file.
@@ -87,6 +87,8 @@ using 'caddy run' instead to keep it in the foreground.`,
fs := flag.NewFlagSet("start", flag.ExitOnError)
fs.String("config", "", "Configuration file")
fs.String("adapter", "", "Name of config adapter to apply")
fs.String("pidfile", "", "Path of file to which to write process ID")
fs.Bool("watch", false, "Reload changed config file automatically")
return fs
}(),
})
@@ -94,7 +96,7 @@ using 'caddy run' instead to keep it in the foreground.`,
RegisterCommand(Command{
Name: "run",
Func: cmdRun,
Usage: "[--config <path> [--adapter <name>]] [--environ]",
Usage: "[--config <path> [--adapter <name>]] [--envfile <path>] [--environ] [--resume] [--watch] [--pidfile <fil>]",
Short: `Starts the Caddy process and blocks indefinitely`,
Long: `
Starts the Caddy process, optionally bootstrapped with an initial config file,
@@ -114,18 +116,28 @@ As a special case, if the current working directory has a file called
that file will be loaded and used to configure Caddy, even without any command
line flags.
If --envfile is specified, an environment file with environment variables in
the KEY=VALUE format will be loaded into the Caddy process.
If --environ is specified, the environment as seen by the Caddy process will
be printed before starting. This is the same as the environ command but does
not quit after printing, and can be useful for troubleshooting.
The --resume flag will override the --config flag if there is a config auto-
save file. It is not an error if --resume is used and no autosave file exists.`,
save file. It is not an error if --resume is used and no autosave file exists.
If --watch is specified, the config file will be loaded automatically after
changes. This is dangerous in production! Only use this option in a local
development environment.`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("run", flag.ExitOnError)
fs.String("config", "", "Configuration file")
fs.String("adapter", "", "Name of config adapter to apply")
fs.Bool("resume", false, "Use saved config, if any (and prefer over --config file)")
fs.String("envfile", "", "Environment file to load")
fs.Bool("environ", false, "Print environment")
fs.Bool("resume", false, "Use saved config, if any (and prefer over --config file)")
fs.Bool("watch", false, "Watch config file for changes and reload it automatically")
fs.String("pidfile", "", "Path of file to which to write process ID")
fs.String("pingback", "", "Echo confirmation bytes to this address on success")
return fs
}(),
@@ -242,6 +254,24 @@ provisioning stages.`,
}(),
})
RegisterCommand(Command{
Name: "fmt",
Func: cmdFmt,
Usage: "[--overwrite] [<path>]",
Short: "Formats a Caddyfile",
Long: `
Formats the Caddyfile by adding proper indentation and spaces to improve
human readability. It prints the result to stdout.
If --write is specified, the output will be written to the config file
directly instead of printing it.`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("format", flag.ExitOnError)
fs.Bool("overwrite", false, "Overwrite the input file with the results")
return fs
}(),
})
}
// RegisterCommand registers the command cmd.
@@ -256,6 +286,8 @@ provisioning stages.`,
// This function panics if the name is already registered,
// if the name does not meet the described format, or if
// any of the fields are missing from cmd.
//
// This function should be used in init().
func RegisterCommand(cmd Command) {
if cmd.Name == "" {
panic("command name is required")
+175 -10
View File
@@ -15,29 +15,42 @@
package caddycmd
import (
"bufio"
"bytes"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"strconv"
"strings"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/certmagic"
"go.uber.org/zap"
)
func init() {
// set a fitting User-Agent for ACME requests
goModule := caddy.GoModule()
cleanModVersion := strings.TrimPrefix(goModule.Version, "v")
certmagic.UserAgent = "Caddy/" + cleanModVersion
// by using Caddy, user indicates agreement to CA terms
// (very important, or ACME account creation will fail!)
certmagic.DefaultACME.Agreed = true
}
// Main implements the main function of the caddy command.
// Call this if Caddy is to be the main() if your program.
func Main() {
caddy.TrapSignals()
switch len(os.Args) {
case 0:
fmt.Printf("[FATAL] no arguments provided by OS; args[0] must be command\n")
@@ -99,10 +112,10 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
// there is no config available. It prints any warnings to stderr,
// and returns the resulting JSON config bytes along with
// whether a config file was loaded or not.
func loadConfig(configFile, adapterName string) ([]byte, bool, error) {
func loadConfig(configFile, adapterName string) ([]byte, string, error) {
// specifying an adapter without a config file is ambiguous
if adapterName != "" && configFile == "" {
return nil, false, fmt.Errorf("cannot adapt config without config file (use --config)")
return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)")
}
// load initial config and adapter
@@ -112,7 +125,7 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) {
if configFile != "" {
config, err = ioutil.ReadFile(configFile)
if err != nil {
return nil, false, fmt.Errorf("reading config file: %v", err)
return nil, "", fmt.Errorf("reading config file: %v", err)
}
caddy.Log().Info("using provided configuration",
zap.String("config_file", configFile),
@@ -129,7 +142,7 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) {
cfgAdapter = nil
} else if err != nil {
// default Caddyfile exists, but error reading it
return nil, false, fmt.Errorf("reading default Caddyfile: %v", err)
return nil, "", fmt.Errorf("reading default Caddyfile: %v", err)
} else {
// success reading default Caddyfile
configFile = "Caddyfile"
@@ -151,7 +164,7 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) {
if adapterName != "" {
cfgAdapter = caddyconfig.GetAdapter(adapterName)
if cfgAdapter == nil {
return nil, false, fmt.Errorf("unrecognized config adapter: %s", adapterName)
return nil, "", fmt.Errorf("unrecognized config adapter: %s", adapterName)
}
}
@@ -161,7 +174,7 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) {
"filename": configFile,
})
if err != nil {
return nil, false, fmt.Errorf("adapting config using %s: %v", adapterName, err)
return nil, "", fmt.Errorf("adapting config using %s: %v", adapterName, err)
}
for _, warn := range warnings {
msg := warn.Message
@@ -173,7 +186,91 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) {
config = adaptedConfig
}
return config, configFile != "", nil
return config, configFile, nil
}
// watchConfigFile watches the config file at filename for changes
// and reloads the config if the file was updated. This function
// blocks indefinitely; it only quits if the poller has errors for
// long enough time. The filename passed in must be the actual
// config file used, not one to be discovered.
func watchConfigFile(filename, adapterName string) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] watching config file: %v\n%s", err, debug.Stack())
}
}()
// make our logger; since config reloads can change the
// default logger, we need to get it dynamically each time
logger := func() *zap.Logger {
return caddy.Log().
Named("watcher").
With(zap.String("config_file", filename))
}
// get the initial timestamp on the config file
info, err := os.Stat(filename)
if err != nil {
logger().Error("cannot watch config file", zap.Error(err))
return
}
lastModified := info.ModTime()
logger().Info("watching config file for changes")
// if the file disappears or something, we can
// stop polling if the error lasts long enough
var lastErr time.Time
finalError := func(err error) bool {
if lastErr.IsZero() {
lastErr = time.Now()
return false
}
if time.Since(lastErr) > 30*time.Second {
logger().Error("giving up watching config file; too many errors",
zap.Error(err))
return true
}
return false
}
// begin poller
for range time.Tick(1 * time.Second) {
// get the file info
info, err := os.Stat(filename)
if err != nil {
if finalError(err) {
return
}
continue
}
lastErr = time.Time{} // no error, so clear any memory of one
// if it hasn't changed, nothing to do
if !info.ModTime().After(lastModified) {
continue
}
logger().Info("config file changed; reloading")
// remember this timestamp
lastModified = info.ModTime()
// load the contents of the file
config, _, err := loadConfig(filename, adapterName)
if err != nil {
logger().Error("unable to load latest config", zap.Error(err))
continue
}
// apply the updated config
err = caddy.Load(config, false)
if err != nil {
logger().Error("applying latest config", zap.Error(err))
continue
}
}
}
// Flags wraps a FlagSet so that typed values
@@ -221,7 +318,7 @@ func (f Flags) Float64(name string) float64 {
// is not a duration type. It panics if the flag is
// not in the flag set.
func (f Flags) Duration(name string) time.Duration {
val, _ := time.ParseDuration(f.String(name))
val, _ := caddy.ParseDuration(f.String(name))
return val
}
@@ -241,11 +338,79 @@ func flagHelp(fs *flag.FlagSet) string {
return buf.String()
}
func loadEnvFromFile(envFile string) error {
file, err := os.Open(envFile)
if err != nil {
return fmt.Errorf("reading environment file: %v", err)
}
defer file.Close()
envMap, err := parseEnvFile(file)
if err != nil {
return fmt.Errorf("parsing environment file: %v", err)
}
for k, v := range envMap {
if err := os.Setenv(k, v); err != nil {
return fmt.Errorf("setting environment variables: %v", err)
}
}
return nil
}
func parseEnvFile(envInput io.Reader) (map[string]string, error) {
envMap := make(map[string]string)
scanner := bufio.NewScanner(envInput)
var line string
lineNumber := 0
for scanner.Scan() {
line = strings.TrimSpace(scanner.Text())
lineNumber++
// skip lines starting with comment
if strings.HasPrefix(line, "#") {
continue
}
// skip empty line
if len(line) == 0 {
continue
}
fields := strings.SplitN(line, "=", 2)
if len(fields) != 2 {
return nil, fmt.Errorf("can't parse line %d; line should be in KEY=VALUE format", lineNumber)
}
if strings.Contains(fields[0], " ") {
return nil, fmt.Errorf("bad key on line %d: contains whitespace", lineNumber)
}
key := fields[0]
val := fields[1]
if key == "" {
return nil, fmt.Errorf("missing or empty key on line %d", lineNumber)
}
envMap[key] = val
}
if err := scanner.Err(); err != nil {
return nil, err
}
return envMap, nil
}
func printEnvironment() {
fmt.Printf("caddy.HomeDir=%s\n", caddy.HomeDir())
fmt.Printf("caddy.AppDataDir=%s\n", caddy.AppDataDir())
fmt.Printf("caddy.AppConfigDir=%s\n", caddy.AppConfigDir())
fmt.Printf("caddy.ConfigAutosavePath=%s\n", caddy.ConfigAutosavePath)
fmt.Printf("caddy.Version=%s\n", caddy.GoModule().Version)
fmt.Printf("runtime.GOOS=%s\n", runtime.GOOS)
fmt.Printf("runtime.GOARCH=%s\n", runtime.GOARCH)
fmt.Printf("runtime.Compiler=%s\n", runtime.Compiler)
+38 -7
View File
@@ -21,7 +21,7 @@ import (
"log"
"reflect"
"github.com/mholt/certmagic"
"github.com/caddyserver/certmagic"
"go.uber.org/zap"
)
@@ -92,6 +92,7 @@ func (ctx *Context) OnCancel(f func()) {
//
// json.RawMessage => interface{}
// []json.RawMessage => []interface{}
// [][]json.RawMessage => [][]interface{}
// map[string]json.RawMessage => map[string]interface{}
// []map[string]json.RawMessage => []map[string]interface{}
//
@@ -179,6 +180,27 @@ func (ctx Context) LoadModule(structPointer interface{}, fieldName string) (inte
}
result = all
} else if typ.Elem().Kind() == reflect.Slice && isJSONRawMessage(typ.Elem().Elem()) {
// val is `[][]json.RawMessage`
if inlineModuleKey == "" {
panic("unable to determine module name without inline_key because type is not a ModuleMap")
}
var all [][]interface{}
for i := 0; i < val.Len(); i++ {
innerVal := val.Index(i)
var allInner []interface{}
for j := 0; j < innerVal.Len(); j++ {
innerInnerVal, err := ctx.loadModuleInline(inlineModuleKey, moduleNamespace, innerVal.Index(j).Interface().(json.RawMessage))
if err != nil {
return nil, fmt.Errorf("position %d: %v", j, err)
}
allInner = append(allInner, innerInnerVal)
}
all = append(all, allInner)
}
result = all
} else if isModuleMapType(typ.Elem()) {
// val is `[]map[string]json.RawMessage`
@@ -211,7 +233,7 @@ func (ctx Context) LoadModule(structPointer interface{}, fieldName string) (inte
}
// loadModulesFromSomeMap loads modules from val, which must be a type of map[string]interface{}.
// Depending on inlineModuleKey, it will be interpeted as either a ModuleMap (key is the module
// Depending on inlineModuleKey, it will be interpreted as either a ModuleMap (key is the module
// name) or as a regular map (key is not the module name, and module name is defined inline).
func (ctx Context) loadModulesFromSomeMap(namespace, inlineModuleKey string, val reflect.Value) (map[string]interface{}, error) {
// if no inline_key is specified, then val must be a ModuleMap,
@@ -377,16 +399,25 @@ func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.
return val, nil
}
// App returns the configured app named name. If no app with
// that name is currently configured, a new empty one will be
// instantiated. (The app module must still be registered.)
// App returns the configured app named name. If that app has
// not yet been loaded and provisioned, it will be immediately
// loaded and provisioned. If no app with that name is
// configured, a new empty one will be instantiated instead.
// (The app module must still be registered.) This must not be
// called during the Provision/Validate phase to reference a
// module's own host app (since the parent app module is still
// in the process of being provisioned, it is not yet ready).
func (ctx Context) App(name string) (interface{}, error) {
if app, ok := ctx.cfg.apps[name]; ok {
return app, nil
}
modVal, err := ctx.LoadModuleByID(name, nil)
appRaw := ctx.cfg.AppsRaw[name]
modVal, err := ctx.LoadModuleByID(name, appRaw)
if err != nil {
return nil, fmt.Errorf("instantiating new module %s: %v", name, err)
return nil, fmt.Errorf("loading %s app module: %v", name, err)
}
if appRaw != nil {
ctx.cfg.AppsRaw[name] = nil // allow GC to deallocate
}
ctx.cfg.apps[name] = modVal.(App)
return modVal, nil
+24 -29
View File
@@ -1,38 +1,33 @@
module github.com/caddyserver/caddy/v2
go 1.13
go 1.14
require (
github.com/Masterminds/sprig/v3 v3.0.0
github.com/alecthomas/chroma v0.7.0
github.com/andybalholm/brotli v0.0.0-20190821151343-b60f0d972eeb
github.com/cenkalti/backoff/v3 v3.1.1 // indirect
github.com/dustin/go-humanize v1.0.0
github.com/go-acme/lego/v3 v3.3.0
github.com/golang/groupcache v0.0.0-20191002201903-404acd9df4cc
github.com/ilibs/json5 v1.0.1
github.com/imdario/mergo v0.3.8 // indirect
github.com/Masterminds/sprig/v3 v3.1.0
github.com/alecthomas/chroma v0.8.0
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a
github.com/caddyserver/certmagic v0.11.3-0.20200810220624-10a8b5c72339
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
github.com/go-chi/chi v4.1.2+incompatible
github.com/google/cel-go v0.5.1
github.com/jsternberg/zap-logfmt v1.2.0
github.com/klauspost/compress v1.8.6
github.com/klauspost/cpuid v1.2.2
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lucas-clemente/quic-go v0.14.1
github.com/mholt/certmagic v0.9.3
github.com/miekg/dns v1.1.25 // indirect
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20190906142622-1265e9b150c6
github.com/klauspost/compress v1.10.10
github.com/klauspost/cpuid v1.2.5
github.com/lucas-clemente/quic-go v0.17.3
github.com/mholt/acmez v0.1.1-0.20200810215816-dbe88fc6cf09
github.com/naoina/go-stringutil v0.1.0 // indirect
github.com/naoina/toml v0.1.1
github.com/onsi/ginkgo v1.8.0 // indirect
github.com/onsi/gomega v1.5.0 // indirect
github.com/vulcand/oxy v1.0.0
github.com/yuin/goldmark v1.1.17
github.com/yuin/goldmark-highlighting v0.0.0-20191202084645-78f32c8dd6d5
go.uber.org/multierr v1.2.0 // indirect
go.uber.org/zap v1.10.0
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect
github.com/smallstep/certificates v0.14.6
github.com/smallstep/cli v0.14.6
github.com/smallstep/nosql v0.3.0
github.com/smallstep/truststore v0.9.6
github.com/yuin/goldmark v1.2.1
github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691
go.uber.org/zap v1.15.0
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
golang.org/x/net v0.0.0-20200707034311-ab3426394381
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98
google.golang.org/protobuf v1.24.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/square/go-jose.v2 v2.4.1 // indirect
gopkg.in/yaml.v2 v2.2.2
gopkg.in/yaml.v2 v2.3.0
)
+623 -211
View File
File diff suppressed because it is too large Load Diff
+67 -27
View File
@@ -254,49 +254,76 @@ type globalListener struct {
pc net.PacketConn
}
// ParsedAddress contains the individual components
// NetworkAddress contains the individual components
// for a parsed network address of the form accepted
// by ParseNetworkAddress(). Network should be a
// network value accepted by Go's net package. Port
// ranges are given by [StartPort, EndPort].
type ParsedAddress struct {
type NetworkAddress struct {
Network string
Host string
StartPort uint
EndPort uint
}
// IsUnixNetwork returns true if pa.Network is
// IsUnixNetwork returns true if na.Network is
// unix, unixgram, or unixpacket.
func (pa ParsedAddress) IsUnixNetwork() bool {
return isUnixNetwork(pa.Network)
func (na NetworkAddress) IsUnixNetwork() bool {
return isUnixNetwork(na.Network)
}
// JoinHostPort is like net.JoinHostPort, but where the port
// is StartPort + offset.
func (pa ParsedAddress) JoinHostPort(offset uint) string {
if pa.IsUnixNetwork() {
return pa.Host
func (na NetworkAddress) JoinHostPort(offset uint) string {
if na.IsUnixNetwork() {
return na.Host
}
return net.JoinHostPort(pa.Host, strconv.Itoa(int(pa.StartPort+offset)))
return net.JoinHostPort(na.Host, strconv.Itoa(int(na.StartPort+offset)))
}
// PortRangeSize returns how many ports are in
// pa's port range. Port ranges are inclusive,
// so the size is the difference of start and
// end ports plus one.
func (pa ParsedAddress) PortRangeSize() uint {
return (pa.EndPort - pa.StartPort) + 1
func (na NetworkAddress) PortRangeSize() uint {
return (na.EndPort - na.StartPort) + 1
}
func (na NetworkAddress) isLoopback() bool {
if na.IsUnixNetwork() {
return true
}
if na.Host == "localhost" {
return true
}
if ip := net.ParseIP(na.Host); ip != nil {
return ip.IsLoopback()
}
return false
}
func (na NetworkAddress) isWildcardInterface() bool {
if na.Host == "" {
return true
}
if ip := net.ParseIP(na.Host); ip != nil {
return ip.IsUnspecified()
}
return false
}
func (na NetworkAddress) port() string {
if na.StartPort == na.EndPort {
return strconv.FormatUint(uint64(na.StartPort), 10)
}
return fmt.Sprintf("%d-%d", na.StartPort, na.EndPort)
}
// String reconstructs the address string to the form expected
// by ParseNetworkAddress().
func (pa ParsedAddress) String() string {
port := strconv.FormatUint(uint64(pa.StartPort), 10)
if pa.StartPort != pa.EndPort {
port += "-" + strconv.FormatUint(uint64(pa.EndPort), 10)
}
return JoinNetworkAddress(pa.Network, pa.Host, port)
// by ParseNetworkAddress(). If the address is a unix socket,
// any non-zero port will be dropped.
func (na NetworkAddress) String() string {
return JoinNetworkAddress(na.Network, na.Host, na.port())
}
func isUnixNetwork(netw string) bool {
@@ -311,17 +338,17 @@ func isUnixNetwork(netw string) bool {
//
// Network addresses are distinct from URLs and do not
// use URL syntax.
func ParseNetworkAddress(addr string) (ParsedAddress, error) {
func ParseNetworkAddress(addr string) (NetworkAddress, error) {
var host, port string
network, host, port, err := SplitNetworkAddress(addr)
if network == "" {
network = "tcp"
}
if err != nil {
return ParsedAddress{}, err
return NetworkAddress{}, err
}
if isUnixNetwork(network) {
return ParsedAddress{
return NetworkAddress{
Network: network,
Host: host,
}, nil
@@ -333,19 +360,19 @@ func ParseNetworkAddress(addr string) (ParsedAddress, error) {
var start, end uint64
start, err = strconv.ParseUint(ports[0], 10, 16)
if err != nil {
return ParsedAddress{}, fmt.Errorf("invalid start port: %v", err)
return NetworkAddress{}, fmt.Errorf("invalid start port: %v", err)
}
end, err = strconv.ParseUint(ports[1], 10, 16)
if err != nil {
return ParsedAddress{}, fmt.Errorf("invalid end port: %v", err)
return NetworkAddress{}, fmt.Errorf("invalid end port: %v", err)
}
if end < start {
return ParsedAddress{}, fmt.Errorf("end port must not be less than start port")
return NetworkAddress{}, fmt.Errorf("end port must not be less than start port")
}
if (end - start) > maxPortSpan {
return ParsedAddress{}, fmt.Errorf("port range exceeds %d ports", maxPortSpan)
return NetworkAddress{}, fmt.Errorf("port range exceeds %d ports", maxPortSpan)
}
return ParsedAddress{
return NetworkAddress{
Network: network,
Host: host,
StartPort: uint(start),
@@ -378,7 +405,7 @@ func JoinNetworkAddress(network, host, port string) string {
if network != "" {
a = network + "/"
}
if host != "" && port == "" {
if (host != "" && port == "") || isUnixNetwork(network) {
a += host
} else if port != "" {
a += net.JoinHostPort(host, port)
@@ -386,6 +413,19 @@ func JoinNetworkAddress(network, host, port string) string {
return a
}
// ListenerWrapper is a type that wraps a listener
// so it can modify the input listener's methods.
// Modules that implement this interface are found
// in the caddy.listeners namespace. Usually, to
// wrap a listener, you will define your own struct
// type that embeds the input listener, then
// implement your own methods that you want to wrap,
// calling the underlying listener's methods where
// appropriate.
type ListenerWrapper interface {
WrapListener(net.Listener) net.Listener
}
var (
listeners = make(map[string]*globalListener)
listenersMu sync.Mutex
-1
View File
@@ -13,7 +13,6 @@
// limitations under the License.
// +build gofuzz
// +build gofuzz_libfuzzer
package caddy
+21 -13
View File
@@ -138,6 +138,14 @@ func TestJoinNetworkAddress(t *testing.T) {
network: "unix", host: "/foo/bar", port: "",
expect: "unix//foo/bar",
},
{
network: "unix", host: "/foo/bar", port: "0",
expect: "unix//foo/bar",
},
{
network: "unix", host: "/foo/bar", port: "1234",
expect: "unix//foo/bar",
},
{
network: "", host: "::1", port: "1234",
expect: "[::1]:1234",
@@ -153,7 +161,7 @@ func TestJoinNetworkAddress(t *testing.T) {
func TestParseNetworkAddress(t *testing.T) {
for i, tc := range []struct {
input string
expectAddr ParsedAddress
expectAddr NetworkAddress
expectErr bool
}{
{
@@ -166,7 +174,7 @@ func TestParseNetworkAddress(t *testing.T) {
},
{
input: ":1234",
expectAddr: ParsedAddress{
expectAddr: NetworkAddress{
Network: "tcp",
Host: "",
StartPort: 1234,
@@ -175,7 +183,7 @@ func TestParseNetworkAddress(t *testing.T) {
},
{
input: "tcp/:1234",
expectAddr: ParsedAddress{
expectAddr: NetworkAddress{
Network: "tcp",
Host: "",
StartPort: 1234,
@@ -184,7 +192,7 @@ func TestParseNetworkAddress(t *testing.T) {
},
{
input: "tcp6/:1234",
expectAddr: ParsedAddress{
expectAddr: NetworkAddress{
Network: "tcp6",
Host: "",
StartPort: 1234,
@@ -193,7 +201,7 @@ func TestParseNetworkAddress(t *testing.T) {
},
{
input: "tcp4/localhost:1234",
expectAddr: ParsedAddress{
expectAddr: NetworkAddress{
Network: "tcp4",
Host: "localhost",
StartPort: 1234,
@@ -202,14 +210,14 @@ func TestParseNetworkAddress(t *testing.T) {
},
{
input: "unix//foo/bar",
expectAddr: ParsedAddress{
expectAddr: NetworkAddress{
Network: "unix",
Host: "/foo/bar",
},
},
{
input: "localhost:1234-1234",
expectAddr: ParsedAddress{
expectAddr: NetworkAddress{
Network: "tcp",
Host: "localhost",
StartPort: 1234,
@@ -222,7 +230,7 @@ func TestParseNetworkAddress(t *testing.T) {
},
{
input: "localhost:0",
expectAddr: ParsedAddress{
expectAddr: NetworkAddress{
Network: "tcp",
Host: "localhost",
StartPort: 0,
@@ -253,12 +261,12 @@ func TestParseNetworkAddress(t *testing.T) {
func TestJoinHostPort(t *testing.T) {
for i, tc := range []struct {
pa ParsedAddress
pa NetworkAddress
offset uint
expect string
}{
{
pa: ParsedAddress{
pa: NetworkAddress{
Network: "tcp",
Host: "localhost",
StartPort: 1234,
@@ -267,7 +275,7 @@ func TestJoinHostPort(t *testing.T) {
expect: "localhost:1234",
},
{
pa: ParsedAddress{
pa: NetworkAddress{
Network: "tcp",
Host: "localhost",
StartPort: 1234,
@@ -276,7 +284,7 @@ func TestJoinHostPort(t *testing.T) {
expect: "localhost:1234",
},
{
pa: ParsedAddress{
pa: NetworkAddress{
Network: "tcp",
Host: "localhost",
StartPort: 1234,
@@ -286,7 +294,7 @@ func TestJoinHostPort(t *testing.T) {
expect: "localhost:1235",
},
{
pa: ParsedAddress{
pa: NetworkAddress{
Network: "unix",
Host: "/run/php/php7.3-fpm.sock",
},
+42 -26
View File
@@ -27,6 +27,7 @@ import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/crypto/ssh/terminal"
)
func init() {
@@ -35,12 +36,14 @@ func init() {
RegisterModule(DiscardWriter{})
}
// Logging facilitates logging within Caddy.
// Logging facilitates logging within Caddy. The default log is
// called "default" and you can customize it. You can also define
// additional logs.
//
// By default, all logs at INFO level and higher are written to
// standard error ("stderr" writer) in a human-readable format
// ("console" encoder). The default log is called "default" and
// you can customize it. You can also define additional logs.
// ("console" encoder if stdout is an interactive terminal, "json"
// encoder otherwise).
//
// All defined logs accept all log entries by default, but you
// can filter by level and module/logger names. A logger's name
@@ -50,10 +53,10 @@ func init() {
// "http.handlers", because all HTTP handler module names have
// that prefix.
//
// Caddy logs (except the sink) are mostly zero-allocation, so
// they are very high-performing in terms of memory and CPU time.
// Enabling sampling can further increase throughput on extremely
// high-load servers.
// Caddy logs (except the sink) are zero-allocation, so they are
// very high-performing in terms of memory and CPU time. Enabling
// sampling can further increase throughput on extremely high-load
// servers.
type Logging struct {
// Sink is the destination for all unstructured logs emitted
// from Go's standard library logger. These logs are common
@@ -214,7 +217,7 @@ func (logging *Logging) Logger(mod Module) *zap.Logger {
multiCore := zapcore.NewTee(cores...)
return zap.New(multiCore).Named(string(modID))
return zap.New(multiCore).Named(modID)
}
// openWriter opens a writer using opener, and returns true if
@@ -393,17 +396,6 @@ func (cl *CustomLog) provision(ctx Context, logging *Logging) error {
}
}
if cl.EncoderRaw != nil {
mod, err := ctx.LoadModule(cl, "EncoderRaw")
if err != nil {
return fmt.Errorf("loading log encoder module: %v", err)
}
cl.encoder = mod.(zapcore.Encoder)
}
if cl.encoder == nil {
cl.encoder = newDefaultProductionLogEncoder()
}
if cl.WriterRaw != nil {
mod, err := ctx.LoadModule(cl, "WriterRaw")
if err != nil {
@@ -420,6 +412,24 @@ func (cl *CustomLog) provision(ctx Context, logging *Logging) error {
return fmt.Errorf("opening log writer using %#v: %v", cl.writerOpener, err)
}
if cl.EncoderRaw != nil {
mod, err := ctx.LoadModule(cl, "EncoderRaw")
if err != nil {
return fmt.Errorf("loading log encoder module: %v", err)
}
cl.encoder = mod.(zapcore.Encoder)
}
if cl.encoder == nil {
// only allow colorized output if this log is going to stdout or stderr
var colorize bool
switch cl.writerOpener.(type) {
case StdoutWriter, StderrWriter,
*StdoutWriter, *StderrWriter:
colorize = true
}
cl.encoder = newDefaultProductionLogEncoder(colorize)
}
cl.buildCore()
return nil
@@ -455,7 +465,7 @@ func (cl *CustomLog) buildCore() {
}
func (cl *CustomLog) matchesModule(moduleID string) bool {
return cl.loggerAllowed(string(moduleID), true)
return cl.loggerAllowed(moduleID, true)
}
// loggerAllowed returns true if name is allowed to emit
@@ -647,7 +657,7 @@ func newDefaultProductionLog() (*defaultCustomLog, error) {
if err != nil {
return nil, err
}
cl.encoder = newDefaultProductionLogEncoder()
cl.encoder = newDefaultProductionLogEncoder(true)
cl.levelEnabler = zapcore.InfoLevel
cl.buildCore()
@@ -658,13 +668,19 @@ func newDefaultProductionLog() (*defaultCustomLog, error) {
}, nil
}
func newDefaultProductionLogEncoder() zapcore.Encoder {
func newDefaultProductionLogEncoder(colorize bool) zapcore.Encoder {
encCfg := zap.NewProductionEncoderConfig()
encCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
encCfg.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(ts.UTC().Format("2006/01/02 15:04:05.000"))
if terminal.IsTerminal(int(os.Stdout.Fd())) {
// if interactive terminal, make output more human-readable by default
encCfg.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(ts.UTC().Format("2006/01/02 15:04:05.000"))
}
if colorize {
encCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
}
return zapcore.NewConsoleEncoder(encCfg)
}
return zapcore.NewConsoleEncoder(encCfg)
return zapcore.NewJSONEncoder(encCfg)
}
// Log returns the current default logger.
+21 -17
View File
@@ -65,7 +65,12 @@ type ModuleInfo struct {
// New returns a pointer to a new, empty
// instance of the module's type. This
// function must not have any side-effects.
// method must not have any side-effects,
// and no other initialization should
// occur within it. Any initialization
// of the returned value should be done
// in a Provision() method (see the
// Provisioner interface).
New func() Module
}
@@ -101,7 +106,7 @@ func (id ModuleID) Namespace() string {
return string(id)[:lastDot]
}
// Name returns the Name (last element) of a module name.
// Name returns the Name (last element) of a module ID.
func (id ModuleID) Name() string {
if id == "" {
return ""
@@ -125,33 +130,32 @@ type ModuleMap map[string]json.RawMessage
// be properly recorded, this should be called in the
// init phase of runtime. Typically, the module package
// will do this as a side-effect of being imported.
// This function returns an error if the module's info
// is incomplete or invalid, or if the module is
// already registered.
func RegisterModule(instance Module) error {
// This function panics if the module's info is
// incomplete or invalid, or if the module is already
// registered.
func RegisterModule(instance Module) {
mod := instance.CaddyModule()
if mod.ID == "" {
return fmt.Errorf("module ID missing")
panic("module ID missing")
}
if mod.ID == "caddy" || mod.ID == "admin" {
return fmt.Errorf("module ID '%s' is reserved", mod.ID)
panic(fmt.Sprintf("module ID '%s' is reserved", mod.ID))
}
if mod.New == nil {
return fmt.Errorf("missing ModuleInfo.New")
panic("missing ModuleInfo.New")
}
if val := mod.New(); val == nil {
return fmt.Errorf("ModuleInfo.New must return a non-nil module instance")
panic("ModuleInfo.New must return a non-nil module instance")
}
modulesMu.Lock()
defer modulesMu.Unlock()
if _, ok := modules[string(mod.ID)]; ok {
return fmt.Errorf("module already registered: %s", mod.ID)
panic(fmt.Sprintf("module already registered: %s", mod.ID))
}
modules[string(mod.ID)] = mod
return nil
}
// GetModule returns module information from its ID (full name).
@@ -210,7 +214,7 @@ func GetModules(scope string) []ModuleInfo {
var mods []ModuleInfo
iterateModules:
for id, m := range modules {
modParts := strings.Split(string(id), ".")
modParts := strings.Split(id, ".")
// match only the next level of nesting
if len(modParts) != len(scopeParts)+1 {
@@ -241,9 +245,9 @@ func Modules() []string {
modulesMu.RLock()
defer modulesMu.RUnlock()
var names []string
names := make([]string, 0, len(modules))
for name := range modules {
names = append(names, string(name))
names = append(names, name)
}
sort.Strings(names)
@@ -294,8 +298,8 @@ type Provisioner interface {
// Validator is implemented by modules which can verify that their
// configurations are valid. This method will be called after
// Provision() (if implemented). Validation should always be fast
// (imperceptible running time) and an error should be returned only
// if the value's configuration is invalid.
// (imperceptible running time) and an error must be returned if
// the module's configuration is invalid.
type Validator interface {
Validate() error
}
+456
View File
@@ -0,0 +1,456 @@
// 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 caddyhttp
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"strconv"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/lucas-clemente/quic-go/http3"
"go.uber.org/zap"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
func init() {
caddy.RegisterModule(App{})
}
// App is a robust, production-ready HTTP server.
//
// HTTPS is enabled by default if host matchers with qualifying names are used
// in any of routes; certificates are automatically provisioned and renewed.
// Additionally, automatic HTTPS will also enable HTTPS for servers that listen
// only on the HTTPS port but which do not have any TLS connection policies
// defined by adding a good, default TLS connection policy.
//
// In HTTP routes, additional placeholders are available (replace any `*`):
//
// Placeholder | Description
// ------------|---------------
// `{http.request.body}` | The request body (⚠️ inefficient; use only for debugging)
// `{http.request.cookie.*}` | HTTP request cookie
// `{http.request.header.*}` | Specific request header field
// `{http.request.host.labels.*}` | Request host labels (0-based from right); e.g. for foo.example.com: 0=com, 1=example, 2=foo
// `{http.request.host}` | The host part of the request's Host header
// `{http.request.hostport}` | The host and port from the request's Host header
// `{http.request.method}` | The request method
// `{http.request.orig_method}` | The request's original method
// `{http.request.orig_uri.path.dir}` | The request's original directory
// `{http.request.orig_uri.path.file}` | The request's original filename
// `{http.request.orig_uri.path}` | The request's original path
// `{http.request.orig_uri.query}` | The request's original query string (without `?`)
// `{http.request.orig_uri}` | The request's original URI
// `{http.request.port}` | The port part of the request's Host header
// `{http.request.proto}` | The protocol of the request
// `{http.request.remote.host}` | The host part of the remote client's address
// `{http.request.remote.port}` | The port part of the remote client's address
// `{http.request.remote}` | The address of the remote client
// `{http.request.scheme}` | The request scheme
// `{http.request.tls.version}` | The TLS version name
// `{http.request.tls.cipher_suite}` | The TLS cipher suite
// `{http.request.tls.resumed}` | The TLS connection resumed a previous connection
// `{http.request.tls.proto}` | The negotiated next protocol
// `{http.request.tls.proto_mutual}` | The negotiated next protocol was advertised by the server
// `{http.request.tls.server_name}` | The server name requested by the client, if any
// `{http.request.tls.client.fingerprint}` | The SHA256 checksum of the client certificate
// `{http.request.tls.client.public_key}` | The public key of the client certificate.
// `{http.request.tls.client.public_key_sha256}` | The SHA256 checksum of the client's public key.
// `{http.request.tls.client.issuer}` | The issuer DN of the client certificate
// `{http.request.tls.client.serial}` | The serial number of the client certificate
// `{http.request.tls.client.subject}` | The subject DN of the client certificate
// `{http.request.tls.client.san.dns_names.*}` | SAN DNS names(index optional)
// `{http.request.tls.client.san.emails.*}` | SAN email addresses (index optional)
// `{http.request.tls.client.san.ips.*}` | SAN IP addresses (index optional)
// `{http.request.tls.client.san.uris.*}` | SAN URIs (index optional)
// `{http.request.uri.path.*}` | Parts of the path, split by `/` (0-based from left)
// `{http.request.uri.path.dir}` | The directory, excluding leaf filename
// `{http.request.uri.path.file}` | The filename of the path, excluding directory
// `{http.request.uri.path}` | The path component of the request URI
// `{http.request.uri.query.*}` | Individual query string value
// `{http.request.uri.query}` | The query string (without `?`)
// `{http.request.uri}` | The full request URI
// `{http.response.header.*}` | Specific response header field
// `{http.vars.*}` | Custom variables in the HTTP handler chain
type App struct {
// HTTPPort specifies the port to use for HTTP (as opposed to HTTPS),
// which is used when setting up HTTP->HTTPS redirects or ACME HTTP
// challenge solvers. Default: 80.
HTTPPort int `json:"http_port,omitempty"`
// HTTPSPort specifies the port to use for HTTPS, which is used when
// solving the ACME TLS-ALPN challenges, or whenever HTTPS is needed
// but no specific port number is given. Default: 443.
HTTPSPort int `json:"https_port,omitempty"`
// GracePeriod is how long to wait for active connections when shutting
// down the server. Once the grace period is over, connections will
// be forcefully closed.
GracePeriod caddy.Duration `json:"grace_period,omitempty"`
// Servers is the list of servers, keyed by arbitrary names chosen
// at your discretion for your own convenience; the keys do not
// affect functionality.
Servers map[string]*Server `json:"servers,omitempty"`
servers []*http.Server
h3servers []*http3.Server
h3listeners []net.PacketConn
ctx caddy.Context
logger *zap.Logger
tlsApp *caddytls.TLS
// used temporarily between phases 1 and 2 of auto HTTPS
allCertDomains []string
}
// CaddyModule returns the Caddy module information.
func (App) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http",
New: func() caddy.Module { return new(App) },
}
}
// Provision sets up the app.
func (app *App) Provision(ctx caddy.Context) error {
// store some references
tlsAppIface, err := ctx.App("tls")
if err != nil {
return fmt.Errorf("getting tls app: %v", err)
}
app.tlsApp = tlsAppIface.(*caddytls.TLS)
app.ctx = ctx
app.logger = ctx.Logger(app)
repl := caddy.NewReplacer()
// this provisions the matchers for each route,
// and prepares auto HTTP->HTTPS redirects, and
// is required before we provision each server
err = app.automaticHTTPSPhase1(ctx, repl)
if err != nil {
return err
}
// prepare each server
for srvName, srv := range app.Servers {
srv.tlsApp = app.tlsApp
srv.logger = app.logger.Named("log")
srv.errorLogger = app.logger.Named("log.error")
// only enable access logs if configured
if srv.Logs != nil {
srv.accessLogger = app.logger.Named("log.access")
}
// if not explicitly configured by the user, disallow TLS
// client auth bypass (domain fronting) which could
// otherwise be exploited by sending an unprotected SNI
// value during a TLS handshake, then putting a protected
// domain in the Host header after establishing connection;
// this is a safe default, but we allow users to override
// it for example in the case of running a proxy where
// domain fronting is desired and access is not restricted
// based on hostname
if srv.StrictSNIHost == nil && srv.hasTLSClientAuth() {
app.logger.Info("enabling strict SNI-Host matching because TLS client auth is configured",
zap.String("server_name", srvName),
)
trueBool := true
srv.StrictSNIHost = &trueBool
}
// process each listener address
for i := range srv.Listen {
lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true)
if err != nil {
return fmt.Errorf("server %s, listener %d: %v",
srvName, i, err)
}
srv.Listen[i] = lnOut
}
// set up each listener modifier
if srv.ListenerWrappersRaw != nil {
vals, err := ctx.LoadModule(srv, "ListenerWrappersRaw")
if err != nil {
return fmt.Errorf("loading listener wrapper modules: %v", err)
}
var hasTLSPlaceholder bool
for i, val := range vals.([]interface{}) {
if _, ok := val.(*tlsPlaceholderWrapper); ok {
if i == 0 {
// putting the tls placeholder wrapper first is nonsensical because
// that is the default, implicit setting: without it, all wrappers
// will go after the TLS listener anyway
return fmt.Errorf("it is unnecessary to specify the TLS listener wrapper in the first position because that is the default")
}
if hasTLSPlaceholder {
return fmt.Errorf("TLS listener wrapper can only be specified once")
}
hasTLSPlaceholder = true
}
srv.listenerWrappers = append(srv.listenerWrappers, val.(caddy.ListenerWrapper))
}
// if any wrappers were configured but the TLS placeholder wrapper is
// absent, prepend it so all defined wrappers come after the TLS
// handshake; this simplifies logic when starting the server, since we
// can simply assume the TLS placeholder will always be there
if !hasTLSPlaceholder && len(srv.listenerWrappers) > 0 {
srv.listenerWrappers = append([]caddy.ListenerWrapper{new(tlsPlaceholderWrapper)}, srv.listenerWrappers...)
}
}
// pre-compile the primary handler chain, and be sure to wrap it in our
// route handler so that important security checks are done, etc.
primaryRoute := emptyHandler
if srv.Routes != nil {
err := srv.Routes.ProvisionHandlers(ctx)
if err != nil {
return fmt.Errorf("server %s: setting up route handlers: %v", srvName, err)
}
primaryRoute = srv.Routes.Compile(emptyHandler)
}
srv.primaryHandlerChain = srv.wrapPrimaryRoute(primaryRoute)
// pre-compile the error handler chain
if srv.Errors != nil {
err := srv.Errors.Routes.Provision(ctx)
if err != nil {
return fmt.Errorf("server %s: setting up server error handling routes: %v", srvName, err)
}
srv.errorHandlerChain = srv.Errors.Routes.Compile(errorEmptyHandler)
}
// prepare the TLS connection policies
err = srv.TLSConnPolicies.Provision(ctx)
if err != nil {
return fmt.Errorf("server %s: setting up TLS connection policies: %v", srvName, err)
}
}
return nil
}
// Validate ensures the app's configuration is valid.
func (app *App) Validate() error {
// each server must use distinct listener addresses
lnAddrs := make(map[string]string)
for srvName, srv := range app.Servers {
for _, addr := range srv.Listen {
listenAddr, err := caddy.ParseNetworkAddress(addr)
if err != nil {
return fmt.Errorf("invalid listener address '%s': %v", addr, err)
}
// check that every address in the port range is unique to this server;
// we do not use <= here because PortRangeSize() adds 1 to EndPort for us
for i := uint(0); i < listenAddr.PortRangeSize(); i++ {
addr := caddy.JoinNetworkAddress(listenAddr.Network, listenAddr.Host, strconv.Itoa(int(listenAddr.StartPort+i)))
if sn, ok := lnAddrs[addr]; ok {
return fmt.Errorf("server %s: listener address repeated: %s (already claimed by server '%s')", srvName, addr, sn)
}
lnAddrs[addr] = srvName
}
}
}
return nil
}
// Start runs the app. It finishes automatic HTTPS if enabled,
// including management of certificates.
func (app *App) Start() error {
for srvName, srv := range app.Servers {
s := &http.Server{
ReadTimeout: time.Duration(srv.ReadTimeout),
ReadHeaderTimeout: time.Duration(srv.ReadHeaderTimeout),
WriteTimeout: time.Duration(srv.WriteTimeout),
IdleTimeout: time.Duration(srv.IdleTimeout),
MaxHeaderBytes: srv.MaxHeaderBytes,
Handler: srv,
}
// enable h2c if configured
if srv.AllowH2C {
h2server := &http2.Server{
IdleTimeout: time.Duration(srv.IdleTimeout),
}
s.Handler = h2c.NewHandler(srv, h2server)
}
for _, lnAddr := range srv.Listen {
listenAddr, err := caddy.ParseNetworkAddress(lnAddr)
if err != nil {
return fmt.Errorf("%s: parsing listen address '%s': %v", srvName, lnAddr, err)
}
for portOffset := uint(0); portOffset < listenAddr.PortRangeSize(); portOffset++ {
// create the listener for this socket
hostport := listenAddr.JoinHostPort(portOffset)
ln, err := caddy.Listen(listenAddr.Network, hostport)
if err != nil {
return fmt.Errorf("%s: listening on %s: %v", listenAddr.Network, hostport, err)
}
// wrap listener before TLS (up to the TLS placeholder wrapper)
var lnWrapperIdx int
for i, lnWrapper := range srv.listenerWrappers {
if _, ok := lnWrapper.(*tlsPlaceholderWrapper); ok {
lnWrapperIdx = i + 1 // mark the next wrapper's spot
break
}
ln = lnWrapper.WrapListener(ln)
}
// enable TLS if there is a policy and if this is not the HTTP port
useTLS := len(srv.TLSConnPolicies) > 0 && int(listenAddr.StartPort+portOffset) != app.httpPort()
if useTLS {
// create TLS listener
tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx)
ln = tls.NewListener(ln, tlsCfg)
/////////
// TODO: HTTP/3 support is experimental for now
if srv.ExperimentalHTTP3 {
app.logger.Info("enabling experimental HTTP/3 listener",
zap.String("addr", hostport),
)
h3ln, err := caddy.ListenPacket("udp", hostport)
if err != nil {
return fmt.Errorf("getting HTTP/3 UDP listener: %v", err)
}
h3srv := &http3.Server{
Server: &http.Server{
Addr: hostport,
Handler: srv,
TLSConfig: tlsCfg,
},
}
go h3srv.Serve(h3ln)
app.h3servers = append(app.h3servers, h3srv)
app.h3listeners = append(app.h3listeners, h3ln)
srv.h3server = h3srv
}
/////////
}
// finish wrapping listener where we left off before TLS
for i := lnWrapperIdx; i < len(srv.listenerWrappers); i++ {
ln = srv.listenerWrappers[i].WrapListener(ln)
}
// if binding to port 0, the OS chooses a port for us;
// but the user won't know the port unless we print it
if listenAddr.StartPort == 0 && listenAddr.EndPort == 0 {
app.logger.Info("port 0 listener",
zap.String("input_address", lnAddr),
zap.String("actual_address", ln.Addr().String()),
)
}
app.logger.Debug("starting server loop",
zap.String("address", ln.Addr().String()),
zap.Bool("http3", srv.ExperimentalHTTP3),
zap.Bool("tls", useTLS),
)
go s.Serve(ln)
app.servers = append(app.servers, s)
}
}
}
// finish automatic HTTPS by finally beginning
// certificate management
err := app.automaticHTTPSPhase2()
if err != nil {
return fmt.Errorf("finalizing automatic HTTPS: %v", err)
}
return nil
}
// Stop gracefully shuts down the HTTP server.
func (app *App) Stop() error {
ctx := context.Background()
if app.GracePeriod > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(app.GracePeriod))
defer cancel()
}
for _, s := range app.servers {
err := s.Shutdown(ctx)
if err != nil {
return err
}
}
// close the http3 servers; it's unclear whether the bug reported in
// https://github.com/caddyserver/caddy/pull/2727#issuecomment-526856566
// was ever truly fixed, since it seemed racey/nondeterministic; but
// recent tests in 2020 were unable to replicate the issue again after
// repeated attempts (the bug manifested after a config reload; i.e.
// reusing a http3 server or listener was problematic), but it seems
// to be working fine now
for _, s := range app.h3servers {
// TODO: CloseGracefully, once implemented upstream
// (see https://github.com/lucas-clemente/quic-go/issues/2103)
err := s.Close()
if err != nil {
return err
}
}
// closing an http3.Server does not close their underlying listeners
// since apparently the listener can be used both by servers and
// clients at the same time; so we need to manually call Close()
// on the underlying h3 listeners (see lucas-clemente/quic-go#2103)
for _, pc := range app.h3listeners {
err := pc.Close()
if err != nil {
return err
}
}
return nil
}
func (app *App) httpPort() int {
if app.HTTPPort == 0 {
return DefaultHTTPPort
}
return app.HTTPPort
}
func (app *App) httpsPort() int {
if app.HTTPSPort == 0 {
return DefaultHTTPSPort
}
return app.HTTPSPort
}
// Interface guards
var (
_ caddy.App = (*App)(nil)
_ caddy.Provisioner = (*App)(nil)
_ caddy.Validator = (*App)(nil)
)
+439 -161
View File
@@ -1,3 +1,17 @@
// 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 caddyhttp
import (
@@ -8,7 +22,7 @@ import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/mholt/certmagic"
"github.com/caddyserver/certmagic"
"go.uber.org/zap"
)
@@ -42,12 +56,10 @@ type AutoHTTPSConfig struct {
// enabled. To force automated certificate management
// regardless of loaded certificates, set this to true.
IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"`
domainSet map[string]struct{}
}
// Skipped returns true if name is in skipSlice, which
// should be one of the Skip* fields on ahc.
// should be either the Skip or SkipCerts field on ahc.
func (ahc AutoHTTPSConfig) Skipped(name string, skipSlice []string) bool {
for _, n := range skipSlice {
if name == n {
@@ -64,9 +76,15 @@ func (ahc AutoHTTPSConfig) Skipped(name string, skipSlice []string) bool {
// even servers to the app, which still need to be set up with the
// rest of them during provisioning.
func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) error {
// this map will store associations of HTTP listener
// addresses to the routes that do HTTP->HTTPS redirects
lnAddrRedirRoutes := make(map[string]Route)
// this map acts as a set to store the domain names
// for which we will manage certificates automatically
uniqueDomainsForCerts := make(map[string]struct{})
// this maps domain names for automatic HTTP->HTTPS
// redirects to their destination server addresses
// (there might be more than 1 if bind is used; see
// https://github.com/caddyserver/caddy/issues/3443)
redirDomains := make(map[string][]caddy.NetworkAddress)
for srvName, srv := range app.Servers {
// as a prerequisite, provision route matchers; this is
@@ -99,10 +117,6 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
continue
}
defaultConnPolicies := caddytls.ConnectionPolicies{
&caddytls.ConnectionPolicy{ALPN: defaultALPN},
}
// if all listeners are on the HTTPS port, make sure
// there is at least one TLS connection policy; it
// should be obvious that they want to use TLS without
@@ -113,11 +127,12 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
zap.String("server_name", srvName),
zap.Int("https_port", app.httpsPort()),
)
srv.TLSConnPolicies = defaultConnPolicies
srv.TLSConnPolicies = caddytls.ConnectionPolicies{new(caddytls.ConnectionPolicy)}
}
// find all qualifying domain names in this server
srv.AutoHTTPS.domainSet = make(map[string]struct{})
// find all qualifying domain names (deduplicated) in this server
// (this is where we need the provisioned, decoded request matchers)
serverDomainSet := make(map[string]struct{})
for routeIdx, route := range srv.Routes {
for matcherSetIdx, matcherSet := range route.MatcherSets {
for matcherIdx, m := range matcherSet {
@@ -129,9 +144,8 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
return fmt.Errorf("%s: route %d, matcher set %d, matcher %d, host matcher %d: %v",
srvName, routeIdx, matcherSetIdx, matcherIdx, hostMatcherIdx, err)
}
if certmagic.HostQualifies(d) &&
!srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) {
srv.AutoHTTPS.domainSet[d] = struct{}{}
if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) {
serverDomainSet[d] = struct{}{}
}
}
}
@@ -139,15 +153,49 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
}
}
// nothing more to do here if there are no
// domains that qualify for automatic HTTPS
if len(srv.AutoHTTPS.domainSet) == 0 {
// nothing more to do here if there are no domains that qualify for
// automatic HTTPS and there are no explicit TLS connection policies:
// if there is at least one domain but no TLS conn policy (F&&T), we'll
// add one below; if there are no domains but at least one TLS conn
// policy (meaning TLS is enabled) (T&&F), it could be a catch-all with
// on-demand TLS -- and in that case we would still need HTTP->HTTPS
// redirects, which we set up below; hence these two conditions
if len(serverDomainSet) == 0 && len(srv.TLSConnPolicies) == 0 {
continue
}
// for all the hostnames we found, filter them so we have
// a deduplicated list of names for which to obtain certs
for d := range serverDomainSet {
if certmagic.SubjectQualifiesForCert(d) &&
!srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) {
// if a certificate for this name is already loaded,
// don't obtain another one for it, unless we are
// supposed to ignore loaded certificates
if !srv.AutoHTTPS.IgnoreLoadedCerts &&
len(app.tlsApp.AllMatchingCertificates(d)) > 0 {
app.logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded",
zap.String("domain", d),
zap.String("server_name", srvName),
)
continue
}
// most clients don't accept wildcards like *.tld... we
// can handle that, but as a courtesy, warn the user
if strings.Contains(d, "*") &&
strings.Count(strings.Trim(d, "."), ".") == 1 {
app.logger.Warn("most clients do not trust second-level wildcard certificates (*.tld)",
zap.String("domain", d))
}
uniqueDomainsForCerts[d] = struct{}{}
}
}
// tell the server to use TLS if it is not already doing so
if srv.TLSConnPolicies == nil {
srv.TLSConnPolicies = defaultConnPolicies
srv.TLSConnPolicies = caddytls.ConnectionPolicies{new(caddytls.ConnectionPolicy)}
}
// nothing left to do if auto redirects are disabled
@@ -161,125 +209,408 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// create HTTP->HTTPS redirects
for _, addr := range srv.Listen {
netw, host, port, err := caddy.SplitNetworkAddress(addr)
// figure out the address we will redirect to...
addr, err := caddy.ParseNetworkAddress(addr)
if err != nil {
return fmt.Errorf("%s: invalid listener address: %v", srvName, addr)
}
if parts := strings.SplitN(port, "-", 2); len(parts) == 2 {
port = parts[0]
}
redirTo := "https://{http.request.host}"
if port != strconv.Itoa(app.httpsPort()) {
redirTo += ":" + port
}
redirTo += "{http.request.uri}"
// build the plaintext HTTP variant of this address
httpRedirLnAddr := caddy.JoinNetworkAddress(netw, host, strconv.Itoa(app.httpPort()))
// build the matcher set for this redirect route
// (note that we happen to bypass Provision and
// Validate steps for these matcher modules)
matcherSet := MatcherSet{MatchProtocol("http")}
if len(srv.AutoHTTPS.Skip) > 0 {
matcherSet = append(matcherSet, MatchNegate{
Matchers: MatcherSet{MatchHost(srv.AutoHTTPS.Skip)},
})
// this address might not have a hostname, i.e. might be a
// catch-all address for a particular port; we need to keep
// track if it is, so we can set up redirects for it anyway
// (e.g. the user might have enabled on-demand TLS); we use
// an empty string to indicate a catch-all, which we have to
// treat special later
if len(serverDomainSet) == 0 {
redirDomains[""] = append(redirDomains[""], addr)
continue
}
// create the route that does the redirect and associate
// it with the listener address it will be served from
// (note that we happen to bypass any Provision or Validate
// steps on the handler modules created here)
lnAddrRedirRoutes[httpRedirLnAddr] = Route{
MatcherSets: []MatcherSet{matcherSet},
Handlers: []MiddlewareHandler{
StaticResponse{
StatusCode: WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
Headers: http.Header{
"Location": []string{redirTo},
"Connection": []string{"close"},
},
Close: true,
},
},
// ...and associate it with each domain in this server
for d := range serverDomainSet {
// if this domain is used on more than one HTTPS-enabled
// port, we'll have to choose one, so prefer the HTTPS port
if _, ok := redirDomains[d]; !ok ||
addr.StartPort == uint(app.httpsPort()) {
redirDomains[d] = append(redirDomains[d], addr)
}
}
}
}
// if there are HTTP->HTTPS redirects to add, do so now
if len(lnAddrRedirRoutes) == 0 {
return nil
// we now have a list of all the unique names for which we need certs;
// turn the set into a slice so that phase 2 can use it
app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts))
var internal, external []string
uniqueDomainsLoop:
for d := range uniqueDomainsForCerts {
// whether or not there is already an automation policy for this
// name, we should add it to the list to manage a cert for it
app.allCertDomains = append(app.allCertDomains, d)
// some names we've found might already have automation policies
// explicitly specified for them; we should exclude those from
// our hidden/implicit policy, since applying a name to more than
// one automation policy would be confusing and an error
if app.tlsApp.Automation != nil {
for _, ap := range app.tlsApp.Automation.Policies {
for _, apHost := range ap.Subjects {
if apHost == d {
continue uniqueDomainsLoop
}
}
}
}
// if no automation policy exists for the name yet, we
// will associate it with an implicit one
if certmagic.SubjectQualifiesForPublicCert(d) {
external = append(external, d)
} else {
internal = append(internal, d)
}
}
var redirServerAddrs []string
// ensure there is an automation policy to handle these certs
err := app.createAutomationPolicies(ctx, external, internal)
if err != nil {
return err
}
// we need to reduce the mapping, i.e. group domains by address
// since new routes are appended to servers by their address
domainsByAddr := make(map[string][]string)
for domain, addrs := range redirDomains {
for _, addr := range addrs {
addrStr := addr.String()
domainsByAddr[addrStr] = append(domainsByAddr[addrStr], domain)
}
}
// these keep track of the redirect server address(es)
// and the routes for those servers which actually
// respond with the redirects
redirServerAddrs := make(map[string]struct{})
redirServers := make(map[string][]Route)
var redirRoutes RouteList
// for each redirect listener, see if there's already a
// server configured to listen on that exact address; if so,
// simply add the redirect route to the end of its route
// list; otherwise, we'll create a new server for all the
// listener addresses that are unused and serve the
// remaining redirects from it
redirRoutesLoop:
for addr, redirRoute := range lnAddrRedirRoutes {
for addrStr, domains := range domainsByAddr {
// build the matcher set for this redirect route; (note that we happen
// to bypass Provision and Validate steps for these matcher modules)
matcherSet := MatcherSet{MatchProtocol("http")}
// match on known domain names, unless it's our special case of a
// catch-all which is an empty string (common among catch-all sites
// that enable on-demand TLS for yet-unknown domain names)
if !(len(domains) == 1 && domains[0] == "") {
matcherSet = append(matcherSet, MatchHost(domains))
}
// build the address to which to redirect
addr, err := caddy.ParseNetworkAddress(addrStr)
if err != nil {
return err
}
redirTo := "https://{http.request.host}"
if addr.StartPort != uint(app.httpsPort()) {
redirTo += ":" + strconv.Itoa(int(addr.StartPort))
}
redirTo += "{http.request.uri}"
// build the route
redirRoute := Route{
MatcherSets: []MatcherSet{matcherSet},
Handlers: []MiddlewareHandler{
StaticResponse{
StatusCode: WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
Headers: http.Header{
"Location": []string{redirTo},
"Connection": []string{"close"},
},
Close: true,
},
},
}
// use the network/host information from the address,
// but change the port to the HTTP port then rebuild
redirAddr := addr
redirAddr.StartPort = uint(app.httpPort())
redirAddr.EndPort = redirAddr.StartPort
redirAddrStr := redirAddr.String()
redirServers[redirAddrStr] = append(redirServers[redirAddrStr], redirRoute)
}
// on-demand TLS means that hostnames may be used which are not
// explicitly defined in the config, and we still need to redirect
// those; so we can append a single catch-all route (notice there
// is no Host matcher) after the other redirect routes which will
// allow us to handle unexpected/new hostnames... however, it's
// not entirely clear what the redirect destination should be,
// so I'm going to just hard-code the app's HTTPS port and call
// it good for now...
// TODO: This implies that all plaintext requests will be blindly
// redirected to their HTTPS equivalent, even if this server
// doesn't handle that hostname at all; I don't think this is a
// bad thing, and it also obscures the actual hostnames that this
// server is configured to match on, which may be desirable, but
// it's not something that should be relied on. We can change this
// if we want to.
appendCatchAll := func(routes []Route) []Route {
redirTo := "https://{http.request.host}"
if app.httpsPort() != DefaultHTTPSPort {
redirTo += ":" + strconv.Itoa(app.httpsPort())
}
redirTo += "{http.request.uri}"
routes = append(routes, Route{
MatcherSets: []MatcherSet{{MatchProtocol("http")}},
Handlers: []MiddlewareHandler{
StaticResponse{
StatusCode: WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
Headers: http.Header{
"Location": []string{redirTo},
"Connection": []string{"close"},
},
Close: true,
},
},
})
return routes
}
redirServersLoop:
for redirServerAddr, routes := range redirServers {
// for each redirect listener, see if there's already a
// server configured to listen on that exact address; if so,
// simply add the redirect route to the end of its route
// list; otherwise, we'll create a new server for all the
// listener addresses that are unused and serve the
// remaining redirects from it
for srvName, srv := range app.Servers {
if srv.hasListenerAddress(addr) {
if srv.hasListenerAddress(redirServerAddr) {
// user has configured a server for the same address
// that the redirect runs from; simply append our
// redirect route to the existing routes, with a
// caveat that their config might override ours
app.logger.Warn("server is listening on same interface as redirects, so automatic HTTP->HTTPS redirects might be overridden by your own configuration",
app.logger.Warn("user server is listening on same interface as automatic HTTP->HTTPS redirects; user-configured routes might override these redirects",
zap.String("server_name", srvName),
zap.String("interface", addr),
zap.String("interface", redirServerAddr),
)
srv.Routes = append(srv.Routes, redirRoute)
continue redirRoutesLoop
srv.Routes = append(srv.Routes, appendCatchAll(routes)...)
continue redirServersLoop
}
}
// no server with this listener address exists;
// save this address and route for custom server
redirServerAddrs = append(redirServerAddrs, addr)
redirRoutes = append(redirRoutes, redirRoute)
redirServerAddrs[redirServerAddr] = struct{}{}
redirRoutes = append(redirRoutes, routes...)
}
// if there are routes remaining which do not belong
// in any existing server, make our own to serve the
// rest of the redirects
if len(redirServerAddrs) > 0 {
redirServerAddrsList := make([]string, 0, len(redirServerAddrs))
for a := range redirServerAddrs {
redirServerAddrsList = append(redirServerAddrsList, a)
}
app.Servers["remaining_auto_https_redirects"] = &Server{
Listen: redirServerAddrs,
Routes: redirRoutes,
Listen: redirServerAddrsList,
Routes: appendCatchAll(redirRoutes),
}
}
return nil
}
// automaticHTTPSPhase2 attaches a TLS app pointer to each
// server. This phase must occur after provisioning, and
// at the beginning of the app start, before starting each
// of the servers.
func (app *App) automaticHTTPSPhase2() error {
tlsAppIface, err := app.ctx.App("tls")
if err != nil {
return fmt.Errorf("getting tls app: %v", err)
// createAutomationPolicy ensures that automated certificates for this
// app are managed properly. This adds up to two automation policies:
// one for the public names, and one for the internal names. If a catch-all
// automation policy exists, it will be shallow-copied and used as the
// base for the new ones (this is important for preserving behavior the
// user intends to be "defaults").
func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, internalNames []string) error {
// before we begin, loop through the existing automation policies
// and, for any ACMEIssuers we find, make sure they're filled in
// with default values that might be specified in our HTTP app; also
// look for a base (or "catch-all" / default) automation policy,
// which we're going to essentially require, to make sure it has
// those defaults, too
var basePolicy *caddytls.AutomationPolicy
var foundBasePolicy bool
if app.tlsApp.Automation == nil {
// we will expect this to not be nil from now on
app.tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp := tlsAppIface.(*caddytls.TLS)
for _, ap := range app.tlsApp.Automation.Policies {
// set up default issuer -- honestly, this is only
// really necessary because the HTTP app is opinionated
// and has settings which could be inferred as new
// defaults for the ACMEIssuer in the TLS app
if ap.Issuer == nil {
ap.Issuer = new(caddytls.ACMEIssuer)
}
if acmeIssuer, ok := ap.Issuer.(acmeCapable); ok {
err := app.fillInACMEIssuer(acmeIssuer.GetACMEIssuer())
if err != nil {
return err
}
}
// set the tlsApp pointer before starting any
// challenges, since it is required to solve
// the ACME HTTP challenge
for _, srv := range app.Servers {
srv.tlsApp = tlsApp
// while we're here, is this the catch-all/base policy?
if !foundBasePolicy && len(ap.Subjects) == 0 {
basePolicy = ap
foundBasePolicy = true
}
}
if basePolicy == nil {
// no base policy found, we will make one!
basePolicy = new(caddytls.AutomationPolicy)
}
// if the basePolicy has an existing ACMEIssuer (particularly to
// include any type that embeds/wraps an ACMEIssuer), let's use it,
// otherwise we'll make one
var baseACMEIssuer *caddytls.ACMEIssuer
if acmeWrapper, ok := basePolicy.Issuer.(acmeCapable); ok {
baseACMEIssuer = acmeWrapper.GetACMEIssuer()
}
if baseACMEIssuer == nil {
// note that this happens if basePolicy.Issuer is nil
// OR if it is not nil but is not an ACMEIssuer
baseACMEIssuer = new(caddytls.ACMEIssuer)
}
// if there was a base policy to begin with, we already
// filled in its issuer's defaults; if there wasn't, we
// stil need to do that
if !foundBasePolicy {
err := app.fillInACMEIssuer(baseACMEIssuer)
if err != nil {
return err
}
}
// never overwrite any other issuer that might already be configured
if basePolicy.Issuer == nil {
basePolicy.Issuer = baseACMEIssuer
}
if !foundBasePolicy {
// there was no base policy to begin with, so add
// our base/catch-all policy - this will serve the
// public-looking names as well as any other names
// that don't match any other policy
app.tlsApp.AddAutomationPolicy(basePolicy)
} else {
// a base policy already existed; we might have
// changed it, so re-provision it
err := basePolicy.Provision(app.tlsApp)
if err != nil {
return err
}
}
// public names will be taken care of by the base (catch-all)
// policy, which we've ensured exists if not already specified;
// internal names, however, need to be handled by an internal
// issuer, which we need to make a new policy for, scoped to
// just those names (yes, this logic is a bit asymmetric, but
// it works, because our assumed/natural default issuer is an
// ACME issuer)
if len(internalNames) > 0 {
internalIssuer := new(caddytls.InternalIssuer)
// shallow-copy the base policy; we want to inherit
// from it, not replace it... this takes two lines to
// overrule compiler optimizations
policyCopy := *basePolicy
newPolicy := &policyCopy
// very important to provision the issuer, since we
// are bypassing the JSON-unmarshaling step
if err := internalIssuer.Provision(ctx); err != nil {
return err
}
// this policy should apply only to the given names
// and should use our issuer -- yes, this overrides
// any issuer that may have been set in the base
// policy, but we do this because these names do not
// already have a policy associated with them, which
// is easy to do; consider the case of a Caddyfile
// that has only "localhost" as a name, but sets the
// default/global ACME CA to the Let's Encrypt staging
// endpoint... they probably don't intend to change the
// fundamental set of names that setting applies to,
// rather they just want to change the CA for the set
// of names that would normally use the production API;
// anyway, that gets into the weeds a bit...
newPolicy.Subjects = internalNames
newPolicy.Issuer = internalIssuer
err := app.tlsApp.AddAutomationPolicy(newPolicy)
if err != nil {
return err
}
}
// we just changed a lot of stuff, so double-check that it's all good
err := app.tlsApp.Validate()
if err != nil {
return err
}
return nil
}
// automaticHTTPSPhase3 begins certificate management for
// fillInACMEIssuer fills in default values into acmeIssuer that
// are defined in app; these values at time of writing are just
// app.HTTPPort and app.HTTPSPort, which are used by ACMEIssuer.
// Sure, we could just use the global/CertMagic defaults, but if
// a user has configured those ports in the HTTP app, it makes
// sense to use them in the TLS app too, even if they forgot (or
// were too lazy, like me) to set it in each automation policy
// that uses it -- this just makes things a little less tedious
// for the user, so they don't have to repeat those ports in
// potentially many places. This function never steps on existing
// config values. If any changes are made, acmeIssuer is
// reprovisioned. acmeIssuer must not be nil.
func (app *App) fillInACMEIssuer(acmeIssuer *caddytls.ACMEIssuer) error {
if app.HTTPPort > 0 || app.HTTPSPort > 0 {
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
}
if app.HTTPPort > 0 {
if acmeIssuer.Challenges.HTTP == nil {
acmeIssuer.Challenges.HTTP = new(caddytls.HTTPChallengeConfig)
}
// don't overwrite existing explicit config
if acmeIssuer.Challenges.HTTP.AlternatePort == 0 {
acmeIssuer.Challenges.HTTP.AlternatePort = app.HTTPPort
}
}
if app.HTTPSPort > 0 {
if acmeIssuer.Challenges.TLSALPN == nil {
acmeIssuer.Challenges.TLSALPN = new(caddytls.TLSALPNChallengeConfig)
}
// don't overwrite existing explicit config
if acmeIssuer.Challenges.TLSALPN.AlternatePort == 0 {
acmeIssuer.Challenges.TLSALPN.AlternatePort = app.HTTPSPort
}
}
// we must provision all ACME issuers, even if nothing
// was changed, because we don't know if they are new
// and haven't been provisioned yet; if an ACME issuer
// never gets provisioned, its Agree field stays false,
// which leads to, um, problems later on
return acmeIssuer.Provision(app.ctx)
}
// automaticHTTPSPhase2 begins certificate management for
// all names in the qualifying domain set for each server.
// This phase must occur after provisioning and at the end
// of app start, after all the servers have been started.
@@ -289,72 +620,19 @@ func (app *App) automaticHTTPSPhase2() error {
// first, then our servers would fail to bind to them,
// which would be bad, since CertMagic's bindings are
// temporary and don't serve the user's sites!).
func (app *App) automaticHTTPSPhase3() error {
// begin managing certificates for enabled servers
for srvName, srv := range app.Servers {
if srv.AutoHTTPS == nil ||
srv.AutoHTTPS.Disabled ||
len(srv.AutoHTTPS.domainSet) == 0 {
continue
}
// marshal the domains into a slice
var domains, domainsForCerts []string
for d := range srv.AutoHTTPS.domainSet {
domains = append(domains, d)
if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) {
// if a certificate for this name is already loaded,
// don't obtain another one for it, unless we are
// supposed to ignore loaded certificates
if !srv.AutoHTTPS.IgnoreLoadedCerts &&
len(srv.tlsApp.AllMatchingCertificates(d)) > 0 {
app.logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded",
zap.String("domain", d),
zap.String("server_name", srvName),
)
continue
}
domainsForCerts = append(domainsForCerts, d)
}
}
// ensure that these certificates are managed properly;
// for example, it's implied that the HTTPPort should also
// be the port the HTTP challenge is solved on, and so
// for HTTPS port and TLS-ALPN challenge also - we need
// to tell the TLS app to manage these certs by honoring
// those port configurations
acmeManager := &caddytls.ACMEManagerMaker{
Challenges: &caddytls.ChallengesConfig{
HTTP: &caddytls.HTTPChallengeConfig{
AlternatePort: app.HTTPPort, // we specifically want the user-configured port, if any
},
TLSALPN: &caddytls.TLSALPNChallengeConfig{
AlternatePort: app.HTTPSPort, // we specifically want the user-configured port, if any
},
},
}
if srv.tlsApp.Automation == nil {
srv.tlsApp.Automation = new(caddytls.AutomationConfig)
}
srv.tlsApp.Automation.Policies = append(srv.tlsApp.Automation.Policies,
caddytls.AutomationPolicy{
Hosts: domainsForCerts,
Management: acmeManager,
})
// manage their certificates
app.logger.Info("enabling automatic TLS certificate management",
zap.Strings("domains", domainsForCerts),
)
err := srv.tlsApp.Manage(domainsForCerts)
if err != nil {
return fmt.Errorf("%s: managing certificate for %s: %s", srvName, domains, err)
}
// no longer needed; allow GC to deallocate
srv.AutoHTTPS.domainSet = nil
func (app *App) automaticHTTPSPhase2() error {
if len(app.allCertDomains) == 0 {
return nil
}
app.logger.Info("enabling automatic TLS certificate management",
zap.Strings("domains", app.allCertDomains),
)
err := app.tlsApp.Manage(app.allCertDomains)
if err != nil {
return fmt.Errorf("managing certificates for %v: %s", app.allCertDomains, err)
}
app.allCertDomains = nil // no longer needed; allow GC to deallocate
return nil
}
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
+119 -21
View File
@@ -16,15 +16,21 @@ package caddyauth
import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
weakrand "math/rand"
"net/http"
"sync"
"time"
"github.com/caddyserver/caddy/v2"
)
func init() {
caddy.RegisterModule(HTTPBasicAuth{})
weakrand.Seed(time.Now().UnixNano())
}
// HTTPBasicAuth facilitates HTTP basic authentication.
@@ -38,6 +44,17 @@ type HTTPBasicAuth struct {
// The name of the realm. Default: restricted
Realm string `json:"realm,omitempty"`
// If non-nil, a mapping of plaintext passwords to their
// hashes will be cached in memory (with random eviction).
// This can greatly improve the performance of traffic-heavy
// servers that use secure password hashing algorithms, with
// the downside that plaintext passwords will be stored in
// memory for a longer time (this should not be a problem
// as long as your machine is not compromised, at which point
// all bets are off, since basicauth necessitates plaintext
// passwords being received over the wire anyway).
HashCache *Cache `json:"hash_cache,omitempty"`
Accounts map[string]Account `json:"-"`
Hash Comparer `json:"-"`
}
@@ -53,7 +70,7 @@ func (HTTPBasicAuth) CaddyModule() caddy.ModuleInfo {
// Provision provisions the HTTP basic auth provider.
func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
if hba.HashRaw == nil {
return fmt.Errorf("passwords must be hashed, so a hash must be defined")
hba.HashRaw = json.RawMessage(`{"algorithm": "bcrypt"}`)
}
// load password hasher
@@ -77,8 +94,8 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
}
acct.Username = repl.ReplaceAll(acct.Username, "")
acct.Password = repl.ReplaceAll(string(acct.Password), "")
acct.Salt = repl.ReplaceAll(string(acct.Salt), "")
acct.Password = repl.ReplaceAll(acct.Password, "")
acct.Salt = repl.ReplaceAll(acct.Salt, "")
if acct.Username == "" || acct.Password == "" {
return fmt.Errorf("account %d: username and password are required", i)
@@ -99,45 +116,126 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
}
hba.AccountList = nil // allow GC to deallocate
if hba.HashCache != nil {
hba.HashCache.cache = make(map[string]bool)
hba.HashCache.mu = new(sync.Mutex)
}
return nil
}
// Authenticate validates the user credentials in req and returns the user, if valid.
func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request) (User, bool, error) {
username, plaintextPasswordStr, ok := req.BasicAuth()
// if basic auth is missing or invalid, prompt for credentials
if !ok {
// browsers show a message that says something like:
// "The website says: <realm>"
// which is kinda dumb, but whatever.
realm := hba.Realm
if realm == "" {
realm = "restricted"
}
w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm))
return User{}, false, nil
return hba.promptForCredentials(w, nil)
}
plaintextPassword := []byte(plaintextPasswordStr)
account, accountExists := hba.Accounts[username]
// don't return early if account does not exist; we want
// to try to avoid side-channels that leak existence
same, err := hba.Hash.Compare(account.password, plaintextPassword, account.salt)
same, err := hba.correctPassword(account, []byte(plaintextPasswordStr))
if err != nil {
return User{}, false, err
return hba.promptForCredentials(w, err)
}
if !same || !accountExists {
return User{}, false, nil
return hba.promptForCredentials(w, nil)
}
return User{ID: username}, true, nil
}
func (hba HTTPBasicAuth) correctPassword(account Account, plaintextPassword []byte) (bool, error) {
compare := func() (bool, error) {
return hba.Hash.Compare(account.password, plaintextPassword, account.salt)
}
// if no caching is enabled, simply return the result of hashing + comparing
if hba.HashCache == nil {
return compare()
}
// compute a cache key that is unique for these input parameters
cacheKey := hex.EncodeToString(append(append(account.password, account.salt...), plaintextPassword...))
// fast track: if the result of the input is already cached, use it
hba.HashCache.mu.Lock()
same, ok := hba.HashCache.cache[cacheKey]
if ok {
hba.HashCache.mu.Unlock()
return same, nil
}
hba.HashCache.mu.Unlock()
// slow track: do the expensive op, then add it to the cache
same, err := compare()
if err != nil {
return false, err
}
hba.HashCache.mu.Lock()
if len(hba.HashCache.cache) >= 1000 {
hba.HashCache.makeRoom() // keep cache size under control
}
hba.HashCache.cache[cacheKey] = same
hba.HashCache.mu.Unlock()
return same, nil
}
func (hba HTTPBasicAuth) promptForCredentials(w http.ResponseWriter, err error) (User, bool, error) {
// browsers show a message that says something like:
// "The website says: <realm>"
// which is kinda dumb, but whatever.
realm := hba.Realm
if realm == "" {
realm = "restricted"
}
w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm))
return User{}, false, err
}
// Cache enables caching of basic auth results. This is especially
// helpful for secure password hashes which can be expensive to
// compute on every HTTP request.
type Cache struct {
mu *sync.Mutex
// map of concatenated hashed password + plaintext password + salt, to result
cache map[string]bool
}
// makeRoom deletes about 1/10 of the items in the cache
// in order to keep its size under control. It must not be
// called without a lock on c.mu.
func (c *Cache) makeRoom() {
// we delete more than just 1 entry so that we don't have
// to do this on every request; assuming the capacity of
// the cache is on a long tail, we can save a lot of CPU
// time by doing a whole bunch of deletions now and then
// we won't have to do them again for a while
numToDelete := len(c.cache) / 10
if numToDelete < 1 {
numToDelete = 1
}
for deleted := 0; deleted <= numToDelete; deleted++ {
// Go maps are "nondeterministic" not actually random,
// so although we could just chop off the "front" of the
// map with less code, this is a heavily skewed eviction
// strategy; generating random numbers is cheap and
// ensures a much better distribution.
rnd := weakrand.Intn(len(c.cache))
i := 0
for key := range c.cache {
if i == rnd {
delete(c.cache, key)
break
}
i++
}
}
}
// Comparer is a type that can securely compare
// a plaintext password with a hashed password
// in constant-time. Comparers should hash the
+14 -1
View File
@@ -29,6 +29,8 @@ func init() {
// Authentication is a middleware which provides user authentication.
// Rejects requests with HTTP 401 if the request is not authenticated.
//
// Its API is still experimental and may be subject to change.
type Authentication struct {
// A set of authentication providers. If none are specified,
// all requests will always be unauthenticated.
@@ -77,7 +79,10 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
}
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
repl.Set("http.authentication.user.id", user.ID)
repl.Set("http.auth.user.id", user.ID)
for k, v := range user.Metadata {
repl.Set("http.auth.user."+k, v)
}
return next.ServeHTTP(w, r)
}
@@ -92,7 +97,15 @@ type Authenticator interface {
// User represents an authenticated user.
type User struct {
// The ID of the authenticated user.
ID string
// Any other relevant data about this
// user. Keys should be adhere to Caddy
// conventions (snake_casing), as all
// keys will be made available as
// placeholders.
Metadata map[string]string
}
// Interface guards
+5 -1
View File
@@ -27,7 +27,7 @@ func init() {
// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax:
//
// basicauth [<matcher>] [<hash_algorithm>] {
// basicauth [<matcher>] [<hash_algorithm> [<realm>]] {
// <username> <hashed_password_base64> [<salt_base64>]
// ...
// }
@@ -35,6 +35,7 @@ func init() {
// If no hash algorithm is supplied, bcrypt will be assumed.
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var ba HTTPBasicAuth
ba.HashCache = new(Cache)
for h.Next() {
var cmp Comparer
@@ -46,6 +47,9 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
hashName = "bcrypt"
case 1:
hashName = args[0]
case 2:
hashName = args[0]
ba.Realm = args[1]
default:
return nil, h.ArgErr()
}
+57 -5
View File
@@ -15,26 +15,35 @@
package caddyauth
import (
"bufio"
"bytes"
"encoding/base64"
"flag"
"fmt"
"os"
"os/signal"
"github.com/caddyserver/caddy/v2"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/scrypt"
"golang.org/x/crypto/ssh/terminal"
)
func init() {
caddycmd.RegisterCommand(caddycmd.Command{
Name: "hash-password",
Func: cmdHashPassword,
Usage: "--plaintext <password> [--salt <string>] [--algorithm <name>]",
Usage: "[--algorithm <name>] [--salt <string>] [--plaintext <password>]",
Short: "Hashes a password and writes base64",
Long: `
Convenient way to hash a plaintext password. The resulting
hash is written to stdout as a base64 string.
--plaintext, when omitted, will be read from stdin. If
Caddy is attached to a controlling tty, the plaintext will
not be echoed.
--algorithm may be bcrypt or scrypt. If script, the default
parameters are used.
@@ -52,19 +61,62 @@ be provided (scrypt).
}
func cmdHashPassword(fs caddycmd.Flags) (int, error) {
var err error
algorithm := fs.String("algorithm")
plaintext := []byte(fs.String("plaintext"))
salt := []byte(fs.String("salt"))
if len(plaintext) == 0 {
return caddy.ExitCodeFailedStartup, fmt.Errorf("password is required")
fd := int(os.Stdin.Fd())
if terminal.IsTerminal(fd) {
// ensure the terminal state is restored on SIGINT
state, _ := terminal.GetState(fd)
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt)
go func() {
<-c
_ = terminal.Restore(fd, state)
os.Exit(caddy.ExitCodeFailedStartup)
}()
defer signal.Stop(c)
fmt.Fprint(os.Stderr, "Enter password: ")
plaintext, err = terminal.ReadPassword(fd)
fmt.Fprintln(os.Stderr)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
fmt.Fprint(os.Stderr, "Confirm password: ")
confirmation, err := terminal.ReadPassword(fd)
fmt.Fprintln(os.Stderr)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
if !bytes.Equal(plaintext, confirmation) {
return caddy.ExitCodeFailedStartup, fmt.Errorf("password does not match")
}
} else {
rd := bufio.NewReader(os.Stdin)
plaintext, err = rd.ReadBytes('\n')
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
plaintext = plaintext[:len(plaintext)-1] // Trailing newline
}
if len(plaintext) == 0 {
return caddy.ExitCodeFailedStartup, fmt.Errorf("plaintext is required")
}
}
var hash []byte
var err error
switch algorithm {
case "bcrypt":
hash, err = bcrypt.GenerateFromPassword(plaintext, bcrypt.DefaultCost)
hash, err = bcrypt.GenerateFromPassword(plaintext, 14)
case "scrypt":
def := ScryptHash{}
def.SetDefaults()
@@ -76,7 +128,7 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
return caddy.ExitCodeFailedStartup, err
}
hashBase64 := base64.StdEncoding.EncodeToString([]byte(hash))
hashBase64 := base64.StdEncoding.EncodeToString(hash)
fmt.Println(hashBase64)
+71 -350
View File
@@ -16,10 +16,7 @@ package caddyhttp
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
weakrand "math/rand"
"net"
@@ -28,341 +25,14 @@ import (
"time"
"github.com/caddyserver/caddy/v2"
"github.com/lucas-clemente/quic-go/http3"
"go.uber.org/zap"
)
func init() {
weakrand.Seed(time.Now().UnixNano())
err := caddy.RegisterModule(App{})
if err != nil {
caddy.Log().Fatal(err.Error())
}
caddy.RegisterModule(tlsPlaceholderWrapper{})
}
// App is a robust, production-ready HTTP server.
//
// HTTPS is enabled by default if host matchers with qualifying names are used
// in any of routes; certificates are automatically provisioned and renewed.
// Additionally, automatic HTTPS will also enable HTTPS for servers that listen
// only on the HTTPS port but which do not have any TLS connection policies
// defined by adding a good, default TLS connection policy.
//
// In HTTP routes, additional placeholders are available (replace any `*`):
//
// Placeholder | Description
// ------------|---------------
// `{http.request.cookie.*}` | HTTP request cookie
// `{http.request.header.*}` | Specific request header field
// `{http.request.host.labels.*}` | Request host labels (0-based from right); e.g. for foo.example.com: 0=com, 1=example, 2=foo
// `{http.request.host}` | The host part of the request's Host header
// `{http.request.hostport}` | The host and port from the request's Host header
// `{http.request.method}` | The request method
// `{http.request.orig_method}` | The request's original method
// `{http.request.orig_uri.path.dir}` | The request's original directory
// `{http.request.orig_uri.path.file}` | The request's original filename
// `{http.request.orig_uri.path}` | The request's original path
// `{http.request.orig_uri.query}` | The request's original query string (without `?`)
// `{http.request.orig_uri}` | The request's original URI
// `{http.request.port}` | The port part of the request's Host header
// `{http.request.proto}` | The protocol of the request
// `{http.request.remote.host}` | The host part of the remote client's address
// `{http.request.remote.port}` | The port part of the remote client's address
// `{http.request.remote}` | The address of the remote client
// `{http.request.scheme}` | The request scheme
// `{http.request.uri.path.*}` | Parts of the path, split by `/` (0-based from left)
// `{http.request.uri.path.dir}` | The directory, excluding leaf filename
// `{http.request.uri.path.file}` | The filename of the path, excluding directory
// `{http.request.uri.path}` | The path component of the request URI
// `{http.request.uri.query.*}` | Individual query string value
// `{http.request.uri.query}` | The query string (without `?`)
// `{http.request.uri}` | The full request URI
// `{http.response.header.*}` | Specific response header field
// `{http.vars.*}` | Custom variables in the HTTP handler chain
type App struct {
// HTTPPort specifies the port to use for HTTP (as opposed to HTTPS),
// which is used when setting up HTTP->HTTPS redirects or ACME HTTP
// challenge solvers. Default: 80.
HTTPPort int `json:"http_port,omitempty"`
// HTTPSPort specifies the port to use for HTTPS, which is used when
// solving the ACME TLS-ALPN challenges, or whenever HTTPS is needed
// but no specific port number is given. Default: 443.
HTTPSPort int `json:"https_port,omitempty"`
// GracePeriod is how long to wait for active connections when shutting
// down the server. Once the grace period is over, connections will
// be forcefully closed.
GracePeriod caddy.Duration `json:"grace_period,omitempty"`
// Servers is the list of servers, keyed by arbitrary names chosen
// at your discretion for your own convenience; the keys do not
// affect functionality.
Servers map[string]*Server `json:"servers,omitempty"`
servers []*http.Server
h3servers []*http3.Server
h3listeners []net.PacketConn
ctx caddy.Context
logger *zap.Logger
}
// CaddyModule returns the Caddy module information.
func (App) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http",
New: func() caddy.Module { return new(App) },
}
}
// Provision sets up the app.
func (app *App) Provision(ctx caddy.Context) error {
app.ctx = ctx
app.logger = ctx.Logger(app)
repl := caddy.NewReplacer()
// this provisions the matchers for each route,
// and prepares auto HTTP->HTTP redirects, and
// is required before we provision each server
err := app.automaticHTTPSPhase1(ctx, repl)
if err != nil {
return err
}
for srvName, srv := range app.Servers {
srv.logger = app.logger.Named("log")
srv.errorLogger = app.logger.Named("log.error")
// only enable access logs if configured
if srv.Logs != nil {
srv.accessLogger = app.logger.Named("log.access")
}
// if not explicitly configured by the user, disallow TLS
// client auth bypass (domain fronting) which could
// otherwise be exploited by sending an unprotected SNI
// value during a TLS handshake, then putting a protected
// domain in the Host header after establishing connection;
// this is a safe default, but we allow users to override
// it for example in the case of running a proxy where
// domain fronting is desired and access is not restricted
// based on hostname
if srv.StrictSNIHost == nil && srv.hasTLSClientAuth() {
app.logger.Info("enabling strict SNI-Host matching because TLS client auth is configured",
zap.String("server_name", srvName),
)
trueBool := true
srv.StrictSNIHost = &trueBool
}
for i := range srv.Listen {
lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true)
if err != nil {
return fmt.Errorf("server %s, listener %d: %v",
srvName, i, err)
}
srv.Listen[i] = lnOut
}
// pre-compile the primary handler chain, and be sure to wrap it in our
// route handler so that important security checks are done, etc.
primaryRoute := emptyHandler
if srv.Routes != nil {
err := srv.Routes.ProvisionHandlers(ctx)
if err != nil {
return fmt.Errorf("server %s: setting up route handlers: %v", srvName, err)
}
primaryRoute = srv.Routes.Compile(emptyHandler)
}
srv.primaryHandlerChain = srv.wrapPrimaryRoute(primaryRoute)
// pre-compile the error handler chain
if srv.Errors != nil {
err := srv.Errors.Routes.Provision(ctx)
if err != nil {
return fmt.Errorf("server %s: setting up server error handling routes: %v", srvName, err)
}
srv.errorHandlerChain = srv.Errors.Routes.Compile(emptyHandler)
}
}
return nil
}
// Validate ensures the app's configuration is valid.
func (app *App) Validate() error {
// each server must use distinct listener addresses
lnAddrs := make(map[string]string)
for srvName, srv := range app.Servers {
for _, addr := range srv.Listen {
listenAddr, err := caddy.ParseNetworkAddress(addr)
if err != nil {
return fmt.Errorf("invalid listener address '%s': %v", addr, err)
}
// check that every address in the port range is unique to this server;
// we do not use <= here because PortRangeSize() adds 1 to EndPort for us
for i := uint(0); i < listenAddr.PortRangeSize(); i++ {
addr := caddy.JoinNetworkAddress(listenAddr.Network, listenAddr.Host, strconv.Itoa(int(listenAddr.StartPort+i)))
if sn, ok := lnAddrs[addr]; ok {
return fmt.Errorf("server %s: listener address repeated: %s (already claimed by server '%s')", srvName, addr, sn)
}
lnAddrs[addr] = srvName
}
}
}
return nil
}
// Start runs the app. It finishes automatic HTTPS if enabled,
// including management of certificates.
func (app *App) Start() error {
// give each server a pointer to the TLS app;
// this is required before they are started so
// they can solve ACME challenges
err := app.automaticHTTPSPhase2()
if err != nil {
return fmt.Errorf("enabling automatic HTTPS, phase 2: %v", err)
}
for srvName, srv := range app.Servers {
s := &http.Server{
ReadTimeout: time.Duration(srv.ReadTimeout),
ReadHeaderTimeout: time.Duration(srv.ReadHeaderTimeout),
WriteTimeout: time.Duration(srv.WriteTimeout),
IdleTimeout: time.Duration(srv.IdleTimeout),
MaxHeaderBytes: srv.MaxHeaderBytes,
Handler: srv,
}
for _, lnAddr := range srv.Listen {
listenAddr, err := caddy.ParseNetworkAddress(lnAddr)
if err != nil {
return fmt.Errorf("%s: parsing listen address '%s': %v", srvName, lnAddr, err)
}
for portOffset := uint(0); portOffset < listenAddr.PortRangeSize(); portOffset++ {
hostport := listenAddr.JoinHostPort(portOffset)
ln, err := caddy.Listen(listenAddr.Network, hostport)
if err != nil {
return fmt.Errorf("%s: listening on %s: %v", listenAddr.Network, hostport, err)
}
// enable HTTP/2 by default
for _, pol := range srv.TLSConnPolicies {
if len(pol.ALPN) == 0 {
pol.ALPN = append(pol.ALPN, defaultALPN...)
}
}
// enable TLS if there is a policy and if this is not the HTTP port
if len(srv.TLSConnPolicies) > 0 &&
int(listenAddr.StartPort+portOffset) != app.httpPort() {
// create TLS listener
tlsCfg, err := srv.TLSConnPolicies.TLSConfig(app.ctx)
if err != nil {
return fmt.Errorf("%s/%s: making TLS configuration: %v", listenAddr.Network, hostport, err)
}
ln = tls.NewListener(ln, tlsCfg)
/////////
// TODO: HTTP/3 support is experimental for now
if srv.ExperimentalHTTP3 {
app.logger.Info("enabling experimental HTTP/3 listener",
zap.String("addr", hostport),
)
h3ln, err := caddy.ListenPacket("udp", hostport)
if err != nil {
return fmt.Errorf("getting HTTP/3 UDP listener: %v", err)
}
h3srv := &http3.Server{
Server: &http.Server{
Addr: hostport,
Handler: srv,
TLSConfig: tlsCfg,
},
}
go h3srv.Serve(h3ln)
app.h3servers = append(app.h3servers, h3srv)
app.h3listeners = append(app.h3listeners, h3ln)
srv.h3server = h3srv
}
/////////
}
go s.Serve(ln)
app.servers = append(app.servers, s)
}
}
}
// finish automatic HTTPS by finally beginning
// certificate management
err = app.automaticHTTPSPhase3()
if err != nil {
return fmt.Errorf("finalizing automatic HTTPS: %v", err)
}
return nil
}
// Stop gracefully shuts down the HTTP server.
func (app *App) Stop() error {
ctx := context.Background()
if app.GracePeriod > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(app.GracePeriod))
defer cancel()
}
for _, s := range app.servers {
err := s.Shutdown(ctx)
if err != nil {
return err
}
}
// TODO: Closing the http3.Server is the right thing to do,
// however, doing so sometimes causes connections from clients
// to fail after config reloads due to a bug that is yet
// unsolved: https://github.com/caddyserver/caddy/pull/2727
// for _, s := range app.h3servers {
// // TODO: CloseGracefully, once implemented upstream
// // (see https://github.com/lucas-clemente/quic-go/issues/2103)
// err := s.Close()
// if err != nil {
// return err
// }
// }
// as of September 2019, closing the http3.Server
// instances doesn't close their underlying listeners
// so we have todo that ourselves
// (see https://github.com/lucas-clemente/quic-go/issues/2103)
for _, pc := range app.h3listeners {
err := pc.Close()
if err != nil {
return err
}
}
return nil
}
func (app *App) httpPort() int {
if app.HTTPPort == 0 {
return DefaultHTTPPort
}
return app.HTTPPort
}
func (app *App) httpsPort() int {
if app.HTTPSPort == 0 {
return DefaultHTTPSPort
}
return app.HTTPSPort
}
var defaultALPN = []string{"h2", "http/1.1"}
// RequestMatcher is a type that can match to a request.
// A route matcher MUST NOT modify the request, with the
// only exception being its context.
@@ -408,6 +78,59 @@ type MiddlewareHandler interface {
// emptyHandler is used as a no-op handler.
var emptyHandler Handler = HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })
// An implicit suffix middleware that, if reached, sets the StatusCode to the
// error stored in the ErrorCtxKey. This is to prevent situations where the
// Error chain does not actually handle the error (for instance, it matches only
// on some errors). See #3053
var errorEmptyHandler Handler = HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
httpError := r.Context().Value(ErrorCtxKey)
if handlerError, ok := httpError.(HandlerError); ok {
w.WriteHeader(handlerError.StatusCode)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
return nil
})
// ResponseHandler pairs a response matcher with custom handling
// logic. Either the status code can be changed to something else
// while using the original response body, or, if a status code
// is not set, it can execute a custom route list; this is useful
// for executing handler routes based on the properties of an HTTP
// response that has not been written out to the client yet.
//
// To use this type, provision it at module load time, then when
// ready to use, match the response against its matcher; if it
// matches (or doesn't have a matcher), change the status code on
// the response if configured; otherwise invoke the routes by
// calling `rh.Routes.Compile(next).ServeHTTP(rw, req)` (or similar).
type ResponseHandler struct {
// The response matcher for this handler. If empty/nil,
// it always matches.
Match *ResponseMatcher `json:"match,omitempty"`
// To write the original response body but with a different
// status code, set this field to the desired status code.
// If set, this takes priority over routes.
StatusCode WeakString `json:"status_code,omitempty"`
// The list of HTTP routes to execute if no status code is
// specified. If evaluated, the original response body
// will not be written.
Routes RouteList `json:"routes,omitempty"`
}
// Provision sets up the routse in rh.
func (rh *ResponseHandler) Provision(ctx caddy.Context) error {
if rh.Routes != nil {
err := rh.Routes.Provision(ctx)
if err != nil {
return err
}
}
return nil
}
// WeakString is a type that unmarshals any JSON value
// as a string literal, with the following exceptions:
//
@@ -481,19 +204,6 @@ func (ws WeakString) String() string {
return string(ws)
}
// CopyHeader copies HTTP headers by completely
// replacing dest with src. (This allows deletions
// to be propagated, assuming src started as a
// consistent copy of dest.)
func CopyHeader(dest, src http.Header) {
for field := range dest {
delete(dest, field)
}
for field, val := range src {
dest[field] = val
}
}
// StatusCodeMatches returns true if a real HTTP status code matches
// the configured status code, which may be either a real HTTP status
// code or an integer representing a class of codes (e.g. 4 for all
@@ -510,6 +220,21 @@ func StatusCodeMatches(actual, configured int) bool {
return false
}
// tlsPlaceholderWrapper is a no-op listener wrapper that marks
// where the TLS listener should be in a chain of listener wrappers.
// It should only be used if another listener wrapper must be placed
// in front of the TLS handshake.
type tlsPlaceholderWrapper struct{}
func (tlsPlaceholderWrapper) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "caddy.listeners.tls",
New: func() caddy.Module { return new(tlsPlaceholderWrapper) },
}
}
func (tlsPlaceholderWrapper) WrapListener(ln net.Listener) net.Listener { return ln }
const (
// DefaultHTTPPort is the default port for HTTP.
DefaultHTTPPort = 80
@@ -518,9 +243,5 @@ const (
DefaultHTTPSPort = 443
)
// Interface guards
var (
_ caddy.App = (*App)(nil)
_ caddy.Provisioner = (*App)(nil)
_ caddy.Validator = (*App)(nil)
)
// Interface guard
var _ caddy.ListenerWrapper = (*tlsPlaceholderWrapper)(nil)

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