Compare commits

..

850 Commits

Author SHA1 Message Date
Dave Henderson d16ede358a metrics: Fix hidden panic while observing with bad exemplars (#3733)
* metrics: Fixing panic while observing with bad exemplars

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

* Minor cleanup

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

* Add integration test for auto HTTP->HTTPS redirects

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

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

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

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

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

* reverseproxy: cleanup health-check integration test

* reverseproxy: fix health-check of unix socket upstreams

* reverseproxy: skip unix socket tests on Windows

* tabs > spaces

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

* make the linter (and @francislavoie) happy

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

* One more lint fix

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

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

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

* httpcaddyfile: Add comment linking to the issue being fixed

* httpcaddyfile: Typo fix, comment clarity

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

* Update caddyconfig/httpcaddyfile/httptype.go

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

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

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

* fileserver: Review cleanup

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

* Update modules/caddyhttp/replacer.go

Change type of block and empty headers removed

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

* fixed tests

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

* logging: Add support for parsing IP masks from strings


wip

* logging: Implement Caddyfile support for ip_mask

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

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

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

* Minor adjustments

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

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

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

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

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

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

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

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

* Go 1.15 was released; let's try it

* Update to latest quic-go

* Attempt at fixing broken test

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

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

* httpcaddyfile: Generalize reverse sort directives, improve logic

* httpcaddyfile: Fix "spelling" issue

* httpcaddyfile: Turns out the second change precludes the first


httpcaddyfile: Delete test that no longer makes sense

* httpcaddyfile: Shorten logic

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

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

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

New Caddyfile global option:

    {
        cert_issuer <name> ...
    }

Or, alternatively, as a tls subdirective:

    tls {
        issuer <name> ...
    }

For example, to use ZeroSSL with an API key:

    {
        cert_issuser zerossl API_KEY
    }

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

    {
        cert_issuer acme {
            eab KEY_ID MAC_KEY
        }
    }

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

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

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

* Fix broken test

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

* Fix broken test (post-merge)

* Update modules/caddytls/acmeissuer.go

Fix godoc comment

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

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

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

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

* Wrap underlying logic rather than repeating it

Oops, duh

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

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

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

* Minor cleanup; apply review suggestions

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

* Add unit test for caddyfile admin config update

* Add caddyfile adapt test for typical admin setup

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

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

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

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

* ci: split cross-build check into separate workflow

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

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

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

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

* ci: Simplify workflow names

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

I think I updated the wrong test case before

* Make new test function

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

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

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

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

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

* Update modules/caddyhttp/templates/templates.go

updates JSON doc

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

* Update modules/caddyhttp/templates/tplcontext.go

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

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

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

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

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

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

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

* reverse proxy: adjust comments as francislavoie suggests

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

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

* Improve documentation of reverseproxy.UpstreamResolver fields

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

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

* remove support for bootstraper of resolver

* godoc and code-style changes

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

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

* Update modules/caddyhttp/reverseproxy/caddyfile.go

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

* Rename empty transport test

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

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

* Update browselisting.go

* Update browsetpl.go

* fix linter err

* Update modules/caddyhttp/fileserver/browse.go

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

* Update modules/caddyhttp/fileserver/browselisting.go

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

* Update browsetpl.go

change from -> offset

* Update browse.go

* Update browselisting.go

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

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

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

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

* resolve the value during middleware execution

* use regex instead

* pr feedback

* renamed mmap to maphandler

* refactored GetString implementation

* fixed mispelling

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

https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#github-context
2020-06-26 16:00:54 -04:00
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
Matthew Holt 17d938fc54 httpcaddyfile: Add support for DNS challenge solvers
Configuration via the Caddyfile requires use of env variables, but
an upstream issue is currently blocking that:
https://github.com/go-acme/lego/issues/1054

Providers will need to be retrofitted upstream in order to support env
var configuration.
2020-02-08 18:43:35 -07:00
Jeremy Lin 98bbc54fdc browse: allow filter init via filter query param (#3027)
This allows creating links that display only a subset of files in a directory.
2020-02-08 12:36:37 -07:00
Mohammed Al Sahaf 9bdd6caa0b v2: Implement RegExp Vars Matcher (#2997)
* implement regexp var matcher

* use subtests pattern for tests

* be more consistent with naming: MatchVarRE -> MatchVarsRE, var_regexp -> vars_regexp
2020-02-08 12:26:31 -07:00
Matthew Holt f7f6e371ef tls: Slight adjustment to how DNS provider modules are loaded
We don't load the provider directly, because the lego provider types
aren't designed for JSON configuration and they are not implemented
as Caddy modules (there are some setup steps which a Provision call
would need to do, but they do not have Provision methods, they have
their own constructor functions that we have to wrap).

Instead of loading the challenge providers directly, the modules are
simple wrappers over the challenge providers, to facilitate the JSON
config structure and to provide a consistent experience. This also lets
us swap out the underlying challenge providers transparently if needed;
it acts as a layer of abstraction.
2020-02-07 21:59:25 -07:00
Matthew Holt b8cf4d5897 Fix typo in readme 2020-02-07 11:26:48 -07:00
Matthew Holt 04ec3c5f05 Update readme
The list of improvements and FAQ were moved to the wiki for now. They
still need to be updated.
2020-02-07 10:59:09 -07:00
Matthew Holt 8b28c36d48 Remove Starlark, for now
This is temporary as we prepare for a stable v2 release. We don't want
to make promises we don't know we can keep, and the Starlark integration
deserves much more focused attention which resources and funding do not
currently permit. When the project is financially stable, I will be able
to revisit this properly and add flexible, robust Starlark scripting
support to Caddy 2.
2020-02-06 18:46:52 -07:00
Matthew Holt 4a07a5d41e caddyfile: tls: Ensure there is always a catch-all conn policy (#3005)
If user provides their own certs or makes any hostname-specific TLS
connection policy, it means that no TLS connection would be served for
any other hostnames, even though you'd expect that TLS is enabled for
them, too. So now we append a catch-all conn policy if none exist, which
allows all ClientHellos to be matched and served.

We also fix the consolidation of automation policies, which previously
gobbled up automation policies without hosts in favor of automation
policies with hosts. Instead of a host-specific policy eating up an
identical catch-all policy, the catch-all policy eats up the identical
host-specific policy, ensuring that the policy is applied to all hosts
which need it.

See also:
https://caddy.community/t/v2-automatic-https-certificate-errors/6847/9?u=matt
2020-02-06 13:00:41 -07:00
Matthew Holt b81ae38686 caddyfile: tls: Tag manual certificates (#2588)
This ensure that if there are multiple certs that match a particular
ServerName or other parameter, then specifically the one the user
provided in the Caddyfile will be used.
2020-02-06 12:55:26 -07:00
Matthew Holt 5c7ca7d96e http: Split 2-phase auto-HTTPS into 3 phases
This is necessary to avoid a race for sockets. Both the HTTP servers and
CertMagic solvers will try to bind the HTTP/HTTPS ports, but we need to
make sure that our HTTP servers bind first. This is kind of a new thing
now that management is async in Caddy 2.

Also update to CertMagic 0.9.2, which fixes some async use cases at
scale.
2020-02-05 17:34:28 -07:00
Francis Lavoie ec56c25708 caddyhttp: Fix orig_uri placeholder docs (#3002)
Fixes #3001
2020-02-04 15:49:38 -07:00
Matthew Holt c0f827e0bd httpcaddyfile: Add {remote} shorthand placeholders
Also sort the list
2020-02-04 13:31:22 -07:00
Matthew Holt 490cd02f82 httpcaddyfile: Make root directive mutually exclusive
See https://caddy.community/t/caddyfile-and-v2/6766/22?u=matt
2020-02-04 13:04:34 -07:00
Matthew Holt 9639fe7d28 header: caddyfile: Defer header operations for deletions or manually
See https://caddy.community/t/caddy-server-that-returns-only-ip-address-as-text/6928/6?u=matt

In most cases, we will want to apply header operations immediately,
rather than waiting until the response is written. The exceptions are
generally going to be if we are deleting a header field or if a field is
to be overwritten. We now automatically defer header ops if deleting a
header field, and allow the user to manually enable deferred mode with
the defer subdirective.
2020-02-04 11:05:32 -07:00
Matthew Holt 3592e59399 cmd: adapt: Make --config flag optional when Caddyfile exists 2020-02-04 10:48:02 -07:00
Mohammed Al Sahaf f74fed3f54 v2: only compare TLS protocol versions if both are set (#3005) 2020-02-03 09:25:32 -07:00
Matthew Holt 8b2ad61220 httpcaddyfile: Skip hosts from auto-https when http:// scheme (fix #2998) 2020-01-23 13:17:16 -07:00
Matthew Holt 6614d1c495 cmd: Emit error if reload cannot find a config to load 2020-01-22 10:04:58 -07:00
Matthew Holt c6bddbfbe2 http: Fix vars matcher 2020-01-22 09:43:42 -07:00
Matthew Holt 0742530d3d rewrite: Prepend "/" if missing from strip path prefix
Paths always begin with a slash, and omitting the leading slash could be
convenient to avoid confusion with a path matcher in the Caddyfile. I do
not think there would be any harm to implicitly add the leading slash.
2020-01-22 09:36:05 -07:00
Matthew Holt 6b6cd934d0 reverseproxy: Fix casing of RootCAPEMFiles 2020-01-22 09:35:03 -07:00
Matthew Holt 5b878d5bd3 reverseproxy: Accept integer values for flush_interval (fix #2996) 2020-01-22 09:34:16 -07:00
Matthew Holt 2105d59936 httpcaddyfile: Rename 'headers' directive to 'header' 2020-01-22 09:33:53 -07:00
Matthew Holt 9a1370c2c8 cmd: Make --config flag optional for reload command
In case it is using the default Caddyfile
2020-01-22 09:33:22 -07:00
Matthew Holt d810637a9f httpcaddyfile: Update directive docs; put root after rewrite 2020-01-22 09:32:38 -07:00
Matthew Holt 5d3ccf1eb7 httpcaddyfile: Get rid of 'tls off' parameter; probably not useful 2020-01-22 09:29:50 -07:00
Matthew Holt aad9f90cad httpcaddyfile: Fix address parsing; don't infer port at parse-time
Before, listener ports could be wrong because ParseAddress doesn't know
about the user-configured HTTP/HTTPS ports, instead hard-coding port 80
or 443, which could be wrong if the user changed them to something else.
Now we defer port and scheme validation/inference to a later part of
building the output JSON.
2020-01-19 11:51:17 -07:00
Zaq? Wiedmann 07ef4b0c7d Merge pull request #2980 from moorereason/bugfix-ciphersuite-logging
v2: http: Fix ciphersuite logging
2020-01-18 19:37:50 -08:00
Mohammed Al Sahaf 2bfaf8e896 reverse_proxy: CB docs; rename type -> factor (#2986)
* v2: add documentation for circuit breaker config and "random selection" load balancing policy

* v2: rename circuit breaker config inline key from `type` to `breaker` to avoid json key clash between the `circuit_breaker` type and the `type` field of the generic circuit breaker Config struct used by circuit breaking implementations

* v2: restore the circuit breaker inline key to `type` and rename the name circuit breaker config field from `Type` to `Factor`
2020-01-18 18:42:56 -07:00
Matthew Holt 372540f0ee httpcaddyfile: Move redir before rewrite
Using rewrite is like saying, "I accept this request, but I just need
to act on it as if it came in differently."

Whereas redir implies more of, "I reject this request, send it to me
differently, then I will process it."

Makes sense for it to come before rewrites. This can always be changed
using the 'order' global option if needed.
2020-01-17 11:38:49 -07:00
Matthew Holt 793a405810 caddyhttp: Improve docs, and Caddyfile for respond directive 2020-01-17 10:57:57 -07:00
Matthew Holt 85ff0e3604 cmd: version: Add module replace to output 2020-01-17 09:50:23 -07:00
Matthew Holt e51e56a494 httpcaddyfile: Fix nested blocks; add handle directive; refactor
The fix that was initially put forth in #2971 was good, but only for
up to one layer of nesting. The real problem was that we forgot to
increment nesting when already inside a block if we saw another open
curly brace that opens another block (dispenser.go L157-158).

The new 'handle' directive allows HTTP Caddyfiles to be designed more
like nginx location blocks if the user prefers. Inside a handle block,
directives are still ordered just like they are outside of them, but
handler blocks at a given level of nesting are mutually exclusive.

This work benefitted from some refactoring and cleanup.
2020-01-16 17:08:52 -07:00
Cameron Moore 35174a8ba8 http: Fix ciphersuite logging 2020-01-16 15:44:49 -06:00
Matthew Holt 21643a007a httpcaddyfile: Replace 'handler_order' option with 'order'
This allows individual directives to be ordered relative to others,
where order matters (for example HTTP handlers). Will primarily be
useful when developing new directives, so you don't have to modify the
Caddy source code. Can also be useful if you prefer that redir comes
before rewrite, for example. Note that these are global options. The
route directive can be used to give a specific order to a specific group
of HTTP handler directives.
2020-01-16 12:09:54 -07:00
Matthew Holt 2466ed1484 httpcaddyfile: Group try_files routes together (#2891)
This ensures that only the first matching route is used.
2020-01-16 11:29:20 -07:00
Matthew Holt a66f461201 caddyfile: Sort site subroutes by key specificity, and make exclusive
In the v1 Caddyfile, only the first matching site definition would be
used, so setting these `Terminal: true` ensures that only the first
matching one is used in v2, too.

We also have to sort by key specificity... Caddy 1 had a special data
structure for selecting the most specific site definition, but we don't
have that structure in v2, so we need to sort by length (of host and
path, separately). For blocks where more than one key is present, we
choose the longest host and path (independently, need not be from same
key) by which to sort.
2020-01-15 13:51:12 -07:00
Matthew Holt 07ad4655db rewrite: Make URI modifications more transactional (#2891)
Before, modifying the path might have affected how a new query string
was built if the query string relied on the path. Now, we build each
component in isolation and only change the URI on the request later.

Also, prevent trailing & in query string.
2020-01-15 11:44:21 -07:00
Matthew Holt 271b5af148 http: Refactor automatic HTTPS (fixes #2972)
This splits automatic HTTPS into two phases. The first provisions the
route matchers and uses them to build the domain set and configure
auto HTTP->HTTPS redirects. This happens before the rest of the
provisioning does.

The second phase takes place at the beginning of the app start. It
attaches pointers to the tls app to each server, and begins certificate
management for the domains that were found in the first phase.
2020-01-13 16:16:20 -07:00
Matthew Holt 99e2b56519 cmd: adapt: Set config filename so it can be hidden (fixes #2974) 2020-01-12 18:20:19 -07:00
Matthew Holt 64f0173948 http: Fix subroutes, ensure that next handlers can still be called 2020-01-12 13:39:32 -07:00
Matthew Holt fe5a531c58 http: Fix empty responses
Sigh... this is what I get for writing code when I'm tired and sick.

See https://github.com/caddyserver/caddy/commit/8be1f0ea668492000cdefbd937e0359bdc24bfc1#r36764627
2020-01-12 13:34:55 -07:00
Matthew Holt 8c0c1a7b88 cmd: Assume Caddyfile if name starts with Caddyfile
And doesn't have .json extension -- in case someone names their
JSON config something like Caddyfile.json, which is unconventional.
2020-01-11 13:48:29 -07:00
Matthew Holt 25dea2903e http: A little more polish on rewrite handler and try_files directive 2020-01-11 13:47:42 -07:00
Matthew Holt d876de61e5 rewrite: Fix query string logic 2020-01-11 11:40:03 -07:00
Matthew Holt 8be1f0ea66 http: Ensure primary routes always get compiled (fix #2972)
Including servers for HTTP->HTTPS redirects which do not get provisioned
like the rest.
2020-01-11 00:33:47 -07:00
Matthew Holt 2eda21ec6d http: Remove {...query_string} placeholder, in favor of {...query}
I am not sure if the query_string one is necessary or useful yet. We
can always add it later if needed.
2020-01-10 17:02:11 -07:00
Matthew Holt d418e319ab rewrite: Rename parameters; implement custom query string parser
Our new parser also preserves original parameter order, rather than
re-encoding using the std lib (which sorts).

The renamed parameters are a breaking change but they're new enough
that I don't think anyone is using them.
2020-01-10 17:00:57 -07:00
Matthew Holt ba514f9660 cmd: Add build-info command; update CertMagic 2020-01-10 11:53:07 -07:00
Zaq? Wiedmann 3dcc34d341 caddyfile: advance cursor for claimed token in NewFromNextTokens() (#2971)
When we append a token to the new dispenser, we need to consume it in the parent, too; otherwise it gets scanned twice, which in this case messed up the nesting count which got decremented once too many times.
2020-01-09 20:48:15 -07:00
Mark Sargent 871abf1053 caddyfile: fix replacing variables on imported files (#2970)
* fix replacing variables on imported files

* refactored replaceEnvVars to ensure it is always called

* Use byte slices for easier use

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-01-09 19:34:22 -07:00
Matthew Holt 29315847a8 caddyfile: Use of vars no longer requires nesting in subroutes
This is because of our sequential handling logic which was recently
merged; if vars is the first handler in the chain, it will be run before
the next route's matchers are executed, so there's no need to nest the
handlers anymore.
2020-01-09 16:56:20 -07:00
Matthew Holt 994b9033e9 http: Don't use a Host matcher for HTTP->HTTPS redirects
In case on-demand TLS is enabled, in that case we don't know the only
names that have automatic HTTPS.

See https://caddy.community/t/v2-http-to-https-redirects-fail-for-on-demand-ssl-certs/6742?u=matt
2020-01-09 14:39:49 -07:00
Matthew Holt 590480513a Update docs for couple of Caddyfile directives 2020-01-09 14:38:59 -07:00
Matt Holt 7527c01705 v2: Implement Caddyfile enhancements (breaking changes) (#2960)
* http: path matcher: exact match by default; substring matches (#2959)

This is a breaking change.

* caddyfile: Change "matcher" directive to "@matcher" syntax (#2959)

* cmd: Assume caddyfile adapter for config files named Caddyfile

* Sub-sort handlers by path matcher length (#2959)

Caddyfile-generated subroutes have handlers, which are sorted first by
directive order (this is unchanged), but within directives we now sort
by specificity of path matcher in descending order (longest path first,
assuming that longest path is most specific).

This only applies if there is only one matcher set, and the path
matcher in that set has only one path in it. Path matchers with two or
more paths are not sorted like this; and routes with more than one
matcher set are not sorted like this either, since specificity is
difficult or impossible to infer correctly.

This is a special case, but definitely a very common one, as a lot of
routing decisions are based on paths.

* caddyfile: New 'route' directive for appearance-order handling (#2959)

* caddyfile: Make rewrite directives mutually exclusive (#2959)

This applies only to rewrites in the top-level subroute created by the
HTTP caddyfile.
2020-01-09 14:00:32 -07:00
Matthew Holt 8aef859a55 caddyfile: Less strict URL parsing; allows placeholders
See https://caddy.community/t/caddy-v2-reusable-snippets/6744/11?u=matt
2020-01-09 12:35:53 -07:00
Matt Holt a5ebec0041 http: Change routes to sequential matcher evaluation (#2967)
Previously, all matchers in a route would be evaluated before any
handlers were executed, and a composite route of the matching routes
would be created. This made rewrites especially tricky, since the only
way to defer later matchers' evaluation was to wrap them in a subroute,
or to invoke a "rehandle" which often caused bugs.

Instead, this new sequential design evaluates each route's matchers then
its handlers in lock-step; matcher-handlers-matcher-handlers...

If the first matching route consists of a rewrite, then the second route
will be evaluated against the rewritten request, rather than the original
one, and so on.

This should do away with any need for rehandling.

I've also taken this opportunity to avoid adding new values to the
request context in the handler chain, as this creates a copy of the
Request struct, which may possibly lead to bugs like it has in the past
(see PR #1542, PR #1481, and maybe issue #2463). We now add all the
expected context values in the top-level handler at the server, then
any new values can be added to the variable table via the VarsCtxKey
context key, or just the GetVar/SetVar functions. In particular, we are
using this facility to convey dial information in the reverse proxy.

Had to be careful in one place as the middleware compilation logic has
changed, and moved a bit. We no longer compile a middleware chain per-
request; instead, we can compile it at provision-time, and defer only the
evaluation of matchers to request-time, which should slightly improve
performance. Doing this, however, we take advantage of multiple function
closures, and we also changed the use of HandlerFunc (function pointer)
to Handler (interface)... this led to a situation where, if we aren't
careful, allows one request routed a certain way to permanently change
the "next" handler for all/most other requests! We avoid this by making
a copy of the interface value (which is a lightweight pointer copy) and
using exclusively that within our wrapped handlers. This way, the
original stack frame is preserved in a "read-only" fashion. The comments
in the code describe this phenomenon.

This may very well be a breaking change for some configurations, however
I do not expect it to impact many people. I will make it clear in the
release notes that this change has occurred.
2020-01-09 10:00:13 -07:00
Mark Sargent 7c419d5349 caddyfile: Preprocess env vars in {$THIS} format (#2963)
* transform a caddyfile with environment variables

* support adapt time and runtime variables in the caddyfile

* caddyfile: Pre-process environment variables before parsing

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-01-09 09:40:16 -07:00
Matthew Holt 3828a3aaac go.mod: Update lego, tidy up 2020-01-08 18:40:17 -07:00
Matthew Holt 8bae8f5f5a http: Always set status code via response recorder
Fixes panic if no upstream handler wrote anything to the response
2020-01-08 18:37:41 -07:00
Zaq? Wiedmann 21f1f95e7b reverse_proxy: Add tls_trusted_ca_certs to Caddyfile (#2936)
Allows specifying ca certs with by filename in
`reverse_proxy.transport`.

Example
```
reverse_proxy /api api:443 {
    transport http {
        tls
        tls_trusted_ca_certs certs/rootCA.pem
    }
}
```
2020-01-07 12:07:42 -07:00
Matthew Holt 78e98c40d3 basicauth: Accept placeholders; move base64 decoding to provision
See https://caddy.community/t/v2-basicauth-bug/6738?u=matt
2020-01-07 08:50:18 -07:00
Matthew Holt 5c99267dd8 A few miscellaneous, minor fixes 2020-01-06 08:10:20 -07:00
Matthew Holt a6df4cdbbc logging: Add doc about which fields can't be filtered 2020-01-03 15:28:05 -07:00
Mohammed Al Sahaf dff78d82ce v2: housekeeping: address minor lint complaints (#2957)
* v2: housekeeping: update tools

* v2: housekeeping: adhere to US locale in spelling

* v2: housekeeping: simplify code
2020-01-03 11:33:22 -07:00
Matthew Holt 8c7c2e4af2 logging: Little fix for filtering object fields 2020-01-01 10:26:37 -07:00
Matthew Holt 3d9f8eac08 Couple of minor fixes, update readme 2019-12-31 22:51:55 -07:00
Matthew Holt 06ea0a5295 Tune AppConfigDir and docs for Storage module 2019-12-31 18:31:43 -07:00
Matthew Holt 788462bd4c file-server command: Use safer defaults; http: improve host matcher docs 2019-12-31 16:57:54 -07:00
Matthew Holt 5a0603ed72 Config auto-save; run --resume flag; update environ output (close #2903)
Config auto-saving is on by default and can be disabled. The --environ
flag (or environ subcommand) now print more useful information from
Caddy and the runtime, including some nifty paths.
2019-12-31 16:56:19 -07:00
Matthew Holt 984d384d14 Change storage paths to follow OS conventions; migrate folder (#2955) 2019-12-31 16:47:35 -07:00
Matthew Holt fdabac51a8 Improve docs, especially w.r.t. placeholders and template actions 2019-12-29 13:16:34 -07:00
Matthew Holt 95d944613b Export Replacer and use concrete type instead of interface
The interface was only making things difficult; a concrete pointer is
probably best.
2019-12-29 13:12:52 -07:00
Matthew Holt 2b33d9a5e5 http: Enable TLS for servers listening only on HTTPS port
It seems silly to have to add a single, empty TLS connection policy to
a server to enable TLS when it's only listening on the HTTPS port. We
now do this for the user as part of automatic HTTPS (thus, it can be
disabled / overridden).

See https://caddy.community/t/v2-catch-all-server-with-automatic-tls/6692/2?u=matt
2019-12-28 23:56:08 -07:00
Matthew Holt 5c8b502964 fastcgi: Set SERVER_SOFTWARE, _NAME, and _PORT properly (fixes #2952) 2019-12-28 16:35:29 -07:00
Matthew Holt 82bebfab8a templates: Change functions, add front matter support, better markdown 2019-12-23 12:56:41 -07:00
Matthew Holt be3849c267 Remove markdown module 2019-12-23 12:55:52 -07:00
Matthew Holt 16ee985c22 admin: Only write most CORS headers in OPTIONS requests 2019-12-23 12:46:01 -07:00
Matthew Holt 95ed603de7 Improve godocs all around
These will be used in the new automated documentation system
2019-12-23 12:45:35 -07:00
Matthew Holt cbb405f6aa cmd: Eliminate unintended use of cgo
This means the stop command can only use the API to stop the instance;
no more signaling, unless we find a cgo-free way of doing it.
2019-12-23 12:41:05 -07:00
Matthew Holt 724c728678 rewrite: Attempt query string fix (#2891) 2019-12-17 16:30:26 -07:00
Matthew Holt 21408212da http: query and query_string placeholders should use RawQuery, probably 2019-12-17 16:29:37 -07:00
Matthew Holt fe516575db core: Add ReplaceFunc method to Replacer to allow dynamic replacements 2019-12-17 16:29:09 -07:00
Matthew Holt 080a62d5c5 Update go.mod; use CertMagic v0.9.0 2019-12-17 10:59:35 -07:00
Matthew Holt dae4913fe3 http: Patch path matcher to ignore dots and spaces (#2917)
(Try saying "patch path match" ten times fast)
2019-12-17 10:14:04 -07:00
Matthew Holt 6455efa5d3 admin: POST /... expands and appends all array elements
Makes it easy to append many items to an array in one command
2019-12-17 10:11:45 -07:00
Matthew Holt 5ab17a3a37 admin: /stop endpoint gracefully shuts down; fixes caddy stop command 2019-12-16 13:46:39 -07:00
Abdelmalek Ihdene c3bcd967bd logging: Implement net writer (#2884)
* Implement UDP writer

* Implement Net Writer

* Utilize Caddy's address parsing functions

* A couple little fixes (see #2884)
2019-12-15 12:58:01 -07:00
Matthew Holt 6ea121ddf8 tls: Ensure conn policy is created when providing certs in Caddyfile
Fixes #2929
2019-12-13 16:32:27 -07:00
Matthew Holt 8005b7ab73 Couple of quick fixes 2019-12-13 15:36:00 -07:00
Matthew Holt b1a456cfe3 rewrite: strip_prefix, strip_suffix, and uri_replace dirs (closes #2906) 2019-12-12 15:46:13 -07:00
Matthew Holt 5e9d81b507 try_files, rewrite: allow query string in try_files (fix #2891)
Also some minor cleanup/improvements discovered along the way
2019-12-12 15:27:09 -07:00
Matthew Holt 09a8517065 rewrite: query string enh.; substring replace; add tests (see #2891) 2019-12-12 14:32:35 -07:00
Matthew Holt 87b6cf470b Minor improvements; comments and shorter placeholders & module IDs 2019-12-12 14:31:20 -07:00
Matthew Holt f935458e3e cmd: Fix validate command when JSON contains "@id" fields
Also, don't run admin server when validating...
2019-12-12 14:30:22 -07:00
Matt Holt 2e0615270d fuzz: Remove Caddyfile adapter from fuzz corpus (#2925)
The Caddyfile adapter does not need to be fuzzed, as all it really does
is invoke the Caddyfile parser, which is already fuzzed
2019-12-10 15:00:31 -07:00
Matthew Holt fab5e4372a core: Add godoc examples for LoadModule 2019-12-10 14:06:35 -07:00
Matt Holt 3c90e370a4 v2: Module documentation; refactor LoadModule(); new caddy struct tags (#2924)
This commit goes a long way toward making automated documentation of
Caddy config and Caddy modules possible. It's a broad, sweeping change,
but mostly internal. It allows us to automatically generate docs for all
Caddy modules (including future third-party ones) and make them viewable
on a web page; it also doubles as godoc comments.

As such, this commit makes significant progress in migrating the docs
from our temporary wiki page toward our new website which is still under
construction.

With this change, all host modules will use ctx.LoadModule() and pass in
both the struct pointer and the field name as a string. This allows the
reflect package to read the struct tag from that field so that it can
get the necessary information like the module namespace and the inline
key.

This has the nice side-effect of unifying the code and documentation. It
also simplifies module loading, and handles several variations on field
types for raw module fields (i.e. variations on json.RawMessage, such as
arrays and maps).

I also renamed ModuleInfo.Name -> ModuleInfo.ID, to make it clear that
the ID is the "full name" which includes both the module namespace and
the name. This clarity is helpful when describing module hierarchy.

As of this change, Caddy modules are no longer an experimental design.
I think the architecture is good enough to go forward.
2019-12-10 13:36:46 -07:00
Marten Seemann a8533e5630 update quic-go to v0.14.1 (#2918) 2019-12-07 10:29:03 -07:00
Matthew Holt b07f6958ac Use "IsUnixNetwork" function instead of repeating the logic 2019-12-06 12:00:04 -07:00
Matthew Holt 33a318d173 Don't append port to unix sockets
See https://caddy.community/t/caddy-v2-php-fpm-502-error/6571?u=matt
2019-12-06 11:45:50 -07:00
lu4p 68adfdc559 Fix misspellings (#2908) 2019-12-04 16:28:13 -07:00
Marten Seemann a841688cc0 update quic-go to v0.14.0 (#2916) 2019-12-03 20:49:01 -07:00
Matthew Holt 52ae5f70d2 Merge branch 'v2' of ssh://github.com/caddyserver/caddy into v2 2019-11-30 17:53:38 -07:00
Matthew Holt 44f23a67bb http: Don't listen 1 port beyond port range 2019-11-30 17:53:25 -07:00
Mark Sargent 8b7d6a9ee8 v2: fixes query matcher parsing (#2901)
* fixes query matcher parsing

* return correct argument error when parsing query matcher
2019-11-29 13:05:22 -07:00
Matthew Holt 7c7ef8d40e http: Shorten regexp matcher placeholders; allow "=/" for simple matcher 2019-11-29 11:23:49 -07:00
Matthew Holt 14d3fd7d03 http: path matcher supports exact matching with = prefix 2019-11-28 21:11:45 -07:00
Matthew Holt 512b004332 http: header matcher supports fast prefix and suffix matching (#2888) 2019-11-27 11:52:31 -07:00
Matthew Holt db4293cb5f reverse_proxy: Add flush_interval to caddyfile syntax (#1460)
Also add godoc for Caddyfile syntax for file_server
2019-11-27 11:51:32 -07:00
Matthew Holt 6e10586303 admin: Preserve "@id" fields through partial changes (fixes #2902) 2019-11-27 11:49:49 -07:00
Matthew Holt 8de1a76227 reverse_proxy: Fix invalid argument to Intn in RandomChoice selection 2019-11-18 14:22:55 -07:00
Matthew Holt 9fe54e1c60 file_server: Use HTTPS port when a qualifying domain is specified
Also little comment cleanups
2019-11-16 10:44:45 -07:00
Matthew Holt b43e986a52 file_server: Optional pass_thru mode
If enabled, will call the next handler in the chain instead of returning
a 404.
2019-11-15 17:32:13 -07:00
Matthew Holt 1228dd7d93 reverse_proxy: Allow buffering of client requests
This is a bad idea, but some backends apparently require it. See
discussion in #176.
2019-11-15 17:15:33 -07:00
Matthew Holt af26a03da1 http: Only enable access logs if configured 2019-11-15 17:01:07 -07:00
Matthew Holt 8025ad9107 cmd: Disable admin endpoint for file-server and reverse-proxy commands
This makes it easier to use multiple instances on the same machine
2019-11-15 15:52:19 -07:00
Matthew Holt 6cdb2392d7 cmd: Improve stop command by trying API before signaling process
This allows graceful shutdown on all platforms
2019-11-15 15:45:18 -07:00
Matthew Holt 0ca109db4a Minor cleanups 2019-11-15 12:47:38 -07:00
Matthew Holt 0fc97211ab http: Make path matcher case-insensitive
Adds tests for both the path matcher and host matcher for case
insensitivity.

If case sensitivity is required for the path, a regexp matcher can
be used instead.

This is the v2 equivalent fix of PR #2882.
2019-11-15 12:47:06 -07:00
Matthew Holt ad90b273db core: Add tests to Replacer; fix panic (fixes #2852) 2019-11-11 19:29:31 -07:00
Mohammed Al Sahaf 93bc1b72e3 core: Use port ranges to avoid OOM with bad inputs (#2859)
* fix OOM issue caught by fuzzing

* use ParsedAddress as the struct name for the result of ParseNetworkAddress

* simplify code using the ParsedAddress type

* minor cleanups
2019-11-11 15:33:38 -07:00
Matthew Holt a19da07b72 http: Add response headers to access logs 2019-11-11 14:02:01 -07:00
Matthew Holt 16782d9988 http: Use permanent redirects for HTTP->HTTPS 2019-11-11 14:01:42 -07:00
Sarat Chandra dfdddcfacb logging: Support placeholders in level and filename (#2872)
* Add support for placeholders in Config

Fixes #2870

* Replace placeholders only in logging config.

Placeholders in log level and filename incase of file output are replaced.

* Add Provision to filewriter module for replacing placeholders
2019-11-11 11:04:41 -07:00
Marten Seemann 7ff02f37b6 go.mod: update quic-go to v0.13.1 (#2871) 2019-11-09 08:10:43 -07:00
Matthew Holt e4a2add73f cmd: Print errors to stderr 2019-11-08 09:59:49 -07:00
Matthew Holt 95615f5377 reverse_proxy: Fix NTLM auth detection
D'oh. Got mixed up in a refactoring.
2019-11-06 00:16:16 -07:00
Matthew Holt 8e515289cb reverse_proxy: Add support for NTLM 2019-11-05 16:29:10 -07:00
Matthew Holt 6e95477224 http: Eliminate allocation in cloneURL; add RemoteAddr to origRequest 2019-11-05 16:28:33 -07:00
Matthew Holt 97d918df3e reverse_proxy: Make HTTP versions configurable, don't set NextProtos 2019-11-05 16:27:51 -07:00
Matthew Holt f5c6a8553c Prepare for beta 9 tag 2019-11-04 13:43:39 -07:00
Matthew Holt 263ffbfaec caddyfile: Fix bug with Delete
It now will delete the current token even if it is the last one
2019-11-04 13:25:37 -07:00
Matthew Holt bf363f061d reverse_proxy: Add UnmarshalCaddyfile for random_choose selection policy
Also allow caddy.Duration to be given integer values which are treated
like regular time.Duration values (nanoseconds).

Fixes #2856
2019-11-04 12:54:46 -07:00
Matthew Holt 7129f6c1c0 admin: Remove /unload endpoint (is same as DELETE /config/) 2019-11-04 12:53:14 -07:00
Matthew Holt cb25dd72ab reverse_proxy: Add port to upstream address if only implied in scheme 2019-11-04 12:18:42 -07:00
Matthew Holt d55fa68902 http: Only log handler errors >= 500
Errors in the 4xx range are client errors, and they don't need to be
entered into the server's error logs. 4xx errors are still recorded in
the access logs at the error level.
2019-11-04 12:18:01 -07:00
Matthew Holt b1f41d0ff1 logging: Default logger should use wall time with milliseconds
This format is easier for humans to read and is still very precise.
2019-11-04 12:14:22 -07:00
Matthew Holt 6011ce120a cmd: Move module imports into standard packages
This makes it easier to make "standard" caddy builds, since you'll only
need to add a single import to get all of Caddy's standard modules.

There is a package for all of Caddy's standard modules (modules/standard)
and a package for the HTTP app's standard modules only
(modules/caddyhttp/standard).

We still need to decide which of these, if not all of them, should be
kept in the standard build. Those which aren't should be moved out of
this repo. See #2780.
2019-11-04 12:13:21 -07:00
Matthew Holt 27e288ab19 core: Synchronize calls to SetDeadline within fakeCloseListener
First evidenced in #2658, listener deadlines would sometimes be set
after clearing them, resulting in endless i/o timeout errors, which
leave all requests hanging. This bug is fixed by synchronizing the
calls to SetDeadline: when Close() is called, the deadline is first
set to a time in the past, and the lock is released only after the
deadline is set, so when the other servers break out of their Accept()
calls, they will clear the deadline *after* it was set. Before, the
clearing could sometimes come before the set, which meant that it was
left in a timeout state indefinitely.

This may not yet be a perfect solution -- ideally, the setting and
clearing of the deadline would happen exactly once per underlying
listener, not once per fakeCloseListener, but in rigorous testing with
these changes (comprising tens of thousands of config reloads), I was
able to verify that no race condition is manifest.
2019-11-04 12:10:03 -07:00
Matthew Holt 35f70c98fa core: Major refactor of admin endpoint and config handling
Fixed several bugs and made other improvements. All config changes are
now mediated by the global config state manager. It used to be that
initial configs given at startup weren't tracked, so you could start
caddy with --config caddy.json and then do a GET /config/ and it would
return null. That is fixed, along with several other general flow/API
enhancements, with more to come.
2019-11-04 12:05:20 -07:00
Matthew Holt fb06c041c4 http: Ensure server loggers are not nil (fixes #2849) 2019-10-31 11:45:18 -06:00
Matthew Holt 8ef0a0b4f8 reverse_proxy: Fix panic for some CLI flag values (closes #2848) 2019-10-31 11:34:54 -06:00
Matthew Holt 8d3c64932e http: Avoid panic if handler errors lack underlying error value
Fixes #2845
2019-10-30 21:41:52 -06:00
Mohammed Al Sahaf 0dd9243478 Re-remove admin fuzz target from azure-pipelines.yml (#2846)
Fixing a git-oopsie on my behalf
2019-10-31 01:49:18 +03:00
Andreas Schneider 432b94239d admin listener as opt-in for initial config (#2834)
* Always cleanup admin endpoint first

* Error out if no config has been set (#2833)

* Ignore explicitly missing admin config (#2833)

* Separate config loading from admin initialization (#2833)

* Add admin option to specify admin listener address (#2833)

* Use zap for reporting admin endpoint status
2019-10-30 15:12:42 -06:00
Mohammed Al Sahaf 4611537f06 Add missing fuzzer (#2844)
* fuzz: add missing fuzzer by fixing .gitignore adding a negation for caddyfile/ directory

* ci: print fuzzing type for debuggability and traceability

* README: update the Fuzzit badge to point to the correct Caddy server Github organization
2019-10-30 23:57:22 +03:00
Matthew Holt 76c22c7b38 auth: Clean up basicauth 2019-10-30 13:56:27 -06:00
Matthew Holt c7da6175bc fuzz: Remove admin fuzzer
Not really necessary; underlying work is done by json.Unmarshal which
is part of the Go standard lib. Also, it called Run, which potentially
tries to get certificates; we should not let that happen.
2019-10-30 12:19:59 -06:00
Matthew Holt 11a2733dc2 ci: Change fuzz type from regression to local-regression
As per recommendation from Fuzzit devs
2019-10-30 11:50:19 -06:00
Matthew Holt 1be121cec7 fuzz: Don't call Load() in HTTP caddyfile adapter fuzz tests
Doing so has a tendency to request certificates...
2019-10-30 11:48:21 -06:00
Matthew Holt dccba71276 reverse_proxy: Structured logs 2019-10-29 16:02:58 -06:00
Mohammed Al Sahaf be36aade9a ci: Update fuzzer target name (#2841)
Update the fuzzer target name for the address parser so it better matches the func name
2019-10-29 13:20:34 -06:00
Matthew Holt ba0000678d Remove unused fields from HandlerError 2019-10-29 11:59:08 -06:00
Matthew Holt c4c45f8e01 logging: Tweak defaults (enable logging by default, color level enc.) 2019-10-29 11:58:29 -06:00
Matthew Holt 54e458b756 proxy: Forgot to commit import 2019-10-29 10:22:49 -06:00
Matthew Holt d803561212 caddyhttp: Fix nil pointer dereference 2019-10-29 00:08:06 -06:00
Matthew Holt 813fff0584 proxy: Enable HTTP/2 on transport to backend 2019-10-29 00:07:45 -06:00
Matthew Holt d2e7baed8d Plug in distributed STEK module 2019-10-29 00:06:04 -06:00
Matthew Holt d6dad04e96 cache: Make peer addresses configurable 2019-10-28 15:09:12 -06:00
Matthew Holt 442fd748f6 caddyhttp: Minor cleanup and fix nil pointer deref in caddyfile adapter 2019-10-28 15:08:45 -06:00
Matt Holt b00dfd3965 v2: Logging! (#2831)
* logging: Initial implementation

* logging: More encoder formats, better defaults

* logging: Fix repetition bug with FilterEncoder; add more presets

* logging: DiscardWriter; delete or no-op logs that discard their output

* logging: Add http.handlers.log module; enhance Replacer methods

The Replacer interface has new methods to customize how to handle empty
or unrecognized placeholders. Closes #2815.

* logging: Overhaul HTTP logging, fix bugs, improve filtering, etc.

* logging: General cleanup, begin transitioning to using new loggers

* Fixes after merge conflict
2019-10-28 14:39:37 -06:00
Mohammed Al Sahaf 6c533558a3 fuzz-ci: fix & enhance fuzzing process (#2835)
* fuzz-ci: fix the authentication call for fuzzit by using the --api-key flag rather than the `auth` command

* Allow fuzzing on schedules as well as non-fork PRs

Closes #2710
2019-10-28 20:45:55 +03:00
Mohammed Al Sahaf 2fbe2ff40b fuzz: introduce continuous fuzzing for Caddy (#2723)
* fuzz: lay down the foundation for continuous fuzzing

* improve the fuzzers and add some

* fuzz: add Fuzzit badge to README & enable fuzzers submission in CI

* v2-fuzz: do away with the submodule approach for fuzzers

* fuzz: enable fuzzit
2019-10-25 18:52:16 -06:00
Matthew Holt faf67b1067 tls: Make the on-demand rate limiter actually work
This required a custom rate limiter implementation in CertMagic
2019-10-21 12:03:51 -06:00
Matthew Holt 208f2ff93c rewrite: Options to strip prefix/suffix and issue redirects
Fixes #2011
2019-10-19 19:22:29 -06:00
Mohammed Al Sahaf 19e834cf36 v2 ci: speed up some of powershell's processes (#2818)
* v2: speed up some of powershell's processes

* v2-ci: downloading latest Go on Windows isn't slow anymore, so update the log message accordingly

* v2: CI: use 7z on Windows instead of Expand-Archive
2019-10-17 14:58:22 -06:00
Matthew Holt bce2edd22d tls: Asynchronous cert management at startup (uses CertMagic v0.8.0) 2019-10-16 15:20:27 -06:00
Matthew Holt a458544d9f Minor enhancements/fixes to rewrite directive and template virt req's 2019-10-16 15:18:02 -06:00
Matt Holt 2f91b44587 v2: Make tests work on Windows (#2782)
* file_server: Make tests work on Windows

* caddyfile: Fix escaping when character is not escapable

We only escape certain characters depending on inside or outside of
quotes (mainly newlines and quotes). We don't want everyone to have to
escape Windows file paths like C:\\Windows\\... but we can't drop the
\ either if it's just C:\Windows\...
2019-10-15 16:05:53 -06:00
Mohammed Al Sahaf e3726588b4 v2: Project-and-CI-wide linter config (#2812)
* v2: split golangci-lint configuration into its own file to allow code editors to take advantage of it

* v2: simplify code

* v2: set the correct lint output formatting

* v2: invert the logic of linter's configuration of output formatting to allow the editor  convenience over CI-specific customization. Customize the output format in CI by passing the flag.

* v2: remove irrelevant golangci-lint config
2019-10-15 15:37:46 -06:00
Matthew Holt abf5ab340e caddyhttp: Improve ResponseRecorder to buffer headers 2019-10-15 14:07:10 -06:00
Matthew Holt acf7dea68f caddyhttp: host labels placeholders endianness from right->left
https://caddy.community/t/labeln-placeholder-endian-issue/5366

(I thought we had this before but it must have gotten lost somewhere)
2019-10-14 12:09:43 -06:00
Pascal bc738991b6 caddyhttp: Support placeholders in MatchHost (#2810)
* Replace global placeholders in host matcher

* caddyhttp: Fix panic on MatchHost tests
2019-10-14 11:29:36 -06:00
yzongyue fcd8869f51 reverse_proxy: optimize MaxIdleConnsPerHost default (#2809) 2019-10-11 23:57:11 -06:00
Matthew Holt 1e31be8de0 reverse_proxy: Allow dynamic backends (closes #990 and #1539)
This PR enables the use of placeholders in an upstream's Dial address.

A Dial address must represent precisely one socket after replacements.

See also #998 and #1639.
2019-10-11 14:25:39 -06:00
Matthew Holt 4aa3af4b78 go.mod: Use latest certmagic which uses lego v3.1.0 2019-10-11 10:48:06 -06:00
Matthew Holt 8715a28320 reverse_proxy: Customize SNI value in upstream request (closes #2483) 2019-10-10 17:17:06 -06:00
Matthew Holt 715e6ddf51 go.mod: Update dependencies 2019-10-10 15:47:26 -06:00
Matthew Holt 9c0bf311f9 Miscellaneous cleanups / comments 2019-10-10 15:38:30 -06:00
Matthew Holt 5300949e0d caddyhttp: Make responseRecorder capable of counting body size 2019-10-10 15:36:28 -06:00
Matthew Holt 411152016e Remove unused/placeholder log handler 2019-10-10 15:35:33 -06:00
Matthew Holt 5c7640a8d9 cmd: Plug in the http.handlers.authentication module 2019-10-10 15:05:33 -06:00
Matthew Holt f8366c2f09 http: authentication module; hash-password cmd; http_basic provider
This implements HTTP basicauth into Caddy 2. The basic auth module will
not work with passwords that are not securely hashed, so a subcommand
hash-password was added to make it convenient to produce those hashes.

Also included is Caddyfile support.

Closes #2747.
2019-10-10 14:37:27 -06:00
Pascal fe36d26b63 caddyhttp: Add RemoteAddr placeholders (#2801)
* Ignore build artifacts

* Add RemoteAddr placeholders
2019-10-10 13:37:08 -06:00
Matt Holt b38365ff3b Merge pull request #2799 from caddyserver/v2-enterprise-merge
v2: Merge enterprise code into open source v2 branch
2019-10-10 11:27:45 -06:00
Matthew Holt 26cc883708 http: Add Starlark handler
This migrates a feature that was previously reserved for enterprise
users, according to #2786.

The Starlark integration needs to be updated since this was made before
some significant changes in the v2 code base. When functional, it makes
it possible to have very dynamic HTTP handlers. This will be a long-term
ongoing project.

Credit to Danny Navarro
2019-10-10 11:02:16 -06:00
Matthew Holt 93943a6ac2 readme: Remove mentions of Caddy Enterprise (as per #2786) 2019-10-09 20:30:21 -06:00
Matthew Holt 85ce15a5ad tls: Add custom certificate selection policy
This migrates a feature that was previously reserved for enterprise
users, according to https://github.com/caddyserver/caddy/issues/2786.

Custom certificate selection policies allow advanced control over which
cert is selected when multiple qualify to satisfy a TLS handshake.
2019-10-09 19:41:45 -06:00
Matthew Holt dedcfd4e3d tls: Add distributed_stek module
This migrates a feature that was previously reserved for enterprise
users, according to https://github.com/caddyserver/caddy/issues/2786.

TLS session ticket keys are sensitive, so they should be rotated on a
regular basis. Only Caddy does this by default. However, a cluster of
servers that rotate keys without synchronization will lose the benefits
of having sessions in the first place if the client is routed to a
different backend. This module coordinates STEK rotation in a fleet so
the same keys are used, and rotated, across the whole cluster. No other
server does this, but Twitter wrote about how they hacked together a
solution a few years ago:
https://blog.twitter.com/engineering/en_us/a/2013/forward-secrecy-at-twitter.html
2019-10-09 19:38:26 -06:00
Matthew Holt 20fe9cf024 tls: Add pem_loader module
This migrates a feature that was previously reserved for enterprise
users, according to https://github.com/caddyserver/caddy/issues/2786.

The PEM loader allows you to embed PEM files (certificates and keys)
directly into your config, rather than requiring them to be stored on
potentially insecure storage, which adds attack vectors. This is useful
in automated settings where sensitive key material is stored only in
memory.

Note that if the config is persisted to disk, that added benefit may go
away, but there will still be the benefit of having lesser dependence on
external files.
2019-10-09 19:34:14 -06:00
Matthew Holt bcbe1c220d reverse_proxy: Add local circuit breaker
This migrates a feature that was previously reserved for enterprise
users, according to https://github.com/caddyserver/caddy/issues/2786.

The local circuit breaker is a simple metrics counter that can cause
the reverse proxy to consider a backend unhealthy before it actually
goes offline, by measuring recent latencies over a sliding window.

Credit to Danny Navarro
2019-10-09 19:28:07 -06:00
Matthew Holt a53b27c62e http: Add work-in-progress cache handler module
This migrates a feature that was previously reserved for enterprise
users, according to https://github.com/caddyserver/caddy/issues/2786.

The cache HTTP handler will be a high-performing, distributed cache
layer for HTTP requests. Right now, the implementation is a very basic
proof-of-concept, and further development is required.
2019-10-09 19:22:46 -06:00
Matthew Holt 03306e646e admin: /config and /id endpoints
This integrates a feature that was previously reserved for enterprise
users, according to https://github.com/caddyserver/caddy/issues/2786.

The /config and /id endpoints make granular config changes possible as
well as the exporting of the current configuration.

The /load endpoint has been modified to wrap the /config handler so that
the currently-running config can always be available for export. The
difference is that /load allows configs of varying formats and converts
them using config adapters. The adapted config is then processed with
/config as JSON. The /config and /id endpoints accept only JSON.
2019-10-09 19:10:00 -06:00
yzongyue 53dd600b4d cmd: Built-in commands all use RegisterCommand (#2794) 2019-10-08 20:12:15 -06:00
Matthew Holt ce1205239a cmd/main: Plug in json5 and jsonc config adapters 2019-10-06 20:48:31 -06:00
Matthew Holt bc3e44c1a6 cmd: adapt: Default --adapter value is "caddyfile" 2019-10-06 20:48:09 -06:00
Matthew Holt 8c55167f71 rewrite: Return parse error if too many Caddyfile args (fixes #2791) 2019-10-06 20:46:10 -06:00
Matthew Holt be7abda7d4 reverse_proxy: Implement retry_match; by default only retry GET requests
See https://caddy.community/t/http-proxy-and-non-get-retries/6304
2019-10-05 16:22:05 -06:00
Matthew Holt 6fd28b81dc caddyhttp: Define MatcherSets and RawMatcherSets types 2019-10-05 16:20:07 -06:00
Matthew Holt 65c060f56e file_server: Set default address to :2015 if --listen not specified 2019-10-04 17:30:51 -06:00
Matthew Holt 44cb804b9e reverse_proxy: Configurable request headers on active health checks
See https://caddy.community/t/health-check-user-agent/6309
2019-10-04 17:21:38 -06:00
Matthew Holt c11e3bffd6 Add file-server and reverse-proxy subcommands 2019-10-03 16:00:41 -06:00
Matthew Holt f29a9eee0d caddytls: nil check on storageClean fields on Stop 2019-10-02 23:39:32 -06:00
Matthew Holt 370b78c5c7 Update CLI docs in README 2019-10-01 20:45:31 -06:00
Mohammed Al Sahaf 1ecb216001 v2: introduce CI (#2768)
* v2: introduce CI for v2 branch

* v2-ci: split test report generation from test pass to preserve exit code

* v2-ci: spilt lint results from unit test results

* v2-ci: fix testRunTitle name

* v2-ci: break up the steps for more accurate status indicators

* v2-ci: break steps into different jobs

* v2-ci: revert back to single-job pattern

* v2-ci: reflect the true result by coercing SucceededWithIssues into Failed in the last step

* v2-ci: don't fail the build on lint errors
2019-10-01 16:47:29 -06:00
Matthew Holt 94f98c0733 go.mod: Use latest certmagic 2019-10-01 11:25:52 -06:00
Matthew Holt 2c3657bb8a cmd: CLI improvements; add --validate to adapt command 2019-10-01 11:02:13 -06:00
Matthew Holt 5b36424cf0 cmd: Add validate subcommand; list-modules --versions; some renaming
Renames --config-adapter flag to --adapter, adapt-config command to
adapt, --print-env flag to --environ, and --input flag to --config.
2019-09-30 23:43:39 -06:00
aca 0006df6026 cmd: Refactor subcommands, add help, make them pluggable
* cli: Change command structure, add help subcommand (#328)

* cli: improve subcommand structure

- make help command as normal subcommand
- add flag usage message for each command

* cmd: Refactor subcommands and command line help; make commands pluggable
2019-09-30 21:23:58 -06:00
Matthew Holt c95db3551d caddytls: Ensure automation field is not nil when appending (fix #2779) 2019-09-30 11:53:21 -06:00
Matthew Holt 8eb2c37251 Clean up provisioned modules on error; refactor Run(); add Validate()
Modules that return an error during provisioning should still be cleaned
up so that they don't leak any resources they may have allocated before
the error occurred. Cleanup should be able to run even if Provision does
not complete fully.
2019-09-30 09:16:01 -06:00
Matthew Holt 1e66226217 httpcaddyfile: Add acme_ca and email global options
Also add ability to access options from individual unmarshalers through
the Helper values
2019-09-30 09:11:30 -06:00
Matthew Holt 7b4aa108c7 caddyhttp: 'not' matcher: Support Caddyfile unmarshaling 2019-09-30 09:09:57 -06:00
Matthew Holt 8b11ed347b Add license header to filestorage.go 2019-09-30 09:08:04 -06:00
Matthew Holt b249b45d10 tls: Change struct fields to pointers, add nil checks; rate.Burst update
Making them pointers makes for cleaner JSON when adapting configs, if
the struct is empty now it will be omitted entirely.

The x/time/rate package was updated to support changing the burst, so
we've incorporated that here and removed a TODO.
2019-09-30 09:07:43 -06:00
Matthew Holt c12bf4054c caddyfile: Fix lexer behavior with regards to escaped newlines
Newlines (\n) can be escaped outside of quoted areas and the newline
will be treated as whitespace but not as an actual line break. Escaping
newlines inside a quoted area is not necessary, and because quotes
trigger literal interpretation of the contents, the escaping backslash
will be parsed as a literal backslash, and the newline will not be
escaped.

Caveat: When a newline is escaped, tokens after it until an unescaped
newline will appear to the parser be on the same line as the initial
token after the last unescaped newline. This may technically lead to
some false line numbers if errors are given, but escaped newlines are
counted so that the next token after an unescaped newline is correct.

See #2766
2019-09-28 21:18:36 -06:00
Matthew Holt 735d6ce405 httpcaddyfile: Fix missing module name of storage adapter 2019-09-26 17:06:15 -07:00
Matthew Holt 7b33c8db31 tls: Make cert and OCSP check intervals configurable
This enables use of ACME CAs that issue shorter-lived certs
2019-09-24 17:04:03 -07:00
Matt Holt 11696793bd tls/acme: Ability to customize trusted roots for ACME servers (#2756)
Closes #2702
2019-09-24 15:46:39 -07:00
Matthew Holt 3e8bff594a go.mod: Update certmagic to v0.7.3 2019-09-20 13:17:17 -06:00
Matthew Holt 2f684e42d5 reverse_proxy/headers: Expose header replacement ability in Caddyfile
Adds header_up and header_down subdirectives to reverse_proxy
2019-09-20 13:13:49 -06:00
Matthew Holt ba29f9d41d httpcaddyfile: Global storage configuration (closes #2758) 2019-09-19 12:42:36 -06:00
Matthew Holt 40e05e5a01 http: Improve auto HTTP->HTTPS redirects, fix edge cases
See https://caddy.community/t/v2-issues-with-multiple-server-blocks-in-caddyfile-style-config/6206/13?u=matt

Also print pid when using `caddy start`
2019-09-18 18:01:32 -06:00
Matthew Holt 39d61cad2d httpcaddyfile: Fix nil pointer dereference 2019-09-18 10:51:49 -06:00
Matthew Holt bc9f944837 host matcher: Strip [ ] from IPv6 addresses 2019-09-18 09:45:21 -06:00
Matthew Holt 4c289fc6ad Allow domain fronting with TLS client auth if explicitly configured 2019-09-17 23:13:21 -06:00
Matthew Holt 19f36667f7 tls: Clean up expired OCSP staples and certificates 2019-09-17 16:00:15 -06:00
Matt Holt 484cee1ac1 fastcgi: Implement / redirect for index.php with php_fastcgi directive (#2754)
* fastcgi: Implement / redirect for index.php with php_fastcgi directive

See #2752 and https://caddy.community/t/v2-redirect-path-to-path-index-php-with-assets/6196?u=matt

* caddyhttp: MatchNegate implements json.Marshaler

* fastcgi: Add /index.php element to try_files matcher

* fastcgi: Make /index.php redirect permanent
2019-09-17 15:16:17 -06:00
Matthew Holt d030bfdae0 httpcaddyfile: static_response -> respond; minor cleanups 2019-09-16 11:04:18 -06:00
Matthew Holt db4c73dd58 reverse_proxy: Close idle connections on module unload 2019-09-14 18:10:29 -06:00
Matthew Holt f15f0d5839 Eliminate some TODOs 2019-09-14 18:05:45 -06:00
Matthew Holt e73b117332 reverse_proxy: Ability to mutate headers; set upstream placeholders 2019-09-14 13:25:26 -06:00
Matthew Holt 2fd22139c6 headers: Ability to mutate request headers including http.Request.Host
Also a few bug fixes
2019-09-14 13:22:48 -06:00
Mohammed Al Sahaf 5c9ebe3af1 Use keybase fork of mitchellh/go-ps for bug fixes (#2750) 2019-09-13 23:40:29 -06:00
Matthew Holt 2ab2d5bf9e Forgot to commit caddyfile.go changes in last commit 2019-09-13 23:38:52 -06:00
Matthew Holt c09e86fddc headers: Add ability to replace substrings in header fields
This will probably be useful so the proxy can rewrite header values.
2019-09-13 16:24:51 -06:00
Matthew Holt 46aaf02371 encode: Fix bug where default status code was being written
for small responses.

See https://caddy.community/t/v2-permanent-redirect-prompt/6190?u=matt
2019-09-13 16:00:03 -06:00
Matthew Holt 3b80c505fb Update v2 readme in prep for beta1 2019-09-13 12:50:06 -06:00
Matthew Holt 1d1e194229 Hard-code 'main' module name until bug upstream in Go modules is fixed
See https://github.com/golang/go/issues/29228
2019-09-13 12:43:28 -06:00
Matthew Holt 839507e24e http: Consider wildcards when evaluating automatic HTTPS 2019-09-13 11:46:58 -06:00
Matthew Holt 833d67446f admin: Allow listening on unix socket (closes #2749) 2019-09-13 11:24:07 -06:00
Matthew Holt d0c1756fc5 httpcaddyfile: Fix tls certificate loader module names (#2748) 2019-09-13 09:45:10 -06:00
Matthew Holt ed40a5dcab tls: Do away with SetDefaults which did nothing useful
CertMagic uses the same defaults for us
2019-09-12 17:31:54 -06:00
Matthew Holt 7799554baa go.mod: Use lego v3 and CertMagic 0.7.0 2019-09-12 17:31:10 -06:00
Matthew Holt 2cb01d43cf tls: Remove support for TLS 1.0 and TLS 1.1 2019-09-11 22:26:06 -06:00
Matthew Holt 758269124e reverseproxy: Fix host and port on requests; fix Caddyfile parser 2019-09-11 18:53:44 -06:00
Matthew Holt b4dce74e59 tls: Use Let's Encrypt production endpoint
We're done testing this in staging
2019-09-11 18:52:07 -06:00
Matthew Holt fe389fcbd7 http: Set Alt-Svc header if experimental HTTP3 server is enabled 2019-09-11 18:49:21 -06:00
Matthew Holt 005a11cf4b headers: New 'request_header' directive; handle Host header specially
Before this change, only response headers could be manipulated with the
Caddyfile's 'header' directive.

Also handle the request Host header specially, since the Go standard
library treats it separately from the other header fields...
2019-09-11 18:48:37 -06:00
Matthew Holt 194df652eb reverseproxy: Add 'tls' option to enable HTTPS with HTTP transport 2019-09-11 18:46:32 -06:00
Matthew Holt 53bbdf1766 httpcaddyfile: Add 'experimental_http3' option 2019-09-11 17:16:21 -06:00
Matthew Holt e48d83452e httpcaddyfile: Switch order; reverse_proxy comes before php_fastcgi 2019-09-11 12:02:35 -06:00
Matthew Holt 2459c292a4 caddyfile: Improve Dispenser.NextBlock() to support nesting 2019-09-10 19:21:52 -06:00
Matthew Holt 0cf592fa2e New 'php_fastcgi' directive for convenient PHP+FastCGI reverse proxy 2019-09-10 14:16:41 -06:00
Matthew Holt d9136fb0a0 rewrite: Caddyfile directive should always invoke a rehandle
This is unless each route's matcher is dynamically executed after
previous handlers...
2019-09-10 14:13:52 -06:00
Matthew Holt c32b7e8865 fastcgi: Make EnvVars a map instead of a slice 2019-09-10 14:12:51 -06:00
Matthew Holt 1ce10b453f Require Go 1.13; use Go 1.13's default support for TLS 1.3 2019-09-10 13:11:27 -06:00
Matt Holt 0c8ad52be1 Experimental IETF-standard HTTP/3 support (known issue exists) (#2727)
* Begin WIP integration of HTTP/3 support

* http3: Set actual Handler, make fakeClosePacketConn type for UDP sockets

Also use latest quic-go for ALPN fix

* Manually keep track of and close HTTP/3 listeners

* Update quic-go after working through some http3 bugs

* Fix go mod

* Make http3 optional for now
2019-09-10 08:03:37 -06:00
Matthew Holt d67d8cf5a8 Fix build (sigh) 2019-09-10 07:15:36 -06:00
Matt Holt 44b7ce9850 Merge pull request #2737 from caddyserver/fastcgi (reverse proxy!)
v2: Refactor reverse proxy and add FastCGI support
2019-09-09 21:46:21 -06:00
Matthew Holt b4f4fcd437 Migrate some selection policy tests over to v2 2019-09-09 21:44:58 -06:00
Matthew Holt 50e62d06bc reverse_proxy: Caddyfile integration (and fix blocks in Dispenser) 2019-09-09 12:23:27 -06:00
Matthew Holt 9169cd43d4 Log when auto HTTPS or auto HTTP->HTTPS redirects are disabled 2019-09-09 08:25:48 -06:00
Matthew Holt e12c62e60b file_server: Enforce URL canonicalization (closes #2741) 2019-09-09 08:21:45 -06:00
Ingo Gottwald 3e9e7555ef Fix build (#2740)
Build was broken with commit 50961ec.
2019-09-07 14:25:04 -06:00
Matthew Holt f6126acf37 Header matchers: allow matching presence of header with empty list 2019-09-06 14:25:16 -06:00
Matthew Holt 97ace2a39e File matcher enforces trailing-slash convention to match dirs/files 2019-09-06 13:32:02 -06:00
Matthew Holt 4bd9496525 Fix Schrodinger's file existence check in file matcher
See: https://stackoverflow.com/a/12518877/1048862

For example, trying to check the existence of "/www/index.php/index.php"
fails but not with an os.IsNotExist()-type error. So we have to assume
that a file that cannot be successfully stat'ed at all does not exist.
2019-09-06 12:57:12 -06:00
Matthew Holt 14f9662f9c Various fixes/tweaks to HTTP placeholder variables and file matching
- Rename http.var.* -> http.vars.* to be more consistent
- Prefixing a path matcher with * now invokes simple suffix matching
- Handlers and matchers that need a root path default to {http.vars.root}
- Clean replacer output on the file matcher's file selection suffix
2019-09-06 12:36:45 -06:00
Matthew Holt 21d7b662e7 fastcgi: Use request context as base, not a new one 2019-09-06 12:02:11 -06:00
Matthew Holt 3ba9e143a2 cli: Fix run and start when no config file is available 2019-09-05 14:59:19 -06:00
Matthew Holt d2e46c2be0 fastcgi: Set default root path; add interface guards 2019-09-05 13:42:20 -06:00
Matthew Holt 80b54f3b9d Add original URI to request context; implement into fastcgi env 2019-09-05 13:36:42 -06:00
Matthew Holt 0830fbad03 Reconcile upstream dial addresses and request host/URL information
My goodness that was complicated

Blessed be request.Context

Sort of
2019-09-05 13:14:39 -06:00
Matthew Holt a60d54dbfd reverse_proxy: Ignore context.Canceled errors
These happen when downstream clients cancel the request, but that's not
our problem nor a failure in our end
2019-09-03 19:10:09 -06:00
Matthew Holt acb8f0e0c2 Integrate circuit breaker modules with reverse proxy 2019-09-03 19:06:54 -06:00
Matthew Holt 652460e03e Some cleanup and godoc 2019-09-03 16:56:09 -06:00
Matthew Holt 4a1e1649bc reverse_proxy: Implement remaining TLS config for proxy to backend 2019-09-03 15:26:09 -06:00
Matthew Holt ccfb12347b reverse_proxy: Implement active health checks 2019-09-03 12:10:11 -06:00
Alexandre Stein 50961ecc77 Initial implementation of TLS client authentication (#2731)
* Add support for client TLS authentication

Signed-off-by: Alexandre Stein <alexandre_stein@interlab-net.com>

* make and use client authentication struct

* force StrictSNIHost if TLSConnPolicies is not empty

* Implement leafs verification

* Fixes issue when using multiple verification

* applies the comments from maintainers

* Apply comment

* Refactor/cleanup initial TLS client auth implementation
2019-09-03 09:35:36 -06:00
Matthew Holt 026df7c5cb reverse_proxy: WIP refactor and support for FastCGI 2019-09-02 22:01:02 -06:00
Matthew Holt 8e821b5039 caddyconfig: Add JSON5 and JSON-C adapters (closes #2735) 2019-09-02 12:21:41 -06:00
Matthew Holt 9d8bff28c2 oops, also update the Caddyfile's {query} var to use query_string 2019-08-27 14:41:57 -06:00
Matthew Holt d242f10eda Add query_string to HTTP replacer and use it for try_files 2019-08-27 14:38:24 -06:00
Ariel Núñez 2dc4fcc62b Fix caddyconfig import in admin.go (#2725) 2019-08-23 10:57:51 -06:00
Matthew Holt afd154119a admin: Support config adapters at /load endpoint
Based on Content-Type
2019-08-22 14:52:39 -06:00
Matthew Holt e34ff21a71 caddyfile: Allow handler order to be customized 2019-08-22 14:26:33 -06:00
Matthew Holt af25f0254e caddyfile: Support global config block; allow non-empty blocks w/ 0 keys 2019-08-22 13:38:37 -06:00
Mohammed Al Sahaf a0fd2b6c0a Fix SIV where /v2 was missing from caddyfile adapter work (#2721) 2019-08-22 12:26:48 -06:00
Matthew Holt c0da7d487a file_server: Automatically hide all involved Caddyfiles 2019-08-21 15:50:02 -06:00
Matthew Holt 8420a2f250 Clean up Dispenser and filename handling a bit 2019-08-21 15:23:00 -06:00
Matthew Holt 59910923d1 Update readme for v2 caddyfile and config adapters 2019-08-21 12:31:58 -06:00
Matt Holt 0544f0266a Merge pull request #2699 from caddyserver/cfadapter
v2: Implement config adapters and WIP Caddyfile adapter
2019-08-21 11:28:03 -06:00
Matthew Holt b2aa679c33 Fix snippet nesting bug 2019-08-21 11:26:48 -06:00
Matthew Holt fa334c4bdf Implement some shorthand placeholders for Caddyfile 2019-08-21 11:03:50 -06:00
Matthew Holt d73b650c26 Update go.mod 2019-08-21 10:47:09 -06:00
Matthew Holt c9980fd367 Refactor Caddyfile adapter and module registration
Use piles from which to draw config values.

Module values can return their name, so now we can do two-way mapping
from value to name and name to value; whereas before we could only map
name to value. This was problematic with the Caddyfile adapter since
it receives values and needs to know the name to put in the config.
2019-08-21 10:46:35 -06:00
Albert Shirima 42f75a4ca9 Fixing a compilation error (#2712)
./caddy.go:230:12: cannot use *dep (type debug.Module) as type *debug.Module in return argument
./caddy.go:233:12: cannot use bi.Main (type debug.Module) as type *debug.Module in return argument
2019-08-17 19:14:55 -06:00
Matthew Holt c4159ef76d Fix module-related errors 2019-08-09 12:19:56 -06:00
Matthew Holt ab885f07b8 Implement config adapters and beginning of Caddyfile adapter
Along with several other changes, such as renaming caddyhttp.ServerRoute
to caddyhttp.Route, exporting some types that were not exported before,
and tweaking the caddytls TLS values to be more consistent.

Notably, we also now disable automatic cert management for names which
already have a cert (manually) loaded into the cache. These names no
longer need to be specified in the "skip_certificates" field of the
automatic HTTPS config, because they will be skipped automatically.
2019-08-09 12:05:47 -06:00
Dominik Braun 4950ce485f Part 1: Optimize using compiler's inliner (#2687)
* optimized functions for inlining

* added note regarding ResponseWriterWrapper

* optimzed browseWrite* methods for FileServer

* created benchmarks for comparison

* creating browseListing instance in each function

* created benchmarks for openResponseWriter

* removed benchmarks of old implementations

* implemented sync.Pool for byte buffers

* using global sync.Pool for writing JSON/HTML
2019-08-07 23:59:02 -06:00
Dreamacro c8b0a97b1c Add missing imports (#2688) 2019-07-24 01:28:33 -06:00
Johannes Hörmann 95a447de9c Tests for replacer (#2675)
* Tests for Replacer: Replacer.Set and Replacer.Delete

* update replacer test to new implementation

* fix replacer: counted position wrong if placeholder was found

* fix replacer: found placeholder again, if it was a non-existing one

* test with spaces between the placeholders as this could have a different behaviour

* Tests for Replacer.Map

* Tests for Replacer.Set: check also for something like {l{test1}
This should be replaced as {lTEST1REPLACEMENT

* fix replacer: fix multiple occurrence of phOpen sign

* Tests for Replacer: rewrite Set and ReplaceAll tests to use implementation not interface

* Tests for Replacer: rewrite Delete test to use implementation not interface

* Tests for Replacer: rewrite Map tests to use implementation not interface

* Tests for Replacer: add test for NewReplacer

* Tests for Replacer: add test for default replacements

* Tests for Replacer: fixed and refactored tests

* Tests for Replacer: moved default replacement tests to New-test
as new should return a replace with provider which defines global replacements
2019-07-21 09:57:34 -06:00
Toby Allen d98f2faef9 Add /stop endpoint to admin (#2671)
* Add stop command to admin.  Exit after stop.

* Return error on incorrect http Method and provide better logging.

* reuse stopAndCleanup function for all graceful stops
2019-07-20 10:48:46 -06:00
Toby Allen b855e66170 Force quit on Windows with taskkill /f (#2670)
* Force quit /f on windows, also check for processname '.exe' on windows.

* Remove unneeded spaces

* fix tabs

* go fmt tabs

* Return consistent appname which always includes .exe

* Change func name
2019-07-20 10:44:54 -06:00
Matthew Holt 0d3f99e85a cmd: Add print-env flag to run command 2019-07-18 10:58:31 -06:00
Matthew Holt 28df6cedfe tls: Use IANA-standard cipher suite names 2019-07-18 09:52:43 -06:00
Matthew Holt dd6aa91d72 Fix DNS provider module unmarshaling (closes #2676) 2019-07-18 09:15:23 -06:00
Matt Holt b44a22a9d4 Performance improvements to Replacer implementation (placeholders) (#2674)
Closes #2673
2019-07-16 12:27:11 -06:00
Matthew Holt bdf92ee84e Minor tweaks 2019-07-15 17:33:47 -06:00
Matthew Holt f217181293 mod: Use blackfriday's standard v2 module import path 2019-07-15 17:33:08 -06:00
Matthew Holt ccb5d19c25 Get module name at runtime, and tidy up modules 2019-07-12 10:15:27 -06:00
Matthew Holt b780f0f49b Standardize exit codes and improve shutdown handling; update gitignore 2019-07-12 10:07:11 -06:00
Matthew Holt 2141626269 Fix readme example for updated handler structure 2019-07-12 08:53:02 -06:00
Matthew Holt 63674ba081 Rename handler modules to use http.handlers namespace 2019-07-11 22:03:12 -06:00
Matthew Holt 9722dbe18a Fix rehandling bug 2019-07-11 22:02:47 -06:00
Matthew Holt 4698352b20 Merge branch 'v2-handlers' into v2
# Conflicts:
#	modules/caddyhttp/caddyhttp.go
#	modules/caddyhttp/fileserver/staticfiles.go
#	modules/caddyhttp/routes.go
#	modules/caddyhttp/server.go
#	modules/caddyhttp/staticresp.go
#	modules/caddyhttp/staticresp_test.go
2019-07-11 17:07:52 -06:00
Matthew Holt eb8625f774 Add error & subroute handlers; weakString; other minor handler changes 2019-07-11 17:02:57 -06:00
Matt Holt 9343403358 Flatten HTTP handler config (#2662) (#2663)
Differentiating middleware and responders has one benefit, namely that
it's clear which module provides the response, but even then it's not
a great advantage. Linear handler config makes a little more sense,
giving greater flexibility and simplifying the core a bit, even though
it's slightly awkward that handlers which are responders may not use
the 'next' handler that is passed in at all.
2019-07-11 15:32:34 -06:00
Matthew Holt 4a3a418156 Flatten HTTP handler config (#2662)
Differentiating middleware and responders has one benefit, namely that
it's clear which module provides the response, but even then it's not
a great advantage. Linear handler config makes a little more sense,
giving greater flexibility and simplifying the core a bit, even though
it's slightly awkward that handlers which are responders may not use
the 'next' handler that is passed in at all.
2019-07-09 12:58:39 -06:00
Matthew Holt 6dfba5fda8 Add path components to HTTP replacer 2019-07-08 16:46:55 -06:00
Matthew Holt d25008d2c8 Move listen address functions into caddy package; fix unix bug 2019-07-08 16:46:38 -06:00
Matthew Holt 4eb5fc541b Better error handling in CLI commands 2019-07-07 16:39:21 -06:00
Matthew Holt 42acdad9e5 Fix error handling with Validate when loading modules (fixes #2658)
The return statement was improperly nested in context.go
2019-07-07 14:12:22 -06:00
Matthew Holt 84f9f7cd60 Little cleanups 2019-07-05 13:59:30 -06:00
Matthew Holt 79216d356c acmemanager: Use storage module key "module" instead of "system" 2019-07-05 09:59:46 -06:00
Matthew Holt 9429c843c8 cmd: New reload command 2019-07-05 09:59:13 -06:00
Matthew Holt 6bcba91fbe Lowercase env var names in replacer 2019-07-03 15:42:21 -06:00
Matthew Holt ab101d75d0 Update readme docs 2019-07-03 14:50:59 -06:00
Matthew Holt 7512ea1a64 Change storage module key from "system" to "module" 2019-07-03 10:40:25 -06:00
Matthew Holt 902ec37062 Minor improvements to readme 2019-07-02 21:00:49 -06:00
Matthew Holt bed05f2450 Fix links in readme 2019-07-02 16:18:35 -06:00
Matthew Holt fdd871e177 go.mod: Append /v2 to module name; update all import paths
See https://github.com/golang/go/wiki/Modules#semantic-import-versioning
2019-07-02 12:37:06 -06:00
Matthew Holt 94c28a2574 Fix README typo, sigh... 2019-07-02 12:29:38 -06:00
Matthew Holt 42386a7272 Add menu and list of improvements to readme 2019-07-02 12:13:09 -06:00
Matthew Holt 5e858a15f7 Add a proper readme 2019-07-01 18:08:56 -06:00
Matthew Holt 533d1afb4b tls: Enable TLS 1.3 by default; set sane defaults on tls.Config structs 2019-07-01 11:47:46 -06:00
Matthew Holt 9f8d3611eb encode: Add "Vary" response header 2019-06-30 23:38:36 -06:00
Matthew Holt 3177ee8010 Add license 2019-06-30 16:07:58 -06:00
Matthew Holt 7a7c5f00c0 Add authors file 2019-06-30 16:06:24 -06:00
Matthew Holt fee0b38b48 Fix encoder name bug; remove unused field in encode middleware struct 2019-06-29 16:57:55 -06:00
Matthew Holt d5ae3a4966 httpserver: Set default Server header 2019-06-28 19:28:47 -06:00
Matthew Holt 31ab737bf2 Refactor code related to getting current version
And set version in CertMagic for User-Agent purposes
2019-06-28 19:28:28 -06:00
Matthew Holt a4bdf249db Caddy 2 gets a CLI! And admin endpoint is now configurable via JSON 2019-06-28 15:39:41 -06:00
Matthew Holt 006dc1792f Use html/template for escaping by default
Allow HTML only with a few specific functions
2019-06-27 13:30:41 -06:00
Matthew Holt a63cb3e3fd Implement etag; fix related bugs in encode and templates middlewares 2019-06-27 13:09:10 -06:00
Matthew Holt 2b22d2e6ea Optionally enforce strict TLS SNI + HTTP Host matching, & misc. cleanup
We should look into a way to enable this by default when TLS client auth
is configured for a server
2019-06-26 16:03:29 -06:00
Matthew Holt a524bcfe78 Enable skipping just certificate management for some auto HTTPS names 2019-06-26 10:57:18 -06:00
Matthew Holt 91b03dccb0 Refactor automatic HTTPS configuration; ability to skip certain names 2019-06-26 10:49:32 -06:00
Matthew Holt 6000855c82 Fix panics by disallowing explicitly-defined null modules in config 2019-06-26 10:45:34 -06:00
Matthew Holt 38677aaa58 caddytls: Support tags for manually-loaded certificates 2019-06-24 12:16:10 -06:00
Matthew Holt d49f762f6d Various bug fixes and minor improvements
- Fix static responder so it doesn't replace its own headers config,
  and instead replaces the actual response header values
- caddyhttp.ResponseRecorder type optionally buffers response
- Add interface guards to ensure regexp matchers get provisioned
- Use default HTTP port if one is not explicitly set
- Encode middleware writes status code 200 if not written upstream
- Templates and markdown only try to execute on text responses
- Static file server sets Content-Type based on file extension only
  (this whole thing -- MIME sniffing, etc -- needs more configurability)
2019-06-21 14:36:26 -06:00
Matthew Holt 81a9e125b5 Oops 2019-06-21 08:52:15 -06:00
Matthew Holt 70c788ce0c Minor cleanups/improvements 2019-06-21 08:08:26 -06:00
Matthew Holt 1c443beb9c caddyhttp: ResponseRecorder type for middlewares to buffer responses
Unfortunately, templates and markdown require buffering the full
response before it can be processed and written to the client
2019-06-20 21:49:45 -06:00
Matthew Holt 269b1e9aa3 tls: Improve (and fix) on-demand configuration 2019-06-20 20:36:29 -06:00
Matthew Holt 6d0350d04e caddyhttp: Fix host matching when host has a port 2019-06-20 20:24:46 -06:00
Matthew Holt 15647bdfb7 templates: Remove context functions implemented by sprig 2019-06-18 15:43:51 -06:00
Matthew Holt 2663dd176d Refactor templates execution; add sprig functions 2019-06-18 15:17:48 -06:00
Matthew Holt 6706c9225a Implement templates handler; various minor cleanups and bug fixes 2019-06-18 11:13:12 -06:00
Matthew Holt 5137859e47 Rename caddy2 -> caddy
Removes the version from the package name
2019-06-14 11:58:28 -06:00
Matthew Holt b8e7453fef Implement brotli encoder; improve validation of other encoders 2019-06-13 11:20:43 -06:00
Matthew Holt f93dab755b Update go modules 2019-06-13 10:55:25 -06:00
Matthew Holt 0c8763a728 Add simple tests for static responder 2019-06-11 17:46:11 -06:00
Matt Holt f5b4f268dc Implement encode middleware (#2)
* Implement encode middleware

* Add missing break; and add missing JSON struct field tag
2019-06-10 10:21:25 -06:00
Matthew Holt ef5f29cfb2 Do not allow Go standard lib to sniff Content-Type header 2019-06-07 19:59:25 -06:00
Matt Holt 8947ae0cc1 Merge pull request #1 from caddyserver/fix/goroutine-leak-healthchecker
fix goroutine leak in healthcheckers
2019-06-07 17:24:10 -06:00
dev 878ae0002a fix goroutine leak in healthcheckers 2019-06-07 15:52:10 -04:00
dev 37da91cfe7 fix module import paths and add cors to admin endpoints
fix go module refs and add cors to admin endpoints
2019-06-07 11:40:25 -04:00
Matthew Holt b79f86f256 Fix bugs related to auto HTTPS and alternate port configurations 2019-06-04 22:43:21 -06:00
Matthew Holt 613aecb898 Change import paths to GitHub package names 2019-06-04 13:52:37 -06:00
Matthew Holt 39db06d9c4 Implement IP/CIDR matcher and Not (negated) matcher 2019-06-04 13:42:54 -06:00
Matthew Holt f064889a4f Customize admin endpoint address with -listen flag
This is a temporary holdover for development purposes
2019-06-03 15:35:14 -06:00
Matthew Holt 3439933235 Implement session ticket keys; default STEK module with rotation 2019-05-29 23:11:46 -06:00
Matthew Holt 1b6b422c63 Add cleanup callbacks to context 2019-05-29 23:10:12 -06:00
Matthew Holt 2265db9028 Fix bug unmarshaling custom duration values 2019-05-29 23:09:51 -06:00
Matthew Holt bf54615efc ResponseMatcher for conditional logic of response headers 2019-05-28 18:53:08 -06:00
Matthew Holt da6a8cfc86 Minor cleanups 2019-05-28 18:52:21 -06:00
Matthew Holt 9cd6f35e9d Separate out certificate selection 2019-05-27 11:31:47 -06:00
Matthew Holt 210d0cf7f1 Implement custom cert selection policies; optimize matching for SNI 2019-05-24 13:18:45 -06:00
Matthew Holt 5a4a1421de Fix error handling and matching catch-all routes 2019-05-23 14:42:14 -06:00
Matthew Holt 34a25dd558 Add very simple markdown middleware for now 2019-05-23 14:41:43 -06:00
Matthew Holt 9e576c76e7 Add request_body middleware and some limits to HTTP servers 2019-05-23 13:16:34 -06:00
Matthew Holt c24a3e389f Change admin listener to :1234 for now; output message when listening 2019-05-22 19:10:29 -06:00
Matthew Holt f976451d19 Disallow unknown fields (strict unmarshal) when loading modules
This makes it faster and easier to detect broken configurations, but
is a slight performance hit on config loads since we have to re-encode
the decoded struct back into JSON without the module name's key
2019-05-22 14:32:12 -06:00
Matthew Holt 869fbac632 Don't use auto HTTPS for servers with only HTTP port listeners 2019-05-22 14:14:26 -06:00
Matthew Holt 284fb3a98c Allow multiple matcher sets in routes (OR'ed together)
Also export MatchRegexp in case other matcher modules find it useful.
Add comments to the exported matchers.
2019-05-22 13:13:39 -06:00
Matthew Holt bc00d840e8 Export types and fields necessary to build configs (for config adapters)
Also flag most fields with 'omitempty' for JSON marshaling
2019-05-22 12:32:36 -06:00
Matthew Holt be9b6e7b57 Honor the configured CA value 2019-05-21 14:22:33 -06:00
Matthew Holt 2fd98cb040 Module.New() does not need to return an error 2019-05-21 14:22:21 -06:00
Matthew Holt 67d32e6779 Fix up matchers tests and take care of TODO in rewrite 2019-05-21 13:10:14 -06:00
Matthew Holt 9d54f655aa Take care of remaining TODOs in the browse responder 2019-05-21 13:03:52 -06:00
Matthew Holt 65195a726d Implement rewrite middleware; fix middleware stack bugs 2019-05-20 23:48:43 -06:00
Matthew Holt b84cb05848 Fix deferred header ops 2019-05-20 22:00:54 -06:00
Matthew Holt a969872850 Default error handler; rename StaticFiles -> FileServer 2019-05-20 21:21:33 -06:00
Matthew Holt aaacab1bc3 Sanitize paths in static file server; some cleanup
Also remove AutomaticHTTPSError for now
2019-05-20 17:15:38 -06:00
Matthew Holt d22f64e6d4 Implement headers middleware 2019-05-20 15:46:52 -06:00
Matthew Holt 22995e5655 Implement most of browse; fix a couple obvious bugs; some cleanup 2019-05-20 15:46:52 -06:00
dev 043eb1d9e5 move internal packages to pkg folder and update reverse proxy
* set automatic https error type for cert-magic failures
* add state to onload and unload methods
* update reverse proxy to use Provision() and Cleanup()
2019-05-20 14:48:26 -04:00
Matthew Holt fec7fa8bfd Implement most of static file server; refactor and improve Replacer 2019-05-20 10:59:20 -06:00
Matthew Holt 1a20fe330e Improve godoc for contexts 2019-05-17 08:48:12 -06:00
Matthew Holt 1f0c061ce3 Architectural shift to using context for config and module state 2019-05-16 16:05:38 -06:00
Matthew Holt ff5b4639d5 Some minor updates, and get rid of OnLoad/OnUnload 2019-05-16 11:46:17 -06:00
Matthew Holt f9d93ead4e Rename and export some types, other minor changes 2019-05-14 14:14:05 -06:00
Matthew Holt 8ae0d6a509 caddyhttp: Implement better HTTP matchers including regexp; add tests 2019-05-10 21:07:02 -06:00
Matthew Holt 48b5a80320 Remove (unimplemented) enterprise TLS matchers 2019-05-07 11:58:58 -06:00
Matthew Holt ad3d408067 Add some tests and fix vet warning 2019-05-07 10:15:46 -06:00
Matthew Holt e40bbecb16 Rough implementation of auto HTTP->HTTPS redirects
Also added GracePeriod for server shutdowns
2019-05-07 09:56:18 -06:00
dev 8eba582efe Add go module files 2019-05-06 17:26:05 -04:00
Matthew Holt fbea3374e9 Add missing run.go (oops) 2019-05-06 12:43:04 -06:00
Matthew Holt 2eb3593327 Begin implementing HTTP replacer and static responder 2019-05-04 13:21:20 -06:00
Matthew Holt 1136e2cfee Add reverse proxy 2019-05-04 10:49:50 -06:00
Matthew Holt 5859cd8dad Instantiate apps that are needed but not explicitly configured 2019-04-29 09:22:00 -06:00
Matthew Holt 43961b542b General cleanup and more godocs 2019-04-26 12:35:39 -06:00
Matthew Holt 2d056fbe66 Initial commit of Storage, TLS, and automatic HTTPS implementations 2019-04-25 13:54:48 -06:00
Matthew Holt 545f28008e Begin implementing error handling and re-handling 2019-04-11 20:42:55 -06:00
dev d42529348f Updated proxy module import 2019-04-08 16:25:27 -04:00
dev 27ecc7f384 Protocol and Caddyscript matchers
* Added matcher to determine what protocol the request is being made by
  - grpc, tls, http
* Added ability to run caddyscript in a matcher to evaluate the http request
* Added TLS field to caddyscript request time
* Added a library to manipulate and compare a new caddyscript time type
* Library for regex in starlark
2019-04-08 09:58:11 -04:00
Matthew Holt 402f423693 Implement "global" state for modules, OnLoad and OnUnload callbacks
Tested for memory leaks and performance. Obviously the added locking and
global state is not awesome, but the alternative is a little uglier IMO:
we'd have to make some sort of "liaison" value which stores the state,
then pass it around to every module, and so LoadModule becomes a lot
less accessible, and each module would need to maintain a reference to
it... nope, just ugly. I think this is the cleaner solution: just make
sure only one Start() happens at a time, and keep global things global.

Very simple log middleware is an example.

Might need to reorder the operations in Start() and handle errors
differently, etc. Otherwise, I'm mostly happy with this solution...
2019-04-08 00:00:14 -06:00
Matthew Holt 3eae6d43b6 Add Validator interface
Modules can now verify their own configurations
2019-04-03 11:41:36 -06:00
Matthew Holt 59a5d0db28 Close listeners which are no longer used 2019-04-02 15:31:02 -06:00
Matt Holt f976aa7443 Merged in deadlines (pull request #1)
Cleanly fake-close listeners

* WIP debugging listener deadlines

* Fix listener deadlines
2019-04-02 20:58:24 +00:00
Matthew Holt 6621406fa8 Very basic middleware and route matching functionality 2019-03-31 20:41:29 -06:00
Matthew Holt 27ff6aeccb Fix goroutine leak in Run
D'oh, the servers' Shutdown() would never be called because they were
never added to the list of servers.

Thanks Danny for finding this.
2019-03-27 12:36:30 -06:00
Matthew Holt a8dc73b4d9 Performance testing Load function 2019-03-26 19:42:52 -06:00
Matthew Holt 86e2d1b0a4 Rudimentary start of HTTP servers 2019-03-26 15:45:51 -06:00
Matthew Holt 859b5d7ea3 Initial commit 2019-03-26 12:00:54 -06:00
462 changed files with 42157 additions and 43612 deletions
-21
View File
@@ -1,21 +0,0 @@
# shell scripts should not use tabs to indent!
*.bash text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.sh text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
# files for systemd (shell-similar)
*.path text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.service text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.timer text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
# go fmt will enforce this, but in case a user has not called "go fmt" allow GIT to catch this:
*.go text eol=lf core.whitespace whitespace=indent-with-non-tab,trailing-space,tabwidth=4
go.mod text eol=lf
go.sum text eol=lf
*.txt text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.tpl text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.htm text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.html text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.md text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.yml text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
.git* text eol=auto core.whitespace whitespace=trailing-space
+46 -22
View File
@@ -8,7 +8,7 @@ For starters, we invite you to join [the Caddy forum](https://caddy.community) w
## Common Tasks
- [Contributing code](#contributing-code)
- [Writing a plugin](#writing-a-plugin)
- [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)
@@ -17,21 +17,21 @@ For starters, we invite you to join [the Caddy forum](https://caddy.community) w
Other menu items:
- [Values](#values)
- [Responsible Disclosure](#responsible-disclosure)
- [Coordinated Disclosure](#coordinated-disclosure)
- [Thank You](#thank-you)
### Contributing code
You can have a direct impact on the project by helping with its code. To contribute code to Caddy, open a [pull request](https://github.com/caddyserver/caddy/pulls) (PR). If you're new to our community, that's okay: **we gladly welcome pull requests from anyone, regardless of your native language or coding experience.** You can get familiar with Caddy's code base by using [code search at Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy/-/search).
You can have a huge impact on the project by helping with its code. To contribute code to Caddy, 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. :wink: If your change is on the right track, we can guide you to make it mergable.
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:
- If your change is more than just a minor alteration, **open an issue to propose your change first.** This way we can avoid confusion, coordinate what everyone is working on, and ensure that changes are in-line with the project's goals and the best interests of its users. If there's already an issue about it, comment on the existing issue to claim it.
- **Open an issue to propose your change first.** This way we can avoid confusion, coordinate what everyone is working on, and ensure that any changes are in-line with the project's goals and the best interests of its users. We can also discuss the best possible implementation. If there's already an issue about it, comment on the existing issue to claim it.
- **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 DON'T do.](https://twitter.com/iamdevloper/status/397664295875805184)
- **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.
@@ -54,9 +54,9 @@ Contributing to Go projects on GitHub is fun and easy. We recommend the followin
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/caddy`.
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/you/caddy.git`
3. Tell git that it can push the caddyserver/caddy.git repo to your fork by adding a remote: `git remote add myfork https://github.com/<your-username>/caddy.git`
4. Make your changes in the caddyserver/caddy.git repo on your computer.
@@ -67,11 +67,11 @@ Contributing to Go projects on GitHub is fun and easy. We recommend the followin
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 plugin
### Writing a Caddy module
Caddy can do more with plugins! Anyone can write a plugin. Plugins are Go libraries that get compiled into Caddy, extending its feature set. They can add directives to the Caddyfile, change how the Caddyfile is loaded, and even implement new server types (e.g. HTTP, DNS). When it's ready, you can submit your plugin to the Caddy website so others can download it.
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 and submit a plugin](https://github.com/caddyserver/caddy/wiki) on the wiki. You should also share and discuss your plugin idea [on the forums](https://caddy.community) to have people test it out. We don't use the Caddy issue tracker for plugins.
[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
@@ -83,7 +83,7 @@ Many people on the forums could benefit from your experience and expertise, too.
### 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, not plugins.)
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.
@@ -93,19 +93,44 @@ We suggest reading [How to Report Bugs Effectively](http://www.chiark.greenend.o
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. You don't have to follow the bug template for feature requests. 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.
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 plugins](https://github.com/caddyserver/caddy/wiki), though, which can be made by anyone! But if a feature is not in the best interest of the Caddy project or its users in general, we may politely decline to implement it into Caddy core.
While we really do value your requests and implement many of them, not all features are a good fit for Caddy. Most of those [make good modules](#writing-a-caddy-module), which can be made by anyone! But if a feature is not in the best interest of the Caddy project or its users in general, we may politely decline to implement it into Caddy core.
### Improving documentation
Caddy's documentation is available at [https://caddyserver.com/docs](https://caddyserver.com/docs). If you would like to make a fix to the docs, please submit an issue here describing the change to make.
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 plugin documentation is not hosted by the Caddy website, other than basic usage examples. They are managed by the individual plugin authors, and you will have to contact them to change their documentation.
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.
@@ -128,9 +153,9 @@ Collaborators have push rights to the repository. We grant this permission after
- **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 vendors all dependencies with the help of [gvt](https://github.com/FiloSottile/gvt). All external dependencies must be vendored, and _Caddy must not export any types defined by those dependencies_. Check this diligently!
- **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 `caddy` and `caddytls` packages especially.
- **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.
@@ -149,12 +174,11 @@ Collaborators have push rights to the repository. We grant this permission after
- 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.
## Responsible Disclosure
## Security Policy
If you've found a security vulnerability, please email me, the author, directly: Matthew dot Holt at Gmail. I'll need enough information to verify the bug and make a patch. It will speed things up if you suggest a working patch. 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 me the name to use. Thanks for responsibly helping Caddy&mdash;and thousands of websites&mdash;be more secure!
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.
Thanks for your help! Caddy would not be what it is today without your contributions.
-83
View File
@@ -1,83 +0,0 @@
---
name: Bug report
about: For behaviors which violate documentation or cause incorrect results
title: ''
labels: ''
assignees: ''
---
<!--
This template is for bug reports. The lack of a feature is not a bug; to request a feature, please switch templates.
Are you asking for help with using Caddy? Please ask on our forum: https://caddy.community
Please do not skip relevant questions; this will slow down the debugging process and your issue may be closed.
-->
## 1. Which version of Caddy are you using (`caddy -version`)?
<!-- If there is no version information, please paste commit SHA instead. -->
## 2. What are you trying to do?
<!-- Please clearly describe what you are trying to do thoroughly enough so that a reader with no background information can repeat it. -->
## 3. What is your Caddyfile?
```text
paste entire Caddyfile here - DO NOT REDACT ANYTHING (except credentials)
```
<!-- Changing or hiding parts of your Caddyfile only slows things down and may result in your report being closed.
For more information, see https://caddy.community/t/how-to-get-help-with-caddy-more-effectively/5222 -->
<!-- If you are unable to post this publicly, we offer private support: https://caddyserver.com/products/support -->
## 4. How did you run Caddy (give the full command and describe the execution environment)?
<!-- IMPORTANT: Please eliminate Docker, systemd, reverse proxies, upstream dependencies, caches, firewalls, and other unnecessary, external factors from your setup first. This will help prove that this is a bug in Caddy and not a misconfiguration of your environment. We may close issues that are too complex to replicate. Thank you! -->
## 5. Please paste any relevant HTTP request(s) here.
<!-- Paste curl command, or full HTTP request including headers and body. You may skip this if the bug does not require HTTP requests. -->
## 6. What did you expect to see?
<!-- Describe your expected results as precisely as possible. -->
## 7. What did you see instead (give full error messages and/or log)?
<!-- Please run Caddy with the -log flag, and use the log and errors directives as needed. DO NOT REDACT INFORMATION except for credentials. See https://caddy.community/t/how-to-get-help-with-caddy-more-effectively/5222 -->
## 8. Why is this a bug, and how do you think this should be fixed?
<!-- Help us understand why it is a bug; it is not always obvious. You can help us get this resolved faster by thinking about the problem and describing possible solutions! -->
## 9. What are you doing to work around the problem in the meantime?
<!-- This can help others who encounter the same problem, until we implement a fix. -->
## 10. Please link to any related issues, pull requests, and/or discussion.
<!-- This can add crucial context to your report. -->
## Bonus: What do you use Caddy for? Why did you choose Caddy?
<!-- We'd like to know! -->
-35
View File
@@ -1,35 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature request
assignees: ''
---
<!--
This template is for feature requests. If you are reporting a bug instead, please switch templates.
Are you asking for help with using Caddy? Please ask on our forum: https://caddy.community
-->
## 1. What would you like to have changed?
<!-- Fully describe the feature or enhancement you are requesting; examples can be helpful too -->
## 2. Why is this feature a useful, necessary, and/or important addition to this project?
<!-- Please justify why this change adds value to the project, considering the added maintenance burden and complexity the change introduces -->
## 3. What alternatives are there, or what are you doing in the meantime to work around the lack of this feature?
<!-- We want to get an idea of what is being done in practice, or how other projects support your feature -->
## 4. Please link to any relevant issues, pull requests, or other discussions.
<!-- This adds crucial context to your feature request and can speed things up -->
-40
View File
@@ -1,40 +0,0 @@
---
name: Pull request
about: Propose changes to the code
title: ''
labels: ''
assignees: ''
---
<!--
Thank you for contributing to Caddy! Please fill this out to help us make the most of your pull request.
Was this change discussed in an issue first? That can help save time in case the change is not a good fit for the project. Not all pull requests get merged.
It is not uncommon for pull requests to go through several, iterative reviews. Please be patient with us! Every reviewer is a volunteer, and each has their own style.
-->
## 1. What does this change do, exactly?
<!-- Please be specific. Motivate the problem, and justify why this is the best solution. -->
## 2. Please link to the relevant issues.
<!-- This adds crucial context to your change. -->
## 3. Which documentation changes (if any) need to be made because of this PR?
<!-- Reviewers will often reference this first in order to know what to expect from the change. Please be specific enough so that they can paste your wording into the documentation directly. -->
## 4. Checklist
- [ ] I have written tests and verified that they fail without my change
- [ ] I have squashed any insignificant commits
- [ ] This change has comments explaining package types, values, functions, and non-obvious lines of code
- [ ] I am willing to help maintain this change if there are issues with it later
+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: (EOL 1 Oct 2020) |
| < 1.x | :x: |
## Reporting a Vulnerability
Please email Matt Holt (the author) directly: matt [at] ardanlabs [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: [ '1.14', '1.15' ]
# 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@v2
with:
go-version: ${{ matrix.go }}
- 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@v2
with:
path: ${{ steps.vars.outputs.go_cache }}
key: ${{ runner.os }}-${{ matrix.go }}-go-ci-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go }}-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_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
path: ${{ matrix.CADDY_BIN_PATH }}
# Commented bits below were useful to allow the job to continue
# 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@v2
with:
version: latest
args: check
env:
TAG: ${{ steps.vars.outputs.version_tag }}
+60
View File
@@ -0,0 +1,60 @@
name: Cross-Build
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
cross-build-test:
strategy:
fail-fast: false
matrix:
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
go: [ '1.14', '1.15' ]
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Print Go version and environment
id: vars
run: |
printf "Using go at: $(which go)\n"
printf "Go version: $(go version)\n"
printf "\n\nGo environment:\n\n"
go env
printf "\n\nSystem environment:\n\n"
env
echo "::set-output name=go_cache::$(go env GOCACHE)"
- name: Cache the build cache
uses: actions/cache@v2
with:
path: ${{ steps.vars.outputs.go_cache }}
key: cross-build-go${{ matrix.go }}-${{ matrix.goos }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
cross-build-go${{ matrix.go }}-${{ matrix.goos }}
- name: Checkout code into the Go module directory
uses: actions/checkout@v2
- name: Run Build
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
shell: bash
continue-on-error: true
working-directory: ./cmd/caddy
run: |
GOOS=$GOOS go build -trimpath -o caddy-"$GOOS"-amd64 2> /dev/null
if [ $? -ne 0 ]; then
echo "::warning ::$GOOS Build Failed"
exit 0
fi
+73
View File
@@ -0,0 +1,73 @@
name: Fuzzing
on:
# Daily midnight fuzzing
schedule:
- cron: '0 0 * * *'
jobs:
fuzzing:
name: Fuzzing
strategy:
matrix:
os: [ ubuntu-latest ]
go: [ '1.14' ]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Checkout code
uses: actions/checkout@v2
- name: Download go-fuzz tools and the Fuzzit CLI, move Fuzzit CLI to GOBIN
# If we decide we need to prevent this from running on forks, we can use this line:
# if: github.repository == 'caddyserver/caddy'
run: |
go get -v github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
wget -q -O fuzzit https://github.com/fuzzitdev/fuzzit/releases/download/v2.4.77/fuzzit_Linux_x86_64
chmod a+x fuzzit
mv fuzzit $(go env GOPATH)/bin
echo "::add-path::$(go env GOPATH)/bin"
- name: Generate fuzzers & submit them to Fuzzit
continue-on-error: true
env:
FUZZIT_API_KEY: ${{ secrets.FUZZIT_API_KEY }}
SYSTEM_PULLREQUEST_SOURCEBRANCH: ${{ github.ref }}
BUILD_SOURCEVERSION: ${{ github.sha }}
run: |
# debug
echo "PR Source Branch: $SYSTEM_PULLREQUEST_SOURCEBRANCH"
echo "Source version: $BUILD_SOURCEVERSION"
declare -A fuzzers_funcs=(\
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="FuzzParseAddress" \
["./listeners_fuzz.go"]="FuzzParseNetworkAddress" \
["./replacer_fuzz.go"]="FuzzReplacer" \
)
declare -A fuzzers_targets=(\
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="parse-address" \
["./listeners_fuzz.go"]="parse-network-address" \
["./replacer_fuzz.go"]="replacer" \
)
fuzz_type="fuzzing"
for f in $(find . -name \*_fuzz.go); do
FUZZER_DIRECTORY=$(dirname "$f")
echo "go-fuzz-build func ${fuzzers_funcs[$f]} residing in $f"
go-fuzz-build -func "${fuzzers_funcs[$f]}" -o "$FUZZER_DIRECTORY/${fuzzers_targets[$f]}.zip" "$FUZZER_DIRECTORY"
fuzzit create job --engine go-fuzz caddyserver/"${fuzzers_targets[$f]}" "$FUZZER_DIRECTORY"/"${fuzzers_targets[$f]}.zip" --api-key "${FUZZIT_API_KEY}" --type "${fuzz_type}" --branch "${SYSTEM_PULLREQUEST_SOURCEBRANCH}" --revision "${BUILD_SOURCEVERSION}"
echo "Completed $f"
done
+83
View File
@@ -0,0 +1,83 @@
name: Release
on:
push:
tags:
- 'v*.*.*'
jobs:
release:
name: Release
strategy:
matrix:
os: [ ubuntu-latest ]
go: [ '1.15' ]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Checkout code
uses: actions/checkout@v2
# 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@v2
with:
path: ${{ steps.vars.outputs.go_cache }}
key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go${{ matrix.go }}-release
# GoReleaser will take care of publishing those artifacts into the release
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
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 }}"}'
+17 -16
View File
@@ -1,22 +1,23 @@
.DS_Store
Thumbs.db
_gitignore/
Vagrantfile
.vagrant/
/.idea
dist/builds/
dist/release/
error.log
access.log
/*.conf
*.log
Caddyfile
!caddyfile/
og_static/
# artifacts from pprof tooling
*.prof
*.test
.vscode/
# build artifacts
cmd/caddy/caddy
cmd/caddy/caddy.exe
*.bat
# mac specific
.DS_Store
# go modules
vendor
# goreleaser artifacts
dist
caddy-build
caddy-dist
+51
View File
@@ -0,0 +1,51 @@
linters-settings:
errcheck:
ignore: fmt:.*,io/ioutil:^Read.*,github.com/caddyserver/caddy/v2/caddyconfig:RegisterAdapter,github.com/caddyserver/caddy/v2:RegisterModule
ignoretests: true
misspell:
locale: US
linters:
enable:
- bodyclose
- prealloc
- unconvert
- errcheck
- gofmt
- goimports
- gosec
- ineffassign
- misspell
run:
# default concurrency is a available CPU number.
# concurrency: 4 # explicitly omit this value to fully utilize available resources.
deadline: 5m
issues-exit-code: 1
tests: false
# output configuration options
output:
format: 'colored-line-number'
print-issued-lines: true
print-linter-name: true
issues:
exclude-rules:
# we aren't calling unknown URL
- text: "G107" # G107: Url provided to HTTP request as taint input
linters:
- gosec
# as a web server that's expected to handle any template, this is totally in the hands of the user.
- text: "G203" # G203: Use of unescaped data in HTML templates
linters:
- gosec
# we're shelling out to known commands, not relying on user-defined input.
- text: "G204" # G204: Audit use of command execution
linters:
- gosec
# the choice of weakrand is deliberate, hence the named import "weakrand"
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go
text: "G404" # G404: Insecure random number source (rand)
linters:
- gosec
+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
+10
View File
@@ -0,0 +1,10 @@
# This is the official list of Caddy Authors for copyright purposes.
# Authors may be either individual people or legal entities.
#
# Not all individual contributors are authors. For the full list of
# contributors, refer to the project's page on GitHub or the repo's
# commit history.
Matthew Holt <Matthew.Holt@gmail.com>
Light Code Labs <sales@lightcodelabs.com>
Ardan Labs <info@ardanlabs.com>
+3 -2
View File
@@ -1,3 +1,4 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@@ -178,7 +179,7 @@
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
@@ -186,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+98 -172
View File
@@ -1,225 +1,151 @@
<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 <!-- Serve Confidently --></h3>
<p align="center">Caddy is a general-purpose HTTP/2 web server that serves HTTPS by default.</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?definitionId=5"><img src="https://img.shields.io/azure-devops/build/mholt-dev/afec6074-9842-457f-98cf-69df6adbbf2e/5/master.svg?label=cross-platform%20tests"></a>
<a href="https://godoc.org/github.com/caddyserver/caddy"><img src="https://img.shields.io/badge/godoc-reference-blue.svg"></a>
<a href="https://goreportcard.com/report/caddyserver/caddy"><img src="https://goreportcard.com/badge/github.com/caddyserver/caddy"></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://caddyserver.com/download">Download</a> ·
<a href="https://caddyserver.com/docs">Documentation</a> ·
<a href="https://caddy.community">Community</a>
<a href="https://github.com/caddyserver/caddy/releases">Releases</a> ·
<a href="https://caddyserver.com/docs/">Documentation</a> ·
<a href="https://caddy.community">Get Help</a>
</p>
---
Caddy is a **production-ready** open-source web server that is fast, easy to use, and makes you more productive.
Available for Windows, Mac, Linux, BSD, Solaris, and [Android](https://github.com/caddyserver/caddy/wiki/Running-Caddy-on-Android).
<p align="center">
<b>Thanks to our special sponsor:</b>
<br><br>
<a href="https://relicabackup.com"><img src="https://caddyserver.com/resources/images/sponsors/relica.png" width="220" alt="Relica - Cross-platform file backup to the cloud, local disks, or other computers"></a>
</p>
## Menu
### Menu
- [Features](#features)
- [Install](#install)
- [Quick Start](#quick-start)
- [Running in Production](#running-in-production)
- [Contributing](#contributing)
- [Donors](#donors)
- [About the Project](#about-the-project)
## Features
- **Easy configuration** with the Caddyfile
- **Automatic HTTPS** on by default (via [Let's Encrypt](https://letsencrypt.org))
- **HTTP/2** by default
- **Virtual hosting** so multiple sites just work
- Experimental **QUIC support** for cutting-edge transmissions
- TLS session ticket **key rotation** for more secure connections
- **Extensible with plugins** because a convenient web server is a helpful one
- **Runs anywhere** with **no external dependencies** (not even libc)
[See a more complete list of features built into Caddy.](https://caddyserver.com/features) On top of all those, Caddy does even more with plugins: choose which plugins you want at [download](https://caddyserver.com/download).
Altogether, Caddy can do things other web servers simply cannot do. Its features and plugins save you time and mistakes, and will cheer you up. Your Caddy instance takes care of the details for you!
- [Build from source](#build-from-source)
- [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)
- [About](#about)
<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>
## Install
## Features
Caddy binaries have no dependencies and are available for every platform. Get Caddy any of these ways:
- **[Download page](https://caddyserver.com/download)** (RECOMMENDED) allows you to customize your build in the browser
- **[Latest release](https://github.com/caddyserver/caddy/releases/latest)** for pre-built, vanilla binaries
- **[AWS Marketplace](https://aws.amazon.com/marketplace/pp/B07J1WNK75?qid=1539015041932&sr=0-1&ref_=srh_res_product_title&cl_spe=C)** makes it easy to deploy directly to your cloud environment. <a href="https://aws.amazon.com/marketplace/pp/B07J1WNK75?qid=1539015041932&sr=0-1&ref_=srh_res_product_title&cl_spe=C" target="_blank">
<img src="https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png" alt="Get Caddy on the AWS Marketplace" height="25"/></a>
- **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
To build from source you need **[Git](https://git-scm.com/downloads)** and **[Go](https://golang.org/doc/install)** (1.13 or newer).
## Build from source
**To build Caddy without plugins:**
Requirements:
<!-- TODO: This env variable will not be required starting with Go 1.13 -->
1. Set the transitional environment variable for Go modules: `export GO111MODULE=on`
2. Run `go get github.com/caddyserver/caddy/caddy`
- [Go 1.14 or newer](https://golang.org/dl/)
Caddy will be installed to your `$GOPATH/bin` folder.
With these instructions, the binary will not have embedded version information (see [golang/go#29228](https://github.com/golang/go/issues/29228)), but it is fine for a quick start.
**To build Caddy with plugins (and with version information):**
There is no need to modify the Caddy code to build it with plugins. We will create a simple Go module with our own `main()` that you can use to make custom Caddy builds.
<!-- TODO: This env variable will not be required starting with Go 1.13 -->
1. Set the transitional environment variable for Go modules: `export GO111MODULE=on`
2. Create a new folder anywhere and within create a Go file (with an extension of `.go`, such as `main.go`) with the contents below, adjusting to import the plugins you want to include:
```go
package main
import (
"github.com/caddyserver/caddy/caddy/caddymain"
// plug in plugins here, for example:
// _ "import/path/here"
)
func main() {
// optional: disable telemetry
// caddymain.EnableTelemetry = false
caddymain.Run()
}
```
3. `go mod init caddy`
4. Run `go get github.com/caddyserver/caddy`
5. `go install` will then create your binary at `$GOPATH/bin`, or `go build` will put it in the current directory.
**To install Caddy's source code for development:**
<!-- TODO: This env variable will not be required starting with Go 1.13 -->
1. Set the transitional environment variable for Go modules: `export GO111MODULE=on`
2. Run `git clone https://github.com/caddyserver/caddy.git` in any folder (doesn't have to be in GOPATH).
You can make changes to the source code from that clone and checkout any commit or tag you wish to develop on.
When building from source, telemetry is enabled by default. You can disable it by changing `caddymain.EnableTelemetry = false` in run.go, or use the `-disabled-metrics` flag at runtime to disable only certain metrics.
## Quick Start
To serve static files from the current working directory, run:
```
caddy
### For development
```bash
$ git clone "https://github.com/caddyserver/caddy.git"
$ cd caddy/cmd/caddy/
$ go build
```
Caddy's default port is 2015, so open your browser to [http://localhost:2015](http://localhost:2015).
_**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions below._
### Go from 0 to HTTPS in 5 seconds
### With version information and/or plugins
If the `caddy` binary has permission to bind to low ports and your domain name's DNS records point to the machine you're on:
Using [our builder tool, `xcaddy`](https://github.com/caddyserver/xcaddy)...
```
caddy -host example.com
$ xcaddy build
```
This command serves static files from the current directory over HTTPS. Certificates are automatically obtained and renewed for you! Caddy is also automatically configuring ports 80 and 443 for you, and redirecting HTTP to HTTPS. Cool, huh?
...the following steps are automated:
### Customizing your site
To customize how your site is served, create a file named Caddyfile by your site and paste this into it:
```plain
localhost
push
browse
websocket /echo cat
ext .html
log /var/log/access.log
proxy /api 127.0.0.1:7005
header /api Access-Control-Allow-Origin *
```
When you run `caddy` in that directory, it will automatically find and use that Caddyfile.
This simple file enables server push (via Link headers), allows directory browsing (for folders without an index file), hosts a WebSocket echo server at /echo, serves clean URLs, logs requests to an access log, proxies all API requests to a backend on port 7005, and adds the coveted `Access-Control-Allow-Origin: *` header for all responses from the API.
Wow! Caddy can do a lot with just a few lines.
### Doing more with Caddy
To host multiple sites and do more with the Caddyfile, please see the [Caddyfile tutorial](https://caddyserver.com/tutorial/caddyfile).
Sites with qualifying hostnames are served over [HTTPS by default](https://caddyserver.com/docs/automatic-https).
Caddy has a nice little command line interface. Run `caddy -h` to view basic help or see the [CLI documentation](https://caddyserver.com/docs/cli) for details.
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/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`
## Running in Production
Caddy is production-ready if you find it to be a good fit for your site and workflow.
**Running as root:** We advise against this. You can still listen on ports < 1024 on Linux using setcap like so: `sudo setcap cap_net_bind_service=+ep ./caddy`
The Caddy project does not officially maintain any system-specific integrations nor suggest how to administer your own system. But your download file includes [unofficial resources](https://github.com/caddyserver/caddy/tree/master/dist/init) contributed by the community that you may find helpful for running Caddy in production.
How you choose to run Caddy is up to you. Many users are satisfied with `nohup caddy &`. Others use `screen`. Users who need Caddy to come back up after reboots either do so in the script that caused the reboot, add a command to an init script, or configure a service with their OS.
If you have questions or concerns about Caddy' underlying crypto implementations, consult Go's [crypto packages](https://golang.org/pkg/crypto), starting with their documentation, then issues, then the code itself; as Caddy uses mainly those libraries.
## Contributing
## Quick start
**[Join our forum](https://caddy.community) where you can chat with other Caddy users and developers!** To get familiar with the code base, try [Caddy code search on Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy/)!
The [Caddy website](https://caddyserver.com/docs/) has documentation that includes tutorials, quick-start guides, reference, and more.
Please see our [contributing guidelines](https://github.com/caddyserver/caddy/blob/master/.github/CONTRIBUTING.md) for instructions. If you want to write a plugin, check out the [developer wiki](https://github.com/caddyserver/caddy/wiki).
**We recommend that all users do our [Getting Started](https://caddyserver.com/docs/getting-started) guide to become familiar with using Caddy.**
We use GitHub issues and pull requests only for discussing bug reports and the development of specific changes. We welcome all other topics on the [forum](https://caddy.community)!
If you want to contribute to the documentation, please [submit an issue](https://github.com/caddyserver/caddy/issues/new) describing the change that should be made.
### Good First Issue
If you are looking for somewhere to start and would like to help out by working on an existing issue, take a look at our [`Good First Issue`](https://github.com/caddyserver/caddy/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) tag
Thanks for making Caddy -- and the Web -- better!
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. 🙂
## Donors
- [DigitalOcean](https://m.do.co/c/6d7bdafccf96) is hosting the Caddy project.
- [DNSimple](https://dnsimple.link/resolving-caddy) provides DNS services for Caddy's sites.
- [DNS Spy](https://dnsspy.io) keeps an eye on Caddy's DNS properties.
We thank them for their services. **If you want to help keep Caddy free, please [become a sponsor](https://caddyserver.com/pricing)!**
## About the Project
## Overview
Caddy was born out of the need for a "batteries-included" web server that runs anywhere and doesn't have to take its configuration with it. Caddy took inspiration from [spark](https://github.com/rif/spark), [nginx](https://github.com/nginx/nginx), lighttpd,
[Websocketd](https://github.com/joewalnes/websocketd) and [Vagrant](https://www.vagrantup.com/), which provides a pleasant mixture of features from each of them.
Caddy is most often used as an HTTPS server, but it is suitable for any long-running Go program. First and foremost, it is a platform to run Go applications. Caddy "apps" are just Go programs that are implemented as Caddy modules. Two apps -- `tls` and `http` -- ship standard with Caddy.
**The name "Caddy" is trademarked:** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". See [brand guidelines](https://caddyserver.com/brand). Caddy is a registered trademark of Light Code Labs, LLC.
Caddy apps instantly benefit from [automated documentation](https://caddyserver.com/docs/json/), graceful on-line [config changes via API](https://caddyserver.com/docs/api), and unification with other Caddy apps.
*Author on Twitter: [@mholt6](https://twitter.com/mholt6)*
Although [JSON](https://caddyserver.com/docs/json/) is Caddy's native config language, Caddy can accept input from [config adapters](https://caddyserver.com/docs/config-adapters) which can essentially convert any config format of your choice into JSON: Caddyfile, JSON 5, YAML, TOML, NGINX config, and more.
The primary way to configure Caddy is through [its API](https://caddyserver.com/docs/api), but if you prefer config files, the [command-line interface](https://caddyserver.com/docs/command-line) supports those too.
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 [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.
## Full documentation
Our website has complete documentation:
**https://caddyserver.com/docs/**
The docs are also open source. You can contribute to them here: https://github.com/caddyserver/website
## Getting help
- We **strongly recommend** that all professionals or companies using Caddy get a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed.
- Individuals can exchange help for free on our community forum at https://caddy.community. Remember that people give help out of their spare time and good will. The best way to get help is to give it first!
Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only for bug reports and feature requests, i.e. actionable development items (support questions will usually be referred to the forums).
## About
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Light Code Labs, LLC.
- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_
- _Author on Twitter: [@mholt6](https://twitter.com/mholt6)_
+865
View File
@@ -0,0 +1,865 @@
// 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 (
"bytes"
"context"
"encoding/json"
"errors"
"expvar"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/pprof"
"net/url"
"os"
"path"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.uber.org/zap"
)
// TODO: is there a way to make the admin endpoint so that it can be plugged into the HTTP app? see issue #2833
// AdminConfig configures Caddy's API endpoint, which is used
// to manage Caddy while it is running.
type AdminConfig struct {
// If true, the admin endpoint will be completely disabled.
// Note that this makes any runtime changes to the config
// impossible, since the interface to do so is through the
// admin endpoint.
Disabled bool `json:"disabled,omitempty"`
// The address to which the admin endpoint's listener should
// bind itself. Can be any single network address that can be
// parsed by Caddy. Default: localhost:2019
Listen string `json:"listen,omitempty"`
// If true, CORS headers will be emitted, and requests to the
// API will be rejected if their `Host` and `Origin` headers
// do not match the expected value(s). Use `origins` to
// customize which origins/hosts are allowed.If `origins` is
// not set, the listen address is the only value allowed by
// default.
EnforceOrigin bool `json:"enforce_origin,omitempty"`
// The list of allowed origins/hosts for API requests. Only needed
// if accessing the admin endpoint from a host different from the
// socket's network interface or if `enforce_origin` is true. If not
// set, the listener address will be the default value. If set but
// empty, no origins will be allowed.
Origins []string `json:"origins,omitempty"`
// Options related to configuration management.
Config *ConfigSettings `json:"config,omitempty"`
}
// ConfigSettings configures the, uh, configuration... and
// management thereof.
type ConfigSettings struct {
// Whether to keep a copy of the active config on disk. Default is true.
Persist *bool `json:"persist,omitempty"`
}
// listenAddr extracts a singular listen address from ac.Listen,
// returning the network and the address of the listener.
func (admin AdminConfig) listenAddr() (NetworkAddress, error) {
input := admin.Listen
if input == "" {
input = DefaultAdminListen
}
listenAddr, err := ParseNetworkAddress(input)
if err != nil {
return NetworkAddress{}, fmt.Errorf("parsing admin listener address: %v", err)
}
if listenAddr.PortRangeSize() != 1 {
return NetworkAddress{}, fmt.Errorf("admin endpoint must have exactly one address; cannot listen on %v", listenAddr)
}
return listenAddr, nil
}
// newAdminHandler reads admin's config and returns an http.Handler suitable
// for use in an admin endpoint server, which will be listening on listenAddr.
func (admin AdminConfig) newAdminHandler(addr NetworkAddress) adminHandler {
muxWrap := adminHandler{
enforceOrigin: admin.EnforceOrigin,
enforceHost: !addr.isWildcardInterface(),
allowedOrigins: admin.allowedOrigins(addr),
mux: http.NewServeMux(),
}
addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
labels := prometheus.Labels{"path": pattern, "handler": handlerLabel}
h = promhttp.InstrumentHandlerCounter(
adminMetrics.requestCount.MustCurryWith(labels),
h,
)
muxWrap.mux.Handle(pattern, h)
}
// addRoute just calls muxWrap.mux.Handle after
// wrapping the handler with error handling
addRoute := func(pattern string, handlerLabel string, h AdminHandler) {
wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := h.ServeHTTP(w, r)
if err != nil {
labels := prometheus.Labels{
"path": pattern,
"handler": handlerLabel,
"method": r.Method,
}
adminMetrics.requestErrors.With(labels).Inc()
}
muxWrap.handleError(w, r, err)
})
addRouteWithMetrics(pattern, handlerLabel, wrapper)
}
const handlerLabel = "admin"
// register standard config control endpoints
addRoute("/"+rawConfigKey+"/", handlerLabel, AdminHandlerFunc(handleConfig))
addRoute("/id/", handlerLabel, AdminHandlerFunc(handleConfigID))
addRoute("/stop", handlerLabel, AdminHandlerFunc(handleStop))
// register debugging endpoints
addRouteWithMetrics("/debug/pprof/", handlerLabel, http.HandlerFunc(pprof.Index))
addRouteWithMetrics("/debug/pprof/cmdline", handlerLabel, http.HandlerFunc(pprof.Cmdline))
addRouteWithMetrics("/debug/pprof/profile", handlerLabel, http.HandlerFunc(pprof.Profile))
addRouteWithMetrics("/debug/pprof/symbol", handlerLabel, http.HandlerFunc(pprof.Symbol))
addRouteWithMetrics("/debug/pprof/trace", handlerLabel, http.HandlerFunc(pprof.Trace))
addRouteWithMetrics("/debug/vars", handlerLabel, expvar.Handler())
// register third-party module endpoints
for _, m := range GetModules("admin.api") {
router := m.New().(AdminRouter)
handlerLabel := m.ID.Name()
for _, route := range router.Routes() {
addRoute(route.Pattern, handlerLabel, route.Handler)
}
}
return muxWrap
}
// allowedOrigins returns a list of origins that are allowed.
// 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 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 {
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{}{}
}
}
allowed := make([]string, 0, len(uniqueOrigins))
for origin := range uniqueOrigins {
allowed = append(allowed, origin)
}
return allowed
}
// replaceAdmin replaces the running admin server according
// to the relevant configuration in cfg. If no configuration
// for the admin endpoint exists in cfg, a default one is
// used, so that there is always an admin server (unless it
// is explicitly configured to be disabled).
func replaceAdmin(cfg *Config) error {
// always be sure to close down the old admin endpoint
// as gracefully as possible, even if the new one is
// disabled -- careful to use reference to the current
// (old) admin endpoint since it will be different
// when the function returns
oldAdminServer := adminServer
defer func() {
// do the shutdown asynchronously so that any
// current API request gets a response; this
// goroutine may last a few seconds
if oldAdminServer != nil {
go func(oldAdminServer *http.Server) {
err := stopAdminServer(oldAdminServer)
if err != nil {
Log().Named("admin").Error("stopping current admin endpoint", zap.Error(err))
}
}(oldAdminServer)
}
}()
// always get a valid admin config
adminConfig := DefaultAdminConfig
if cfg != nil && cfg.Admin != nil {
adminConfig = cfg.Admin
}
// if new admin endpoint is to be disabled, we're done
if adminConfig.Disabled {
Log().Named("admin").Warn("admin endpoint disabled")
return nil
}
// extract a singular listener address
addr, err := adminConfig.listenAddr()
if err != nil {
return err
}
handler := adminConfig.newAdminHandler(addr)
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
if err != nil {
return err
}
adminServer = &http.Server{
Handler: handler,
ReadTimeout: 10 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1024 * 64,
}
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))
}
}()
adminLogger.Info("admin endpoint started",
zap.String("address", addr.String()),
zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
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
}
func stopAdminServer(srv *http.Server) error {
if srv == nil {
return fmt.Errorf("no admin server")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := srv.Shutdown(ctx)
if err != nil {
return fmt.Errorf("shutting down admin server: %v", err)
}
Log().Named("admin").Info("stopped previous server")
return nil
}
// AdminRouter is a type which can return routes for the admin API.
type AdminRouter interface {
Routes() []AdminRoute
}
// AdminRoute represents a route for the admin endpoint.
type AdminRoute struct {
Pattern string
Handler AdminHandler
}
type adminHandler struct {
enforceOrigin bool
enforceHost bool
allowedOrigins []string
mux *http.ServeMux
}
// ServeHTTP is the external entry point for API requests.
// It will only be called once per request.
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Log().Named("admin.api").Info("received request",
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),
)
h.serveHTTP(w, r)
}
// serveHTTP is the internal entry point for API requests. It may
// 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 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 {
h.handleError(w, r, err)
return
}
if r.Method == http.MethodOptions {
w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Cache-Control")
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
w.Header().Set("Access-Control-Allow-Origin", origin)
}
// TODO: authentication & authorization, if configured
h.mux.ServeHTTP(w, r)
}
func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err error) {
if err == nil {
return
}
if err == ErrInternalRedir {
h.serveHTTP(w, r)
return
}
apiErr, ok := err.(APIError)
if !ok {
apiErr = APIError{
Code: http.StatusInternalServerError,
Err: err,
}
}
if apiErr.Code == 0 {
apiErr.Code = http.StatusInternalServerError
}
if apiErr.Message == "" && apiErr.Err != nil {
apiErr.Message = apiErr.Err.Error()
}
Log().Named("admin.api").Error("request error",
zap.Error(err),
zap.Int("status_code", apiErr.Code),
)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(apiErr.Code)
json.NewEncoder(w).Encode(apiErr)
}
// checkHost returns a handler that wraps next such that
// it will only be called if the request's Host header matches
// a trustworthy/expected value. This helps to mitigate DNS
// rebinding attacks.
func (h adminHandler) checkHost(r *http.Request) error {
var allowed bool
for _, allowedHost := range h.allowedOrigins {
if r.Host == allowedHost {
allowed = true
break
}
}
if !allowed {
return APIError{
Code: http.StatusForbidden,
Err: fmt.Errorf("host not allowed: %s", r.Host),
}
}
return nil
}
// checkOrigin ensures that the Origin header, if
// set, matches the intended target; prevents arbitrary
// sites from issuing requests to our listener. It
// returns the origin that was obtained from r.
func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
origin := h.getOriginHost(r)
if origin == "" {
return origin, APIError{
Code: http.StatusForbidden,
Err: fmt.Errorf("missing required Origin header"),
}
}
if !h.originAllowed(origin) {
return origin, APIError{
Code: http.StatusForbidden,
Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
}
}
return origin, nil
}
func (h adminHandler) getOriginHost(r *http.Request) string {
origin := r.Header.Get("Origin")
if origin == "" {
origin = r.Header.Get("Referer")
}
originURL, err := url.Parse(origin)
if err == nil && originURL.Host != "" {
origin = originURL.Host
}
return origin
}
func (h adminHandler) originAllowed(origin string) bool {
for _, allowedOrigin := range h.allowedOrigins {
originCopy := origin
if !strings.Contains(allowedOrigin, "://") {
// no scheme specified, so allow both
originCopy = strings.TrimPrefix(originCopy, "http://")
originCopy = strings.TrimPrefix(originCopy, "https://")
}
if originCopy == allowedOrigin {
return true
}
}
return false
}
func handleConfig(w http.ResponseWriter, r *http.Request) error {
switch r.Method {
case http.MethodGet:
w.Header().Set("Content-Type", "application/json")
err := readConfig(r.URL.Path, w)
if err != nil {
return APIError{Code: http.StatusBadRequest, Err: err}
}
return nil
case http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete:
// DELETE does not use a body, but the others do
var body []byte
if r.Method != http.MethodDelete {
if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
return APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
}
}
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()
}
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
err := changeConfig(r.Method, r.URL.Path, body, forceReload)
if err != nil {
return err
}
default:
return APIError{
Code: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method %s not allowed", r.Method),
}
}
return nil
}
func handleConfigID(w http.ResponseWriter, r *http.Request) error {
idPath := r.URL.Path
parts := strings.Split(idPath, "/")
if len(parts) < 3 || parts[2] == "" {
return fmt.Errorf("request path is missing object ID")
}
if parts[0] != "" || parts[1] != "id" {
return fmt.Errorf("malformed object path")
}
id := parts[2]
// map the ID to the expanded path
currentCfgMu.RLock()
expanded, ok := rawCfgIndex[id]
defer currentCfgMu.RUnlock()
if !ok {
return fmt.Errorf("unknown object ID '%s'", id)
}
// piece the full URL path back together
parts = append([]string{expanded}, parts[3:]...)
r.URL.Path = path.Join(parts...)
return ErrInternalRedir
}
func handleStop(w http.ResponseWriter, r *http.Request) error {
err := handleUnload(w, r)
if err != nil {
Log().Named("admin.api").Error("unload error", zap.Error(err))
}
if adminServer != nil {
// use goroutine so that we can finish responding to API request
go func() {
err := stopAdminServer(adminServer)
var exitCode int
if err != nil {
exitCode = ExitCodeFailedQuit
Log().Named("admin.api").Error("failed to stop admin server gracefully", zap.Error(err))
}
Log().Named("admin.api").Info("stopping now, bye!! 👋")
os.Exit(exitCode)
}()
}
return nil
}
// handleUnload stops the current configuration that is running.
// Note that doing this can also be accomplished with DELETE /config/
// but we leave this function because handleStop uses it.
func handleUnload(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodPost {
return APIError{
Code: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed"),
}
}
Log().Named("admin.api").Info("unloading")
if err := stopAndCleanup(); err != nil {
Log().Named("admin.api").Error("error unloading", zap.Error(err))
} else {
Log().Named("admin.api").Info("unloading completed")
}
return nil
}
// unsyncedConfigAccess traverses into the current config and performs
// the operation at path according to method, using body and out as
// needed. This is a low-level, unsynchronized function; most callers
// will want to use changeConfig or readConfig instead. This requires a
// read or write lock on currentCfgMu, depending on method (GET needs
// only a read lock; all others need a write lock).
func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error {
var err error
var val interface{}
// if there is a request body, decode it into the
// variable that will be set in the config according
// to method and path
if len(body) > 0 {
err = json.Unmarshal(body, &val)
if err != nil {
return fmt.Errorf("decoding request body: %v", err)
}
}
enc := json.NewEncoder(out)
cleanPath := strings.Trim(path, "/")
if cleanPath == "" {
return fmt.Errorf("no traversable path")
}
parts := strings.Split(cleanPath, "/")
if len(parts) == 0 {
return fmt.Errorf("path missing")
}
// A path that ends with "..." implies:
// 1) the part before it is an array
// 2) the payload is an array
// and means that the user wants to expand the elements
// in the payload array and append each one into the
// destination array, like so:
// array = append(array, elems...)
// This special case is handled below.
ellipses := parts[len(parts)-1] == "..."
if ellipses {
parts = parts[:len(parts)-1]
}
var ptr interface{} = rawCfg
traverseLoop:
for i, part := range parts {
switch v := ptr.(type) {
case map[string]interface{}:
// if the next part enters a slice, and the slice is our destination,
// handle it specially (because appending to the slice copies the slice
// header, which does not replace the original one like we want)
if arr, ok := v[part].([]interface{}); ok && i == len(parts)-2 {
var idx int
if method != http.MethodPost {
idxStr := parts[len(parts)-1]
idx, err = strconv.Atoi(idxStr)
if err != nil {
return fmt.Errorf("[%s] invalid array index '%s': %v",
path, idxStr, err)
}
if idx < 0 || idx >= len(arr) {
return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
}
}
switch method {
case http.MethodGet:
err = enc.Encode(arr[idx])
if err != nil {
return fmt.Errorf("encoding config: %v", err)
}
case http.MethodPost:
if ellipses {
valArray, ok := val.([]interface{})
if !ok {
return fmt.Errorf("final element is not an array")
}
v[part] = append(arr, valArray...)
} else {
v[part] = append(arr, val)
}
case http.MethodPut:
// avoid creation of new slice and a second copy (see
// https://github.com/golang/go/wiki/SliceTricks#insert)
arr = append(arr, nil)
copy(arr[idx+1:], arr[idx:])
arr[idx] = val
v[part] = arr
case http.MethodPatch:
arr[idx] = val
case http.MethodDelete:
v[part] = append(arr[:idx], arr[idx+1:]...)
default:
return fmt.Errorf("unrecognized method %s", method)
}
break traverseLoop
}
if i == len(parts)-1 {
switch method {
case http.MethodGet:
err = enc.Encode(v[part])
if err != nil {
return fmt.Errorf("encoding config: %v", err)
}
case http.MethodPost:
// if the part is an existing list, POST appends to
// it, otherwise it just sets or creates the value
if arr, ok := v[part].([]interface{}); ok {
if ellipses {
valArray, ok := val.([]interface{})
if !ok {
return fmt.Errorf("final element is not an array")
}
v[part] = append(arr, valArray...)
} else {
v[part] = append(arr, val)
}
} else {
v[part] = val
}
case http.MethodPut:
if _, ok := v[part]; ok {
return fmt.Errorf("[%s] key already exists: %s", path, part)
}
v[part] = val
case http.MethodPatch:
if _, ok := v[part]; !ok {
return fmt.Errorf("[%s] key does not exist: %s", path, part)
}
v[part] = val
case http.MethodDelete:
delete(v, part)
default:
return fmt.Errorf("unrecognized method %s", method)
}
} else {
// if we are "PUTting" a new resource, the key(s) in its path
// might not exist yet; that's OK but we need to make them as
// we go, while we still have a pointer from the level above
if v[part] == nil && method == http.MethodPut {
v[part] = make(map[string]interface{})
}
ptr = v[part]
}
case []interface{}:
partInt, err := strconv.Atoi(part)
if err != nil {
return fmt.Errorf("[/%s] invalid array index '%s': %v",
strings.Join(parts[:i+1], "/"), part, err)
}
if partInt < 0 || partInt >= len(v) {
return fmt.Errorf("[/%s] array index out of bounds: %s",
strings.Join(parts[:i+1], "/"), part)
}
ptr = v[partInt]
default:
return fmt.Errorf("invalid traversal path at: %s", strings.Join(parts[:i+1], "/"))
}
}
return nil
}
// RemoveMetaFields removes meta fields like "@id" from a JSON message
// by using a simple regular expression. (An alternate way to do this
// would be to delete them from the raw, map[string]interface{}
// representation as they are indexed, then iterate the index we made
// and add them back after encoding as JSON, but this is simpler.)
func RemoveMetaFields(rawJSON []byte) []byte {
return idRegexp.ReplaceAllFunc(rawJSON, func(in []byte) []byte {
// matches with a comma on both sides (when "@id" property is
// not the first or last in the object) need to keep exactly
// one comma for correct JSON syntax
comma := []byte{','}
if bytes.HasPrefix(in, comma) && bytes.HasSuffix(in, comma) {
return comma
}
return []byte{}
})
}
// AdminHandler is like http.Handler except ServeHTTP may return an error.
//
// If any handler encounters an error, it should be returned for proper
// handling.
type AdminHandler interface {
ServeHTTP(http.ResponseWriter, *http.Request) error
}
// AdminHandlerFunc is a convenience type like http.HandlerFunc.
type AdminHandlerFunc func(http.ResponseWriter, *http.Request) error
// ServeHTTP implements the Handler interface.
func (f AdminHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
return f(w, r)
}
// APIError is a structured error that every API
// handler should return for consistency in logging
// and client responses. If Message is unset, then
// Err.Error() will be serialized in its place.
type APIError struct {
Code int `json:"-"`
Err error `json:"-"`
Message string `json:"error"`
}
func (e APIError) Error() string {
if e.Err != nil {
return e.Err.Error()
}
return e.Message
}
var (
// DefaultAdminListen is the address for the admin
// listener, if none is specified at startup.
DefaultAdminListen = "localhost:2019"
// ErrInternalRedir indicates an internal redirect
// and is useful when admin API handlers rewrite
// the request; in that case, authentication and
// authorization needs to happen again for the
// rewritten request.
ErrInternalRedir = fmt.Errorf("internal redirect; re-authorization required")
// DefaultAdminConfig is the default configuration
// for the administration endpoint.
DefaultAdminConfig = &AdminConfig{
Listen: DefaultAdminListen,
}
)
// 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*:\s*(-?[0-9]+(\.[0-9]+)?|(?U)".*")\s*,?`)
// pidfile is the name of the pidfile, if any.
var pidfile string
const (
rawConfigKey = "config"
idKey = "@id"
)
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
var adminServer *http.Server
+132
View File
@@ -0,0 +1,132 @@
// 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 (
"encoding/json"
"reflect"
"testing"
)
func TestUnsyncedConfigAccess(t *testing.T) {
// each test is performed in sequence, so
// each change builds on the previous ones;
// the config is not reset between tests
for i, tc := range []struct {
method string
path string // rawConfigKey will be prepended
payload string
expect string // JSON representation of what the whole config is expected to be after the request
shouldErr bool
}{
{
method: "POST",
path: "",
payload: `{"foo": "bar", "list": ["a", "b", "c"]}`, // starting value
expect: `{"foo": "bar", "list": ["a", "b", "c"]}`,
},
{
method: "POST",
path: "/foo",
payload: `"jet"`,
expect: `{"foo": "jet", "list": ["a", "b", "c"]}`,
},
{
method: "POST",
path: "/bar",
payload: `{"aa": "bb", "qq": "zz"}`,
expect: `{"foo": "jet", "bar": {"aa": "bb", "qq": "zz"}, "list": ["a", "b", "c"]}`,
},
{
method: "DELETE",
path: "/bar/qq",
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
},
{
method: "POST",
path: "/list",
payload: `"e"`,
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
},
{
method: "PUT",
path: "/list/3",
payload: `"d"`,
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e"]}`,
},
{
method: "DELETE",
path: "/list/3",
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
},
{
method: "PATCH",
path: "/list/3",
payload: `"d"`,
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d"]}`,
},
{
method: "POST",
path: "/list/...",
payload: `["e", "f", "g"]`,
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e", "f", "g"]}`,
},
} {
err := unsyncedConfigAccess(tc.method, rawConfigKey+tc.path, []byte(tc.payload), nil)
if tc.shouldErr && err == nil {
t.Fatalf("Test %d: Expected error return value, but got: %v", i, err)
}
if !tc.shouldErr && err != nil {
t.Fatalf("Test %d: Should not have had error return value, but got: %v", i, err)
}
// decode the expected config so we can do a convenient DeepEqual
var expectedDecoded interface{}
err = json.Unmarshal([]byte(tc.expect), &expectedDecoded)
if err != nil {
t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err)
}
// make sure the resulting config is as we expect it
if !reflect.DeepEqual(rawCfg[rawConfigKey], expectedDecoded) {
t.Fatalf("Test %d:\nExpected:\n\t%#v\nActual:\n\t%#v",
i, expectedDecoded, rawCfg[rawConfigKey])
}
}
}
func BenchmarkLoad(b *testing.B) {
for i := 0; i < b.N; i++ {
cfg := []byte(`{
"apps": {
"http": {
"servers": {
"myserver": {
"listen": ["tcp/localhost:8080-8084"],
"read_timeout": "30s"
},
"yourserver": {
"listen": ["127.0.0.1:5000"],
"read_header_timeout": "15s"
}
}
}
}
}
`)
Load(cfg, true)
}
}
-48
View File
@@ -1,48 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 (
"os"
"path/filepath"
"runtime"
)
// AssetsPath returns the path to the folder
// where the application may store data. If
// CADDYPATH env variable is set, that value
// is used. Otherwise, the path is the result
// of evaluating "$HOME/.caddy".
func AssetsPath() string {
if caddyPath := os.Getenv("CADDYPATH"); caddyPath != "" {
return caddyPath
}
return filepath.Join(userHomeDir(), ".caddy")
}
// userHomeDir returns the user's home directory according to
// environment variables.
//
// Credit: http://stackoverflow.com/a/7922977/1048862
func userHomeDir() string {
if runtime.GOOS == "windows" {
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
if home == "" {
home = os.Getenv("USERPROFILE")
}
return home
}
return os.Getenv("HOME")
}
-39
View File
@@ -1,39 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 (
"os"
"strings"
"testing"
)
func TestAssetsPath(t *testing.T) {
if actual := AssetsPath(); !strings.HasSuffix(actual, ".caddy") {
t.Errorf("Expected path to be a .caddy folder, got: %v", actual)
}
err := os.Setenv("CADDYPATH", "testpath")
if err != nil {
t.Error("Could not set CADDYPATH")
}
if actual, expected := AssetsPath(), "testpath"; actual != expected {
t.Errorf("Expected path to be %v, got: %v", expected, actual)
}
err = os.Setenv("CADDYPATH", "")
if err != nil {
t.Error("Could not set CADDYPATH")
}
}
-88
View File
@@ -1,88 +0,0 @@
# Mutilated beyond recognition from the example at:
# https://docs.microsoft.com/azure/devops/pipelines/languages/go
trigger:
- master
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)
variables:
GOROOT: $(gorootDir)/go
GOPATH: $(system.defaultWorkingDirectory)/gopath
GOBIN: $(GOPATH)/bin
modulePath: '$(GOPATH)/src/github.com/$(build.repository.name)'
# TODO: modules should be the default in Go 1.13, so this won't be needed
GO111MODULE: on
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
- powershell: |
Write-Host "Downloading Go... (please be patient, I am very slow)"
(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)"
Expand-Archive "$(LATEST_GO).windows-amd64.zip" -DestinationPath "$(gorootDir)"
condition: eq( variables['Agent.OS'], 'Windows_NT' )
displayName: Install Go on Windows
# TODO: When this issue is fixed, replace with installer script:
# https://github.com/golangci/golangci-lint/issues/472
- script: go get -v github.com/golangci/golangci-lint/cmd/golangci-lint
displayName: Install golangci-lint
- 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 ./...
golangci-lint run -E gofmt -E goimports -E misspell
go test -race ./...
workingDirectory: '$(modulePath)'
displayName: Run tests
+490 -925
View File
File diff suppressed because it is too large Load Diff
-612
View File
@@ -1,612 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 caddymain
import (
"bufio"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"strconv"
"strings"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyfile"
"github.com/caddyserver/caddy/caddytls"
"github.com/caddyserver/caddy/telemetry"
"github.com/google/uuid"
"github.com/klauspost/cpuid"
"github.com/mholt/certmagic"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
_ "github.com/caddyserver/caddy/caddyhttp" // plug in the HTTP server type
// This is where other plugins get plugged in (imported)
)
func init() {
caddy.TrapSignals()
flag.BoolVar(&certmagic.Default.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
flag.StringVar(&certmagic.Default.CA, "ca", certmagic.Default.CA, "URL to certificate authority's ACME server directory")
flag.StringVar(&certmagic.Default.DefaultServerName, "default-sni", certmagic.Default.DefaultServerName, "If a ClientHello ServerName is empty, use this ServerName to choose a TLS certificate")
flag.BoolVar(&certmagic.Default.DisableHTTPChallenge, "disable-http-challenge", certmagic.Default.DisableHTTPChallenge, "Disable the ACME HTTP challenge")
flag.BoolVar(&certmagic.Default.DisableTLSALPNChallenge, "disable-tls-alpn-challenge", certmagic.Default.DisableTLSALPNChallenge, "Disable the ACME TLS-ALPN challenge")
flag.StringVar(&disabledMetrics, "disabled-metrics", "", "Comma-separated list of telemetry metrics to disable")
flag.StringVar(&conf, "conf", "", "Caddyfile to load (default \""+caddy.DefaultConfigFile+"\")")
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
flag.BoolVar(&printEnv, "env", false, "Enable to print environment variables")
flag.StringVar(&envFile, "envfile", "", "Path to file with environment variables to load in KEY=VALUE format")
flag.BoolVar(&fromJSON, "json-to-caddyfile", false, "From JSON stdin to Caddyfile stdout")
flag.BoolVar(&plugins, "plugins", false, "List installed plugins")
flag.StringVar(&certmagic.Default.Email, "email", "", "Default ACME CA account email address")
flag.DurationVar(&certmagic.HTTPTimeout, "catimeout", certmagic.HTTPTimeout, "Default ACME CA HTTP timeout")
flag.StringVar(&logfile, "log", "", "Process log file")
flag.BoolVar(&logTimestamps, "log-timestamps", true, "Enable timestamps for the process log")
flag.IntVar(&logRollMB, "log-roll-mb", 100, "Roll process log when it reaches this many megabytes (0 to disable rolling)")
flag.BoolVar(&logRollCompress, "log-roll-compress", true, "Gzip-compress rolled process log files")
flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file")
flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)")
flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate")
flag.StringVar(&serverType, "type", "http", "Type of server to run")
flag.BoolVar(&toJSON, "caddyfile-to-json", false, "From Caddyfile stdin to JSON stdout")
flag.BoolVar(&version, "version", false, "Show version")
flag.BoolVar(&validate, "validate", false, "Parse the Caddyfile but do not start the server")
caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader))
caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader))
}
// Run is Caddy's main() function.
func Run() {
flag.Parse()
module := getBuildModule()
cleanModVersion := strings.TrimPrefix(module.Version, "v")
caddy.AppName = appName
caddy.AppVersion = module.Version
caddy.OnProcessExit = append(caddy.OnProcessExit, certmagic.CleanUpOwnLocks)
certmagic.UserAgent = appName + "/" + cleanModVersion
if !logTimestamps {
// Disable timestamps for logging
log.SetFlags(0)
}
// Set up process log before anything bad happens
switch logfile {
case "stdout":
log.SetOutput(os.Stdout)
case "stderr":
log.SetOutput(os.Stderr)
case "":
log.SetOutput(ioutil.Discard)
default:
if logRollMB > 0 {
log.SetOutput(&lumberjack.Logger{
Filename: logfile,
MaxSize: logRollMB,
MaxAge: 14,
MaxBackups: 10,
Compress: logRollCompress,
})
} else {
err := os.MkdirAll(filepath.Dir(logfile), 0755)
if err != nil {
mustLogFatalf("%v", err)
}
f, err := os.OpenFile(logfile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
mustLogFatalf("%v", err)
}
// don't close file; log should be writeable for duration of process
log.SetOutput(f)
}
}
// load all additional envs as soon as possible
if err := LoadEnvFromFile(envFile); err != nil {
mustLogFatalf("%v", err)
}
if printEnv {
for _, v := range os.Environ() {
fmt.Println(v)
}
}
// initialize telemetry client
if EnableTelemetry {
err := initTelemetry()
if err != nil {
mustLogFatalf("[ERROR] Initializing telemetry: %v", err)
}
} else if disabledMetrics != "" {
mustLogFatalf("[ERROR] Cannot disable specific metrics because telemetry is disabled")
}
// Check for one-time actions
if revoke != "" {
err := caddytls.Revoke(revoke)
if err != nil {
mustLogFatalf("%v", err)
}
fmt.Printf("Revoked certificate for %s\n", revoke)
os.Exit(0)
}
if version {
if module.Sum != "" {
// a build with a known version will also have a checksum
fmt.Printf("Caddy %s (%s)\n", module.Version, module.Sum)
} else {
fmt.Println(module.Version)
}
os.Exit(0)
}
if plugins {
fmt.Println(caddy.DescribePlugins())
os.Exit(0)
}
// Check if we just need to do a Caddyfile Convert and exit
checkJSONCaddyfile()
// Set CPU cap
err := setCPU(cpu)
if err != nil {
mustLogFatalf("%v", err)
}
// Executes Startup events
caddy.EmitEvent(caddy.StartupEvent, nil)
// Get Caddyfile input
caddyfileinput, err := caddy.LoadCaddyfile(serverType)
if err != nil {
mustLogFatalf("%v", err)
}
if validate {
err := caddy.ValidateAndExecuteDirectives(caddyfileinput, nil, true)
if err != nil {
mustLogFatalf("%v", err)
}
msg := "Caddyfile is valid"
fmt.Println(msg)
log.Printf("[INFO] %s", msg)
os.Exit(0)
}
// Log Caddy version before start
log.Printf("[INFO] Caddy version: %s", module.Version)
// Start your engines
instance, err := caddy.Start(caddyfileinput)
if err != nil {
mustLogFatalf("%v", err)
}
// Begin telemetry (these are no-ops if telemetry disabled)
telemetry.Set("caddy_version", module.Version)
telemetry.Set("num_listeners", len(instance.Servers()))
telemetry.Set("server_type", serverType)
telemetry.Set("os", runtime.GOOS)
telemetry.Set("arch", runtime.GOARCH)
telemetry.Set("cpu", struct {
BrandName string `json:"brand_name,omitempty"`
NumLogical int `json:"num_logical,omitempty"`
AESNI bool `json:"aes_ni,omitempty"`
}{
BrandName: cpuid.CPU.BrandName,
NumLogical: runtime.NumCPU(),
AESNI: cpuid.CPU.AesNi(),
})
if containerized := detectContainer(); containerized {
telemetry.Set("container", containerized)
}
telemetry.StartEmitting()
// Twiddle your thumbs
instance.Wait()
}
// mustLogFatalf wraps log.Fatalf() in a way that ensures the
// output is always printed to stderr so the user can see it
// if the user is still there, even if the process log was not
// enabled. If this process is an upgrade, however, and the user
// might not be there anymore, this just logs to the process
// log and exits.
func mustLogFatalf(format string, args ...interface{}) {
if !caddy.IsUpgrade() {
log.SetOutput(os.Stderr)
}
log.Fatalf(format, args...)
}
// confLoader loads the Caddyfile using the -conf flag.
func confLoader(serverType string) (caddy.Input, error) {
if conf == "" {
return nil, nil
}
if conf == "stdin" {
return caddy.CaddyfileFromPipe(os.Stdin, serverType)
}
var contents []byte
if strings.Contains(conf, "*") {
// Let caddyfile.doImport logic handle the globbed path
contents = []byte("import " + conf)
} else {
var err error
contents, err = ioutil.ReadFile(conf)
if err != nil {
return nil, err
}
}
return caddy.CaddyfileInput{
Contents: contents,
Filepath: conf,
ServerTypeName: serverType,
}, nil
}
// defaultLoader loads the Caddyfile from the current working directory.
func defaultLoader(serverType string) (caddy.Input, error) {
contents, err := ioutil.ReadFile(caddy.DefaultConfigFile)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
return caddy.CaddyfileInput{
Contents: contents,
Filepath: caddy.DefaultConfigFile,
ServerTypeName: serverType,
}, nil
}
// getBuildModule returns the build info of Caddy
// from debug.BuildInfo (requires Go modules). If
// no version information is available, a non-nil
// value will still be returned, but with an
// unknown version.
func getBuildModule() *debug.Module {
bi, ok := debug.ReadBuildInfo()
if ok {
// The recommended way to build Caddy involves
// creating a separate main module, which
// preserves caddy a read-only dependency
// TODO: track related Go issue: https://github.com/golang/go/issues/29228
for _, mod := range bi.Deps {
if mod.Path == "github.com/caddyserver/caddy" {
return mod
}
}
}
return &debug.Module{Version: "unknown"}
}
func checkJSONCaddyfile() {
if fromJSON {
jsonBytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "Read stdin failed: %v", err)
os.Exit(1)
}
caddyfileBytes, err := caddyfile.FromJSON(jsonBytes)
if err != nil {
fmt.Fprintf(os.Stderr, "Converting from JSON failed: %v", err)
os.Exit(2)
}
fmt.Println(string(caddyfileBytes))
os.Exit(0)
}
if toJSON {
caddyfileBytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "Read stdin failed: %v", err)
os.Exit(1)
}
jsonBytes, err := caddyfile.ToJSON(caddyfileBytes)
if err != nil {
fmt.Fprintf(os.Stderr, "Converting to JSON failed: %v", err)
os.Exit(2)
}
fmt.Println(string(jsonBytes))
os.Exit(0)
}
}
// setCPU parses string cpu and sets GOMAXPROCS
// according to its value. It accepts either
// a number (e.g. 3) or a percent (e.g. 50%).
// If the percent resolves to less than a single
// GOMAXPROCS, it rounds it up to GOMAXPROCS=1.
func setCPU(cpu string) error {
var numCPU int
availCPU := runtime.NumCPU()
if strings.HasSuffix(cpu, "%") {
// Percent
var percent float32
pctStr := cpu[:len(cpu)-1]
pctInt, err := strconv.Atoi(pctStr)
if err != nil || pctInt < 1 || pctInt > 100 {
return errors.New("invalid CPU value: percentage must be between 1-100")
}
percent = float32(pctInt) / 100
numCPU = int(float32(availCPU) * percent)
if numCPU < 1 {
numCPU = 1
}
} else {
// Number
num, err := strconv.Atoi(cpu)
if err != nil || num < 1 {
return errors.New("invalid CPU value: provide a number or percent greater than 0")
}
numCPU = num
}
if numCPU > availCPU {
numCPU = availCPU
}
runtime.GOMAXPROCS(numCPU)
return nil
}
// detectContainer attempts to determine whether the process is
// being run inside a container. References:
// https://tuhrig.de/how-to-know-you-are-inside-a-docker-container/
// https://stackoverflow.com/a/20012536/1048862
// https://gist.github.com/anantkamath/623ce7f5432680749e087cf8cfba9b69
func detectContainer() bool {
if runtime.GOOS != "linux" {
return false
}
file, err := os.Open("/proc/1/cgroup")
if err != nil {
return false
}
defer file.Close()
i := 0
scanner := bufio.NewScanner(file)
for scanner.Scan() {
i++
if i > 1000 {
return false
}
line := scanner.Text()
parts := strings.SplitN(line, ":", 3)
if len(parts) < 3 {
continue
}
if strings.Contains(parts[2], "docker") ||
strings.Contains(parts[2], "lxc") ||
strings.Contains(parts[2], "moby") {
return true
}
}
return false
}
// initTelemetry initializes the telemetry engine.
func initTelemetry() error {
uuidFilename := filepath.Join(caddy.AssetsPath(), "uuid")
if customUUIDFile := os.Getenv("CADDY_UUID_FILE"); customUUIDFile != "" {
uuidFilename = customUUIDFile
}
newUUID := func() uuid.UUID {
id := uuid.New()
err := os.MkdirAll(caddy.AssetsPath(), 0700)
if err != nil {
log.Printf("[ERROR] Persisting instance UUID: %v", err)
return id
}
err = ioutil.WriteFile(uuidFilename, []byte(id.String()), 0600) // human-readable as a string
if err != nil {
log.Printf("[ERROR] Persisting instance UUID: %v", err)
}
return id
}
var id uuid.UUID
// load UUID from storage, or create one if we don't have one
if uuidFile, err := os.Open(uuidFilename); os.IsNotExist(err) {
// no UUID exists yet; create a new one and persist it
id = newUUID()
} else if err != nil {
log.Printf("[ERROR] Loading persistent UUID: %v", err)
id = newUUID()
} else {
defer uuidFile.Close()
uuidBytes, err := ioutil.ReadAll(uuidFile)
if err != nil {
log.Printf("[ERROR] Reading persistent UUID: %v", err)
id = newUUID()
} else {
id, err = uuid.ParseBytes(uuidBytes)
if err != nil {
log.Printf("[ERROR] Parsing UUID: %v", err)
id = newUUID()
}
}
}
// parse and check the list of disabled metrics
var disabledMetricsSlice []string
if len(disabledMetrics) > 0 {
if len(disabledMetrics) > 1024 {
// mitigate disk space exhaustion at the collection endpoint
return fmt.Errorf("too many metrics to disable")
}
disabledMetricsSlice = splitTrim(disabledMetrics, ",")
for _, metric := range disabledMetricsSlice {
if metric == "instance_id" || metric == "timestamp" || metric == "disabled_metrics" {
return fmt.Errorf("instance_id, timestamp, and disabled_metrics cannot be disabled")
}
}
}
// initialize telemetry
telemetry.Init(id, disabledMetricsSlice)
// if any metrics were disabled, report which ones (so we know how representative the data is)
if len(disabledMetricsSlice) > 0 {
telemetry.Set("disabled_metrics", disabledMetricsSlice)
log.Printf("[NOTICE] The following telemetry metrics are disabled: %s", disabledMetrics)
}
return nil
}
// Split string s into all substrings separated by sep and returns a slice of
// the substrings between those separators.
//
// If s does not contain sep and sep is not empty, Split returns a
// slice of length 1 whose only element is s.
//
// If sep is empty, Split splits after each UTF-8 sequence. If both s
// and sep are empty, Split returns an empty slice.
//
// Each item that in result is trim space and not empty string
func splitTrim(s string, sep string) []string {
splitItems := strings.Split(s, sep)
trimItems := make([]string, 0, len(splitItems))
for _, item := range splitItems {
if item = strings.TrimSpace(item); item != "" {
trimItems = append(trimItems, item)
}
}
return trimItems
}
// LoadEnvFromFile loads additional envs if file provided and exists
// Envs in file should be in KEY=VALUE format
func LoadEnvFromFile(envFile string) error {
if envFile == "" {
return nil
}
file, err := os.Open(envFile)
if err != nil {
return err
}
defer file.Close()
envMap, err := ParseEnvFile(file)
if err != nil {
return err
}
for k, v := range envMap {
if err := os.Setenv(k, v); err != nil {
return err
}
}
return nil
}
// ParseEnvFile implements parse logic for environment files
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("Can't parse line %d; KEY contains whitespace", lineNumber)
}
key := fields[0]
val := fields[1]
if key == "" {
return nil, fmt.Errorf("Can't parse line %d; KEY can't be empty string", lineNumber)
}
envMap[key] = val
}
if err := scanner.Err(); err != nil {
return nil, err
}
return envMap, nil
}
const appName = "Caddy"
// Flags that control program flow or startup
var (
serverType string
conf string
cpu string
envFile string
fromJSON bool
logfile string
logTimestamps bool
logRollMB int
logRollCompress bool
revoke string
toJSON bool
version bool
plugins bool
printEnv bool
validate bool
disabledMetrics string
)
// EnableTelemetry defines whether telemetry is enabled in Run.
var EnableTelemetry = true
-118
View File
@@ -1,118 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 caddymain
import (
"reflect"
"runtime"
"strings"
"testing"
)
func TestSetCPU(t *testing.T) {
currentCPU := runtime.GOMAXPROCS(-1)
maxCPU := runtime.NumCPU()
halfCPU := int(0.5 * float32(maxCPU))
if halfCPU < 1 {
halfCPU = 1
}
for i, test := range []struct {
input string
output int
shouldErr bool
}{
{"1", 1, false},
{"-1", currentCPU, true},
{"0", currentCPU, true},
{"100%", maxCPU, false},
{"50%", halfCPU, false},
{"110%", currentCPU, true},
{"-10%", currentCPU, true},
{"invalid input", currentCPU, true},
{"invalid input%", currentCPU, true},
{"9999", maxCPU, false}, // over available CPU
{"1%", 1, false}, // under a single CPU; assume maxCPU < 100
} {
err := setCPU(test.input)
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected error, but there wasn't any", i)
}
if !test.shouldErr && err != nil {
t.Errorf("Test %d: Expected no error, but there was one: %v", i, err)
}
if actual, expected := runtime.GOMAXPROCS(-1), test.output; actual != expected {
t.Errorf("Test %d: GOMAXPROCS was %d but expected %d", i, actual, expected)
}
// teardown
runtime.GOMAXPROCS(currentCPU)
}
}
func TestSplitTrim(t *testing.T) {
for i, test := range []struct {
input string
output []string
sep string
}{
{"os,arch,cpu,caddy_version", []string{"os", "arch", "cpu", "caddy_version"}, ","},
{"os,arch,cpu,caddy_version,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
{"os,,, arch, cpu, caddy_version,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
{", , os, arch, cpu , caddy_version,, ,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
{"os, ,, arch, cpu , caddy_version,, ,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
} {
got := splitTrim(test.input, test.sep)
if len(got) != len(test.output) {
t.Errorf("Test %d: spliteTrim() = %v, want %v", i, got, test.output)
continue
}
for j, item := range test.output {
if item != got[j] {
t.Errorf("Test %d: spliteTrim() = %v, want %v", i, got, test.output)
break
}
}
}
}
func TestParseEnvFile(t *testing.T) {
tests := []struct {
name string
input string
want map[string]string
wantErr bool
}{
{"parsing KEY=VALUE", "PORT=4096", map[string]string{"PORT": "4096"}, false},
{"empty KEY", "=4096", nil, true},
{"one value", "test", nil, true},
{"comments skipped", "#TEST=1\nPORT=8888", map[string]string{"PORT": "8888"}, false},
{"empty line", "\nPORT=7777", map[string]string{"PORT": "7777"}, false},
{"comments with space skipped", " #TEST=1", map[string]string{}, false},
{"KEY with space", "PORT =8888", nil, true},
{"only spaces", " ", map[string]string{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reader := strings.NewReader(tt.input)
got, err := ParseEnvFile(reader)
if (err != nil) != tt.wantErr {
t.Errorf("ParseEnvFile() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseEnvFile() = %v, want %v", got, tt.want)
}
})
}
}
+49 -187
View File
@@ -1,4 +1,4 @@
// Copyright 2015 Light Code Labs, LLC
// 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.
@@ -15,198 +15,60 @@
package caddy
import (
"fmt"
"log"
"reflect"
"sync"
"testing"
"github.com/caddyserver/caddy/caddyfile"
"time"
)
/*
// TODO
func TestCaddyStartStop(t *testing.T) {
caddyfile := "localhost:1984"
for i := 0; i < 2; i++ {
_, err := Start(CaddyfileInput{Contents: []byte(caddyfile)})
if err != nil {
t.Fatalf("Error starting, iteration %d: %v", i, err)
}
client := http.Client{
Timeout: time.Duration(2 * time.Second),
}
resp, err := client.Get("http://localhost:1984")
if err != nil {
t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err)
}
resp.Body.Close()
err = Stop()
if err != nil {
t.Fatalf("Error stopping, iteration %d: %v", i, err)
}
}
}
*/
// CallbackTestContext implements Context interface
type CallbackTestContext struct {
// If MakeServersFail is set to true then MakeServers returns an error
MakeServersFail bool
}
func (h *CallbackTestContext) InspectServerBlocks(name string, sblock []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
return sblock, nil
}
func (h *CallbackTestContext) MakeServers() ([]Server, error) {
if h.MakeServersFail {
return make([]Server, 0), fmt.Errorf("MakeServers failed")
}
return make([]Server, 0), nil
}
func TestCaddyRestartCallbacks(t *testing.T) {
for i, test := range []struct {
restartFail bool
expectedCalls []string
}{
{false, []string{"OnRestart", "OnShutdown"}},
{true, []string{"OnRestart", "OnRestartFailed"}},
} {
serverName := fmt.Sprintf("%v", i)
// RegisterServerType to make successful restart possible
RegisterServerType(serverName, ServerType{
Directives: func() []string { return []string{} },
// If MakeServersFail is true then the restart will fail due to context failure
NewContext: func(inst *Instance) Context { return &CallbackTestContext{MakeServersFail: test.restartFail} },
})
c := NewTestController(serverName, "")
c.instance = &Instance{
serverType: serverName,
wg: new(sync.WaitGroup),
}
// Register callbacks which save the calls order
calls := make([]string, 0)
c.OnRestart(func() error {
calls = append(calls, "OnRestart")
return nil
})
c.OnRestartFailed(func() error {
calls = append(calls, "OnRestartFailed")
return nil
})
c.OnShutdown(func() error {
calls = append(calls, "OnShutdown")
return nil
})
_, err := c.instance.Restart(CaddyfileInput{Contents: []byte(""), ServerTypeName: serverName})
if err != nil {
log.Printf("[ERROR] Restart failed: %v", err)
}
if !reflect.DeepEqual(calls, test.expectedCalls) {
t.Errorf("Test %d: Callbacks expected: %v, got: %v", i, test.expectedCalls, calls)
}
err = c.instance.Stop()
if err != nil {
log.Printf("[ERROR] Stop failed: %v", err)
}
c.instance.Wait()
}
}
func TestIsLoopback(t *testing.T) {
for i, test := range []struct {
func TestParseDuration(t *testing.T) {
const day = 24 * time.Hour
for i, tc := range []struct {
input string
expect bool
expect time.Duration
}{
{"example.com", false},
{"localhost", true},
{"localhost:1234", true},
{"localhost:", true},
{"127.0.0.1", true},
{"127.0.0.1:443", true},
{"127.0.1.5", true},
{"10.0.0.5", false},
{"12.7.0.1", false},
{"[::1]", true},
{"[::1]:1234", true},
{"::1", true},
{"::", false},
{"[::]", false},
{"local", false},
{
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,
},
} {
if got, want := IsLoopback(test.input), test.expect; got != want {
t.Errorf("Test %d (%s): expected %v but was %v", i, test.input, want, got)
}
}
}
func TestIsInternal(t *testing.T) {
for i, test := range []struct {
input string
expect bool
}{
{"9.255.255.255", false},
{"10.0.0.0", true},
{"10.0.0.1", true},
{"10.255.255.254", true},
{"10.255.255.255", true},
{"11.0.0.0", false},
{"10.0.0.5:1234", true},
{"11.0.0.5:1234", false},
{"172.15.255.255", false},
{"172.16.0.0", true},
{"172.16.0.1", true},
{"172.31.255.254", true},
{"172.31.255.255", true},
{"172.32.0.0", false},
{"172.16.0.1:1234", true},
{"192.167.255.255", false},
{"192.168.0.0", true},
{"192.168.0.1", true},
{"192.168.255.254", true},
{"192.168.255.255", true},
{"192.169.0.0", false},
{"192.168.0.1:1234", true},
{"fbff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", false},
{"fc00::", true},
{"fc00::1", true},
{"[fc00::1]", true},
{"[fc00::1]:8888", true},
{"fdff:ffff:ffff:ffff:ffff:ffff:ffff:fffe", true},
{"fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true},
{"fe00::", false},
{"fd12:3456:789a:1::1:1234", true},
{"example.com", false},
{"localhost", false},
{"localhost:1234", false},
{"localhost:", false},
{"127.0.0.1", false},
{"127.0.0.1:443", false},
{"127.0.1.5", false},
{"12.7.0.1", false},
{"[::1]", false},
{"[::1]:1234", false},
{"::1", false},
{"::", false},
{"[::]", false},
{"local", false},
} {
if got, want := IsInternal(test.input), test.expect; got != want {
t.Errorf("Test %d (%s): expected %v but was %v", i, test.input, want, got)
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)
}
}
}
+91
View File
@@ -0,0 +1,91 @@
// 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 (
"encoding/json"
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
)
// Adapter adapts Caddyfile to Caddy JSON.
type Adapter struct {
ServerType ServerType
}
// Adapt converts the Caddyfile config in body to Caddy JSON.
func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []caddyconfig.Warning, error) {
if a.ServerType == nil {
return nil, nil, fmt.Errorf("no server type")
}
if options == nil {
options = make(map[string]interface{})
}
filename, _ := options["filename"].(string)
if filename == "" {
filename = "Caddyfile"
}
serverBlocks, err := Parse(filename, body)
if err != nil {
return nil, nil, err
}
cfg, warnings, err := a.ServerType.Setup(serverBlocks, options)
if err != nil {
return nil, warnings, err
}
marshalFunc := json.Marshal
if options["pretty"] == "true" {
marshalFunc = caddyconfig.JSONIndent
}
result, err := marshalFunc(cfg)
return result, warnings, err
}
// Unmarshaler is a type that can unmarshal
// Caddyfile tokens to set itself up for a
// JSON encoding. The goal of an unmarshaler
// is not to set itself up for actual use,
// but to set itself up for being marshaled
// 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
}
// ServerType is a type that can evaluate a Caddyfile and set up a caddy config.
type ServerType interface {
// Setup takes the server blocks which
// contain tokens, as well as options
// (e.g. CLI flags) and creates a Caddy
// config, along with any warnings or
// an error.
Setup([]ServerBlock, map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error)
}
// Interface guard
var _ caddyconfig.Adapter = (*Adapter)(nil)
+396
View File
@@ -0,0 +1,396 @@
// 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 (
"errors"
"fmt"
"io"
"log"
"strings"
)
// Dispenser is a type that dispenses tokens, similarly to a lexer,
// except that it can do so with some notion of structure. An empty
// Dispenser is invalid; call NewDispenser to make a proper instance.
type Dispenser struct {
tokens []Token
cursor int
nesting int
}
// NewDispenser returns a Dispenser filled with the given tokens.
func NewDispenser(tokens []Token) *Dispenser {
return &Dispenser{
tokens: tokens,
cursor: -1,
}
}
// 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.
func (d *Dispenser) Next() bool {
if d.cursor < len(d.tokens)-1 {
d.cursor++
return true
}
return false
}
// Prev moves to the previous token. It does the inverse
// of Next(), except this function may decrement the cursor
// to -1 so that the next call to Next() points to the
// first token; this allows dispensing to "start over". This
// method returns true if the cursor ends up pointing to a
// valid token.
func (d *Dispenser) Prev() bool {
if d.cursor > -1 {
d.cursor--
return d.cursor > -1
}
return false
}
// NextArg loads the next token if it is on the same
// line and if it is not a block opening (open curly
// brace). Returns true if an argument token was
// loaded; false otherwise. If false, all tokens on
// the line have been consumed except for potentially
// a block opening. It handles imported tokens
// correctly.
func (d *Dispenser) NextArg() bool {
if !d.nextOnSameLine() {
return false
}
if d.Val() == "{" {
// roll back; a block opening is not an argument
d.cursor--
return false
}
return true
}
// nextOnSameLine advances the cursor if the next
// token is on the same line of the same file.
func (d *Dispenser) nextOnSameLine() bool {
if d.cursor < 0 {
d.cursor++
return true
}
if d.cursor >= len(d.tokens) {
return false
}
if d.cursor < len(d.tokens)-1 &&
d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
d.cursor++
return true
}
return false
}
// NextLine loads the next token only if it is not on the same
// line as the current token, and returns true if a token was
// loaded; false otherwise. If false, there is not another token
// or it is on the same line. It handles imported tokens correctly.
func (d *Dispenser) NextLine() bool {
if d.cursor < 0 {
d.cursor++
return true
}
if d.cursor >= len(d.tokens) {
return false
}
if d.cursor < len(d.tokens)-1 &&
(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
d.cursor++
return true
}
return false
}
// NextBlock can be used as the condition of a for loop
// to load the next token as long as it opens a block or
// is already in a block nested more than initialNestingLevel.
// In other words, a loop over NextBlock() will iterate
// all tokens in the block assuming the next token is an
// open curly brace, until the matching closing brace.
// The open and closing brace tokens for the outer-most
// block will be consumed internally and omitted from
// the iteration.
//
// Proper use of this method looks like this:
//
// for nesting := d.Nesting(); d.NextBlock(nesting); {
// }
//
// However, in simple cases where it is known that the
// Dispenser is new and has not already traversed state
// by a loop over NextBlock(), this will do:
//
// for d.NextBlock(0) {
// }
//
// As with other token parsing logic, a loop over
// NextBlock() should be contained within a loop over
// Next(), as it is usually prudent to skip the initial
// token.
func (d *Dispenser) NextBlock(initialNestingLevel int) bool {
if d.nesting > initialNestingLevel {
if !d.Next() {
return false // should be EOF error
}
if d.Val() == "}" && !d.nextOnSameLine() {
d.nesting--
} else if d.Val() == "{" && !d.nextOnSameLine() {
d.nesting++
}
return d.nesting > initialNestingLevel
}
if !d.nextOnSameLine() { // block must open on same line
return false
}
if d.Val() != "{" {
d.cursor-- // roll back if not opening brace
return false
}
d.Next() // consume open curly brace
if d.Val() == "}" {
return false // open and then closed right away
}
d.nesting++
return true
}
// Nesting returns the current nesting level. Necessary
// if using NextBlock()
func (d *Dispenser) Nesting() int {
return d.nesting
}
// Val gets the text of the current token. If there is no token
// loaded, it returns empty string.
func (d *Dispenser) Val() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return ""
}
return d.tokens[d.cursor].Text
}
// Line gets the line number of the current token.
// If there is no token loaded, it returns 0.
func (d *Dispenser) Line() int {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return 0
}
return d.tokens[d.cursor].Line
}
// File gets the filename where the current token originated.
func (d *Dispenser) File() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return ""
}
return d.tokens[d.cursor].File
}
// Args is a convenience function that loads the next arguments
// (tokens on the same line) into an arbitrary number of strings
// pointed to in targets. If there are not enough argument tokens
// available to fill targets, false is returned and the remaining
// targets are left unchanged. If all the targets are filled,
// then true is returned.
func (d *Dispenser) Args(targets ...*string) bool {
for i := 0; i < len(targets); i++ {
if !d.NextArg() {
return false
}
*targets[i] = d.Val()
}
return true
}
// AllArgs is like Args, but if there are more argument tokens
// available than there are targets, false is returned. The
// number of available argument tokens must match the number of
// targets exactly to return true.
func (d *Dispenser) AllArgs(targets ...*string) bool {
if !d.Args(targets...) {
return false
}
if d.NextArg() {
d.Prev()
return false
}
return true
}
// RemainingArgs loads any more arguments (tokens on the same line)
// into a slice and returns them. Open curly brace tokens also indicate
// the end of arguments, and the curly brace is not included in
// the return value nor is it loaded.
func (d *Dispenser) RemainingArgs() []string {
var args []string
for d.NextArg() {
args = append(args, d.Val())
}
return args
}
// 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) 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())
}
var openedBlock bool
for nesting := d.Nesting(); d.NextBlock(nesting); {
if !openedBlock {
// because NextBlock() consumes the initial open
// curly brace, we rewind here to append it, since
// our case is special in that we want the new
// dispenser to have all the tokens including
// surrounding curly braces
d.Prev()
tkns = append(tkns, d.Token())
d.Next()
openedBlock = true
}
tkns = append(tkns, d.Token())
}
if openedBlock {
// include closing brace
tkns = append(tkns, d.Token())
// do not consume the closing curly brace; the
// next iteration of the enclosing loop will
// call Next() and consume it
}
return tkns
}
// Token returns the current token.
func (d *Dispenser) Token() Token {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return Token{}
}
return d.tokens[d.cursor]
}
// Reset sets d's cursor to the beginning, as
// if this was a new and unused dispenser.
func (d *Dispenser) Reset() {
d.cursor = -1
d.nesting = 0
}
// ArgErr returns an argument error, meaning that another
// argument was expected but not found. In other words,
// a line break or open curly brace was encountered instead of
// an argument.
func (d *Dispenser) ArgErr() error {
if d.Val() == "{" {
return d.Err("Unexpected token '{', expecting argument")
}
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
}
// SyntaxErr creates a generic syntax error which explains what was
// found and what was expected.
func (d *Dispenser) SyntaxErr(expected string) error {
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
return errors.New(msg)
}
// EOFErr returns an error indicating that the dispenser reached
// the end of the input when searching for the next token.
func (d *Dispenser) EOFErr() error {
return d.Errf("Unexpected EOF")
}
// Err generates a custom parse-time error with a message of msg.
func (d *Dispenser) Err(msg string) error {
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
return errors.New(msg)
}
// Errf is like Err, but for formatted error messages
func (d *Dispenser) Errf(format string, args ...interface{}) error {
return d.Err(fmt.Sprintf(format, args...))
}
// Delete deletes the current token and returns the updated slice
// of tokens. The cursor is not advanced to the next token.
// Because deletion modifies the underlying slice, this method
// should only be called if you have access to the original slice
// of tokens and/or are using the slice of tokens outside this
// Dispenser instance. If you do not re-assign the slice with the
// return value of this method, inconsistencies in the token
// array will become apparent (or worse, hide from you like they
// did me for 3 and a half freaking hours late one night).
func (d *Dispenser) Delete() []Token {
if d.cursor >= 0 && d.cursor <= len(d.tokens)-1 {
d.tokens = append(d.tokens[:d.cursor], d.tokens[d.cursor+1:]...)
d.cursor--
}
return d.tokens
}
// numLineBreaks counts how many line breaks are in the token
// value given by the token index tknIdx. It returns 0 if the
// token does not exist or there are no line breaks.
func (d *Dispenser) numLineBreaks(tknIdx int) int {
if tknIdx < 0 || tknIdx >= len(d.tokens) {
return 0
}
return strings.Count(d.tokens[tknIdx].Text, "\n")
}
// isNewLine determines whether the current token is on a different
// line (higher line number) than the previous token. It handles imported
// tokens correctly. If there isn't a previous token, it returns true.
func (d *Dispenser) isNewLine() bool {
if d.cursor < 1 {
return true
}
if d.cursor > len(d.tokens)-1 {
return false
}
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
}
+9 -9
View File
@@ -1,4 +1,4 @@
// Copyright 2015 Light Code Labs, LLC
// 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.
@@ -25,7 +25,7 @@ func TestDispenser_Val_Next(t *testing.T) {
dir1 arg1
dir2 arg2 arg3
dir3`
d := NewDispenser("Testfile", strings.NewReader(input))
d := NewTestDispenser(input)
if val := d.Val(); val != "" {
t.Fatalf("Val(): Should return empty string when no token loaded; got '%s'", val)
@@ -63,7 +63,7 @@ func TestDispenser_NextArg(t *testing.T) {
input := `dir1 arg1
dir2 arg2 arg3
dir3`
d := NewDispenser("Testfile", strings.NewReader(input))
d := NewTestDispenser(input)
assertNext := func(shouldLoad bool, expectedVal string, expectedCursor int) {
if d.Next() != shouldLoad {
@@ -110,7 +110,7 @@ func TestDispenser_NextLine(t *testing.T) {
input := `host:port
dir1 arg1
dir2 arg2 arg3`
d := NewDispenser("Testfile", strings.NewReader(input))
d := NewTestDispenser(input)
assertNextLine := func(shouldLoad bool, expectedVal string, expectedCursor int) {
if d.NextLine() != shouldLoad {
@@ -143,10 +143,10 @@ func TestDispenser_NextBlock(t *testing.T) {
}
foobar2 {
}`
d := NewDispenser("Testfile", strings.NewReader(input))
d := NewTestDispenser(input)
assertNextBlock := func(shouldLoad bool, expectedCursor, expectedNesting int) {
if loaded := d.NextBlock(); loaded != shouldLoad {
if loaded := d.NextBlock(0); loaded != shouldLoad {
t.Errorf("NextBlock(): Should return %v but got %v", shouldLoad, loaded)
}
if d.cursor != expectedCursor {
@@ -173,7 +173,7 @@ func TestDispenser_Args(t *testing.T) {
dir2 arg4 arg5
dir3 arg6 arg7
dir4`
d := NewDispenser("Testfile", strings.NewReader(input))
d := NewTestDispenser(input)
d.Next() // dir1
@@ -240,7 +240,7 @@ func TestDispenser_RemainingArgs(t *testing.T) {
dir2 arg4 arg5
dir3 arg6 { arg7
dir4`
d := NewDispenser("Testfile", strings.NewReader(input))
d := NewTestDispenser(input)
d.Next() // dir1
@@ -277,7 +277,7 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
input := `dir1 {
}
dir2 arg1 arg2`
d := NewDispenser("Testfile", strings.NewReader(input))
d := NewTestDispenser(input)
d.cursor = 1 // {
+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))
}
}
}
+61 -22
View File
@@ -16,6 +16,7 @@ package caddyfile
import (
"bufio"
"bytes"
"io"
"unicode"
)
@@ -26,9 +27,10 @@ type (
// are separated by whitespace. A word can be enclosed
// in quotes if it contains whitespace.
lexer struct {
reader *bufio.Reader
token Token
line int
reader *bufio.Reader
token Token
line int
skippedLines int
}
// Token represents a single parsable unit.
@@ -72,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)
@@ -91,27 +93,32 @@ func (l *lexer) next() bool {
panic(err)
}
if quoted {
if !escaped {
if ch == '\\' {
escaped = true
continue
} else if ch == '"' {
quoted = false
if !escaped && !btQuoted && ch == '\\' {
escaped = true
continue
}
if quoted || btQuoted {
if quoted && escaped {
// all is literal in quoted area,
// so only escape quotes
if ch != '"' {
val = append(val, '\\')
}
escaped = false
} else {
if quoted && ch == '"' {
return makeToken()
}
if btQuoted && ch == '`' {
return makeToken()
}
}
if ch == '\n' {
l.line++
}
if escaped {
// only escape quotes
if ch != '"' {
val = append(val, '\\')
}
l.line += 1 + l.skippedLines
l.skippedLines = 0
}
val = append(val, ch)
escaped = false
continue
}
@@ -120,7 +127,13 @@ func (l *lexer) next() bool {
continue
}
if ch == '\n' {
l.line++
if escaped {
l.skippedLines++
escaped = false
} else {
l.line += 1 + l.skippedLines
l.skippedLines = 0
}
comment = false
}
if len(val) > 0 {
@@ -129,10 +142,9 @@ func (l *lexer) next() bool {
continue
}
if ch == '#' {
if ch == '#' && len(val) == 0 {
comment = true
}
if comment {
continue
}
@@ -143,8 +155,35 @@ func (l *lexer) next() bool {
quoted = true
continue
}
if ch == '`' {
btQuoted = true
continue
}
}
if escaped {
val = append(val, '\\')
escaped = false
}
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
}
+126 -34
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,22 +114,72 @@ func TestLexer(t *testing.T) {
},
},
{
input: `"don't\escape"`,
input: []byte("An escaped \"newline\\\ninside\" quotes"),
expected: []Token{
{Line: 1, Text: "An"},
{Line: 1, Text: "escaped"},
{Line: 1, Text: "newline\\\ninside"},
{Line: 2, Text: "quotes"},
},
},
{
input: []byte("An escaped newline\\\noutside quotes"),
expected: []Token{
{Line: 1, Text: "An"},
{Line: 1, Text: "escaped"},
{Line: 1, Text: "newline"},
{Line: 1, Text: "outside"},
{Line: 1, Text: "quotes"},
},
},
{
input: []byte("line1\\\nescaped\nline2\nline3"),
expected: []Token{
{Line: 1, Text: "line1"},
{Line: 1, Text: "escaped"},
{Line: 3, Text: "line2"},
{Line: 4, Text: "line3"},
},
},
{
input: []byte("line1\\\nescaped1\\\nescaped2\nline4\nline5"),
expected: []Token{
{Line: 1, Text: "line1"},
{Line: 1, Text: "escaped1"},
{Line: 1, Text: "escaped2"},
{Line: 4, Text: "line4"},
{Line: 5, Text: "line5"},
},
},
{
input: []byte(`"unescapable\ in quotes"`),
expected: []Token{
{Line: 1, Text: `unescapable\ in quotes`},
},
},
{
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: `A "quoted value with line
input: []byte(`un\escapable`),
expected: []Token{
{Line: 1, Text: `un\escapable`},
},
},
{
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"},
@@ -121,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: ``},
@@ -135,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"},
@@ -144,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))
+160 -99
View File
@@ -15,11 +15,15 @@
package caddyfile
import (
"io"
"bytes"
"io/ioutil"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2"
)
// Parse parses the input just enough to group tokens, in
@@ -28,33 +32,74 @@ import (
// Directives that do not appear in validDirectives will cause
// an error. If you do not want to check for valid directives,
// pass in nil instead.
func Parse(filename string, input io.Reader, validDirectives []string) ([]ServerBlock, error) {
p := parser{Dispenser: NewDispenser(filename, input), validDirectives: validDirectives}
//
// Environment variables in {$ENVIRONMENT_VARIABLE} notation
// will be replaced before parsing begins.
func Parse(filename string, input []byte) ([]ServerBlock, error) {
tokens, err := allTokens(filename, input)
if err != nil {
return nil, err
}
p := parser{Dispenser: NewDispenser(tokens)}
return p.parseAll()
}
// replaceEnvVars replaces all occurrences of environment variables.
func replaceEnvVars(input []byte) ([]byte, error) {
var offset int
for {
begin := bytes.Index(input[offset:], spanOpen)
if begin < 0 {
break
}
begin += offset // make beginning relative to input, not offset
end := bytes.Index(input[begin+len(spanOpen):], spanClose)
if end < 0 {
break
}
end += begin + len(spanOpen) // make end relative to input, not begin
// get the name; if there is no name, skip it
envVarName := input[begin+len(spanOpen) : end]
if len(envVarName) == 0 {
offset = end + len(spanClose)
continue
}
// get the value of the environment variable
envVarValue := []byte(os.ExpandEnv(os.Getenv(string(envVarName))))
// splice in the value
input = append(input[:begin],
append(envVarValue, input[end+len(spanClose):]...)...)
// continue at the end of the replacement
offset = begin + len(envVarValue)
}
return input, nil
}
// allTokens lexes the entire input, but does not parse it.
// It returns all the tokens from the input, unstructured
// and in order.
func allTokens(input io.Reader) ([]Token, error) {
l := new(lexer)
err := l.load(input)
func allTokens(filename string, input []byte) ([]Token, error) {
input, err := replaceEnvVars(input)
if err != nil {
return nil, err
}
var tokens []Token
for l.next() {
tokens = append(tokens, l.token)
tokens, err := Tokenize(input, filename)
if err != nil {
return nil, err
}
return tokens, nil
}
type parser struct {
Dispenser
*Dispenser
block ServerBlock // current server block being parsed
validDirectives []string // a directive must be valid or it's an error
eof bool // if we encounter a valid EOF in a hard place
definedSnippets map[string][]Token
nesting int
}
func (p *parser) parseAll() ([]ServerBlock, error) {
@@ -65,17 +110,19 @@ func (p *parser) parseAll() ([]ServerBlock, error) {
if err != nil {
return blocks, err
}
if len(p.block.Keys) > 0 {
if len(p.block.Keys) > 0 || len(p.block.Segments) > 0 {
blocks = append(blocks, p.block)
}
if p.nesting > 0 {
return blocks, p.EOFErr()
}
}
return blocks, nil
}
func (p *parser) parseOne() error {
p.block = ServerBlock{Tokens: make(map[string][]Token)}
p.block = ServerBlock{}
return p.begin()
}
@@ -121,7 +168,7 @@ func (p *parser) addresses() error {
var expectingAnother bool
for {
tkn := replaceEnvVars(p.Val())
tkn := p.Val()
// special case: import directive replaces tokens during parse-time
if tkn == "import" && p.isNewLine() {
@@ -182,7 +229,7 @@ func (p *parser) blockContents() error {
return err
}
// Only look for close curly brace if there was an opening
// only look for close curly brace if there was an opening
if errOpenCurlyBrace == nil {
err = p.closeCurlyBrace()
if err != nil {
@@ -201,6 +248,7 @@ func (p *parser) directives() error {
for p.Next() {
// end of server block
if p.Val() == "}" {
// p.nesting has already been decremented
break
}
@@ -214,11 +262,15 @@ func (p *parser) directives() error {
continue
}
// normal case: parse a directive on this line
// normal case: parse a directive as a new segment
// (a "segment" is a line which starts with a directive
// and which ends at the end of the line or at the end of
// the block that is opened at the end of the line)
if err := p.directive(); err != nil {
return err
}
}
return nil
}
@@ -233,15 +285,23 @@ func (p *parser) doImport() error {
if !p.NextArg() {
return p.ArgErr()
}
importPattern := replaceEnvVars(p.Val())
importPattern := p.Val()
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
@@ -254,7 +314,7 @@ func (p *parser) doImport() error {
// list of matching filenames
absFile, err := filepath.Abs(p.Dispenser.File())
if err != nil {
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.File(), err)
}
var matches []string
@@ -293,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
}
@@ -316,7 +386,12 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
return nil, p.Errf("Could not import %s: is a directory", importFile)
}
importedTokens, err := allTokens(file)
input, err := ioutil.ReadAll(file)
if err != nil {
return nil, p.Errf("Could not read imported file %s: %v", importFile, err)
}
importedTokens, err := allTokens(importFile, input)
if err != nil {
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
}
@@ -325,7 +400,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
// (we use full, absolute path to avoid bugs: issue #1892)
filename, err := filepath.Abs(importFile)
if err != nil {
return nil, p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
return nil, p.Errf("Failed to get absolute path of file: %s: %v", importFile, err)
}
for i := 0; i < len(importedTokens); i++ {
importedTokens[i].File = filename
@@ -341,26 +416,22 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
// are loaded into the current server block for later use
// by directive setup functions.
func (p *parser) directive() error {
dir := replaceEnvVars(p.Val())
nesting := 0
// TODO: More helpful error message ("did you mean..." or "maybe you need to install its server type")
if !p.validDirective(dir) {
return p.Errf("Unknown directive '%s'", dir)
}
// a segment is a list of tokens associated with this directive
var segment Segment
// The directive itself is appended as a relevant token
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
// the directive itself is appended as a relevant token
segment = append(segment, p.Token())
for p.Next() {
if p.Val() == "{" {
nesting++
} else if p.isNewLine() && nesting == 0 {
p.nesting++
} else if p.isNewLine() && p.nesting == 0 {
p.cursor-- // read too far
break
} else if p.Val() == "}" && nesting > 0 {
nesting--
} else if p.Val() == "}" && nesting == 0 {
} else if p.Val() == "}" && p.nesting > 0 {
p.nesting--
} else if p.Val() == "}" && p.nesting == 0 {
return p.Err("Unexpected '}' because no matching opening brace")
} else if p.Val() == "import" && p.isNewLine() {
if err := p.doImport(); err != nil {
@@ -369,13 +440,16 @@ func (p *parser) directive() error {
p.cursor-- // cursor is advanced when we continue, so roll back one more
continue
}
p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
segment = append(segment, p.Token())
}
if nesting > 0 {
p.block.Segments = append(p.block.Segments, segment)
if p.nesting > 0 {
return p.EOFErr()
}
return nil
}
@@ -401,56 +475,6 @@ func (p *parser) closeCurlyBrace() error {
return nil
}
// validDirective returns true if dir is in p.validDirectives.
func (p *parser) validDirective(dir string) bool {
if p.validDirectives == nil {
return true
}
for _, d := range p.validDirectives {
if d == dir {
return true
}
}
return false
}
// replaceEnvVars replaces environment variables that appear in the token
// and understands both the $UNIX and %WINDOWS% syntaxes.
func replaceEnvVars(s string) string {
s = replaceEnvReferences(s, "{%", "%}")
s = replaceEnvReferences(s, "{$", "}")
return s
}
// replaceEnvReferences performs the actual replacement of env variables
// in s, given the placeholder start and placeholder end strings.
func replaceEnvReferences(s, refStart, refEnd string) string {
index := strings.Index(s, refStart)
for index != -1 {
endIndex := strings.Index(s[index:], refEnd)
if endIndex == -1 {
break
}
endIndex += index
if endIndex > index+len(refStart) {
ref := s[index : endIndex+len(refEnd)]
s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1)
} else {
return s
}
index = strings.Index(s, refStart)
}
return s
}
// ServerBlock associates any number of keys (usually addresses
// of some sort) with tokens (grouped by directive name).
type ServerBlock struct {
Keys []string
Tokens map[string][]Token
}
func (p *parser) isSnippet() (bool, string) {
keys := p.block.Keys
// A snippet block is a single key with parens. Nothing else qualifies.
@@ -462,29 +486,66 @@ func (p *parser) isSnippet() (bool, string) {
// read and store everything in a block for later replay.
func (p *parser) snippetTokens() ([]Token, error) {
// TODO: disallow imports in snippets for simplicity at import time
// snippet must have curlies.
err := p.openCurlyBrace()
if err != nil {
return nil, err
}
count := 1
nesting := 1 // count our own nesting in snippets
tokens := []Token{}
for p.Next() {
if p.Val() == "}" {
count--
if count == 0 {
nesting--
if nesting == 0 {
break
}
}
if p.Val() == "{" {
count++
nesting++
}
tokens = append(tokens, p.tokens[p.cursor])
}
// make sure we're matched up
if count != 0 {
if nesting != 0 {
return nil, p.SyntaxErr("}")
}
return tokens, nil
}
// ServerBlock associates any number of keys from the
// head of the server block with tokens, which are
// grouped by segments.
type ServerBlock struct {
Keys []string
Segments []Segment
}
// DispenseDirective returns a dispenser that contains
// all the tokens in the server block.
func (sb ServerBlock) DispenseDirective(dir string) *Dispenser {
var tokens []Token
for _, seg := range sb.Segments {
if len(seg) > 0 && seg[0].Text == dir {
tokens = append(tokens, seg...)
}
}
return NewDispenser(tokens)
}
// Segment is a list of tokens which begins with a directive
// and ends at the end of the directive (either at the end of
// the line, or at the end of a block it opens).
type Segment []Token
// Directive returns the directive name for the segment.
// The directive name is the text of the first token.
func (s Segment) Directive() string {
if len(s) > 0 {
return s[0].Text
}
return ""
}
// spanOpen and spanClose are used to bound spans that
// contain the name of an environment variable.
var spanOpen, spanClose = []byte{'{', '$'}, []byte{'}'}
+145 -199
View File
@@ -15,17 +15,17 @@
package caddyfile
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
)
func TestAllTokens(t *testing.T) {
input := strings.NewReader("a b c\nd e")
input := []byte("a b c\nd e")
expected := []string{"a", "b", "c", "d", "e"}
tokens, err := allTokens(input)
tokens, err := allTokens("TestAllTokens", input)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
@@ -53,84 +53,67 @@ func TestParseOneAndImport(t *testing.T) {
input string
shouldErr bool
keys []string
tokens map[string]int // map of directive name to number of tokens expected
numTokens []int // number of tokens to expect in each segment
}{
{`localhost`, false, []string{
"localhost",
}, map[string]int{}},
}, []int{}},
{`localhost
dir1`, false, []string{
"localhost",
}, map[string]int{
"dir1": 1,
}},
}, []int{1}},
{`localhost:1234
dir1 foo bar`, false, []string{
"localhost:1234",
}, map[string]int{
"dir1": 3,
}},
}, []int{3},
},
{`localhost {
dir1
}`, false, []string{
"localhost",
}, map[string]int{
"dir1": 1,
}},
}, []int{1}},
{`localhost:1234 {
dir1 foo bar
dir2
}`, false, []string{
"localhost:1234",
}, map[string]int{
"dir1": 3,
"dir2": 1,
}},
}, []int{3, 1}},
{`http://localhost https://localhost
dir1 foo bar`, false, []string{
"http://localhost",
"https://localhost",
}, map[string]int{
"dir1": 3,
}},
}, []int{3}},
{`http://localhost https://localhost {
dir1 foo bar
}`, false, []string{
"http://localhost",
"https://localhost",
}, map[string]int{
"dir1": 3,
}},
}, []int{3}},
{`http://localhost, https://localhost {
dir1 foo bar
}`, false, []string{
"http://localhost",
"https://localhost",
}, map[string]int{
"dir1": 3,
}},
}, []int{3}},
{`http://localhost, {
}`, true, []string{
"http://localhost",
}, map[string]int{}},
}, []int{}},
{`host1:80, http://host2.com
dir1 foo bar
dir2 baz`, false, []string{
"host1:80",
"http://host2.com",
}, map[string]int{
"dir1": 3,
"dir2": 2,
}},
}, []int{3, 2}},
{`http://host1.com,
http://host2.com,
@@ -138,7 +121,7 @@ func TestParseOneAndImport(t *testing.T) {
"http://host1.com",
"http://host2.com",
"https://host3.com",
}, map[string]int{}},
}, []int{}},
{`http://host1.com:1234, https://host2.com
dir1 foo {
@@ -147,10 +130,7 @@ func TestParseOneAndImport(t *testing.T) {
dir2`, false, []string{
"http://host1.com:1234",
"https://host2.com",
}, map[string]int{
"dir1": 6,
"dir2": 1,
}},
}, []int{6, 1}},
{`127.0.0.1
dir1 {
@@ -160,34 +140,25 @@ func TestParseOneAndImport(t *testing.T) {
foo bar
}`, false, []string{
"127.0.0.1",
}, map[string]int{
"dir1": 5,
"dir2": 5,
}},
}, []int{5, 5}},
{`localhost
dir1 {
foo`, true, []string{
"localhost",
}, map[string]int{
"dir1": 3,
}},
}, []int{3}},
{`localhost
dir1 {
}`, false, []string{
"localhost",
}, map[string]int{
"dir1": 3,
}},
}, []int{3}},
{`localhost
dir1 {
} }`, true, []string{
"localhost",
}, map[string]int{
"dir1": 3,
}},
}, []int{}},
{`localhost
dir1 {
@@ -197,48 +168,41 @@ func TestParseOneAndImport(t *testing.T) {
}
dir2 foo bar`, false, []string{
"localhost",
}, map[string]int{
"dir1": 7,
"dir2": 3,
}},
}, []int{7, 3}},
{``, false, []string{}, map[string]int{}},
{``, false, []string{}, []int{}},
{`localhost
dir1 arg1
import testdata/import_test1.txt`, false, []string{
"localhost",
}, map[string]int{
"dir1": 2,
"dir2": 3,
"dir3": 1,
}},
}, []int{2, 3, 1}},
{`import testdata/import_test2.txt`, false, []string{
"host1",
}, map[string]int{
"dir1": 1,
"dir2": 2,
}},
}, []int{1, 2}},
{`import testdata/import_test1.txt testdata/import_test2.txt`, true, []string{}, map[string]int{}},
{`import testdata/not_found.txt`, true, []string{}, []int{}},
{`import testdata/not_found.txt`, true, []string{}, map[string]int{}},
{`""`, false, []string{}, []int{}},
{`""`, false, []string{}, map[string]int{}},
{``, false, []string{}, []int{}},
{``, false, []string{}, map[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{}, map[string]int{}},
{`import /*/*.txt`, true, []string{}, map[string]int{}},
{`import /???/?*?o`, true, []string{}, map[string]int{}},
{`import /??`, true, []string{}, map[string]int{}},
{`import /[a-z]`, true, []string{}, map[string]int{}},
{`import {$}`, true, []string{}, map[string]int{}},
{`import {%}`, true, []string{}, map[string]int{}},
{`import {$$}`, true, []string{}, map[string]int{}},
{`import {%%}`, true, []string{}, map[string]int{}},
{`import }{$"`, true, []string{}, []int{}},
{`import /*/*.txt`, true, []string{}, []int{}},
{`import /???/?*?o`, true, []string{}, []int{}},
{`import /??`, true, []string{}, []int{}},
{`import /[a-z]`, true, []string{}, []int{}},
{`import {$}`, true, []string{}, []int{}},
{`import {%}`, true, []string{}, []int{}},
{`import {$$}`, true, []string{}, []int{}},
{`import {%%}`, true, []string{}, []int{}},
} {
result, err := testParseOne(test.input)
@@ -249,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))
@@ -261,15 +226,16 @@ func TestParseOneAndImport(t *testing.T) {
}
}
if len(result.Tokens) != len(test.tokens) {
t.Errorf("Test %d: Expected %d directives, had %d",
i, len(test.tokens), len(result.Tokens))
if len(result.Segments) != len(test.numTokens) {
t.Errorf("Test %d: Expected %d segments, had %d",
i, len(test.numTokens), len(result.Segments))
continue
}
for directive, tokens := range result.Tokens {
if len(tokens) != test.tokens[directive] {
t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d",
i, directive, test.tokens[directive], len(tokens))
for j, seg := range result.Segments {
if len(seg) != test.numTokens[j] {
t.Errorf("Test %d, segment %d: Expected %d tokens, counted %d",
i, j, test.numTokens[j], len(seg))
continue
}
}
@@ -289,12 +255,12 @@ func TestRecursiveImport(t *testing.T) {
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
return false
}
if len(got.Tokens) != 2 {
t.Errorf("got wrong number of tokens: expect 2, got %d", len(got.Tokens))
if len(got.Segments) != 2 {
t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments))
return false
}
if len(got.Tokens["dir1"]) != 1 || len(got.Tokens["dir2"]) != 2 {
t.Errorf("got unexpect tokens: %v", got.Tokens)
if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 2 {
t.Errorf("got unexpected tokens: %v", got.Segments)
return false
}
return true
@@ -384,12 +350,12 @@ func TestDirectiveImport(t *testing.T) {
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
return false
}
if len(got.Tokens) != 2 {
t.Errorf("got wrong number of tokens: expect 2, got %d", len(got.Tokens))
if len(got.Segments) != 2 {
t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments))
return false
}
if len(got.Tokens["dir1"]) != 1 || len(got.Tokens["proxy"]) != 8 {
t.Errorf("got unexpect tokens: %v", got.Tokens)
if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 8 {
t.Errorf("got unexpected tokens: %v", got.Segments)
return false
}
return true
@@ -511,96 +477,85 @@ func TestParseAll(t *testing.T) {
}
func TestEnvironmentReplacement(t *testing.T) {
os.Setenv("PORT", "8080")
os.Setenv("ADDRESS", "servername.com")
os.Setenv("FOOBAR", "foobar")
os.Setenv("PARTIAL_DIR", "r1")
// basic test; unix-style env vars
p := testParser(`{$ADDRESS}`)
blocks, _ := p.parseAll()
if actual, expected := blocks[0].Keys[0], "servername.com"; expected != actual {
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
for i, test := range []struct {
input string
expect string
}{
{
input: "",
expect: "",
},
{
input: "foo",
expect: "foo",
},
{
input: "{$NOT_SET}",
expect: "",
},
{
input: "foo{$NOT_SET}bar",
expect: "foobar",
},
{
input: "{$FOOBAR}",
expect: "foobar",
},
{
input: "foo {$FOOBAR} bar",
expect: "foo foobar bar",
},
{
input: "foo{$FOOBAR}bar",
expect: "foofoobarbar",
},
{
input: "foo\n{$FOOBAR}\nbar",
expect: "foo\nfoobar\nbar",
},
{
input: "{$FOOBAR} {$FOOBAR}",
expect: "foobar foobar",
},
{
input: "{$FOOBAR}{$FOOBAR}",
expect: "foobarfoobar",
},
{
input: "{$FOOBAR",
expect: "{$FOOBAR",
},
{
input: "{$LONGER_NAME $FOOBAR}",
expect: "",
},
{
input: "{$}",
expect: "{$}",
},
{
input: "{$$}",
expect: "",
},
{
input: "{$",
expect: "{$",
},
{
input: "}{$",
expect: "}{$",
},
} {
actual, err := replaceEnvVars([]byte(test.input))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(actual, []byte(test.expect)) {
t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
}
}
// basic test; unix-style env vars
p = testParser(`di{$PARTIAL_DIR}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Keys[0], "dir1"; expected != actual {
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
}
// multiple vars per token
p = testParser(`{$ADDRESS}:{$PORT}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
}
// windows-style var and unix style in same token
p = testParser(`{%ADDRESS%}:{$PORT}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
}
// reverse order
p = testParser(`{$ADDRESS}:{%PORT%}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
}
// env var in server block body as argument
p = testParser(":{%PORT%}\ndir1 {$FOOBAR}")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Keys[0], ":8080"; expected != actual {
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "foobar"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
// combined windows env vars in argument
p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "servername.com/foobar"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
// malformed env var (windows)
p = testParser(":1234\ndir1 {%ADDRESS}")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "{%ADDRESS}"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
// malformed (non-existent) env var (unix)
p = testParser(`:{$PORT$}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Keys[0], ":"; expected != actual {
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
}
// in quoted field
p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "Test foobar test"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
// after end token
p = testParser(":1234\nanswer \"{{ .Name }} {$FOOBAR}\"")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Tokens["answer"][1].Text, "{{ .Name }} foobar"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
}
func testParser(input string) parser {
buf := strings.NewReader(input)
p := parser{Dispenser: NewDispenser("Caddyfile", buf)}
return p
}
func TestSnippets(t *testing.T) {
@@ -617,26 +572,21 @@ func TestSnippets(t *testing.T) {
if err != nil {
t.Fatal(err)
}
for _, b := range blocks {
t.Log(b.Keys)
t.Log(b.Tokens)
}
if len(blocks) != 1 {
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
}
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
}
if len(blocks[0].Tokens) != 2 {
t.Fatalf("Server block should have tokens from import")
if len(blocks[0].Segments) != 2 {
t.Fatalf("Server block should have tokens from import, got: %+v", blocks[0])
}
if actual, expected := blocks[0].Tokens["gzip"][0].Text, "gzip"; expected != actual {
if actual, expected := blocks[0].Segments[0][0].Text, "gzip"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Tokens["errors"][1].Text, "stderr"; expected != actual {
if actual, expected := blocks[0].Segments[1][1].Text, "stderr"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
}
func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) {
@@ -666,11 +616,7 @@ func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) {
if err != nil {
t.Fatal(err)
}
for _, b := range blocks {
t.Log(b.Keys)
t.Log(b.Tokens)
}
auth := blocks[0].Tokens["basicauth"]
auth := blocks[0].Segments[0]
line := auth[0].Text + " " + auth[1].Text + " " + auth[2].Text + " " + auth[3].Text
if line != "basicauth / import password" {
// Previously, it would be changed to:
@@ -701,20 +647,20 @@ func TestSnippetAcrossMultipleFiles(t *testing.T) {
if err != nil {
t.Fatal(err)
}
for _, b := range blocks {
t.Log(b.Keys)
t.Log(b.Tokens)
}
if len(blocks) != 1 {
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
}
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
}
if len(blocks[0].Tokens) != 1 {
if len(blocks[0].Segments) != 1 {
t.Fatalf("Server block should have tokens from import")
}
if actual, expected := blocks[0].Tokens["gzip"][0].Text, "gzip"; expected != actual {
if actual, expected := blocks[0].Segments[0][0].Text, "gzip"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
}
func testParser(input string) parser {
return parser{Dispenser: NewTestDispenser(input)}
}
+1
View File
@@ -0,0 +1 @@
{args.0}
+1
View File
@@ -0,0 +1 @@
{args.0} {args.1}
+136
View File
@@ -0,0 +1,136 @@
// 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 (
"encoding/json"
"fmt"
"github.com/caddyserver/caddy/v2"
)
// Adapter is a type which can adapt a configuration to Caddy JSON.
// It returns the results and any warnings, or an error.
type Adapter interface {
Adapt(body []byte, options map[string]interface{}) ([]byte, []Warning, error)
}
// Warning represents a warning or notice related to conversion.
type Warning struct {
File string `json:"file,omitempty"`
Line int `json:"line,omitempty"`
Directive string `json:"directive,omitempty"`
Message string `json:"message,omitempty"`
}
// JSON encodes val as JSON, returning it as a json.RawMessage. Any
// marshaling errors (which are highly unlikely with correct code)
// are converted to warnings. This is convenient when filling config
// structs that require a json.RawMessage, without having to worry
// about errors.
func JSON(val interface{}, warnings *[]Warning) json.RawMessage {
b, err := json.Marshal(val)
if err != nil {
if warnings != nil {
*warnings = append(*warnings, Warning{Message: err.Error()})
}
return nil
}
return b
}
// 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)
if err != nil {
if warnings != nil {
*warnings = append(*warnings, Warning{Message: err.Error()})
}
return nil
}
// then decode the object
var tmp map[string]interface{}
err = json.Unmarshal(enc, &tmp)
if err != nil {
if warnings != nil {
*warnings = append(*warnings, Warning{Message: err.Error()})
}
return nil
}
// so we can easily add the module's field with its appointed value
tmp[fieldName] = fieldVal
// then re-marshal as JSON
result, err := json.Marshal(tmp)
if err != nil {
if warnings != nil {
*warnings = append(*warnings, Warning{Message: err.Error()})
}
return nil
}
return result
}
// JSONIndent is used to JSON-marshal the final resulting Caddy
// configuration in a consistent, human-readable way.
func JSONIndent(val interface{}) ([]byte, error) {
return json.MarshalIndent(val, "", "\t")
}
// RegisterAdapter registers a config adapter with the given name.
// This should usually be done at init-time. It panics if the
// adapter cannot be registered successfully.
func RegisterAdapter(name string, adapter Adapter) {
if _, ok := configAdapters[name]; ok {
panic(fmt.Errorf("%s: already registered", name))
}
configAdapters[name] = adapter
caddy.RegisterModule(adapterModule{name, adapter})
}
// GetAdapter returns the adapter with the given name,
// or nil if one with that name is not registered.
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)
+392
View File
@@ -0,0 +1,392 @@
// 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 (
"fmt"
"net"
"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/caddyserver/certmagic"
)
// mapAddressToServerBlocks returns a map of listener address to list of server
// blocks that will be served on that address. To do this, each server block is
// expanded so that each one is considered individually, although keys of a
// server block that share the same address stay grouped together so the config
// isn't repeated unnecessarily. For example, this Caddyfile:
//
// example.com {
// bind 127.0.0.1
// }
// www.example.com, example.net/path, localhost:9999 {
// bind 127.0.0.1 1.2.3.4
// }
//
// has two server blocks to start with. But expressed in this Caddyfile are
// actually 4 listener addresses: 127.0.0.1:443, 1.2.3.4:443, 127.0.0.1:9999,
// and 127.0.0.1:9999. This is because the bind directive is applied to each
// 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
// 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
// block, and for each server block, iterate its keys. For the first, it
// finds one key (example.com) and determines its listener address
// (127.0.0.1:443 - because of 'bind' and automatic HTTPS). It then adds
// the listener address to the map value returned by this function, with
// the first server block as one of its associations.
//
// It then iterates each key on the second server block and associates them
// with one or more listener addresses. Indeed, each key in this block has
// two listener addresses because of the 'bind' directive. Once we know
// which addresses serve which keys, we can create a new server block for
// each address containing the contents of the server block and only those
// specific keys of the server block which use that address.
//
// It is possible and even likely that some keys in the returned map have
// the exact same list of server blocks (i.e. they are identical). This
// happens when multiple hosts are declared with a 'bind' directive and
// the resulting listener addresses are not shared by any other server
// block (or the other server blocks are exactly identical in their token
// contents). This happens with our example above because 1.2.3.4:443
// and 1.2.3.4:9999 are used exclusively with the second server block. This
// repetition may be undesirable, so call consolidateAddrMappings() to map
// multiple addresses to the same lists of server blocks (a many:many mapping).
// (Doing this is essentially a map-reduce technique.)
func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock,
options map[string]interface{}) (map[string][]serverBlock, error) {
sbmap := make(map[string][]serverBlock)
for i, sblock := range originalServerBlocks {
// within a server block, we need to map all the listener addresses
// implied by the server block to the keys of the server block which
// will be served by them; this has the effect of treating each
// key of a server block as its own, but without having to repeat its
// contents in cases where multiple keys really can be served together
addrToKeys := make(map[string][]string)
for j, key := range sblock.block.Keys {
// a key can have multiple listener addresses if there are multiple
// arguments to the 'bind' directive (although they will all have
// the same port, since the port is defined by the key or is implicit
// through automatic HTTPS)
addrs, err := st.listenerAddrsForServerBlockKey(sblock, key, options)
if err != nil {
return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key, err)
}
// associate this key with each listener address it is served on
for _, addr := range addrs {
addrToKeys[addr] = append(addrToKeys[addr], key)
}
}
// now that we know which addresses serve which keys of this
// server block, we iterate that mapping and create a list of
// new server blocks for each address where the keys of the
// 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,
})
}
}
return sbmap, nil
}
// consolidateAddrMappings eliminates repetition of identical server blocks in a mapping of
// single listener addresses to lists of server blocks. Since multiple addresses may serve
// identical sites (server block contents), this function turns a 1:many mapping into a
// many:many mapping. Server block contents (tokens) must be exactly identical so that
// reflect.DeepEqual returns true in order for the addresses to be combined. Identical
// entries are deleted from the addrToServerBlocks map. Essentially, each pairing (each
// 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 {
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
a := sbAddrAssociation{
addresses: []string{addr},
serverBlocks: sblocks,
}
// now find other addresses that map to identical
// server blocks and add them to our list of
// addresses, while removing them from the map
for otherAddr, otherSblocks := range addrToServerBlocks {
if addr == otherAddr {
continue
}
if reflect.DeepEqual(sblocks, otherSblocks) {
a.addresses = append(a.addresses, otherAddr)
delete(addrToServerBlocks, otherAddr)
}
}
sbaddrs = append(sbaddrs, a)
}
return sbaddrs
}
func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string,
options map[string]interface{}) ([]string, error) {
addr, err := ParseAddress(key)
if err != nil {
return nil, fmt.Errorf("parsing key: %v", err)
}
addr = addr.Normalize()
// figure out the HTTP and HTTPS ports; either
// use defaults, or override with user config
httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort)
if hport, ok := options["http_port"]; ok {
httpPort = strconv.Itoa(hport.(int))
}
if hsport, ok := options["https_port"]; ok {
httpsPort = strconv.Itoa(hsport.(int))
}
// default port is the HTTPS port
lnPort := httpsPort
if addr.Port != "" {
// port explicitly defined
lnPort = addr.Port
} else if addr.Scheme == "http" {
// port inferred from scheme
lnPort = httpPort
}
// error if scheme and port combination violate convention
if (addr.Scheme == "http" && lnPort == httpsPort) || (addr.Scheme == "https" && lnPort == httpPort) {
return nil, fmt.Errorf("[%s] scheme and port violate convention", key)
}
// the bind directive specifies hosts, but is optional
lnHosts := make([]string, 0, len(sblock.pile))
for _, cfgVal := range sblock.pile["bind"] {
lnHosts = append(lnHosts, cfgVal.Value.([]string)...)
}
if len(lnHosts) == 0 {
lnHosts = []string{""}
}
// use a map to prevent duplication
listeners := make(map[string]struct{})
for _, host := range lnHosts {
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
listenersList := make([]string, 0, len(listeners))
for lnStr := range listeners {
listenersList = append(listenersList, lnStr)
}
return listenersList, nil
}
// Address represents a site address. It contains
// the original input value, and the component
// parts of an address. The component parts may be
// updated to the correct values as setup proceeds,
// but the original value should never be changed.
//
// The Host field must be in a normalized form.
type Address struct {
Original, Scheme, Host, Port, Path string
}
// ParseAddress parses an address string into a structured format with separate
// scheme, host, port, and path portions, as well as the original input string.
func ParseAddress(str string) (Address, error) {
const maxLen = 4096
if len(str) > maxLen {
str = str[:maxLen]
}
remaining := strings.TrimSpace(str)
a := Address{Original: remaining}
// extract scheme
splitScheme := strings.SplitN(remaining, "://", 2)
switch len(splitScheme) {
case 0:
return a, nil
case 1:
remaining = splitScheme[0]
case 2:
a.Scheme = splitScheme[0]
remaining = splitScheme[1]
}
// extract host and port
hostSplit := strings.SplitN(remaining, "/", 2)
if len(hostSplit) > 0 {
host, port, err := net.SplitHostPort(hostSplit[0])
if err != nil {
host, port, err = net.SplitHostPort(hostSplit[0] + ":")
if err != nil {
host = hostSplit[0]
}
}
a.Host = host
a.Port = port
}
if len(hostSplit) == 2 {
// all that remains is the path
a.Path = "/" + hostSplit[1]
}
// make sure port is valid
if a.Port != "" {
if portNum, err := strconv.Atoi(a.Port); err != nil {
return Address{}, fmt.Errorf("invalid port '%s': %v", a.Port, err)
} else if portNum < 0 || portNum > 65535 {
return Address{}, fmt.Errorf("port %d is out of range", portNum)
}
}
return a, nil
}
// String returns a human-readable form of a. It will
// be a cleaned-up and filled-out URL string.
func (a Address) String() string {
if a.Host == "" && a.Port == "" {
return ""
}
scheme := a.Scheme
if scheme == "" {
if a.Port == strconv.Itoa(certmagic.HTTPSPort) {
scheme = "https"
} else {
scheme = "http"
}
}
s := scheme
if s != "" {
s += "://"
}
if a.Port != "" &&
((scheme == "https" && a.Port != strconv.Itoa(caddyhttp.DefaultHTTPSPort)) ||
(scheme == "http" && a.Port != strconv.Itoa(caddyhttp.DefaultHTTPPort))) {
s += net.JoinHostPort(a.Host, a.Port)
} else {
s += a.Host
}
if a.Path != "" {
s += a.Path
}
return s
}
// Normalize returns a normalized version of a.
func (a Address) Normalize() Address {
path := a.Path
// ensure host is normalized if it's an IP address
host := strings.TrimSpace(a.Host)
if ip := net.ParseIP(host); ip != nil {
host = ip.String()
}
return Address{
Original: a.Original,
Scheme: lowerExceptPlaceholders(a.Scheme),
Host: lowerExceptPlaceholders(host),
Port: a.Port,
Path: path,
}
}
// Key returns a string form of a, much like String() does, but this
// method doesn't add anything default that wasn't in the original.
func (a Address) Key() string {
res := ""
if a.Scheme != "" {
res += a.Scheme + "://"
}
if a.Host != "" {
res += a.Host
}
// insert port only if the original has its own explicit port
if a.Port != "" &&
len(a.Original) >= len(res) &&
strings.HasPrefix(a.Original[len(res):], ":"+a.Port) {
res += ":" + a.Port
}
if a.Path != "" {
res += a.Path
}
return res
}
// 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()
}
@@ -1,4 +1,4 @@
// Copyright 2015 Light Code Labs, LLC
// 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.
@@ -12,20 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package main
// +build gofuzz
import "testing"
package httpcaddyfile
// This works because it does not have the same signature as the
// conventional "TestMain" function described in the testing package
// godoc.
func TestMain(t *testing.T) {
var ran bool
run = func() {
ran = true
}
main()
if !ran {
t.Error("Expected Run() to be called, but it wasn't")
func FuzzParseAddress(data []byte) int {
addr, err := ParseAddress(string(data))
if err != nil {
if addr == (Address{}) {
return 1
}
return 0
}
return 1
}
+183
View File
@@ -0,0 +1,183 @@
package httpcaddyfile
import (
"testing"
)
func TestParseAddress(t *testing.T) {
for i, test := range []struct {
input string
scheme, host, port, path string
shouldErr bool
}{
{``, "", "", "", "", false},
{`localhost`, "", "localhost", "", "", false},
{`localhost:1234`, "", "localhost", "1234", "", false},
{`localhost:`, "", "localhost", "", "", false},
{`0.0.0.0`, "", "0.0.0.0", "", "", false},
{`127.0.0.1:1234`, "", "127.0.0.1", "1234", "", false},
{`:1234`, "", "", "1234", "", false},
{`[::1]`, "", "::1", "", "", false},
{`[::1]:1234`, "", "::1", "1234", "", false},
{`:`, "", "", "", "", false},
{`:http`, "", "", "", "", true},
{`:https`, "", "", "", "", true},
{`localhost:http`, "", "", "", "", true}, // using service name in port is verboten, as of Go 1.12.8
{`localhost:https`, "", "", "", "", true},
{`http://localhost:https`, "", "", "", "", true}, // conflict
{`http://localhost:http`, "", "", "", "", true}, // repeated scheme
{`host:https/path`, "", "", "", "", true},
{`http://localhost:443`, "http", "localhost", "443", "", false}, // NOTE: not conventional
{`https://localhost:80`, "https", "localhost", "80", "", false}, // NOTE: not conventional
{`http://localhost`, "http", "localhost", "", "", false},
{`https://localhost`, "https", "localhost", "", "", false},
{`http://{env.APP_DOMAIN}`, "http", "{env.APP_DOMAIN}", "", "", false},
{`{env.APP_DOMAIN}:80`, "", "{env.APP_DOMAIN}", "80", "", false},
{`{env.APP_DOMAIN}/path`, "", "{env.APP_DOMAIN}", "", "/path", false},
{`example.com/{env.APP_PATH}`, "", "example.com", "", "/{env.APP_PATH}", false},
{`http://127.0.0.1`, "http", "127.0.0.1", "", "", false},
{`https://127.0.0.1`, "https", "127.0.0.1", "", "", false},
{`http://[::1]`, "http", "::1", "", "", false},
{`http://localhost:1234`, "http", "localhost", "1234", "", false},
{`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", "", false},
{`http://[::1]:1234`, "http", "::1", "1234", "", false},
{``, "", "", "", "", false},
{`::1`, "", "::1", "", "", false},
{`localhost::`, "", "localhost::", "", "", false},
{`#$%@`, "", "#$%@", "", "", false}, // don't want to presume what the hostname could be
{`host/path`, "", "host", "", "/path", false},
{`http://host/`, "http", "host", "", "/", false},
{`//asdf`, "", "", "", "//asdf", false},
{`:1234/asdf`, "", "", "1234", "/asdf", false},
{`http://host/path`, "http", "host", "", "/path", false},
{`https://host:443/path/foo`, "https", "host", "443", "/path/foo", false},
{`host:80/path`, "", "host", "80", "/path", false},
{`/path`, "", "", "", "/path", false},
} {
actual, err := ParseAddress(test.input)
if err != nil && !test.shouldErr {
t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err)
}
if err == nil && test.shouldErr {
t.Errorf("Test %d (%s): Expected error, but had none (%#v)", i, test.input, actual)
}
if !test.shouldErr && actual.Original != test.input {
t.Errorf("Test %d (%s): Expected original '%s', got '%s'", i, test.input, test.input, actual.Original)
}
if actual.Scheme != test.scheme {
t.Errorf("Test %d (%s): Expected scheme '%s', got '%s'", i, test.input, test.scheme, actual.Scheme)
}
if actual.Host != test.host {
t.Errorf("Test %d (%s): Expected host '%s', got '%s'", i, test.input, test.host, actual.Host)
}
if actual.Port != test.port {
t.Errorf("Test %d (%s): Expected port '%s', got '%s'", i, test.input, test.port, actual.Port)
}
if actual.Path != test.path {
t.Errorf("Test %d (%s): Expected path '%s', got '%s'", i, test.input, test.path, actual.Path)
}
}
}
func TestAddressString(t *testing.T) {
for i, test := range []struct {
addr Address
expected string
}{
{Address{Scheme: "http", Host: "host", Port: "1234", Path: "/path"}, "http://host:1234/path"},
{Address{Scheme: "", Host: "host", Port: "", Path: ""}, "http://host"},
{Address{Scheme: "", Host: "host", Port: "80", Path: ""}, "http://host"},
{Address{Scheme: "", Host: "host", Port: "443", Path: ""}, "https://host"},
{Address{Scheme: "https", Host: "host", Port: "443", Path: ""}, "https://host"},
{Address{Scheme: "https", Host: "host", Port: "", Path: ""}, "https://host"},
{Address{Scheme: "", Host: "host", Port: "80", Path: "/path"}, "http://host/path"},
{Address{Scheme: "http", Host: "", Port: "1234", Path: ""}, "http://:1234"},
{Address{Scheme: "", Host: "", Port: "", Path: ""}, ""},
} {
actual := test.addr.String()
if actual != test.expected {
t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual)
}
}
}
func TestKeyNormalization(t *testing.T) {
testCases := []struct {
input string
expect string
}{
{
input: "example.com",
expect: "example.com",
},
{
input: "http://host:1234/path",
expect: "http://host:1234/path",
},
{
input: "HTTP://A/ABCDEF",
expect: "http://a/ABCDEF",
},
{
input: "A/ABCDEF",
expect: "a/ABCDEF",
},
{
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",
},
{
input: ":443",
expect: ":443",
},
{
input: ":1234",
expect: ":1234",
},
{
input: "",
expect: "",
},
{
input: ":",
expect: "",
},
{
input: "[::]",
expect: "::",
},
}
for i, tc := range testCases {
addr, err := ParseAddress(tc.input)
if err != nil {
t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err)
continue
}
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)
}
}
}
+673
View File
@@ -0,0 +1,673 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpcaddyfile
import (
"encoding/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("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:
//
// bind <addresses...>
//
func parseBind(h Helper) ([]ConfigValue, error) {
var lnHosts []string
for h.Next() {
lnHosts = append(lnHosts, h.RemainingArgs()...)
}
return h.NewBindAddresses(lnHosts), nil
}
// parseTLS parses the tls directive. Syntax:
//
// 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
// eab <key_id> <mac_key>
// issuer <module_name> ...
// }
//
func parseTLS(h Helper) ([]ConfigValue, error) {
cp := new(caddytls.ConnectionPolicy)
var fileLoader caddytls.FileLoader
var folderLoader caddytls.FolderLoader
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
firstLine := h.RemainingArgs()
switch len(firstLine) {
case 0:
case 1:
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]
}
case 2:
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 ... 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
}
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 nesting := h.Nesting(); h.NextBlock(nesting); {
hasBlock = true
switch h.Val() {
case "protocols":
args := h.RemainingArgs()
if len(args) == 0 {
return nil, h.SyntaxErr("one or two protocols")
}
if len(args) > 0 {
if _, ok := caddytls.SupportedProtocols[args[0]]; !ok {
return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[0])
}
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])
}
cp.ProtocolMax = args[1]
}
case "ciphers":
for h.NextArg() {
if !caddytls.CipherSuiteNameSupported(h.Val()) {
return nil, h.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", h.Val())
}
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())
}
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()
}
cp.ALPN = args
case "load":
folderLoader = append(folderLoader, h.RemainingArgs()...)
case "ca":
arg := h.RemainingArgs()
if len(arg) != 1 {
return nil, h.ArgErr()
}
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)
}
case "dns":
if !h.NextArg() {
return nil, h.ArgErr()
}
provName := h.Val()
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
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)
}
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())
}
}
// a naked tls directive is not allowed
if len(firstLine) == 0 && !hasBlock {
return nil, h.ArgErr()
}
}
// begin building the final config values
var configVals []ConfigValue
// certificate loaders
if len(fileLoader) > 0 {
configVals = append(configVals, ConfigValue{
Class: "tls.cert_loader",
Value: fileLoader,
})
}
if len(folderLoader) > 0 {
configVals = append(configVals, ConfigValue{
Class: "tls.cert_loader",
Value: folderLoader,
})
}
// 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))
}
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: disambiguateACMEIssuer(acmeIssuer),
})
}
// on-demand TLS
if onDemand {
configVals = append(configVals, ConfigValue{
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>]
//
func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
if !h.Next() {
return nil, h.ArgErr()
}
if !h.NextArg() {
return nil, h.ArgErr()
}
to := h.Val()
var code string
if h.NextArg() {
code = h.Val()
}
if code == "permanent" {
code = "301"
}
if code == "temporary" || code == "" {
code = "302"
}
var body string
if code == "html" {
// Script tag comes first since that will better imitate a redirect in the browser's
// history, but the meta tag is a fallback for most non-JS clients.
const metaRedir = `<!DOCTYPE html>
<html>
<head>
<title>Redirecting...</title>
<script>window.location.replace("%s");</script>
<meta http-equiv="refresh" content="0; URL='%s'">
</head>
<body>Redirecting to <a href="%s">%s</a>...</body>
</html>
`
safeTo := html.EscapeString(to)
body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo)
}
return caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(code),
Headers: http.Header{"Location": []string{to}},
Body: body,
}, nil
}
// parseRespond parses the respond directive.
func parseRespond(h Helper) (caddyhttp.MiddlewareHandler, error) {
sr := new(caddyhttp.StaticResponse)
err := sr.UnmarshalCaddyfile(h.Dispenser)
if err != nil {
return nil, err
}
return sr, nil
}
// parseRoute parses the route directive.
func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) {
sr := new(caddyhttp.Subroute)
allResults, err := parseSegmentAsConfig(h)
if err != nil {
return nil, err
}
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
}
func parseHandle(h Helper) (caddyhttp.MiddlewareHandler, error) {
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() {
// log does not currently support any arguments
if h.NextArg() {
return nil, h.ArgErr()
}
cl := new(caddy.CustomLog)
for h.NextBlock(0) {
switch h.Val() {
case "output":
if !h.NextArg() {
return nil, h.ArgErr()
}
moduleName := h.Val()
// 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())
}
}
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 configValues, nil
}
@@ -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
}
}
}
+502
View File
@@ -0,0 +1,502 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpcaddyfile
import (
"encoding/json"
"net"
"sort"
"strconv"
"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"
)
// 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",
// URI manipulation
"uri",
"try_files",
// middleware handlers; some wrap responses
"basicauth",
"request_header",
"encode",
"templates",
// special routing & dispatching directives
"handle",
"handle_path",
"route",
"push",
// handlers that typically respond to requests
"respond",
"metrics",
"reverse_proxy",
"php_fastcgi",
"file_server",
"acme_server",
}
// directiveIsOrdered returns true if dir is
// a known, ordered (sorted) directive.
func directiveIsOrdered(dir string) bool {
for _, d := range directiveOrder {
if d == dir {
return true
}
}
return false
}
// RegisterDirective registers a unique directive dir with an
// associated unmarshaling (setup) function. When directive dir
// is encountered in a Caddyfile, setupFunc will be called to
// unmarshal its tokens.
func RegisterDirective(dir string, setupFunc UnmarshalFunc) {
if _, ok := registeredDirectives[dir]; ok {
panic("directive " + dir + " already registered")
}
registeredDirectives[dir] = setupFunc
}
// RegisterHandlerDirective is like RegisterDirective, but for
// directives which specifically output only an HTTP handler.
// Directives registered with this function will always have
// an optional matcher token as the first argument.
func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
RegisterDirective(dir, func(h Helper) ([]ConfigValue, error) {
if !h.Next() {
return nil, h.ArgErr()
}
matcherSet, ok, err := h.MatcherToken()
if err != nil {
return nil, err
}
if ok {
// strip matcher token; we don't need to
// use the return value here because a
// new dispenser should have been made
// solely for this directive's tokens,
// with no other uses of same slice
h.Dispenser.Delete()
}
h.Dispenser.Reset() // pretend this lookahead never happened
val, err := setupFunc(h)
if err != nil {
return nil, err
}
return h.NewRoute(matcherSet, val), nil
})
}
// 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
parentBlock caddyfile.ServerBlock
groupCounter counter
}
// Option gets the option keyed by name.
func (h Helper) Option(name string) interface{} {
return h.options[name]
}
// Caddyfiles returns the list of config files from
// which tokens in the current server block were loaded.
func (h Helper) Caddyfiles() []string {
// first obtain set of names of files involved
// in this server block, without duplicates
files := make(map[string]struct{})
for _, segment := range h.parentBlock.Segments {
for _, token := range segment {
files[token.File] = struct{}{}
}
}
// then convert the set into a slice
filesSlice := make([]string, 0, len(files))
for file := range files {
filesSlice = append(filesSlice, file)
}
return filesSlice
}
// JSON converts val into JSON. Any errors are added to warnings.
func (h Helper) JSON(val interface{}) json.RawMessage {
return caddyconfig.JSON(val, h.warnings)
}
// MatcherToken assumes the next argument token is (possibly) a matcher,
// and if so, returns the matcher set along with a true value. If the next
// token is not a matcher, nil and false is returned. Note that a true
// value may be returned with a nil matcher set if it is a catch-all.
func (h Helper) MatcherToken() (caddy.ModuleMap, bool, error) {
if !h.NextArg() {
return nil, false, nil
}
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 {
mod, err := caddy.GetModule(caddy.GetModuleID(handler))
if err != nil {
*h.warnings = append(*h.warnings, caddyconfig.Warning{
File: h.File(),
Line: h.Line(),
Message: err.Error(),
})
return nil
}
var matcherSetsRaw []caddy.ModuleMap
if matcherSet != nil {
matcherSetsRaw = append(matcherSetsRaw, matcherSet)
}
return []ConfigValue{
{
Class: "route",
Value: caddyhttp.Route{
MatcherSetsRaw: matcherSetsRaw,
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", mod.ID.Name(), h.warnings)},
},
},
}
}
// GroupRoutes adds the routes (caddyhttp.Route type) in vals to the
// same group, if there is more than one route in vals.
func (h Helper) GroupRoutes(vals []ConfigValue) {
// ensure there's at least two routes; group of one is pointless
var count int
for _, v := range vals {
if _, ok := v.Value.(caddyhttp.Route); ok {
count++
if count > 1 {
break
}
}
}
if count < 2 {
return
}
// now that we know the group will have some effect, do it
groupName := h.groupCounter.nextGroup()
for i := range vals {
if route, ok := vals[i].Value.(caddyhttp.Route); ok {
route.Group = groupName
vals[i].Value = route
}
}
}
// NewBindAddresses returns config values relevant to adding
// listener bind addresses to the config.
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.
type ConfigValue struct {
// The kind of value this is. As the config is
// being built, the adapter will look in the
// "pile" for values belonging to a certain
// class when it is setting up a certain part
// of the config. The associated value will be
// type-asserted and placed accordingly.
Class string
// The value to be used when building the config.
// Generally its type is associated with the
// name of the Class.
Value interface{}
directive string
}
func sortRoutes(routes []ConfigValue) {
dirPositions := make(map[string]int)
for i, dir := range directiveOrder {
dirPositions[dir] = i
}
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 {
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])
}
// if both directives have no path matcher, use whichever one
// has any kind of matcher defined first.
if iPathLen == 0 && jPathLen == 0 {
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
}
// sort with the most-specific (longest) path first
return iPathLen > jPathLen
})
}
// 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 (
// UnmarshalFunc is a function which can unmarshal Caddyfile
// tokens into zero or more config values using a Helper type.
// These are passed in a call to RegisterDirective.
UnmarshalFunc func(h Helper) ([]ConfigValue, error)
// UnmarshalHandlerFunc is like UnmarshalFunc, except the
// output of the unmarshaling is an HTTP handler. This
// function does not need to deal with HTTP request matching
// which is abstracted away. Since writing HTTP handlers
// with Caddyfile support is very common, this is a more
// convenient way to add a handler to the chain since a lot
// of the details common to HTTP handlers are taken care of
// 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
+237
View File
@@ -0,0 +1,237 @@
package httpcaddyfile
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 {
input string
expect int
}{
{"", 0},
{"*", 0},
{"*.*", 1},
{"{placeholder}", 0},
{"/{placeholder}", 1},
{"foo", 3},
{"example.com", 11},
{"a.example.com", 13},
{"*.example.com", 12},
{"/foo", 4},
{"/foo*", 4},
{"{placeholder}.example.com", 12},
{"{placeholder.example.com", 24},
{"}.", 2},
{"}{", 2},
{"{}", 0},
{"{{{}}", 1},
} {
actual := specificity(tc.input)
if actual != tc.expect {
t.Errorf("Test %d (%s): Expected %d but got %d", i, tc.input, tc.expect, actual)
}
}
}
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
}
}
}
+363
View File
@@ -0,0 +1,363 @@
// 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 (
"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 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
if !d.AllArgs(&httpPortStr) {
return 0, d.ArgErr()
}
var err error
httpPort, err = strconv.Atoi(httpPortStr)
if err != nil {
return 0, d.Errf("converting port '%s' to integer value: %v", httpPortStr, err)
}
}
return httpPort, nil
}
func parseOptHTTPSPort(d *caddyfile.Dispenser) (interface{}, error) {
var httpsPort int
for d.Next() {
var httpsPortStr string
if !d.AllArgs(&httpsPortStr) {
return 0, d.ArgErr()
}
var err error
httpsPort, err = strconv.Atoi(httpsPortStr)
if err != nil {
return 0, d.Errf("converting port '%s' to integer value: %v", httpsPortStr, err)
}
}
return httpsPort, nil
}
func parseOptOrder(d *caddyfile.Dispenser) (interface{}, error) {
newOrder := directiveOrder
for d.Next() {
// get directive name
if !d.Next() {
return nil, d.ArgErr()
}
dirName := d.Val()
if _, ok := registeredDirectives[dirName]; !ok {
return nil, d.Errf("%s is not a registered directive", dirName)
}
// get positional token
if !d.Next() {
return nil, d.ArgErr()
}
pos := d.Val()
// if directive exists, first remove it
for i, d := range newOrder {
if d == dirName {
newOrder = append(newOrder[:i], newOrder[i+1:]...)
break
}
}
// act on the positional
switch pos {
case "first":
newOrder = append([]string{dirName}, newOrder...)
if d.NextArg() {
return nil, d.ArgErr()
}
directiveOrder = newOrder
return newOrder, nil
case "last":
newOrder = append(newOrder, dirName)
if d.NextArg() {
return nil, d.ArgErr()
}
directiveOrder = newOrder
return newOrder, nil
case "before":
case "after":
default:
return nil, d.Errf("unknown positional '%s'", pos)
}
// get name of other directive
if !d.NextArg() {
return nil, d.ArgErr()
}
otherDir := d.Val()
if d.NextArg() {
return nil, d.ArgErr()
}
// insert directive into proper position
for i, d := range newOrder {
if d == otherDir {
if pos == "before" {
newOrder = append(newOrder[:i], append([]string{dirName}, newOrder[i:]...)...)
} else if pos == "after" {
newOrder = append(newOrder[:i+1], append([]string{dirName}, newOrder[i+1:]...)...)
}
break
}
}
}
directiveOrder = newOrder
return newOrder, nil
}
func parseOptStorage(d *caddyfile.Dispenser) (interface{}, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
if !d.Next() { // get storage module name
return nil, d.ArgErr()
}
modName := d.Val()
mod, err := caddy.GetModule("caddy.storage." + modName)
if err != nil {
return nil, d.Errf("getting storage module '%s': %v", modName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, d.Errf("storage module '%s' is not a Caddyfile unmarshaler", mod.ID)
}
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
if err != nil {
return nil, err
}
storage, ok := unm.(caddy.StorageConverter)
if !ok {
return nil, d.Errf("module %s is not a StorageConverter", mod.ID)
}
return storage, nil
}
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()
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 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
}
+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
}
+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)
},
}
-260
View File
@@ -1,260 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 (
"errors"
"fmt"
"io"
"strings"
)
// Dispenser is a type that dispenses tokens, similarly to a lexer,
// except that it can do so with some notion of structure and has
// some really convenient methods.
type Dispenser struct {
filename string
tokens []Token
cursor int
nesting int
}
// NewDispenser returns a Dispenser, ready to use for parsing the given input.
func NewDispenser(filename string, input io.Reader) Dispenser {
tokens, _ := allTokens(input) // ignoring error because nothing to do with it
return Dispenser{
filename: filename,
tokens: tokens,
cursor: -1,
}
}
// NewDispenserTokens returns a Dispenser filled with the given tokens.
func NewDispenserTokens(filename string, tokens []Token) Dispenser {
return Dispenser{
filename: filename,
tokens: tokens,
cursor: -1,
}
}
// Next loads the next token. Returns true if a token
// was loaded; false otherwise. If false, all tokens
// have been consumed.
func (d *Dispenser) Next() bool {
if d.cursor < len(d.tokens)-1 {
d.cursor++
return true
}
return false
}
// NextArg loads the next token if it is on the same
// line. Returns true if a token was loaded; false
// otherwise. If false, all tokens on the line have
// been consumed. It handles imported tokens correctly.
func (d *Dispenser) NextArg() bool {
if d.cursor < 0 {
d.cursor++
return true
}
if d.cursor >= len(d.tokens) {
return false
}
if d.cursor < len(d.tokens)-1 &&
d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
d.cursor++
return true
}
return false
}
// NextLine loads the next token only if it is not on the same
// line as the current token, and returns true if a token was
// loaded; false otherwise. If false, there is not another token
// or it is on the same line. It handles imported tokens correctly.
func (d *Dispenser) NextLine() bool {
if d.cursor < 0 {
d.cursor++
return true
}
if d.cursor >= len(d.tokens) {
return false
}
if d.cursor < len(d.tokens)-1 &&
(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
d.cursor++
return true
}
return false
}
// NextBlock can be used as the condition of a for loop
// to load the next token as long as it opens a block or
// is already in a block. It returns true if a token was
// loaded, or false when the block's closing curly brace
// was loaded and thus the block ended. Nested blocks are
// not supported.
func (d *Dispenser) NextBlock() bool {
if d.nesting > 0 {
d.Next()
if d.Val() == "}" {
d.nesting--
return false
}
return true
}
if !d.NextArg() { // block must open on same line
return false
}
if d.Val() != "{" {
d.cursor-- // roll back if not opening brace
return false
}
d.Next()
if d.Val() == "}" {
// Open and then closed right away
return false
}
d.nesting++
return true
}
// Val gets the text of the current token. If there is no token
// loaded, it returns empty string.
func (d *Dispenser) Val() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return ""
}
return d.tokens[d.cursor].Text
}
// Line gets the line number of the current token. If there is no token
// loaded, it returns 0.
func (d *Dispenser) Line() int {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return 0
}
return d.tokens[d.cursor].Line
}
// File gets the filename of the current token. If there is no token loaded,
// it returns the filename originally given when parsing started.
func (d *Dispenser) File() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return d.filename
}
if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" {
return tokenFilename
}
return d.filename
}
// Args is a convenience function that loads the next arguments
// (tokens on the same line) into an arbitrary number of strings
// pointed to in targets. If there are fewer tokens available
// than string pointers, the remaining strings will not be changed
// and false will be returned. If there were enough tokens available
// to fill the arguments, then true will be returned.
func (d *Dispenser) Args(targets ...*string) bool {
enough := true
for i := 0; i < len(targets); i++ {
if !d.NextArg() {
enough = false
break
}
*targets[i] = d.Val()
}
return enough
}
// RemainingArgs loads any more arguments (tokens on the same line)
// into a slice and returns them. Open curly brace tokens also indicate
// the end of arguments, and the curly brace is not included in
// the return value nor is it loaded.
func (d *Dispenser) RemainingArgs() []string {
var args []string
for d.NextArg() {
if d.Val() == "{" {
d.cursor--
break
}
args = append(args, d.Val())
}
return args
}
// ArgErr returns an argument error, meaning that another
// argument was expected but not found. In other words,
// a line break or open curly brace was encountered instead of
// an argument.
func (d *Dispenser) ArgErr() error {
if d.Val() == "{" {
return d.Err("Unexpected token '{', expecting argument")
}
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
}
// SyntaxErr creates a generic syntax error which explains what was
// found and what was expected.
func (d *Dispenser) SyntaxErr(expected string) error {
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
return errors.New(msg)
}
// EOFErr returns an error indicating that the dispenser reached
// the end of the input when searching for the next token.
func (d *Dispenser) EOFErr() error {
return d.Errf("Unexpected EOF")
}
// Err generates a custom parse-time error with a message of msg.
func (d *Dispenser) Err(msg string) error {
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
return errors.New(msg)
}
// Errf is like Err, but for formatted error messages
func (d *Dispenser) Errf(format string, args ...interface{}) error {
return d.Err(fmt.Sprintf(format, args...))
}
// numLineBreaks counts how many line breaks are in the token
// value given by the token index tknIdx. It returns 0 if the
// token does not exist or there are no line breaks.
func (d *Dispenser) numLineBreaks(tknIdx int) int {
if tknIdx < 0 || tknIdx >= len(d.tokens) {
return 0
}
return strings.Count(d.tokens[tknIdx].Text, "\n")
}
// isNewLine determines whether the current token is on a different
// line (higher line number) than the previous token. It handles imported
// tokens correctly. If there isn't a previous token, it returns true.
func (d *Dispenser) isNewLine() bool {
if d.cursor < 1 {
return true
}
if d.cursor > len(d.tokens)-1 {
return false
}
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
}
-198
View File
@@ -1,198 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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"
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
)
const filename = "Caddyfile"
// ToJSON converts caddyfile to its JSON representation.
func ToJSON(caddyfile []byte) ([]byte, error) {
var j EncodedCaddyfile
serverBlocks, err := Parse(filename, bytes.NewReader(caddyfile), nil)
if err != nil {
return nil, err
}
for _, sb := range serverBlocks {
block := EncodedServerBlock{
Keys: sb.Keys,
Body: [][]interface{}{},
}
// Extract directives deterministically by sorting them
var directives = make([]string, len(sb.Tokens))
for dir := range sb.Tokens {
directives = append(directives, dir)
}
sort.Strings(directives)
// Convert each directive's tokens into our JSON structure
for _, dir := range directives {
disp := NewDispenserTokens(filename, sb.Tokens[dir])
for disp.Next() {
block.Body = append(block.Body, constructLine(&disp))
}
}
// tack this block onto the end of the list
j = append(j, block)
}
result, err := json.Marshal(j)
if err != nil {
return nil, err
}
return result, nil
}
// constructLine transforms tokens into a JSON-encodable structure;
// but only one line at a time, to be used at the top-level of
// a server block only (where the first token on each line is a
// directive) - not to be used at any other nesting level.
func constructLine(d *Dispenser) []interface{} {
var args []interface{}
args = append(args, d.Val())
for d.NextArg() {
if d.Val() == "{" {
args = append(args, constructBlock(d))
continue
}
args = append(args, d.Val())
}
return args
}
// constructBlock recursively processes tokens into a
// JSON-encodable structure. To be used in a directive's
// block. Goes to end of block.
func constructBlock(d *Dispenser) [][]interface{} {
block := [][]interface{}{}
for d.Next() {
if d.Val() == "}" {
break
}
block = append(block, constructLine(d))
}
return block
}
// FromJSON converts JSON-encoded jsonBytes to Caddyfile text
func FromJSON(jsonBytes []byte) ([]byte, error) {
var j EncodedCaddyfile
var result string
err := json.Unmarshal(jsonBytes, &j)
if err != nil {
return nil, err
}
for sbPos, sb := range j {
if sbPos > 0 {
result += "\n\n"
}
for i, key := range sb.Keys {
if i > 0 {
result += ", "
}
//result += standardizeScheme(key)
result += key
}
result += jsonToText(sb.Body, 1)
}
return []byte(result), nil
}
// jsonToText recursively transforms a scope of JSON into plain
// Caddyfile text.
func jsonToText(scope interface{}, depth int) string {
var result string
switch val := scope.(type) {
case string:
if strings.ContainsAny(val, "\" \n\t\r") {
result += `"` + strings.Replace(val, "\"", "\\\"", -1) + `"`
} else {
result += val
}
case int:
result += strconv.Itoa(val)
case float64:
result += fmt.Sprintf("%v", val)
case bool:
result += fmt.Sprintf("%t", val)
case [][]interface{}:
result += " {\n"
for _, arg := range val {
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
}
result += strings.Repeat("\t", depth-1) + "}"
case []interface{}:
for i, v := range val {
if block, ok := v.([]interface{}); ok {
result += "{\n"
for _, arg := range block {
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
}
result += strings.Repeat("\t", depth-1) + "}"
continue
}
result += jsonToText(v, depth)
if i < len(val)-1 {
result += " "
}
}
}
return result
}
// TODO: Will this function come in handy somewhere else?
/*
// standardizeScheme turns an address like host:https into https://host,
// or "host:" into "host".
func standardizeScheme(addr string) string {
if hostname, port, err := net.SplitHostPort(addr); err == nil {
if port == "http" || port == "https" {
addr = port + "://" + hostname
}
}
return strings.TrimSuffix(addr, ":")
}
*/
// EncodedCaddyfile encapsulates a slice of EncodedServerBlocks.
type EncodedCaddyfile []EncodedServerBlock
// EncodedServerBlock represents a server block ripe for encoding.
type EncodedServerBlock struct {
Keys []string `json:"keys"`
Body [][]interface{} `json:"body"`
}
-178
View File
@@ -1,178 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 "testing"
var tests = []struct {
caddyfile, json string
}{
{ // 0
caddyfile: `foo {
root /bar
}`,
json: `[{"keys":["foo"],"body":[["root","/bar"]]}]`,
},
{ // 1
caddyfile: `host1, host2 {
dir {
def
}
}`,
json: `[{"keys":["host1","host2"],"body":[["dir",[["def"]]]]}]`,
},
{ // 2
caddyfile: `host1, host2 {
dir abc {
def ghi
jkl
}
}`,
json: `[{"keys":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`,
},
{ // 3
caddyfile: `host1:1234, host2:5678 {
dir abc {
}
}`,
json: `[{"keys":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`,
},
{ // 4
caddyfile: `host {
foo "bar baz"
}`,
json: `[{"keys":["host"],"body":[["foo","bar baz"]]}]`,
},
{ // 5
caddyfile: `host, host:80 {
foo "bar \"baz\""
}`,
json: `[{"keys":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`,
},
{ // 6
caddyfile: `host {
foo "bar
baz"
}`,
json: `[{"keys":["host"],"body":[["foo","bar\nbaz"]]}]`,
},
{ // 7
caddyfile: `host {
dir 123 4.56 true
}`,
json: `[{"keys":["host"],"body":[["dir","123","4.56","true"]]}]`, // NOTE: I guess we assume numbers and booleans should be encoded as strings...?
},
{ // 8
caddyfile: `http://host, https://host {
}`,
json: `[{"keys":["http://host","https://host"],"body":[]}]`, // hosts in JSON are always host:port format (if port is specified), for consistency
},
{ // 9
caddyfile: `host {
dir1 a b
dir2 c d
}`,
json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`,
},
{ // 10
caddyfile: `host {
dir a b
dir c d
}`,
json: `[{"keys":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`,
},
{ // 11
caddyfile: `host {
dir1 a b
dir2 {
c
d
}
}`,
json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`,
},
{ // 12
caddyfile: `host1 {
dir1
}
host2 {
dir2
}`,
json: `[{"keys":["host1"],"body":[["dir1"]]},{"keys":["host2"],"body":[["dir2"]]}]`,
},
}
func TestToJSON(t *testing.T) {
for i, test := range tests {
output, err := ToJSON([]byte(test.caddyfile))
if err != nil {
t.Errorf("Test %d: %v", i, err)
}
if string(output) != test.json {
t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.json, string(output))
}
}
}
func TestFromJSON(t *testing.T) {
for i, test := range tests {
output, err := FromJSON([]byte(test.json))
if err != nil {
t.Errorf("Test %d: %v", i, err)
}
if string(output) != test.caddyfile {
t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.caddyfile, string(output))
}
}
}
// TODO: Will these tests come in handy somewhere else?
/*
func TestStandardizeAddress(t *testing.T) {
// host:https should be converted to https://host
output, err := ToJSON([]byte(`host:https`))
if err != nil {
t.Fatal(err)
}
if expected, actual := `[{"keys":["https://host"],"body":[]}]`, string(output); expected != actual {
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
}
output, err = FromJSON([]byte(`[{"keys":["https://host"],"body":[]}]`))
if err != nil {
t.Fatal(err)
}
if expected, actual := "https://host {\n}", string(output); expected != actual {
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
}
// host: should be converted to just host
output, err = ToJSON([]byte(`host:`))
if err != nil {
t.Fatal(err)
}
if expected, actual := `[{"keys":["host"],"body":[]}]`, string(output); expected != actual {
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
}
output, err = FromJSON([]byte(`[{"keys":["host:"],"body":[]}]`))
if err != nil {
t.Fatal(err)
}
if expected, actual := "host {\n}", string(output); expected != actual {
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
}
}
*/
-209
View File
@@ -1,209 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 basicauth implements HTTP Basic Authentication for Caddy.
//
// This is useful for simple protections on a website, like requiring
// a password to access an admin interface. This package assumes a
// fairly small threat model.
package basicauth
import (
"bufio"
"context"
"crypto/sha1"
"crypto/subtle"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
"github.com/jimstudt/http-authentication/basic"
)
// BasicAuth is middleware to protect resources with a username and password.
// Note that HTTP Basic Authentication is not secure by itself and should
// not be used to protect important assets without HTTPS. Even then, the
// security of HTTP Basic Auth is disputed. Use discretion when deciding
// what to protect with BasicAuth.
type BasicAuth struct {
Next httpserver.Handler
SiteRoot string
Rules []Rule
}
// ServeHTTP implements the httpserver.Handler interface.
func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
var protected, isAuthenticated bool
var realm string
var username string
var password string
var ok bool
// do not check for basic auth on OPTIONS call
if r.Method == http.MethodOptions {
// Pass-through when no paths match
return a.Next.ServeHTTP(w, r)
}
for _, rule := range a.Rules {
for _, res := range rule.Resources {
if !httpserver.Path(r.URL.Path).Matches(res) {
continue
}
// path matches; this endpoint is protected
protected = true
realm = rule.Realm
// parse auth header
username, password, ok = r.BasicAuth()
// check credentials
if !ok ||
username != rule.Username ||
!rule.Password(password) {
continue
}
// by this point, authentication was successful
isAuthenticated = true
// let upstream middleware (e.g. fastcgi and cgi) know about authenticated
// user; this replaces the request with a wrapped instance
r = r.WithContext(context.WithValue(r.Context(),
httpserver.RemoteUserCtxKey, username))
// Provide username to be used in log by replacer
repl := httpserver.NewReplacer(r, nil, "-")
repl.Set("user", username)
}
}
if protected && !isAuthenticated {
// browsers show a message that says something like:
// "The website says: <realm>"
// which is kinda dumb, but whatever.
if realm == "" {
realm = "Restricted"
}
w.Header().Set("WWW-Authenticate", "Basic realm=\""+realm+"\"")
// Get a replacer so we can provide basic info for the authentication error.
repl := httpserver.NewReplacer(r, nil, "-")
repl.Set("user", username)
errstr := repl.Replace("BasicAuth: user \"{user}\" was not found or password was incorrect. {remote} {host} {uri} {proto}")
err := fmt.Errorf("%s", errstr)
return http.StatusUnauthorized, err
}
// Pass-through when no paths match
return a.Next.ServeHTTP(w, r)
}
// Rule represents a BasicAuth rule. A username and password
// combination protect the associated resources, which are
// file or directory paths.
type Rule struct {
Username string
Password func(string) bool
Resources []string
Realm string // See RFC 1945 and RFC 2617, default: "Restricted"
}
// PasswordMatcher determines whether a password matches a rule.
type PasswordMatcher func(pw string) bool
var (
htpasswords map[string]map[string]PasswordMatcher
htpasswordsMu sync.Mutex
)
// GetHtpasswdMatcher matches password rules.
func GetHtpasswdMatcher(filename, username, siteRoot string) (PasswordMatcher, error) {
filename = filepath.Join(siteRoot, filename)
htpasswordsMu.Lock()
if htpasswords == nil {
htpasswords = make(map[string]map[string]PasswordMatcher)
}
pm := htpasswords[filename]
if pm == nil {
fh, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("open %q: %v", filename, err)
}
defer fh.Close()
pm = make(map[string]PasswordMatcher)
if err = parseHtpasswd(pm, fh); err != nil {
return nil, fmt.Errorf("parsing htpasswd %q: %v", fh.Name(), err)
}
htpasswords[filename] = pm
}
htpasswordsMu.Unlock()
if pm[username] == nil {
return nil, fmt.Errorf("username %q not found in %q", username, filename)
}
return pm[username], nil
}
func parseHtpasswd(pm map[string]PasswordMatcher, r io.Reader) error {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.IndexByte(line, '#') == 0 {
continue
}
i := strings.IndexByte(line, ':')
if i <= 0 {
return fmt.Errorf("malformed line, no color: %q", line)
}
user, encoded := line[:i], line[i+1:]
for _, p := range basic.DefaultSystems {
matcher, err := p(encoded)
if err != nil {
return err
}
if matcher != nil {
pm[user] = matcher.MatchesPassword
break
}
}
}
return scanner.Err()
}
// PlainMatcher returns a PasswordMatcher that does a constant-time
// byte comparison against the password passw.
func PlainMatcher(passw string) PasswordMatcher {
// compare hashes of equal length instead of actual password
// to avoid leaking password length
passwHash := sha1.New()
if _, err := passwHash.Write([]byte(passw)); err != nil {
log.Printf("[ERROR] unable to write password hash: %v", err)
}
passwSum := passwHash.Sum(nil)
return func(pw string) bool {
pwHash := sha1.New()
if _, err := pwHash.Write([]byte(pw)); err != nil {
log.Printf("[ERROR] unable to write password hash: %v", err)
}
pwSum := pwHash.Sum(nil)
return subtle.ConstantTimeCompare([]byte(pwSum), []byte(passwSum)) == 1
}
}
-230
View File
@@ -1,230 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 basicauth
import (
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestBasicAuth(t *testing.T) {
var i int
// This handler is registered for tests in which the only authorized user is
// "okuser"
upstreamHandler := func(w http.ResponseWriter, r *http.Request) (int, error) {
remoteUser, _ := r.Context().Value(httpserver.RemoteUserCtxKey).(string)
if remoteUser != "okuser" {
t.Errorf("Test %d: expecting remote user 'okuser', got '%s'", i, remoteUser)
}
return http.StatusOK, nil
}
rws := []BasicAuth{
{
Next: httpserver.HandlerFunc(upstreamHandler),
Rules: []Rule{
{Username: "okuser", Password: PlainMatcher("okpass"),
Resources: []string{"/testing"}, Realm: "Resources"},
},
},
{
Next: httpserver.HandlerFunc(upstreamHandler),
Rules: []Rule{
{Username: "okuser", Password: PlainMatcher("okpass"),
Resources: []string{"/testing"}},
},
},
}
type testType struct {
from string
result int
user string
password string
haserror bool
}
tests := []testType{
{"/testing", http.StatusOK, "okuser", "okpass", false},
{"/testing", http.StatusUnauthorized, "baduser", "okpass", true},
{"/testing", http.StatusUnauthorized, "okuser", "badpass", true},
{"/testing", http.StatusUnauthorized, "OKuser", "okpass", true},
{"/testing", http.StatusUnauthorized, "OKuser", "badPASS", true},
{"/testing", http.StatusUnauthorized, "", "okpass", true},
{"/testing", http.StatusUnauthorized, "okuser", "", true},
{"/testing", http.StatusUnauthorized, "", "", true},
}
var test testType
for _, rw := range rws {
expectRealm := rw.Rules[0].Realm
if expectRealm == "" {
expectRealm = "Restricted" // Default if Realm not specified in rule
}
for i, test = range tests {
req, err := http.NewRequest("GET", test.from, nil)
if err != nil {
t.Fatalf("Test %d: Could not create HTTP request: %v", i, err)
}
req.SetBasicAuth(test.user, test.password)
rec := httptest.NewRecorder()
result, err := rw.ServeHTTP(rec, req)
if err != nil {
if !test.haserror || !strings.HasPrefix(err.Error(), "BasicAuth: user") {
t.Fatalf("Test %d: Could not ServeHTTP: %v", i, err)
}
}
if result != test.result {
t.Errorf("Test %d: Expected status code %d but was %d",
i, test.result, result)
}
if test.result == http.StatusUnauthorized {
headers := rec.Header()
if val, ok := headers["Www-Authenticate"]; ok {
if got, want := val[0], "Basic realm=\""+expectRealm+"\""; got != want {
t.Errorf("Test %d: Www-Authenticate header should be '%s', got: '%s'", i, want, got)
}
} else {
t.Errorf("Test %d: response should have a 'Www-Authenticate' header", i)
}
} else {
if req.Header.Get("Authorization") == "" {
// see issue #1508: https://github.com/caddyserver/caddy/issues/1508
t.Errorf("Test %d: Expected Authorization header to be retained after successful auth, but was empty", i)
}
}
}
}
}
func TestMultipleOverlappingRules(t *testing.T) {
rw := BasicAuth{
Next: httpserver.HandlerFunc(contentHandler),
Rules: []Rule{
{Username: "t", Password: PlainMatcher("p1"), Resources: []string{"/t"}},
{Username: "t1", Password: PlainMatcher("p2"), Resources: []string{"/t/t"}},
},
}
tests := []struct {
from string
result int
cred string
haserror bool
}{
{"/t", http.StatusOK, "t:p1", false},
{"/t/t", http.StatusOK, "t:p1", false},
{"/t/t", http.StatusOK, "t1:p2", false},
{"/a", http.StatusOK, "t1:p2", false},
{"/t/t", http.StatusUnauthorized, "t1:p3", true},
{"/t", http.StatusUnauthorized, "t1:p2", true},
}
for i, test := range tests {
req, err := http.NewRequest("GET", test.from, nil)
if err != nil {
t.Fatalf("Test %d: Could not create HTTP request %v", i, err)
}
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(test.cred))
req.Header.Set("Authorization", auth)
rec := httptest.NewRecorder()
result, err := rw.ServeHTTP(rec, req)
if err != nil {
if !test.haserror || !strings.HasPrefix(err.Error(), "BasicAuth: user") {
t.Fatalf("Test %d: Could not ServeHTTP %v", i, err)
}
}
if result != test.result {
t.Errorf("Test %d: Expected Header '%d' but was '%d'",
i, test.result, result)
}
}
}
func contentHandler(w http.ResponseWriter, r *http.Request) (int, error) {
fmt.Fprintf(w, r.URL.String())
return http.StatusOK, nil
}
func TestHtpasswd(t *testing.T) {
htpasswdPasswd := "IedFOuGmTpT8"
htpasswdFile := `sha1:{SHA}dcAUljwz99qFjYR0YLTXx0RqLww=
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
htfh, err := ioutil.TempFile("", "basicauth-")
if err != nil {
t.Skip("Error creating temp file, will skip htpassword test")
return
}
defer os.Remove(htfh.Name())
if _, err = htfh.Write([]byte(htpasswdFile)); err != nil {
t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err)
}
htfh.Close()
for i, username := range []string{"sha1", "md5"} {
rule := Rule{Username: username, Resources: []string{"/testing"}}
siteRoot := filepath.Dir(htfh.Name())
filename := filepath.Base(htfh.Name())
if rule.Password, err = GetHtpasswdMatcher(filename, rule.Username, siteRoot); err != nil {
t.Fatalf("GetHtpasswdMatcher(%q, %q): %v", htfh.Name(), rule.Username, err)
}
t.Logf("%d. username=%q", i, rule.Username)
if !rule.Password(htpasswdPasswd) || rule.Password(htpasswdPasswd+"!") {
t.Errorf("%d (%s) password does not match.", i, rule.Username)
}
}
}
func TestOptionsMethod(t *testing.T) {
rw := BasicAuth{
Next: httpserver.HandlerFunc(contentHandler),
Rules: []Rule{
{Username: "username", Password: PlainMatcher("password"), Resources: []string{"/testing"}},
},
}
req, err := http.NewRequest(http.MethodOptions, "/testing", nil)
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
// add basic auth with invalid username
// and password to make sure basic auth is ignored
req.SetBasicAuth("invaliduser", "invalidpassword")
rec := httptest.NewRecorder()
result, err := rw.ServeHTTP(rec, req)
if err != nil {
t.Fatalf("Could not ServeHTTP: %v", err)
}
if result != http.StatusOK {
t.Errorf("Expected status code %d but was %d", http.StatusOK, result)
}
}
-113
View File
@@ -1,113 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 basicauth
import (
"strings"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func init() {
caddy.RegisterPlugin("basicauth", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
// setup configures a new BasicAuth middleware instance.
func setup(c *caddy.Controller) error {
cfg := httpserver.GetConfig(c)
root := cfg.Root
rules, err := basicAuthParse(c)
if err != nil {
return err
}
basic := BasicAuth{Rules: rules}
cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
basic.Next = next
basic.SiteRoot = root
return basic
})
return nil
}
func basicAuthParse(c *caddy.Controller) ([]Rule, error) {
var rules []Rule
cfg := httpserver.GetConfig(c)
var err error
for c.Next() {
var rule Rule
args := c.RemainingArgs()
switch len(args) {
case 2:
rule.Username = args[0]
if rule.Password, err = passwordMatcher(rule.Username, args[1], cfg.Root); err != nil {
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
}
case 3:
rule.Resources = append(rule.Resources, args[0])
rule.Username = args[1]
if rule.Password, err = passwordMatcher(rule.Username, args[2], cfg.Root); err != nil {
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
}
default:
return rules, c.ArgErr()
}
// If nested block is present, process it here
for c.NextBlock() {
val := c.Val()
args = c.RemainingArgs()
switch len(args) {
case 0:
// Assume single argument is path resource
rule.Resources = append(rule.Resources, val)
case 1:
if val == "realm" {
if rule.Realm == "" {
rule.Realm = strings.Replace(args[0], `"`, `\"`, -1)
} else {
return rules, c.Errf("\"realm\" subdirective can only be specified once")
}
} else {
return rules, c.Errf("expecting \"realm\", got \"%s\"", val)
}
default:
return rules, c.ArgErr()
}
}
rules = append(rules, rule)
}
return rules, nil
}
func passwordMatcher(username, passw, siteRoot string) (PasswordMatcher, error) {
htpasswdPrefix := "htpasswd="
if !strings.HasPrefix(passw, htpasswdPrefix) {
return PlainMatcher(passw), nil
}
return GetHtpasswdMatcher(passw[len(htpasswdPrefix):], username, siteRoot)
}
-178
View File
@@ -1,178 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 basicauth
import (
"fmt"
"io/ioutil"
"os"
"strings"
"testing"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("http", `basicauth user pwd`)
err := setup(c)
if err != nil {
t.Errorf("Expected no errors, but got: %v", err)
}
mids := httpserver.GetConfig(c).Middleware()
if len(mids) == 0 {
t.Fatal("Expected middleware, got 0 instead")
}
handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(BasicAuth)
if !ok {
t.Fatalf("Expected handler to be type BasicAuth, got: %#v", handler)
}
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
func TestBasicAuthParse(t *testing.T) {
htpasswdPasswd := "IedFOuGmTpT8"
htpasswdFile := `sha1:{SHA}dcAUljwz99qFjYR0YLTXx0RqLww=
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
var skipHtpassword bool
htfh, err := ioutil.TempFile(".", "basicauth-")
if err != nil {
t.Logf("Error creating temp file (%v), will skip htpassword test", err)
skipHtpassword = true
} else {
if _, err = htfh.Write([]byte(htpasswdFile)); err != nil {
t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err)
}
htfh.Close()
defer os.Remove(htfh.Name())
}
tests := []struct {
input string
shouldErr bool
password string
expected []Rule
}{
{`basicauth user pwd`, false, "pwd", []Rule{
{Username: "user"},
}},
{`basicauth user pwd {
}`, false, "pwd", []Rule{
{Username: "user"},
}},
{`basicauth /resource1 user pwd {
}`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource1"}},
}},
{`basicauth /resource1 user pwd {
realm Resources
}`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource1"}, Realm: "Resources"},
}},
{`basicauth user pwd {
/resource1
/resource2
}`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource1", "/resource2"}},
}},
{`basicauth user pwd {
/resource1
/resource2
realm "Secure resources"
}`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource1", "/resource2"}, Realm: "Secure resources"},
}},
{`basicauth user pwd {
/resource1
realm "Secure resources"
realm Extra
/resource2
}`, true, "pwd", []Rule{}},
{`basicauth user pwd {
/resource1
foo "Resources"
/resource2
}`, true, "pwd", []Rule{}},
{`basicauth /resource user pwd`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource"}},
}},
{`basicauth /res1 user1 pwd1
basicauth /res2 user2 pwd2`, false, "pwd", []Rule{
{Username: "user1", Resources: []string{"/res1"}},
{Username: "user2", Resources: []string{"/res2"}},
}},
{`basicauth user`, true, "", []Rule{}},
{`basicauth`, true, "", []Rule{}},
{`basicauth /resource user pwd asdf`, true, "", []Rule{}},
{`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []Rule{
{Username: "sha1"},
}},
}
for i, test := range tests {
actual, err := basicAuthParse(caddy.NewTestController("http", test.input))
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actual) != len(test.expected) {
t.Fatalf("Test %d expected %d rules, but got %d",
i, len(test.expected), len(actual))
}
for j, expectedRule := range test.expected {
actualRule := actual[j]
if actualRule.Username != expectedRule.Username {
t.Errorf("Test %d, rule %d: Expected username '%s', got '%s'",
i, j, expectedRule.Username, actualRule.Username)
}
if actualRule.Realm != expectedRule.Realm {
t.Errorf("Test %d, rule %d: Expected realm '%s', got '%s'",
i, j, expectedRule.Realm, actualRule.Realm)
}
if strings.Contains(test.input, "htpasswd=") && skipHtpassword {
continue
}
pwd := test.password
if len(actual) > 1 {
pwd = fmt.Sprintf("%s%d", pwd, j+1)
}
if !actualRule.Password(pwd) || actualRule.Password(test.password+"!") {
t.Errorf("Test %d, rule %d: Expected password '%v', got '%v'",
i, j, test.password, actualRule.Password(""))
}
expectedRes := fmt.Sprintf("%v", expectedRule.Resources)
actualRes := fmt.Sprintf("%v", actualRule.Resources)
if actualRes != expectedRes {
t.Errorf("Test %d, rule %d: Expected resource list %s, but got %s",
i, j, expectedRes, actualRes)
}
}
}
}
-38
View File
@@ -1,38 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 bind
import (
"testing"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetupBind(t *testing.T) {
c := caddy.NewTestController("http", `bind 1.2.3.4`)
err := setupBind(c)
if err != nil {
t.Fatalf("Expected no errors, but got: %v", err)
}
cfg := httpserver.GetConfig(c)
if got, want := cfg.ListenHost, "1.2.3.4"; got != want {
t.Errorf("Expected the config's ListenHost to be %s, was %s", want, got)
}
if got, want := cfg.TLS.Manager.ListenHost, "1.2.3.4"; got != want {
t.Errorf("Expected the TLS config's ListenHost to be %s, was %s", want, got)
}
}
-527
View File
@@ -1,527 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 browse provides middleware for listing files in a directory
// when directory path is requested instead of a specific file.
package browse
import (
"bytes"
"encoding/json"
"net/http"
"net/url"
"os"
"path"
"sort"
"strconv"
"strings"
"text/template"
"time"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
"github.com/dustin/go-humanize"
)
const (
sortByName = "name"
sortByNameDirFirst = "namedirfirst"
sortBySize = "size"
sortByTime = "time"
)
// Browse is an http.Handler that can show a file listing when
// directories in the given paths are specified.
type Browse struct {
Next httpserver.Handler
Configs []Config
IgnoreIndexes bool
}
// Config is a configuration for browsing in a particular path.
type Config struct {
PathScope string // the base path the URL must match to enable browsing
Fs staticfiles.FileServer
Variables interface{}
Template *template.Template
}
// A Listing is the context used to fill out a template.
type Listing struct {
// The name of the directory (the last element of the path).
Name string
// The full path of the request.
Path string
// Whether the parent directory is browse-able.
CanGoUp bool
// The items (files and folders) in the path.
Items []FileInfo
// The number of directories in the listing.
NumDirs int
// The number of files (items that aren't directories) in the listing.
NumFiles int
// Which sorting order is used.
Sort string
// And which order.
Order string
// If ≠0 then Items have been limited to that many elements.
ItemsLimitedTo int
// Optional custom variables for use in browse templates.
User interface{}
httpserver.Context
}
// Crumb represents part of a breadcrumb menu.
type Crumb struct {
Link, Text string
}
// Breadcrumbs returns l.Path where every element maps
// the link to the text to display.
func (l Listing) Breadcrumbs() []Crumb {
var result []Crumb
if len(l.Path) == 0 {
return result
}
// skip trailing slash
lpath := l.Path
if lpath[len(lpath)-1] == '/' {
lpath = lpath[:len(lpath)-1]
}
parts := strings.Split(lpath, "/")
for i := range parts {
txt := parts[i]
if i == 0 && parts[i] == "" {
txt = "/"
}
result = append(result, Crumb{Link: strings.Repeat("../", len(parts)-i-1), Text: txt})
}
return result
}
// FileInfo is the info about a particular file or directory
type FileInfo struct {
Name string
Size int64
URL string
ModTime time.Time
Mode os.FileMode
IsDir bool
IsSymlink bool
}
// HumanSize returns the size of the file as a human-readable string
// in IEC format (i.e. power of 2 or base 1024).
func (fi FileInfo) HumanSize() string {
return humanize.IBytes(uint64(fi.Size))
}
// HumanModTime returns the modified time of the file as a human-readable string.
func (fi FileInfo) HumanModTime(format string) string {
return fi.ModTime.Format(format)
}
// Implement sorting for Listing
type byName Listing
type byNameDirFirst Listing
type bySize Listing
type byTime Listing
// By Name
func (l byName) Len() int { return len(l.Items) }
func (l byName) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
// Treat upper and lower case equally
func (l byName) Less(i, j int) bool {
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
}
// By Name Dir First
func (l byNameDirFirst) Len() int { return len(l.Items) }
func (l byNameDirFirst) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
// Treat upper and lower case equally
func (l byNameDirFirst) Less(i, j int) bool {
// if both are dir or file sort normally
if l.Items[i].IsDir == l.Items[j].IsDir {
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
}
// always sort dir ahead of file
return l.Items[i].IsDir
}
// By Size
func (l bySize) Len() int { return len(l.Items) }
func (l bySize) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
const directoryOffset = -1 << 31 // = math.MinInt32
func (l bySize) Less(i, j int) bool {
iSize, jSize := l.Items[i].Size, l.Items[j].Size
// Directory sizes depend on the filesystem implementation,
// which is opaque to a visitor, and should indeed does not change if the operator chooses to change the fs.
// For a consistent user experience directories are pulled to the front…
if l.Items[i].IsDir {
iSize = directoryOffset
}
if l.Items[j].IsDir {
jSize = directoryOffset
}
// … and sorted by name.
if l.Items[i].IsDir && l.Items[j].IsDir {
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
}
return iSize < jSize
}
// By Time
func (l byTime) Len() int { return len(l.Items) }
func (l byTime) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
func (l byTime) Less(i, j int) bool { return l.Items[i].ModTime.Before(l.Items[j].ModTime) }
// Add sorting method to "Listing"
// it will apply what's in ".Sort" and ".Order"
func (l Listing) applySort() {
// Check '.Order' to know how to sort
if l.Order == "desc" {
switch l.Sort {
case sortByName:
sort.Sort(sort.Reverse(byName(l)))
case sortByNameDirFirst:
sort.Sort(sort.Reverse(byNameDirFirst(l)))
case sortBySize:
sort.Sort(sort.Reverse(bySize(l)))
case sortByTime:
sort.Sort(sort.Reverse(byTime(l)))
default:
// If not one of the above, do nothing
return
}
} else { // If we had more Orderings we could add them here
switch l.Sort {
case sortByName:
sort.Sort(byName(l))
case sortByNameDirFirst:
sort.Sort(byNameDirFirst(l))
case sortBySize:
sort.Sort(bySize(l))
case sortByTime:
sort.Sort(byTime(l))
default:
// If not one of the above, do nothing
return
}
}
}
func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, config *Config) (Listing, bool) {
var (
fileInfos []FileInfo
dirCount, fileCount int
hasIndexFile bool
)
for _, f := range files {
name := f.Name()
for _, indexName := range config.Fs.IndexPages {
if name == indexName {
hasIndexFile = true
break
}
}
isDir := f.IsDir() || isSymlinkTargetDir(f, urlPath, config)
if isDir {
name += "/"
dirCount++
} else {
fileCount++
}
if config.Fs.IsHidden(f) {
continue
}
u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
fileInfos = append(fileInfos, FileInfo{
IsDir: isDir,
IsSymlink: isSymlink(f),
Name: f.Name(),
Size: f.Size(),
URL: u.String(),
ModTime: f.ModTime().UTC(),
Mode: f.Mode(),
})
}
return Listing{
Name: path.Base(urlPath),
Path: urlPath,
CanGoUp: canGoUp,
Items: fileInfos,
NumDirs: dirCount,
NumFiles: fileCount,
}, hasIndexFile
}
// isSymlink return true if f is a symbolic link
func isSymlink(f os.FileInfo) bool {
return f.Mode()&os.ModeSymlink != 0
}
// isSymlinkTargetDir return true if f's symbolic link target
// is a directory. Return false if not a symbolic link.
func isSymlinkTargetDir(f os.FileInfo, urlPath string, config *Config) bool {
if !isSymlink(f) {
return false
}
// a bit strange, but we want Stat thru the jailed filesystem to be safe
target, err := config.Fs.Root.Open(path.Join(urlPath, f.Name()))
if err != nil {
return false
}
defer target.Close()
targetInfo, err := target.Stat()
if err != nil {
return false
}
return targetInfo.IsDir()
}
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
// If so, control is handed over to ServeListing.
func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
// See if there's a browse configuration to match the path
var bc *Config
for i := range b.Configs {
if httpserver.Path(r.URL.Path).Matches(b.Configs[i].PathScope) {
bc = &b.Configs[i]
break
}
}
if bc == nil {
return b.Next.ServeHTTP(w, r)
}
// Browse works on existing directories; delegate everything else
requestedFilepath, err := bc.Fs.Root.Open(r.URL.Path)
if err != nil {
switch {
case os.IsPermission(err):
return http.StatusForbidden, err
case os.IsExist(err):
return http.StatusNotFound, err
default:
return b.Next.ServeHTTP(w, r)
}
}
defer requestedFilepath.Close()
info, err := requestedFilepath.Stat()
if err != nil {
switch {
case os.IsPermission(err):
return http.StatusForbidden, err
case os.IsExist(err):
return http.StatusGone, err
default:
return b.Next.ServeHTTP(w, r)
}
}
if !info.IsDir() {
return b.Next.ServeHTTP(w, r)
}
// Do not reply to anything else because it might be nonsensical
switch r.Method {
case http.MethodGet, http.MethodHead:
// proceed, noop
case "PROPFIND", http.MethodOptions:
return http.StatusNotImplemented, nil
default:
return b.Next.ServeHTTP(w, r)
}
// Browsing navigation gets messed up if browsing a directory
// that doesn't end in "/" (which it should, anyway)
u := *r.URL
if u.Path == "" {
u.Path = "/"
}
if u.Path[len(u.Path)-1] != '/' {
u.Path += "/"
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
return http.StatusMovedPermanently, nil
}
return b.ServeListing(w, r, requestedFilepath, bc)
}
func (b Browse) loadDirectoryContents(requestedFilepath http.File, urlPath string, config *Config) (*Listing, bool, error) {
files, err := requestedFilepath.Readdir(-1)
if err != nil {
return nil, false, err
}
// Determine if user can browse up another folder
var canGoUp bool
curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/"))
for _, other := range b.Configs {
if strings.HasPrefix(curPathDir, other.PathScope) {
canGoUp = true
break
}
}
// Assemble listing of directory contents
listing, hasIndex := directoryListing(files, canGoUp, urlPath, config)
return &listing, hasIndex, nil
}
// handleSortOrder gets and stores for a Listing the 'sort' and 'order',
// and reads 'limit' if given. The latter is 0 if not given.
//
// This sets Cookies.
func (b Browse) handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, limit int, err error) {
sort, order, limitQuery := r.URL.Query().Get("sort"), r.URL.Query().Get("order"), r.URL.Query().Get("limit")
// If the query 'sort' or 'order' is empty, use defaults or any values previously saved in Cookies
switch sort {
case "":
sort = sortByNameDirFirst
if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
sort = sortCookie.Value
}
case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
http.SetCookie(w, &http.Cookie{Name: "sort", Value: sort, Path: scope, Secure: r.TLS != nil})
}
switch order {
case "":
order = "asc"
if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
order = orderCookie.Value
}
case "asc", "desc":
http.SetCookie(w, &http.Cookie{Name: "order", Value: order, Path: scope, Secure: r.TLS != nil})
}
if limitQuery != "" {
limit, err = strconv.Atoi(limitQuery)
if err != nil { // if the 'limit' query can't be interpreted as a number, return err
return
}
}
return
}
// ServeListing returns a formatted view of 'requestedFilepath' contents'.
func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFilepath http.File, bc *Config) (int, error) {
listing, containsIndex, err := b.loadDirectoryContents(requestedFilepath, r.URL.Path, bc)
if err != nil {
switch {
case os.IsPermission(err):
return http.StatusForbidden, err
case os.IsExist(err):
return http.StatusGone, err
default:
return http.StatusInternalServerError, err
}
}
if containsIndex && !b.IgnoreIndexes { // directory isn't browsable
return b.Next.ServeHTTP(w, r)
}
listing.Context = httpserver.Context{
Root: bc.Fs.Root,
Req: r,
URL: r.URL,
}
listing.User = bc.Variables
// Copy the query values into the Listing struct
var limit int
listing.Sort, listing.Order, limit, err = b.handleSortOrder(w, r, bc.PathScope)
if err != nil {
return http.StatusBadRequest, err
}
listing.applySort()
if limit > 0 && limit <= len(listing.Items) {
listing.Items = listing.Items[:limit]
listing.ItemsLimitedTo = limit
}
var buf *bytes.Buffer
acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ","))
switch {
case strings.Contains(acceptHeader, "application/json"):
if buf, err = b.formatAsJSON(listing, bc); err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
default: // There's no 'application/json' in the 'Accept' header; browse normally
if buf, err = b.formatAsHTML(listing, bc); err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
}
_, _ = buf.WriteTo(w)
return http.StatusOK, nil
}
func (b Browse) formatAsJSON(listing *Listing, bc *Config) (*bytes.Buffer, error) {
marsh, err := json.Marshal(listing.Items)
if err != nil {
return nil, err
}
buf := new(bytes.Buffer)
_, err = buf.Write(marsh)
return buf, err
}
func (b Browse) formatAsHTML(listing *Listing, bc *Config) (*bytes.Buffer, error) {
buf := new(bytes.Buffer)
err := bc.Template.Execute(buf, listing)
return buf, err
}
-625
View File
@@ -1,625 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 browse
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"testing"
"text/template"
"time"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
)
const testDirPrefix = "caddy_browse_test"
func TestSort(t *testing.T) {
// making up []fileInfo with bogus values;
// to be used to make up our "listing"
fileInfos := []FileInfo{
{
Name: "fizz",
Size: 4,
ModTime: time.Now().AddDate(-1, 1, 0),
},
{
Name: "buzz",
Size: 2,
ModTime: time.Now().AddDate(0, -3, 3),
},
{
Name: "bazz",
Size: 1,
ModTime: time.Now().AddDate(0, -2, -23),
},
{
Name: "jazz",
Size: 3,
ModTime: time.Now(),
},
}
listing := Listing{
Name: "foobar",
Path: "/fizz/buzz",
CanGoUp: false,
Items: fileInfos,
}
// sort by name
listing.Sort = "name"
listing.applySort()
if !sort.IsSorted(byName(listing)) {
t.Errorf("The listing isn't name sorted: %v", listing.Items)
}
// sort by size
listing.Sort = "size"
listing.applySort()
if !sort.IsSorted(bySize(listing)) {
t.Errorf("The listing isn't size sorted: %v", listing.Items)
}
// sort by Time
listing.Sort = "time"
listing.applySort()
if !sort.IsSorted(byTime(listing)) {
t.Errorf("The listing isn't time sorted: %v", listing.Items)
}
// sort by name dir first
listing.Sort = "namedirfirst"
listing.applySort()
if !sort.IsSorted(byNameDirFirst(listing)) {
t.Errorf("The listing isn't namedirfirst sorted: %v", listing.Items)
}
// reverse by name
listing.Sort = "name"
listing.Order = "desc"
listing.applySort()
if !isReversed(byName(listing)) {
t.Errorf("The listing isn't reversed by name: %v", listing.Items)
}
// reverse by size
listing.Sort = "size"
listing.Order = "desc"
listing.applySort()
if !isReversed(bySize(listing)) {
t.Errorf("The listing isn't reversed by size: %v", listing.Items)
}
// reverse by time
listing.Sort = "time"
listing.Order = "desc"
listing.applySort()
if !isReversed(byTime(listing)) {
t.Errorf("The listing isn't reversed by time: %v", listing.Items)
}
// reverse by name dir first
listing.Sort = "namedirfirst"
listing.Order = "desc"
listing.applySort()
if !isReversed(byNameDirFirst(listing)) {
t.Errorf("The listing isn't reversed by namedirfirst: %v", listing.Items)
}
}
func TestBrowseHTTPMethods(t *testing.T) {
tmpl, err := template.ParseFiles("testdata/photos.tpl")
if err != nil {
t.Fatalf("An error occurred while parsing the template: %v", err)
}
b := Browse{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
return http.StatusTeapot, nil // not t.Fatalf, or we will not see what other methods yield
}),
Configs: []Config{
{
PathScope: "/photos",
Fs: staticfiles.FileServer{
Root: http.Dir("./testdata"),
},
Template: tmpl,
},
},
}
rec := httptest.NewRecorder()
for method, expected := range map[string]int{
http.MethodGet: http.StatusOK,
http.MethodHead: http.StatusOK,
http.MethodOptions: http.StatusNotImplemented,
"PROPFIND": http.StatusNotImplemented,
} {
req, err := http.NewRequest(method, "/photos/", nil)
if err != nil {
t.Fatalf("Test: Could not create HTTP request: %v", err)
}
ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL)
req = req.WithContext(ctx)
code, _ := b.ServeHTTP(rec, req)
if code != expected {
t.Errorf("Wrong status with HTTP Method %s: expected %d, got %d", method, expected, code)
}
}
}
func TestBrowseTemplate(t *testing.T) {
tmpl, err := template.ParseFiles("testdata/photos.tpl")
if err != nil {
t.Fatalf("An error occurred while parsing the template: %v", err)
}
b := Browse{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
t.Fatalf("Next shouldn't be called")
return 0, nil
}),
Configs: []Config{
{
PathScope: "/photos",
Fs: staticfiles.FileServer{
Root: http.Dir("./testdata"),
Hide: []string{"photos/hidden.html"},
},
Template: tmpl,
},
},
}
req, err := http.NewRequest("GET", "/photos/", nil)
if err != nil {
t.Fatalf("Test: Could not create HTTP request: %v", err)
}
ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL)
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
code, _ := b.ServeHTTP(rec, req)
if code != http.StatusOK {
t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, code)
}
respBody := rec.Body.String()
expectedBody := `<!DOCTYPE html>
<html>
<head>
<title>Template</title>
</head>
<body>
<h1>Header</h1>
<h1>/photos/</h1>
<a href="./test1/">test1</a><br>
<a href="./test.html">test.html</a><br>
<a href="./test2.html">test2.html</a><br>
<a href="./test3.html">test3.html</a><br>
</body>
</html>
`
if respBody != expectedBody {
t.Fatalf("Expected body: '%v' got: '%v'", expectedBody, respBody)
}
}
func TestBrowseJson(t *testing.T) {
b := Browse{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
t.Fatalf("Next shouldn't be called: %s", r.URL)
return 0, nil
}),
Configs: []Config{
{
PathScope: "/photos/",
Fs: staticfiles.FileServer{
Root: http.Dir("./testdata"),
},
},
},
}
//Getting the listing from the ./testdata/photos, the listing returned will be used to validate test results
testDataPath := filepath.Join("./testdata", "photos")
file, err := os.Open(testDataPath)
if err != nil {
if os.IsPermission(err) {
t.Fatalf("Os Permission Error")
}
}
defer file.Close()
files, err := file.Readdir(-1)
if err != nil {
t.Fatalf("Unable to Read Contents of the directory")
}
var fileinfos []FileInfo
for i, f := range files {
name := f.Name()
// Tests fail in CI environment because all file mod times are the same for
// some reason, making the sorting unpredictable. To hack around this,
// we ensure here that each file has a different mod time.
chTime := f.ModTime().UTC().Add(-(time.Duration(i) * time.Second))
if err := os.Chtimes(filepath.Join(testDataPath, name), chTime, chTime); err != nil {
t.Fatal(err)
}
if f.IsDir() {
name += "/"
}
url := url.URL{Path: "./" + name}
fileinfos = append(fileinfos, FileInfo{
IsDir: f.IsDir(),
Name: f.Name(),
Size: f.Size(),
URL: url.String(),
ModTime: chTime,
Mode: f.Mode(),
})
}
// Test that sort=name returns correct listing.
listing := Listing{Items: fileinfos} // this listing will be used for validation inside the tests
tests := []struct {
QueryURL string
SortBy string
OrderBy string
Limit int
shouldErr bool
expectedResult []FileInfo
}{
//test case 1: testing for default sort and order and without the limit parameter, default sort is by name and the default order is ascending
//without the limit query entire listing will be produced
{"/?sort=name", "", "", -1, false, listing.Items},
//test case 2: limit is set to 1, orderBy and sortBy is default
{"/?limit=1&sort=name", "", "", 1, false, listing.Items[:1]},
//test case 3 : if the listing request is bigger than total size of listing then it should return everything
{"/?limit=100000000&sort=name", "", "", 100000000, false, listing.Items},
//test case 4 : testing for negative limit
{"/?limit=-1&sort=name", "", "", -1, false, listing.Items},
//test case 5 : testing with limit set to -1 and order set to descending
{"/?limit=-1&order=desc&sort=name", "", "desc", -1, false, listing.Items},
//test case 6 : testing with limit set to 2 and order set to descending
{"/?limit=2&order=desc&sort=name", "", "desc", 2, false, listing.Items},
//test case 7 : testing with limit set to 3 and order set to descending
{"/?limit=3&order=desc&sort=name", "", "desc", 3, false, listing.Items},
//test case 8 : testing with limit set to 3 and order set to ascending
{"/?limit=3&order=asc&sort=name", "", "asc", 3, false, listing.Items},
//test case 9 : testing with limit set to 1111111 and order set to ascending
{"/?limit=1111111&order=asc&sort=name", "", "asc", 1111111, false, listing.Items},
//test case 10 : testing with limit set to default and order set to ascending and sorting by size
{"/?order=asc&sort=size&sort=name", "size", "asc", -1, false, listing.Items},
//test case 11 : testing with limit set to default and order set to ascending and sorting by last modified
{"/?order=asc&sort=time&sort=name", "time", "asc", -1, false, listing.Items},
//test case 12 : testing with limit set to 1 and order set to ascending and sorting by last modified
{"/?order=asc&sort=time&limit=1&sort=name", "time", "asc", 1, false, listing.Items},
//test case 13 : testing with limit set to -100 and order set to ascending and sorting by last modified
{"/?order=asc&sort=time&limit=-100&sort=name", "time", "asc", -100, false, listing.Items},
//test case 14 : testing with limit set to -100 and order set to ascending and sorting by size
{"/?order=asc&sort=size&limit=-100&sort=name", "size", "asc", -100, false, listing.Items},
}
for i, test := range tests {
var marsh []byte
req, err := http.NewRequest("GET", "/photos"+test.QueryURL, nil)
if err != nil && !test.shouldErr {
t.Errorf("Test %d errored when making request, but it shouldn't have; got '%v'", i, err)
}
ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL)
req = req.WithContext(ctx)
req.Header.Set("Accept", "application/json")
rec := httptest.NewRecorder()
code, err := b.ServeHTTP(rec, req)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if code != http.StatusOK {
t.Fatalf("In test %d: Wrong status, expected %d, got %d", i, http.StatusOK, code)
}
if rec.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
t.Fatalf("Expected Content type to be application/json; charset=utf-8, but got %s ", rec.HeaderMap.Get("Content-Type"))
}
actualJSONResponse := rec.Body.String()
copyOfListing := listing
if test.SortBy == "" {
copyOfListing.Sort = "name"
} else {
copyOfListing.Sort = test.SortBy
}
if test.OrderBy == "" {
copyOfListing.Order = "asc"
} else {
copyOfListing.Order = test.OrderBy
}
copyOfListing.applySort()
limit := test.Limit
if limit <= len(copyOfListing.Items) && limit > 0 {
marsh, err = json.Marshal(copyOfListing.Items[:limit])
} else { // if the 'limit' query is empty, or has the wrong value, list everything
marsh, err = json.Marshal(copyOfListing.Items)
}
if err != nil {
t.Fatalf("Unable to Marshal the listing ")
}
expectedJSON := string(marsh)
if actualJSONResponse != expectedJSON {
t.Errorf("JSON response doesn't match the expected for test number %d with sort=%s, order=%s\nExpected response %s\nActual response = %s\n",
i+1, test.SortBy, test.OrderBy, expectedJSON, actualJSONResponse)
}
}
}
// "sort" package has "IsSorted" function, but no "IsReversed";
func isReversed(data sort.Interface) bool {
n := data.Len()
for i := n - 1; i > 0; i-- {
if !data.Less(i, i-1) {
return false
}
}
return true
}
func TestBrowseRedirect(t *testing.T) {
testCases := []struct {
url string
statusCode int
returnCode int
location string
}{
{
"http://www.example.com/photos",
http.StatusMovedPermanently,
http.StatusMovedPermanently,
"http://www.example.com/photos/",
},
{
"/photos",
http.StatusMovedPermanently,
http.StatusMovedPermanently,
"/photos/",
},
}
for i, tc := range testCases {
b := Browse{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
t.Fatalf("Test %d - Next shouldn't be called", i)
return 0, nil
}),
Configs: []Config{
{
PathScope: "/photos",
Fs: staticfiles.FileServer{
Root: http.Dir("./testdata"),
},
},
},
}
req, err := http.NewRequest("GET", tc.url, nil)
if err != nil {
t.Fatalf("Test %d - could not create HTTP request: %v", i, err)
}
ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL)
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
returnCode, _ := b.ServeHTTP(rec, req)
if returnCode != tc.returnCode {
t.Fatalf("Test %d - wrong return code, expected %d, got %d",
i, tc.returnCode, returnCode)
}
if got := rec.Code; got != tc.statusCode {
t.Errorf("Test %d - wrong status, expected %d, got %d",
i, tc.statusCode, got)
}
if got := rec.Header().Get("Location"); got != tc.location {
t.Errorf("Test %d - wrong Location header, expected %s, got %s",
i, tc.location, got)
}
}
}
func TestDirSymlink(t *testing.T) {
if runtime.GOOS == "windows" {
// Windows support for symlinks is limited, and we had a hard time getting
// all these tests to pass with the permissions of CI; so just skip them
fmt.Println("Skipping browse symlink tests on Windows...")
return
}
testCases := []struct {
source string
target string
pathScope string
url string
expectedName string
expectedURL string
}{
// test case can expect a directory "dir" and a symlink to it called "symlink"
{"dir", "$TMP/rel_symlink_to_dir", "/", "/",
"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
{"$TMP/dir", "$TMP/abs_symlink_to_dir", "/", "/",
"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
{"../../dir", "$TMP/sub/dir/rel_symlink_to_dir", "/", "/sub/dir/",
"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
{"$TMP/dir", "$TMP/sub/dir/abs_symlink_to_dir", "/", "/sub/dir/",
"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
{"../../dir", "$TMP/with/scope/rel_symlink_to_dir", "/with/scope", "/with/scope/",
"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
{"$TMP/dir", "$TMP/with/scope/abs_symlink_to_dir", "/with/scope", "/with/scope/",
"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
{"../../../../dir", "$TMP/with/scope/sub/dir/rel_symlink_to_dir", "/with/scope", "/with/scope/sub/dir/",
"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
{"$TMP/dir", "$TMP/with/scope/sub/dir/abs_symlink_to_dir", "/with/scope", "/with/scope/sub/dir/",
"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
{"symlink", "$TMP/rel_symlink_to_symlink", "/", "/",
"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
{"$TMP/symlink", "$TMP/abs_symlink_to_symlink", "/", "/",
"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
{"../../symlink", "$TMP/sub/dir/rel_symlink_to_symlink", "/", "/sub/dir/",
"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
{"$TMP/symlink", "$TMP/sub/dir/abs_symlink_to_symlink", "/", "/sub/dir/",
"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
{"../../symlink", "$TMP/with/scope/rel_symlink_to_symlink", "/with/scope", "/with/scope/",
"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
{"$TMP/symlink", "$TMP/with/scope/abs_symlink_to_symlink", "/with/scope", "/with/scope/",
"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
{"../../../../symlink", "$TMP/with/scope/sub/dir/rel_symlink_to_symlink", "/with/scope", "/with/scope/sub/dir/",
"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
{"$TMP/symlink", "$TMP/with/scope/sub/dir/abs_symlink_to_symlink", "/with/scope", "/with/scope/sub/dir/",
"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
}
for i, tc := range testCases {
func() {
tmpdir, err := ioutil.TempDir("", testDirPrefix)
if err != nil {
t.Fatalf("failed to create test directory: %v", err)
}
defer os.RemoveAll(tmpdir)
if err := os.MkdirAll(filepath.Join(tmpdir, "dir"), 0755); err != nil {
t.Fatalf("failed to create test dir 'dir': %v", err)
}
if err := os.Symlink("dir", filepath.Join(tmpdir, "symlink")); err != nil {
t.Fatalf("failed to create test symlink 'symlink': %v", err)
}
sourceResolved := strings.Replace(tc.source, "$TMP", tmpdir, -1)
targetResolved := strings.Replace(tc.target, "$TMP", tmpdir, -1)
if err := os.MkdirAll(filepath.Dir(sourceResolved), 0755); err != nil {
t.Fatalf("failed to create source symlink dir: %v", err)
}
if err := os.MkdirAll(filepath.Dir(targetResolved), 0755); err != nil {
t.Fatalf("failed to create target symlink dir: %v", err)
}
if err := os.Symlink(sourceResolved, targetResolved); err != nil {
t.Fatalf("failed to create test symlink: %v", err)
}
b := Browse{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
t.Fatalf("Test %d - Next shouldn't be called", i)
return 0, nil
}),
Configs: []Config{
{
PathScope: tc.pathScope,
Fs: staticfiles.FileServer{
Root: http.Dir(tmpdir),
},
},
},
}
req, err := http.NewRequest("GET", tc.url, nil)
req.Header.Add("Accept", "application/json")
if err != nil {
t.Fatalf("Test %d - could not create HTTP request: %v", i, err)
}
rec := httptest.NewRecorder()
returnCode, _ := b.ServeHTTP(rec, req)
if returnCode != http.StatusOK {
t.Fatalf("Test %d - wrong return code, expected %d, got %d",
i, http.StatusOK, returnCode)
}
type jsonEntry struct {
Name string
IsDir bool
IsSymlink bool
URL string
}
var entries []jsonEntry
if err := json.Unmarshal(rec.Body.Bytes(), &entries); err != nil {
t.Fatalf("Test %d - failed to parse json: %v", i, err)
}
found := false
for _, e := range entries {
if e.Name != tc.expectedName {
continue
}
found = true
if !e.IsDir {
t.Errorf("Test %d - expected to be a dir, got %v", i, e.IsDir)
}
if !e.IsSymlink {
t.Errorf("Test %d - expected to be a symlink, got %v", i, e.IsSymlink)
}
if e.URL != tc.expectedURL {
t.Errorf("Test %d - wrong URL, expected %v, got %v", i, tc.expectedURL, e.URL)
}
}
if !found {
t.Errorf("Test %d - failed, could not find name %v", i, tc.expectedName)
}
}()
}
}
-95
View File
@@ -1,95 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 browse
import (
"io/ioutil"
"os"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
tempDirPath := os.TempDir()
_, err := os.Stat(tempDirPath)
if err != nil {
t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err)
}
nonExistentDirPath := filepath.Join(tempDirPath, strconv.Itoa(int(time.Now().UnixNano())))
tempTemplate, err := ioutil.TempFile(".", "tempTemplate")
if err != nil {
t.Fatalf("BeforeTest: Failed to create a temporary file in the working directory! Error was: %v", err)
}
defer os.Remove(tempTemplate.Name())
tempTemplatePath := filepath.Join(".", tempTemplate.Name())
for i, test := range []struct {
input string
expectedPathScope []string
shouldErr bool
}{
// test case #0 tests handling of multiple pathscopes
{"browse " + tempDirPath + "\n browse .", []string{tempDirPath, "."}, false},
// test case #1 tests instantiation of Config with default values
{"browse /", []string{"/"}, false},
// test case #2 tests detection of custom template
{"browse . " + tempTemplatePath, []string{"."}, false},
// test case #3 tests detection of non-existent template
{"browse . " + nonExistentDirPath, nil, true},
// test case #4 tests detection of duplicate pathscopes
{"browse " + tempDirPath + "\n browse " + tempDirPath, nil, true},
} {
c := caddy.NewTestController("http", test.input)
err := setup(c)
if err != nil && !test.shouldErr {
t.Errorf("Test case #%d received an error of %v", i, err)
}
if test.expectedPathScope == nil {
continue
}
mids := httpserver.GetConfig(c).Middleware()
mid := mids[len(mids)-1]
receivedConfigs := mid(nil).(Browse).Configs
for j, config := range receivedConfigs {
if config.PathScope != test.expectedPathScope[j] {
t.Errorf("Test case #%d expected a pathscope of %v, but got %v", i, test.expectedPathScope, config.PathScope)
}
}
}
// test case #6 tests startup with missing root directory in combination with default browse settings
controller := caddy.NewTestController("http", "browse")
cfg := httpserver.GetConfig(controller)
// Make sure non-existent root path doesn't return error
cfg.Root = nonExistentDirPath
err = setup(controller)
if err != nil {
t.Errorf("Test for non-existent browse path received an error, but shouldn't have: %v", err)
}
}
-1
View File
@@ -1 +0,0 @@
<h1>Header</h1>
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Template</title>
</head>
<body>
{{.Include "header.html"}}
<h1>{{.Path}}</h1>
{{range .Items}}
<a href="{{.URL}}">{{.Name}}</a><br>
{{end}}
</body>
</html>
-1
View File
@@ -1 +0,0 @@
Should be hidden
-8
View File
@@ -1,8 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
</body>
</html>
-8
View File
@@ -1,8 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
</body>
</html>
-8
View File
@@ -1,8 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Test 2</title>
</head>
<body>
</body>
</html>
-3
View File
@@ -1,3 +0,0 @@
<!DOCTYPE html>
<html>
</html>
-49
View File
@@ -1,49 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 (
// plug in the server
_ "github.com/caddyserver/caddy/caddyhttp/httpserver"
// plug in the standard directives
_ "github.com/caddyserver/caddy/caddyhttp/basicauth"
_ "github.com/caddyserver/caddy/caddyhttp/bind"
_ "github.com/caddyserver/caddy/caddyhttp/browse"
_ "github.com/caddyserver/caddy/caddyhttp/errors"
_ "github.com/caddyserver/caddy/caddyhttp/expvar"
_ "github.com/caddyserver/caddy/caddyhttp/extensions"
_ "github.com/caddyserver/caddy/caddyhttp/fastcgi"
_ "github.com/caddyserver/caddy/caddyhttp/gzip"
_ "github.com/caddyserver/caddy/caddyhttp/header"
_ "github.com/caddyserver/caddy/caddyhttp/index"
_ "github.com/caddyserver/caddy/caddyhttp/internalsrv"
_ "github.com/caddyserver/caddy/caddyhttp/limits"
_ "github.com/caddyserver/caddy/caddyhttp/log"
_ "github.com/caddyserver/caddy/caddyhttp/markdown"
_ "github.com/caddyserver/caddy/caddyhttp/mime"
_ "github.com/caddyserver/caddy/caddyhttp/pprof"
_ "github.com/caddyserver/caddy/caddyhttp/proxy"
_ "github.com/caddyserver/caddy/caddyhttp/push"
_ "github.com/caddyserver/caddy/caddyhttp/redirect"
_ "github.com/caddyserver/caddy/caddyhttp/requestid"
_ "github.com/caddyserver/caddy/caddyhttp/rewrite"
_ "github.com/caddyserver/caddy/caddyhttp/root"
_ "github.com/caddyserver/caddy/caddyhttp/status"
_ "github.com/caddyserver/caddy/caddyhttp/templates"
_ "github.com/caddyserver/caddy/caddyhttp/timeouts"
_ "github.com/caddyserver/caddy/caddyhttp/websocket"
_ "github.com/caddyserver/caddy/onevent"
)
-33
View File
@@ -1,33 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 (
"strings"
"testing"
"github.com/caddyserver/caddy"
)
// TODO: this test could be improved; the purpose is to
// ensure that the standard plugins are in fact plugged in
// and registered properly; this is a quick/naive way to do it.
func TestStandardPlugins(t *testing.T) {
numStandardPlugins := 32 // importing caddyhttp plugs in this many plugins
s := caddy.DescribePlugins()
if got, want := strings.Count(s, "\n"), numStandardPlugins+4; got != want {
t.Errorf("Expected all standard plugins to be plugged in, got:\n%s", s)
}
}
-159
View File
@@ -1,159 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 errors implements an HTTP error handling middleware.
package errors
import (
"fmt"
"io"
"net/http"
"os"
"runtime"
"strings"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func init() {
caddy.RegisterPlugin("errors", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
// ErrorHandler handles HTTP errors (and errors from other middleware).
type ErrorHandler struct {
Next httpserver.Handler
GenericErrorPage string // default error page filename
ErrorPages map[int]string // map of status code to filename
Log *httpserver.Logger
Debug bool // if true, errors are written out to client rather than to a log
}
func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
defer h.recovery(w, r)
status, err := h.Next.ServeHTTP(w, r)
if err != nil {
errMsg := fmt.Sprintf("[ERROR %d %s] %v", status, r.URL.Path, err)
if h.Debug {
// Write error to response instead of to log
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(status)
fmt.Fprintln(w, errMsg)
return 0, err // returning 0 signals that a response has been written
}
h.Log.Println(errMsg)
}
if status >= 400 {
h.errorPage(w, r, status)
return 0, err
}
return status, err
}
// errorPage serves a static error page to w according to the status
// code. If there is an error serving the error page, a plaintext error
// message is written instead, and the extra error is logged.
func (h ErrorHandler) errorPage(w http.ResponseWriter, r *http.Request, code int) {
// See if an error page for this status code was specified
if pagePath, ok := h.findErrorPage(code); ok {
// Try to open it
errorPage, err := os.Open(pagePath)
if err != nil {
// An additional error handling an error... <insert grumpy cat here>
h.Log.Printf("[NOTICE %d %s] could not load error page: %v", code, r.URL.String(), err)
httpserver.DefaultErrorFunc(w, r, code)
return
}
defer errorPage.Close()
// Copy the page body into the response
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(code)
_, err = io.Copy(w, errorPage)
if err != nil {
// Epic fail... sigh.
h.Log.Printf("[NOTICE %d %s] could not respond with %s: %v", code, r.URL.String(), pagePath, err)
httpserver.DefaultErrorFunc(w, r, code)
}
return
}
// Default error response
httpserver.DefaultErrorFunc(w, r, code)
}
func (h ErrorHandler) findErrorPage(code int) (string, bool) {
if pagePath, ok := h.ErrorPages[code]; ok {
return pagePath, true
}
if h.GenericErrorPage != "" {
return h.GenericErrorPage, true
}
return "", false
}
func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) {
rec := recover()
if rec == nil {
return
}
// Obtain source of panic
// From: https://gist.github.com/swdunlop/9629168
var name, file string // function name, file name
var line int
var pc [16]uintptr
n := runtime.Callers(3, pc[:])
for _, pc := range pc[:n] {
fn := runtime.FuncForPC(pc)
if fn == nil {
continue
}
file, line = fn.FileLine(pc)
name = fn.Name()
if !strings.HasPrefix(name, "runtime.") {
break
}
}
// Trim file path
delim := "/github.com/caddyserver/caddy/"
pkgPathPos := strings.Index(file, delim)
if pkgPathPos > -1 && len(file) > pkgPathPos+len(delim) {
file = file[pkgPathPos+len(delim):]
}
panicMsg := fmt.Sprintf("[PANIC %s] %s:%d - %v", r.URL.String(), file, line, rec)
if h.Debug {
// Write error and stack trace to the response rather than to a log
var stackBuf [4096]byte
stack := stackBuf[:runtime.Stack(stackBuf[:], false)]
httpserver.WriteTextResponse(w, http.StatusInternalServerError, fmt.Sprintf("%s\n\n%s", panicMsg, stack))
} else {
// Currently we don't use the function name, since file:line is more conventional
h.Log.Printf(panicMsg)
h.errorPage(w, r, http.StatusInternalServerError)
}
}
-272
View File
@@ -1,272 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 errors
import (
"bytes"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestErrors(t *testing.T) {
// create a temporary page
const content = "This is a error page"
path, err := createErrorPageFile("errors_test.html", content)
if err != nil {
t.Fatal(err)
}
defer os.Remove(path)
buf := bytes.Buffer{}
em := ErrorHandler{
ErrorPages: map[int]string{
http.StatusNotFound: path,
http.StatusForbidden: "not_exist_file",
},
Log: httpserver.NewTestLogger(&buf),
}
_, notExistErr := os.Open("not_exist_file")
testErr := errors.New("test error")
tests := []struct {
next httpserver.Handler
expectedCode int
expectedBody string
expectedLog string
expectedErr error
}{
{
next: genErrorHandler(http.StatusOK, nil, "normal"),
expectedCode: http.StatusOK,
expectedBody: "normal",
expectedLog: "",
expectedErr: nil,
},
{
next: genErrorHandler(http.StatusMovedPermanently, testErr, ""),
expectedCode: http.StatusMovedPermanently,
expectedBody: "",
expectedLog: fmt.Sprintf("[ERROR %d %s] %v\n", http.StatusMovedPermanently, "/", testErr),
expectedErr: testErr,
},
{
next: genErrorHandler(http.StatusBadRequest, nil, ""),
expectedCode: 0,
expectedBody: fmt.Sprintf("%d %s\n", http.StatusBadRequest,
http.StatusText(http.StatusBadRequest)),
expectedLog: "",
expectedErr: nil,
},
{
next: genErrorHandler(http.StatusNotFound, nil, ""),
expectedCode: 0,
expectedBody: content,
expectedLog: "",
expectedErr: nil,
},
{
next: genErrorHandler(http.StatusForbidden, nil, ""),
expectedCode: 0,
expectedBody: fmt.Sprintf("%d %s\n", http.StatusForbidden,
http.StatusText(http.StatusForbidden)),
expectedLog: fmt.Sprintf("[NOTICE %d /] could not load error page: %v\n",
http.StatusForbidden, notExistErr),
expectedErr: nil,
},
}
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
for i, test := range tests {
em.Next = test.next
buf.Reset()
rec := httptest.NewRecorder()
code, err := em.ServeHTTP(rec, req)
if err != test.expectedErr {
t.Errorf("Test %d: Expected error %v, but got %v",
i, test.expectedErr, err)
}
if code != test.expectedCode {
t.Errorf("Test %d: Expected status code %d, but got %d",
i, test.expectedCode, code)
}
if body := rec.Body.String(); body != test.expectedBody {
t.Errorf("Test %d: Expected body %q, but got %q",
i, test.expectedBody, body)
}
if log := buf.String(); !strings.Contains(log, test.expectedLog) {
t.Errorf("Test %d: Expected log %q, but got %q",
i, test.expectedLog, log)
}
}
}
func TestVisibleErrorWithPanic(t *testing.T) {
const panicMsg = "I'm a panic"
eh := ErrorHandler{
ErrorPages: make(map[int]string),
Debug: true,
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
panic(panicMsg)
}),
}
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
code, err := eh.ServeHTTP(rec, req)
if code != 0 {
t.Errorf("Expected error handler to return 0 (it should write to response), got status %d", code)
}
if err != nil {
t.Errorf("Expected error handler to return nil error (it should panic!), but got '%v'", err)
}
body := rec.Body.String()
if !strings.Contains(body, "[PANIC /] caddyhttp/errors/errors_test.go") {
t.Errorf("Expected response body to contain error log line, but it didn't:\n%s", body)
}
if !strings.Contains(body, panicMsg) {
t.Errorf("Expected response body to contain panic message, but it didn't:\n%s", body)
}
if len(body) < 500 {
t.Errorf("Expected response body to contain stack trace, but it was too short: len=%d", len(body))
}
}
func TestGenericErrorPage(t *testing.T) {
// create temporary generic error page
const genericErrorContent = "This is a generic error page"
genericErrorPagePath, err := createErrorPageFile("generic_error_test.html", genericErrorContent)
if err != nil {
t.Fatal(err)
}
defer os.Remove(genericErrorPagePath)
// create temporary error page
const notFoundErrorContent = "This is a error page"
notFoundErrorPagePath, err := createErrorPageFile("not_found.html", notFoundErrorContent)
if err != nil {
t.Fatal(err)
}
defer os.Remove(notFoundErrorPagePath)
buf := bytes.Buffer{}
em := ErrorHandler{
GenericErrorPage: genericErrorPagePath,
ErrorPages: map[int]string{
http.StatusNotFound: notFoundErrorPagePath,
},
Log: httpserver.NewTestLogger(&buf),
}
tests := []struct {
next httpserver.Handler
expectedCode int
expectedBody string
expectedLog string
expectedErr error
}{
{
next: genErrorHandler(http.StatusNotFound, nil, ""),
expectedCode: 0,
expectedBody: notFoundErrorContent,
expectedLog: "",
expectedErr: nil,
},
{
next: genErrorHandler(http.StatusInternalServerError, nil, ""),
expectedCode: 0,
expectedBody: genericErrorContent,
expectedLog: "",
expectedErr: nil,
},
}
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
for i, test := range tests {
em.Next = test.next
buf.Reset()
rec := httptest.NewRecorder()
code, err := em.ServeHTTP(rec, req)
if err != test.expectedErr {
t.Errorf("Test %d: Expected error %v, but got %v",
i, test.expectedErr, err)
}
if code != test.expectedCode {
t.Errorf("Test %d: Expected status code %d, but got %d",
i, test.expectedCode, code)
}
if body := rec.Body.String(); body != test.expectedBody {
t.Errorf("Test %d: Expected body %q, but got %q",
i, test.expectedBody, body)
}
if log := buf.String(); !strings.Contains(log, test.expectedLog) {
t.Errorf("Test %d: Expected log %q, but got %q",
i, test.expectedLog, log)
}
}
}
func genErrorHandler(status int, err error, body string) httpserver.Handler {
return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
if len(body) > 0 {
w.Header().Set("Content-Length", strconv.Itoa(len(body)))
fmt.Fprint(w, body)
}
return status, err
})
}
func createErrorPageFile(name string, content string) (string, error) {
errorPageFilePath := filepath.Join(os.TempDir(), name)
f, err := os.Create(errorPageFilePath)
if err != nil {
return "", err
}
_, err = f.WriteString(content)
if err != nil {
return "", err
}
f.Close()
return errorPageFilePath, nil
}
-138
View File
@@ -1,138 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 errors
import (
"log"
"os"
"path/filepath"
"strconv"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
// setup configures a new errors middleware instance.
func setup(c *caddy.Controller) error {
handler, err := errorsParse(c)
if err != nil {
return err
}
handler.Log.Attach(c)
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
handler.Next = next
return handler
})
return nil
}
func errorsParse(c *caddy.Controller) (*ErrorHandler, error) {
// Very important that we make a pointer because the startup
// function that opens the log file must have access to the
// same instance of the handler, not a copy.
handler := &ErrorHandler{
ErrorPages: make(map[int]string),
Log: &httpserver.Logger{},
}
cfg := httpserver.GetConfig(c)
optionalBlock := func() error {
for c.NextBlock() {
what := c.Val()
where := c.RemainingArgs()
if httpserver.IsLogRollerSubdirective(what) {
var err error
err = httpserver.ParseRoller(handler.Log.Roller, what, where...)
if err != nil {
return err
}
} else {
if len(where) != 1 {
return c.ArgErr()
}
where := where[0]
// Error page; ensure it exists
if !filepath.IsAbs(where) {
where = filepath.Join(cfg.Root, where)
}
f, err := os.Open(where)
if err != nil {
log.Printf("[WARNING] Unable to open error page '%s': %v", where, err)
}
f.Close()
if what == "*" {
if handler.GenericErrorPage != "" {
return c.Errf("Duplicate status code entry: %s", what)
}
handler.GenericErrorPage = where
} else {
whatInt, err := strconv.Atoi(what)
if err != nil {
return c.Err("Expecting a numeric status code or '*', got '" + what + "'")
}
if _, exists := handler.ErrorPages[whatInt]; exists {
return c.Errf("Duplicate status code entry: %s", what)
}
handler.ErrorPages[whatInt] = where
}
}
}
return nil
}
for c.Next() {
// weird hack to avoid having the handler values overwritten.
if c.Val() == "}" {
continue
}
args := c.RemainingArgs()
if len(args) == 1 {
switch args[0] {
case "visible":
handler.Debug = true
default:
handler.Log.Output = args[0]
handler.Log.Roller = httpserver.DefaultLogRoller()
}
}
if len(args) > 1 {
return handler, c.Errf("Only 1 Argument expected for errors directive")
}
// Configuration may be in a block
err := optionalBlock()
if err != nil {
return handler, err
}
}
return handler, nil
}
-204
View File
@@ -1,204 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 errors
import (
"path/filepath"
"reflect"
"testing"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("http", `errors`)
err := setup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
mids := httpserver.GetConfig(c).Middleware()
if len(mids) == 0 {
t.Fatal("Expected middlewares, was nil instead")
}
handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(*ErrorHandler)
if !ok {
t.Fatalf("Expected handler to be type ErrorHandler, got: %#v", handler)
}
expectedLogger := &httpserver.Logger{}
if !reflect.DeepEqual(expectedLogger, myHandler.Log) {
t.Errorf("Expected '%v' as the default Log, got: '%v'", expectedLogger, myHandler.Log)
}
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
// Test Startup function -- TODO
// if len(c.Startup) == 0 {
// t.Fatal("Expected 1 startup function, had 0")
// }
// c.Startup[0]()
// if myHandler.Log == nil {
// t.Error("Expected Log to be non-nil after startup because Debug is not enabled")
// }
}
func TestErrorsParse(t *testing.T) {
testAbs, err := filepath.Abs("./404.html")
if err != nil {
t.Error(err)
}
tests := []struct {
inputErrorsRules string
shouldErr bool
expectedErrorHandler ErrorHandler
}{
{`errors`, false, ErrorHandler{
ErrorPages: map[int]string{},
Log: &httpserver.Logger{},
}},
{`errors errors.txt`, false, ErrorHandler{
ErrorPages: map[int]string{},
Log: &httpserver.Logger{
Output: "errors.txt",
Roller: httpserver.DefaultLogRoller(),
},
}},
{`errors visible`, false, ErrorHandler{
ErrorPages: map[int]string{},
Debug: true,
Log: &httpserver.Logger{},
}},
{`errors errors.txt {
404 404.html
500 500.html
}`, false, ErrorHandler{
ErrorPages: map[int]string{
404: "404.html",
500: "500.html",
},
Log: &httpserver.Logger{
Output: "errors.txt",
Roller: httpserver.DefaultLogRoller(),
},
}},
{`errors errors.txt {
rotate_size 2
rotate_age 10
rotate_keep 3
rotate_compress
}`, false, ErrorHandler{
ErrorPages: map[int]string{},
Log: &httpserver.Logger{
Output: "errors.txt", Roller: &httpserver.LogRoller{
MaxSize: 2,
MaxAge: 10,
MaxBackups: 3,
Compress: true,
LocalTime: true,
},
},
}},
{`errors errors.txt {
rotate_size 3
rotate_age 11
rotate_keep 5
404 404.html
503 503.html
}`, false, ErrorHandler{
ErrorPages: map[int]string{
404: "404.html",
503: "503.html",
},
Log: &httpserver.Logger{
Output: "errors.txt",
Roller: &httpserver.LogRoller{
MaxSize: 3,
MaxAge: 11,
MaxBackups: 5,
Compress: false,
LocalTime: true,
},
},
}},
{`errors errors.txt {
* generic_error.html
404 404.html
503 503.html
}`, false, ErrorHandler{
Log: &httpserver.Logger{
Output: "errors.txt",
Roller: httpserver.DefaultLogRoller(),
},
GenericErrorPage: "generic_error.html",
ErrorPages: map[int]string{
404: "404.html",
503: "503.html",
},
}},
// test absolute file path
{`errors {
404 ` + testAbs + `
}`,
false, ErrorHandler{
ErrorPages: map[int]string{
404: testAbs,
},
Log: &httpserver.Logger{},
}},
{`errors errors.txt { rotate_size 2 rotate_age 10 rotate_keep 3 rotate_compress }`,
true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
{`errors errors.txt {
rotate_compress invalid
}`,
true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
// Next two test cases is the detection of duplicate status codes
{`errors {
503 503.html
503 503.html
}`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
{`errors {
* generic_error.html
* generic_error.html
}`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
{`errors /path error.txt {
404
}`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
{`errors /path error.txt`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
}
for i, test := range tests {
actualErrorsRule, err := errorsParse(caddy.NewTestController("http", test.inputErrorsRules))
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
} else if err != nil && test.shouldErr {
continue
}
if !reflect.DeepEqual(actualErrorsRule, &test.expectedErrorHandler) {
t.Errorf("Test %d expect %v, but got %v", i,
test.expectedErrorHandler, actualErrorsRule)
}
}
}
-59
View File
@@ -1,59 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 expvar
import (
"expvar"
"fmt"
"net/http"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
// ExpVar is a simple struct to hold expvar's configuration
type ExpVar struct {
Next httpserver.Handler
Resource Resource
}
// ServeHTTP handles requests to expvar's configured entry point with
// expvar, or passes all other requests up the chain.
func (e ExpVar) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
if httpserver.Path(r.URL.Path).Matches(string(e.Resource)) {
expvarHandler(w, r)
return 0, nil
}
return e.Next.ServeHTTP(w, r)
}
// expvarHandler returns a JSON object will all the published variables.
//
// This is lifted straight from the expvar package.
func expvarHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
fmt.Fprintf(w, "{\n")
first := true
expvar.Do(func(kv expvar.KeyValue) {
if !first {
fmt.Fprintf(w, ",\n")
}
first = false
fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
})
fmt.Fprintf(w, "\n}\n")
}
// Resource contains the path to the expvar entry point
type Resource string
-60
View File
@@ -1,60 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 expvar
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestExpVar(t *testing.T) {
rw := ExpVar{
Next: httpserver.HandlerFunc(contentHandler),
Resource: "/d/v",
}
tests := []struct {
from string
result int
}{
{"/d/v", 0},
{"/x/y", http.StatusOK},
}
for i, test := range tests {
req, err := http.NewRequest("GET", test.from, nil)
if err != nil {
t.Fatalf("Test %d: Could not create HTTP request %v", i, err)
}
rec := httptest.NewRecorder()
result, err := rw.ServeHTTP(rec, req)
if err != nil {
t.Fatalf("Test %d: Could not ServeHTTP %v", i, err)
}
if result != test.result {
t.Errorf("Test %d: Expected Header '%d' but was '%d'",
i, test.result, result)
}
}
}
func contentHandler(w http.ResponseWriter, r *http.Request) (int, error) {
fmt.Fprintf(w, r.URL.String())
return http.StatusOK, nil
}
-83
View File
@@ -1,83 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 expvar
import (
"expvar"
"runtime"
"sync"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func init() {
caddy.RegisterPlugin("expvar", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
// setup configures a new ExpVar middleware instance.
func setup(c *caddy.Controller) error {
resource, err := expVarParse(c)
if err != nil {
return err
}
// publish any extra information/metrics we may want to capture
publishExtraVars()
ev := ExpVar{Resource: resource}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
ev.Next = next
return ev
})
return nil
}
func expVarParse(c *caddy.Controller) (Resource, error) {
var resource Resource
var err error
for c.Next() {
args := c.RemainingArgs()
switch len(args) {
case 0:
resource = Resource(defaultExpvarPath)
case 1:
resource = Resource(args[0])
default:
return resource, c.ArgErr()
}
}
return resource, err
}
func publishExtraVars() {
// By using sync.Once instead of an init() function, we don't clutter
// the app's expvar export unnecessarily, or risk colliding with it.
publishOnce.Do(func() {
expvar.Publish("Goroutines", expvar.Func(func() interface{} {
return runtime.NumGoroutine()
}))
})
}
var publishOnce sync.Once // publishing variables should only be done once
var defaultExpvarPath = "/debug/vars"
-56
View File
@@ -1,56 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 expvar
import (
"testing"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("http", `expvar`)
err := setup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
mids := httpserver.GetConfig(c).Middleware()
if len(mids) == 0 {
t.Fatal("Expected middleware, got 0 instead")
}
c = caddy.NewTestController("http", `expvar /d/v`)
err = setup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
mids = httpserver.GetConfig(c).Middleware()
if len(mids) == 0 {
t.Fatal("Expected middleware, got 0 instead")
}
handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(ExpVar)
if !ok {
t.Fatalf("Expected handler to be type ExpVar, got: %#v", handler)
}
if myHandler.Resource != "/d/v" {
t.Errorf("Expected /d/v as expvar resource")
}
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
-58
View File
@@ -1,58 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 extensions contains middleware for clean URLs.
//
// The root path of the site is passed in as well as possible extensions
// to try internally for paths requested that don't match an existing
// resource. The first path+ext combination that matches a valid file
// will be used.
package extensions
import (
"net/http"
"os"
"path"
"strings"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
// Ext can assume an extension from clean URLs.
// It tries extensions in the order listed in Extensions.
type Ext struct {
// Next handler in the chain
Next httpserver.Handler
// Path to site root
Root string
// List of extensions to try
Extensions []string
}
// ServeHTTP implements the httpserver.Handler interface.
func (e Ext) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
urlpath := strings.TrimSuffix(r.URL.Path, "/")
if len(r.URL.Path) > 0 && path.Ext(urlpath) == "" && r.URL.Path[len(r.URL.Path)-1] != '/' {
for _, ext := range e.Extensions {
_, err := os.Stat(httpserver.SafePath(e.Root, urlpath) + ext)
if err == nil {
r.URL.Path = urlpath + ext
break
}
}
}
return e.Next.ServeHTTP(w, r)
}
-67
View File
@@ -1,67 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 extensions
import (
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func init() {
caddy.RegisterPlugin("ext", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
// setup configures a new instance of 'extensions' middleware for clean URLs.
func setup(c *caddy.Controller) error {
cfg := httpserver.GetConfig(c)
root := cfg.Root
exts, err := extParse(c)
if err != nil {
return err
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return Ext{
Next: next,
Extensions: exts,
Root: root,
}
})
return nil
}
// extParse sets up an instance of extension middleware
// from a middleware controller and returns a list of extensions.
func extParse(c *caddy.Controller) ([]string, error) {
var exts []string
for c.Next() {
// At least one extension is required
if !c.NextArg() {
return exts, c.ArgErr()
}
exts = append(exts, c.Val())
// Tack on any other extensions that may have been listed
exts = append(exts, c.RemainingArgs()...)
}
return exts, nil
}
-89
View File
@@ -1,89 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 extensions
import (
"testing"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("http", `ext .html .htm .php`)
err := setup(c)
if err != nil {
t.Fatalf("Expected no errors, got: %v", err)
}
mids := httpserver.GetConfig(c).Middleware()
if len(mids) == 0 {
t.Fatal("Expected middleware, had 0 instead")
}
handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(Ext)
if !ok {
t.Fatalf("Expected handler to be type Ext, got: %#v", handler)
}
if myHandler.Extensions[0] != ".html" {
t.Errorf("Expected .html in the list of Extensions")
}
if myHandler.Extensions[1] != ".htm" {
t.Errorf("Expected .htm in the list of Extensions")
}
if myHandler.Extensions[2] != ".php" {
t.Errorf("Expected .php in the list of Extensions")
}
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
func TestExtParse(t *testing.T) {
tests := []struct {
inputExts string
shouldErr bool
expectedExts []string
}{
{`ext .html .htm .php`, false, []string{".html", ".htm", ".php"}},
{`ext .php .html .xml`, false, []string{".php", ".html", ".xml"}},
{`ext .txt .php .xml`, false, []string{".txt", ".php", ".xml"}},
}
for i, test := range tests {
actualExts, err := extParse(caddy.NewTestController("http", test.inputExts))
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actualExts) != len(test.expectedExts) {
t.Fatalf("Test %d expected %d rules, but got %d",
i, len(test.expectedExts), len(actualExts))
}
for j, actualExt := range actualExts {
if actualExt != test.expectedExts[j] {
t.Fatalf("Test %d expected %dth extension to be %s , but got %s",
i, j, test.expectedExts[j], actualExt)
}
}
}
}
-497
View File
@@ -1,497 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 fastcgi has middleware that acts as a FastCGI client. Requests
// that get forwarded to FastCGI stop the middleware execution chain.
// The most common use for this package is to serve PHP websites via php-fpm.
package fastcgi
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"sync/atomic"
"time"
"crypto/tls"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy/caddytls"
)
// Handler is a middleware type that can handle requests as a FastCGI client.
type Handler struct {
Next httpserver.Handler
Rules []Rule
Root string
FileSys http.FileSystem
// These are sent to CGI scripts in env variables
SoftwareName string
SoftwareVersion string
ServerName string
ServerPort string
}
// ServeHTTP satisfies the httpserver.Handler interface.
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for _, rule := range h.Rules {
// First requirement: Base path must match request path. If it doesn't,
// we check to make sure the leading slash is not missing, and if so,
// we check again with it prepended. This is in case people forget
// a leading slash when performing rewrites, and we don't want to expose
// the contents of the (likely PHP) script. See issue #1645.
hpath := httpserver.Path(r.URL.Path)
if !hpath.Matches(rule.Path) {
if strings.HasPrefix(string(hpath), "/") {
// this is a normal-looking path, and it doesn't match; try next rule
continue
}
hpath = httpserver.Path("/" + string(hpath)) // prepend leading slash
if !hpath.Matches(rule.Path) {
// even after fixing the request path, it still doesn't match; try next rule
continue
}
}
// The path must also be allowed (not ignored).
if !rule.AllowedPath(r.URL.Path) {
continue
}
// In addition to matching the path, a request must meet some
// other criteria before being proxied as FastCGI. For example,
// we probably want to exclude static assets (CSS, JS, images...)
// but we also want to be flexible for the script we proxy to.
fpath := r.URL.Path
if idx, ok := httpserver.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok {
fpath = idx
// Index file present.
// If request path cannot be split, return error.
if !rule.canSplit(fpath) {
return http.StatusInternalServerError, ErrIndexMissingSplit
}
} else {
// No index file present.
// If request path cannot be split, ignore request.
if !rule.canSplit(fpath) {
continue
}
}
// These criteria work well in this order for PHP sites
// We lower path and Ext as on Windows, the system is case insensitive, so .PHP is served as .php
if !h.exists(fpath) || fpath[len(fpath)-1] == '/' || strings.HasSuffix(strings.ToLower(fpath), strings.ToLower(rule.Ext)) {
// Create environment for CGI script
env, err := h.buildEnv(r, rule, fpath)
if err != nil {
return http.StatusInternalServerError, err
}
// Connect to FastCGI gateway
address, err := rule.Address()
if err != nil {
return http.StatusBadGateway, err
}
network, address := parseAddress(address)
ctx := context.Background()
if rule.ConnectTimeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, rule.ConnectTimeout)
defer cancel()
}
fcgiBackend, err := DialContext(ctx, network, address)
if err != nil {
return http.StatusBadGateway, err
}
defer fcgiBackend.Close()
// read/write timeouts
if err := fcgiBackend.SetReadTimeout(rule.ReadTimeout); err != nil {
return http.StatusInternalServerError, err
}
if err := fcgiBackend.SetSendTimeout(rule.SendTimeout); err != nil {
return http.StatusInternalServerError, err
}
var resp *http.Response
var contentLength int64
// if ContentLength is already set
if r.ContentLength > 0 {
contentLength = r.ContentLength
} else {
contentLength, _ = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64)
}
switch r.Method {
case "HEAD":
resp, err = fcgiBackend.Head(env)
case "GET":
resp, err = fcgiBackend.Get(env, r.Body, contentLength)
case "OPTIONS":
resp, err = fcgiBackend.Options(env)
default:
resp, err = fcgiBackend.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength)
}
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
if err != nil {
if err, ok := err.(net.Error); ok && err.Timeout() {
return http.StatusGatewayTimeout, err
} else if err != io.EOF {
return http.StatusBadGateway, err
}
}
// Write response header
writeHeader(w, resp)
// Write the response body
_, err = io.Copy(w, resp.Body)
if err != nil {
return http.StatusBadGateway, err
}
// Log any stderr output from upstream
if fcgiBackend.stderr.Len() != 0 {
// Remove trailing newline, error logger already does this.
err = LogError(strings.TrimSuffix(fcgiBackend.stderr.String(), "\n"))
}
// Normally we would return the status code if it is an error status (>= 400),
// however, upstream FastCGI apps don't know about our contract and have
// probably already written an error page. So we just return 0, indicating
// that the response body is already written. However, we do return any
// error value so it can be logged.
// Note that the proxy middleware works the same way, returning status=0.
return 0, err
}
}
return h.Next.ServeHTTP(w, r)
}
// parseAddress returns the network and address of fcgiAddress.
// The first string is the network, "tcp" or "unix", implied from the scheme and address.
// The second string is fcgiAddress, with scheme prefixes removed.
// The two returned strings can be used as parameters to the Dial() function.
func parseAddress(fcgiAddress string) (string, string) {
// check if address has tcp scheme explicitly set
if strings.HasPrefix(fcgiAddress, "tcp://") {
return "tcp", fcgiAddress[len("tcp://"):]
}
// check if address has fastcgi scheme explicitly set
if strings.HasPrefix(fcgiAddress, "fastcgi://") {
return "tcp", fcgiAddress[len("fastcgi://"):]
}
// check if unix socket
if trim := strings.HasPrefix(fcgiAddress, "unix"); strings.HasPrefix(fcgiAddress, "/") || trim {
if trim {
return "unix", fcgiAddress[len("unix:"):]
}
return "unix", fcgiAddress
}
// default case, a plain tcp address with no scheme
return "tcp", fcgiAddress
}
func writeHeader(w http.ResponseWriter, r *http.Response) {
for key, vals := range r.Header {
for _, val := range vals {
w.Header().Add(key, val)
}
}
w.WriteHeader(r.StatusCode)
}
func (h Handler) exists(path string) bool {
if _, err := os.Stat(h.Root + path); err == nil {
return true
}
return false
}
// buildEnv returns a set of CGI environment variables for the request.
func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]string, error) {
var env map[string]string
// Separate remote IP and port; more lenient than net.SplitHostPort
var ip, port string
if idx := strings.LastIndex(r.RemoteAddr, ":"); idx > -1 {
ip = r.RemoteAddr[:idx]
port = r.RemoteAddr[idx+1:]
} else {
ip = r.RemoteAddr
}
// Remove [] from IPv6 addresses
ip = strings.Replace(ip, "[", "", 1)
ip = strings.Replace(ip, "]", "", 1)
// Split path in preparation for env variables.
// Previous rule.canSplit checks ensure this can never be -1.
splitPos := rule.splitPos(fpath)
// Request has the extension; path was split successfully
docURI := fpath[:splitPos+len(rule.SplitPath)]
pathInfo := fpath[splitPos+len(rule.SplitPath):]
scriptName := fpath
// Strip PATH_INFO from SCRIPT_NAME
scriptName = strings.TrimSuffix(scriptName, pathInfo)
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
scriptFilename := filepath.Join(rule.Root, scriptName)
// Add vhost path prefix to scriptName. Otherwise, some PHP software will
// have difficulty discovering its URL.
pathPrefix, _ := r.Context().Value(caddy.CtxKey("path_prefix")).(string)
scriptName = path.Join(pathPrefix, scriptName)
// Get the request URI from context. The context stores the original URI in case
// it was changed by a middleware such as rewrite. By default, we pass the
// original URI in as the value of REQUEST_URI (the user can overwrite this
// if desired). Most PHP apps seem to want the original URI. Besides, this is
// how nginx defaults: http://stackoverflow.com/a/12485156/1048862
reqURL, _ := r.Context().Value(httpserver.OriginalURLCtxKey).(url.URL)
// Retrieve name of remote user that was set by some downstream middleware such as basicauth.
remoteUser, _ := r.Context().Value(httpserver.RemoteUserCtxKey).(string)
requestScheme := "http"
if r.TLS != nil {
requestScheme = "https"
}
// Some variables are unused but cleared explicitly to prevent
// the parent environment from interfering.
env = map[string]string{
// Variables defined in CGI 1.1 spec
"AUTH_TYPE": "", // Not used
"CONTENT_LENGTH": r.Header.Get("Content-Length"),
"CONTENT_TYPE": r.Header.Get("Content-Type"),
"GATEWAY_INTERFACE": "CGI/1.1",
"PATH_INFO": pathInfo,
"QUERY_STRING": r.URL.RawQuery,
"REMOTE_ADDR": ip,
"REMOTE_HOST": ip, // For speed, remote host lookups disabled
"REMOTE_PORT": port,
"REMOTE_IDENT": "", // Not used
"REMOTE_USER": remoteUser,
"REQUEST_METHOD": r.Method,
"REQUEST_SCHEME": requestScheme,
"SERVER_NAME": h.ServerName,
"SERVER_PORT": h.ServerPort,
"SERVER_PROTOCOL": r.Proto,
"SERVER_SOFTWARE": h.SoftwareName + "/" + h.SoftwareVersion,
// Other variables
"DOCUMENT_ROOT": rule.Root,
"DOCUMENT_URI": docURI,
"HTTP_HOST": r.Host, // added here, since not always part of headers
"REQUEST_URI": reqURL.RequestURI(),
"SCRIPT_FILENAME": scriptFilename,
"SCRIPT_NAME": scriptName,
}
// compliance with the CGI specification requires that
// PATH_TRANSLATED should only exist if PATH_INFO is defined.
// Info: https://www.ietf.org/rfc/rfc3875 Page 14
if env["PATH_INFO"] != "" {
env["PATH_TRANSLATED"] = filepath.Join(rule.Root, pathInfo) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
}
// Some web apps rely on knowing HTTPS or not
if r.TLS != nil {
env["HTTPS"] = "on"
// and pass the protocol details in a manner compatible with apache's mod_ssl
// (which is why they have a SSL_ prefix and not TLS_).
v, ok := tlsProtocolStringToMap[r.TLS.Version]
if ok {
env["SSL_PROTOCOL"] = v
}
// and pass the cipher suite in a manner compatible with apache's mod_ssl
for k, v := range caddytls.SupportedCiphersMap {
if v == r.TLS.CipherSuite {
env["SSL_CIPHER"] = k
break
}
}
}
// Add env variables from config (with support for placeholders in values)
replacer := httpserver.NewReplacer(r, nil, "")
for _, envVar := range rule.EnvVars {
env[envVar[0]] = replacer.Replace(envVar[1])
}
// Add all HTTP headers to env variables
for field, val := range r.Header {
header := strings.ToUpper(field)
header = headerNameReplacer.Replace(header)
env["HTTP_"+header] = strings.Join(val, ", ")
}
return env, nil
}
// Rule represents a FastCGI handling rule.
// It is parsed from the fastcgi directive in the Caddyfile, see setup.go.
type Rule struct {
// The base path to match. Required.
Path string
// upstream load balancer
balancer
// Always process files with this extension with fastcgi.
Ext string
// Use this directory as the fastcgi root directory. Defaults to the root
// directory of the parent virtual host.
Root string
// The path in the URL will be split into two, with the first piece ending
// with the value of SplitPath. The first piece will be assumed as the
// actual resource (CGI script) name, and the second piece will be set to
// PATH_INFO for the CGI script to use.
SplitPath string
// If the URL ends with '/' (which indicates a directory), these index
// files will be tried instead.
IndexFiles []string
// Environment Variables
EnvVars [][2]string
// Ignored paths
IgnoredSubPaths []string
// The duration used to set a deadline when connecting to an upstream.
ConnectTimeout time.Duration
// The duration used to set a deadline when reading from the FastCGI server.
ReadTimeout time.Duration
// The duration used to set a deadline when sending to the FastCGI server.
SendTimeout time.Duration
}
// balancer is a fastcgi upstream load balancer.
type balancer interface {
// Address picks an upstream address from the
// underlying load balancer.
Address() (string, error)
}
// roundRobin is a round robin balancer for fastcgi upstreams.
type roundRobin struct {
// Known Go bug: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
// must be first field for 64 bit alignment
// on x86 and arm.
index int64
addresses []string
}
func (r *roundRobin) Address() (string, error) {
index := atomic.AddInt64(&r.index, 1) % int64(len(r.addresses))
return r.addresses[index], nil
}
// srvResolver is a private interface used to abstract
// the DNS resolver. It is mainly used to facilitate testing.
type srvResolver interface {
LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error)
}
// srv is a service locator for fastcgi upstreams
type srv struct {
resolver srvResolver
service string
}
// Address looks up the service and returns the address:port
// from first result in resolved list.
// No explicit balancing is required because net.LookupSRV
// sorts the results by priority and randomizes within priority.
func (s *srv) Address() (string, error) {
_, addrs, err := s.resolver.LookupSRV(context.Background(), "", "", s.service)
if err != nil {
return "", err
}
return fmt.Sprintf("%s:%d", strings.TrimRight(addrs[0].Target, "."), addrs[0].Port), nil
}
// canSplit checks if path can split into two based on rule.SplitPath.
func (r Rule) canSplit(path string) bool {
return r.splitPos(path) >= 0
}
// splitPos returns the index where path should be split
// based on rule.SplitPath.
func (r Rule) splitPos(path string) int {
if httpserver.CaseSensitivePath {
return strings.Index(path, r.SplitPath)
}
return strings.Index(strings.ToLower(path), strings.ToLower(r.SplitPath))
}
// AllowedPath checks if requestPath is not an ignored path.
func (r Rule) AllowedPath(requestPath string) bool {
for _, ignoredSubPath := range r.IgnoredSubPaths {
if httpserver.Path(path.Clean(requestPath)).Matches(path.Join(r.Path, ignoredSubPath)) {
return false
}
}
return true
}
var (
headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
// ErrIndexMissingSplit describes an index configuration error.
ErrIndexMissingSplit = errors.New("configured index file(s) must include split value")
)
// LogError is a non fatal error that allows requests to go through.
type LogError string
// Error satisfies error interface.
func (l LogError) Error() string {
return string(l)
}
// Map of supported protocols to Apache ssl_mod format
// Note that these are slightly different from SupportedProtocols in caddytls/config.go's
var tlsProtocolStringToMap = map[uint16]string{
tls.VersionTLS10: "TLSv1",
tls.VersionTLS11: "TLSv1.1",
tls.VersionTLS12: "TLSv1.2",
}
-424
View File
@@ -1,424 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 fastcgi
import (
"context"
"log"
"net"
"net/http"
"net/http/fcgi"
"net/http/httptest"
"net/url"
"path/filepath"
"strconv"
"sync"
"testing"
"time"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestServeHTTP(t *testing.T) {
body := "This is some test body content"
bodyLenStr := strconv.Itoa(len(body))
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Unable to create listener for test: %v", err)
}
defer func() { _ = listener.Close() }()
go func() {
err := fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", bodyLenStr)
_, err := w.Write([]byte(body))
if err != nil {
log.Printf("[ERROR] unable to write header: %v", err)
}
}))
if err != nil {
log.Printf("[ERROR] unable to start server: %v", err)
}
}()
handler := Handler{
Next: nil,
Rules: []Rule{{Path: "/", balancer: address(listener.Addr().String())}},
}
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("Unable to create request: %v", err)
}
w := httptest.NewRecorder()
status, err := handler.ServeHTTP(w, r)
if got, want := status, 0; got != want {
t.Errorf("Expected returned status code to be %d, got %d", want, got)
}
if err != nil {
t.Errorf("Expected nil error, got: %v", err)
}
if got, want := w.Header().Get("Content-Length"), bodyLenStr; got != want {
t.Errorf("Expected Content-Length to be '%s', got: '%s'", want, got)
}
if got, want := w.Body.String(), body; got != want {
t.Errorf("Expected response body to be '%s', got: '%s'", want, got)
}
}
func TestRuleParseAddress(t *testing.T) {
getClientTestTable := []struct {
rule *Rule
expectednetwork string
expectedaddress string
}{
{&Rule{balancer: address("tcp://172.17.0.1:9000")}, "tcp", "172.17.0.1:9000"},
{&Rule{balancer: address("fastcgi://localhost:9000")}, "tcp", "localhost:9000"},
{&Rule{balancer: address("172.17.0.15")}, "tcp", "172.17.0.15"},
{&Rule{balancer: address("/my/unix/socket")}, "unix", "/my/unix/socket"},
{&Rule{balancer: address("unix:/second/unix/socket")}, "unix", "/second/unix/socket"},
}
for _, entry := range getClientTestTable {
addr, err := entry.rule.Address()
if err != nil {
t.Errorf("Unexpected error in retrieving address: %s", err.Error())
}
if actualnetwork, _ := parseAddress(addr); actualnetwork != entry.expectednetwork {
t.Errorf("Unexpected network for address string %v. Got %v, expected %v", addr, actualnetwork, entry.expectednetwork)
}
if _, actualaddress := parseAddress(addr); actualaddress != entry.expectedaddress {
t.Errorf("Unexpected parsed address for address string %v. Got %v, expected %v", addr, actualaddress, entry.expectedaddress)
}
}
}
func TestRuleIgnoredPath(t *testing.T) {
rule := &Rule{
Path: "/fastcgi",
IgnoredSubPaths: []string{"/download", "/static"},
}
tests := []struct {
url string
expected bool
}{
{"/fastcgi", true},
{"/fastcgi/dl", true},
{"/fastcgi/download", false},
{"/fastcgi/download/static", false},
{"/fastcgi/static", false},
{"/fastcgi/static/download", false},
{"/fastcgi/something/download", true},
{"/fastcgi/something/static", true},
{"/fastcgi//static", false},
{"/fastcgi//static//download", false},
{"/fastcgi//download", false},
}
for i, test := range tests {
allowed := rule.AllowedPath(test.url)
if test.expected != allowed {
t.Errorf("Test %d: expected %v found %v", i, test.expected, allowed)
}
}
}
func TestBuildEnv(t *testing.T) {
testBuildEnv := func(r *http.Request, rule Rule, fpath string, envExpected map[string]string) {
var h Handler
env, err := h.buildEnv(r, rule, fpath)
if err != nil {
t.Error("Unexpected error:", err.Error())
}
for k, v := range envExpected {
if env[k] != v {
t.Errorf("Unexpected %v. Got %v, expected %v", k, env[k], v)
}
}
}
rule := Rule{
Ext: ".php",
SplitPath: ".php",
IndexFiles: []string{"index.php"},
}
u, err := url.Parse("http://localhost:2015/fgci_test.php?test=foobar")
if err != nil {
t.Error("Unexpected error:", err.Error())
}
var newReq = func() *http.Request {
r := http.Request{
Method: "GET",
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Host: "localhost:2015",
RemoteAddr: "[2b02:1810:4f2d:9400:70ab:f822:be8a:9093]:51688",
RequestURI: "/fgci_test.php",
Header: map[string][]string{
"Foo": {"Bar", "two"},
},
}
ctx := context.WithValue(r.Context(), httpserver.OriginalURLCtxKey, *r.URL)
return r.WithContext(ctx)
}
fpath := "/fgci_test.php"
var newEnv = func() map[string]string {
return map[string]string{
"REMOTE_ADDR": "2b02:1810:4f2d:9400:70ab:f822:be8a:9093",
"REMOTE_PORT": "51688",
"SERVER_PROTOCOL": "HTTP/1.1",
"QUERY_STRING": "test=foobar",
"REQUEST_METHOD": "GET",
"HTTP_HOST": "localhost:2015",
"SCRIPT_NAME": "/fgci_test.php",
}
}
// request
var r *http.Request
// expected environment variables
var envExpected map[string]string
// 1. Test for full canonical IPv6 address
r = newReq()
testBuildEnv(r, rule, fpath, envExpected)
// 2. Test for shorthand notation of IPv6 address
r = newReq()
r.RemoteAddr = "[::1]:51688"
envExpected = newEnv()
envExpected["REMOTE_ADDR"] = "::1"
testBuildEnv(r, rule, fpath, envExpected)
// 3. Test for IPv4 address
r = newReq()
r.RemoteAddr = "192.168.0.10:51688"
envExpected = newEnv()
envExpected["REMOTE_ADDR"] = "192.168.0.10"
testBuildEnv(r, rule, fpath, envExpected)
// 4. Test for environment variable
r = newReq()
rule.EnvVars = [][2]string{
{"HTTP_HOST", "localhost:2016"},
{"REQUEST_METHOD", "POST"},
}
envExpected = newEnv()
envExpected["HTTP_HOST"] = "localhost:2016"
envExpected["REQUEST_METHOD"] = "POST"
testBuildEnv(r, rule, fpath, envExpected)
// 5. Test for environment variable placeholders
r = newReq()
rule.EnvVars = [][2]string{
{"HTTP_HOST", "{host}"},
{"CUSTOM_URI", "custom_uri{uri}"},
{"CUSTOM_QUERY", "custom=true&{query}"},
}
envExpected = newEnv()
envExpected["HTTP_HOST"] = "localhost:2015"
envExpected["CUSTOM_URI"] = "custom_uri/fgci_test.php?test=foobar"
envExpected["CUSTOM_QUERY"] = "custom=true&test=foobar"
testBuildEnv(r, rule, fpath, envExpected)
// 6. Test SCRIPT_NAME includes path prefix
r = newReq()
ctx := context.WithValue(r.Context(), caddy.CtxKey("path_prefix"), "/test")
r = r.WithContext(ctx)
envExpected = newEnv()
envExpected["SCRIPT_NAME"] = "/test/fgci_test.php"
testBuildEnv(r, rule, fpath, envExpected)
// 7. Test SCRIPT_NAME,SCRIPT_FILENAME do not include PATH_INFO
fpath = "/fgci_test.php/extra/paths"
r = newReq()
envExpected = newEnv()
envExpected["PATH_INFO"] = "/extra/paths"
envExpected["SCRIPT_NAME"] = "/fgci_test.php"
envExpected["SCRIPT_FILENAME"] = filepath.FromSlash("/fgci_test.php")
testBuildEnv(r, rule, fpath, envExpected)
// 8. Test REQUEST_SCHEME in env
r = newReq()
envExpected = newEnv()
envExpected["REQUEST_SCHEME"] = "http"
testBuildEnv(r, rule, fpath, envExpected)
}
func TestReadTimeout(t *testing.T) {
tests := []struct {
sleep time.Duration
readTimeout time.Duration
shouldErr bool
}{
{75 * time.Millisecond, 50 * time.Millisecond, true},
{0, -1 * time.Second, true},
{0, time.Minute, false},
}
var wg sync.WaitGroup
for i, test := range tests {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Test %d: Unable to create listener for test: %v", i, err)
}
defer func() { _ = listener.Close() }()
handler := Handler{
Next: nil,
Rules: []Rule{
{
Path: "/",
balancer: address(listener.Addr().String()),
ReadTimeout: test.readTimeout,
},
},
}
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("Test %d: Unable to create request: %v", i, err)
}
w := httptest.NewRecorder()
wg.Add(1)
go func() {
err := fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(test.sleep)
w.WriteHeader(http.StatusOK)
wg.Done()
}))
if err != nil {
log.Printf("[ERROR] unable to start server: %v", err)
}
}()
got, err := handler.ServeHTTP(w, r)
if test.shouldErr {
if err == nil {
t.Errorf("Test %d: Expected i/o timeout error but had none", i)
} else if err, ok := err.(net.Error); !ok || !err.Timeout() {
t.Errorf("Test %d: Expected i/o timeout error, got: '%s'", i, err.Error())
}
want := http.StatusGatewayTimeout
if got != want {
t.Errorf("Test %d: Expected returned status code to be %d, got: %d",
i, want, got)
}
} else if err != nil {
t.Errorf("Test %d: Expected nil error, got: %v", i, err)
}
wg.Wait()
}
}
func TestSendTimeout(t *testing.T) {
tests := []struct {
sendTimeout time.Duration
shouldErr bool
}{
{-1 * time.Second, true},
{time.Minute, false},
}
for i, test := range tests {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Test %d: Unable to create listener for test: %v", i, err)
}
defer func() { _ = listener.Close() }()
handler := Handler{
Next: nil,
Rules: []Rule{
{
Path: "/",
balancer: address(listener.Addr().String()),
SendTimeout: test.sendTimeout,
},
},
}
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("Test %d: Unable to create request: %v", i, err)
}
w := httptest.NewRecorder()
go func() {
err := fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
if err != nil {
log.Printf("[ERROR] unable to start server: %v", err)
}
}()
got, err := handler.ServeHTTP(w, r)
if test.shouldErr {
if err == nil {
t.Errorf("Test %d: Expected i/o timeout error but had none", i)
} else if err, ok := err.(net.Error); !ok || !err.Timeout() {
t.Errorf("Test %d: Expected i/o timeout error, got: '%s'", i, err.Error())
}
want := http.StatusGatewayTimeout
if got != want {
t.Errorf("Test %d: Expected returned status code to be %d, got: %d",
i, want, got)
}
} else if err != nil {
t.Errorf("Test %d: Expected nil error, got: %v", i, err)
}
}
}
func TestBalancer(t *testing.T) {
tests := [][]string{
{"localhost", "host.local"},
{"localhost"},
{"localhost", "host.local", "example.com"},
{"localhost", "host.local", "example.com", "127.0.0.1"},
}
for i, test := range tests {
b := address(test...)
for _, host := range test {
a, err := b.Address()
if err != nil {
t.Errorf("Unexpected error in trying to retrieve address: %s", err.Error())
}
if a != host {
t.Errorf("Test %d: expected %s, found %s", i, host, a)
}
}
}
}
func address(addresses ...string) balancer {
return &roundRobin{
addresses: addresses,
index: -1,
}
}
-79
View File
@@ -1,79 +0,0 @@
<?php
ini_set("display_errors",1);
echo "resp: start\n";//.print_r($GLOBALS,1)."\n".print_r($_SERVER,1)."\n";
//echo print_r($_SERVER,1)."\n";
$length = 0;
$stat = "PASSED";
$ret = "[";
if (count($_POST) || count($_FILES)) {
foreach($_POST as $key => $val) {
$md5 = md5($val);
if ($key != $md5) {
$stat = "FAILED";
echo "server:err ".$md5." != ".$key."\n";
}
$length += strlen($key) + strlen($val);
$ret .= $key."(".strlen($key).") ";
}
$ret .= "] [";
foreach ($_FILES as $k0 => $val) {
$error = $val["error"];
if ($error == UPLOAD_ERR_OK) {
$tmp_name = $val["tmp_name"];
$name = $val["name"];
$datafile = "/tmp/test.go";
move_uploaded_file($tmp_name, $datafile);
$md5 = md5_file($datafile);
if ($k0 != $md5) {
$stat = "FAILED";
echo "server:err ".$md5." != ".$key."\n";
}
$length += strlen($k0) + filesize($datafile);
unlink($datafile);
$ret .= $k0."(".strlen($k0).") ";
}
else{
$stat = "FAILED";
echo "server:file err ".file_upload_error_message($error)."\n";
}
}
$ret .= "]";
echo "server:got data length " .$length."\n";
}
echo "-{$stat}-POST(".count($_POST).") FILE(".count($_FILES).")\n";
function file_upload_error_message($error_code) {
switch ($error_code) {
case UPLOAD_ERR_INI_SIZE:
return 'The uploaded file exceeds the upload_max_filesize directive in php.ini';
case UPLOAD_ERR_FORM_SIZE:
return 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form';
case UPLOAD_ERR_PARTIAL:
return 'The uploaded file was only partially uploaded';
case UPLOAD_ERR_NO_FILE:
return 'No file was uploaded';
case UPLOAD_ERR_NO_TMP_DIR:
return 'Missing a temporary folder';
case UPLOAD_ERR_CANT_WRITE:
return 'Failed to write file to disk';
case UPLOAD_ERR_EXTENSION:
return 'File upload stopped by extension';
default:
return 'Unknown upload error';
}
}
-220
View File
@@ -1,220 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 fastcgi
import (
"errors"
"fmt"
"net"
"net/http"
"path/filepath"
"strings"
"time"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
var defaultTimeout = 60 * time.Second
func init() {
caddy.RegisterPlugin("fastcgi", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
// setup configures a new FastCGI middleware instance.
func setup(c *caddy.Controller) error {
cfg := httpserver.GetConfig(c)
rules, err := fastcgiParse(c)
if err != nil {
return err
}
cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return Handler{
Next: next,
Rules: rules,
Root: cfg.Root,
FileSys: http.Dir(cfg.Root),
SoftwareName: caddy.AppName,
SoftwareVersion: caddy.AppVersion,
ServerName: cfg.Addr.Host,
ServerPort: cfg.Addr.Port,
}
})
return nil
}
func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
var rules []Rule
cfg := httpserver.GetConfig(c)
absRoot, err := filepath.Abs(cfg.Root)
if err != nil {
return nil, err
}
for c.Next() {
args := c.RemainingArgs()
if len(args) < 2 || len(args) > 3 {
return rules, c.ArgErr()
}
rule := Rule{
Root: absRoot,
Path: args[0],
ConnectTimeout: defaultTimeout,
ReadTimeout: defaultTimeout,
SendTimeout: defaultTimeout,
}
upstreams := []string{args[1]}
srvUpstream := false
if strings.HasPrefix(upstreams[0], "srv://") {
srvUpstream = true
}
if len(args) == 3 {
if err := fastcgiPreset(args[2], &rule); err != nil {
return rules, err
}
}
var err error
for c.NextBlock() {
switch c.Val() {
case "root":
if !c.NextArg() {
return rules, c.ArgErr()
}
rule.Root = c.Val()
case "ext":
if !c.NextArg() {
return rules, c.ArgErr()
}
rule.Ext = c.Val()
case "split":
if !c.NextArg() {
return rules, c.ArgErr()
}
rule.SplitPath = c.Val()
case "index":
args := c.RemainingArgs()
if len(args) == 0 {
return rules, c.ArgErr()
}
rule.IndexFiles = args
case "upstream":
if srvUpstream {
return rules, c.Err("additional upstreams are not supported with SRV upstream")
}
args := c.RemainingArgs()
if len(args) != 1 {
return rules, c.ArgErr()
}
upstreams = append(upstreams, args[0])
case "env":
envArgs := c.RemainingArgs()
if len(envArgs) < 2 {
return rules, c.ArgErr()
}
rule.EnvVars = append(rule.EnvVars, [2]string{envArgs[0], envArgs[1]})
case "except":
ignoredPaths := c.RemainingArgs()
if len(ignoredPaths) == 0 {
return rules, c.ArgErr()
}
rule.IgnoredSubPaths = ignoredPaths
case "connect_timeout":
if !c.NextArg() {
return rules, c.ArgErr()
}
rule.ConnectTimeout, err = time.ParseDuration(c.Val())
if err != nil {
return rules, err
}
case "read_timeout":
if !c.NextArg() {
return rules, c.ArgErr()
}
readTimeout, err := time.ParseDuration(c.Val())
if err != nil {
return rules, err
}
rule.ReadTimeout = readTimeout
case "send_timeout":
if !c.NextArg() {
return rules, c.ArgErr()
}
sendTimeout, err := time.ParseDuration(c.Val())
if err != nil {
return rules, err
}
rule.SendTimeout = sendTimeout
}
}
if srvUpstream {
balancer, err := parseSRV(upstreams[0])
if err != nil {
return rules, c.Err("malformed service locator string: " + err.Error())
}
rule.balancer = balancer
} else {
rule.balancer = &roundRobin{addresses: upstreams, index: -1}
}
rules = append(rules, rule)
}
return rules, nil
}
func parseSRV(locator string) (*srv, error) {
if locator[6:] == "" {
return nil, fmt.Errorf("%s does not include the host", locator)
}
return &srv{
service: locator[6:],
resolver: &net.Resolver{},
}, nil
}
// fastcgiPreset configures rule according to name. It returns an error if
// name is not a recognized preset name.
func fastcgiPreset(name string, rule *Rule) error {
switch name {
case "php":
rule.Ext = ".php"
rule.SplitPath = ".php"
rule.IndexFiles = []string{"index.php"}
default:
return errors.New(name + " is not a valid preset name")
}
return nil
}
-255
View File
@@ -1,255 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 fastcgi
import (
"context"
"fmt"
"net"
"testing"
"time"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("http", `fastcgi / 127.0.0.1:9000`)
err := setup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
mids := httpserver.GetConfig(c).Middleware()
if len(mids) == 0 {
t.Fatal("Expected middleware, got 0 instead")
}
handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(Handler)
if !ok {
t.Fatalf("Expected handler to be type , got: %#v", handler)
}
if myHandler.Rules[0].Path != "/" {
t.Errorf("Expected / as the Path")
}
addr, err := myHandler.Rules[0].Address()
if err != nil {
t.Errorf("Unexpected error in trying to retrieve address: %s", err.Error())
}
if addr != "127.0.0.1:9000" {
t.Errorf("Expected 127.0.0.1:9000 as the Address")
}
if myHandler.Rules[0].ConnectTimeout != 60*time.Second {
t.Errorf("Expected default value of 60 seconds")
}
if myHandler.Rules[0].ReadTimeout != 60*time.Second {
t.Errorf("Expected default value of 60 seconds")
}
if myHandler.Rules[0].SendTimeout != 60*time.Second {
t.Errorf("Expected default value of 60 seconds")
}
}
func TestFastcgiParse(t *testing.T) {
tests := []struct {
inputFastcgiConfig string
shouldErr bool
expectedFastcgiConfig []Rule
}{
{`fastcgi /blog 127.0.0.1:9000 php`,
false, []Rule{{
Path: "/blog",
balancer: &roundRobin{addresses: []string{"127.0.0.1:9000"}},
Ext: ".php",
SplitPath: ".php",
IndexFiles: []string{"index.php"},
SendTimeout: 60 * time.Second,
}}},
{`fastcgi / 127.0.0.1:9001 {
split .html
}`,
false, []Rule{{
Path: "/",
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
Ext: "",
SplitPath: ".html",
IndexFiles: []string{},
SendTimeout: 60 * time.Second,
}}},
{`fastcgi / 127.0.0.1:9001 {
split .html
except /admin /user
}`,
false, []Rule{{
Path: "/",
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
Ext: "",
SplitPath: ".html",
IndexFiles: []string{},
IgnoredSubPaths: []string{"/admin", "/user"},
SendTimeout: 60 * time.Second,
}}},
{`fastcgi / 127.0.0.1:9001 {
send_timeout 30s
}`,
false, []Rule{{
Path: "/",
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
Ext: "",
IndexFiles: []string{},
SendTimeout: 30 * time.Second,
}}},
}
for i, test := range tests {
actualFastcgiConfigs, err := fastcgiParse(caddy.NewTestController("http", test.inputFastcgiConfig))
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actualFastcgiConfigs) != len(test.expectedFastcgiConfig) {
t.Fatalf("Test %d expected %d no of FastCGI configs, but got %d ",
i, len(test.expectedFastcgiConfig), len(actualFastcgiConfigs))
}
for j, actualFastcgiConfig := range actualFastcgiConfigs {
if actualFastcgiConfig.Path != test.expectedFastcgiConfig[j].Path {
t.Errorf("Test %d expected %dth FastCGI Path to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].Path, actualFastcgiConfig.Path)
}
actualAddr, err := actualFastcgiConfig.Address()
if err != nil {
t.Errorf("Test %d unexpected error in trying to retrieve %dth actual address: %s", i, j, err.Error())
}
expectedAddr, err := test.expectedFastcgiConfig[j].Address()
if err != nil {
t.Errorf("Test %d unexpected error in trying to retrieve %dth expected address: %s", i, j, err.Error())
}
if actualAddr != expectedAddr {
t.Errorf("Test %d expected %dth FastCGI Address to be %s , but got %s",
i, j, expectedAddr, actualAddr)
}
if actualFastcgiConfig.Ext != test.expectedFastcgiConfig[j].Ext {
t.Errorf("Test %d expected %dth FastCGI Ext to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].Ext, actualFastcgiConfig.Ext)
}
if actualFastcgiConfig.SplitPath != test.expectedFastcgiConfig[j].SplitPath {
t.Errorf("Test %d expected %dth FastCGI SplitPath to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].SplitPath, actualFastcgiConfig.SplitPath)
}
if fmt.Sprint(actualFastcgiConfig.IndexFiles) != fmt.Sprint(test.expectedFastcgiConfig[j].IndexFiles) {
t.Errorf("Test %d expected %dth FastCGI IndexFiles to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].IndexFiles, actualFastcgiConfig.IndexFiles)
}
if fmt.Sprint(actualFastcgiConfig.IgnoredSubPaths) != fmt.Sprint(test.expectedFastcgiConfig[j].IgnoredSubPaths) {
t.Errorf("Test %d expected %dth FastCGI IgnoredSubPaths to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].IgnoredSubPaths, actualFastcgiConfig.IgnoredSubPaths)
}
if actualFastcgiConfig.SendTimeout != test.expectedFastcgiConfig[j].SendTimeout {
t.Errorf("Test %d expected %dth FastCGI SendTimeout to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].SendTimeout, actualFastcgiConfig.SendTimeout)
}
}
}
}
func TestFastCGIResolveSRV(t *testing.T) {
tests := []struct {
inputFastcgiConfig string
locator string
target string
port uint16
shouldErr bool
}{
{
`fastcgi / srv://fpm.tcp.service.consul {
upstream yolo
}`,
"fpm.tcp.service.consul",
"127.0.0.1",
9000,
true,
},
{
`fastcgi / srv://fpm.tcp.service.consul`,
"fpm.tcp.service.consul",
"127.0.0.1",
9000,
false,
},
}
for i, test := range tests {
actualFastcgiConfigs, err := fastcgiParse(caddy.NewTestController("http", test.inputFastcgiConfig))
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
for _, actualFastcgiConfig := range actualFastcgiConfigs {
resolver, ok := (actualFastcgiConfig.balancer).(*srv)
if !ok {
t.Errorf("Test %d upstream balancer is not srv", i)
}
resolver.resolver = buildTestResolver(test.target, test.port)
addr, err := actualFastcgiConfig.Address()
if err != nil {
t.Errorf("Test %d failed to retrieve upstream address. %s", i, err.Error())
}
expectedAddr := fmt.Sprintf("%s:%d", test.target, test.port)
if addr != expectedAddr {
t.Errorf("Test %d expected upstream address to be %s, got %s", i, expectedAddr, addr)
}
}
}
}
func buildTestResolver(target string, port uint16) srvResolver {
return &testSRVResolver{target, port}
}
type testSRVResolver struct {
target string
port uint16
}
func (r *testSRVResolver) LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error) {
return "", []*net.SRV{
{Target: r.target,
Port: r.port,
Priority: 1,
Weight: 1}}, nil
}
-163
View File
@@ -1,163 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 gzip provides a middleware layer that performs
// gzip compression on the response.
package gzip
import (
"compress/gzip"
"io"
"net/http"
"strings"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func init() {
caddy.RegisterPlugin("gzip", caddy.Plugin{
ServerType: "http",
Action: setup,
})
initWriterPool()
}
// Gzip is a middleware type which gzips HTTP responses. It is
// imperative that any handler which writes to a gzipped response
// specifies the Content-Type, otherwise some clients will assume
// application/x-gzip and try to download a file.
type Gzip struct {
Next httpserver.Handler
Configs []Config
}
// Config holds the configuration for Gzip middleware
type Config struct {
RequestFilters []RequestFilter
ResponseFilters []ResponseFilter
Level int // Compression level
}
// ServeHTTP serves a gzipped response if the client supports it.
func (g Gzip) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
return g.Next.ServeHTTP(w, r)
}
outer:
for _, c := range g.Configs {
// Check request filters to determine if gzipping is permitted for this request
for _, filter := range c.RequestFilters {
if !filter.ShouldCompress(r) {
continue outer
}
}
// In order to avoid unused memory allocation, gzip.putWriter only be called when gzip compression happened.
// see https://github.com/caddyserver/caddy/issues/2395
gz := &gzipResponseWriter{
ResponseWriterWrapper: &httpserver.ResponseWriterWrapper{ResponseWriter: w},
newWriter: func() io.Writer {
// gzipWriter modifies underlying writer at init,
// use a discard writer instead to leave ResponseWriter in
// original form.
return getWriter(c.Level)
},
}
defer func() {
if gzWriter, ok := gz.internalWriter.(*gzip.Writer); ok {
putWriter(c.Level, gzWriter)
}
}()
var rw http.ResponseWriter
// if no response filter is used
if len(c.ResponseFilters) == 0 {
// replace discard writer with ResponseWriter
if gzWriter, ok := gz.Writer().(*gzip.Writer); ok {
gzWriter.Reset(w)
}
rw = gz
} else {
// wrap gzip writer with ResponseFilterWriter
rw = NewResponseFilterWriter(c.ResponseFilters, gz)
}
// Any response in forward middleware will now be compressed
status, err := g.Next.ServeHTTP(rw, r)
// If there was an error that remained unhandled, we need
// to send something back before gzipWriter gets closed at
// the return of this method!
if status >= 400 {
httpserver.DefaultErrorFunc(w, r, status)
return 0, err
}
return status, err
}
// no matching filter
return g.Next.ServeHTTP(w, r)
}
// gzipResponseWriter wraps the underlying Write method
// with a gzip.Writer to compress the output.
type gzipResponseWriter struct {
internalWriter io.Writer
*httpserver.ResponseWriterWrapper
statusCodeWritten bool
newWriter func() io.Writer
}
// WriteHeader wraps the underlying WriteHeader method to prevent
// problems with conflicting headers from proxied backends. For
// example, a backend system that calculates Content-Length would
// be wrong because it doesn't know it's being gzipped.
func (w *gzipResponseWriter) WriteHeader(code int) {
w.Header().Del("Content-Length")
w.Header().Set("Content-Encoding", "gzip")
w.Header().Add("Vary", "Accept-Encoding")
originalEtag := w.Header().Get("ETag")
if originalEtag != "" && !strings.HasPrefix(originalEtag, "W/") {
w.Header().Set("ETag", "W/"+originalEtag)
}
w.ResponseWriterWrapper.WriteHeader(code)
w.statusCodeWritten = true
}
// Write wraps the underlying Write method to do compression.
func (w *gzipResponseWriter) Write(b []byte) (int, error) {
if w.Header().Get("Content-Type") == "" {
w.Header().Set("Content-Type", http.DetectContentType(b))
}
if !w.statusCodeWritten {
w.WriteHeader(http.StatusOK)
}
n, err := w.Writer().Write(b)
return n, err
}
//Writer use a lazy way to initialize Writer
func (w *gzipResponseWriter) Writer() io.Writer {
if w.internalWriter == nil {
w.internalWriter = w.newWriter()
}
return w.internalWriter
}
// Interface guards
var _ httpserver.HTTPInterfaces = (*gzipResponseWriter)(nil)
-196
View File
@@ -1,196 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 gzip
import (
"compress/gzip"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestGzipHandler(t *testing.T) {
pathFilter := PathFilter{make(Set)}
badPaths := []string{"/bad", "/nogzip", "/nongzip"}
for _, p := range badPaths {
pathFilter.IgnoredPaths.Add(p)
}
extFilter := ExtFilter{make(Set)}
for _, e := range []string{".txt", ".html", ".css", ".md"} {
extFilter.Exts.Add(e)
}
gz := Gzip{Configs: []Config{
{RequestFilters: []RequestFilter{pathFilter, extFilter}},
}}
w := httptest.NewRecorder()
gz.Next = nextFunc(true)
var exts = []string{
".html", ".css", ".md",
}
for _, e := range exts {
url := "/file" + e
r, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Error(err)
}
r.Header.Set("Accept-Encoding", "gzip")
w.Header().Set("ETag", `"2n9cd"`)
_, err = gz.ServeHTTP(w, r)
if err != nil {
t.Error(err)
}
// The second pass, test if the ETag is already weak
w.Header().Set("ETag", `W/"2n9cd"`)
_, err = gz.ServeHTTP(w, r)
if err != nil {
t.Error(err)
}
}
w = httptest.NewRecorder()
gz.Next = nextFunc(false)
for _, p := range badPaths {
for _, e := range exts {
url := p + "/file" + e
r, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Error(err)
}
r.Header.Set("Accept-Encoding", "gzip")
_, err = gz.ServeHTTP(w, r)
if err != nil {
t.Error(err)
}
}
}
w = httptest.NewRecorder()
gz.Next = nextFunc(false)
exts = []string{
".htm1", ".abc", ".mdx",
}
for _, e := range exts {
url := "/file" + e
r, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Error(err)
}
r.Header.Set("Accept-Encoding", "gzip")
_, err = gz.ServeHTTP(w, r)
if err != nil {
t.Error(err)
}
}
// test all levels
w = httptest.NewRecorder()
gz.Next = nextFunc(true)
for i := 0; i <= gzip.BestCompression; i++ {
gz.Configs[0].Level = i
r, err := http.NewRequest("GET", "/file.txt", nil)
if err != nil {
t.Error(err)
}
r.Header.Set("Accept-Encoding", "gzip")
_, err = gz.ServeHTTP(w, r)
if err != nil {
t.Error(err)
}
}
}
func nextFunc(shouldGzip bool) httpserver.Handler {
return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
// write a relatively large text file
b, err := ioutil.ReadFile("testdata/test.txt")
if err != nil {
return 500, err
}
if _, err := w.Write(b); err != nil {
return 500, err
}
if shouldGzip {
if w.Header().Get("Content-Encoding") != "gzip" {
return 0, fmt.Errorf("Content-Encoding must be gzip, found %v", w.Header().Get("Content-Encoding"))
}
if w.Header().Get("Vary") != "Accept-Encoding" {
return 0, fmt.Errorf("Vary must be Accept-Encoding, found %v", w.Header().Get("Vary"))
}
etag := w.Header().Get("ETag")
if etag != "" && etag != `W/"2n9cd"` {
return 0, fmt.Errorf("ETag must be converted to weak Etag, found %v", w.Header().Get("ETag"))
}
if _, ok := w.(*gzipResponseWriter); !ok {
return 0, fmt.Errorf("ResponseWriter should be gzipResponseWriter, found %T", w)
}
if strings.Contains(w.Header().Get("Content-Type"), "application/x-gzip") {
return 0, fmt.Errorf("Content-Type should not be gzip")
}
return 0, nil
}
if r.Header.Get("Accept-Encoding") == "" {
return 0, fmt.Errorf("Accept-Encoding header expected")
}
if w.Header().Get("Content-Encoding") == "gzip" {
return 0, fmt.Errorf("Content-Encoding must not be gzip, found gzip")
}
if _, ok := w.(*gzipResponseWriter); ok {
return 0, fmt.Errorf("ResponseWriter should not be gzipResponseWriter")
}
return 0, nil
})
}
func BenchmarkGzip(b *testing.B) {
pathFilter := PathFilter{make(Set)}
badPaths := []string{"/bad", "/nogzip", "/nongzip"}
for _, p := range badPaths {
pathFilter.IgnoredPaths.Add(p)
}
extFilter := ExtFilter{make(Set)}
for _, e := range []string{".txt", ".html", ".css", ".md"} {
extFilter.Exts.Add(e)
}
gz := Gzip{Configs: []Config{
{
RequestFilters: []RequestFilter{pathFilter, extFilter},
},
}}
w := httptest.NewRecorder()
gz.Next = nextFunc(true)
url := "/file.txt"
r, err := http.NewRequest("GET", url, nil)
if err != nil {
b.Fatal(err)
}
r.Header.Set("Accept-Encoding", "gzip")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err = gz.ServeHTTP(w, r)
if err != nil {
b.Fatal(err)
}
}
}
-105
View File
@@ -1,105 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 gzip
import (
"net/http"
"path"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
// RequestFilter determines if a request should be gzipped.
type RequestFilter interface {
// ShouldCompress tells if gzip compression
// should be done on the request.
ShouldCompress(*http.Request) bool
}
// defaultExtensions is the list of default extensions for which to enable gzipping.
var defaultExtensions = []string{"", ".txt", ".htm", ".html", ".css", ".php", ".js", ".json",
".md", ".mdown", ".xml", ".svg", ".go", ".cgi", ".py", ".pl", ".aspx", ".asp", ".m3u", ".m3u8", ".wasm"}
// DefaultExtFilter creates an ExtFilter with default extensions.
func DefaultExtFilter() ExtFilter {
m := ExtFilter{Exts: make(Set)}
for _, extension := range defaultExtensions {
m.Exts.Add(extension)
}
return m
}
// ExtFilter is RequestFilter for file name extensions.
type ExtFilter struct {
// Exts is the file name extensions to accept
Exts Set
}
// ExtWildCard is the wildcard for extensions.
const ExtWildCard = "*"
// ShouldCompress checks if the request file extension matches any
// of the registered extensions. It returns true if the extension is
// found and false otherwise.
func (e ExtFilter) ShouldCompress(r *http.Request) bool {
ext := path.Ext(r.URL.Path)
return e.Exts.Contains(ExtWildCard) || e.Exts.Contains(ext)
}
// PathFilter is RequestFilter for request path.
type PathFilter struct {
// IgnoredPaths is the paths to ignore
IgnoredPaths Set
}
// ShouldCompress checks if the request path matches any of the
// registered paths to ignore. It returns false if an ignored path
// is found and true otherwise.
func (p PathFilter) ShouldCompress(r *http.Request) bool {
return !p.IgnoredPaths.ContainsFunc(func(value string) bool {
return httpserver.Path(r.URL.Path).Matches(value)
})
}
// Set stores distinct strings.
type Set map[string]struct{}
// Add adds an element to the set.
func (s Set) Add(value string) {
s[value] = struct{}{}
}
// Remove removes an element from the set.
func (s Set) Remove(value string) {
delete(s, value)
}
// Contains check if the set contains value.
func (s Set) Contains(value string) bool {
_, ok := s[value]
return ok
}
// ContainsFunc is similar to Contains. It iterates all the
// elements in the set and passes each to f. It returns true
// on the first call to f that returns true and false otherwise.
func (s Set) ContainsFunc(f func(string) bool) bool {
for k := range s {
if f(k) {
return true
}
}
return false
}
-127
View File
@@ -1,127 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 gzip
import (
"net/http"
"testing"
)
func TestSet(t *testing.T) {
set := make(Set)
set.Add("a")
if len(set) != 1 {
t.Errorf("Expected 1 found %v", len(set))
}
set.Add("a")
if len(set) != 1 {
t.Errorf("Expected 1 found %v", len(set))
}
set.Add("b")
if len(set) != 2 {
t.Errorf("Expected 2 found %v", len(set))
}
if !set.Contains("a") {
t.Errorf("Set should contain a")
}
if !set.Contains("b") {
t.Errorf("Set should contain a")
}
set.Add("c")
if len(set) != 3 {
t.Errorf("Expected 3 found %v", len(set))
}
if !set.Contains("c") {
t.Errorf("Set should contain c")
}
set.Remove("a")
if len(set) != 2 {
t.Errorf("Expected 2 found %v", len(set))
}
if set.Contains("a") {
t.Errorf("Set should not contain a")
}
if !set.ContainsFunc(func(v string) bool {
return v == "c"
}) {
t.Errorf("ContainsFunc should return true")
}
}
func TestExtFilter(t *testing.T) {
var filter RequestFilter = ExtFilter{make(Set)}
for _, e := range []string{".txt", ".html", ".css", ".md"} {
filter.(ExtFilter).Exts.Add(e)
}
r := urlRequest("file.txt")
if !filter.ShouldCompress(r) {
t.Errorf("Should be valid filter")
}
var exts = []string{
".html", ".css", ".md",
}
for i, e := range exts {
r := urlRequest("file" + e)
if !filter.ShouldCompress(r) {
t.Errorf("Test %v: Should be valid filter", i)
}
}
exts = []string{
".htm1", ".abc", ".mdx",
}
for i, e := range exts {
r := urlRequest("file" + e)
if filter.ShouldCompress(r) {
t.Errorf("Test %v: Should not be valid filter", i)
}
}
filter.(ExtFilter).Exts.Add(ExtWildCard)
for i, e := range exts {
r := urlRequest("file" + e)
if !filter.ShouldCompress(r) {
t.Errorf("Test %v: Should be valid filter. Wildcard used.", i)
}
}
}
func TestPathFilter(t *testing.T) {
paths := []string{
"/a", "/b", "/c", "/de",
}
var filter RequestFilter = PathFilter{make(Set)}
for _, p := range paths {
filter.(PathFilter).IgnoredPaths.Add(p)
}
for i, p := range paths {
r := urlRequest(p)
if filter.ShouldCompress(r) {
t.Errorf("Test %v: Should not be valid filter", i)
}
}
paths = []string{
"/f", "/g", "/h", "/ed",
}
for i, p := range paths {
r := urlRequest(p)
if !filter.ShouldCompress(r) {
t.Errorf("Test %v: Should be valid filter", i)
}
}
}
func urlRequest(url string) *http.Request {
r, _ := http.NewRequest("GET", url, nil)
return r
}
-107
View File
@@ -1,107 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 gzip
import (
"compress/gzip"
"net/http"
"strconv"
)
// ResponseFilter determines if the response should be gzipped.
type ResponseFilter interface {
ShouldCompress(http.ResponseWriter) bool
}
// LengthFilter is ResponseFilter for minimum content length.
type LengthFilter int64
// ShouldCompress returns if content length is greater than or
// equals to minimum length.
func (l LengthFilter) ShouldCompress(w http.ResponseWriter) bool {
contentLength := w.Header().Get("Content-Length")
length, err := strconv.ParseInt(contentLength, 10, 64)
if err != nil || length == 0 {
return false
}
return l != 0 && int64(l) <= length
}
// SkipCompressedFilter is ResponseFilter that will discard already compressed responses
type SkipCompressedFilter struct{}
// ShouldCompress returns true if served file is not already compressed
// encodings via https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
func (n SkipCompressedFilter) ShouldCompress(w http.ResponseWriter) bool {
switch w.Header().Get("Content-Encoding") {
case "gzip", "compress", "deflate", "br":
return false
default:
return true
}
}
// ResponseFilterWriter validates ResponseFilters. It writes
// gzip compressed data if ResponseFilters are satisfied or
// uncompressed data otherwise.
type ResponseFilterWriter struct {
filters []ResponseFilter
shouldCompress bool
statusCodeWritten bool
*gzipResponseWriter
}
// NewResponseFilterWriter creates and initializes a new ResponseFilterWriter.
func NewResponseFilterWriter(filters []ResponseFilter, gz *gzipResponseWriter) *ResponseFilterWriter {
return &ResponseFilterWriter{filters: filters, gzipResponseWriter: gz}
}
// WriteHeader wraps underlying WriteHeader method and
// compresses if filters are satisfied.
func (r *ResponseFilterWriter) WriteHeader(code int) {
// Determine if compression should be used or not.
r.shouldCompress = true
for _, filter := range r.filters {
if !filter.ShouldCompress(r) {
r.shouldCompress = false
break
}
}
if r.shouldCompress {
// replace discard writer with ResponseWriter
if gzWriter, ok := r.gzipResponseWriter.Writer().(*gzip.Writer); ok {
gzWriter.Reset(r.ResponseWriter)
}
// use gzip WriteHeader to include and delete
// necessary headers
r.gzipResponseWriter.WriteHeader(code)
} else {
r.ResponseWriter.WriteHeader(code)
}
r.statusCodeWritten = true
}
// Write wraps underlying Write method and compresses if filters
// are satisfied
func (r *ResponseFilterWriter) Write(b []byte) (int, error) {
if !r.statusCodeWritten {
r.WriteHeader(http.StatusOK)
}
if r.shouldCompress {
return r.gzipResponseWriter.Write(b)
}
return r.ResponseWriter.Write(b)
}
-135
View File
@@ -1,135 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 gzip
import (
"compress/gzip"
"fmt"
"log"
"net/http"
"net/http/httptest"
"testing"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestLengthFilter(t *testing.T) {
var filters = []ResponseFilter{
LengthFilter(100),
LengthFilter(1000),
LengthFilter(0),
}
var tests = []struct {
length int64
shouldCompress [3]bool
}{
{20, [3]bool{false, false, false}},
{50, [3]bool{false, false, false}},
{100, [3]bool{true, false, false}},
{500, [3]bool{true, false, false}},
{1000, [3]bool{true, true, false}},
{1500, [3]bool{true, true, false}},
}
for i, ts := range tests {
for j, filter := range filters {
r := httptest.NewRecorder()
r.Header().Set("Content-Length", fmt.Sprint(ts.length))
wWriter := NewResponseFilterWriter([]ResponseFilter{filter}, &gzipResponseWriter{gzip.NewWriter(r), &httpserver.ResponseWriterWrapper{ResponseWriter: r}, false, nil})
if filter.ShouldCompress(wWriter) != ts.shouldCompress[j] {
t.Errorf("Test %v: Expected %v found %v", i, ts.shouldCompress[j], filter.ShouldCompress(r))
}
}
}
}
func TestResponseFilterWriter(t *testing.T) {
tests := []struct {
body string
shouldCompress bool
}{
{"Hello\t\t\t\n", false},
{"Hello the \t\t\t world is\n\n\n great", true},
{"Hello \t\t\nfrom gzip", true},
{"Hello gzip\n", false},
}
filters := []ResponseFilter{
LengthFilter(15),
}
server := Gzip{Configs: []Config{
{ResponseFilters: filters},
}}
for i, ts := range tests {
server.Next = httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
w.Header().Set("Content-Length", fmt.Sprint(len(ts.body)))
if _, err := w.Write([]byte(ts.body)); err != nil {
log.Println("[ERROR] failed to write response: ", err)
}
return 200, nil
})
r := urlRequest("/")
r.Header.Set("Accept-Encoding", "gzip")
w := httptest.NewRecorder()
if _, err := server.ServeHTTP(w, r); err != nil {
log.Println("[ERROR] unable to serve a gzipped response: ", err)
}
resp := w.Body.String()
if !ts.shouldCompress {
if resp != ts.body {
t.Errorf("Test %v: No compression expected, found %v", i, resp)
}
} else {
if resp == ts.body {
t.Errorf("Test %v: Compression expected, found %v", i, resp)
}
}
}
}
func TestResponseGzippedOutput(t *testing.T) {
server := Gzip{Configs: []Config{
{ResponseFilters: []ResponseFilter{SkipCompressedFilter{}}},
}}
server.Next = httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
w.Header().Set("Content-Encoding", "gzip")
if _, err := w.Write([]byte("gzipped")); err != nil {
log.Println("[ERROR] failed to write response: ", err)
}
return 200, nil
})
r := urlRequest("/")
r.Header.Set("Accept-Encoding", "gzip")
w := httptest.NewRecorder()
if _, err := server.ServeHTTP(w, r); err != nil {
log.Println("[ERROR] unable to serve a gzipped response: ", err)
}
resp := w.Body.String()
if resp != "gzipped" {
t.Errorf("Expected output not to be gzipped")
}
}
-183
View File
@@ -1,183 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 gzip
import (
"compress/gzip"
"fmt"
"io/ioutil"
"strconv"
"strings"
"sync"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
// setup configures a new gzip middleware instance.
func setup(c *caddy.Controller) error {
configs, err := gzipParse(c)
if err != nil {
return err
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return Gzip{Next: next, Configs: configs}
})
return nil
}
func gzipParse(c *caddy.Controller) ([]Config, error) {
var configs []Config
for c.Next() {
config := Config{}
// Request Filters
pathFilter := PathFilter{IgnoredPaths: make(Set)}
extFilter := ExtFilter{Exts: make(Set)}
// Response Filters
lengthFilter := LengthFilter(0)
// No extra args expected
if len(c.RemainingArgs()) > 0 {
return configs, c.ArgErr()
}
for c.NextBlock() {
switch c.Val() {
case "ext":
exts := c.RemainingArgs()
if len(exts) == 0 {
return configs, c.ArgErr()
}
for _, e := range exts {
if !strings.HasPrefix(e, ".") && e != ExtWildCard && e != "" {
return configs, fmt.Errorf(`gzip: invalid extension "%v" (must start with dot)`, e)
}
extFilter.Exts.Add(e)
}
case "not":
paths := c.RemainingArgs()
if len(paths) == 0 {
return configs, c.ArgErr()
}
for _, p := range paths {
if p == "/" {
return configs, fmt.Errorf(`gzip: cannot exclude path "/" - remove directive entirely instead`)
}
if !strings.HasPrefix(p, "/") {
return configs, fmt.Errorf(`gzip: invalid path "%v" (must start with /)`, p)
}
pathFilter.IgnoredPaths.Add(p)
}
case "level":
if !c.NextArg() {
return configs, c.ArgErr()
}
level, _ := strconv.Atoi(c.Val())
config.Level = level
case "min_length":
if !c.NextArg() {
return configs, c.ArgErr()
}
length, err := strconv.ParseInt(c.Val(), 10, 64)
if err != nil {
return configs, err
} else if length == 0 {
return configs, fmt.Errorf(`gzip: min_length must be greater than 0`)
}
lengthFilter = LengthFilter(length)
default:
return configs, c.ArgErr()
}
}
// Request Filters
config.RequestFilters = []RequestFilter{}
// If ignored paths are specified, put in front to filter with path first
if len(pathFilter.IgnoredPaths) > 0 {
config.RequestFilters = []RequestFilter{pathFilter}
}
// Then, if extensions are specified, use those to filter.
// Otherwise, use default extensions filter.
if len(extFilter.Exts) > 0 {
config.RequestFilters = append(config.RequestFilters, extFilter)
} else {
config.RequestFilters = append(config.RequestFilters, DefaultExtFilter())
}
config.ResponseFilters = append(config.ResponseFilters, SkipCompressedFilter{})
// Response Filters
// If min_length is specified, use it.
if int64(lengthFilter) != 0 {
config.ResponseFilters = append(config.ResponseFilters, lengthFilter)
}
configs = append(configs, config)
}
return configs, nil
}
// pool gzip.Writer according to compress level
// so we can reuse allocations over time
var (
writerPool = map[int]*sync.Pool{}
defaultWriterPoolIndex int
)
func initWriterPool() {
var i int
newWriterPool := func(level int) *sync.Pool {
return &sync.Pool{
New: func() interface{} {
w, _ := gzip.NewWriterLevel(ioutil.Discard, level)
return w
},
}
}
for i = gzip.BestSpeed; i <= gzip.BestCompression; i++ {
writerPool[i] = newWriterPool(i)
}
// add default writer pool
defaultWriterPoolIndex = i
writerPool[defaultWriterPoolIndex] = newWriterPool(gzip.DefaultCompression)
}
func getWriter(level int) *gzip.Writer {
index := defaultWriterPoolIndex
if level >= gzip.BestSpeed && level <= gzip.BestCompression {
index = level
}
w := writerPool[index].Get().(*gzip.Writer)
w.Reset(ioutil.Discard)
return w
}
func putWriter(level int, w *gzip.Writer) {
index := defaultWriterPoolIndex
if level >= gzip.BestSpeed && level <= gzip.BestCompression {
index = level
}
w.Close()
writerPool[index].Put(w)
}
-143
View File
@@ -1,143 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// 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 gzip
import (
"testing"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("http", `gzip`)
err := setup(c)
if err != nil {
t.Errorf("Expected no errors, but got: %v", err)
}
mids := httpserver.GetConfig(c).Middleware()
if mids == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(Gzip)
if !ok {
t.Fatalf("Expected handler to be type Gzip, got: %#v", handler)
}
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
tests := []struct {
input string
shouldErr bool
}{
{`gzip {`, true},
{`gzip {}`, true},
{`gzip a b`, true},
{`gzip a {`, true},
{`gzip { not f } `, true},
{`gzip { not } `, true},
{`gzip { not /file
ext .html
level 1
} `, false},
{`gzip { level 9 } `, false},
{`gzip { ext } `, true},
{`gzip { ext /f
} `, true},
{`gzip { not /file
ext .html
level 1
}
gzip`, false},
{`gzip {
ext ""
}`, false},
{`gzip { not /file
ext .html
level 1
}
gzip { not /file1
ext .htm
level 3
}
`, false},
{`gzip { not /file
ext .html
level 1
}
gzip { not /file1
ext .htm
level 3
}
`, false},
{`gzip { not /file
ext *
level 1
}
`, false},
{`gzip { not /file
ext *
level 1
min_length ab
}
`, true},
{`gzip { not /file
ext *
level 1
min_length 1000
}
`, false},
}
for i, test := range tests {
_, err := gzipParse(caddy.NewTestController("http", test.input))
if test.shouldErr && err == nil {
t.Errorf("Test %v: Expected error but found nil", i)
} else if !test.shouldErr && err != nil {
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
}
}
}
func TestShouldAddResponseFilters(t *testing.T) {
configs, err := gzipParse(caddy.NewTestController("http", `gzip { min_length 654 }`))
if err != nil {
t.Errorf("Test expected no error but found: %v", err)
}
filters := 0
for _, config := range configs {
for _, filter := range config.ResponseFilters {
switch filter.(type) {
case SkipCompressedFilter:
filters++
case LengthFilter:
filters++
if filter != LengthFilter(654) {
t.Errorf("Expected LengthFilter to have length 654, got: %v", filter)
}
}
}
if filters != 2 {
t.Errorf("Expected 2 response filters to be registered, got: %v", filters)
}
}
}
-308
View File
@@ -1,308 +0,0 @@
Sigh view am high neat half to what. Sent late held than set why wife our. If an blessing building steepest. Agreement distrusts mrs six affection satisfied. Day blushes visitor end company old prevent chapter. Consider declared out expenses her concerns. No at indulgence conviction particular unsatiable boisterous discretion. Direct enough off others say eldest may exeter she. Possible all ignorant supplied get settling marriage recurred.
Boy desirous families prepared gay reserved add ecstatic say. Replied joy age visitor nothing cottage. Mrs door paid led loud sure easy read. Hastily at perhaps as neither or ye fertile tedious visitor. Use fine bed none call busy dull when. Quiet ought match my right by table means. Principles up do in me favourable affronting. Twenty mother denied effect we to do on.
Compliment interested discretion estimating on stimulated apartments oh. Dear so sing when in find read of call. As distrusts behaviour abilities defective is. Never at water me might. On formed merits hunted unable merely by mr whence or. Possession the unpleasing simplicity her uncommonly.
Bringing so sociable felicity supplied mr. September suspicion far him two acuteness perfectly. Covered as an examine so regular of. Ye astonished friendship remarkably no. Window admire matter praise you bed whence. Delivered ye sportsmen zealously arranging frankness estimable as. Nay any article enabled musical shyness yet sixteen yet blushes. Entire its the did figure wonder off.
Inhabit hearing perhaps on ye do no. It maids decay as there he. Smallest on suitable disposed do although blessing he juvenile in. Society or if excited forbade. Here name off yet she long sold easy whom. Differed oh cheerful procured pleasure securing suitable in. Hold rich on an he oh fine. Chapter ability shyness article welcome be do on service.
An sincerity so extremity he additions. Her yet there truth merit. Mrs all projecting favourable now unpleasing. Son law garden chatty temper. Oh children provided to mr elegance marriage strongly. Off can admiration prosperous now devonshire diminution law.
Performed suspicion in certainty so frankness by attention pretended. Newspaper or in tolerably education enjoyment. Extremity excellent certainty discourse sincerity no he so resembled. Joy house worse arise total boy but. Elderly up chicken do at feeling is. Like seen drew no make fond at on rent. Behaviour extremely her explained situation yet september gentleman are who. Is thought or pointed hearing he.
Not far stuff she think the jokes. Going as by do known noise he wrote round leave. Warmly put branch people narrow see. Winding its waiting yet parlors married own feeling. Marry fruit do spite jokes an times. Whether at it unknown warrant herself winding if. Him same none name sake had post love. An busy feel form hand am up help. Parties it brother amongst an fortune of. Twenty behind wicket why age now itself ten.
On no twenty spring of in esteem spirit likely estate. Continue new you declared differed learning bringing honoured. At mean mind so upon they rent am walk. Shortly am waiting inhabit smiling he chiefly of in. Lain tore time gone him his dear sure. Fat decisively estimating affronting assistance not. Resolve pursuit regular so calling me. West he plan girl been my then up no.
Expenses as material breeding insisted building to in. Continual so distrusts pronounce by unwilling listening. Thing do taste on we manor. Him had wound use found hoped. Of distrusts immediate enjoyment curiosity do. Marianne numerous saw thoughts the humoured.
Tolerably earnestly middleton extremely distrusts she boy now not. Add and offered prepare how cordial two promise. Greatly who affixed suppose but enquire compact prepare all put. Added forth chief trees but rooms think may. Wicket do manner others seemed enable rather in. Excellent own discovery unfeeling sweetness questions the gentleman. Chapter shyness matters mr parlors if mention thought.
Or kind rest bred with am shed then. In raptures building an bringing be. Elderly is detract tedious assured private so to visited. Do travelling companions contrasted it. Mistress strongly remember up to. Ham him compass you proceed calling detract. Better of always missed we person mr. September smallness northward situation few her certainty something.
Moments its musical age explain. But extremity sex now education concluded earnestly her continual. Oh furniture acuteness suspected continual ye something frankness. Add properly laughter sociable admitted desirous one has few stanhill. Opinion regular in perhaps another enjoyed no engaged he at. It conveying he continual ye suspected as necessary. Separate met packages shy for kindness.
Conveying or northward offending admitting perfectly my. Colonel gravity get thought fat smiling add but. Wonder twenty hunted and put income set desire expect. Am cottage calling my is mistake cousins talking up. Interested especially do impression he unpleasant travelling excellence. All few our knew time done draw ask.
In it except to so temper mutual tastes mother. Interested cultivated its continuing now yet are. Out interested acceptance our partiality affronting unpleasant why add. Esteem garden men yet shy course. Consulted up my tolerably sometimes perpetual oh. Expression acceptance imprudence particular had eat unsatiable.
Son agreed others exeter period myself few yet nature. Mention mr manners opinion if garrets enabled. To an occasional dissimilar impossible sentiments. Do fortune account written prepare invited no passage. Garrets use ten you the weather ferrars venture friends. Solid visit seems again you nor all.
You vexed shy mirth now noise. Talked him people valley add use her depend letter. Allowance too applauded now way something recommend. Mrs age men and trees jokes fancy. Gay pretended engrossed eagerness continued ten. Admitting day him contained unfeeling attention mrs out.
Advantage old had otherwise sincerity dependent additions. It in adapted natural hastily is justice. Six draw you him full not mean evil. Prepare garrets it expense windows shewing do an. She projection advantages resolution son indulgence. Part sure on no long life am at ever. In songs above he as drawn to. Gay was outlived peculiar rendered led six.
Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer ladies valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is.
Meant balls it if up doubt small purse. Required his you put the outlived answered position. An pleasure exertion if believed provided to. All led out world these music while asked. Paid mind even sons does he door no. Attended overcame repeated it is perceive marianne in. In am think on style child of. Servants moreover in sensible he it ye possible.
Neat own nor she said see walk. And charm add green you these. Sang busy in this drew ye fine. At greater prepare musical so attacks as on distant. Improving age our her cordially intention. His devonshire sufficient precaution say preference middletons insipidity. Since might water hence the her worse. Concluded it offending dejection do earnestly as me direction. Nature played thirty all him.
Guest it he tears aware as. Make my no cold of need. He been past in by my hard. Warmly thrown oh he common future. Otherwise concealed favourite frankness on be at dashwoods defective at. Sympathize interested simplicity at do projecting increasing terminated. As edward settle limits at in.
Lose john poor same it case do year we. Full how way even the sigh. Extremely nor furniture fat questions now provision incommode preserved. Our side fail find like now. Discovered travelling for insensible partiality unpleasing impossible she. Sudden up my excuse to suffer ladies though or. Bachelor possible marianne directly confined relation as on he.
Is post each that just leaf no. He connection interested so we an sympathize advantages. To said is it shed want do. Occasional middletons everything so to. Have spot part for his quit may. Enable it is square my an regard. Often merit stuff first oh up hills as he. Servants contempt as although addition dashwood is procured. Interest in yourself an do of numerous feelings cheerful confined.
rnestly middleton extremely distrusts she boy now not. Add and offered prepare how cordial two promise. Greatly who affixed suppose but enquire compact prepare all put. Added forth chief trees but rooms think may. Wicket do manner others seemed enable rather in. Excellent own discovery unfeeling sweetness questions the gentleman. Chapter shyness matters mr parlors if mention thought.
Sudden looked elinor off gay estate nor silent. Son read such next see the rest two. Was use extent old entire sussex. Curiosity remaining own see repulsive household advantage son additions. Supposing exquisite daughters eagerness why repulsive for. Praise turned it lovers be warmly by. Little do it eldest former be if.
Certain but she but shyness why cottage. Gay the put instrument sir entreaties affronting. Pretended exquisite see cordially the you. Weeks quiet do vexed or whose. Motionless if no to affronting imprudence no precaution. My indulged as disposal strongly attended. Parlors men express had private village man. Discovery moonlight recommend all one not. Indulged to answered prospect it bachelor is he bringing shutters. Pronounce forfeited mr direction oh he dashwoods ye unwilling.
Of resolve to gravity thought my prepare chamber so. Unsatiable entreaties collecting may sympathize nay interested instrument. If continue building numerous of at relation in margaret. Lasted engage roused mother an am at. Other early while if by do to. Missed living excuse as be. Cause heard fat above first shall for. My smiling to he removal weather on anxious.
Tiled say decay spoil now walls meant house. My mr interest thoughts screened of outweigh removing. Evening society musical besides inhabit ye my. Lose hill well up will he over on. Increasing sufficient everything men him admiration unpleasing sex. Around really his use uneasy longer him man. His our pulled nature elinor talked now for excuse result. Admitted add peculiar get joy doubtful.
Had repulsive dashwoods suspicion sincerity but advantage now him. Remark easily garret nor nay. Civil those mrs enjoy shy fat merry. You greatest jointure saw horrible. He private he on be imagine suppose. Fertile beloved evident through no service elderly is. Blind there if every no so at. Own neglected you preferred way sincerity delivered his attempted. To of message cottage windows do besides against uncivil.
So if on advanced addition absolute received replying throwing he. Delighted consisted newspaper of unfeeling as neglected so. Tell size come hard mrs and four fond are. Of in commanded earnestly resources it. At quitting in strictly up wandered of relation answered felicity. Side need at in what dear ever upon if. Same down want joy neat ask pain help she. Alone three stuff use law walls fat asked. Near do that he help.
Out too the been like hard off. Improve enquire welcome own beloved matters her. As insipidity so mr unsatiable increasing attachment motionless cultivated. Addition mr husbands unpacked occasion he oh. Is unsatiable if projecting boisterous insensible. It recommend be resolving pretended middleton.
She literature discovered increasing how diminution understood. Though and highly the enough county for man. Of it up he still court alone widow seems. Suspected he remainder rapturous my sweetness. All vanity regard sudden nor simple can. World mrs and vexed china since after often.
Put all speaking her delicate recurred possible. Set indulgence inquietude discretion insensible bed why announcing. Middleton fat two satisfied additions. So continued he or commanded household smallness delivered. Door poor on do walk in half. Roof his head the what.
Seen you eyes son show. Far two unaffected one alteration apartments celebrated but middletons interested. Described deficient applauded consisted my me do. Passed edward two talent effect seemed engage six. On ye great do child sorry lived. Proceed cottage far letters ashamed get clothes day. Stairs regret at if matter to. On as needed almost at basket remain. By improved sensible servants children striking in surprise.
Living valley had silent eat merits esteem bed. In last an or went wise as left. Visited civilly am demesne so colonel he calling. So unreserved do interested increasing sentiments. Vanity day giving points within six not law. Few impression difficulty his use has comparison decisively.
To shewing another demands to. Marianne property cheerful informed at striking at. Clothes parlors however by cottage on. In views it or meant drift to. Be concern parlors settled or do shyness address. Remainder northward performed out for moonlight. Yet late add name was rent park from rich. He always do do former he highly.
Meant balls it if up doubt small purse. Required his you put the outlived answered position. An pleasure exertion if believed provided to. All led out world these music while asked. Paid mind even sons does he door no. Attended overcame repeated it is perceive marianne in. In am think on style child of. Servants moreover in sensible he it ye possible.
On it differed repeated wandered required in. Then girl neat why yet knew rose spot. Moreover property we he kindness greatest be oh striking laughter. In me he at collecting affronting principles apartments. Has visitor law attacks pretend you calling own excited painted. Contented attending smallness it oh ye unwilling. Turned favour man two but lovers. Suffer should if waited common person little oh. Improved civility graceful sex few smallest screened settling. Likely active her warmly has.
He an thing rapid these after going drawn or. Timed she his law the spoil round defer. In surprise concerns informed betrayed he learning is ye. Ignorant formerly so ye blessing. He as spoke avoid given downs money on we. Of properly carriage shutters ye as wandered up repeated moreover. Inquietude attachment if ye an solicitude to. Remaining so continued concealed as knowledge happiness. Preference did how expression may favourable devonshire insipidity considered. An length design regret an hardly barton mr figure.
Was certainty remaining engrossed applauded sir how discovery. Settled opinion how enjoyed greater joy adapted too shy. Now properly surprise expenses interest nor replying she she. Bore tall nay many many time yet less. Doubtful for answered one fat indulged margaret sir shutters together. Ladies so in wholly around whence in at. Warmth he up giving oppose if. Impossible is dissimilar entreaties oh on terminated. Earnest studied article country ten respect showing had. But required offering him elegance son improved informed.
Received overcame oh sensible so at an. Formed do change merely to county it. Am separate contempt domestic to to oh. On relation my so addition branched. Put hearing cottage she norland letters equally prepare too. Replied exposed savings he no viewing as up. Soon body add him hill. No father living really people estate if. Mistake do produce beloved demesne if am pursuit.
Finished her are its honoured drawings nor. Pretty see mutual thrown all not edward ten. Particular an boisterous up he reasonably frequently. Several any had enjoyed shewing studied two. Up intention remainder sportsmen behaviour ye happiness. Few again any alone style added abode ask. Nay projecting unpleasing boisterous eat discovered solicitude. Own six moments produce elderly pasture far arrival. Hold our year they ten upon. Gentleman contained so intention sweetness in on resolving.
Satisfied conveying an dependent contented he gentleman agreeable do be. Warrant private blushes removed an in equally totally if. Delivered dejection necessary objection do mr prevailed. Mr feeling do chiefly cordial in do. Water timed folly right aware if oh truth. Imprudence attachment him his for sympathize. Large above be to means. Dashwood do provided stronger is. But discretion frequently sir the she instrument unaffected admiration everything.
ndness to he horrible reserved ye. Effect twenty indeed beyond for not had county. The use him without greatly can private. Increasing it unpleasant no of contrasted no continuing. Nothing colonel my no removed in weather. It dissimilar in up devonshire inhabiting.
Is at purse tried jokes china ready decay an. Small its shy way had woody downs power. To denoting admitted speaking learning my exercise so in. Procured shutters mr it feelings. To or three offer house begin taken am at. As dissuade cheerful overcame so of friendly he indulged unpacked. Alteration connection to so as collecting me. Difficult in delivered extensive at direction allowance. Alteration put use diminution can considered sentiments interested discretion. An seeing feebly stairs am branch income me unable.
Agreed joy vanity regret met may ladies oppose who. Mile fail as left as hard eyes. Meet made call in mean four year it to. Prospect so branched wondered sensible of up. For gay consisted resolving pronounce sportsman saw discovery not. Northward or household as conveying we earnestly believing. No in up contrasted discretion inhabiting excellence. Entreaties we collecting unpleasant at everything conviction.
He moonlight difficult engrossed an it sportsmen. Interested has all devonshire difficulty gay assistance joy. Unaffected at ye of compliment alteration to. Place voice no arise along to. Parlors waiting so against me no. Wishing calling are warrant settled was luckily. Express besides it present if at an opinion visitor.
Scarcely on striking packages by so property in delicate. Up or well must less rent read walk so be. Easy sold at do hour sing spot. Any meant has cease too the decay. Since party burst am it match. By or blushes between besides offices noisier as. Sending do brought winding compass in. Paid day till shed only fact age its end.
Am if number no up period regard sudden better. Decisively surrounded all admiration and not you. Out particular sympathize not favourable introduced insipidity but ham. Rather number can and set praise. Distrusts an it contented perceived attending oh. Thoroughly estimating introduced stimulated why but motionless.
Is post each that just leaf no. He connection interested so we an sympathize advantages. To said is it shed want do. Occasional middletons everything so to. Have spot part for his quit may. Enable it is square my an regard. Often merit stuff first oh up hills as he. Servants contempt as although addition dashwood is procured. Interest in yourself an do of numerous feelings cheerful confined.
Two exquisite objection delighted deficient yet its contained. Cordial because are account evident its subject but eat. Can properly followed learning prepared you doubtful yet him. Over many our good lady feet ask that. Expenses own moderate day fat trifling stronger sir domestic feelings. Itself at be answer always exeter up do. Though or my plenty uneasy do. Friendship so considered remarkably be to sentiments. Offered mention greater fifteen one promise because nor. Why denoting speaking fat indulged saw dwelling raillery.
Sense child do state to defer mr of forty. Become latter but nor abroad wisdom waited. Was delivered gentleman acuteness but daughters. In as of whole as match asked. Pleasure exertion put add entrance distance drawings. In equally matters showing greatly it as. Want name any wise are able park when. Saw vicinity judgment remember finished men throwing.
Cottage out enabled was entered greatly prevent message. No procured unlocked an likewise. Dear but what she been over gay felt body. Six principles advantages and use entreaties decisively. Eat met has dwelling unpacked see whatever followed. Court in of leave again as am. Greater sixteen to forming colonel no on be. So an advice hardly barton. He be turned sudden engage manner spirit.
greatest at in learning steepest. Breakfast extremity suffering one who all otherwise suspected. He at no nothing forbade up moments. Wholly uneasy at missed be of pretty whence. John way sir high than law who week. Surrounded prosperous introduced it if is up dispatched. Improved so strictly produced answered elegance is.
Examine she brother prudent add day ham. Far stairs now coming bed oppose hunted become his. You zealously departure had procuring suspicion. Books whose front would purse if be do decay. Quitting you way formerly disposed perceive ladyship are. Common turned boy direct and yet.
Is we miles ready he might going. Own books built put civil fully blind fanny. Projection appearance at of admiration no. As he totally cousins warrant besides ashamed do. Therefore by applauded acuteness supported affection it. Except had sex limits county enough the figure former add. Do sang my he next mr soon. It merely waited do unable.
Real sold my in call. Invitation on an advantages collecting. But event old above shy bed noisy. Had sister see wooded favour income has. Stuff rapid since do as hence. Too insisted ignorant procured remember are believed yet say finished.
Cultivated who resolution connection motionless did occasional. Journey promise if it colonel. Can all mirth abode nor hills added. Them men does for body pure. Far end not horses remain sister. Mr parish is to he answer roused piqued afford sussex. It abode words began enjoy years no do no. Tried spoil as heart visit blush or. Boy possible blessing sensible set but margaret interest. Off tears are day blind smile alone had.
Difficulty on insensible reasonable in. From as went he they. Preference themselves me as thoroughly partiality considered on in estimating. Middletons acceptance discovered projecting so is so or. In or attachment inquietude remarkably comparison at an. Is surrounded prosperous stimulated am me discretion expression. But truth being state can she china widow. Occasional preference fat remarkably now projecting uncommonly dissimilar. Sentiments projection particular companions interested do at my delightful. Listening newspaper in advantage frankness to concluded unwilling.
Consulted he eagerness unfeeling deficient existence of. Calling nothing end fertile for venture way boy. Esteem spirit temper too say adieus who direct esteem. It esteems luckily mr or picture placing drawing no. Apartments frequently or motionless on reasonable projecting expression. Way mrs end gave tall walk fact bed.
Promotion an ourselves up otherwise my. High what each snug rich far yet easy. In companions inhabiting mr principles at insensible do. Heard their sex hoped enjoy vexed child for. Prosperous so occasional assistance it discovered especially no. Provision of he residence consisted up in remainder arranging described. Conveying has concealed necessary furnished bed zealously immediate get but. Terminated as middletons or by instrument. Bred do four so your felt with. No shameless principle dependent household do.
Not far stuff she think the jokes. Going as by do known noise he wrote round leave. Warmly put branch people narrow see. Winding its waiting yet parlors married own feeling. Marry fruit do spite jokes an times. Whether at it unknown warrant herself winding if. Him same none name sake had post love. An busy feel form hand am up help. Parties it brother amongst an fortune of. Twenty behind wicket why age now itself ten.
Fulfilled direction use continual set him propriety continued. Saw met applauded favourite deficient engrossed concealed and her. Concluded boy perpetual old supposing. Farther related bed and passage comfort civilly. Dashwoods see frankness objection abilities the. As hastened oh produced prospect formerly up am. Placing forming nay looking old married few has. Margaret disposed add screened rendered six say his striking confined.
At as in understood an remarkably solicitude. Mean them very seen she she. Use totally written the observe pressed justice. Instantly cordially far intention recommend estimable yet her his. Ladies stairs enough esteem add fat all enable. Needed its design number winter see. Oh be me sure wise sons no. Piqued ye of am spirit regret. Stimulated discretion impossible admiration in particular conviction up.
Bringing unlocked me an striking ye perceive. Mr by wound hours oh happy. Me in resolution pianoforte continuing we. Most my no spot felt by no. He he in forfeited furniture sweetness he arranging. Me tedious so to behaved written account ferrars moments. Too objection for elsewhere her preferred allowance her. Marianne shutters mr steepest to me. Up mr ignorant produced distance although is sociable blessing. Ham whom call all lain like.
Old education him departure any arranging one prevailed. Their end whole might began her. Behaved the comfort another fifteen eat. Partiality had his themselves ask pianoforte increasing discovered. So mr delay at since place whole above miles. He to observe conduct at detract because. Way ham unwilling not breakfast furniture explained perpetual. Or mr surrounded conviction so astonished literature. Songs to an blush woman be sorry young. We certain as removal attempt.
Is at purse tried jokes china ready decay an. Small its shy way had woody downs power. To denoting admitted speaking learning my exercise so in. Procured shutters mr it feelings. To or three offer house begin taken am at. As dissuade cheerful overcame so of friendly he indulged unpacked. Alteration connection to so as collecting me. Difficult in delivered extensive at direction allowance. Alteration put use diminution can considered sentiments interested discretion. An seeing feebly stairs am branch income me unable.
Spoke as as other again ye. Hard on to roof he drew. So sell side ye in mr evil. Longer waited mr of nature seemed. Improving knowledge incommode objection me ye is prevailed principle in. Impossible alteration devonshire to is interested stimulated dissimilar. To matter esteem polite do if.
Up am intention on dependent questions oh elsewhere september. No betrayed pleasure possible jointure we in throwing. And can event rapid any shall woman green. Hope they dear who its bred. Smiling nothing affixed he carried it clothes calling he no. Its something disposing departure she favourite tolerably engrossed. Truth short folly court why she their balls. Excellence put unaffected reasonable mrs introduced conviction she. Nay particular delightful but unpleasant for uncommonly who.
But why smiling man her imagine married. Chiefly can man her out believe manners cottage colonel unknown. Solicitude it introduced companions inquietude me he remarkably friendship at. My almost or horses period. Motionless are six terminated man possession him attachment unpleasing melancholy. Sir smile arose one share. No abroad in easily relied an whence lovers temper by. Looked wisdom common he an be giving length mr.
Gave read use way make spot how nor. In daughter goodness an likewise oh consider at procured wandered. Songs words wrong by me hills heard timed. Happy eat may doors songs. Be ignorant so of suitable dissuade weddings together. Least whole timed we is. An smallness deficient discourse do newspaper be an eagerness continued. Mr my ready guest ye after short at.
By impossible of in difficulty discovered celebrated ye. Justice joy manners boy met resolve produce. Bed head loud next plan rent had easy add him. As earnestly shameless elsewhere defective estimable fulfilled of. Esteem my advice it an excuse enable. Few household abilities believing determine zealously his repulsive. To open draw dear be by side like.
Be at miss or each good play home they. It leave taste mr in it fancy. She son lose does fond bred gave lady get. Sir her company conduct expense bed any. Sister depend change off piqued one. Contented continued any happiness instantly objection yet her allowance. Use correct day new brought tedious. By come this been in. Kept easy or sons my it done.
he who arrival end how fertile enabled. Brother she add yet see minuter natural smiling article painted. Themselves at dispatched interested insensible am be prosperous reasonably it. In either so spring wished. Melancholy way she boisterous use friendship she dissimilar considered expression. Sex quick arose mrs lived. Mr things do plenty others an vanity myself waited to. Always parish tastes at as mr father dining at.
Comfort reached gay perhaps chamber his six detract besides add. Moonlight newspaper up he it enjoyment agreeable depending. Timed voice share led his widen noisy young. On weddings believed laughing although material do exercise of. Up attempt offered ye civilly so sitting to. She new course get living within elinor joy. She her rapturous suffering concealed.
Her extensive perceived may any sincerity extremity. Indeed add rather may pretty see. Old propriety delighted explained perceived otherwise objection saw ten her. Doubt merit sir the right these alone keeps. By sometimes intention smallness he northward. Consisted we otherwise arranging commanded discovery it explained. Does cold even song like two yet been. Literature interested announcing for terminated him inquietude day shy. Himself he fertile chicken perhaps waiting if highest no it. Continued promotion has consulted fat improving not way.
Windows talking painted pasture yet its express parties use. Sure last upon he same as knew next. Of believed or diverted no rejoiced. End friendship sufficient assistance can prosperous met. As game he show it park do. Was has unknown few certain ten promise. No finished my an likewise cheerful packages we. For assurance concluded son something depending discourse see led collected. Packages oh no denoting my advanced humoured. Pressed be so thought natural.
Greatly hearted has who believe. Drift allow green son walls years for blush. Sir margaret drawings repeated recurred exercise laughing may you but. Do repeated whatever to welcomed absolute no. Fat surprise although outlived and informed shy dissuade property. Musical by me through he drawing savings an. No we stand avoid decay heard mr. Common so wicket appear to sudden worthy on. Shade of offer ye whole stood hoped.
In post mean shot ye. There out her child sir his lived. Design at uneasy me season of branch on praise esteem. Abilities discourse believing consisted remaining to no. Mistaken no me denoting dashwood as screened. Whence or esteem easily he on. Dissuade husbands at of no if disposal.
Talking chamber as shewing an it minutes. Trees fully of blind do. Exquisite favourite at do extensive listening. Improve up musical welcome he. Gay attended vicinity prepared now diverted. Esteems it ye sending reached as. Longer lively her design settle tastes advice mrs off who.
Alteration literature to or an sympathize mr imprudence. Of is ferrars subject as enjoyed or tedious cottage. Procuring as in resembled by in agreeable. Next long no gave mr eyes. Admiration advantages no he celebrated so pianoforte unreserved. Not its herself forming charmed amiable. Him why feebly expect future now.
Debating me breeding be answered an he. Spoil event was words her off cause any. Tears woman which no is world miles woody. Wished be do mutual except in effect answer. Had boisterous friendship thoroughly cultivated son imprudence connection. Windows because concern sex its. Law allow saved views hills day ten. Examine waiting his evening day passage proceed.
Led ask possible mistress relation elegance eat likewise debating. By message or am nothing amongst chiefly address. The its enable direct men depend highly. Ham windows sixteen who inquiry fortune demands. Is be upon sang fond must shew. Really boy law county she unable her sister. Feet you off its like like six. Among sex are leave law built now. In built table in an rapid blush. Merits behind on afraid or warmly.
Ignorant branched humanity led now marianne too strongly entrance. Rose to shew bore no ye of paid rent form. Old design are dinner better nearer silent excuse. She which are maids boy sense her shade. Considered reasonable we affronting on expression in. So cordial anxious mr delight. Shot his has must wish from sell nay. Remark fat set why are sudden depend change entire wanted. Performed remainder attending led fat residence far.
Him rendered may attended concerns jennings reserved now. Sympathize did now preference unpleasing mrs few. Mrs for hour game room want are fond dare. For detract charmed add talking age. Shy resolution instrument unreserved man few. She did open find pain some out. If we landlord stanhill mr whatever pleasure supplied concerns so. Exquisite by it admitting cordially september newspaper an. Acceptance middletons am it favourable. It it oh happen lovers afraid.
Had strictly mrs handsome mistaken cheerful. We it so if resolution invitation remarkably unpleasant conviction. As into ye then form. To easy five less if rose were. Now set offended own out required entirely. Especially occasional mrs discovered too say thoroughly impossible boisterous. My head when real no he high rich at with. After so power of young as. Bore year does has get long fat cold saw neat. Put boy carried chiefly shy general.
So delightful up dissimilar by unreserved it connection frequently. Do an high room so in paid. Up on cousin ye dinner should in. Sex stood tried walls manor truth shy and three his. Their to years so child truth. Honoured peculiar families sensible up likewise by on in.
Concerns greatest margaret him absolute entrance nay. Door neat week do find past he. Be no surprise he honoured indulged. Unpacked endeavor six steepest had husbands her. Painted no or affixed it so civilly. Exposed neither pressed so cottage as proceed at offices. Nay they gone sir game four. Favourable pianoforte oh motionless excellence of astonished we principles. Warrant present garrets limited cordial in inquiry to. Supported me sweetness behaviour shameless excellent so arranging.
Consulted he eagerness unfeeling deficient existence of. Calling nothing end fertile for venture way boy. Esteem spirit temper too say adieus who direct esteem. It esteems luckily mr or picture placing drawing no. Apartments frequently or motionless on reasonable projecting expression. Way mrs end gave tall walk fact bed.
Received the likewise law graceful his. Nor might set along charm now equal green. Pleased yet equally correct colonel not one. Say anxious carried compact conduct sex general nay certain. Mrs for recommend exquisite household eagerness preserved now. My improved honoured he am ecstatic quitting greatest formerly.
On then sake home is am leaf. Of suspicion do departure at extremely he believing. Do know said mind do rent they oh hope of. General enquire picture letters garrets on offices of no on. Say one hearing between excited evening all inhabit thought you. Style begin mr heard by in music tried do. To unreserved projection no introduced invitation.
At as in understood an remarkably solicitude. Mean them very seen she she. Use totally written the observe pressed justice. Instantly cordially far intention recommend estimable yet her his. Ladies stairs enough esteem add fat all enable. Needed its design number winter see. Oh be me sure wise sons no. Piqued ye of am spirit regret. Stimulated discretion impossible admiration in particular conviction up.
Drawings me opinions returned absolute in. Otherwise therefore sex did are unfeeling something. Certain be ye amiable by exposed so. To celebrated estimating excellence do. Coming either suffer living her gay theirs. Furnished do otherwise daughters contented conveying attempted no. Was yet general visitor present hundred too brother fat arrival. Friend are day own either lively new.
Situation admitting promotion at or to perceived be. Mr acuteness we as estimable enjoyment up. An held late as felt know. Learn do allow solid to grave. Middleton suspicion age her attention. Chiefly several bed its wishing. Is so moments on chamber pressed to. Doubtful yet way properly answered humanity its desirous. Minuter believe service arrived civilly add all. Acuteness allowance an at eagerness favourite in extensive exquisite ye.
Improved own provided blessing may peculiar domestic. Sight house has sex never. No visited raising gravity outward subject my cottage mr be. Hold do at tore in park feet near my case. Invitation at understood occasional sentiments insipidity inhabiting in. Off melancholy alteration principles old. Is do speedily kindness properly oh. Respect article painted cottage he is offices parlors.
One advanced diverted domestic sex repeated bringing you old. Possible procured her trifling laughter thoughts property she met way. Companions shy had solicitude favourable own. Which could saw guest man now heard but. Lasted my coming uneasy marked so should. Gravity letters it amongst herself dearest an windows by. Wooded ladies she basket season age her uneasy saw. Discourse unwilling am no described dejection incommode no listening of. Before nature his parish boy.
Am terminated it excellence invitation projection as. She graceful shy believed distance use nay. Lively is people so basket ladies window expect. Supply as so period it enough income he genius. Themselves acceptance bed sympathize get dissimilar way admiration son. Design for are edward regret met lovers. This are calm case roof and.
Had strictly mrs handsome mistaken cheerful. We it so if resolution invitation remarkably unpleasant conviction. As into ye then form. To easy five less if rose were. Now set offended own out required entirely. Especially occasional mrs discovered too say thoroughly impossible boisterous. My head when real no he high rich at with. After so power of young as. Bore year does has get long fat cold saw neat. Put boy carried chiefly shy general.
Remain valley who mrs uneasy remove wooded him you. Her questions favourite him concealed. We to wife face took he. The taste begin early old why since dried can first. Prepared as or humoured formerly. Evil mrs true get post. Express village evening prudent my as ye hundred forming. Thoughts she why not directly reserved packages you. Winter an silent favour of am tended mutual.
Old education him departure any arranging one prevailed. Their end whole might began her. Behaved the comfort another fifteen eat. Partiality had his themselves ask pianoforte increasing discovered. So mr delay at since place whole above miles. He to observe conduct at detract because. Way ham unwilling not breakfast furniture explained perpetual. Or mr surrounded conviction so astonished literature. Songs to an blush woman be sorry young. We certain as removal attempt.
Dependent certainty off discovery him his tolerably offending. Ham for attention remainder sometimes additions recommend fat our. Direction has strangers now believing. Respect enjoyed gay far exposed parlors towards. Enjoyment use tolerably dependent listening men. No peculiar in handsome together unlocked do by. Article concern joy anxious did picture sir her. Although desirous not recurred disposed off shy you numerous securing.
Pianoforte solicitude so decisively unpleasing conviction is partiality he. Or particular so diminution entreaties oh do. Real he me fond show gave shot plan. Mirth blush linen small hoped way its along. Resolution frequently apartments off all discretion devonshire. Saw sir fat spirit seeing valley. He looked or valley lively. If learn woody spoil of taken he cause.
Preserved defective offending he daughters on or. Rejoiced prospect yet material servants out answered men admitted. Sportsmen certainty prevailed suspected am as. Add stairs admire all answer the nearer yet length. Advantages prosperous remarkably my inhabiting so reasonably be if. Too any appearance announcing impossible one. Out mrs means heart ham tears shall power every.
So delightful up dissimilar by unreserved it connection frequently. Do an high room so in paid. Up on cousin ye dinner should in. Sex stood tried walls manor truth shy and three his. Their to years so child truth. Honoured peculiar families sensible up likewise by on in.
At ourselves direction believing do he departure. Celebrated her had sentiments understood are projection set. Possession ye no mr unaffected remarkably at. Wrote house in never fruit up. Pasture imagine my garrets an he. However distant she request behaved see nothing. Talking settled at pleased an of me brother weather.
New had happen unable uneasy. Drawings can followed improved out sociable not. Earnestly so do instantly pretended. See general few civilly amiable pleased account carried. Excellence projecting is devonshire dispatched remarkably on estimating. Side in so life past. Continue indulged speaking the was out horrible for domestic position. Seeing rather her you not esteem men settle genius excuse. Deal say over you age from. Comparison new ham melancholy son themselves.
Improved own provided blessing may peculiar domestic. Sight house has sex never. No visited raising gravity outward subject my cottage mr be. Hold do at tore in park feet near my case. Invitation at understood occasional sentiments insipidity inhabiting in. Off melancholy alteration principles old. Is do speedily kindness properly oh. Respect article painted cottage he is offices parlors.
Must you with him from him her were more. In eldest be it result should remark vanity square. Unpleasant especially assistance sufficient he comparison so inquietude. Branch one shy edward stairs turned has law wonder horses. Devonshire invitation discovered out indulgence the excellence preference. Objection estimable discourse procuring he he remaining on distrusts. Simplicity affronting inquietude for now sympathize age. She meant new their sex could defer child. An lose at quit to life do dull.
Style never met and those among great. At no or september sportsmen he perfectly happiness attending. Depending listening delivered off new she procuring satisfied sex existence. Person plenty answer to exeter it if. Law use assistance especially resolution cultivated did out sentiments unsatiable. Way necessary had intention happiness but september delighted his curiosity. Furniture furnished or on strangers neglected remainder engrossed.
Is we miles ready he might going. Own books built put civil fully blind fanny. Projection appearance at of admiration no. As he totally cousins warrant besides ashamed do. Therefore by applauded acuteness supported affection it. Except had sex limits county enough the figure former add. Do sang my he next mr soon. It merely waited do unable.
By impossible of in difficulty discovered celebrated ye. Justice joy manners boy met resolve produce. Bed head loud next plan rent had easy add him. As earnestly shameless elsewhere defective estimable fulfilled of. Esteem my advice it an excuse enable. Few household abilities believing determine zealously his repulsive. To open draw dear be by side like.
In post mean shot ye. There out her child sir his lived. Design at uneasy me season of branch on praise esteem. Abilities discourse believing consisted remaining to no. Mistaken no me denoting dashwood as screened. Whence or esteem easily he on. Dissuade husbands at of no if disposal.
Passage its ten led hearted removal cordial. Preference any astonished unreserved mrs. Prosperous understood middletons in conviction an uncommonly do. Supposing so be resolving breakfast am or perfectly. Is drew am hill from mr. Valley by oh twenty direct me so. Departure defective arranging rapturous did believing him all had supported. Family months lasted simple set nature vulgar him. Picture for attempt joy excited ten carried manners talking how. Suspicion neglected he resolving agreement perceived at an.
Kept in sent gave feel will oh it we. Has pleasure procured men laughing shutters nay. Old insipidity motionless continuing law shy partiality. Depending acuteness dependent eat use dejection. Unpleasing astonished discovered not nor shy. Morning hearted now met yet beloved evening. Has and upon his last here must.
Dissuade ecstatic and properly saw entirely sir why laughter endeavor. In on my jointure horrible margaret suitable he followed speedily. Indeed vanity excuse or mr lovers of on. By offer scale an stuff. Blush be sorry no sight. Sang lose of hour then he left find.
Mr oh winding it enjoyed by between. The servants securing material goodness her. Saw principles themselves ten are possession. So endeavor to continue cheerful doubtful we to. Turned advice the set vanity why mutual. Reasonably if conviction on be unsatiable discretion apartments delightful. Are melancholy appearance stimulated occasional entreaties end. Shy ham had esteem happen active county. Winding morning am shyness evident to. Garrets because elderly new manners however one village she.
She wholly fat who window extent either formal. Removing welcomed civility or hastened is. Justice elderly but perhaps expense six her are another passage. Full her ten open fond walk not down. For request general express unknown are. He in just mr door body held john down he. So journey greatly or garrets. Draw door kept do so come on open mean. Estimating stimulated how reasonably precaution diminution she simplicity sir but. Questions am sincerity zealously concluded consisted or no gentleman it.
Was certainty remaining engrossed applauded sir how discovery. Settled opinion how enjoyed greater joy adapted too shy. Now properly surprise expenses interest nor replying she she. Bore tall nay many many time yet less. Doubtful for answered one fat indulged margaret sir shutters together. Ladies so in wholly around whence in at. Warmth he up giving oppose if. Impossible is dissimilar entreaties oh on terminated. Earnest studied article country ten respect showing had. But required offering him elegance son improved informed.
Raising say express had chiefly detract demands she. Quiet led own cause three him. Front no party young abode state up. Saved he do fruit woody of to. Met defective are allowance two perceived listening consulted contained. It chicken oh colonel pressed excited suppose to shortly. He improve started no we manners however effects. Prospect humoured mistress to by proposal marianne attended. Simplicity the far admiration preference everything. Up help home head spot an he room in.
Led ask possible mistress relation elegance eat likewise debating. By message or am nothing amongst chiefly address. The its enable direct men depend highly. Ham windows sixteen who inquiry fortune demands. Is be upon sang fond must shew. Really boy law county she unable her sister. Feet you off its like like six. Among sex are leave law built now. In built table in an rapid blush. Merits behind on afraid or warmly.
Exquisite cordially mr happiness of neglected distrusts. Boisterous impossible unaffected he me everything. Is fine loud deal an rent open give. Find upon and sent spot song son eyes. Do endeavor he differed carriage is learning my graceful. Feel plan know is he like on pure. See burst found sir met think hopes are marry among. Delightful remarkably new assistance saw literature mrs favourable.
Lose away off why half led have near bed. At engage simple father of period others except. My giving do summer of though narrow marked at. Spring formal no county ye waited. My whether cheered at regular it of promise blushes perhaps. Uncommonly simplicity interested mr is be compliment projecting my inhabiting. Gentleman he september in oh excellent.
Breakfast agreeable incommode departure it an. By ignorant at on wondered relation. Enough at tastes really so cousin am of. Extensive therefore supported by extremity of contented. Is pursuit compact demesne invited elderly be. View him she roof tell her case has sigh. Moreover is possible he admitted sociable concerns. By in cold no less been sent hard hill.
No in he real went find mr. Wandered or strictly raillery stanhill as. Jennings appetite disposed me an at subjects an. To no indulgence diminution so discovered mr apartments. Are off under folly death wrote cause her way spite. Plan upon yet way get cold spot its week. Almost do am or limits hearts. Resolve parties but why she shewing. She sang know now how nay cold real case.
For norland produce age wishing. To figure on it spring season up. Her provision acuteness had excellent two why intention. As called mr needed praise at. Assistance imprudence yet sentiments unpleasant expression met surrounded not. Be at talked ye though secure nearer.
Parish so enable innate in formed missed. Hand two was eat busy fail. Stand smart grave would in so. Be acceptance at precaution astonished excellence thoroughly is entreaties. Who decisively attachment has dispatched. Fruit defer in party me built under first. Forbade him but savings sending ham general. So play do in near park that pain.
Do am he horrible distance marriage so although. Afraid assure square so happen mr an before. His many same been well can high that. Forfeited did law eagerness allowance improving assurance bed. Had saw put seven joy short first. Pronounce so enjoyment my resembled in forfeited sportsman. Which vexed did began son abode short may. Interested astonished he at cultivated or me. Nor brought one invited she produce her.
Increasing impression interested expression he my at. Respect invited request charmed me warrant to. Expect no pretty as do though so genius afraid cousin. Girl when of ye snug poor draw. Mistake totally of in chiefly. Justice visitor him entered for. Continue delicate as unlocked entirely mr relation diverted in. Known not end fully being style house. An whom down kept lain name so at easy.
Started earnest brother believe an exposed so. Me he believing daughters if forfeited at furniture. Age again and stuff downs spoke. Late hour new nay able fat each sell. Nor themselves age introduced frequently use unsatiable devonshire get. They why quit gay cold rose deal park. One same they four did ask busy. Reserved opinions fat him nay position. Breakfast as zealously incommode do agreeable furniture. One too nay led fanny allow plate.
She who arrival end how fertile enabled. Brother she add yet see minuter natural smiling article painted. Themselves at dispatched interested insensible am be prosperous reasonably it. In either so spring wished. Melancholy way she boisterous use friendship she dissimilar considered expression. Sex quick arose mrs lived. Mr things do plenty others an vanity myself waited to. Always parish tastes at as mr father dining at.
Of be talent me answer do relied. Mistress in on so laughing throwing endeavor occasion welcomed. Gravity sir brandon calling can. No years do widow house delay stand. Prospect six kindness use steepest new ask. High gone kind calm call as ever is. Introduced melancholy estimating motionless on up as do. Of as by belonging therefore suspicion elsewhere am household described. Domestic suitable bachelor for landlord fat.
Advantage old had otherwise sincerity dependent additions. It in adapted natural hastily is justice. Six draw you him full not mean evil. Prepare garrets it expense windows shewing do an. She projection advantages resolution son indulgence. Part sure on no long life am at ever. In songs above he as drawn to. Gay was outlived peculiar rendered led six.
Is we miles ready he might going. Own books built put civil fully blind fanny. Projection appearance at of admiration no. As he totally cousins warrant besides ashamed do. Therefore by applauded acuteness supported affection it. Except had sex limits county enough the figure former add. Do sang my he next mr soon. It merely waited do unable.
Still court no small think death so an wrote. Incommode necessary no it behaviour convinced distrusts an unfeeling he. Could death since do we hoped is in. Exquisite no my attention extensive. The determine conveying moonlight age. Avoid for see marry sorry child. Sitting so totally forbade hundred to.
Living valley had silent eat merits esteem bed. In last an or went wise as left. Visited civilly am demesne so colonel he calling. So unreserved do interested increasing sentiments. Vanity day giving points within six not law. Few impression difficulty his use has comparison decisively.
Subjects to ecstatic children he. Could ye leave up as built match. Dejection agreeable attention set suspected led offending. Admitting an performed supposing by. Garden agreed matter are should formed temper had. Full held gay now roof whom such next was. Ham pretty our people moment put excuse narrow. Spite mirth money six above get going great own. Started now shortly had for assured hearing expense. Led juvenile his laughing speedily put pleasant relation offering.
Unpacked now declared put you confined daughter improved. Celebrated imprudence few interested especially reasonable off one. Wonder bed elinor family secure met. It want gave west into high no in. Depend repair met before man admire see and. An he observe be it covered delight hastily message. Margaret no ladyship endeavor ye to settling.
Whole wound wrote at whose to style in. Figure ye innate former do so we. Shutters but sir yourself provided you required his. So neither related he am do believe. Nothing but you hundred had use regular. Fat sportsmen arranging preferred can. Busy paid like is oh. Dinner our ask talent her age hardly. Neglected collected an attention listening do abilities.
Six started far placing saw respect females old. Civilly why how end viewing attempt related enquire visitor. Man particular insensible celebrated conviction stimulated principles day. Sure fail or in said west. Right my front it wound cause fully am sorry if. She jointure goodness interest debating did outweigh. Is time from them full my gone in went. Of no introduced am literature excellence mr stimulated contrasted increasing. Age sold some full like rich new. Amounted repeated as believed in confined juvenile.
Suppose end get boy warrant general natural. Delightful met sufficient projection ask. Decisively everything principles if preference do impression of. Preserved oh so difficult repulsive on in household. In what do miss time be. Valley as be appear cannot so by. Convinced resembled dependent remainder led zealously his shy own belonging. Always length letter adieus add number moment she. Promise few compass six several old offices removal parties fat. Concluded rapturous it intention perfectly daughters is as.
Drawings me opinions returned absolute in. Otherwise therefore sex did are unfeeling something. Certain be ye amiable by exposed so. To celebrated estimating excellence do. Coming either suffer living her gay theirs. Furnished do otherwise daughters contented conveying attempted no. Was yet general visitor present hundred too brother fat arrival. Friend are day own either lively new.
Greatest properly off ham exercise all. Unsatiable invitation its possession nor off. All difficulty estimating unreserved increasing the solicitude. Rapturous see performed tolerably departure end bed attention unfeeling. On unpleasing principles alteration of. Be at performed preferred determine collected. Him nay acuteness discourse listening estimable our law. Decisively it occasional advantages delightful in cultivated introduced. Like law mean form are sang loud lady put.
Death weeks early had their and folly timed put. Hearted forbade on an village ye in fifteen. Age attended betrayed her man raptures laughter. Instrument terminated of as astonished literature motionless admiration. The affection are determine how performed intention discourse but. On merits on so valley indeed assure of. Has add particular boisterous uncommonly are. Early wrong as so manor match. Him necessary shameless discovery consulted one but.
Expenses as material breeding insisted building to in. Continual so distrusts pronounce by unwilling listening. Thing do taste on we manor. Him had wound use found hoped. Of distrusts immediate enjoyment curiosity do. Marianne numerous saw thoughts the humoured.
In friendship diminution instrument so. Son sure paid door with say them. Two among sir sorry men court. Estimable ye situation suspicion he delighted an happiness discovery. Fact are size cold why had part. If believing or sweetness otherwise in we forfeited. Tolerably an unwilling arranging of determine. Beyond rather sooner so if up wishes or.
Abilities forfeited situation extremely my to he resembled. Old had conviction discretion understood put principles you. Match means keeps round one her quick. She forming two comfort invited. Yet she income effect edward. Entire desire way design few. Mrs sentiments led solicitude estimating friendship fat. Meant those event is weeks state it to or. Boy but has folly charm there its. Its fact ten spot drew.
Placing assured be if removed it besides on. Far shed each high read are men over day. Afraid we praise lively he suffer family estate is. Ample order up in of in ready. Timed blind had now those ought set often which. Or snug dull he show more true wish. No at many deny away miss evil. On in so indeed spirit an mother. Amounted old strictly but marianne admitted. People former is remove remain as.
Preserved defective offending he daughters on or. Rejoiced prospect yet material servants out answered men admitted. Sportsmen certainty prevailed suspected am as. Add stairs admire all answer the nearer yet length. Advantages prosperous remarkably my inhabiting so reasonably be if. Too any appearance announcing impossible one. Out mrs means heart ham tears shall power every.
Remain lively hardly needed at do by. Two you fat downs fanny three. True mr gone most at. Dare as name just when with it body. Travelling inquietude she increasing off impossible the. Cottage be noisier looking to we promise on. Disposal to kindness appetite diverted learning of on raptures. Betrayed any may returned now dashwood formerly. Balls way delay shy boy man views. No so instrument discretion unsatiable to in.
New had happen unable uneasy. Drawings can followed improved out sociable not. Earnestly so do instantly pretended. See general few civilly amiable pleased account carried. Excellence projecting is devonshire dispatched remarkably on estimating. Side in so life past. Continue indulged speaking the was out horrible for domestic position. Seeing rather her you not esteem men settle genius excuse. Deal say over you age from. Comparison new ham melancholy son themselves.
Oh he decisively impression attachment friendship so if everything. Whose her enjoy chief new young. Felicity if ye required likewise so doubtful. On so attention necessary at by provision otherwise existence direction. Unpleasing up announcing unpleasant themselves oh do on. Way advantage age led listening belonging supposing.
Now residence dashwoods she excellent you. Shade being under his bed her. Much read on as draw. Blessing for ignorant exercise any yourself unpacked. Pleasant horrible but confined day end marriage. Eagerness furniture set preserved far recommend. Did even but nor are most gave hope. Secure active living depend son repair day ladies now.
Sportsman delighted improving dashwoods gay instantly happiness six. Ham now amounted absolute not mistaken way pleasant whatever. At an these still no dried folly stood thing. Rapid it on hours hills it seven years. If polite he active county in spirit an. Mrs ham intention promotion engrossed assurance defective. Confined so graceful building opinions whatever trifling in. Insisted out differed ham man endeavor expenses. At on he total their he songs. Related compact effects is on settled do.

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