Compare commits

..

620 Commits

Author SHA1 Message Date
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
286 changed files with 37317 additions and 12052 deletions
+12
View File
@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [mholt] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+126
View File
@@ -0,0 +1,126 @@
# Used as inspiration: https://github.com/mvdan/github-actions-golang
name: Cross-Platform
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
test:
strategy:
# Default is true, cancels jobs for other platforms in the matrix if one fails
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
go-version: [ 1.14.x ]
# Set some variables per OS, usable via ${{ matrix.VAR }}
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
# SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
include:
- os: ubuntu-latest
CADDY_BIN_PATH: ./cmd/caddy/caddy
SUCCESS: 0
- os: macos-latest
CADDY_BIN_PATH: ./cmd/caddy/caddy
SUCCESS: 0
- os: windows-latest
CADDY_BIN_PATH: ./cmd/caddy/caddy.exe
SUCCESS: 'True'
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
# These tools would be useful if we later decide to reinvestigate
# publishing test/coverage reports to some tool for easier consumption
# - name: Install test and coverage analysis tools
# run: |
# go get github.com/axw/gocov/gocov
# go get github.com/AlekSi/gocov-xml
# go get -u github.com/jstemmer/go-junit-report
# echo "::add-path::$(go env GOPATH)/bin"
- name: Print Go version and environment
id: vars
run: |
printf "Using go at: $(which go)\n"
printf "Go version: $(go version)\n"
printf "\n\nGo environment:\n\n"
go env
printf "\n\nSystem environment:\n\n"
env
# Calculate the short SHA1 hash of the git commit
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
- 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 -a -ldflags="-w -s" -v
- name: Publish Build Artifact
uses: actions/upload-artifact@v1
with:
name: caddy_v2_${{ runner.os }}_${{ steps.vars.outputs.short_sha }}
path: ${{ matrix.CADDY_BIN_PATH }}
# Commented bits below were useful to allow the job to continue
# even if the tests fail, so we can publish the report separately
# For info about set-output, see https://stackoverflow.com/questions/57850553/github-actions-check-steps-status
- name: Run tests
# id: step_test
# continue-on-error: true
run: |
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
go test -v -coverprofile="cover-profile.out" -short -race ./...
# echo "::set-output name=status::$?"
# Relevant step if we reinvestigate publishing test/coverage reports
# - name: Prepare coverage reports
# run: |
# mkdir coverage
# gocov convert cover-profile.out > coverage/coverage.json
# # Because Windows doesn't work with input redirection like *nix, but output redirection works.
# (cat ./coverage/coverage.json | gocov-xml) > coverage/coverage.xml
# To return the correct result even though we set 'continue-on-error: true'
# - name: Coerce correct build result
# if: matrix.os != 'windows-latest' && steps.step_test.outputs.status != ${{ matrix.SUCCESS }}
# run: |
# echo "step_test ${{ steps.step_test.outputs.status }}\n"
# exit 1
# 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 }}
+75
View File
@@ -0,0 +1,75 @@
name: Fuzzing
on:
# Daily midnight fuzzing
schedule:
- cron: '0 0 * * *'
jobs:
fuzzing:
name: Fuzzing
strategy:
matrix:
os: [ ubuntu-latest ]
go-version: [ 1.14.x ]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Download go-fuzz tools and the Fuzzit CLI, move Fuzzit CLI to GOBIN
# If we decide we need to prevent this from running on forks, we can use this line:
# if: github.repository == 'caddyserver/caddy'
run: |
go get -v github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
wget -q -O fuzzit https://github.com/fuzzitdev/fuzzit/releases/download/v2.4.77/fuzzit_Linux_x86_64
chmod a+x fuzzit
mv fuzzit $(go env GOPATH)/bin
echo "::add-path::$(go env GOPATH)/bin"
- name: Generate fuzzers & submit them to Fuzzit
continue-on-error: true
env:
FUZZIT_API_KEY: ${{ secrets.FUZZIT_API_KEY }}
SYSTEM_PULLREQUEST_SOURCEBRANCH: ${{ github.ref }}
BUILD_SOURCEVERSION: ${{ github.sha }}
run: |
# debug
echo "PR Source Branch: $SYSTEM_PULLREQUEST_SOURCEBRANCH"
echo "Source version: $BUILD_SOURCEVERSION"
declare -A fuzzers_funcs=(\
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="FuzzParseAddress" \
["./caddyconfig/caddyfile/parse_fuzz.go"]="FuzzParseCaddyfile" \
["./listeners_fuzz.go"]="FuzzParseNetworkAddress" \
["./replacer_fuzz.go"]="FuzzReplacer" \
)
declare -A fuzzers_targets=(\
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="parse-address" \
["./caddyconfig/caddyfile/parse_fuzz.go"]="parse-caddyfile" \
["./listeners_fuzz.go"]="parse-network-address" \
["./replacer_fuzz.go"]="replacer" \
)
fuzz_type="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
+53
View File
@@ -0,0 +1,53 @@
name: Release
on:
push:
tags:
- 'v*.*.*'
jobs:
release:
name: Release
strategy:
matrix:
os: [ ubuntu-latest ]
go-version: [ 1.14.x ]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
# So GoReleaser can generate the changelog properly
- name: Unshallowify the repo clone
run: git fetch --prune --unshallow
- 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
# https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
- name: Get the version
id: get_version
run: echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\//}"
# GoReleaser will take care of publishing those artifacts into the release
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v1
with:
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.get_version.outputs.version_tag }}
+21 -12
View File
@@ -1,14 +1,23 @@
.DS_Store
Thumbs.db
_gitignore/
Vagrantfile
.vagrant/
dist/builds/
dist/release/
error.log
access.log
/*.conf
*.log
Caddyfile
!caddyfile/
# artifacts from pprof tooling
*.prof
*.test
# build artifacts
cmd/caddy/caddy
cmd/caddy/caddy.exe
# 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
+60
View File
@@ -0,0 +1,60 @@
before:
hooks:
- 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
goarm:
- 6
- 7
ignore:
- goos: darwin
goarch: arm
flags:
- -trimpath
ldflags:
- -s -w
archives:
- format_overrides:
- goos: windows
format: zip
replacements:
darwin: mac
checksum:
algorithm: sha512
release:
github:
owner: caddyserver
name: caddy
draft: true
prerelease: auto
changelog:
sort: asc
filters:
exclude:
- '^chore:'
- '^ci:'
- '^docs?:'
- '^tests?:'
- '^\w+\s+' # a hack to remove commit messages without colons thus don't correspond to a package
-7
View File
@@ -1,7 +0,0 @@
language: go
go:
- 1.4
- tip
script: go test ./...
+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>
-32
View File
@@ -1,32 +0,0 @@
## Contributing to Caddy
**[Join us on Slack](https://gophers.slack.com/messages/caddy/)** to chat with other Caddy developers! ([Request an invite](http://bit.ly/go-slack-signup), then join the #caddy channel.)
This project gladly accepts contributions and we encourage interested users to get involved!
#### For small tweaks, bug fixes, and tests
Submit [pull requests](https://github.com/mholt/caddy/pulls) at any time. Thank you for helping out in simple ways! Bug fixes should be under test to assert correct behavior.
#### Ideas, questions, bug reports
You should totally [open an issue](https://github.com/mholt/caddy/issues) with your ideas, questions, and bug reports, if one does not already exist for it. Bug reports should state expected behavior and contain clear instructions for reproducing the problem.
#### New features
Before submitting a pull request, please open an issue first to discuss it and claim it. This prevents overlapping efforts and keeps the project in-line with its goals. If you prefer to discuss the feature privately, you can reach other developers on Slack or you may email me directly. (My email address is below.)
And don't forget to write tests for new features!
#### Vulnerabilities
If you've found a vulnerability that is serious, please email me: Matthew dot Holt at Gmail. If it's not a big deal, a pull request will probably be faster.
## Thank you
Thanks for your help! Caddy would not be what it is today without your contributions.
+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.
+118 -95
View File
@@ -1,129 +1,152 @@
[![Caddy](https://caddyserver.com/resources/images/caddy-boxed.png)](https://caddyserver.com)
[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/mholt/caddy) [![Build Status](https://img.shields.io/travis/mholt/caddy.svg?style=flat-square)](https://travis-ci.org/mholt/caddy)
Caddy is a lightweight, general-purpose web server for Windows, Mac, Linux, BSD, and [Android](https://github.com/mholt/caddy/wiki/Running-Caddy-on-Android). It is a capable alternative to other popular and easy to use web servers.
The most notable features are HTTP/2, Virtual Hosts, TLS + SNI, and easy configuration with a [Caddyfile](https://caddyserver.com/docs/caddyfile). Usually, you have one Caddyfile per site. Most directives for the Caddyfile invoke a layer of middleware which can be [used in your own Go programs](https://github.com/mholt/caddy/wiki/Using-Caddy-Middleware-in-Your-Own-Programs).
[Download](https://github.com/mholt/caddy/releases) · [User Guide](https://caddyserver.com/docs)
<p align="center">
<a href="https://caddyserver.com"><img src="https://user-images.githubusercontent.com/1128849/36338535-05fb646a-136f-11e8-987b-e6901e717d5a.png" alt="Caddy" width="450"></a>
</p>
<h3 align="center">Every site on HTTPS</h3>
<p align="center">Caddy is an extensible server platform that uses TLS by default.</p>
<p align="center">
<a href="https://github.com/caddyserver/caddy/actions?query=workflow%3ACross-Platform"><img src="https://github.com/caddyserver/caddy/workflows/Cross-Platform/badge.svg"></a>
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-blue.svg"></a>
<a href="https://app.fuzzit.dev/orgs/caddyserver-gh/dashboard"><img src="https://app.fuzzit.dev/badge?org_id=caddyserver-gh"></a>
<br>
<a href="https://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" alt="@caddyserver on Twitter"></a>
<a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
<a href="https://sourcegraph.com/github.com/caddyserver/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/caddyserver/caddy/-/badge.svg" alt="Caddy on Sourcegraph"></a>
</p>
<p align="center">
<a href="https://github.com/caddyserver/caddy/releases">Download</a> ·
<a href="https://caddyserver.com/docs/">Documentation</a> ·
<a href="https://caddy.community">Community</a>
</p>
### Menu
- [Getting Caddy](#getting-caddy)
- [Running from Source](#running-from-source)
- [Quick Start](#quick-start)
- [Contributing](#contributing)
- [About the Project](#about-the-project)
- [Features](#features)
- [Build from source](#build-from-source)
- [For development](#for-development)
- [With version information and/or plugins](#with-version-information-andor-plugins)
- [Getting started](#getting-started)
- [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/caddyserver/certmagic"><img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250"></a>
</p>
## Features
- **Easy configuration** with the [Caddyfile](https://caddyserver.com/docs/caddyfile)
- **Powerful configuration** with its [native JSON config](https://caddyserver.com/docs/json/)
- **Dynamic configuration** with the [JSON API](https://caddyserver.com/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/extending-caddy) lets Caddy do anything without bloat
- **Runs anywhere** with **no external dependencies** (not even libc)
- Written in Go, a language with higher **memory safety guarantees** than other servers
- Actually **fun to use**
- So, so much more to discover
## Build from source
## Getting Caddy
Requirements:
Caddy binaries have no dependencies and are available for nearly every platform.
- [Go 1.14 or newer](https://golang.org/dl/)
- Do NOT disable [Go modules](https://github.com/golang/go/wiki/Modules) (`export GO111MODULE=on`)
[Latest release](https://github.com/mholt/caddy/releases/latest)
## Running from Source
Note: You will need **[Go 1.4](https://golang.org/dl)** or newer
1. `$ go get github.com/mholt/caddy`
2. `cd` into your website's directory
3. Run `caddy` (assumes `$GOPATH/bin` is in your `$PATH`)
If you're tinkering, you can also use `go run main.go`.
By default, Caddy serves the current directory at [localhost:2015](http://localhost:2015). You can place a Caddyfile to configure Caddy for serving your site.
Caddy accepts some flags from the command line. Run `caddy -h` to view the help for flags. You can also pipe a Caddyfile into the caddy command.
#### Docker Container
Caddy is available as a Docker container from any of these sources:
- [abiosoft/caddy](https://registry.hub.docker.com/u/abiosoft/caddy/)
- [darron/caddy](https://registry.hub.docker.com/u/darron/caddy/)
- [jumanjiman/caddy](https://registry.hub.docker.com/u/jumanjiman/caddy/)
#### 3rd-party libraries
Although Caddy's binaries are completely static, Caddy relies on some excellent libraries. [Godoc.org](https://godoc.org/github.com/mholt/caddy) shows the packages that each Caddy package imports.
## Quick Start
The website has [full documentation](https://caddyserver.com/docs) but this will get you started in about 30 seconds:
Place a file named "Caddyfile" with your site. Paste this into it and save:
```
localhost
gzip
browse
ext .html
websocket /echo cat
log ../access.log
header /api Access-Control-Allow-Origin *
### For development
```bash
$ git clone "https://github.com/caddyserver/caddy.git"
$ cd caddy/cmd/caddy/
$ go build
```
Run `caddy` from that directory, and it will automatically use that Caddyfile to configure itself.
_**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions below._
That simple file enables compression, allows directory browsing (for folders without an index file), serves clean URLs, hosts an echo server for WebSocket connections at /echo, logs accesses to access.log, and adds the coveted `Access-Control-Allow-Origin: *` header for all responses from some API.
### With version information and/or plugins
Wow! Caddy can do a lot with just a few lines.
#### Defining multiple sites
You can run multiple sites from the same Caddyfile, too:
Using [our builder tool](https://github.com/caddyserver/xcaddy)...
```
http://mysite.com,
http://www.mysite.com {
redir https://mysite.com
}
https://mysite.com {
tls mysite.crt mysite.key
# ...
}
$ xcaddy --version CADDY_VERSION
```
Note that the secure host will automatically be served with HTTP/2 if the client supports it.
...the following steps are automated:
For more documentation, please view [the website](https://caddyserver.com/docs). You may also be interested in the [developer guide](https://github.com/mholt/caddy/wiki) on this project's GitHub wiki.
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. Pin Caddy version: `go get github.com/caddyserver/caddy/v2@TAG` replacing `TAG` with a git tag or commit. You can also pin any plugin versions similarly.
6. Compile: `go build`
## Quick start
The [Caddy website](https://caddyserver.com/docs/) has documentation that includes tutorials, quick-start guides, reference, and more.
## Contributing
**We recommend that all users do our [Getting Started](https://caddyserver.com/docs/getting-started) guide to become familiar with using Caddy.**
**[Join us on Slack](https://gophers.slack.com/messages/caddy/)** to chat with other Caddy developers! ([Request an invite](http://bit.ly/go-slack-signup), then join the #caddy channel.)
This project would not be what it is without your help. Please see the [contributing guidelines](https://github.com/mholt/caddy/blob/master/CONTRIBUTING.md) if you haven't already.
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. 🙂
## 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, lighttpd, Websocketd, and Vagrant, and 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.
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.
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 the [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.
*Twitter: [@mholt6](https://twitter.com/mholt6)*
## 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](/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)_
+792
View File
@@ -0,0 +1,792 @@
// 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"
"expvar"
"fmt"
"io"
"net/http"
"net/http/pprof"
"net/url"
"os"
"path"
"regexp"
"strconv"
"strings"
"sync"
"time"
"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 for API requests. Only used if
// `enforce_origin` is true. If not set, the listener address
// will be the default value. If set but empty, no origins will
// be allowed.
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() (string, string, error) {
input := admin.Listen
if input == "" {
input = DefaultAdminListen
}
listenAddr, err := ParseNetworkAddress(input)
if err != nil {
return "", "", fmt.Errorf("parsing admin listener address: %v", err)
}
if listenAddr.PortRangeSize() != 1 {
return "", "", fmt.Errorf("admin endpoint must have exactly one address; cannot listen on %v", listenAddr)
}
return listenAddr.Network, listenAddr.JoinHostPort(0), nil
}
// newAdminHandler reads admin's config and returns an http.Handler suitable
// for use in an admin endpoint server, which will be listening on listenAddr.
func (admin AdminConfig) newAdminHandler(listenAddr string) adminHandler {
muxWrap := adminHandler{
enforceOrigin: admin.EnforceOrigin,
allowedOrigins: admin.allowedOrigins(listenAddr),
mux: http.NewServeMux(),
}
// addRoute just calls muxWrap.mux.Handle after
// wrapping the handler with error handling
addRoute := func(pattern string, h AdminHandler) {
wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := h.ServeHTTP(w, r)
muxWrap.handleError(w, r, err)
})
muxWrap.mux.Handle(pattern, wrapper)
}
// register standard config control endpoints
addRoute("/"+rawConfigKey+"/", AdminHandlerFunc(handleConfig))
addRoute("/id/", AdminHandlerFunc(handleConfigID))
addRoute("/stop", AdminHandlerFunc(handleStop))
// register debugging endpoints
muxWrap.mux.HandleFunc("/debug/pprof/", pprof.Index)
muxWrap.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
muxWrap.mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
muxWrap.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
muxWrap.mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
muxWrap.mux.Handle("/debug/vars", expvar.Handler())
// register third-party module endpoints
for _, m := range GetModules("admin.api") {
router := m.New().(AdminRouter)
for _, route := range router.Routes() {
addRoute(route.Pattern, 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, but whatever.
func (admin AdminConfig) allowedOrigins(listen string) []string {
uniqueOrigins := make(map[string]struct{})
for _, o := range admin.Origins {
uniqueOrigins[o] = struct{}{}
}
if admin.Origins == nil {
uniqueOrigins[listen] = 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
netw, addr, err := adminConfig.listenAddr()
if err != nil {
return err
}
handler := adminConfig.newAdminHandler(addr)
ln, err := Listen(netw, addr)
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,
}
go adminServer.Serve(ln)
Log().Named("admin").Info(
"admin endpoint started",
zap.String("address", addr),
zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
zap.Strings("origins", handler.allowedOrigins),
)
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
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("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 h.enforceOrigin {
// DNS rebinding mitigation
err := h.checkHost(r)
if err != nil {
h.handleError(w, r, err)
return
}
// 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))
}
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"),
}
}
currentCfgMu.RLock()
hasCfg := currentCfg != nil
currentCfgMu.RUnlock()
if !hasCfg {
Log().Named("admin.api").Info("nothing to unload")
return nil
}
Log().Named("admin.api").Info("unloading")
if err := stopAndCleanup(); err != nil {
Log().Named("admin.api").Error("error unloading", zap.Error(err))
} 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,
}
)
// 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*,?`)
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)
}
}
-76
View File
@@ -1,76 +0,0 @@
// Package app holds application-global state to make it accessible
// by other packages in the application.
//
// This package differs from config in that the things in app aren't
// really related to server configuration.
package app
import (
"errors"
"runtime"
"strconv"
"strings"
"sync"
"github.com/mholt/caddy/server"
)
const (
// Name is the program name
Name = "Caddy"
// Version is the program version
Version = "0.7.4"
)
var (
// Servers is a list of all the currently-listening servers
Servers []*server.Server
// ServersMutex protects the Servers slice during changes
ServersMutex sync.Mutex
// Wg is used to wait for all servers to shut down
Wg sync.WaitGroup
// Http2 indicates whether HTTP2 is enabled or not
Http2 bool // TODO: temporary flag until http2 is standard
// Quiet mode hides non-error initialization output
Quiet bool
)
// 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%).
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)
} 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
}
+570
View File
@@ -0,0 +1,570 @@
// 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"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"path/filepath"
"runtime/debug"
"strconv"
"strings"
"sync"
"time"
"github.com/caddyserver/certmagic"
"go.uber.org/zap"
)
// Config is the top (or beginning) of the Caddy configuration structure.
// Caddy config is expressed natively as a JSON document. If you prefer
// not to work with JSON directly, there are [many config adapters](/docs/config-adapters)
// available that can convert various inputs into Caddy JSON.
//
// Many parts of this config are extensible through the use of Caddy modules.
// Fields which have a json.RawMessage type and which appear as dots (•••) in
// the online docs can be fulfilled by modules in a certain module
// namespace. The docs show which modules can be used in a given place.
//
// Whenever a module is used, its name must be given either inline as part of
// the module, or as the key to the module's value. The docs will make it clear
// which to use.
//
// Generally, all config settings are optional, as it is Caddy convention to
// have good, documented default values. If a parameter is required, the docs
// should say so.
//
// Go programs which are directly building a Config struct value should take
// care to populate the JSON-encodable fields of the struct (i.e. the fields
// with `json` struct tags) if employing the module lifecycle (e.g. Provision
// method calls).
type Config struct {
Admin *AdminConfig `json:"admin,omitempty"`
Logging *Logging `json:"logging,omitempty"`
// StorageRaw is a storage module that defines how/where Caddy
// stores assets (such as TLS certificates). The default storage
// module is `caddy.storage.file_system` (the local file system),
// and the default path
// [depends on the OS and environment](/docs/conventions#data-directory).
StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
// AppsRaw are the apps that Caddy will load and run. The
// app module name is the key, and the app's config is the
// associated value.
AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
apps map[string]App
storage certmagic.Storage
cancelFunc context.CancelFunc
}
// App is a thing that Caddy runs.
type App interface {
Start() error
Stop() error
}
// Run runs the given config, replacing any existing config.
func Run(cfg *Config) error {
cfgJSON, err := json.Marshal(cfg)
if err != nil {
return err
}
return Load(cfgJSON, true)
}
// Load loads the given config JSON and runs it only
// if it is different from the current config or
// forceReload is true.
func Load(cfgJSON []byte, forceReload bool) error {
return changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
}
// changeConfig changes the current config (rawCfg) according to the
// method, traversed via the given path, and uses the given input as
// the new value (if applicable; i.e. "DELETE" doesn't have an input).
// If the resulting config is the same as the previous, no reload will
// occur unless forceReload is true. This function is safe for
// concurrent use.
func changeConfig(method, path string, input []byte, forceReload bool) error {
switch method {
case http.MethodGet,
http.MethodHead,
http.MethodOptions,
http.MethodConnect,
http.MethodTrace:
return fmt.Errorf("method not allowed")
}
currentCfgMu.Lock()
defer currentCfgMu.Unlock()
err := unsyncedConfigAccess(method, path, input, nil)
if err != nil {
return err
}
// the mutation is complete, so encode the entire config as JSON
newCfg, err := json.Marshal(rawCfg[rawConfigKey])
if err != nil {
return APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("encoding new config: %v", err),
}
}
// if nothing changed, no need to do a whole reload unless the client forces it
if !forceReload && bytes.Equal(rawCfgJSON, newCfg) {
Log().Named("admin.api").Info("config is unchanged")
return nil
}
// find any IDs in this config and index them
idx := make(map[string]string)
err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
if err != nil {
return APIError{
Code: http.StatusInternalServerError,
Err: fmt.Errorf("indexing config: %v", err),
}
}
// load this new config; if it fails, we need to revert to
// our old representation of caddy's actual config
err = unsyncedDecodeAndRun(newCfg)
if err != nil {
if len(rawCfgJSON) > 0 {
// restore old config state to keep it consistent
// with what caddy is still running; we need to
// unmarshal it again because it's likely that
// pointers deep in our rawCfg map were modified
var oldCfg interface{}
err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
if err2 != nil {
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
}
rawCfg[rawConfigKey] = oldCfg
}
return fmt.Errorf("loading new config: %v", err)
}
// success, so update our stored copy of the encoded
// config to keep it consistent with what caddy is now
// running (storing an encoded copy is not strictly
// necessary, but avoids an extra json.Marshal for
// each config change)
rawCfgJSON = newCfg
rawCfgIndex = idx
return nil
}
// readConfig traverses the current config to path
// and writes its JSON encoding to out.
func readConfig(path string, out io.Writer) error {
currentCfgMu.RLock()
defer currentCfgMu.RUnlock()
return unsyncedConfigAccess(http.MethodGet, path, nil, out)
}
// indexConfigObjects recursively searches ptr for object fields named
// "@id" and maps that ID value to the full configPath in the index.
// This function is NOT safe for concurrent access; obtain a write lock
// on currentCfgMu.
func indexConfigObjects(ptr interface{}, configPath string, index map[string]string) error {
switch val := ptr.(type) {
case map[string]interface{}:
for k, v := range val {
if k == idKey {
switch idVal := v.(type) {
case string:
index[idVal] = configPath
case float64: // all JSON numbers decode as float64
index[fmt.Sprintf("%v", idVal)] = configPath
default:
return fmt.Errorf("%s: %s field must be a string or number", configPath, idKey)
}
continue
}
// traverse this object property recursively
err := indexConfigObjects(val[k], path.Join(configPath, k), index)
if err != nil {
return err
}
}
case []interface{}:
// traverse each element of the array recursively
for i := range val {
err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index)
if err != nil {
return err
}
}
}
return nil
}
// unsyncedDecodeAndRun removes any meta fields (like @id tags)
// from cfgJSON, decodes the result into a *Config, and runs
// it as the new config, replacing any other current config.
// It does NOT update the raw config state, as this is a
// lower-level function; most callers will want to use Load
// instead. A write lock on currentCfgMu is required!
func unsyncedDecodeAndRun(cfgJSON []byte) error {
// remove any @id fields from the JSON, which would cause
// loading to break since the field wouldn't be recognized
strippedCfgJSON := RemoveMetaFields(cfgJSON)
var newCfg *Config
err := strictUnmarshalJSON(strippedCfgJSON, &newCfg)
if err != nil {
return err
}
// run the new config and start all its apps
err = run(newCfg, true)
if err != nil {
return err
}
// swap old config with the new one
oldCfg := currentCfg
currentCfg = newCfg
// Stop, Cleanup each old app
unsyncedStop(oldCfg)
// autosave a non-nil config, if not disabled
if newCfg != nil &&
(newCfg.Admin == nil ||
newCfg.Admin.Config == nil ||
newCfg.Admin.Config.Persist == nil ||
*newCfg.Admin.Config.Persist) {
dir := filepath.Dir(ConfigAutosavePath)
err := os.MkdirAll(dir, 0700)
if err != nil {
Log().Error("unable to create folder for config autosave",
zap.String("dir", dir),
zap.Error(err))
} else {
err := ioutil.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
if err == nil {
Log().Info("autosaved config", zap.String("file", ConfigAutosavePath))
} else {
Log().Error("unable to autosave config",
zap.String("file", ConfigAutosavePath),
zap.Error(err))
}
}
}
return nil
}
// run runs newCfg and starts all its apps if
// start is true. If any errors happen, cleanup
// is performed if any modules were provisioned;
// apps that were started already will be stopped,
// so this function should not leak resources if
// an error is returned. However, if no error is
// returned and start == false, you should cancel
// the config if you are not going to start it,
// so that each provisioned module will be
// cleaned up.
//
// This is a low-level function; most callers
// will want to use Run instead, which also
// updates the config's raw state.
func run(newCfg *Config, start bool) error {
// because we will need to roll back any state
// modifications if this function errors, we
// keep a single error value and scope all
// sub-operations to their own functions to
// ensure this error value does not get
// overridden or missed when it should have
// been set by a short assignment
var err error
// start the admin endpoint (and stop any prior one)
if start {
err = replaceAdmin(newCfg)
if err != nil {
return fmt.Errorf("starting caddy administration endpoint: %v", err)
}
}
if newCfg == nil {
return nil
}
// prepare the new config for use
newCfg.apps = make(map[string]App)
// create a context within which to load
// modules - essentially our new config's
// execution environment; be sure that
// cleanup occurs when we return if there
// was an error; if no error, it will get
// cleaned up on next config cycle
ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
defer func() {
if err != nil {
// if there were any errors during startup,
// we should cancel the new context we created
// since the associated config won't be used;
// this will cause all modules that were newly
// provisioned to clean themselves up
cancel()
// also undo any other state changes we made
if currentCfg != nil {
certmagic.Default.Storage = currentCfg.storage
}
}
}()
newCfg.cancelFunc = cancel // clean up later
// set up logging before anything bad happens
if newCfg.Logging == nil {
newCfg.Logging = new(Logging)
}
err = newCfg.Logging.openLogs(ctx)
if err != nil {
return err
}
// set up global storage and make it CertMagic's default storage, too
err = func() error {
if newCfg.StorageRaw != nil {
val, err := ctx.LoadModule(newCfg, "StorageRaw")
if err != nil {
return fmt.Errorf("loading storage module: %v", err)
}
stor, err := val.(StorageConverter).CertMagicStorage()
if err != nil {
return fmt.Errorf("creating storage value: %v", err)
}
newCfg.storage = stor
}
if newCfg.storage == nil {
newCfg.storage = DefaultStorage
}
certmagic.Default.Storage = newCfg.storage
return nil
}()
if err != nil {
return err
}
// Load and Provision each app and their submodules
err = func() error {
for appName := range newCfg.AppsRaw {
if _, err := ctx.App(appName); err != nil {
return err
}
}
return nil
}()
if err != nil {
return err
}
if !start {
return nil
}
// Start
return func() error {
var started []string
for name, a := range newCfg.apps {
err := a.Start()
if err != nil {
// an app failed to start, so we need to stop
// all other apps that were already started
for _, otherAppName := range started {
err2 := newCfg.apps[otherAppName].Stop()
if err2 != nil {
err = fmt.Errorf("%v; additionally, aborting app %s: %v",
err, otherAppName, err2)
}
}
return fmt.Errorf("%s app module: start: %v", name, err)
}
started = append(started, name)
}
return nil
}()
}
// Stop stops running the current configuration.
// It is the antithesis of Run(). This function
// will log any errors that occur during the
// stopping of individual apps and continue to
// stop the others. Stop should only be called
// if not replacing with a new config.
func Stop() error {
currentCfgMu.Lock()
defer currentCfgMu.Unlock()
unsyncedStop(currentCfg)
currentCfg = nil
rawCfgJSON = nil
rawCfgIndex = nil
rawCfg[rawConfigKey] = nil
return nil
}
// unsyncedStop stops cfg from running, but has
// no locking around cfg. It is a no-op if cfg is
// nil. If any app returns an error when stopping,
// it is logged and the function continues stopping
// the next app. This function assumes all apps in
// cfg were successfully started first.
func unsyncedStop(cfg *Config) {
if cfg == nil {
return
}
// stop each app
for name, a := range cfg.apps {
err := a.Stop()
if err != nil {
log.Printf("[ERROR] stop %s: %v", name, err)
}
}
// clean up all modules
cfg.cancelFunc()
}
// stopAndCleanup calls stop and cleans up anything
// else that is expedient. This should only be used
// when stopping and not replacing with a new config.
func stopAndCleanup() error {
if err := Stop(); err != nil {
return err
}
certmagic.CleanUpOwnLocks()
return nil
}
// Validate loads, provisions, and validates
// cfg, but does not start running it.
func Validate(cfg *Config) error {
err := run(cfg, false)
if err == nil {
cfg.cancelFunc() // call Cleanup on all modules
}
return err
}
// Duration can be an integer or a string. An integer is
// interpreted as nanoseconds. If a string, it is a Go
// time.Duration value such as `300ms`, `1.5h`, or `2h45m`;
// valid units are `ns`, `us`/`µs`, `ms`, `s`, `m`, and `h`.
type Duration time.Duration
// UnmarshalJSON satisfies json.Unmarshaler.
func (d *Duration) UnmarshalJSON(b []byte) error {
if len(b) == 0 {
return io.EOF
}
var dur time.Duration
var err error
if b[0] == byte('"') && b[len(b)-1] == byte('"') {
dur, err = time.ParseDuration(strings.Trim(string(b), `"`))
} else {
err = json.Unmarshal(b, &dur)
}
*d = Duration(dur)
return err
}
// GoModule returns the build info of this Caddy
// build 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 GoModule() *debug.Module {
var mod debug.Module
return goModule(&mod)
}
// goModule holds the actual implementation of GoModule.
// Allocating debug.Module in GoModule() and passing a
// reference to goModule enables mid-stack inlining.
func goModule(mod *debug.Module) *debug.Module {
mod.Version = "unknown"
bi, ok := debug.ReadBuildInfo()
if ok {
mod.Path = bi.Main.Path
// The recommended way to build Caddy involves
// creating a separate main module, which
// TODO: track related Go issue: https://github.com/golang/go/issues/29228
// once that issue is fixed, we should just be able to use bi.Main... hopefully.
for _, dep := range bi.Deps {
if dep.Path == ImportPath {
return dep
}
}
return &bi.Main
}
return mod
}
// CtxKey is a value type for use with context.WithValue.
type CtxKey string
// This group of variables pertains to the current configuration.
var (
// currentCfgMu protects everything in this var block.
currentCfgMu sync.RWMutex
// currentCfg is the currently-running configuration.
currentCfg *Config
// rawCfg is the current, generic-decoded configuration;
// we initialize it as a map with one field ("config")
// to maintain parity with the API endpoint and to avoid
// the special case of having to access/mutate the variable
// directly without traversing into it.
rawCfg = map[string]interface{}{
rawConfigKey: nil,
}
// rawCfgJSON is the JSON-encoded form of rawCfg. Keeping
// this around avoids an extra Marshal call during changes.
rawCfgJSON []byte
// rawCfgIndex is the map of user-assigned ID to expanded
// path, for converting /id/ paths to /config/ paths.
rawCfgIndex map[string]string
)
// ImportPath is the package import path for Caddy core.
const ImportPath = "github.com/caddyserver/caddy/v2"
+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)
+384
View File
@@ -0,0 +1,384 @@
// 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"
"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,
}
}
// 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
}
+37 -11
View File
@@ -1,6 +1,22 @@
package parse
// 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 (
"io"
"log"
"reflect"
"strings"
"testing"
@@ -11,7 +27,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)
@@ -49,7 +65,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 {
@@ -64,7 +80,7 @@ func TestDispenser_NextArg(t *testing.T) {
}
assertNextArg := func(expectedVal string, loadAnother bool, expectedCursor int) {
if d.NextArg() != true {
if !d.NextArg() {
t.Error("NextArg(): Should load next argument but got false instead")
}
if d.cursor != expectedCursor {
@@ -74,7 +90,7 @@ func TestDispenser_NextArg(t *testing.T) {
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
}
if !loadAnother {
if d.NextArg() != false {
if d.NextArg() {
t.Fatalf("NextArg(): Should NOT load another argument, but got true instead (val: '%s')", d.Val())
}
if d.cursor != expectedCursor {
@@ -96,7 +112,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 {
@@ -129,10 +145,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 {
@@ -159,7 +175,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
@@ -226,7 +242,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
@@ -263,7 +279,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 // {
@@ -290,3 +306,13 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
}
}
// NewTestDispenser parses input into tokens and creates a new
// Disenser 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)
}
+216
View File
@@ -0,0 +1,216 @@
// 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 == '#' {
if !spacePrior && !beginningOfLine {
write(' ')
}
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')
}
+322
View File
@@ -0,0 +1,322 @@
// 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`,
},
} {
// 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))
}
}
}
+68 -27
View File
@@ -1,4 +1,18 @@
package parse
// 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 (
"bufio"
@@ -12,23 +26,38 @@ 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.
token struct {
file string
line int
text string
// Token represents a single parsable unit.
Token struct {
File string
Line int
Text string
}
)
// load prepares the lexer to scan an input for tokens.
// It discards any leading byte order mark.
func (l *lexer) load(input io.Reader) error {
l.reader = bufio.NewReader(input)
l.line = 1
// discard byte order mark, if present
firstCh, _, err := l.reader.ReadRune()
if err != nil {
return err
}
if firstCh != 0xFEFF {
err := l.reader.UnreadRune()
if err != nil {
return err
}
}
return nil
}
@@ -47,7 +76,7 @@ func (l *lexer) next() bool {
var comment, quoted, escaped bool
makeToken := func() bool {
l.token.text = string(val)
l.token.Text = string(val)
return true
}
@@ -63,27 +92,29 @@ func (l *lexer) next() bool {
panic(err)
}
if !escaped && ch == '\\' {
escaped = true
continue
}
if quoted {
if !escaped {
if ch == '\\' {
escaped = true
continue
} else if ch == '"' {
quoted = false
if escaped {
// all is literal in quoted area,
// so only escape quotes
if ch != '"' {
val = append(val, '\\')
}
escaped = false
} else {
if 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
}
@@ -92,7 +123,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 {
@@ -104,19 +141,23 @@ func (l *lexer) next() bool {
if ch == '#' {
comment = true
}
if comment {
continue
}
if len(val) == 0 {
l.token = token{line: l.line}
l.token = Token{Line: l.line}
if ch == '"' {
quoted = true
continue
}
}
if escaped {
val = append(val, '\\')
escaped = false
}
val = append(val, ch)
}
}
+238
View File
@@ -0,0 +1,238 @@
// 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 (
"log"
"strings"
"testing"
)
type lexerTestCase struct {
input string
expected []Token
}
func TestLexer(t *testing.T) {
testCases := []lexerTestCase{
{
input: `host:123`,
expected: []Token{
{Line: 1, Text: "host:123"},
},
},
{
input: `host:123
directive`,
expected: []Token{
{Line: 1, Text: "host:123"},
{Line: 3, Text: "directive"},
},
},
{
input: `host:123 {
directive
}`,
expected: []Token{
{Line: 1, Text: "host:123"},
{Line: 1, Text: "{"},
{Line: 2, Text: "directive"},
{Line: 3, Text: "}"},
},
},
{
input: `host:123 { directive }`,
expected: []Token{
{Line: 1, Text: "host:123"},
{Line: 1, Text: "{"},
{Line: 1, Text: "directive"},
{Line: 1, Text: "}"},
},
},
{
input: `host:123 {
#comment
directive
# comment
foobar # another comment
}`,
expected: []Token{
{Line: 1, Text: "host:123"},
{Line: 1, Text: "{"},
{Line: 3, Text: "directive"},
{Line: 5, Text: "foobar"},
{Line: 6, Text: "}"},
},
},
{
input: `a "quoted value" b
foobar`,
expected: []Token{
{Line: 1, Text: "a"},
{Line: 1, Text: "quoted value"},
{Line: 1, Text: "b"},
{Line: 2, Text: "foobar"},
},
},
{
input: `A "quoted \"value\" inside" B`,
expected: []Token{
{Line: 1, Text: "A"},
{Line: 1, Text: `quoted "value" inside`},
{Line: 1, Text: "B"},
},
},
{
input: "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: "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: "line1\\\nescaped\nline2\nline3",
expected: []Token{
{Line: 1, Text: "line1"},
{Line: 1, Text: "escaped"},
{Line: 3, Text: "line2"},
{Line: 4, Text: "line3"},
},
},
{
input: "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: `"unescapable\ in quotes"`,
expected: []Token{
{Line: 1, Text: `unescapable\ in quotes`},
},
},
{
input: `"don't\escape"`,
expected: []Token{
{Line: 1, Text: `don't\escape`},
},
},
{
input: `"don't\\escape"`,
expected: []Token{
{Line: 1, Text: `don't\\escape`},
},
},
{
input: `un\escapable`,
expected: []Token{
{Line: 1, Text: `un\escapable`},
},
},
{
input: `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"},
{Line: 2, Text: "{"},
{Line: 3, Text: "foobar"},
{Line: 4, Text: "}"},
},
},
{
input: `"C:\php\php-cgi.exe"`,
expected: []Token{
{Line: 1, Text: `C:\php\php-cgi.exe`},
},
},
{
input: `empty "" string`,
expected: []Token{
{Line: 1, Text: `empty`},
{Line: 1, Text: ``},
{Line: 1, Text: `string`},
},
},
{
input: "skip those\r\nCR characters",
expected: []Token{
{Line: 1, Text: "skip"},
{Line: 1, Text: "those"},
{Line: 2, Text: "CR"},
{Line: 2, Text: "characters"},
},
},
{
input: "\xEF\xBB\xBF:8080", // test with leading byte order mark
expected: []Token{
{Line: 1, Text: ":8080"},
},
},
}
for i, testCase := range testCases {
actual := tokenize(testCase.input)
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))
}
for i := 0; i < len(actual) && i < len(expected); i++ {
if actual[i].Line != expected[i].Line {
t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d",
n, i, expected[i].Text, expected[i].Line, actual[i].Line)
break
}
if actual[i].Text != expected[i].Text {
t.Errorf("Test case %d token %d: expected text '%s' but was '%s'",
n, i, expected[i].Text, actual[i].Text)
break
}
}
}
+536
View File
@@ -0,0 +1,536 @@
// 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"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
)
// Parse parses the input just enough to group tokens, in
// order, by server block. No further parsing is performed.
// Server blocks are returned in the order in which they appear.
// 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.
//
// 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(filename string, input []byte) ([]Token, error) {
input, err := replaceEnvVars(input)
if err != nil {
return nil, err
}
l := new(lexer)
err = l.load(bytes.NewReader(input))
if err != nil {
return nil, err
}
var tokens []Token
for l.next() {
l.token.File = filename
tokens = append(tokens, l.token)
}
return tokens, nil
}
type parser struct {
*Dispenser
block ServerBlock // current server block being parsed
eof bool // if we encounter a valid EOF in a hard place
definedSnippets map[string][]Token
nesting int
}
func (p *parser) parseAll() ([]ServerBlock, error) {
var blocks []ServerBlock
for p.Next() {
err := p.parseOne()
if err != nil {
return blocks, err
}
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{}
return p.begin()
}
func (p *parser) begin() error {
if len(p.tokens) == 0 {
return nil
}
err := p.addresses()
if err != nil {
return err
}
if p.eof {
// this happens if the Caddyfile consists of only
// a line of addresses and nothing else
return nil
}
if ok, name := p.isSnippet(); ok {
if p.definedSnippets == nil {
p.definedSnippets = map[string][]Token{}
}
if _, found := p.definedSnippets[name]; found {
return p.Errf("redeclaration of previously declared snippet %s", name)
}
// consume all tokens til matched close brace
tokens, err := p.snippetTokens()
if err != nil {
return err
}
p.definedSnippets[name] = tokens
// empty block keys so we don't save this block as a real server.
p.block.Keys = nil
return nil
}
return p.blockContents()
}
func (p *parser) addresses() error {
var expectingAnother bool
for {
tkn := p.Val()
// special case: import directive replaces tokens during parse-time
if tkn == "import" && p.isNewLine() {
err := p.doImport()
if err != nil {
return err
}
continue
}
// Open brace definitely indicates end of addresses
if tkn == "{" {
if expectingAnother {
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
}
break
}
if tkn != "" { // empty token possible if user typed ""
// Trailing comma indicates another address will follow, which
// may possibly be on the next line
if tkn[len(tkn)-1] == ',' {
tkn = tkn[:len(tkn)-1]
expectingAnother = true
} else {
expectingAnother = false // but we may still see another one on this line
}
p.block.Keys = append(p.block.Keys, tkn)
}
// Advance token and possibly break out of loop or return error
hasNext := p.Next()
if expectingAnother && !hasNext {
return p.EOFErr()
}
if !hasNext {
p.eof = true
break // EOF
}
if !expectingAnother && p.isNewLine() {
break
}
}
return nil
}
func (p *parser) blockContents() error {
errOpenCurlyBrace := p.openCurlyBrace()
if errOpenCurlyBrace != nil {
// single-server configs don't need curly braces
p.cursor--
}
err := p.directives()
if err != nil {
return err
}
// only look for close curly brace if there was an opening
if errOpenCurlyBrace == nil {
err = p.closeCurlyBrace()
if err != nil {
return err
}
}
return nil
}
// directives parses through all the lines for directives
// and it expects the next token to be the first
// directive. It goes until EOF or closing curly brace
// which ends the server block.
func (p *parser) directives() error {
for p.Next() {
// end of server block
if p.Val() == "}" {
// p.nesting has already been decremented
break
}
// special case: import directive replaces tokens during parse-time
if p.Val() == "import" {
err := p.doImport()
if err != nil {
return err
}
p.cursor-- // cursor is advanced when we continue, so roll back one more
continue
}
// 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
}
// doImport swaps out the import directive and its argument
// (a total of 2 tokens) with the tokens in the specified file
// or globbing pattern. When the function returns, the cursor
// is on the token before where the import directive was. In
// other words, call Next() to access the first token that was
// imported.
func (p *parser) doImport() error {
// syntax checks
if !p.NextArg() {
return p.ArgErr()
}
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)")
}
// splice out the import directive and its argument (2 tokens total)
tokensBefore := p.tokens[:p.cursor-1]
tokensAfter := p.tokens[p.cursor+1:]
var importedTokens []Token
// first check snippets. That is a simple, non-recursive replacement
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
importedTokens = p.definedSnippets[importPattern]
} else {
// make path relative to the file of the _token_ being processed rather
// than current working directory (issue #867) and then use glob to get
// 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.File(), err)
}
var matches []string
var globPattern string
if !filepath.IsAbs(importPattern) {
globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
} else {
globPattern = importPattern
}
if strings.Count(globPattern, "*") > 1 || strings.Count(globPattern, "?") > 1 ||
(strings.Contains(globPattern, "[") && strings.Contains(globPattern, "]")) {
// See issue #2096 - a pattern with many glob expansions can hang for too long
return p.Errf("Glob pattern may only contain one wildcard (*), but has others: %s", globPattern)
}
matches, err = filepath.Glob(globPattern)
if err != nil {
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
}
if len(matches) == 0 {
if strings.ContainsAny(globPattern, "*?[]") {
log.Printf("[WARNING] No files matching import glob pattern: %s", importPattern)
} else {
return p.Errf("File to import not found: %s", importPattern)
}
}
// collect all the imported tokens
for _, importFile := range matches {
newTokens, err := p.doSingleImport(importFile)
if err != nil {
return err
}
importedTokens = append(importedTokens, newTokens...)
}
}
// 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--
return nil
}
// doSingleImport lexes the individual file at importFile and returns
// its tokens or an error, if any.
func (p *parser) doSingleImport(importFile string) ([]Token, error) {
file, err := os.Open(importFile)
if err != nil {
return nil, p.Errf("Could not import %s: %v", importFile, err)
}
defer file.Close()
if info, err := file.Stat(); err != nil {
return nil, p.Errf("Could not import %s: %v", importFile, err)
} else if info.IsDir() {
return nil, p.Errf("Could not import %s: is a directory", importFile)
}
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)
}
// Tack the file path onto these tokens so errors show the imported file's name
// (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", importFile, err)
}
for i := 0; i < len(importedTokens); i++ {
importedTokens[i].File = filename
}
return importedTokens, nil
}
// directive collects tokens until the directive's scope
// closes (either end of line or end of curly brace block).
// It expects the currently-loaded token to be a directive
// (or } that ends a server block). The collected tokens
// are loaded into the current server block for later use
// by directive setup functions.
func (p *parser) directive() error {
// a segment is a list of tokens associated with this directive
var segment Segment
// the directive itself is appended as a relevant token
segment = append(segment, p.Token())
for p.Next() {
if p.Val() == "{" {
p.nesting++
} else if p.isNewLine() && p.nesting == 0 {
p.cursor-- // read too far
break
} 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 {
return err
}
p.cursor-- // cursor is advanced when we continue, so roll back one more
continue
}
segment = append(segment, p.Token())
}
p.block.Segments = append(p.block.Segments, segment)
if p.nesting > 0 {
return p.EOFErr()
}
return nil
}
// openCurlyBrace expects the current token to be an
// opening curly brace. This acts like an assertion
// because it returns an error if the token is not
// a opening curly brace. It does NOT advance the token.
func (p *parser) openCurlyBrace() error {
if p.Val() != "{" {
return p.SyntaxErr("{")
}
return nil
}
// closeCurlyBrace expects the current token to be
// a closing curly brace. This acts like an assertion
// because it returns an error if the token is not
// a closing curly brace. It does NOT advance the token.
func (p *parser) closeCurlyBrace() error {
if p.Val() != "}" {
return p.SyntaxErr("}")
}
return nil
}
func (p *parser) isSnippet() (bool, string) {
keys := p.block.Keys
// A snippet block is a single key with parens. Nothing else qualifies.
if len(keys) == 1 && strings.HasPrefix(keys[0], "(") && strings.HasSuffix(keys[0], ")") {
return true, strings.TrimSuffix(keys[0][1:], ")")
}
return false, ""
}
// read and store everything in a block for later replay.
func (p *parser) snippetTokens() ([]Token, error) {
// snippet must have curlies.
err := p.openCurlyBrace()
if err != nil {
return nil, err
}
nesting := 1 // count our own nesting in snippets
tokens := []Token{}
for p.Next() {
if p.Val() == "}" {
nesting--
if nesting == 0 {
break
}
}
if p.Val() == "{" {
nesting++
}
tokens = append(tokens, p.tokens[p.cursor])
}
// make sure we're matched up
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{'}'}
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build gofuzz
package caddyfile
import "bytes"
func FuzzParseCaddyfile(data []byte) (score int) {
if bytes.Contains(data, []byte("import")) {
return -1
}
sb, err := Parse("Caddyfile", data)
if err != nil {
// if both an error is received and some ServerBlocks,
// then the parse was able to parse partially. Mark this
// result as interesting to push the fuzzer further through the parser.
if sb != nil && len(sb) > 0 {
return 1
}
return 0
}
return 1
}
+662
View File
@@ -0,0 +1,662 @@
// 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"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func TestAllTokens(t *testing.T) {
input := []byte("a b c\nd e")
expected := []string{"a", "b", "c", "d", "e"}
tokens, err := allTokens("TestAllTokens", input)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(tokens) != len(expected) {
t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens))
}
for i, val := range expected {
if tokens[i].Text != val {
t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].Text)
}
}
}
func TestParseOneAndImport(t *testing.T) {
testParseOne := func(input string) (ServerBlock, error) {
p := testParser(input)
p.Next() // parseOne doesn't call Next() to start, so we must
err := p.parseOne()
return p.block, err
}
for i, test := range []struct {
input string
shouldErr bool
keys []string
numTokens []int // number of tokens to expect in each segment
}{
{`localhost`, false, []string{
"localhost",
}, []int{}},
{`localhost
dir1`, false, []string{
"localhost",
}, []int{1}},
{`localhost:1234
dir1 foo bar`, false, []string{
"localhost:1234",
}, []int{3},
},
{`localhost {
dir1
}`, false, []string{
"localhost",
}, []int{1}},
{`localhost:1234 {
dir1 foo bar
dir2
}`, false, []string{
"localhost:1234",
}, []int{3, 1}},
{`http://localhost https://localhost
dir1 foo bar`, false, []string{
"http://localhost",
"https://localhost",
}, []int{3}},
{`http://localhost https://localhost {
dir1 foo bar
}`, false, []string{
"http://localhost",
"https://localhost",
}, []int{3}},
{`http://localhost, https://localhost {
dir1 foo bar
}`, false, []string{
"http://localhost",
"https://localhost",
}, []int{3}},
{`http://localhost, {
}`, true, []string{
"http://localhost",
}, []int{}},
{`host1:80, http://host2.com
dir1 foo bar
dir2 baz`, false, []string{
"host1:80",
"http://host2.com",
}, []int{3, 2}},
{`http://host1.com,
http://host2.com,
https://host3.com`, false, []string{
"http://host1.com",
"http://host2.com",
"https://host3.com",
}, []int{}},
{`http://host1.com:1234, https://host2.com
dir1 foo {
bar baz
}
dir2`, false, []string{
"http://host1.com:1234",
"https://host2.com",
}, []int{6, 1}},
{`127.0.0.1
dir1 {
bar baz
}
dir2 {
foo bar
}`, false, []string{
"127.0.0.1",
}, []int{5, 5}},
{`localhost
dir1 {
foo`, true, []string{
"localhost",
}, []int{3}},
{`localhost
dir1 {
}`, false, []string{
"localhost",
}, []int{3}},
{`localhost
dir1 {
} }`, true, []string{
"localhost",
}, []int{}},
{`localhost
dir1 {
nested {
foo
}
}
dir2 foo bar`, false, []string{
"localhost",
}, []int{7, 3}},
{``, false, []string{}, []int{}},
{`localhost
dir1 arg1
import testdata/import_test1.txt`, false, []string{
"localhost",
}, []int{2, 3, 1}},
{`import testdata/import_test2.txt`, false, []string{
"host1",
}, []int{1, 2}},
{`import testdata/import_test1.txt testdata/import_test2.txt`, true, []string{}, []int{}},
{`import testdata/not_found.txt`, true, []string{}, []int{}},
{`""`, false, []string{}, []int{}},
{``, false, []string{}, []int{}},
// test cases found by fuzzing!
{`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)
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected an error, but didn't get one", i)
}
if !test.shouldErr && err != nil {
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
}
if len(result.Keys) != len(test.keys) {
t.Errorf("Test %d: Expected %d keys, got %d",
i, len(test.keys), len(result.Keys))
continue
}
for j, addr := range result.Keys {
if addr != test.keys[j] {
t.Errorf("Test %d, key %d: Expected '%s', but was '%s'",
i, j, test.keys[j], addr)
}
}
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 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
}
}
}
}
func TestRecursiveImport(t *testing.T) {
testParseOne := func(input string) (ServerBlock, error) {
p := testParser(input)
p.Next() // parseOne doesn't call Next() to start, so we must
err := p.parseOne()
return p.block, err
}
isExpected := func(got ServerBlock) bool {
if len(got.Keys) != 1 || got.Keys[0] != "localhost" {
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
return false
}
if len(got.Segments) != 2 {
t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments))
return false
}
if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 2 {
t.Errorf("got unexpected tokens: %v", got.Segments)
return false
}
return true
}
recursiveFile1, err := filepath.Abs("testdata/recursive_import_test1")
if err != nil {
t.Fatal(err)
}
recursiveFile2, err := filepath.Abs("testdata/recursive_import_test2")
if err != nil {
t.Fatal(err)
}
// test relative recursive import
err = ioutil.WriteFile(recursiveFile1, []byte(
`localhost
dir1
import recursive_import_test2`), 0644)
if err != nil {
t.Fatal(err)
}
defer os.Remove(recursiveFile1)
err = ioutil.WriteFile(recursiveFile2, []byte("dir2 1"), 0644)
if err != nil {
t.Fatal(err)
}
defer os.Remove(recursiveFile2)
// import absolute path
result, err := testParseOne("import " + recursiveFile1)
if err != nil {
t.Fatal(err)
}
if !isExpected(result) {
t.Error("absolute+relative import failed")
}
// import relative path
result, err = testParseOne("import testdata/recursive_import_test1")
if err != nil {
t.Fatal(err)
}
if !isExpected(result) {
t.Error("relative+relative import failed")
}
// test absolute recursive import
err = ioutil.WriteFile(recursiveFile1, []byte(
`localhost
dir1
import `+recursiveFile2), 0644)
if err != nil {
t.Fatal(err)
}
// import absolute path
result, err = testParseOne("import " + recursiveFile1)
if err != nil {
t.Fatal(err)
}
if !isExpected(result) {
t.Error("absolute+absolute import failed")
}
// import relative path
result, err = testParseOne("import testdata/recursive_import_test1")
if err != nil {
t.Fatal(err)
}
if !isExpected(result) {
t.Error("relative+absolute import failed")
}
}
func TestDirectiveImport(t *testing.T) {
testParseOne := func(input string) (ServerBlock, error) {
p := testParser(input)
p.Next() // parseOne doesn't call Next() to start, so we must
err := p.parseOne()
return p.block, err
}
isExpected := func(got ServerBlock) bool {
if len(got.Keys) != 1 || got.Keys[0] != "localhost" {
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
return false
}
if len(got.Segments) != 2 {
t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments))
return false
}
if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 8 {
t.Errorf("got unexpected tokens: %v", got.Segments)
return false
}
return true
}
directiveFile, err := filepath.Abs("testdata/directive_import_test")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(directiveFile, []byte(`prop1 1
prop2 2`), 0644)
if err != nil {
t.Fatal(err)
}
defer os.Remove(directiveFile)
// import from existing file
result, err := testParseOne(`localhost
dir1
proxy {
import testdata/directive_import_test
transparent
}`)
if err != nil {
t.Fatal(err)
}
if !isExpected(result) {
t.Error("directive import failed")
}
// import from nonexistent file
_, err = testParseOne(`localhost
dir1
proxy {
import testdata/nonexistent_file
transparent
}`)
if err == nil {
t.Fatal("expected error when importing a nonexistent file")
}
}
func TestParseAll(t *testing.T) {
for i, test := range []struct {
input string
shouldErr bool
keys [][]string // keys per server block, in order
}{
{`localhost`, false, [][]string{
{"localhost"},
}},
{`localhost:1234`, false, [][]string{
{"localhost:1234"},
}},
{`localhost:1234 {
}
localhost:2015 {
}`, false, [][]string{
{"localhost:1234"},
{"localhost:2015"},
}},
{`localhost:1234, http://host2`, false, [][]string{
{"localhost:1234", "http://host2"},
}},
{`localhost:1234, http://host2,`, true, [][]string{}},
{`http://host1.com, http://host2.com {
}
https://host3.com, https://host4.com {
}`, false, [][]string{
{"http://host1.com", "http://host2.com"},
{"https://host3.com", "https://host4.com"},
}},
{`import testdata/import_glob*.txt`, false, [][]string{
{"glob0.host0"},
{"glob0.host1"},
{"glob1.host0"},
{"glob2.host0"},
}},
{`import notfound/*`, false, [][]string{}}, // glob needn't error with no matches
{`import notfound/file.conf`, true, [][]string{}}, // but a specific file should
} {
p := testParser(test.input)
blocks, err := p.parseAll()
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected an error, but didn't get one", i)
}
if !test.shouldErr && err != nil {
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
}
if len(blocks) != len(test.keys) {
t.Errorf("Test %d: Expected %d server blocks, got %d",
i, len(test.keys), len(blocks))
continue
}
for j, block := range blocks {
if len(block.Keys) != len(test.keys[j]) {
t.Errorf("Test %d: Expected %d keys in block %d, got %d",
i, len(test.keys[j]), j, len(block.Keys))
continue
}
for k, addr := range block.Keys {
if addr != test.keys[j][k] {
t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'",
i, j, k, test.keys[j][k], addr)
}
}
}
}
}
func TestEnvironmentReplacement(t *testing.T) {
os.Setenv("FOOBAR", "foobar")
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)
}
}
}
func TestSnippets(t *testing.T) {
p := testParser(`
(common) {
gzip foo
errors stderr
}
http://example.com {
import common
}
`)
blocks, err := p.parseAll()
if err != nil {
t.Fatal(err)
}
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].Segments) != 2 {
t.Fatalf("Server block should have tokens from import, got: %+v", blocks[0])
}
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].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) {
file, err := ioutil.TempFile("", t.Name())
if err != nil {
panic(err) // get a stack trace so we know where this was called from.
}
if _, err := file.WriteString(str); err != nil {
panic(err)
}
if err := file.Close(); err != nil {
panic(err)
}
return file.Name()
}
func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) {
fileName := writeStringToTempFileOrDie(t, `
http://example.com {
# This isn't an import directive, it's just an arg with value 'import'
basicauth / import password
}
`)
// Parse the root file that imports the other one.
p := testParser(`import ` + fileName)
blocks, err := p.parseAll()
if err != nil {
t.Fatal(err)
}
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:
// basicauth / import /path/to/test/dir/password
// referencing a file that (probably) doesn't exist and changing the
// password!
t.Errorf("Expected basicauth tokens to be 'basicauth / import password' but got %#q", line)
}
}
func TestSnippetAcrossMultipleFiles(t *testing.T) {
// Make the derived Caddyfile that expects (common) to be defined.
fileName := writeStringToTempFileOrDie(t, `
http://example.com {
import common
}
`)
// Parse the root file that defines (common) and then imports the other one.
p := testParser(`
(common) {
gzip foo
}
import ` + fileName + `
`)
blocks, err := p.parseAll()
if err != nil {
t.Fatal(err)
}
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].Segments) != 1 {
t.Fatalf("Server block should have tokens from import")
}
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)}
}
+6
View File
@@ -0,0 +1,6 @@
glob0.host0 {
dir2 arg1
}
glob0.host1 {
}
+4
View File
@@ -0,0 +1,4 @@
glob1.host0 {
dir1
dir2 arg1
}
+3
View File
@@ -0,0 +1,3 @@
glob2.host0 {
dir2 arg1
}
View File
View File
+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
// and then adds a key to that object named fieldName with the value fieldVal.
// This is useful for JSON-encoding module values where the module name has to
// be described within the object by a certain key; for example,
// "responder": "file_server" for a file server HTTP responder. The val must
// encode into a map[string]interface{} (i.e. it must be a struct or map),
// and any errors are converted into warnings, so this can be conveniently
// used when filling a struct. For correct code, there should be no errors.
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.
func RegisterAdapter(name string, adapter Adapter) error {
if _, ok := configAdapters[name]; ok {
return fmt.Errorf("%s: already registered", name)
}
configAdapters[name] = adapter
return 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)
+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 (
"fmt"
"net"
"reflect"
"strconv"
"strings"
"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: strings.ToLower(a.Scheme),
Host: strings.ToLower(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
}
@@ -0,0 +1,28 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build gofuzz
package httpcaddyfile
func FuzzParseAddress(data []byte) int {
addr, err := ParseAddress(string(data))
if err != nil {
if addr == (Address{}) {
return 1
}
return 0
}
return 1
}
+163
View File
@@ -0,0 +1,163 @@
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: "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: ":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: Normalized key for address '%s' was '%s' but expected '%s'", i, tc.input, actual, tc.expect)
}
}
}
+561
View File
@@ -0,0 +1,561 @@
// 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"
"html"
"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"
"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...>
// alpn <values...>
// load <paths...>
// ca <acme_ca_endpoint>
// ca_root <pem_file>
// dns <provider_name>
// on_demand
// }
//
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 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 h.NextBlock(0) {
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 "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 "dns":
if !h.Next() {
return nil, h.ArgErr()
}
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
provName := h.Val()
if acmeIssuer.Challenges == nil {
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
}
dnsProvModule, err := caddy.GetModule("tls.dns." + provName)
if err != nil {
return nil, h.Errf("getting DNS provider module named '%s': %v", provName, err)
}
acmeIssuer.Challenges.DNSRaw = caddyconfig.JSONModuleObject(dnsProvModule.New(), "provider", 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.certificate_loader",
Value: fileLoader,
})
}
if len(folderLoader) > 0 {
configVals = append(configVals, ConfigValue{
Class: "tls.certificate_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 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: acmeIssuer,
})
} else if internalIssuer != nil {
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: internalIssuer,
})
}
// 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)
for h.Next() {
for nesting := h.Nesting(); h.NextBlock(nesting); {
dir := h.Val()
dirFunc, ok := registeredDirectives[dir]
if !ok {
return nil, h.Errf("unrecognized directive: %s", dir)
}
subHelper := h
subHelper.Dispenser = h.NewFromNextSegment()
results, err := dirFunc(subHelper)
if err != nil {
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
}
for _, result := range results {
switch handler := result.Value.(type) {
case caddyhttp.Route:
sr.Routes = append(sr.Routes, handler)
case caddyhttp.Subroute:
// directives which return a literal subroute instead of a route
// means they intend to keep those handlers together without
// them being reordered; we're doing that anyway since we're in
// the route directive, so just append its handlers
sr.Routes = append(sr.Routes, handler.Routes...)
default:
return nil, h.Errf("%s directive returned something other than an HTTP route or subroute: %#v (only handler directives can be used in routes)", dir, result.Value)
}
}
}
}
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() {
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
}
+467
View File
@@ -0,0 +1,467 @@
// 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{
"root",
"header",
"redir",
"rewrite",
// URI manipulation
"uri",
"try_files",
// middleware handlers; some wrap responses
"basicauth",
"request_header",
"encode",
"templates",
// special routing directives
"handle",
"route",
// handlers that typically respond to requests
"respond",
"reverse_proxy",
"php_fastcgi",
"file_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
})
}
// 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}}
}
// 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
}
// while we are sorting, we will need to decode a route's path matcher
// in order to sub-sort by path length; we can amortize this operation
// for efficiency by storing the decoded matchers in a slice
decodedMatchers := make([]caddyhttp.MatchPath, len(routes))
sort.SliceStable(routes, func(i, j int) bool {
iDir, jDir := routes[i].directive, routes[j].directive
if iDir == jDir {
// directives are the same; sub-sort by path matcher length
// if there's only one matcher set and one path (common case)
iRoute, ok := routes[i].Value.(caddyhttp.Route)
if !ok {
return false
}
jRoute, ok := routes[j].Value.(caddyhttp.Route)
if !ok {
return false
}
// use already-decoded matcher, or decode if it's the first time seeing it
iPM, jPM := decodedMatchers[i], decodedMatchers[j]
if iPM == nil && len(iRoute.MatcherSetsRaw) == 1 {
var pathMatcher caddyhttp.MatchPath
_ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &pathMatcher)
decodedMatchers[i] = pathMatcher
iPM = pathMatcher
}
if jPM == nil && len(jRoute.MatcherSetsRaw) == 1 {
var pathMatcher caddyhttp.MatchPath
_ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &pathMatcher)
decodedMatchers[j] = pathMatcher
jPM = pathMatcher
}
// sort by longer path (more specific) first; missing
// path matchers are treated as zero-length paths
var iPathLen, jPathLen int
if iPM != nil {
iPathLen = len(iPM[0])
}
if jPM != nil {
jPathLen = len(jPM[0])
}
return iPathLen > jPathLen
}
return dirPositions[iDir] < dirPositions[jDir]
})
}
// 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) {
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 buildSubroute(allResults, h.groupCounter)
}
// 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)
)
var registeredDirectives = make(map[string]UnmarshalFunc)
@@ -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{
Address{Original: "foo", Host: "foo"},
},
[]string{"foo"},
[]string{"foo"},
},
{
[]Address{
Address{Original: "foo", Host: "foo"},
Address{Original: "bar", Host: "bar"},
},
[]string{"bar", "foo"},
[]string{"bar", "foo"},
},
{
[]Address{
Address{Original: ":2015", Port: "2015"},
},
[]string{}, []string{},
},
{
[]Address{
Address{Original: ":443", Port: "443"},
},
[]string{}, []string{},
},
{
[]Address{
Address{Original: "foo", Host: "foo"},
Address{Original: ":2015", Port: "2015"},
},
[]string{}, []string{"foo"},
},
{
[]Address{
Address{Original: "example.com:2015", Host: "example.com", Port: "2015"},
},
[]string{"example.com"},
[]string{"example.com:2015"},
},
{
[]Address{
Address{Original: "example.com:80", Host: "example.com", Port: "80"},
},
[]string{"example.com"},
[]string{"example.com"},
},
{
[]Address{
Address{Original: "https://:2015/foo", Scheme: "https", Port: "2015", Path: "/foo"},
},
[]string{},
[]string{},
},
{
[]Address{
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
+169
View File
@@ -0,0 +1,169 @@
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,
},
} {
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,
},
} {
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
}
}
}
+250
View File
@@ -0,0 +1,250 @@
// 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"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddytls"
)
func parseOptHTTPPort(d *caddyfile.Dispenser) (int, 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) (int, 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 parseOptExperimentalHTTP3(d *caddyfile.Dispenser) (bool, error) {
return true, nil
}
func parseOptOrder(d *caddyfile.Dispenser) ([]string, 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) (caddy.StorageConverter, error) {
if !d.Next() {
return nil, d.ArgErr()
}
args := d.RemainingArgs()
if len(args) != 1 {
return nil, d.ArgErr()
}
modName := args[0]
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 parseOptSingleString(d *caddyfile.Dispenser) (string, error) {
d.Next() // consume parameter name
if !d.Next() {
return "", d.ArgErr()
}
val := d.Val()
if d.Next() {
return "", d.ArgErr()
}
return val, nil
}
func parseOptAdmin(d *caddyfile.Dispenser) (string, error) {
if d.Next() {
var listenAddress string
if !d.AllArgs(&listenAddress) {
return "", d.ArgErr()
}
if listenAddress == "" {
listenAddress = caddy.DefaultAdminListen
}
return listenAddress, nil
}
return "", nil
}
func parseOptOnDemand(d *caddyfile.Dispenser) (*caddytls.OnDemandConfig, 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 := time.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
}
+439
View File
@@ -0,0 +1,439 @@
// 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"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
)
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
// count how many server blocks have a 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 serverBlocksWithHostlessKey int
hostsSharedWithHostlessKey := make(map[string]struct{})
for _, pair := range pairings {
for _, sb := range pair.serverBlocks {
for _, addr := range sb.keys {
if addr.Host == "" {
serverBlocksWithHostlessKey++
// 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 {
var ok bool
if acmeIssuer, ok = ap.Issuer.(*caddytls.ACMEIssuer); !ok {
break
}
}
// proceed to configure the ACME issuer's bind host, without
// overwriting any existing settings
if acmeIssuer == 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 {
// 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 serverBlocksWithHostlessKey > 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 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.certificate_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
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 {
// 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{}{}
}
}
// consolidate automation policies that are the exact same
tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
}
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) {
acmeCA, hasACMECA := options["acme_ca"]
acmeDNS, hasACMEDNS := options["acme_dns"]
acmeCARoot, hasACMECARoot := options["acme_ca_root"]
email, hasEmail := options["email"]
localCerts, hasLocalCerts := options["local_certs"]
keyType, hasKeyType := options["key_type"]
hasGlobalAutomationOpts := hasACMECA || hasACMEDNS || hasACMECARoot || 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 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("tls.dns." + provName)
if err != nil {
return nil, fmt.Errorf("getting DNS provider module named '%s': %v", provName, err)
}
mgr.Challenges = &caddytls.ChallengesConfig{
DNSRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "provider", provName, &warnings),
}
}
if acmeCARoot != nil {
mgr.TrustedRootsPEMFiles = []string{acmeCARoot.(string)}
}
if keyType != nil {
ap.KeyType = keyType.(string)
}
ap.Issuer = mgr // we'll encode it later
}
return ap, nil
}
// 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 {
aps[i].Subjects = append(aps[i].Subjects, aps[j].Subjects...)
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)
},
}
+23
View File
@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIID5zCCAs8CFG4+w/pqR5AZQ+aVB330uRRRKMF0MA0GCSqGSIb3DQEBCwUAMIGv
MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZBgNVBAoMEkxvY2FsIERldmVs
b3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxvcGVtZW50MRowGAYDVQQDDBFh
LmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9jYWwgRGV2ZWxvcGVtZW50MSAw
HgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2NhbDAeFw0yMDAzMTMxODUwMTda
Fw0zMDAzMTExODUwMTdaMIGvMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZ
BgNVBAoMEkxvY2FsIERldmVsb3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxv
cGVtZW50MRowGAYDVQQDDBFhLmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9j
YWwgRGV2ZWxvcGVtZW50MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2Nh
bDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMd9pC9wF7j0459FndPs
Deud/rq41jEZFsVOVtjQgjS1A5ct6NfeMmSlq8i1F7uaTMPZjbOHzY6y6hzLc9+y
/VWNgyUC543HjXnNTnp9Xug6tBBxOxvRMw5mv2nAyzjBGDePPgN84xKhOXG2Wj3u
fOZ+VPVISefRNvjKfN87WLJ0B0HI9wplG5ASVdPQsWDY1cndrZgt2sxQ/3fjIno4
VvrgRWC9Penizgps/a0ZcFZMD/6HJoX/mSZVa1LjopwbMTXvyHCpXkth21E+rBt6
I9DMHerdioVQcX25CqPmAwePxPZSNGEQo/Qu32kzcmscmYxTtYBhDa+yLuHgGggI
j7ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAP/94KPtkpYtkWADnhtzDmgQ6Q1pH
SubTUZdCwQtm6/LrvpT+uFNsOj4L3Mv3TVUnIQDmKd5VvR42W2MRBiTN2LQptgEn
C7g9BB+UA9kjL3DPk1pJMjzxLHohh0uNLi7eh4mAj8eNvjz9Z4qMWPQoVS0y7/ZK
cCBRKh2GkIqKm34ih6pX7xmMpPEQsFoTVPRHYJfYD1SZ8Iui+EN+7WqLuJWPsPXw
JM1HuZKn7pZmJU2MZZBsrupHGUvNMbBg2mFJcxt4D1VvU+p+a67PSjpFQ6dJG2re
pZoF+N1vMGAFkxe6UqhcC/bXDX+ILVQHJ+RNhzDO6DcWf8dRrC2LaJk3WA==
-----END CERTIFICATE-----
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAx32kL3AXuPTjn0Wd0+wN653+urjWMRkWxU5W2NCCNLUDly3o
194yZKWryLUXu5pMw9mNs4fNjrLqHMtz37L9VY2DJQLnjceNec1Oen1e6Dq0EHE7
G9EzDma/acDLOMEYN48+A3zjEqE5cbZaPe585n5U9UhJ59E2+Mp83ztYsnQHQcj3
CmUbkBJV09CxYNjVyd2tmC3azFD/d+MiejhW+uBFYL096eLOCmz9rRlwVkwP/ocm
hf+ZJlVrUuOinBsxNe/IcKleS2HbUT6sG3oj0Mwd6t2KhVBxfbkKo+YDB4/E9lI0
YRCj9C7faTNyaxyZjFO1gGENr7Iu4eAaCAiPsQIDAQABAoIBAQDD/YFIBeWYlifn
e9risQDAIrp3sk7lb9O6Rwv1+Wxi4hBEABvJsYhq74VFK/3EF4UhyWR5JIvkjYyK
e6w887oGyoA05ZSe65XoO7fFidSrbbkoikZbPv3dQT7/ZCWEfdkQBNAVVyY0UGeC
e3hPbjYRsb5AOSQ694X9idqC6uhqcOrBDjITFrctUoP4S6l9A6a+mLSUIwiICcuh
mrNl+j0lzy7DMXRp/Z5Hyo5kuUlrC0dCLa1UHqtrrK7MR55AVEOihSNp1w+OC+vw
f0VjE4JUtO7RQEQUmD1tDfLXwNfMFeWaobB2W0WMvRg0IqoitiqPxsPHRm56OxfM
SRo/Q7QBAoGBAP8DapzBMuaIcJ7cE8Yl07ZGndWWf8buIKIItGF8rkEO3BXhrIke
EmpOi+ELtpbMOG0APhORZyQ58f4ZOVrqZfneNKtDiEZV4mJZaYUESm1pU+2Y6+y5
g4bpQSVKN0ow0xR+MH7qDYtSlsmBU7qAOz775L7BmMA1Bnu72aN/H1JBAoGBAMhD
OzqCSakHOjUbEd22rPwqWmcIyVyo04gaSmcVVT2dHbqR4/t0gX5a9D9U2qwyO6xi
/R+PXyMd32xIeVR2D/7SQ0x6dK68HXICLV8ofHZ5UQcHbxy5og4v/YxSZVTkN374
cEsUeyB0s/UPOHLktFU5hpIlON72/Rp7b+pNIwFxAoGAczpq+Qu/YTWzlcSh1r4O
7OT5uqI3eH7vFehTAV3iKxl4zxZa7NY+wfRd9kFhrr/2myIp6pOgBFl+hC+HoBIc
JAyIxf5M3GNAWOpH6MfojYmzV7/qktu8l8BcJGplk0t+hVsDtMUze4nFAqZCXBpH
Kw2M7bjyuZ78H/rgu6TcVUECgYEAo1M5ldE2U/VCApeuLX1TfWDpU8i1uK0zv3d5
oLKkT1i5KzTak3SEO9HgC1qf8PoS8tfUio26UICHe99rnHehOfivzEq+qNdgyF+A
M3BoeZMdgzcL5oh640k+Zte4LtDlddcWdhUhCepD7iPYrNNbQ3pkBwL2a9lRuOxc
7OC2IPECgYBH8f3OrwXjDltIG1dDvuDPNljxLZbFEFbQyVzMePYNftgZknAyGEdh
NW/LuWeTzstnmz/s6RE3jN5ZrrMa4sW77VA9+yU9QW2dkHqFyukQ4sfuNg6kDDNZ
+lqZYMCLw0M5P9fIbmnIYwey7tXkHfmzoCpnYHGQDN6hL0Bh0zGwmg==
-----END RSA PRIVATE KEY-----
+23
View File
@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIID5zCCAs8CFFmAAFKV79uhzxc5qXbUw3oBNsYXMA0GCSqGSIb3DQEBCwUAMIGv
MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZBgNVBAoMEkxvY2FsIERldmVs
b3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxvcGVtZW50MRowGAYDVQQDDBEq
LmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9jYWwgRGV2ZWxvcGVtZW50MSAw
HgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2NhbDAeFw0yMDAzMDIwODAxMTZa
Fw0zMDAyMjgwODAxMTZaMIGvMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZ
BgNVBAoMEkxvY2FsIERldmVsb3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxv
cGVtZW50MRowGAYDVQQDDBEqLmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9j
YWwgRGV2ZWxvcGVtZW50MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2Nh
bDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJngfeirQkWaU8ihgIC5
SKpRQX/3koRjljDK/oCbhLs+wg592kIwVv06l7+mn7NSaNBloabjuA1GqyLRsNLL
ptrv0HvXa5qLx28+icsb2Ny3dJnQaj9w9PwjxQ1qZqEJfWRH1D8Vz9AmB+QSV/Gu
8e8alGFewlYZVfH1kbxoTT6QorF37TeA3bh1fgKFtzsGYKswcaZNdDBBHzLunCKZ
HU6U6L45hm+yLADj3mmDLafUeiVOt6MRLLoSD1eLRVSXGrNo+brJ87zkZntI9+W1
JxOBoXtZCwka7k2DlAtLihsrmBZA2ZC9yVeu/SQy3qb3iCNnTFTCyAnWeTCr6Tcq
6w8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAOWfXqpAmD4C3wGiMeZAeaaS4hDAR
+JmN+avPDA6F6Bq7DB4NJuIwVUlaDL2s07w5VJJtW52aZVKoBlgHR5yG/XUli6J7
YUJRmdQJvHUSu26cmKvyoOaTrEYbmvtGICWtZc8uTlMf9wQZbJA4KyxTgEQJDXsZ
B2XFe+wVdhAgEpobYDROi+l/p8TL5z3U24LpwVTcJy5sEZVv7Wfs886IyxU8ORt8
VZNcDiH6V53OIGeiufIhia/mPe6jbLntfGZfIFxtCcow4IA/lTy1ned7K5fmvNNb
ZilxOQUk+wVK8genjdrZVAnAxsYLHJIb5yf9O7rr6fWciVMF3a0k5uNK1w==
-----END CERTIFICATE-----
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAmeB96KtCRZpTyKGAgLlIqlFBf/eShGOWMMr+gJuEuz7CDn3a
QjBW/TqXv6afs1Jo0GWhpuO4DUarItGw0sum2u/Qe9drmovHbz6JyxvY3Ld0mdBq
P3D0/CPFDWpmoQl9ZEfUPxXP0CYH5BJX8a7x7xqUYV7CVhlV8fWRvGhNPpCisXft
N4DduHV+AoW3OwZgqzBxpk10MEEfMu6cIpkdTpTovjmGb7IsAOPeaYMtp9R6JU63
oxEsuhIPV4tFVJcas2j5usnzvORme0j35bUnE4Ghe1kLCRruTYOUC0uKGyuYFkDZ
kL3JV679JDLepveII2dMVMLICdZ5MKvpNyrrDwIDAQABAoIBAFcPK01zb6hfm12c
+k5aBiHOnUdgc/YRPg1XHEz5MEycQkDetZjTLrRQ7UBSbnKPgpu9lIsOtbhVLkgh
6XAqJroiCou2oruqr+hhsqZGmBiwdvj7cNF6ADGTr05az7v22YneFdinZ481pStF
sZocx+bm2+KHMV5zMSwXKyA0xtdJLxs2yklniDBxSZRppgppq1pDPprP5DkgKPfe
3ekUmbQd5bHmivhW8ItbJLuf82XSsMBZ9ZhKiKIlWlbKAgiSV3SqnUQb5fi7l8hG
yYZxbuCUIGFwKmEpUBBt/nyxrOlMiNtDh9JhrPmijTV3slq70pCLwLL/Ai2aeear
EVA5VhkCgYEAyAmxfPqc2P7BsDAp67/sA7OEPso9qM4WyuWiVdlX2gb9TLNLYbPX
Kk/UmpAIVzpoTAGY5Zp3wkvdD/ou8uUQsE8ioNn4S1a4G9XURH1wVhcEbUiAKI1S
QVBH9B/Pj3eIp5OTKwob0Wj7DNdxoH7ed/Eok0EaTWzOA8pCWADKv/MCgYEAxOzY
YsX7Nl+eyZr2+9unKyeAK/D1DCT/o99UUAHx72/xaBVP/06cfzpvKBNcF9iYc+fq
R1yIUIrDRoSmYKBq+Kb3+nOg1nrqih/NBTokbTiI4Q+/30OQt0Al1e7y9iNKqV8H
jYZItzluGNrWKedZbATwBwbVCY2jnNl6RMDnS3UCgYBxj3cwQUHLuoyQjjcuO80r
qLzZvIxWiXDNDKIk5HcIMlGYOmz/8U2kGp/SgxQJGQJeq8V2C0QTjGfaCyieAcaA
oNxCvptDgd6RBsoze5bLeNOtiqwe2WOp6n5+q5R0mOJ+Z7vzghCayGNFPgWmnH+F
TeW/+wSIkc0+v5L8TK7NWwKBgBrlWlyLO9deUfqpHqihhICBYaEexOlGuF+yZfqT
eW7BdFBJ8OYm33sFCR+JHV/oZlIWT8o1Wizd9vPPtEWoQ1P4wg/D8Si6GwSIeWEI
YudD/HX4x7T/rmlI6qIAg9CYW18sqoRq3c2gm2fro6qPfYgiWIItLbWjUcBfd7Ki
QjTtAoGARKdRv3jMWL84rlEx1nBRgL3pe9Dt+Uxzde2xT3ZeF+5Hp9NfU01qE6M6
1I6H64smqpetlsXmCEVKwBemP3pJa6avLKgIYiQvHAD/v4rs9mqgy1RTqtYyGNhR
1A/6dKkbiZ6wzePLLPasXVZxSKEviXf5gJooqumQVSVhCswyCZ0=
-----END RSA PRIVATE KEY-----
+373
View File
@@ -0,0 +1,373 @@
package caddytest
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"path"
"regexp"
"runtime"
"strings"
"testing"
"time"
"github.com/aryann/difflib"
"github.com/caddyserver/caddy/v2/caddyconfig"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
// plug in Caddy modules here
_ "github.com/caddyserver/caddy/v2/modules/standard"
)
// Defaults store any configuration required to make the tests run
type Defaults struct {
// Port we expect caddy to listening on
AdminPort int
// Certificates we expect to be loaded before attempting to run the tests
Certifcates []string
}
// Default testing values
var Default = Defaults{
AdminPort: 2019,
Certifcates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
}
var (
matchKey = regexp.MustCompile(`(/[\w\d\.]+\.key)`)
matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`)
)
type configLoadError struct {
Response string
}
func (e configLoadError) Error() string { return e.Response }
func timeElapsed(start time.Time, name string) {
elapsed := time.Since(start)
log.Printf("%s took %s", name, elapsed)
}
// InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type.
func InitServer(t *testing.T, rawConfig string, configType string) {
if err := initServer(t, rawConfig, configType); err != nil {
t.Logf("failed to load config: %s", err)
t.Fail()
}
}
// InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type.
func initServer(t *testing.T, rawConfig string, configType string) error {
if testing.Short() {
t.SkipNow()
return nil
}
err := validateTestPrerequisites()
if err != nil {
t.Skipf("skipping tests as failed integration prerequisites. %s", err)
return nil
}
t.Cleanup(func() {
if t.Failed() {
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
t.Log("unable to read the current config")
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
var out bytes.Buffer
json.Indent(&out, body, "", " ")
t.Logf("----------- failed with config -----------\n%s", out.String())
}
})
rawConfig = prependCaddyFilePath(rawConfig)
client := &http.Client{
Timeout: time.Second * 2,
}
start := time.Now()
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig))
if err != nil {
t.Errorf("failed to create request. %s", err)
return err
}
if configType == "json" {
req.Header.Add("Content-Type", "application/json")
} else {
req.Header.Add("Content-Type", "text/"+configType)
}
res, err := client.Do(req)
if err != nil {
t.Errorf("unable to contact caddy server. %s", err)
return err
}
timeElapsed(start, "caddytest: config load time")
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Errorf("unable to read response. %s", err)
return err
}
if res.StatusCode != 200 {
return configLoadError{Response: string(body)}
}
return nil
}
var hasValidated bool
var arePrerequisitesValid bool
func validateTestPrerequisites() error {
if hasValidated {
if !arePrerequisitesValid {
return errors.New("caddy integration prerequisites failed. see first error")
}
return nil
}
hasValidated = true
arePrerequisitesValid = false
// check certificates are found
for _, certName := range Default.Certifcates {
if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) {
return fmt.Errorf("caddy integration test certificates (%s) not found", certName)
}
}
if isCaddyAdminRunning() != nil {
// start inprocess caddy server
os.Args = []string{"caddy", "run"}
go func() {
caddycmd.Main()
}()
// wait for caddy to start
retries := 4
for ; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
time.Sleep(10 * time.Millisecond)
}
}
// assert that caddy is running
if err := isCaddyAdminRunning(); err != nil {
return err
}
arePrerequisitesValid = true
return nil
}
func isCaddyAdminRunning() error {
// assert that caddy is running
client := &http.Client{
Timeout: time.Second * 2,
}
_, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
return errors.New("caddy integration test caddy server not running. Expected to be listening on localhost:2019")
}
return nil
}
func getIntegrationDir() string {
_, filename, _, ok := runtime.Caller(1)
if !ok {
panic("unable to determine the current file path")
}
return path.Dir(filename)
}
// use the convention to replace /[certificatename].[crt|key] with the full path
// this helps reduce the noise in test configurations and also allow this
// to run in any path
func prependCaddyFilePath(rawConfig string) string {
r := matchKey.ReplaceAllString(rawConfig, getIntegrationDir()+"$1")
r = matchCert.ReplaceAllString(r, getIntegrationDir()+"$1")
return r
}
// creates a testing transport that forces call dialing connections to happen locally
func createTestingTransport() *http.Transport {
dialer := net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 5 * time.Second,
DualStack: true,
}
dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
parts := strings.Split(addr, ":")
destAddr := fmt.Sprintf("127.0.0.1:%s", parts[1])
log.Printf("caddytest: redirecting the dialer from %s to %s", addr, destAddr)
return dialer.DialContext(ctx, network, destAddr)
}
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
// AssertLoadError will load a config and expect an error
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
err := initServer(t, rawConfig, configType)
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error())
}
}
// AssertGetResponse request a URI and assert the status code and the body contains a string
func AssertGetResponse(t *testing.T, requestURI string, statusCode int, expectedBody string) (*http.Response, string) {
resp, body := AssertGetResponseBody(t, requestURI, statusCode)
if !strings.Contains(body, expectedBody) {
t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", requestURI, expectedBody, body)
}
return resp, body
}
// AssertGetResponseBody request a URI and assert the status code matches
func AssertGetResponseBody(t *testing.T, requestURI string, expectedStatusCode int) (*http.Response, string) {
client := &http.Client{
Transport: createTestingTransport(),
}
resp, err := client.Get(requestURI)
if err != nil {
t.Errorf("failed to call server %s", err)
return nil, ""
}
defer resp.Body.Close()
if expectedStatusCode != resp.StatusCode {
t.Errorf("requesting \"%s\" expected status code: %d but got %d", requestURI, expectedStatusCode, resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("unable to read the response body %s", err)
return nil, ""
}
return resp, string(body)
}
// AssertRedirect makes a request and asserts the redirection happens
func AssertRedirect(t *testing.T, requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
client := &http.Client{
CheckRedirect: redirectPolicyFunc,
Transport: createTestingTransport(),
}
resp, err := client.Get(requestURI)
if err != nil {
t.Errorf("failed to call server %s", err)
return nil
}
if expectedStatusCode != resp.StatusCode {
t.Errorf("requesting \"%s\" expected status code: %d but got %d", requestURI, expectedStatusCode, resp.StatusCode)
}
loc, err := resp.Location()
if err != nil {
t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err)
}
if expectedToLocation != loc.String() {
t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String())
}
return resp
}
// AssertAdapt adapts a config and then tests it against an expected result
func AssertAdapt(t *testing.T, rawConfig string, adapterName string, expectedResponse string) {
cfgAdapter := caddyconfig.GetAdapter(adapterName)
if cfgAdapter == nil {
t.Errorf("unrecognized config adapter '%s'", adapterName)
return
}
options := make(map[string]interface{})
options["pretty"] = "true"
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
if err != nil {
t.Errorf("adapting config using %s adapter: %v", adapterName, err)
return
}
if len(warnings) > 0 {
for _, w := range warnings {
t.Logf("warning: directive: %s : %s", w.Directive, w.Message)
}
}
diff := difflib.Diff(
strings.Split(expectedResponse, "\n"),
strings.Split(string(result), "\n"))
// scan for failure
failed := false
for _, d := range diff {
if d.Delta != difflib.Common {
failed = true
break
}
}
if failed {
for _, d := range diff {
switch d.Delta {
case difflib.Common:
fmt.Printf(" %s\n", d.Payload)
case difflib.LeftOnly:
fmt.Printf(" - %s\n", d.Payload)
case difflib.RightOnly:
fmt.Printf(" + %s\n", d.Payload)
}
}
t.Fail()
}
}
+33
View File
@@ -0,0 +1,33 @@
package caddytest
import (
"strings"
"testing"
)
func TestReplaceCertificatePaths(t *testing.T) {
rawConfig := `a.caddy.localhost:9443 {
tls /caddy.localhost.crt /caddy.localhost.key {
}
redir / https://b.caddy.localhost:9443/version 301
respond /version 200 {
body "hello from a.caddy.localhost"
}
}`
r := prependCaddyFilePath(rawConfig)
if !strings.Contains(r, getIntegrationDir()+"/caddy.localhost.crt") {
t.Error("expected the /caddy.localhost.crt to be expanded to include the full path")
}
if !strings.Contains(r, getIntegrationDir()+"/caddy.localhost.key") {
t.Error("expected the /caddy.localhost.crt to be expanded to include the full path")
}
if !strings.Contains(r, "https://b.caddy.localhost:9443/version") {
t.Error("expected redirect uri to be unchanged")
}
}
@@ -0,0 +1,285 @@
package integration
import (
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestHttpOnlyOnLocalhost(t *testing.T) {
caddytest.AssertAdapt(t, `
localhost:80 {
respond /version 200 {
body "hello from localhost"
}
}
`, "caddyfile", `{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}`)
}
func TestHttpOnlyOnAnyAddress(t *testing.T) {
caddytest.AssertAdapt(t, `
:80 {
respond /version 200 {
body "hello from localhost"
}
}
`, "caddyfile", `{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/version"
]
}
],
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
]
}
]
}
}
}
}
}`)
}
func TestHttpsOnDomain(t *testing.T) {
caddytest.AssertAdapt(t, `
a.caddy.localhost {
respond /version 200 {
body "hello from localhost"
}
}
`, "caddyfile", `{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"a.caddy.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}`)
}
func TestHttpOnlyOnDomain(t *testing.T) {
caddytest.AssertAdapt(t, `
http://a.caddy.localhost {
respond /version 200 {
body "hello from localhost"
}
}
`, "caddyfile", `{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"a.caddy.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"a.caddy.localhost"
]
}
}
}
}
}
}`)
}
func TestHttpOnlyOnNonStandardPort(t *testing.T) {
caddytest.AssertAdapt(t, `
http://a.caddy.localhost:81 {
respond /version 200 {
body "hello from localhost"
}
}
`, "caddyfile", `{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":81"
],
"routes": [
{
"match": [
{
"host": [
"a.caddy.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"a.caddy.localhost"
]
}
}
}
}
}
}`)
}
+68
View File
@@ -0,0 +1,68 @@
package integration
import (
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestRespond(t *testing.T) {
// arrange
caddytest.InitServer(t, `
{
http_port 9080
https_port 9443
}
localhost:9080 {
respond /version 200 {
body "hello from localhost"
}
}
`, "caddyfile")
// act and assert
caddytest.AssertGetResponse(t, "http://localhost:9080/version", 200, "hello from localhost")
}
func TestRedirect(t *testing.T) {
// arrange
caddytest.InitServer(t, `
{
http_port 9080
https_port 9443
}
localhost:9080 {
redir / http://localhost:9080/hello 301
respond /hello 200 {
body "hello from localhost"
}
}
`, "caddyfile")
// act and assert
caddytest.AssertRedirect(t, "http://localhost:9080/", "http://localhost:9080/hello", 301)
// follow redirect
caddytest.AssertGetResponse(t, "http://localhost:9080/", 200, "hello from localhost")
}
func TestDuplicateHosts(t *testing.T) {
// act and assert
caddytest.AssertLoadError(t,
`
localhost:9080 {
}
localhost:9080 {
}
`,
"caddyfile",
"duplicate site address not allowed")
}
+317
View File
@@ -0,0 +1,317 @@
package integration
import (
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestDefaultSNI(t *testing.T) {
// arrange
caddytest.InitServer(t, `{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from a.caddy.localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"match": [
{
"host": [
"127.0.0.1"
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"match": {
"sni": [
"127.0.0.1"
]
}
},
{
"default_sni": "*.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/caddy.localhost.crt",
"key": "/caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
// act and assert
// makes a request with no sni
caddytest.AssertGetResponse(t, "https://127.0.0.1:9443/version", 200, "hello from a")
}
func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
// arrange
caddytest.InitServer(t, `
{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from a",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"match": [
{
"host": [
"a.caddy.localhost",
"127.0.0.1"
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost",
"match": {
"sni": [
"a.caddy.localhost",
"127.0.0.1",
""
]
}
},
{
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
// act and assert
// makes a request with no sni
caddytest.AssertGetResponse(t, "https://127.0.0.1:9443/version", 200, "hello from a")
}
func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
// arrange
caddytest.InitServer(t, `
{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9443"
],
"routes": [
{
"handle": [
{
"body": "hello from a.caddy.localhost",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
],
"tls_connection_policies": [
{
"certificate_selection": {
"any_tag": ["cert0"]
},
"default_sni": "a.caddy.localhost"
}
]
}
}
},
"tls": {
"certificates": {
"load_files": [
{
"certificate": "/a.caddy.localhost.crt",
"key": "/a.caddy.localhost.key",
"tags": [
"cert0"
]
}
]
}
},
"pki": {
"certificate_authorities" : {
"local" : {
"install_trust": false
}
}
}
}
}
`, "json")
// act and assert
// makes a request with no sni
caddytest.AssertGetResponse(t, "https://127.0.0.1:9443/version", 200, "hello from a")
}
func TestHttpOnlyOnDomainWithSNI(t *testing.T) {
caddytest.AssertAdapt(t, `
{
default_sni a.caddy.localhost
}
:80 {
respond /version 200 {
body "hello from localhost"
}
}
`, "caddyfile", `{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"path": [
"/version"
]
}
],
"handle": [
{
"body": "hello from localhost",
"handler": "static_response",
"status_code": 200
}
]
}
]
}
}
}
}
}`)
}
+38
View File
@@ -0,0 +1,38 @@
// 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 main is the entry point of the Caddy application.
// Most of Caddy's functionality is provided through modules,
// which can be plugged in by adding their import below.
//
// There is no need to modify the Caddy source code to customize your
// builds. You can easily build a custom Caddy with these simple steps:
//
// 1. Copy this file (main.go) into a new folder
// 2. Edit the imports below to include the modules you want plugged in
// 3. Run `go mod init caddy`
// 4. Run `go install` or `go build` - you now have a custom binary!
//
package main
import (
caddycmd "github.com/caddyserver/caddy/v2/cmd"
// plug in Caddy modules here
_ "github.com/caddyserver/caddy/v2/modules/standard"
)
func main() {
caddycmd.Main()
}
+639
View File
@@ -0,0 +1,639 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddycmd
import (
"bytes"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"os/exec"
"reflect"
"runtime"
"runtime/debug"
"sort"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"go.uber.org/zap"
)
func cmdStart(fl Flags) (int, error) {
startCmdConfigFlag := fl.String("config")
startCmdConfigAdapterFlag := fl.String("adapter")
startCmdWatchFlag := fl.Bool("watch")
// open a listener to which the child process will connect when
// it is ready to confirm that it has successfully started
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("opening listener for success confirmation: %v", err)
}
defer ln.Close()
// craft the command with a pingback address and with a
// pipe for its stdin, so we can tell it our confirmation
// code that we expect so that some random port scan at
// the most unfortunate time won't fool us into thinking
// the child succeeded (i.e. the alternative is to just
// wait for any connection on our listener, but better to
// ensure it's the process we're expecting - we can be
// sure by giving it some random bytes and having it echo
// them back to us)
cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
if startCmdConfigFlag != "" {
cmd.Args = append(cmd.Args, "--config", startCmdConfigFlag)
}
if startCmdConfigAdapterFlag != "" {
cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag)
}
if startCmdWatchFlag {
cmd.Args = append(cmd.Args, "--watch")
}
stdinpipe, err := cmd.StdinPipe()
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("creating stdin pipe: %v", err)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// generate the random bytes we'll send to the child process
expect := make([]byte, 32)
_, err = rand.Read(expect)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("generating random confirmation bytes: %v", err)
}
// begin writing the confirmation bytes to the child's
// stdin; use a goroutine since the child hasn't been
// started yet, and writing synchronously would result
// in a deadlock
go func() {
stdinpipe.Write(expect)
stdinpipe.Close()
}()
// start the process
err = cmd.Start()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy process: %v", err)
}
// there are two ways we know we're done: either
// the process will connect to our listener, or
// it will exit with an error
success, exit := make(chan struct{}), make(chan error)
// in one goroutine, we await the success of the child process
go func() {
for {
conn, err := ln.Accept()
if err != nil {
if !strings.Contains(err.Error(), "use of closed network connection") {
log.Println(err)
}
break
}
err = handlePingbackConn(conn, expect)
if err == nil {
close(success)
break
}
log.Println(err)
}
}()
// in another goroutine, we await the failure of the child process
go func() {
err := cmd.Wait() // don't send on this line! Wait blocks, but send starts before it unblocks
exit <- err // sending on separate line ensures select won't trigger until after Wait unblocks
}()
// when one of the goroutines unblocks, we're done and can exit
select {
case <-success:
fmt.Printf("Successfully started Caddy (pid=%d) - Caddy is running in the background\n", cmd.Process.Pid)
case err := <-exit:
return caddy.ExitCodeFailedStartup,
fmt.Errorf("caddy process exited with error: %v", err)
}
return caddy.ExitCodeSuccess, nil
}
func cmdRun(fl Flags) (int, error) {
runCmdConfigFlag := fl.String("config")
runCmdConfigAdapterFlag := fl.String("adapter")
runCmdResumeFlag := fl.Bool("resume")
runCmdPrintEnvFlag := fl.Bool("environ")
runCmdWatchFlag := fl.Bool("watch")
runCmdPingbackFlag := fl.String("pingback")
// if we are supposed to print the environment, do that first
if runCmdPrintEnvFlag {
printEnvironment()
}
// TODO: This is TEMPORARY, until the RCs
moveStorage()
// load the config, depending on flags
var config []byte
var err error
if runCmdResumeFlag {
config, err = ioutil.ReadFile(caddy.ConfigAutosavePath)
if os.IsNotExist(err) {
// not a bad error; just can't resume if autosave file doesn't exist
caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
runCmdResumeFlag = false
} else if err != nil {
return caddy.ExitCodeFailedStartup, err
} else {
if runCmdConfigFlag == "" {
caddy.Log().Info("resuming from last configuration",
zap.String("autosave_file", caddy.ConfigAutosavePath))
} else {
// if they also specified a config file, user should be aware that we're not
// using it (doing so could lead to data/config loss by overwriting!)
caddy.Log().Warn("--config and --resume flags were used together; ignoring --config and resuming from last configuration",
zap.String("autosave_file", caddy.ConfigAutosavePath))
}
}
}
// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
var configFile string
if !runCmdResumeFlag {
config, configFile, err = loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
}
// run the initial config
err = caddy.Load(config, true)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err)
}
caddy.Log().Info("serving initial configuration")
// if we are to report to another process the successful start
// of the server, do so now by echoing back contents of stdin
if runCmdPingbackFlag != "" {
confirmationBytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading confirmation bytes from stdin: %v", err)
}
conn, err := net.Dial("tcp", runCmdPingbackFlag)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("dialing confirmation address: %v", err)
}
defer conn.Close()
_, err = conn.Write(confirmationBytes)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("writing confirmation bytes to %s: %v", runCmdPingbackFlag, err)
}
}
// if enabled, reload config file automatically on changes
// (this better only be used in dev!)
if runCmdWatchFlag {
go watchConfigFile(configFile, runCmdConfigAdapterFlag)
}
// warn if the environment does not provide enough information about the disk
hasXDG := os.Getenv("XDG_DATA_HOME") != "" &&
os.Getenv("XDG_CONFIG_HOME") != "" &&
os.Getenv("XDG_CACHE_HOME") != ""
switch runtime.GOOS {
case "windows":
if os.Getenv("HOME") == "" && os.Getenv("USERPROFILE") == "" && !hasXDG {
caddy.Log().Warn("neither HOME nor USERPROFILE environment variables are set - please fix; some assets might be stored in ./caddy")
}
case "plan9":
if os.Getenv("home") == "" && !hasXDG {
caddy.Log().Warn("$home environment variable is empty - please fix; some assets might be stored in ./caddy")
}
default:
if os.Getenv("HOME") == "" && !hasXDG {
caddy.Log().Warn("$HOME environment variable is empty - please fix; some assets might be stored in ./caddy")
}
}
select {}
}
func cmdStop(fl Flags) (int, error) {
stopCmdAddrFlag := fl.String("address")
adminAddr := caddy.DefaultAdminListen
if stopCmdAddrFlag != "" {
adminAddr = stopCmdAddrFlag
}
stopEndpoint := fmt.Sprintf("http://%s/stop", adminAddr)
req, err := http.NewRequest(http.MethodPost, stopEndpoint, nil)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("making request: %v", err)
}
req.Header.Set("Origin", adminAddr)
err = apiRequest(req)
if err != nil {
caddy.Log().Warn("failed using API to stop instance",
zap.String("endpoint", stopEndpoint),
zap.Error(err),
)
return caddy.ExitCodeFailedStartup, err
}
return caddy.ExitCodeSuccess, nil
}
func cmdReload(fl Flags) (int, error) {
reloadCmdConfigFlag := fl.String("config")
reloadCmdConfigAdapterFlag := fl.String("adapter")
reloadCmdAddrFlag := fl.String("address")
// get the config in caddy's native format
config, configFile, err := loadConfig(reloadCmdConfigFlag, reloadCmdConfigAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
if configFile == "" {
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
}
// get the address of the admin listener and craft endpoint URL
adminAddr := reloadCmdAddrFlag
if adminAddr == "" && len(config) > 0 {
var tmpStruct struct {
Admin caddy.AdminConfig `json:"admin"`
}
err = json.Unmarshal(config, &tmpStruct)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("unmarshaling admin listener address from config: %v", err)
}
adminAddr = tmpStruct.Admin.Listen
}
if adminAddr == "" {
adminAddr = caddy.DefaultAdminListen
}
loadEndpoint := fmt.Sprintf("http://%s/load", adminAddr)
// prepare the request to update the configuration
req, err := http.NewRequest(http.MethodPost, loadEndpoint, bytes.NewReader(config))
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("making request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Origin", adminAddr)
err = apiRequest(req)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err)
}
return caddy.ExitCodeSuccess, nil
}
func cmdVersion(_ Flags) (int, error) {
goModule := caddy.GoModule()
fmt.Print(goModule.Version)
if goModule.Sum != "" {
// a build with a known version will also have a checksum
fmt.Printf(" %s", goModule.Sum)
}
if goModule.Replace != nil {
fmt.Printf(" => %s", goModule.Replace.Path)
if goModule.Replace.Version != "" {
fmt.Printf(" %s", goModule.Replace.Version)
}
}
fmt.Println()
return caddy.ExitCodeSuccess, nil
}
func cmdBuildInfo(fl Flags) (int, error) {
bi, ok := debug.ReadBuildInfo()
if !ok {
return caddy.ExitCodeFailedStartup, fmt.Errorf("no build information")
}
fmt.Printf("path: %s\n", bi.Path)
fmt.Printf("main: %s %s %s\n", bi.Main.Path, bi.Main.Version, bi.Main.Sum)
fmt.Println("dependencies:")
for _, goMod := range bi.Deps {
fmt.Printf("%s %s %s", goMod.Path, goMod.Version, goMod.Sum)
if goMod.Replace != nil {
fmt.Printf(" => %s %s %s", goMod.Replace.Path, goMod.Replace.Version, goMod.Replace.Sum)
}
fmt.Println()
}
return caddy.ExitCodeSuccess, nil
}
func cmdListModules(fl Flags) (int, error) {
versions := fl.Bool("versions")
bi, ok := debug.ReadBuildInfo()
if !ok || !versions {
// if there's no build information,
// just print out the modules
for _, m := range caddy.Modules() {
fmt.Println(m)
}
return caddy.ExitCodeSuccess, nil
}
for _, modID := range caddy.Modules() {
modInfo, err := caddy.GetModule(modID)
if err != nil {
// that's weird
fmt.Println(modID)
continue
}
// to get the Caddy plugin's version info, we need to know
// the package that the Caddy module's value comes from; we
// can use reflection but we need a non-pointer value (I'm
// not sure why), and since New() should return a pointer
// value, we need to dereference it first
iface := interface{}(modInfo.New())
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
}
modPkgPath := reflect.TypeOf(iface).PkgPath()
// now we find the Go module that the Caddy module's package
// belongs to; we assume the Caddy module package path will
// be prefixed by its Go module path, and we will choose the
// longest matching prefix in case there are nested modules
var matched *debug.Module
for _, dep := range bi.Deps {
if strings.HasPrefix(modPkgPath, dep.Path) {
if matched == nil || len(dep.Path) > len(matched.Path) {
matched = dep
}
}
}
// if we could find no matching module, just print out
// the module ID instead
if matched == nil {
fmt.Println(modID)
continue
}
fmt.Printf("%s %s\n", modID, matched.Version)
}
return caddy.ExitCodeSuccess, nil
}
func cmdEnviron(_ Flags) (int, error) {
printEnvironment()
return caddy.ExitCodeSuccess, nil
}
func cmdAdaptConfig(fl Flags) (int, error) {
adaptCmdInputFlag := fl.String("config")
adaptCmdAdapterFlag := fl.String("adapter")
adaptCmdPrettyFlag := fl.Bool("pretty")
adaptCmdValidateFlag := fl.Bool("validate")
// if no input file was specified, try a default
// Caddyfile if the Caddyfile adapter is plugged in
if adaptCmdInputFlag == "" && caddyconfig.GetAdapter("caddyfile") != nil {
_, err := os.Stat("Caddyfile")
if err == nil {
// default Caddyfile exists
adaptCmdInputFlag = "Caddyfile"
caddy.Log().Info("using adjacent Caddyfile")
} else if !os.IsNotExist(err) {
// default Caddyfile exists, but error accessing it
return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing default Caddyfile: %v", err)
}
}
if adaptCmdInputFlag == "" {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
}
if adaptCmdAdapterFlag == "" {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)")
}
cfgAdapter := caddyconfig.GetAdapter(adaptCmdAdapterFlag)
if cfgAdapter == nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("unrecognized config adapter: %s", adaptCmdAdapterFlag)
}
input, err := ioutil.ReadFile(adaptCmdInputFlag)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading input file: %v", err)
}
opts := make(map[string]interface{})
if adaptCmdPrettyFlag {
opts["pretty"] = "true"
}
opts["filename"] = adaptCmdInputFlag
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// print warnings to stderr
for _, warn := range warnings {
msg := warn.Message
if warn.Directive != "" {
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
}
fmt.Fprintf(os.Stderr, "[WARNING][%s] %s:%d: %s\n", adaptCmdAdapterFlag, warn.File, warn.Line, msg)
}
// print result to stdout
fmt.Println(string(adaptedConfig))
// validate output if requested
if adaptCmdValidateFlag {
var cfg *caddy.Config
err = json.Unmarshal(adaptedConfig, &cfg)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err)
}
err = caddy.Validate(cfg)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("validation: %v", err)
}
}
return caddy.ExitCodeSuccess, nil
}
func cmdValidateConfig(fl Flags) (int, error) {
validateCmdConfigFlag := fl.String("config")
validateCmdAdapterFlag := fl.String("adapter")
input, _, err := loadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
input = caddy.RemoveMetaFields(input)
var cfg *caddy.Config
err = json.Unmarshal(input, &cfg)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err)
}
err = caddy.Validate(cfg)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
fmt.Println("Valid configuration")
return caddy.ExitCodeSuccess, nil
}
func cmdFmt(fl Flags) (int, error) {
formatCmdConfigFile := fl.Arg(0)
if formatCmdConfigFile == "" {
formatCmdConfigFile = "Caddyfile"
}
overwrite := fl.Bool("overwrite")
input, err := ioutil.ReadFile(formatCmdConfigFile)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading input file: %v", err)
}
output := caddyfile.Format(input)
if overwrite {
err = ioutil.WriteFile(formatCmdConfigFile, output, 0644)
if err != nil {
return caddy.ExitCodeFailedStartup, nil
}
} else {
fmt.Print(string(output))
}
return caddy.ExitCodeSuccess, nil
}
func cmdHelp(fl Flags) (int, error) {
const fullDocs = `Full documentation is available at:
https://caddyserver.com/docs/command-line`
args := fl.Args()
if len(args) == 0 {
s := `Caddy is an extensible server platform.
usage:
caddy <command> [<args...>]
commands:
`
keys := make([]string, 0, len(commands))
for k := range commands {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
cmd := commands[k]
short := strings.TrimSuffix(cmd.Short, ".")
s += fmt.Sprintf(" %-15s %s\n", cmd.Name, short)
}
s += "\nUse 'caddy help <command>' for more information about a command.\n"
s += "\n" + fullDocs + "\n"
fmt.Print(s)
return caddy.ExitCodeSuccess, nil
} else if len(args) > 1 {
return caddy.ExitCodeFailedStartup, fmt.Errorf("can only give help with one command")
}
subcommand, ok := commands[args[0]]
if !ok {
return caddy.ExitCodeFailedStartup, fmt.Errorf("unknown command: %s", args[0])
}
helpText := strings.TrimSpace(subcommand.Long)
if helpText == "" {
helpText = subcommand.Short
if !strings.HasSuffix(helpText, ".") {
helpText += "."
}
}
result := fmt.Sprintf("%s\n\nusage:\n caddy %s %s\n",
helpText,
subcommand.Name,
strings.TrimSpace(subcommand.Usage),
)
if help := flagHelp(subcommand.Flags); help != "" {
result += fmt.Sprintf("\nflags:\n%s", help)
}
result += "\n" + fullDocs + "\n"
fmt.Print(result)
return caddy.ExitCodeSuccess, nil
}
func apiRequest(req *http.Request) error {
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("performing request: %v", err)
}
defer resp.Body.Close()
// if it didn't work, let the user know
if resp.StatusCode >= 400 {
respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*10))
if err != nil {
return fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
}
return fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
}
return nil
}
+304
View File
@@ -0,0 +1,304 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddycmd
import (
"flag"
"regexp"
)
// Command represents a subcommand. Name, Func,
// and Short are required.
type Command struct {
// The name of the subcommand. Must conform to the
// format described by the RegisterCommand() godoc.
// Required.
Name string
// Run is a function that executes a subcommand using
// the parsed flags. It returns an exit code and any
// associated error.
// Required.
Func CommandFunc
// Usage is a brief message describing the syntax of
// the subcommand's flags and args. Use [] to indicate
// optional parameters and <> to enclose literal values
// intended to be replaced by the user. Do not prefix
// the string with "caddy" or the name of the command
// since these will be prepended for you; only include
// the actual parameters for this command.
Usage string
// Short is a one-line message explaining what the
// command does. Should not end with punctuation.
// Required.
Short string
// Long is the full help text shown to the user.
// Will be trimmed of whitespace on both ends before
// being printed.
Long string
// Flags is the flagset for command.
Flags *flag.FlagSet
}
// CommandFunc is a command's function. It runs the
// command and returns the proper exit code along with
// any error that occurred.
type CommandFunc func(Flags) (int, error)
var commands = make(map[string]Command)
func init() {
RegisterCommand(Command{
Name: "help",
Func: cmdHelp,
Usage: "<command>",
Short: "Shows help for a Caddy subcommand",
})
RegisterCommand(Command{
Name: "start",
Func: cmdStart,
Usage: "[--config <path> [--adapter <name>]] [--watch]",
Short: "Starts the Caddy process in the background and then returns",
Long: `
Starts the Caddy process, optionally bootstrapped with an initial config file.
This command unblocks after the server starts running or fails to run.
On Windows, the spawned child process will remain attached to the terminal, so
closing the window will forcefully stop Caddy; to avoid forgetting this, try
using 'caddy run' instead to keep it in the foreground.`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("start", flag.ExitOnError)
fs.String("config", "", "Configuration file")
fs.String("adapter", "", "Name of config adapter to apply")
fs.Bool("watch", false, "Reload changed config file automatically")
return fs
}(),
})
RegisterCommand(Command{
Name: "run",
Func: cmdRun,
Usage: "[--config <path> [--adapter <name>]] [--environ] [--watch]",
Short: `Starts the Caddy process and blocks indefinitely`,
Long: `
Starts the Caddy process, optionally bootstrapped with an initial config file,
and blocks indefinitely until the server is stopped; i.e. runs Caddy in
"daemon" mode (foreground).
If a config file is specified, it will be applied immediately after the process
is running. If the config file is not in Caddy's native JSON format, you can
specify an adapter with --adapter to adapt the given config file to
Caddy's native format. The config adapter must be a registered module. Any
warnings will be printed to the log, but beware that any adaptation without
errors will immediately be used. If you want to review the results of the
adaptation first, use the 'adapt' subcommand.
As a special case, if the current working directory has a file called
"Caddyfile" and the caddyfile config adapter is plugged in (default), then
that file will be loaded and used to configure Caddy, even without any command
line flags.
If --environ is specified, the environment as seen by the Caddy process will
be printed before starting. This is the same as the environ command but does
not quit after printing, and can be useful for troubleshooting.
The --resume flag will override the --config flag if there is a config auto-
save file. It is not an error if --resume is used and no autosave file exists.
If --watch is specified, the config file will be loaded automatically after
changes. ⚠️ This is dangerous in production! Only use this option in a local
development environment.`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("run", flag.ExitOnError)
fs.String("config", "", "Configuration file")
fs.String("adapter", "", "Name of config adapter to apply")
fs.Bool("environ", false, "Print environment")
fs.Bool("resume", false, "Use saved config, if any (and prefer over --config file)")
fs.Bool("watch", false, "Watch config file for changes and reload it automatically")
fs.String("pingback", "", "Echo confirmation bytes to this address on success")
return fs
}(),
})
RegisterCommand(Command{
Name: "stop",
Func: cmdStop,
Short: "Gracefully stops a started Caddy process",
Long: `
Stops the background Caddy process as gracefully as possible.
It requires that the admin API is enabled and accessible, since it will
use the API's /stop endpoint. The address of this request can be
customized using the --address flag if it is not the default.`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("stop", flag.ExitOnError)
fs.String("address", "", "The address to use to reach the admin API endpoint, if not the default")
return fs
}(),
})
RegisterCommand(Command{
Name: "reload",
Func: cmdReload,
Usage: "--config <path> [--adapter <name>] [--address <interface>]",
Short: "Changes the config of the running Caddy instance",
Long: `
Gives the running Caddy instance a new configuration. This has the same effect
as POSTing a document to the /load API endpoint, but is convenient for simple
workflows revolving around config files.
Since the admin endpoint is configurable, the endpoint configuration is loaded
from the --address flag if specified; otherwise it is loaded from the given
config file; otherwise the default is assumed.`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("reload", flag.ExitOnError)
fs.String("config", "", "Configuration file (required)")
fs.String("adapter", "", "Name of config adapter to apply")
fs.String("address", "", "Address of the administration listener, if different from config")
return fs
}(),
})
RegisterCommand(Command{
Name: "version",
Func: cmdVersion,
Short: "Prints the version",
})
RegisterCommand(Command{
Name: "list-modules",
Func: cmdListModules,
Usage: "[--versions]",
Short: "Lists the installed Caddy modules",
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("list-modules", flag.ExitOnError)
fs.Bool("versions", false, "Print version information")
return fs
}(),
})
RegisterCommand(Command{
Name: "build-info",
Func: cmdBuildInfo,
Short: "Prints information about this build",
})
RegisterCommand(Command{
Name: "environ",
Func: cmdEnviron,
Short: "Prints the environment",
})
RegisterCommand(Command{
Name: "adapt",
Func: cmdAdaptConfig,
Usage: "--config <path> [--adapter <name>] [--pretty] [--validate]",
Short: "Adapts a configuration to Caddy's native JSON",
Long: `
Adapts a configuration to Caddy's native JSON format and writes the
output to stdout, along with any warnings to stderr.
If --pretty is specified, the output will be formatted with indentation
for human readability.
If --validate is used, the adapted config will be checked for validity.
If the config is invalid, an error will be printed to stderr and a non-
zero exit status will be returned.`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("adapt", flag.ExitOnError)
fs.String("config", "", "Configuration file to adapt (required)")
fs.String("adapter", "caddyfile", "Name of config adapter")
fs.Bool("pretty", false, "Format the output for human readability")
fs.Bool("validate", false, "Validate the output")
return fs
}(),
})
RegisterCommand(Command{
Name: "validate",
Func: cmdValidateConfig,
Usage: "--config <path> [--adapter <name>]",
Short: "Tests whether a configuration file is valid",
Long: `
Loads and provisions the provided config, but does not start running it.
This reveals any errors with the configuration through the loading and
provisioning stages.`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("load", flag.ExitOnError)
fs.String("config", "", "Input configuration file")
fs.String("adapter", "", "Name of config adapter")
return fs
}(),
})
RegisterCommand(Command{
Name: "fmt",
Func: cmdFmt,
Usage: "[--overwrite] [<path>]",
Short: "Formats a Caddyfile",
Long: `
Formats the Caddyfile by adding proper indentation and spaces to improve
human readability. It prints the result to stdout.
If --write is specified, the output will be written to the config file
directly instead of printing it.`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("format", flag.ExitOnError)
fs.Bool("overwrite", false, "Overwrite the input file with the results")
return fs
}(),
})
}
// RegisterCommand registers the command cmd.
// cmd.Name must be unique and conform to the
// following format:
//
// - lowercase
// - alphanumeric and hyphen characters only
// - cannot start or end with a hyphen
// - hyphen cannot be adjacent to another hyphen
//
// This function panics if the name is already registered,
// if the name does not meet the described format, or if
// any of the fields are missing from cmd.
//
// This function should be used in init().
func RegisterCommand(cmd Command) {
if cmd.Name == "" {
panic("command name is required")
}
if cmd.Func == nil {
panic("command function missing")
}
if cmd.Short == "" {
panic("command short string is required")
}
if _, exists := commands[cmd.Name]; exists {
panic("command already registered: " + cmd.Name)
}
if !commandNameRegex.MatchString(cmd.Name) {
panic("invalid command name")
}
commands[cmd.Name] = cmd
}
var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)
+421
View File
@@ -0,0 +1,421 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddycmd
import (
"bytes"
"flag"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/certmagic"
"go.uber.org/zap"
)
func init() {
// set a fitting User-Agent for ACME requests
goModule := caddy.GoModule()
cleanModVersion := strings.TrimPrefix(goModule.Version, "v")
certmagic.UserAgent = "Caddy/" + cleanModVersion
// by using Caddy, user indicates agreement to CA terms
// (very important, or ACME account creation will fail!)
certmagic.DefaultACME.Agreed = true
}
// Main implements the main function of the caddy command.
// Call this if Caddy is to be the main() if your program.
func Main() {
caddy.TrapSignals()
switch len(os.Args) {
case 0:
fmt.Printf("[FATAL] no arguments provided by OS; args[0] must be command\n")
os.Exit(caddy.ExitCodeFailedStartup)
case 1:
os.Args = append(os.Args, "help")
}
subcommandName := os.Args[1]
subcommand, ok := commands[subcommandName]
if !ok {
if strings.HasPrefix(os.Args[1], "-") {
// user probably forgot to type the subcommand
fmt.Println("[ERROR] first argument must be a subcommand; see 'caddy help'")
} else {
fmt.Printf("[ERROR] '%s' is not a recognized subcommand; see 'caddy help'\n", os.Args[1])
}
os.Exit(caddy.ExitCodeFailedStartup)
}
fs := subcommand.Flags
if fs == nil {
fs = flag.NewFlagSet(subcommand.Name, flag.ExitOnError)
}
err := fs.Parse(os.Args[2:])
if err != nil {
fmt.Println(err)
os.Exit(caddy.ExitCodeFailedStartup)
}
exitCode, err := subcommand.Func(Flags{fs})
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", subcommand.Name, err)
}
os.Exit(exitCode)
}
// handlePingbackConn reads from conn and ensures it matches
// the bytes in expect, or returns an error if it doesn't.
func handlePingbackConn(conn net.Conn, expect []byte) error {
defer conn.Close()
confirmationBytes, err := ioutil.ReadAll(io.LimitReader(conn, 32))
if err != nil {
return err
}
if !bytes.Equal(confirmationBytes, expect) {
return fmt.Errorf("wrong confirmation: %x", confirmationBytes)
}
return nil
}
// loadConfig loads the config from configFile and adapts it
// using adapterName. If adapterName is specified, configFile
// must be also. If no configFile is specified, it tries
// loading a default config file. The lack of a config file is
// not treated as an error, but false will be returned if
// there is no config available. It prints any warnings to stderr,
// and returns the resulting JSON config bytes along with
// whether a config file was loaded or not.
func loadConfig(configFile, adapterName string) ([]byte, string, error) {
// specifying an adapter without a config file is ambiguous
if adapterName != "" && configFile == "" {
return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)")
}
// load initial config and adapter
var config []byte
var cfgAdapter caddyconfig.Adapter
var err error
if configFile != "" {
config, err = ioutil.ReadFile(configFile)
if err != nil {
return nil, "", fmt.Errorf("reading config file: %v", err)
}
caddy.Log().Info("using provided configuration",
zap.String("config_file", configFile),
zap.String("config_adapter", adapterName))
} else if adapterName == "" {
// as a special case when no config file or adapter
// is specified, see if the Caddyfile adapter is
// plugged in, and if so, try using a default Caddyfile
cfgAdapter = caddyconfig.GetAdapter("caddyfile")
if cfgAdapter != nil {
config, err = ioutil.ReadFile("Caddyfile")
if os.IsNotExist(err) {
// okay, no default Caddyfile; pretend like this never happened
cfgAdapter = nil
} else if err != nil {
// default Caddyfile exists, but error reading it
return nil, "", fmt.Errorf("reading default Caddyfile: %v", err)
} else {
// success reading default Caddyfile
configFile = "Caddyfile"
caddy.Log().Info("using adjacent Caddyfile")
}
}
}
// as a special case, if a config file called "Caddyfile" was
// specified, and no adapter is specified, assume caddyfile adapter
// for convenience
if strings.HasPrefix(filepath.Base(configFile), "Caddyfile") &&
filepath.Ext(configFile) != ".json" &&
adapterName == "" {
adapterName = "caddyfile"
}
// load config adapter
if adapterName != "" {
cfgAdapter = caddyconfig.GetAdapter(adapterName)
if cfgAdapter == nil {
return nil, "", fmt.Errorf("unrecognized config adapter: %s", adapterName)
}
}
// adapt config
if cfgAdapter != nil {
adaptedConfig, warnings, err := cfgAdapter.Adapt(config, map[string]interface{}{
"filename": configFile,
})
if err != nil {
return nil, "", fmt.Errorf("adapting config using %s: %v", adapterName, err)
}
for _, warn := range warnings {
msg := warn.Message
if warn.Directive != "" {
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
}
fmt.Printf("[WARNING][%s] %s:%d: %s\n", adapterName, warn.File, warn.Line, msg)
}
config = adaptedConfig
}
return config, configFile, nil
}
// watchConfigFile watches the config file at filename for changes
// and reloads the config if the file was updated. This function
// blocks indefinitely; it only quits if the poller has errors for
// long enough time. The filename passed in must be the actual
// config file used, not one to be discovered.
func watchConfigFile(filename, adapterName string) {
// make our logger; since config reloads can change the
// default logger, we need to get it dynamically each time
logger := func() *zap.Logger {
return caddy.Log().
Named("watcher").
With(zap.String("config_file", filename))
}
// get the initial timestamp on the config file
info, err := os.Stat(filename)
if err != nil {
logger().Error("cannot watch config file", zap.Error(err))
return
}
lastModified := info.ModTime()
logger().Info("watching config file for changes")
// if the file disappears or something, we can
// stop polling if the error lasts long enough
var lastErr time.Time
finalError := func(err error) bool {
if lastErr.IsZero() {
lastErr = time.Now()
return false
}
if time.Since(lastErr) > 30*time.Second {
logger().Error("giving up watching config file; too many errors",
zap.Error(err))
return true
}
return false
}
// begin poller
for range time.Tick(1 * time.Second) {
// get the file info
info, err := os.Stat(filename)
if err != nil {
if finalError(err) {
return
}
continue
}
lastErr = time.Time{} // no error, so clear any memory of one
// if it hasn't changed, nothing to do
if !info.ModTime().After(lastModified) {
continue
}
logger().Info("config file changed; reloading")
// remember this timestamp
lastModified = info.ModTime()
// load the contents of the file
config, _, err := loadConfig(filename, adapterName)
if err != nil {
logger().Error("unable to load latest config", zap.Error(err))
continue
}
// apply the updated config
err = caddy.Load(config, false)
if err != nil {
logger().Error("applying latest config", zap.Error(err))
continue
}
}
}
// Flags wraps a FlagSet so that typed values
// from flags can be easily retrieved.
type Flags struct {
*flag.FlagSet
}
// String returns the string representation of the
// flag given by name. It panics if the flag is not
// in the flag set.
func (f Flags) String(name string) string {
return f.FlagSet.Lookup(name).Value.String()
}
// Bool returns the boolean representation of the
// flag given by name. It returns false if the flag
// is not a boolean type. It panics if the flag is
// not in the flag set.
func (f Flags) Bool(name string) bool {
val, _ := strconv.ParseBool(f.String(name))
return val
}
// Int returns the integer representation of the
// flag given by name. It returns 0 if the flag
// is not an integer type. It panics if the flag is
// not in the flag set.
func (f Flags) Int(name string) int {
val, _ := strconv.ParseInt(f.String(name), 0, strconv.IntSize)
return int(val)
}
// Float64 returns the float64 representation of the
// flag given by name. It returns false if the flag
// is not a float63 type. It panics if the flag is
// not in the flag set.
func (f Flags) Float64(name string) float64 {
val, _ := strconv.ParseFloat(f.String(name), 64)
return val
}
// Duration returns the duration representation of the
// flag given by name. It returns false if the flag
// is not a duration type. It panics if the flag is
// not in the flag set.
func (f Flags) Duration(name string) time.Duration {
val, _ := time.ParseDuration(f.String(name))
return val
}
// flagHelp returns the help text for fs.
func flagHelp(fs *flag.FlagSet) string {
if fs == nil {
return ""
}
// temporarily redirect output
out := fs.Output()
defer fs.SetOutput(out)
buf := new(bytes.Buffer)
fs.SetOutput(buf)
fs.PrintDefaults()
return buf.String()
}
func printEnvironment() {
fmt.Printf("caddy.HomeDir=%s\n", caddy.HomeDir())
fmt.Printf("caddy.AppDataDir=%s\n", caddy.AppDataDir())
fmt.Printf("caddy.AppConfigDir=%s\n", caddy.AppConfigDir())
fmt.Printf("caddy.ConfigAutosavePath=%s\n", caddy.ConfigAutosavePath)
fmt.Printf("runtime.GOOS=%s\n", runtime.GOOS)
fmt.Printf("runtime.GOARCH=%s\n", runtime.GOARCH)
fmt.Printf("runtime.Compiler=%s\n", runtime.Compiler)
fmt.Printf("runtime.NumCPU=%d\n", runtime.NumCPU())
fmt.Printf("runtime.GOMAXPROCS=%d\n", runtime.GOMAXPROCS(0))
fmt.Printf("runtime.Version=%s\n", runtime.Version())
cwd, err := os.Getwd()
if err != nil {
cwd = fmt.Sprintf("<error: %v>", err)
}
fmt.Printf("os.Getwd=%s\n\n", cwd)
for _, v := range os.Environ() {
fmt.Println(v)
}
}
// moveStorage moves the old default dataDir to the new default dataDir.
// TODO: This is TEMPORARY until the release candidates.
func moveStorage() {
// get the home directory (the old way)
oldHome := os.Getenv("HOME")
if oldHome == "" && runtime.GOOS == "windows" {
drive := os.Getenv("HOMEDRIVE")
path := os.Getenv("HOMEPATH")
oldHome = drive + path
if drive == "" || path == "" {
oldHome = os.Getenv("USERPROFILE")
}
}
if oldHome == "" {
oldHome = "."
}
oldDataDir := filepath.Join(oldHome, ".local", "share", "caddy")
// nothing to do if old data dir doesn't exist
_, err := os.Stat(oldDataDir)
if os.IsNotExist(err) {
return
}
// nothing to do if the new data dir is the same as the old one
newDataDir := caddy.AppDataDir()
if oldDataDir == newDataDir {
return
}
logger := caddy.Log().Named("automigrate").With(
zap.String("old_dir", oldDataDir),
zap.String("new_dir", newDataDir))
logger.Info("beginning one-time data directory migration",
zap.String("details", "https://github.com/caddyserver/caddy/issues/2955"))
// if new data directory exists, avoid auto-migration as a conservative safety measure
_, err = os.Stat(newDataDir)
if !os.IsNotExist(err) {
logger.Error("new data directory already exists; skipping auto-migration as conservative safety measure",
zap.Error(err),
zap.String("instructions", "https://github.com/caddyserver/caddy/issues/2955#issuecomment-570000333"))
return
}
// construct the new data directory's parent folder
err = os.MkdirAll(filepath.Dir(newDataDir), 0700)
if err != nil {
logger.Error("unable to make new datadirectory - follow link for instructions",
zap.String("instructions", "https://github.com/caddyserver/caddy/issues/2955#issuecomment-570000333"),
zap.Error(err))
return
}
// folder structure is same, so just try to rename (move) it;
// this fails if the new path is on a separate device
err = os.Rename(oldDataDir, newDataDir)
if err != nil {
logger.Error("new data directory already exists; skipping auto-migration as conservative safety measure - follow link for instructions",
zap.String("instructions", "https://github.com/caddyserver/caddy/issues/2955#issuecomment-570000333"),
zap.Error(err))
}
logger.Info("successfully completed one-time migration of data directory",
zap.String("details", "https://github.com/caddyserver/caddy/issues/2955"))
}
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build !windows
package caddycmd
import (
"fmt"
"os"
"path/filepath"
"syscall"
)
func gracefullyStopProcess(pid int) error {
fmt.Print("Graceful stop... ")
err := syscall.Kill(pid, syscall.SIGINT)
if err != nil {
return fmt.Errorf("kill: %v", err)
}
return nil
}
func getProcessName() string {
return filepath.Base(os.Args[0])
}
+44
View File
@@ -0,0 +1,44 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddycmd
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
)
func gracefullyStopProcess(pid int) error {
fmt.Print("Forceful stop... ")
// process on windows will not stop unless forced with /f
cmd := exec.Command("taskkill", "/pid", strconv.Itoa(pid), "/f")
if err := cmd.Run(); err != nil {
return fmt.Errorf("taskkill: %v", err)
}
return nil
}
// On Windows the app name passed in os.Args[0] will match how
// caddy was started eg will match caddy or caddy.exe.
// So return appname with .exe for consistency
func getProcessName() string {
base := filepath.Base(os.Args[0])
if filepath.Ext(base) == "" {
return base + ".exe"
}
return base
}
-225
View File
@@ -1,225 +0,0 @@
package config
import (
"fmt"
"io"
"log"
"net"
"github.com/mholt/caddy/app"
"github.com/mholt/caddy/config/parse"
"github.com/mholt/caddy/config/setup"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/server"
)
const (
DefaultHost = "0.0.0.0"
DefaultPort = "2015"
DefaultRoot = "."
// DefaultConfigFile is the name of the configuration file that is loaded
// by default if no other file is specified.
DefaultConfigFile = "Caddyfile"
)
func Load(filename string, input io.Reader) ([]server.Config, error) {
var configs []server.Config
// turn off timestamp for parsing
flags := log.Flags()
log.SetFlags(0)
serverBlocks, err := parse.ServerBlocks(filename, input)
if err != nil {
return configs, err
}
// Each server block represents a single server/address.
// Iterate each server block and make a config for each one,
// executing the directives that were parsed.
for _, sb := range serverBlocks {
config := server.Config{
Host: sb.Host,
Port: sb.Port,
Root: Root,
Middleware: make(map[string][]middleware.Middleware),
ConfigFile: filename,
AppName: app.Name,
AppVersion: app.Version,
}
// It is crucial that directives are executed in the proper order.
for _, dir := range directiveOrder {
// Execute directive if it is in the server block
if tokens, ok := sb.Tokens[dir.name]; ok {
// Each setup function gets a controller, which is the
// server config and the dispenser containing only
// this directive's tokens.
controller := &setup.Controller{
Config: &config,
Dispenser: parse.NewDispenserTokens(filename, tokens),
}
midware, err := dir.setup(controller)
if err != nil {
return configs, err
}
if midware != nil {
// TODO: For now, we only support the default path scope /
config.Middleware["/"] = append(config.Middleware["/"], midware)
}
}
}
if config.Port == "" {
config.Port = Port
}
configs = append(configs, config)
}
// restore logging settings
log.SetFlags(flags)
return configs, nil
}
// ArrangeBindings groups configurations by their bind address. For example,
// a server that should listen on localhost and another on 127.0.0.1 will
// be grouped into the same address: 127.0.0.1. It will return an error
// if an address is malformed or a TLS listener is configured on the
// same address as a plaintext HTTP listener. The return value is a map of
// bind address to list of configs that would become VirtualHosts on that
// server. Use the keys of the returned map to create listeners, and use
// the associated values to set up the virtualhosts.
func ArrangeBindings(allConfigs []server.Config) (map[*net.TCPAddr][]server.Config, error) {
addresses := make(map[*net.TCPAddr][]server.Config)
// Group configs by bind address
for _, conf := range allConfigs {
newAddr, warnErr, fatalErr := resolveAddr(conf)
if fatalErr != nil {
return addresses, fatalErr
}
if warnErr != nil {
log.Println("[Warning]", warnErr)
}
// Make sure to compare the string representation of the address,
// not the pointer, since a new *TCPAddr is created each time.
var existing bool
for addr := range addresses {
if addr.String() == newAddr.String() {
addresses[addr] = append(addresses[addr], conf)
existing = true
break
}
}
if !existing {
addresses[newAddr] = append(addresses[newAddr], conf)
}
}
// Don't allow HTTP and HTTPS to be served on the same address
for _, configs := range addresses {
isTLS := configs[0].TLS.Enabled
for _, config := range configs {
if config.TLS.Enabled != isTLS {
thisConfigProto, otherConfigProto := "HTTP", "HTTP"
if config.TLS.Enabled {
thisConfigProto = "HTTPS"
}
if configs[0].TLS.Enabled {
otherConfigProto = "HTTPS"
}
return addresses, fmt.Errorf("configuration error: Cannot multiplex %s (%s) and %s (%s) on same address",
configs[0].Address(), otherConfigProto, config.Address(), thisConfigProto)
}
}
}
return addresses, nil
}
// resolveAddr determines the address (host and port) that a config will
// bind to. The returned address, resolvAddr, should be used to bind the
// listener or group the config with other configs using the same address.
// The first error, if not nil, is just a warning and should be reported
// but execution may continue. The second error, if not nil, is a real
// problem and the server should not be started.
//
// This function handles edge cases gracefully. If a port name like
// "http" or "https" is unknown to the system, this function will
// change them to 80 or 443 respectively. If a hostname fails to
// resolve, that host can still be served but will be listening on
// the wildcard host instead. This function takes care of this for you.
func resolveAddr(conf server.Config) (resolvAddr *net.TCPAddr, warnErr error, fatalErr error) {
// The host to bind to may be different from the (virtual)host to serve
bindHost := conf.BindHost
if bindHost == "" {
bindHost = conf.Host
}
resolvAddr, warnErr = net.ResolveTCPAddr("tcp", net.JoinHostPort(bindHost, conf.Port))
if warnErr != nil {
// Most likely the host lookup failed or the port is unknown
tryPort := conf.Port
switch errVal := warnErr.(type) {
case *net.AddrError:
if errVal.Err == "unknown port" {
// some odd Linux machines don't support these port names; see issue #136
switch conf.Port {
case "http":
tryPort = "80"
case "https":
tryPort = "443"
}
}
resolvAddr, fatalErr = net.ResolveTCPAddr("tcp", net.JoinHostPort(bindHost, tryPort))
if fatalErr != nil {
return
}
default:
// the hostname probably couldn't be resolved, just bind to wildcard then
resolvAddr, fatalErr = net.ResolveTCPAddr("tcp", net.JoinHostPort("0.0.0.0", tryPort))
if fatalErr != nil {
return
}
}
return
}
return
}
// validDirective returns true if d is a valid
// directive; false otherwise.
func validDirective(d string) bool {
for _, dir := range directiveOrder {
if dir.name == d {
return true
}
}
return false
}
// Default makes a default configuration which
// is empty except for root, host, and port,
// which are essentials for serving the cwd.
func Default() server.Config {
return server.Config{
Root: Root,
Host: Host,
Port: Port,
}
}
// These three defaults are configurable through the command line
var (
Root = DefaultRoot
Host = DefaultHost
Port = DefaultPort
)
-62
View File
@@ -1,62 +0,0 @@
package config
import (
"testing"
"github.com/mholt/caddy/server"
)
func TestReolveAddr(t *testing.T) {
// NOTE: If tests fail due to comparing to string "127.0.0.1",
// it's possible that system env resolves with IPv6, or ::1.
// If that happens, maybe we should use actualAddr.IP.IsLoopback()
// for the assertion, rather than a direct string comparison.
// NOTE: Tests with {Host: "", Port: ""} and {Host: "localhost", Port: ""}
// will not behave the same cross-platform, so they have been omitted.
for i, test := range []struct {
config server.Config
shouldWarnErr bool
shouldFatalErr bool
expectedIP string
expectedPort int
}{
{server.Config{Host: "localhost", Port: "1234"}, false, false, "127.0.0.1", 1234},
{server.Config{Host: "127.0.0.1", Port: "1234"}, false, false, "127.0.0.1", 1234},
{server.Config{Host: "should-not-resolve", Port: "1234"}, true, false, "0.0.0.0", 1234},
{server.Config{Host: "localhost", Port: "http"}, false, false, "127.0.0.1", 80},
{server.Config{Host: "localhost", Port: "https"}, false, false, "127.0.0.1", 443},
{server.Config{Host: "", Port: "1234"}, false, false, "<nil>", 1234},
{server.Config{Host: "localhost", Port: "abcd"}, false, true, "", 0},
{server.Config{BindHost: "127.0.0.1", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234},
{server.Config{BindHost: "localhost", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234},
{server.Config{BindHost: "should-not-resolve", Host: "localhost", Port: "1234"}, true, false, "0.0.0.0", 1234},
} {
actualAddr, warnErr, fatalErr := resolveAddr(test.config)
if test.shouldFatalErr && fatalErr == nil {
t.Errorf("Test %d: Expected error, but there wasn't any", i)
}
if !test.shouldFatalErr && fatalErr != nil {
t.Errorf("Test %d: Expected no error, but there was one: %v", i, fatalErr)
}
if fatalErr != nil {
continue
}
if test.shouldWarnErr && warnErr == nil {
t.Errorf("Test %d: Expected warning, but there wasn't any", i)
}
if !test.shouldWarnErr && warnErr != nil {
t.Errorf("Test %d: Expected no warning, but there was one: %v", i, warnErr)
}
if actual, expected := actualAddr.IP.String(), test.expectedIP; actual != expected {
t.Errorf("Test %d: IP was %s but expected %s", i, actual, expected)
}
if actual, expected := actualAddr.Port, test.expectedPort; actual != expected {
t.Errorf("Test %d: Port was %d but expected %d", i, actual, expected)
}
}
}
-79
View File
@@ -1,79 +0,0 @@
package config
import (
"github.com/mholt/caddy/config/parse"
"github.com/mholt/caddy/config/setup"
"github.com/mholt/caddy/middleware"
)
func init() {
// The parse package must know which directives
// are valid, but it must not import the setup
// or config package. To solve this problem, we
// fill up this map in our init function here.
// The parse package does not need to know the
// ordering of the directives.
for _, dir := range directiveOrder {
parse.ValidDirectives[dir.name] = struct{}{}
}
}
// Directives are registered in the order they should be
// executed. Middleware (directives that inject a handler)
// are executed in the order A-B-C-*-C-B-A, assuming
// they all call the Next handler in the chain.
//
// Ordering is VERY important. Every middleware will
// feel the effects of all other middleware below
// (after) them during a request, but they must not
// care what middleware above them are doing.
//
// For example, log needs to know the status code and
// exactly how many bytes were written to the client,
// which every other middleware can affect, so it gets
// registered first. The errors middleware does not
// care if gzip or log modifies its response, so it
// gets registered below them. Gzip, on the other hand,
// DOES care what errors does to the response since it
// must compress every output to the client, even error
// pages, so it must be registered before the errors
// middleware and any others that would write to the
// response.
var directiveOrder = []directive{
// Essential directives that initialize vital configuration settings
{"root", setup.Root},
{"tls", setup.TLS},
{"bind", setup.BindHost},
// Other directives that don't create HTTP handlers
{"startup", setup.Startup},
{"shutdown", setup.Shutdown},
// Directives that inject handlers (middleware)
{"log", setup.Log},
{"gzip", setup.Gzip},
{"errors", setup.Errors},
{"header", setup.Headers},
{"rewrite", setup.Rewrite},
{"redir", setup.Redir},
{"ext", setup.Ext},
{"basicauth", setup.BasicAuth},
{"internal", setup.Internal},
{"proxy", setup.Proxy},
{"fastcgi", setup.FastCGI},
{"websocket", setup.WebSocket},
{"markdown", setup.Markdown},
{"templates", setup.Templates},
{"browse", setup.Browse},
}
// directive ties together a directive name with its setup function.
type directive struct {
name string
setup SetupFunc
}
// A setup function takes a setup controller. Its return values may
// both be nil. If middleware is not nil, it will be chained into
// the HTTP handlers in the order specified in this package.
type SetupFunc func(c *setup.Controller) (middleware.Middleware, error)
-245
View File
@@ -1,245 +0,0 @@
package parse
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 {
return Dispenser{
filename: filename,
tokens: allTokens(input),
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 EOF error, meaning that end of input
// was found when another token was expected.
func (d *Dispenser) EofErr() error {
return d.Errf("Unexpected EOF")
}
// Err generates a custom parse error with a message of msg.
func (d *Dispenser) Err(msg string) error {
msg = fmt.Sprintf("%s:%d - Parse error: %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
}
-165
View File
@@ -1,165 +0,0 @@
package parse
import (
"strings"
"testing"
)
type lexerTestCase struct {
input string
expected []token
}
func TestLexer(t *testing.T) {
testCases := []lexerTestCase{
{
input: `host:123`,
expected: []token{
{line: 1, text: "host:123"},
},
},
{
input: `host:123
directive`,
expected: []token{
{line: 1, text: "host:123"},
{line: 3, text: "directive"},
},
},
{
input: `host:123 {
directive
}`,
expected: []token{
{line: 1, text: "host:123"},
{line: 1, text: "{"},
{line: 2, text: "directive"},
{line: 3, text: "}"},
},
},
{
input: `host:123 { directive }`,
expected: []token{
{line: 1, text: "host:123"},
{line: 1, text: "{"},
{line: 1, text: "directive"},
{line: 1, text: "}"},
},
},
{
input: `host:123 {
#comment
directive
# comment
foobar # another comment
}`,
expected: []token{
{line: 1, text: "host:123"},
{line: 1, text: "{"},
{line: 3, text: "directive"},
{line: 5, text: "foobar"},
{line: 6, text: "}"},
},
},
{
input: `a "quoted value" b
foobar`,
expected: []token{
{line: 1, text: "a"},
{line: 1, text: "quoted value"},
{line: 1, text: "b"},
{line: 2, text: "foobar"},
},
},
{
input: `A "quoted \"value\" inside" B`,
expected: []token{
{line: 1, text: "A"},
{line: 1, text: `quoted "value" inside`},
{line: 1, text: "B"},
},
},
{
input: `"don't\escape"`,
expected: []token{
{line: 1, text: `don't\escape`},
},
},
{
input: `"don't\\escape"`,
expected: []token{
{line: 1, text: `don't\\escape`},
},
},
{
input: `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"},
{line: 2, text: "{"},
{line: 3, text: "foobar"},
{line: 4, text: "}"},
},
},
{
input: `"C:\php\php-cgi.exe"`,
expected: []token{
{line: 1, text: `C:\php\php-cgi.exe`},
},
},
{
input: `empty "" string`,
expected: []token{
{line: 1, text: `empty`},
{line: 1, text: ``},
{line: 1, text: `string`},
},
},
{
input: "skip those\r\nCR characters",
expected: []token{
{line: 1, text: "skip"},
{line: 1, text: "those"},
{line: 2, text: "CR"},
{line: 2, text: "characters"},
},
},
}
for i, testCase := range testCases {
actual := tokenize(testCase.input)
lexerCompare(t, i, testCase.expected, actual)
}
}
func tokenize(input string) (tokens []token) {
l := lexer{}
l.load(strings.NewReader(input))
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))
}
for i := 0; i < len(actual) && i < len(expected); i++ {
if actual[i].line != expected[i].line {
t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d",
n, i, expected[i].text, expected[i].line, actual[i].line)
break
}
if actual[i].text != expected[i].text {
t.Errorf("Test case %d token %d: expected text '%s' but was '%s'",
n, i, expected[i].text, actual[i].text)
break
}
}
}
-29
View File
@@ -1,29 +0,0 @@
// Package parse provides facilities for parsing configuration files.
package parse
import "io"
// ServerBlocks parses the input just enough to organize tokens,
// in order, by server block. No further parsing is performed.
// Server blocks are returned in the order in which they appear.
func ServerBlocks(filename string, input io.Reader) ([]serverBlock, error) {
p := parser{Dispenser: NewDispenser(filename, input)}
blocks, err := p.parseAll()
return blocks, err
}
// 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) (tokens []token) {
l := new(lexer)
l.load(input)
for l.next() {
tokens = append(tokens, l.token)
}
return
}
// Set of directives that are valid (unordered). Populated
// by config package's init function.
var ValidDirectives = make(map[string]struct{})
-22
View File
@@ -1,22 +0,0 @@
package parse
import (
"strings"
"testing"
)
func TestAllTokens(t *testing.T) {
input := strings.NewReader("a b c\nd e")
expected := []string{"a", "b", "c", "d", "e"}
tokens := allTokens(input)
if len(tokens) != len(expected) {
t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens))
}
for i, val := range expected {
if tokens[i].text != val {
t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].text)
}
}
}
-325
View File
@@ -1,325 +0,0 @@
package parse
import (
"net"
"os"
"path/filepath"
"strings"
)
type parser struct {
Dispenser
block multiServerBlock // current server block being parsed
eof bool // if we encounter a valid EOF in a hard place
}
func (p *parser) parseAll() ([]serverBlock, error) {
var blocks []serverBlock
for p.Next() {
err := p.parseOne()
if err != nil {
return blocks, err
}
// explode the multiServerBlock into multiple serverBlocks
for _, addr := range p.block.addresses {
blocks = append(blocks, serverBlock{
Host: addr.host,
Port: addr.port,
Tokens: p.block.tokens,
})
}
}
return blocks, nil
}
func (p *parser) parseOne() error {
p.block = multiServerBlock{tokens: make(map[string][]token)}
err := p.begin()
if err != nil {
return err
}
return nil
}
func (p *parser) begin() error {
if len(p.tokens) == 0 {
return nil
}
err := p.addresses()
if err != nil {
return err
}
if p.eof {
// this happens if the Caddyfile consists of only
// a line of addresses and nothing else
return nil
}
err = p.blockContents()
if err != nil {
return err
}
return nil
}
func (p *parser) addresses() error {
var expectingAnother bool
for {
tkn := p.Val()
// special case: import directive replaces tokens during parse-time
if tkn == "import" && p.isNewLine() {
err := p.doImport()
if err != nil {
return err
}
continue
}
// Open brace definitely indicates end of addresses
if tkn == "{" {
if expectingAnother {
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
}
break
}
// Trailing comma indicates another address will follow, which
// may possibly be on the next line
if tkn[len(tkn)-1] == ',' {
tkn = tkn[:len(tkn)-1]
expectingAnother = true
} else {
expectingAnother = false // but we may still see another one on this line
}
// Parse and save this address
host, port, err := standardAddress(tkn)
if err != nil {
return err
}
p.block.addresses = append(p.block.addresses, address{host, port})
// Advance token and possibly break out of loop or return error
hasNext := p.Next()
if expectingAnother && !hasNext {
return p.EofErr()
}
if !hasNext {
p.eof = true
break // EOF
}
if !expectingAnother && p.isNewLine() {
break
}
}
return nil
}
func (p *parser) blockContents() error {
errOpenCurlyBrace := p.openCurlyBrace()
if errOpenCurlyBrace != nil {
// single-server configs don't need curly braces
p.cursor--
}
err := p.directives()
if err != nil {
return err
}
// Only look for close curly brace if there was an opening
if errOpenCurlyBrace == nil {
err = p.closeCurlyBrace()
if err != nil {
return err
}
}
return nil
}
// directives parses through all the lines for directives
// and it expects the next token to be the first
// directive. It goes until EOF or closing curly brace
// which ends the server block.
func (p *parser) directives() error {
for p.Next() {
// end of server block
if p.Val() == "}" {
break
}
// special case: import directive replaces tokens during parse-time
if p.Val() == "import" {
err := p.doImport()
if err != nil {
return err
}
p.cursor-- // cursor is advanced when we continue, so roll back one more
continue
}
// normal case: parse a directive on this line
if err := p.directive(); err != nil {
return err
}
}
return nil
}
// doImport swaps out the import directive and its argument
// (a total of 2 tokens) with the tokens in the file specified.
// When the function returns, the cursor is on the token before
// where the import directive was. In other words, call Next()
// to access the first token that was imported.
func (p *parser) doImport() error {
if !p.NextArg() {
return p.ArgErr()
}
importFile := p.Val()
if p.NextArg() {
return p.Err("Import allows only one file to import")
}
file, err := os.Open(importFile)
if err != nil {
return p.Errf("Could not import %s - %v", importFile, err)
}
defer file.Close()
importedTokens := allTokens(file)
// Tack the filename onto these tokens so any errors show the imported file's name
for i := 0; i < len(importedTokens); i++ {
importedTokens[i].file = filepath.Base(importFile)
}
// Splice out the import directive and its argument (2 tokens total)
// and insert the imported tokens in their place.
tokensBefore := p.tokens[:p.cursor-1]
tokensAfter := p.tokens[p.cursor+1:]
p.tokens = append(tokensBefore, append(importedTokens, tokensAfter...)...)
p.cursor-- // cursor was advanced one position to read the filename; rewind it
return nil
}
// directive collects tokens until the directive's scope
// closes (either end of line or end of curly brace block).
// It expects the currently-loaded token to be a directive
// (or } that ends a server block). The collected tokens
// are loaded into the current server block for later use
// by directive setup functions.
func (p *parser) directive() error {
dir := p.Val()
nesting := 0
if _, ok := ValidDirectives[dir]; !ok {
return p.Errf("Unknown directive '%s'", dir)
}
// The directive itself is appended as a relevant token
p.block.tokens[dir] = append(p.block.tokens[dir], p.tokens[p.cursor])
for p.Next() {
if p.Val() == "{" {
nesting++
} else if p.isNewLine() && nesting == 0 {
p.cursor-- // read too far
break
} else if p.Val() == "}" && nesting > 0 {
nesting--
} else if p.Val() == "}" && nesting == 0 {
return p.Err("Unexpected '}' because no matching opening brace")
}
p.block.tokens[dir] = append(p.block.tokens[dir], p.tokens[p.cursor])
}
if nesting > 0 {
return p.EofErr()
}
return nil
}
// openCurlyBrace expects the current token to be an
// opening curly brace. This acts like an assertion
// because it returns an error if the token is not
// a opening curly brace. It does NOT advance the token.
func (p *parser) openCurlyBrace() error {
if p.Val() != "{" {
return p.SyntaxErr("{")
}
return nil
}
// closeCurlyBrace expects the current token to be
// a closing curly brace. This acts like an assertion
// because it returns an error if the token is not
// a closing curly brace. It does NOT advance the token.
func (p *parser) closeCurlyBrace() error {
if p.Val() != "}" {
return p.SyntaxErr("}")
}
return nil
}
// standardAddress turns the accepted host and port patterns
// into a format accepted by net.Dial.
func standardAddress(str string) (host, port string, err error) {
var schemePort, splitPort string
if strings.HasPrefix(str, "https://") {
schemePort = "https"
str = str[8:]
} else if strings.HasPrefix(str, "http://") {
schemePort = "http"
str = str[7:]
}
host, splitPort, err = net.SplitHostPort(str)
if err != nil {
host, splitPort, err = net.SplitHostPort(str + ":") // tack on empty port
}
if err != nil {
// ¯\_(ツ)_/¯
host = str
}
if splitPort != "" {
port = splitPort
} else {
port = schemePort
}
return
}
type (
// serverBlock stores tokens by directive name for a
// single host:port (address)
serverBlock struct {
Host, Port string
Tokens map[string][]token // directive name to tokens (including directive)
}
// multiServerBlock is the same as serverBlock but for
// multiple addresses that share the same tokens
multiServerBlock struct {
addresses []address
tokens map[string][]token
}
address struct {
host, port string
}
)
-382
View File
@@ -1,382 +0,0 @@
package parse
import (
"reflect"
"strings"
"testing"
)
func TestStandardAddress(t *testing.T) {
for i, test := range []struct {
input string
host, port string
shouldErr bool
}{
{`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},
{`localhost:http`, "localhost", "http", false},
{`localhost:https`, "localhost", "https", false},
{`:http`, "", "http", false},
{`:https`, "", "https", false},
{`http://localhost`, "localhost", "http", false},
{`https://localhost`, "localhost", "https", false},
{`http://127.0.0.1`, "127.0.0.1", "http", false},
{`https://127.0.0.1`, "127.0.0.1", "https", false},
{`http://[::1]`, "::1", "http", false},
{`http://localhost:1234`, "localhost", "1234", false},
{`https://127.0.0.1:1234`, "127.0.0.1", "1234", false},
{`http://[::1]:1234`, "::1", "1234", false},
{``, "", "", false},
{`::1`, "::1", "", true},
{`localhost::`, "localhost::", "", true},
{`#$%@`, "#$%@", "", true},
} {
host, port, err := standardAddress(test.input)
if err != nil && !test.shouldErr {
t.Errorf("Test %d: Expected no error, but had error: %v", i, err)
}
if err == nil && test.shouldErr {
t.Errorf("Test %d: Expected error, but had none", i)
}
if host != test.host {
t.Errorf("Test %d: Expected host '%s', got '%s'", i, test.host, host)
}
if port != test.port {
t.Errorf("Test %d: Expected port '%s', got '%s'", i, test.port, port)
}
}
}
func TestParseOneAndImport(t *testing.T) {
setupParseTests()
testParseOne := func(input string) (multiServerBlock, error) {
p := testParser(input)
p.Next()
err := p.parseOne()
return p.block, err
}
for i, test := range []struct {
input string
shouldErr bool
addresses []address
tokens map[string]int // map of directive name to number of tokens expected
}{
{`localhost`, false, []address{
{"localhost", ""},
}, map[string]int{}},
{`localhost
dir1`, false, []address{
{"localhost", ""},
}, map[string]int{
"dir1": 1,
}},
{`localhost:1234
dir1 foo bar`, false, []address{
{"localhost", "1234"},
}, map[string]int{
"dir1": 3,
}},
{`localhost {
dir1
}`, false, []address{
{"localhost", ""},
}, map[string]int{
"dir1": 1,
}},
{`localhost:1234 {
dir1 foo bar
dir2
}`, false, []address{
{"localhost", "1234"},
}, map[string]int{
"dir1": 3,
"dir2": 1,
}},
{`http://localhost https://localhost
dir1 foo bar`, false, []address{
{"localhost", "http"},
{"localhost", "https"},
}, map[string]int{
"dir1": 3,
}},
{`http://localhost https://localhost {
dir1 foo bar
}`, false, []address{
{"localhost", "http"},
{"localhost", "https"},
}, map[string]int{
"dir1": 3,
}},
{`http://localhost, https://localhost {
dir1 foo bar
}`, false, []address{
{"localhost", "http"},
{"localhost", "https"},
}, map[string]int{
"dir1": 3,
}},
{`http://localhost, {
}`, true, []address{
{"localhost", "http"},
}, map[string]int{}},
{`host1:80, http://host2.com
dir1 foo bar
dir2 baz`, false, []address{
{"host1", "80"},
{"host2.com", "http"},
}, map[string]int{
"dir1": 3,
"dir2": 2,
}},
{`http://host1.com,
http://host2.com,
https://host3.com`, false, []address{
{"host1.com", "http"},
{"host2.com", "http"},
{"host3.com", "https"},
}, map[string]int{}},
{`http://host1.com:1234, https://host2.com
dir1 foo {
bar baz
}
dir2`, false, []address{
{"host1.com", "1234"},
{"host2.com", "https"},
}, map[string]int{
"dir1": 6,
"dir2": 1,
}},
{`127.0.0.1
dir1 {
bar baz
}
dir2 {
foo bar
}`, false, []address{
{"127.0.0.1", ""},
}, map[string]int{
"dir1": 5,
"dir2": 5,
}},
{`127.0.0.1
unknown_directive`, true, []address{
{"127.0.0.1", ""},
}, map[string]int{}},
{`localhost
dir1 {
foo`, true, []address{
{"localhost", ""},
}, map[string]int{
"dir1": 3,
}},
{`localhost
dir1 {
}`, false, []address{
{"localhost", ""},
}, map[string]int{
"dir1": 3,
}},
{`localhost
dir1 {
nested {
foo
}
}
dir2 foo bar`, false, []address{
{"localhost", ""},
}, map[string]int{
"dir1": 7,
"dir2": 3,
}},
{``, false, []address{}, map[string]int{}},
{`localhost
dir1 arg1
import import_test1.txt`, false, []address{
{"localhost", ""},
}, map[string]int{
"dir1": 2,
"dir2": 3,
"dir3": 1,
}},
{`import import_test2.txt`, false, []address{
{"host1", ""},
}, map[string]int{
"dir1": 1,
"dir2": 2,
}},
} {
result, err := testParseOne(test.input)
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected an error, but didn't get one", i)
}
if !test.shouldErr && err != nil {
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
}
if len(result.addresses) != len(test.addresses) {
t.Errorf("Test %d: Expected %d addresses, got %d",
i, len(test.addresses), len(result.addresses))
continue
}
for j, addr := range result.addresses {
if addr.host != test.addresses[j].host {
t.Errorf("Test %d, address %d: Expected host to be '%s', but was '%s'",
i, j, test.addresses[j].host, addr.host)
}
if addr.port != test.addresses[j].port {
t.Errorf("Test %d, address %d: Expected port to be '%s', but was '%s'",
i, j, test.addresses[j].port, addr.port)
}
}
if len(result.tokens) != len(test.tokens) {
t.Errorf("Test %d: Expected %d directives, had %d",
i, len(test.tokens), len(result.tokens))
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))
continue
}
}
}
}
func TestParseAll(t *testing.T) {
setupParseTests()
for i, test := range []struct {
input string
shouldErr bool
addresses []address // one per expected server block, in order
}{
{`localhost`, false, []address{
{"localhost", ""},
}},
{`localhost:1234`, false, []address{
{"localhost", "1234"},
}},
{`localhost:1234 {
}
localhost:2015 {
}`, false, []address{
{"localhost", "1234"},
{"localhost", "2015"},
}},
{`localhost:1234, http://host2`, false, []address{
{"localhost", "1234"},
{"host2", "http"},
}},
{`localhost:1234, http://host2,`, true, []address{}},
{`http://host1.com, http://host2.com {
}
https://host3.com, https://host4.com {
}`, false, []address{
{"host1.com", "http"},
{"host2.com", "http"},
{"host3.com", "https"},
{"host4.com", "https"},
}},
} {
p := testParser(test.input)
blocks, err := p.parseAll()
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected an error, but didn't get one", i)
}
if !test.shouldErr && err != nil {
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
}
if len(blocks) != len(test.addresses) {
t.Errorf("Test %d: Expected %d server blocks, got %d",
i, len(test.addresses), len(blocks))
continue
}
for j, block := range blocks {
if block.Host != test.addresses[j].host {
t.Errorf("Test %d, block %d: Expected host to be '%s', but was '%s'",
i, j, test.addresses[j].host, block.Host)
}
if block.Port != test.addresses[j].port {
t.Errorf("Test %d, block %d: Expected port to be '%s', but was '%s'",
i, j, test.addresses[j].port, block.Port)
}
}
}
// Exploding the server blocks that have more than one address should replicate/share tokens
p := testParser(`host1 {
dir1 foo bar
}
host2, host3 {
dir2 foo bar
dir3 foo {
bar
}
}`)
blocks, err := p.parseAll()
if err != nil {
t.Fatalf("Expected there to not be an error, but there was: %v", err)
}
if !reflect.DeepEqual(blocks[1].Tokens, blocks[2].Tokens) {
t.Errorf("Expected host2 and host3 to have same tokens, but they didn't.\nhost2 Block: %v\nhost3 Block: %v",
blocks[1].Tokens, blocks[2].Tokens)
}
}
func setupParseTests() {
// Set up some bogus directives for testing
ValidDirectives = map[string]struct{}{
"dir1": struct{}{},
"dir2": struct{}{},
"dir3": struct{}{},
}
}
func testParser(input string) parser {
buf := strings.NewReader(input)
p := parser{Dispenser: NewDispenser("Test", buf)}
return p
}
-53
View File
@@ -1,53 +0,0 @@
package setup
import (
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/basicauth"
)
// BasicAuth configures a new BasicAuth middleware instance.
func BasicAuth(c *Controller) (middleware.Middleware, error) {
rules, err := basicAuthParse(c)
if err != nil {
return nil, err
}
basic := basicauth.BasicAuth{Rules: rules}
return func(next middleware.Handler) middleware.Handler {
basic.Next = next
return basic
}, nil
}
func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
var rules []basicauth.Rule
for c.Next() {
var rule basicauth.Rule
args := c.RemainingArgs()
switch len(args) {
case 2:
rule.Username = args[0]
rule.Password = args[1]
for c.NextBlock() {
rule.Resources = append(rule.Resources, c.Val())
if c.NextArg() {
return rules, c.Errf("Expecting only one resource per line (extra '%s')", c.Val())
}
}
case 3:
rule.Resources = append(rule.Resources, args[0])
rule.Username = args[1]
rule.Password = args[2]
default:
return rules, c.ArgErr()
}
rules = append(rules, rule)
}
return rules, nil
}
-100
View File
@@ -1,100 +0,0 @@
package setup
import (
"fmt"
"testing"
"github.com/mholt/caddy/middleware/basicauth"
)
func TestBasicAuth(t *testing.T) {
c := NewTestController(`basicauth user pwd`)
mid, err := BasicAuth(c)
if err != nil {
t.Errorf("Expected no errors, but got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(basicauth.BasicAuth)
if !ok {
t.Fatalf("Expected handler to be type BasicAuth, got: %#v", handler)
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
func TestBasicAuthParse(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expected []basicauth.Rule
}{
{`basicauth user pwd`, false, []basicauth.Rule{
{Username: "user", Password: "pwd"},
}},
{`basicauth user pwd {
}`, false, []basicauth.Rule{
{Username: "user", Password: "pwd"},
}},
{`basicauth user pwd {
/resource1
/resource2
}`, false, []basicauth.Rule{
{Username: "user", Password: "pwd", Resources: []string{"/resource1", "/resource2"}},
}},
{`basicauth /resource user pwd`, false, []basicauth.Rule{
{Username: "user", Password: "pwd", Resources: []string{"/resource"}},
}},
{`basicauth /res1 user1 pwd1
basicauth /res2 user2 pwd2`, false, []basicauth.Rule{
{Username: "user1", Password: "pwd1", Resources: []string{"/res1"}},
{Username: "user2", Password: "pwd2", Resources: []string{"/res2"}},
}},
{`basicauth user`, true, []basicauth.Rule{}},
{`basicauth`, true, []basicauth.Rule{}},
{`basicauth /resource user pwd asdf`, true, []basicauth.Rule{}},
}
for i, test := range tests {
c := NewTestController(test.input)
actual, err := basicAuthParse(c)
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.Password != expectedRule.Password {
t.Errorf("Test %d, rule %d: Expected password '%s', got '%s'",
i, j, expectedRule.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)
}
}
}
}
-12
View File
@@ -1,12 +0,0 @@
package setup
import "github.com/mholt/caddy/middleware"
func BindHost(c *Controller) (middleware.Middleware, error) {
for c.Next() {
if !c.Args(&c.BindHost) {
return nil, c.ArgErr()
}
}
return nil, nil
}
-251
View File
@@ -1,251 +0,0 @@
package setup
import (
"fmt"
"io/ioutil"
"text/template"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/browse"
)
// Browse configures a new Browse middleware instance.
func Browse(c *Controller) (middleware.Middleware, error) {
configs, err := browseParse(c)
if err != nil {
return nil, err
}
browse := browse.Browse{
Root: c.Root,
Configs: configs,
}
return func(next middleware.Handler) middleware.Handler {
browse.Next = next
return browse
}, nil
}
func browseParse(c *Controller) ([]browse.Config, error) {
var configs []browse.Config
appendCfg := func(bc browse.Config) error {
for _, c := range configs {
if c.PathScope == bc.PathScope {
return fmt.Errorf("duplicate browsing config for %s", c.PathScope)
}
}
configs = append(configs, bc)
return nil
}
for c.Next() {
var bc browse.Config
// First argument is directory to allow browsing; default is site root
if c.NextArg() {
bc.PathScope = c.Val()
} else {
bc.PathScope = "/"
}
// Second argument would be the template file to use
var tplText string
if c.NextArg() {
tplBytes, err := ioutil.ReadFile(c.Val())
if err != nil {
return configs, err
}
tplText = string(tplBytes)
} else {
tplText = defaultTemplate
}
// Build the template
tpl, err := template.New("listing").Parse(tplText)
if err != nil {
return configs, err
}
bc.Template = tpl
// Save configuration
err = appendCfg(bc)
if err != nil {
return configs, err
}
}
return configs, nil
}
// The default template to use when serving up directory listings
const defaultTemplate = `<!DOCTYPE html>
<html>
<head>
<title>{{.Name}}</title>
<meta charset="utf-8">
<style>
* { padding: 0; margin: 0; }
body {
padding: 1% 2%;
font: 16px Arial;
}
header {
font-size: 45px;
padding: 25px;
}
header a {
text-decoration: none;
color: inherit;
}
header .up {
display: inline-block;
height: 50px;
width: 50px;
text-align: center;
margin-right: 20px;
}
header a.up:hover {
background: #000;
color: #FFF;
}
h1 {
font-size: 30px;
display: inline;
}
table {
border: 0;
border-collapse: collapse;
max-width: 750px;
margin: 0 auto;
}
th,
td {
padding: 4px 20px;
vertical-align: middle;
line-height: 1.5em; /* emoji are kind of odd heights */
}
th {
text-align: left;
}
th a {
color: #000;
text-decoration: none;
}
@media (max-width: 700px) {
.hideable {
display: none;
}
body {
padding: 0;
}
header,
header h1 {
font-size: 16px;
}
header {
position: fixed;
top: 0;
width: 100%;
background: #333;
color: #FFF;
padding: 15px;
text-align: center;
}
header .up {
height: auto;
width: auto;
display: none;
}
header a.up {
display: inline-block;
position: absolute;
left: 0;
top: 0;
width: 40px;
height: 48px;
font-size: 35px;
}
header h1 {
font-weight: normal;
}
main {
margin-top: 70px;
}
}
</style>
</head>
<body>
<header>
{{if .CanGoUp}}
<a href=".." class="up" title="Up one level">&#11025;</a>
{{else}}
<div class="up">&nbsp;</div>
{{end}}
<h1>{{.Path}}</h1>
</header>
<main>
<table>
<tr>
<th>
{{if and (eq .Sort "name") (ne .Order "desc")}}
<a href="?sort=name&order=desc">Name &#9650;</a>
{{else if and (eq .Sort "name") (ne .Order "asc")}}
<a href="?sort=name&order=asc">Name &#9660;</a>
{{else}}
<a href="?sort=name&order=asc">Name</a>
{{end}}
</th>
<th>
{{if and (eq .Sort "size") (ne .Order "desc")}}
<a href="?sort=size&order=desc">Size &#9650;</a>
{{else if and (eq .Sort "size") (ne .Order "asc")}}
<a href="?sort=size&order=asc">Size &#9660;</a>
{{else}}
<a href="?sort=size&order=asc">Size</a>
{{end}}
</th>
<th class="hideable">
{{if and (eq .Sort "time") (ne .Order "desc")}}
<a href="?sort=time&order=desc">Modified &#9650;</a>
{{else if and (eq .Sort "time") (ne .Order "asc")}}
<a href="?sort=time&order=asc">Modified &#9660;</a>
{{else}}
<a href="?sort=time&order=asc">Modified</a>
{{end}}
</th>
</tr>
{{range .Items}}
<tr>
<td>
{{if .IsDir}}&#128194;{{else}}&#128196;{{end}}
<a href="{{.URL}}">{{.Name}}</a>
</td>
<td>{{.HumanSize}}</td>
<td class="hideable">{{.HumanModTime "01/02/2006 3:04:05 PM -0700"}}</td>
</tr>
{{end}}
</table>
</main>
</body>
</html>`
-11
View File
@@ -1,11 +0,0 @@
package setup
import (
"github.com/mholt/caddy/config/parse"
"github.com/mholt/caddy/server"
)
type Controller struct {
*server.Config
parse.Dispenser
}
-32
View File
@@ -1,32 +0,0 @@
package setup
import (
"fmt"
"net/http"
"strings"
"github.com/mholt/caddy/config/parse"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/server"
)
// NewTestController creates a new *Controller for
// the input specified, with a filename of "Testfile"
func NewTestController(input string) *Controller {
return &Controller{
Config: &server.Config{},
Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)),
}
}
// EmptyNext is a no-op function that can be passed into
// middleware.Middleware functions so that the assignment
// to the Next field of the Handler can be tested.
var EmptyNext = middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
})
// SameNext does a pointer comparison between next1 and next2.
func SameNext(next1, next2 middleware.Handler) bool {
return fmt.Sprintf("%p", next1) == fmt.Sprintf("%p", next2)
}
-104
View File
@@ -1,104 +0,0 @@
package setup
import (
"fmt"
"log"
"os"
"path"
"strconv"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/errors"
)
// Errors configures a new gzip middleware instance.
func Errors(c *Controller) (middleware.Middleware, error) {
handler, err := errorsParse(c)
if err != nil {
return nil, err
}
// Open the log file for writing when the server starts
c.Startup = append(c.Startup, func() error {
var err error
var file *os.File
if handler.LogFile == "stdout" {
file = os.Stdout
} else if handler.LogFile == "stderr" {
file = os.Stderr
} else if handler.LogFile != "" {
file, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
return err
}
}
handler.Log = log.New(file, "", 0)
return nil
})
return func(next middleware.Handler) middleware.Handler {
handler.Next = next
return handler
}, nil
}
func errorsParse(c *Controller) (*errors.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 := &errors.ErrorHandler{ErrorPages: make(map[int]string)}
optionalBlock := func() (bool, error) {
var hadBlock bool
for c.NextBlock() {
hadBlock = true
what := c.Val()
if !c.NextArg() {
return hadBlock, c.ArgErr()
}
where := c.Val()
if what == "log" {
handler.LogFile = where
} else {
// Error page; ensure it exists
where = path.Join(c.Root, where)
f, err := os.Open(where)
if err != nil {
fmt.Println("Warning: Unable to open error page '" + where + "': " + err.Error())
}
f.Close()
whatInt, err := strconv.Atoi(what)
if err != nil {
return hadBlock, c.Err("Expecting a numeric status code, got '" + what + "'")
}
handler.ErrorPages[whatInt] = where
}
}
return hadBlock, nil
}
for c.Next() {
// Configuration may be in a block
hadBlock, err := optionalBlock()
if err != nil {
return handler, err
}
// Otherwise, the only argument would be an error log file name
if !hadBlock {
if c.NextArg() {
handler.LogFile = c.Val()
} else {
handler.LogFile = errors.DefaultLogFilename
}
}
}
return handler, nil
}
-54
View File
@@ -1,54 +0,0 @@
package setup
import (
"os"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/extensions"
)
// Ext configures a new instance of 'extensions' middleware for clean URLs.
func Ext(c *Controller) (middleware.Middleware, error) {
root := c.Root
exts, err := extParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return extensions.Ext{
Next: next,
Extensions: exts,
Root: root,
}
}, nil
}
// extParse sets up an instance of extension middleware
// from a middleware controller and returns a list of extensions.
func extParse(c *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
}
// resourceExists returns true if the file specified at
// root + path exists; false otherwise.
func resourceExists(root, path string) bool {
_, err := os.Stat(root + path)
// technically we should use os.IsNotExist(err)
// but we don't handle any other kinds of errors anyway
return err == nil
}
-76
View File
@@ -1,76 +0,0 @@
package setup
import (
"testing"
"github.com/mholt/caddy/middleware/extensions"
)
func TestExt(t *testing.T) {
c := NewTestController(`ext .html .htm .php`)
mid, err := Ext(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(extensions.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 !SameNext(myHandler.Next, 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 {
c := NewTestController(test.inputExts)
actualExts, err := extParse(c)
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)
}
}
}
}
-110
View File
@@ -1,110 +0,0 @@
package setup
import (
"errors"
"net/http"
"path/filepath"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/fastcgi"
)
// FastCGI configures a new FastCGI middleware instance.
func FastCGI(c *Controller) (middleware.Middleware, error) {
absRoot, err := filepath.Abs(c.Root)
if err != nil {
return nil, err
}
rules, err := fastcgiParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return fastcgi.Handler{
Next: next,
Rules: rules,
Root: c.Root,
AbsRoot: absRoot,
FileSys: http.Dir(c.Root),
SoftwareName: c.AppName,
SoftwareVersion: c.AppVersion,
ServerName: c.Host,
ServerPort: c.Port,
}
}, nil
}
func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) {
var rules []fastcgi.Rule
for c.Next() {
var rule fastcgi.Rule
args := c.RemainingArgs()
switch len(args) {
case 0:
return rules, c.ArgErr()
case 1:
rule.Path = "/"
rule.Address = args[0]
case 2:
rule.Path = args[0]
rule.Address = args[1]
case 3:
rule.Path = args[0]
rule.Address = args[1]
err := fastcgiPreset(args[2], &rule)
if err != nil {
return rules, c.Err("Invalid fastcgi rule preset '" + args[2] + "'")
}
}
for c.NextBlock() {
switch 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 "env":
envArgs := c.RemainingArgs()
if len(envArgs) < 2 {
return rules, c.ArgErr()
}
rule.EnvVars = append(rule.EnvVars, [2]string{envArgs[0], envArgs[1]})
}
}
rules = append(rules, rule)
}
return rules, nil
}
// fastcgiPreset configures rule according to name. It returns an error if
// name is not a recognized preset name.
func fastcgiPreset(name string, rule *fastcgi.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
}
-97
View File
@@ -1,97 +0,0 @@
package setup
import (
"fmt"
"github.com/mholt/caddy/middleware/fastcgi"
"testing"
)
func TestFastCGI(t *testing.T) {
c := NewTestController(`fastcgi / 127.0.0.1:9000`)
mid, err := FastCGI(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(fastcgi.Handler)
if !ok {
t.Fatalf("Expected handler to be type , got: %#v", handler)
}
if myHandler.Rules[0].Path != "/" {
t.Errorf("Expected / as the Path")
}
if myHandler.Rules[0].Address != "127.0.0.1:9000" {
t.Errorf("Expected 127.0.0.1:9000 as the Address")
}
}
func TestFastcgiParse(t *testing.T) {
tests := []struct {
inputFastcgiConfig string
shouldErr bool
expectedFastcgiConfig []fastcgi.Rule
}{
{`fastcgi /blog 127.0.0.1:9000 php`,
false, []fastcgi.Rule{{
Path: "/blog",
Address: "127.0.0.1:9000",
Ext: ".php",
SplitPath: ".php",
IndexFiles: []string{"index.php"},
}}},
}
for i, test := range tests {
c := NewTestController(test.inputFastcgiConfig)
actualFastcgiConfigs, err := fastcgiParse(c)
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)
}
if actualFastcgiConfig.Address != test.expectedFastcgiConfig[j].Address {
t.Errorf("Test %d expected %dth FastCGI Address to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].Address, actualFastcgiConfig.Address)
}
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)
}
}
}
}
-95
View File
@@ -1,95 +0,0 @@
package setup
import (
"fmt"
"strconv"
"strings"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/gzip"
)
// Gzip configures a new gzip middleware instance.
func Gzip(c *Controller) (middleware.Middleware, error) {
configs, err := gzipParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return gzip.Gzip{Next: next, Configs: configs}
}, nil
}
func gzipParse(c *Controller) ([]gzip.Config, error) {
var configs []gzip.Config
for c.Next() {
config := gzip.Config{}
pathFilter := gzip.PathFilter{make(gzip.Set)}
extFilter := gzip.ExtFilter{make(gzip.Set)}
// 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 != gzip.ExtWildCard {
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
default:
return configs, c.ArgErr()
}
}
config.Filters = []gzip.Filter{}
// If ignored paths are specified, put in front to filter with path first
if len(pathFilter.IgnoredPaths) > 0 {
config.Filters = []gzip.Filter{pathFilter}
}
// Then, if extensions are specified, use those to filter.
// Otherwise, use default extensions filter.
if len(extFilter.Exts) > 0 {
config.Filters = append(config.Filters, extFilter)
} else {
config.Filters = append(config.Filters, gzip.DefaultExtFilter())
}
configs = append(configs, config)
}
return configs, nil
}
-86
View File
@@ -1,86 +0,0 @@
package setup
import (
"testing"
"github.com/mholt/caddy/middleware/gzip"
)
func TestGzip(t *testing.T) {
c := NewTestController(`gzip`)
mid, err := Gzip(c)
if err != nil {
t.Errorf("Expected no errors, but got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(gzip.Gzip)
if !ok {
t.Fatalf("Expected handler to be type Gzip, got: %#v", handler)
}
if !SameNext(myHandler.Next, 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 { 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},
}
for i, test := range tests {
c := NewTestController(test.input)
_, err := gzipParse(c)
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)
}
}
}
-84
View File
@@ -1,84 +0,0 @@
package setup
import (
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/headers"
)
// Headers configures a new Headers middleware instance.
func Headers(c *Controller) (middleware.Middleware, error) {
rules, err := headersParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return headers.Headers{Next: next, Rules: rules}
}, nil
}
func headersParse(c *Controller) ([]headers.Rule, error) {
var rules []headers.Rule
for c.NextLine() {
var head headers.Rule
var isNewPattern bool
if !c.NextArg() {
return rules, c.ArgErr()
}
pattern := c.Val()
// See if we already have a definition for this Path pattern...
for _, h := range rules {
if h.Path == pattern {
head = h
break
}
}
// ...otherwise, this is a new pattern
if head.Path == "" {
head.Path = pattern
isNewPattern = true
}
for c.NextBlock() {
// A block of headers was opened...
h := headers.Header{Name: c.Val()}
if c.NextArg() {
h.Value = c.Val()
}
head.Headers = append(head.Headers, h)
}
if c.NextArg() {
// ... or single header was defined as an argument instead.
h := headers.Header{Name: c.Val()}
h.Value = c.Val()
if c.NextArg() {
h.Value = c.Val()
}
head.Headers = append(head.Headers, h)
}
if isNewPattern {
rules = append(rules, head)
} else {
for i := 0; i < len(rules); i++ {
if rules[i].Path == pattern {
rules[i] = head
break
}
}
}
}
return rules, nil
}
-85
View File
@@ -1,85 +0,0 @@
package setup
import (
"fmt"
"testing"
"github.com/mholt/caddy/middleware/headers"
)
func TestHeaders(t *testing.T) {
c := NewTestController(`header / Foo Bar`)
mid, err := Headers(c)
if err != nil {
t.Errorf("Expected no errors, but got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(headers.Headers)
if !ok {
t.Fatalf("Expected handler to be type Headers, got: %#v", handler)
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
func TestHeadersParse(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expected []headers.Rule
}{
{`header /foo Foo "Bar Baz"`,
false, []headers.Rule{
{Path: "/foo", Headers: []headers.Header{
{"Foo", "Bar Baz"},
}},
}},
{`header /bar { Foo "Bar Baz" Baz Qux }`,
false, []headers.Rule{
{Path: "/bar", Headers: []headers.Header{
{"Foo", "Bar Baz"},
{"Baz", "Qux"},
}},
}},
}
for i, test := range tests {
c := NewTestController(test.input)
actual, err := headersParse(c)
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.Path != expectedRule.Path {
t.Errorf("Test %d, rule %d: Expected path %s, but got %s",
i, j, expectedRule.Path, actualRule.Path)
}
expectedHeaders := fmt.Sprintf("%v", expectedRule.Headers)
actualHeaders := fmt.Sprintf("%v", actualRule.Headers)
if actualHeaders != expectedHeaders {
t.Errorf("Test %d, rule %d: Expected headers %s, but got %s",
i, j, expectedHeaders, actualHeaders)
}
}
}
}
-31
View File
@@ -1,31 +0,0 @@
package setup
import (
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/inner"
)
// Internal configures a new Internal middleware instance.
func Internal(c *Controller) (middleware.Middleware, error) {
paths, err := internalParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return inner.Internal{Next: next, Paths: paths}
}, nil
}
func internalParse(c *Controller) ([]string, error) {
var paths []string
for c.Next() {
if !c.NextArg() {
return paths, c.ArgErr()
}
paths = append(paths, c.Val())
}
return paths, nil
}
-72
View File
@@ -1,72 +0,0 @@
package setup
import (
"testing"
"github.com/mholt/caddy/middleware/inner"
)
func TestInternal(t *testing.T) {
c := NewTestController(`internal /internal`)
mid, err := Internal(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(inner.Internal)
if !ok {
t.Fatalf("Expected handler to be type Internal, got: %#v", handler)
}
if myHandler.Paths[0] != "/internal" {
t.Errorf("Expected internal in the list of internal Paths")
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
func TestInternalParse(t *testing.T) {
tests := []struct {
inputInternalPaths string
shouldErr bool
expectedInternalPaths []string
}{
{`internal /internal`, false, []string{"/internal"}},
{`internal /internal1
internal /internal2`, false, []string{"/internal1", "/internal2"}},
}
for i, test := range tests {
c := NewTestController(test.inputInternalPaths)
actualInternalPaths, err := internalParse(c)
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(actualInternalPaths) != len(test.expectedInternalPaths) {
t.Fatalf("Test %d expected %d InternalPaths, but got %d",
i, len(test.expectedInternalPaths), len(actualInternalPaths))
}
for j, actualInternalPath := range actualInternalPaths {
if actualInternalPath != test.expectedInternalPaths[j] {
t.Fatalf("Test %d expected %dth Internal Path to be %s , but got %s",
i, j, test.expectedInternalPaths[j], actualInternalPath)
}
}
}
}
-92
View File
@@ -1,92 +0,0 @@
package setup
import (
"log"
"os"
"github.com/mholt/caddy/middleware"
caddylog "github.com/mholt/caddy/middleware/log"
"github.com/mholt/caddy/server"
)
// Log sets up the logging middleware.
func Log(c *Controller) (middleware.Middleware, error) {
rules, err := logParse(c)
if err != nil {
return nil, err
}
// Open the log files for writing when the server starts
c.Startup = append(c.Startup, func() error {
for i := 0; i < len(rules); i++ {
var err error
var file *os.File
if rules[i].OutputFile == "stdout" {
file = os.Stdout
} else if rules[i].OutputFile == "stderr" {
file = os.Stderr
} else {
file, err = os.OpenFile(rules[i].OutputFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
return err
}
}
rules[i].Log = log.New(file, "", 0)
}
return nil
})
return func(next middleware.Handler) middleware.Handler {
return caddylog.Logger{Next: next, Rules: rules, ErrorFunc: server.DefaultErrorFunc}
}, nil
}
func logParse(c *Controller) ([]caddylog.Rule, error) {
var rules []caddylog.Rule
for c.Next() {
args := c.RemainingArgs()
if len(args) == 0 {
// Nothing specified; use defaults
rules = append(rules, caddylog.Rule{
PathScope: "/",
OutputFile: caddylog.DefaultLogFilename,
Format: caddylog.DefaultLogFormat,
})
} else if len(args) == 1 {
// Only an output file specified
rules = append(rules, caddylog.Rule{
PathScope: "/",
OutputFile: args[0],
Format: caddylog.DefaultLogFormat,
})
} else {
// Path scope, output file, and maybe a format specified
format := caddylog.DefaultLogFormat
if len(args) > 2 {
switch args[2] {
case "{common}":
format = caddylog.CommonLogFormat
case "{combined}":
format = caddylog.CombinedLogFormat
default:
format = args[2]
}
}
rules = append(rules, caddylog.Rule{
PathScope: args[0],
OutputFile: args[1],
Format: format,
})
}
}
return rules, nil
}
-134
View File
@@ -1,134 +0,0 @@
package setup
import (
"testing"
caddylog "github.com/mholt/caddy/middleware/log"
)
func TestLog(t *testing.T) {
c := NewTestController(`log`)
mid, err := Log(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(caddylog.Logger)
if !ok {
t.Fatalf("Expected handler to be type Logger, got: %#v", handler)
}
if myHandler.Rules[0].PathScope != "/" {
t.Errorf("Expected / as the default PathScope")
}
if myHandler.Rules[0].OutputFile != caddylog.DefaultLogFilename {
t.Errorf("Expected %s as the default OutputFile", caddylog.DefaultLogFilename)
}
if myHandler.Rules[0].Format != caddylog.DefaultLogFormat {
t.Errorf("Expected %s as the default Log Format", caddylog.DefaultLogFormat)
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
func TestLogParse(t *testing.T) {
tests := []struct {
inputLogRules string
shouldErr bool
expectedLogRules []caddylog.Rule
}{
{`log`, false, []caddylog.Rule{{
PathScope: "/",
OutputFile: caddylog.DefaultLogFilename,
Format: caddylog.DefaultLogFormat,
}}},
{`log log.txt`, false, []caddylog.Rule{{
PathScope: "/",
OutputFile: "log.txt",
Format: caddylog.DefaultLogFormat,
}}},
{`log /api log.txt`, false, []caddylog.Rule{{
PathScope: "/api",
OutputFile: "log.txt",
Format: caddylog.DefaultLogFormat,
}}},
{`log /serve stdout`, false, []caddylog.Rule{{
PathScope: "/serve",
OutputFile: "stdout",
Format: caddylog.DefaultLogFormat,
}}},
{`log /myapi log.txt {common}`, false, []caddylog.Rule{{
PathScope: "/myapi",
OutputFile: "log.txt",
Format: caddylog.CommonLogFormat,
}}},
{`log /test accesslog.txt {combined}`, false, []caddylog.Rule{{
PathScope: "/test",
OutputFile: "accesslog.txt",
Format: caddylog.CombinedLogFormat,
}}},
{`log /api1 log.txt
log /api2 accesslog.txt {combined}`, false, []caddylog.Rule{{
PathScope: "/api1",
OutputFile: "log.txt",
Format: caddylog.DefaultLogFormat,
}, {
PathScope: "/api2",
OutputFile: "accesslog.txt",
Format: caddylog.CombinedLogFormat,
}}},
{`log /api3 stdout {host}
log /api4 log.txt {when}`, false, []caddylog.Rule{{
PathScope: "/api3",
OutputFile: "stdout",
Format: "{host}",
}, {
PathScope: "/api4",
OutputFile: "log.txt",
Format: "{when}",
}}},
}
for i, test := range tests {
c := NewTestController(test.inputLogRules)
actualLogRules, err := logParse(c)
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(actualLogRules) != len(test.expectedLogRules) {
t.Fatalf("Test %d expected %d no of Log rules, but got %d ",
i, len(test.expectedLogRules), len(actualLogRules))
}
for j, actualLogRule := range actualLogRules {
if actualLogRule.PathScope != test.expectedLogRules[j].PathScope {
t.Errorf("Test %d expected %dth LogRule PathScope to be %s , but got %s",
i, j, test.expectedLogRules[j].PathScope, actualLogRule.PathScope)
}
if actualLogRule.OutputFile != test.expectedLogRules[j].OutputFile {
t.Errorf("Test %d expected %dth LogRule OutputFile to be %s , but got %s",
i, j, test.expectedLogRules[j].OutputFile, actualLogRule.OutputFile)
}
if actualLogRule.Format != test.expectedLogRules[j].Format {
t.Errorf("Test %d expected %dth LogRule Format to be %s , but got %s",
i, j, test.expectedLogRules[j].Format, actualLogRule.Format)
}
}
}
}
-168
View File
@@ -1,168 +0,0 @@
package setup
import (
"io/ioutil"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/markdown"
"github.com/russross/blackfriday"
)
// Markdown configures a new Markdown middleware instance.
func Markdown(c *Controller) (middleware.Middleware, error) {
mdconfigs, err := markdownParse(c)
if err != nil {
return nil, err
}
md := markdown.Markdown{
Root: c.Root,
FileSys: http.Dir(c.Root),
Configs: mdconfigs,
IndexFiles: []string{"index.md"},
}
// For any configs that enabled static site gen, sweep the whole path at startup
c.Startup = append(c.Startup, func() error {
for _, cfg := range mdconfigs {
if cfg.StaticDir == "" {
continue
}
if err := markdown.GenerateLinks(md, &cfg); err != nil {
return err
}
// If generated site already exists, clear it out
_, err := os.Stat(cfg.StaticDir)
if err == nil {
err := os.RemoveAll(cfg.StaticDir)
if err != nil {
return err
}
}
fp := filepath.Join(md.Root, cfg.PathScope)
filepath.Walk(fp, func(path string, info os.FileInfo, err error) error {
for _, ext := range cfg.Extensions {
if !info.IsDir() && strings.HasSuffix(info.Name(), ext) {
// Load the file
body, err := ioutil.ReadFile(path)
if err != nil {
return err
}
// Get the relative path as if it were a HTTP request,
// then prepend with "/" (like a real HTTP request)
reqPath, err := filepath.Rel(md.Root, path)
if err != nil {
return err
}
reqPath = "/" + reqPath
// Generate the static file
ctx := middleware.Context{Root: md.FileSys}
_, err = md.Process(cfg, reqPath, body, ctx)
if err != nil {
return err
}
break // don't try other file extensions
}
}
return nil
})
}
return nil
})
return func(next middleware.Handler) middleware.Handler {
md.Next = next
return md
}, nil
}
func markdownParse(c *Controller) ([]markdown.Config, error) {
var mdconfigs []markdown.Config
for c.Next() {
md := markdown.Config{
Renderer: blackfriday.HtmlRenderer(0, "", ""),
Templates: make(map[string]string),
StaticFiles: make(map[string]string),
}
// Get the path scope
if !c.NextArg() || c.Val() == "{" {
return mdconfigs, c.ArgErr()
}
md.PathScope = c.Val()
// Load any other configuration parameters
for c.NextBlock() {
switch c.Val() {
case "ext":
exts := c.RemainingArgs()
if len(exts) == 0 {
return mdconfigs, c.ArgErr()
}
md.Extensions = append(md.Extensions, exts...)
case "css":
if !c.NextArg() {
return mdconfigs, c.ArgErr()
}
md.Styles = append(md.Styles, c.Val())
case "js":
if !c.NextArg() {
return mdconfigs, c.ArgErr()
}
md.Scripts = append(md.Scripts, c.Val())
case "template":
tArgs := c.RemainingArgs()
switch len(tArgs) {
case 0:
return mdconfigs, c.ArgErr()
case 1:
if _, ok := md.Templates[markdown.DefaultTemplate]; ok {
return mdconfigs, c.Err("only one default template is allowed, use alias.")
}
fpath := filepath.Clean(c.Root + string(filepath.Separator) + tArgs[0])
md.Templates[markdown.DefaultTemplate] = fpath
case 2:
fpath := filepath.Clean(c.Root + string(filepath.Separator) + tArgs[1])
md.Templates[tArgs[0]] = fpath
default:
return mdconfigs, c.ArgErr()
}
case "sitegen":
if c.NextArg() {
md.StaticDir = path.Join(c.Root, c.Val())
} else {
md.StaticDir = path.Join(c.Root, markdown.DefaultStaticDir)
}
if c.NextArg() {
// only 1 argument allowed
return mdconfigs, c.ArgErr()
}
default:
return mdconfigs, c.Err("Expected valid markdown configuration property")
}
}
// If no extensions were specified, assume .md
if len(md.Extensions) == 0 {
md.Extensions = []string{".md"}
}
mdconfigs = append(mdconfigs, md)
}
return mdconfigs, nil
}
-182
View File
@@ -1,182 +0,0 @@
package setup
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"testing"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/markdown"
)
func TestMarkdown(t *testing.T) {
c := NewTestController(`markdown /blog`)
mid, err := Markdown(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(markdown.Markdown)
if !ok {
t.Fatalf("Expected handler to be type Markdown, got: %#v", handler)
}
if myHandler.Configs[0].PathScope != "/blog" {
t.Errorf("Expected /blog as the Path Scope")
}
if fmt.Sprint(myHandler.Configs[0].Extensions) != fmt.Sprint([]string{".md"}) {
t.Errorf("Expected .md as the Default Extension")
}
}
func TestMarkdownStaticGen(t *testing.T) {
c := NewTestController(`markdown /blog {
ext .md
template tpl_with_include.html
sitegen
}`)
c.Root = "./testdata"
mid, err := Markdown(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
for _, start := range c.Startup {
err := start()
if err != nil {
t.Errorf("Startup error: %v", err)
}
}
next := middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
t.Fatalf("Next shouldn't be called")
return 0, nil
})
hndlr := mid(next)
mkdwn, ok := hndlr.(markdown.Markdown)
if !ok {
t.Fatalf("Was expecting a markdown.Markdown but got %T", hndlr)
}
expectedStaticFiles := map[string]string{"/blog/first_post.md": "testdata/generated_site/blog/first_post.md/index.html"}
if fmt.Sprint(expectedStaticFiles) != fmt.Sprint(mkdwn.Configs[0].StaticFiles) {
t.Fatalf("Test expected StaticFiles to be %s, but got %s",
fmt.Sprint(expectedStaticFiles), fmt.Sprint(mkdwn.Configs[0].StaticFiles))
}
filePath := "testdata/generated_site/blog/first_post.md/index.html"
if _, err := os.Stat(filePath); err != nil {
t.Fatalf("An error occured when getting the file information: %v", err)
}
html, err := ioutil.ReadFile(filePath)
if err != nil {
t.Fatalf("An error occured when getting the file content: %v", err)
}
expectedBody := `<!DOCTYPE html>
<html>
<head>
<title>first_post</title>
</head>
<body>
<h1>Header title</h1>
<h1>Test h1</h1>
</body>
</html>
`
if string(html) != expectedBody {
t.Fatalf("Expected file content: %v got: %v", expectedBody, html)
}
fp := filepath.Join(c.Root, markdown.DefaultStaticDir)
if err = os.RemoveAll(fp); err != nil {
t.Errorf("Error while removing the generated static files: ", err)
}
}
func TestMarkdownParse(t *testing.T) {
tests := []struct {
inputMarkdownConfig string
shouldErr bool
expectedMarkdownConfig []markdown.Config
}{
{`markdown /blog {
ext .md .txt
css /resources/css/blog.css
js /resources/js/blog.js
}`, false, []markdown.Config{{
PathScope: "/blog",
Extensions: []string{".md", ".txt"},
Styles: []string{"/resources/css/blog.css"},
Scripts: []string{"/resources/js/blog.js"},
}}},
{`markdown /blog {
ext .md
template tpl_with_include.html
sitegen
}`, false, []markdown.Config{{
PathScope: "/blog",
Extensions: []string{".md"},
Templates: map[string]string{markdown.DefaultTemplate: "testdata/tpl_with_include.html"},
StaticDir: markdown.DefaultStaticDir,
}}},
}
for i, test := range tests {
c := NewTestController(test.inputMarkdownConfig)
c.Root = "./testdata"
actualMarkdownConfigs, err := markdownParse(c)
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(actualMarkdownConfigs) != len(test.expectedMarkdownConfig) {
t.Fatalf("Test %d expected %d no of WebSocket configs, but got %d ",
i, len(test.expectedMarkdownConfig), len(actualMarkdownConfigs))
}
for j, actualMarkdownConfig := range actualMarkdownConfigs {
if actualMarkdownConfig.PathScope != test.expectedMarkdownConfig[j].PathScope {
t.Errorf("Test %d expected %dth Markdown PathScope to be %s , but got %s",
i, j, test.expectedMarkdownConfig[j].PathScope, actualMarkdownConfig.PathScope)
}
if fmt.Sprint(actualMarkdownConfig.Styles) != fmt.Sprint(test.expectedMarkdownConfig[j].Styles) {
t.Errorf("Test %d expected %dth Markdown Config Styles to be %s , but got %s",
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Styles), fmt.Sprint(actualMarkdownConfig.Styles))
}
if fmt.Sprint(actualMarkdownConfig.Scripts) != fmt.Sprint(test.expectedMarkdownConfig[j].Scripts) {
t.Errorf("Test %d expected %dth Markdown Config Scripts to be %s , but got %s",
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Scripts), fmt.Sprint(actualMarkdownConfig.Scripts))
}
if fmt.Sprint(actualMarkdownConfig.Templates) != fmt.Sprint(test.expectedMarkdownConfig[j].Templates) {
t.Errorf("Test %d expected %dth Markdown Config Templates to be %s , but got %s",
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Templates), fmt.Sprint(actualMarkdownConfig.Templates))
}
}
}
}
-17
View File
@@ -1,17 +0,0 @@
package setup
import (
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/proxy"
)
// Proxy configures a new Proxy middleware instance.
func Proxy(c *Controller) (middleware.Middleware, error) {
if upstreams, err := proxy.NewStaticUpstreams(c.Dispenser); err == nil {
return func(next middleware.Handler) middleware.Handler {
return proxy.Proxy{Next: next, Upstreams: upstreams}
}, nil
} else {
return nil, err
}
}
-160
View File
@@ -1,160 +0,0 @@
package setup
import (
"net/http"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/redirect"
)
// Redir configures a new Redirect middleware instance.
func Redir(c *Controller) (middleware.Middleware, error) {
rules, err := redirParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return redirect.Redirect{Next: next, Rules: rules}
}, nil
}
func redirParse(c *Controller) ([]redirect.Rule, error) {
var redirects []redirect.Rule
// setRedirCode sets the redirect code for rule if it can, or returns an error
setRedirCode := func(code string, rule *redirect.Rule) error {
if code == "meta" {
rule.Meta = true
} else if codeNumber, ok := httpRedirs[code]; ok {
rule.Code = codeNumber
} else {
return c.Errf("Invalid redirect code '%v'", code)
}
return nil
}
// checkAndSaveRule checks the rule for validity (except the redir code)
// and saves it if it's valid, or returns an error.
checkAndSaveRule := func(rule redirect.Rule) error {
if rule.From == rule.To {
return c.Err("'from' and 'to' values of redirect rule cannot be the same")
}
for _, otherRule := range redirects {
if otherRule.From == rule.From {
return c.Errf("rule with duplicate 'from' value: %s -> %s", otherRule.From, otherRule.To)
}
}
redirects = append(redirects, rule)
return nil
}
for c.Next() {
args := c.RemainingArgs()
var hadOptionalBlock bool
for c.NextBlock() {
hadOptionalBlock = true
var rule redirect.Rule
// Set initial redirect code
// BUG: If the code is specified for a whole block and that code is invalid,
// the line number will appear on the first line inside the block, even if that
// line overwrites the block-level code with a valid redirect code. The program
// still functions correctly, but the line number in the error reporting is
// misleading to the user.
if len(args) == 1 {
err := setRedirCode(args[0], &rule)
if err != nil {
return redirects, err
}
} else {
rule.Code = http.StatusMovedPermanently // default code
}
// RemainingArgs only gets the values after the current token, but in our
// case we want to include the current token to get an accurate count.
insideArgs := append([]string{c.Val()}, c.RemainingArgs()...)
switch len(insideArgs) {
case 1:
// To specified (catch-all redirect)
// Not sure why user is doing this in a table, as it causes all other redirects to be ignored.
// As such, this feature remains undocumented.
rule.From = "/"
rule.To = insideArgs[0]
case 2:
// From and To specified
rule.From = insideArgs[0]
rule.To = insideArgs[1]
case 3:
// From, To, and Code specified
rule.From = insideArgs[0]
rule.To = insideArgs[1]
err := setRedirCode(insideArgs[2], &rule)
if err != nil {
return redirects, err
}
default:
return redirects, c.ArgErr()
}
err := checkAndSaveRule(rule)
if err != nil {
return redirects, err
}
}
if !hadOptionalBlock {
var rule redirect.Rule
rule.Code = http.StatusMovedPermanently // default
switch len(args) {
case 1:
// To specified (catch-all redirect)
rule.From = "/"
rule.To = args[0]
case 2:
// To and Code specified (catch-all redirect)
rule.From = "/"
rule.To = args[0]
err := setRedirCode(args[1], &rule)
if err != nil {
return redirects, err
}
case 3:
// From, To, and Code specified
rule.From = args[0]
rule.To = args[1]
err := setRedirCode(args[2], &rule)
if err != nil {
return redirects, err
}
default:
return redirects, c.ArgErr()
}
err := checkAndSaveRule(rule)
if err != nil {
return redirects, err
}
}
}
return redirects, nil
}
// httpRedirs is a list of supported HTTP redirect codes.
var httpRedirs = map[string]int{
"300": 300, // Multiple Choices
"301": 301, // Moved Permanently
"302": 302, // Found (NOT CORRECT for "Temporary Redirect", see 307)
"303": 303, // See Other
"304": 304, // Not Modified
"305": 305, // Use Proxy
"307": 307, // Temporary Redirect
"308": 308, // Permanent Redirect
}
-79
View File
@@ -1,79 +0,0 @@
package setup
import (
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/rewrite"
)
// Rewrite configures a new Rewrite middleware instance.
func Rewrite(c *Controller) (middleware.Middleware, error) {
rewrites, err := rewriteParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return rewrite.Rewrite{Next: next, Rules: rewrites}
}, nil
}
func rewriteParse(c *Controller) ([]rewrite.Rule, error) {
var simpleRules []rewrite.Rule
var regexpRules []rewrite.Rule
for c.Next() {
var rule rewrite.Rule
var err error
var base = "/"
var pattern, to string
var ext []string
args := c.RemainingArgs()
switch len(args) {
case 2:
rule = rewrite.NewSimpleRule(args[0], args[1])
simpleRules = append(simpleRules, rule)
case 1:
base = args[0]
fallthrough
case 0:
for c.NextBlock() {
switch c.Val() {
case "r", "regexp":
if !c.NextArg() {
return nil, c.ArgErr()
}
pattern = c.Val()
case "to":
if !c.NextArg() {
return nil, c.ArgErr()
}
to = c.Val()
case "ext":
args1 := c.RemainingArgs()
if len(args1) == 0 {
return nil, c.ArgErr()
}
ext = args1
default:
return nil, c.ArgErr()
}
}
// ensure pattern and to are specified
if pattern == "" || to == "" {
return nil, c.ArgErr()
}
if rule, err = rewrite.NewRegexpRule(base, pattern, to, ext); err != nil {
return nil, err
}
regexpRules = append(regexpRules, rule)
default:
return nil, c.ArgErr()
}
}
// put simple rules in front to avoid regexp computation for them
return append(simpleRules, regexpRules...), nil
}
-185
View File
@@ -1,185 +0,0 @@
package setup
import (
"testing"
"fmt"
"regexp"
"github.com/mholt/caddy/middleware/rewrite"
)
func TestRewrite(t *testing.T) {
c := NewTestController(`rewrite /from /to`)
mid, err := Rewrite(c)
if err != nil {
t.Errorf("Expected no errors, but got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(rewrite.Rewrite)
if !ok {
t.Fatalf("Expected handler to be type Rewrite, got: %#v", handler)
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
if len(myHandler.Rules) != 1 {
t.Errorf("Expected handler to have %d rule, has %d instead", 1, len(myHandler.Rules))
}
}
func TestRewriteParse(t *testing.T) {
simpleTests := []struct {
input string
shouldErr bool
expected []rewrite.Rule
}{
{`rewrite /from /to`, false, []rewrite.Rule{
rewrite.SimpleRule{"/from", "/to"},
}},
{`rewrite /from /to
rewrite a b`, false, []rewrite.Rule{
rewrite.SimpleRule{"/from", "/to"},
rewrite.SimpleRule{"a", "b"},
}},
{`rewrite a`, true, []rewrite.Rule{}},
{`rewrite`, true, []rewrite.Rule{}},
{`rewrite a b c`, true, []rewrite.Rule{
rewrite.SimpleRule{"a", "b"},
}},
}
for i, test := range simpleTests {
c := NewTestController(test.input)
actual, err := rewriteParse(c)
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 len(actual) != len(test.expected) {
t.Fatalf("Test %d expected %d rules, but got %d",
i, len(test.expected), len(actual))
}
for j, e := range test.expected {
actualRule := actual[j].(rewrite.SimpleRule)
expectedRule := e.(rewrite.SimpleRule)
if actualRule.From != expectedRule.From {
t.Errorf("Test %d, rule %d: Expected From=%s, got %s",
i, j, expectedRule.From, actualRule.From)
}
if actualRule.To != expectedRule.To {
t.Errorf("Test %d, rule %d: Expected To=%s, got %s",
i, j, expectedRule.To, actualRule.To)
}
}
}
regexpTests := []struct {
input string
shouldErr bool
expected []rewrite.Rule
}{
{`rewrite {
r .*
to /to
}`, false, []rewrite.Rule{
&rewrite.RegexpRule{"/", "/to", nil, regexp.MustCompile(".*")},
}},
{`rewrite {
regexp .*
to /to
ext / html txt
}`, false, []rewrite.Rule{
&rewrite.RegexpRule{"/", "/to", []string{"/", "html", "txt"}, regexp.MustCompile(".*")},
}},
{`rewrite /path {
r rr
to /dest
}
rewrite / {
regexp [a-z]+
to /to
}
`, false, []rewrite.Rule{
&rewrite.RegexpRule{"/path", "/dest", nil, regexp.MustCompile("rr")},
&rewrite.RegexpRule{"/", "/to", nil, regexp.MustCompile("[a-z]+")},
}},
{`rewrite {
to /to
}`, true, []rewrite.Rule{
&rewrite.RegexpRule{},
}},
{`rewrite {
r .*
}`, true, []rewrite.Rule{
&rewrite.RegexpRule{},
}},
{`rewrite {
}`, true, []rewrite.Rule{
&rewrite.RegexpRule{},
}},
{`rewrite /`, true, []rewrite.Rule{
&rewrite.RegexpRule{},
}},
}
for i, test := range regexpTests {
c := NewTestController(test.input)
actual, err := rewriteParse(c)
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 len(actual) != len(test.expected) {
t.Fatalf("Test %d expected %d rules, but got %d",
i, len(test.expected), len(actual))
}
for j, e := range test.expected {
actualRule := actual[j].(*rewrite.RegexpRule)
expectedRule := e.(*rewrite.RegexpRule)
if actualRule.Base != expectedRule.Base {
t.Errorf("Test %d, rule %d: Expected Base=%s, got %s",
i, j, expectedRule.Base, actualRule.Base)
}
if actualRule.To != expectedRule.To {
t.Errorf("Test %d, rule %d: Expected To=%s, got %s",
i, j, expectedRule.To, actualRule.To)
}
if fmt.Sprint(actualRule.Exts) != fmt.Sprint(expectedRule.Exts) {
t.Errorf("Test %d, rule %d: Expected Ext=%v, got %v",
i, j, expectedRule.To, actualRule.To)
}
if actualRule.String() != expectedRule.String() {
t.Errorf("Test %d, rule %d: Expected Pattern=%s, got %s",
i, j, expectedRule.String(), actualRule.String())
}
}
}
}
-31
View File
@@ -1,31 +0,0 @@
package setup
import (
"log"
"os"
"github.com/mholt/caddy/middleware"
)
func Root(c *Controller) (middleware.Middleware, error) {
for c.Next() {
if !c.NextArg() {
return nil, c.ArgErr()
}
c.Root = c.Val()
}
// Check if root path exists
_, err := os.Stat(c.Root)
if err != nil {
if os.IsNotExist(err) {
// Allow this, because the folder might appear later.
// But make sure the user knows!
log.Printf("Warning: Root path does not exist: %s", c.Root)
} else {
return nil, c.Errf("Unable to access root path '%s': %v", c.Root, err)
}
}
return nil, nil
}
-58
View File
@@ -1,58 +0,0 @@
package setup
import (
"os"
"os/exec"
"strings"
"github.com/mholt/caddy/middleware"
)
func Startup(c *Controller) (middleware.Middleware, error) {
return nil, registerCallback(c, &c.Startup)
}
func Shutdown(c *Controller) (middleware.Middleware, error) {
return nil, registerCallback(c, &c.Shutdown)
}
// registerCallback registers a callback function to execute by
// using c to parse the line. It appends the callback function
// to the list of callback functions passed in by reference.
func registerCallback(c *Controller, list *[]func() error) error {
for c.Next() {
args := c.RemainingArgs()
if len(args) == 0 {
return c.ArgErr()
}
nonblock := false
if len(args) > 1 && args[len(args)-1] == "&" {
// Run command in background; non-blocking
nonblock = true
args = args[:len(args)-1]
}
command, args, err := middleware.SplitCommandAndArgs(strings.Join(args, " "))
if err != nil {
return c.Err(err.Error())
}
fn := func() error {
cmd := exec.Command(command, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if nonblock {
return cmd.Start()
} else {
return cmd.Run()
}
}
*list = append(*list, fn)
}
return nil
}
-61
View File
@@ -1,61 +0,0 @@
package setup
import (
"net/http"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/templates"
)
// Templates configures a new Templates middleware instance.
func Templates(c *Controller) (middleware.Middleware, error) {
rules, err := templatesParse(c)
if err != nil {
return nil, err
}
tmpls := templates.Templates{
Rules: rules,
Root: c.Root,
FileSys: http.Dir(c.Root),
}
return func(next middleware.Handler) middleware.Handler {
tmpls.Next = next
return tmpls
}, nil
}
func templatesParse(c *Controller) ([]templates.Rule, error) {
var rules []templates.Rule
for c.Next() {
var rule templates.Rule
if c.NextArg() {
// First argument would be the path
rule.Path = c.Val()
// Any remaining arguments are extensions
rule.Extensions = c.RemainingArgs()
if len(rule.Extensions) == 0 {
rule.Extensions = defaultTemplateExtensions
}
} else {
rule.Path = defaultTemplatePath
rule.Extensions = defaultTemplateExtensions
}
for _, ext := range rule.Extensions {
rule.IndexFiles = append(rule.IndexFiles, "index"+ext)
}
rules = append(rules, rule)
}
return rules, nil
}
const defaultTemplatePath = "/"
var defaultTemplateExtensions = []string{".html", ".htm", ".tmpl", ".tpl", ".txt"}
-94
View File
@@ -1,94 +0,0 @@
package setup
import (
"fmt"
"github.com/mholt/caddy/middleware/templates"
"testing"
)
func TestTemplates(t *testing.T) {
c := NewTestController(`templates`)
mid, err := Templates(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(templates.Templates)
if !ok {
t.Fatalf("Expected handler to be type Templates, got: %#v", handler)
}
if myHandler.Rules[0].Path != defaultTemplatePath {
t.Errorf("Expected / as the default Path")
}
if fmt.Sprint(myHandler.Rules[0].Extensions) != fmt.Sprint(defaultTemplateExtensions) {
t.Errorf("Expected %v to be the Default Extensions", defaultTemplateExtensions)
}
var indexFiles []string
for _, extension := range defaultTemplateExtensions {
indexFiles = append(indexFiles, "index"+extension)
}
if fmt.Sprint(myHandler.Rules[0].IndexFiles) != fmt.Sprint(indexFiles) {
t.Errorf("Expected %v to be the Default Index files", indexFiles)
}
}
func TestTemplatesParse(t *testing.T) {
tests := []struct {
inputTemplateConfig string
shouldErr bool
expectedTemplateConfig []templates.Rule
}{
{`templates /api1`, false, []templates.Rule{{
Path: "/api1",
Extensions: defaultTemplateExtensions,
}}},
{`templates /api2 .txt .htm`, false, []templates.Rule{{
Path: "/api2",
Extensions: []string{".txt", ".htm"},
}}},
{`templates /api3 .htm .html
templates /api4 .txt .tpl `, false, []templates.Rule{{
Path: "/api3",
Extensions: []string{".htm", ".html"},
}, {
Path: "/api4",
Extensions: []string{".txt", ".tpl"},
}}},
}
for i, test := range tests {
c := NewTestController(test.inputTemplateConfig)
actualTemplateConfigs, err := templatesParse(c)
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(actualTemplateConfigs) != len(test.expectedTemplateConfig) {
t.Fatalf("Test %d expected %d no of Template configs, but got %d ",
i, len(test.expectedTemplateConfig), len(actualTemplateConfigs))
}
for j, actualTemplateConfig := range actualTemplateConfigs {
if actualTemplateConfig.Path != test.expectedTemplateConfig[j].Path {
t.Errorf("Test %d expected %dth Template Config Path to be %s , but got %s",
i, j, test.expectedTemplateConfig[j].Path, actualTemplateConfig.Path)
}
if fmt.Sprint(actualTemplateConfig.Extensions) != fmt.Sprint(test.expectedTemplateConfig[j].Extensions) {
t.Errorf("Expected %v to be the Extensions , but got %v instead", test.expectedTemplateConfig[j].Extensions, actualTemplateConfig.Extensions)
}
}
}
}
-1
View File
@@ -1 +0,0 @@
# Test h1
-1
View File
@@ -1 +0,0 @@
<h1>Header title</h1>
-10
View File
@@ -1,10 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>{{.Doc.title}}</title>
</head>
<body>
{{.Include "header.html"}}
{{.Doc.body}}
</body>
</html>
-139
View File
@@ -1,139 +0,0 @@
package setup
import (
"crypto/tls"
"log"
"strings"
"github.com/mholt/caddy/middleware"
)
func TLS(c *Controller) (middleware.Middleware, error) {
c.TLS.Enabled = true
if c.Port == "http" {
c.TLS.Enabled = false
log.Printf("Warning: TLS disabled for %s://%s. To force TLS over the plaintext HTTP port, "+
"specify port 80 explicitly (https://%s:80).", c.Port, c.Host, c.Host)
}
for c.Next() {
if !c.NextArg() {
return nil, c.ArgErr()
}
c.TLS.Certificate = c.Val()
if !c.NextArg() {
return nil, c.ArgErr()
}
c.TLS.Key = c.Val()
// Optional block
for c.NextBlock() {
switch c.Val() {
case "protocols":
args := c.RemainingArgs()
if len(args) != 2 {
return nil, c.ArgErr()
}
value, ok := supportedProtocols[strings.ToLower(args[0])]
if !ok {
return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val())
}
c.TLS.ProtocolMinVersion = value
value, ok = supportedProtocols[strings.ToLower(args[1])]
if !ok {
return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val())
}
c.TLS.ProtocolMaxVersion = value
case "ciphers":
for c.NextArg() {
value, ok := supportedCiphersMap[strings.ToUpper(c.Val())]
if !ok {
return nil, c.Errf("Wrong cipher name or cipher not supported '%s'", c.Val())
}
c.TLS.Ciphers = append(c.TLS.Ciphers, value)
}
case "clients":
c.TLS.ClientCerts = c.RemainingArgs()
if len(c.TLS.ClientCerts) == 0 {
return nil, c.ArgErr()
}
default:
return nil, c.Errf("Unknown keyword '%s'", c.Val())
}
}
}
// If no ciphers provided, use all that Caddy supports for the protocol
if len(c.TLS.Ciphers) == 0 {
c.TLS.Ciphers = supportedCiphers
}
// Not a cipher suite, but still important for mitigating protocol downgrade attacks
c.TLS.Ciphers = append(c.TLS.Ciphers, tls.TLS_FALLBACK_SCSV)
// Set default protocol min and max versions - must balance compatibility and security
if c.TLS.ProtocolMinVersion == 0 {
c.TLS.ProtocolMinVersion = tls.VersionTLS10
}
if c.TLS.ProtocolMaxVersion == 0 {
c.TLS.ProtocolMaxVersion = tls.VersionTLS12
}
// Prefer server cipher suites
c.TLS.PreferServerCipherSuites = true
return nil, nil
}
// Map of supported protocols
// SSLv3 will be not supported in future release
// HTTP/2 only supports TLS 1.2 and higher
var supportedProtocols = map[string]uint16{
"ssl3.0": tls.VersionSSL30,
"tls1.0": tls.VersionTLS10,
"tls1.1": tls.VersionTLS11,
"tls1.2": tls.VersionTLS12,
}
// Map of supported ciphers, used only for parsing config.
//
// Note that, at time of writing, HTTP/2 blacklists 276 cipher suites,
// including all but two of the suites below (the two GCM suites).
// See https://http2.github.io/http2-spec/#BadCipherSuites
//
// TLS_FALLBACK_SCSV is not in this list because we manually ensure
// it is always added (even though it is not technically a cipher suite).
//
// This map, like any map, is NOT ORDERED. Do not range over this map.
var supportedCiphersMap = map[string]uint16{
"ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
"ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
"ECDHE-RSA-AES128-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
"ECDHE-RSA-AES256-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
"ECDHE-ECDSA-AES256-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
"ECDHE-ECDSA-AES128-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
"RSA-AES128-CBC-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
"RSA-AES256-CBC-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
"ECDHE-RSA-3DES-EDE-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
"RSA-3DES-EDE-CBC-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
}
// List of supported cipher suites in descending order of preference.
// Ordering is very important! Getting the wrong order will break
// mainstream clients, especially with HTTP/2.
//
// Note that TLS_FALLBACK_SCSV is not in this list since it is always
// added manually.
var supportedCiphers = []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
}

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