Compare commits

..

85 Commits

Author SHA1 Message Date
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
100 changed files with 8017 additions and 1491 deletions
+4
View File
@@ -6,6 +6,10 @@ Caddyfile
*.prof
*.test
# build artifacts
cmd/caddy/caddy
cmd/caddy/caddy.exe
# mac specific
.DS_Store
+49
View File
@@ -0,0 +1,49 @@
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
- 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
+32 -44
View File
@@ -1,11 +1,14 @@
Caddy 2 Development Branch
===========================
[![Build Status](https://dev.azure.com/mholt-dev/Caddy/_apis/build/status/Multiplatform%20Tests?branchName=v2)](https://dev.azure.com/mholt-dev/Caddy/_build/latest?definitionId=5&branchName=v2)
[![fuzzit](https://app.fuzzit.dev/badge?org_id=caddyserver)](https://app.fuzzit.dev/orgs/caddyserver/dashboard)
This is the development branch for Caddy 2. This code (version 2) is not yet feature-complete or production-ready, but is already being used in production, and we encourage you to deploy it today on sites that are not very visible or important so that it can obtain crucial experience in the field.
Please file issues to propose new features and report bugs, and after the bug or feature has been discussed, submit a pull request! We need your help to build this web server into what you want it to be. (Caddy 2 issues and pull requests will usually receive priority over Caddy 1 issues and pull requests.)
Please file issues to propose new features and report bugs, and after the bug or feature has been discussed, submit a pull request! We need your help to build this web server into what you want it to be. (Caddy 2 issues and pull requests receive priority over Caddy 1 issues and pull requests.)
**We want Caddy 2 to be the web server of the Go community!** We are looking for maintainers to represent the community. Please become involved (issues, PRs, [our forum](https://caddy.community) etc.) and express interest if you are committed to being a collaborator on the Caddy project.
**Caddy 2 is the web server of the Go community.** We are looking for maintainers to represent the community! Please become involved (issues, PRs, [our forum](https://caddy.community) etc.) and express interest if you are committed to being a collaborator on the Caddy project.
### Menu
@@ -23,7 +26,7 @@ Please file issues to propose new features and report bugs, and after the bug or
Requirements:
- [Go 1.13 or newer](https://golang.org/dl/)
- Make sure you do not disable [Go modules](https://github.com/golang/go/wiki/Modules) (`export GO111MODULE=auto`)
- Do NOT disable [Go modules](https://github.com/golang/go/wiki/Modules) (`export GO111MODULE=auto`)
Download the `v2` source code:
@@ -40,7 +43,7 @@ $ go build
That will put a `caddy(.exe)` binary into the current directory. You can move it into your PATH or use `go install` to do that automatically (assuming `$GOPATH/bin` is already in your PATH). You can also use `go run main.go` for quick, temporary builds while developing.
The initial build may be slow as dependencies are downloaded. Subsequent builds should be very fast. If you encounter any Go-module-related errors, try clearing your Go module cache (`$GOPATH/pkg/mod`) and read [the Go wiki page about modules for help](https://github.com/golang/go/wiki/Modules). If you have issues with Go modules, please consult the Go community for help. But if there is an actual error in Caddy, please report it to us.
The initial build may be slow as dependencies are downloaded. Subsequent builds should be very fast. If you encounter any Go-module-related errors, try clearing your Go module cache (`$GOPATH/pkg/mod`) and Go package cache (`$GOPATH/pkg`) and read [the Go wiki page about modules for help](https://github.com/golang/go/wiki/Modules). If you have issues with Go modules, please consult the Go community for help. But if there is an actual error in Caddy, please report it to us.
## Quick Start
@@ -118,32 +121,32 @@ Caddy 2 can be configured with a Caddyfile, much like in v1, for example:
```plain
example.com
templates
encode gzip zstd
try_files {path}.html {path}
reverse_proxy /api localhost:9005
encode gzip zstd
reverse_proxy /api localhost:9005
php_fastcgi /blog unix//path/to/socket
file_server
```
Instead of being its primary mode of configuration, an internal _config adapter_ adapts the Caddyfile to Caddy's native JSON structure. You can see it in action with the [`adapt-config` command](https://github.com/caddyserver/caddy/wiki/v2:-Documentation#adapt-config):
Instead of being its primary mode of configuration, an internal _config adapter_ adapts the Caddyfile to Caddy's native JSON structure. You can see it in action with the [`adapt` command](https://github.com/caddyserver/caddy/wiki/v2:-Documentation#adapt):
```bash
$ ./caddy adapt-config --input path/to/Caddyfile --adapter caddyfile --pretty
$ ./caddy adapt --config path/to/Caddyfile --adapter caddyfile --pretty
```
But if you just want to run Caddy with your Caddyfile directly, the CLI wraps this up for you nicely. Either of the following commands:
If you just want to run Caddy with your Caddyfile directly, the CLI wraps this up for you nicely. Either of the following commands:
```bash
$ ./caddy start
$ ./caddy run
```
will apply your Caddyfile if it is called `Caddyfile` in the current directory.
will use your Caddyfile if it is called `Caddyfile` in the current directory.
If your Caddyfile is somewhere else, you can still use it:
```bash
$ ./caddy start|run --config path/to/Caddyfile --config-adapter caddyfile
$ ./caddy start|run --config path/to/Caddyfile --adapter caddyfile
```
[Learn more about the Caddyfile in v2.](https://github.com/caddyserver/caddy/wiki/v2:-Documentation#caddyfile-adapter)
@@ -171,14 +174,14 @@ Note that breaking changes are expected until the stable 2.0 release.
## List of Improvements
The following is a non-comprehensive list of significant improvements over Caddy 1. Not everything in this list is finished yet, but they will be finished or at least will be possible with Caddy 2 or Caddy Enterprise:
The following is a non-comprehensive list of significant improvements over Caddy 1. Not everything in this list is finished yet, but they will be finished or at least will be possible with Caddy 2:
- Centralized configuration. No more disparate use of environment variables, config files (potentially multiple!), CLI flags, etc.
- REST API. Control Caddy with HTTP requests to an administration endpoint. Changes are applied immediately and efficiently.
- Dynamic configuration. Any and all specific config values can be modified directly through the admin API with a REST endpoint.
- Enterprise: Change only specific configuration settings instead of needing to specify the whole config each time. This makes it safe and easy to change Caddy's config with manually-crafted curl commands, for example.
- Change only specific configuration settings instead of needing to specify the whole config each time. This makes it safe and easy to change Caddy's config with manually-crafted curl commands, for example.
- No configuration files. Except optionally to bootstrap its configuration at startup. You can still use config files if you wish, and we expect that most people will.
- Enterprise: Export the current Caddy configuration with an API GET request.
- Export the current Caddy configuration with an API GET request.
- Silky-smooth graceful reloads. Update the configuration up to dozens of times per second with no dropped requests and very little memory cost. Our unique graceful reload technology is lighter and faster **and works on all platforms, including Windows**.
- An embedded scripting language! Caddy2 has native Starlark integration. Do things you never thought possible with higher performance than Lua, JavaScript, and other VMs. Starlark is expressive, familiar (dialect of Python), _almost_ Turing-complete, and highly efficient. (We're still improving performance here.)
- Using [XDG standards](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables) instead of dumping all assets in `$HOME/.caddy`.
@@ -194,9 +197,10 @@ The following is a non-comprehensive list of significant improvements over Caddy
- Automation policy doesn't have to be limited to just ACME - could be any way to manage certificates
- Fine-grained control over TLS handshakes
- If an ACME challenge fails, other enabled challenges will be tried (no other web server does this)
- Enterprise: TLS Session Ticket Ephemeral Keys (STEKs) can be rotated in a cluster for increased performance (no other web server does this either!)
- Enterprise: Ability to select a specific certificate per ClientHello given multiple qualifying certificates
- Enterprise: Provide TLS certificates without persisting them to disk; keep private keys entirely in memory
- TLS Session Ticket Ephemeral Keys (STEKs) can be rotated in a cluster for increased performance (no other web server does this either!)
- Ability to select a specific certificate per ClientHello given multiple qualifying certificates
- Provide TLS certificates without persisting them to disk; keep private keys entirely in memory
- Certificate management at startup is now asynchronous and much easier to use through machine reboots and in unsupervised settings
- All-new HTTP server core
- Listeners can be configured for any network type, address, and port range
- Customizable TLS connection policies
@@ -216,8 +220,10 @@ The following is a non-comprehensive list of significant improvements over Caddy
- Done away with URL-rewriting hacks often needed in Caddy 1
- Highly descriptive/traceable errors
- Very flexible error handling, with the ability to specify a whole list of routes just for error cases
- The proxy has numerous improvements, including dynamic backends and more configurable health checks
- FastCGI support integrated with the reverse proxy
- More control over automatic HTTPS: disable entirely, disable only HTTP->HTTPS redirects, disable only cert management, and for certain names, etc.
- Enterprise: Use Starlark to build custom, dynamic HTTP handlers at request-time
- Use Starlark to build custom, dynamic HTTP handlers at request-time
- We are finding that -- on average -- Caddy 2's Starlark handlers are ~1.25-2x faster than NGINX+Lua.
And a few major features still being worked on:
@@ -248,7 +254,7 @@ Yes! Caddy's native JSON configuration via API is nice when you are automating c
The v2 Caddyfile is very similar to the v1 Caddyfile, but they are not compatible. Several improvements have been made to request matching and directives in v2, giving you more power with less complexity and fewer inconsistencies.
Caddy's default _config adapter_ is the Caddyfile adapter. This takes a Caddyfile as input and [outputs the JSON config](https://github.com/caddyserver/caddy/wiki/v2:-Documentation#adapt-config). You can even run Caddy directly without having to see or think about the underlying JSON config.
Caddy's default _config adapter_ is the Caddyfile adapter. This takes a Caddyfile as input and [outputs the JSON config](https://github.com/caddyserver/caddy/wiki/v2:-Documentation#adapt). You can even run Caddy directly without having to see or think about the underlying JSON config.
The following _config adapters_ are already being built or plan to be built:
@@ -334,28 +340,10 @@ Starlark performs at least as well as NGINX+Lua (more performance tests ongoing,
In summary: Caddy 2 config is declarative, but can be imperative where that is useful.
### What will Caddy 2 be licensed as?
### What is Caddy 2 licensed as?
Caddy 2 is licensed under the Apache 2.0 open source license. There are no official Caddy 2 distributions that are proprietary.
### What is Caddy Enterprise?
Caddy Enterprise is a collection of plugins for Caddy 2 which provide features and performance that are crucial in business settings. Caddy Enterprise is not a separate web server and does not even use a separate code base from Caddy 2; it is not even a separate branch that merges the open source core in every once in a while. In other words, open source users aren't missing out on a "better" web server, but Enterprise provides features that are used by businesses.
Caddy Enterprise is for businesses that need more advanced features for higher scalability and easier management of clusters. It includes:
- a web UI
- performance improvements within a cluster
- advanced TLS controls
- fine-grained config changes (i.e. ability to change only certain parts of the configuration)
- training and support
- advanced HTTP handlers for authentication, metrics, debugging, and more
- dynamic HTTP handlers and TLS handshakes with Starlark
Caddy Enterprise can be customized for each customer according to their needs.
Caddy 2 and Caddy Enterprise offer equal levels of security and, as mentioned, share the same open source code base.
### Does Caddy 2 have telemetry?
No. There was not enough academic interest to continue supporting it. If telemetry does get added later, it will not be on by default or will be vastly reduced in its scope.
@@ -366,7 +354,7 @@ Yes. HTTPS is automatic and enabled by default when possible, just like in Caddy
## How do I avoid Let's Encrypt rate limits with Caddy 2?
As you are testing and developing with Caddy 2, you may wish to use test ("staging") certificates from Let's Encrypt to avoid rate limits. By default, Caddy 2 uses Let's Encrypt's production endpoint to get real certificates for your domains, but their [rate limits](https://letsencrypt.org/docs/rate-limits/) forbid testing and development use of this endpoint for good reasons. You can switch to their [staging endpoint](https://letsencrypt.org/docs/staging-environment/) by adding the staging CA to your automation policy in the `tls` app:
As you are testing and developing with Caddy 2, you should use test ("staging") certificates from Let's Encrypt to avoid rate limits. By default, Caddy 2 uses Let's Encrypt's production endpoint to get real certificates for your domains, but their [rate limits](https://letsencrypt.org/docs/rate-limits/) forbid testing and development use of this endpoint for good reasons. You can switch to their [staging endpoint](https://letsencrypt.org/docs/staging-environment/) by adding the staging CA to your automation policy in the `tls` app:
```json
"tls": {
@@ -383,14 +371,14 @@ As you are testing and developing with Caddy 2, you may wish to use test ("stagi
}
```
Or with the Caddyfile:
Or with the Caddyfile, using a global options block at the top:
```
tls {
ca https://acme-staging-v02.api.letsencrypt.org/directory
{
acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}
```
## Can we get some access controls on the admin endpoint?
Yeah, that's coming. For now, you can use a unix socket that is properly permissioned for some basic security.
Yeah, that's coming. For now, you can use a permissioned unix socket for some basic security.
+22 -15
View File
@@ -34,6 +34,7 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/mholt/certmagic"
"github.com/rs/cors"
"go.uber.org/zap"
)
var (
@@ -52,6 +53,8 @@ var DefaultAdminConfig = &AdminConfig{
Listen: DefaultAdminListen,
}
// TODO: holy smokes, the admin endpoint might not have to live in caddy's core.
// StartAdmin starts Caddy's administration endpoint,
// bootstrapping it with an optional configuration
// in the format of JSON bytes. It opens a listener
@@ -87,7 +90,7 @@ func StartAdmin(initialConfigJSON []byte) error {
return fmt.Errorf("parsing admin listener address: %v", err)
}
if len(listenAddrs) != 1 {
return fmt.Errorf("admin endpoint must have exactly one listener; cannot listen on %v", listenAddrs)
return fmt.Errorf("admin endpoint must have exactly one address; cannot listen on %v", listenAddrs)
}
ln, err := net.Listen(netw, listenAddrs[0])
if err != nil {
@@ -113,26 +116,34 @@ func StartAdmin(initialConfigJSON []byte) error {
}
}
handler := cors.Default().Handler(mux)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO: improve/organize this logging
Log().Named("admin.request").Info("",
zap.String("method", r.Method),
zap.String("uri", r.RequestURI),
zap.String("remote", r.RemoteAddr),
)
cors.Default().Handler(mux).ServeHTTP(w, r)
})
cfgEndptSrv = &http.Server{
Handler: handler,
ReadTimeout: 5 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
IdleTimeout: 5 * time.Second,
MaxHeaderBytes: 1024 * 256,
MaxHeaderBytes: 1024 * 64,
}
go cfgEndptSrv.Serve(ln)
log.Println("Caddy 2 admin endpoint listening on", adminConfig.Listen)
fmt.Println("Caddy 2 admin endpoint listening on", adminConfig.Listen)
if len(initialConfigJSON) > 0 {
err := Load(bytes.NewReader(initialConfigJSON))
if err != nil {
return fmt.Errorf("loading initial config: %v", err)
}
log.Println("Caddy 2 serving initial configuration")
fmt.Println("Caddy 2 serving initial configuration")
}
return nil
@@ -169,14 +180,11 @@ type AdminRoute struct {
}
func handleLoadConfig(w http.ResponseWriter, r *http.Request) {
r.Close = true
if r.Method != http.MethodPost {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
var payload io.Reader = r.Body
// 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 != "" {
@@ -215,16 +223,15 @@ func handleLoadConfig(w http.ResponseWriter, r *http.Request) {
}
w.Write(respBody)
}
payload = bytes.NewReader(result)
// replace original request body with adapted JSON
r.Body.Close()
r.Body = ioutil.NopCloser(bytes.NewReader(result))
}
}
err := Load(payload)
if err != nil {
log.Printf("[ADMIN][ERROR] loading config: %v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// pass this off to the /config/ endpoint
r.URL.Path = "/" + rawConfigKey + "/"
handleConfig(w, r)
}
func handleStop(w http.ResponseWriter, r *http.Request) {
+30
View File
@@ -0,0 +1,30 @@
// 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
// +build gofuzz_libfuzzer
package caddy
import (
"bytes"
)
func FuzzAdmin(data []byte) (score int) {
err := Load(bytes.NewReader(data))
if err != nil {
return 0
}
return 1
}
+243
View File
@@ -0,0 +1,243 @@
# Mutilated beyond recognition from the example at:
# https://docs.microsoft.com/azure/devops/pipelines/languages/go
trigger:
- v2
schedules:
- cron: "0 0 * * *"
displayName: Daily midnight fuzzing
branches:
include:
- v2
always: true
variables:
GOROOT: $(gorootDir)/go
GOPATH: $(system.defaultWorkingDirectory)/gopath
GOBIN: $(GOPATH)/bin
modulePath: '$(GOPATH)/src/github.com/$(build.repository.name)'
# TODO: Remove once it's enabled by default
GO111MODULE: on
jobs:
- job: crossPlatformTest
displayName: "Cross-Platform Tests"
strategy:
matrix:
linux:
imageName: ubuntu-16.04
gorootDir: /usr/local
mac:
imageName: macos-10.13
gorootDir: /usr/local
windows:
imageName: windows-2019
gorootDir: C:\
pool:
vmImage: $(imageName)
steps:
- bash: |
latestGo=$(curl "https://golang.org/VERSION?m=text")
echo "##vso[task.setvariable variable=LATEST_GO]$latestGo"
echo "Latest Go version: $latestGo"
displayName: "Get latest Go version"
- bash: |
sudo rm -f $(which go)
echo '##vso[task.prependpath]$(GOBIN)'
echo '##vso[task.prependpath]$(GOROOT)/bin'
mkdir -p '$(modulePath)'
shopt -s extglob
shopt -s dotglob
mv !(gopath) '$(modulePath)'
displayName: Remove old Go, set GOBIN/GOROOT, and move project into GOPATH
# Install Go (this varies by platform)
- bash: |
wget "https://dl.google.com/go/$(LATEST_GO).linux-amd64.tar.gz"
sudo tar -C $(gorootDir) -xzf "$(LATEST_GO).linux-amd64.tar.gz"
condition: eq( variables['Agent.OS'], 'Linux' )
displayName: Install Go on Linux
- bash: |
wget "https://dl.google.com/go/$(LATEST_GO).darwin-amd64.tar.gz"
sudo tar -C $(gorootDir) -xzf "$(LATEST_GO).darwin-amd64.tar.gz"
condition: eq( variables['Agent.OS'], 'Darwin' )
displayName: Install Go on macOS
# The low performance is partly due to PowerShell's attempt to update the progress bar. Disabling it speeds up the process.
# Reference: https://github.com/PowerShell/PowerShell/issues/2138
- powershell: |
$ProgressPreference = 'SilentlyContinue'
Write-Host "Downloading Go..."
(New-Object System.Net.WebClient).DownloadFile("https://dl.google.com/go/$(LATEST_GO).windows-amd64.zip", "$(LATEST_GO).windows-amd64.zip")
Write-Host "Extracting Go... (I'm slow too)"
7z x "$(LATEST_GO).windows-amd64.zip" -o"$(gorootDir)"
condition: eq( variables['Agent.OS'], 'Windows_NT' )
displayName: Install Go on Windows
- bash: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.19.1
displayName: Install golangci-lint
- script: |
go get github.com/axw/gocov/gocov
go get github.com/AlekSi/gocov-xml
go get -u github.com/jstemmer/go-junit-report
displayName: Install test and coverage analysis tools
- bash: |
printf "Using go at: $(which go)\n"
printf "Go version: $(go version)\n"
printf "\n\nGo environment:\n\n"
go env
printf "\n\nSystem environment:\n\n"
env
displayName: Print Go version and environment
- script: |
go get -v -t -d ./...
mkdir test-results
workingDirectory: '$(modulePath)'
displayName: Get dependencies
# its behavior is governed by .golangci.yml
- script: |
(golangci-lint run --out-format junit-xml) > test-results/lint-result.xml
exit 0
workingDirectory: '$(modulePath)'
continueOnError: true
displayName: Run lint check
- script: |
(go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
workingDirectory: '$(modulePath)'
continueOnError: true
displayName: Run tests
- script: |
mkdir coverage
gocov convert cover-profile.out > coverage/coverage.json
# Because Windows doesn't work with input redirection like *nix, but output redirection works.
(cat ./coverage/coverage.json | gocov-xml) > coverage/coverage.xml
workingDirectory: '$(modulePath)'
displayName: Prepare coverage reports
- script: |
(cat ./test-results/test-result.out | go-junit-report) > test-results/test-result.xml
workingDirectory: '$(modulePath)'
displayName: Prepare test report
- task: PublishCodeCoverageResults@1
displayName: Publish test coverage report
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: $(modulePath)/coverage/coverage.xml
- task: PublishTestResults@2
displayName: Publish unit test
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: $(modulePath)/test-results/test-result.xml
testRunTitle: $(agent.OS) Unit Test
mergeTestResults: false
- task: PublishTestResults@2
displayName: Publish lint results
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: $(modulePath)/test-results/lint-result.xml
testRunTitle: $(agent.OS) Lint
mergeTestResults: false
- bash: |
exit 1
condition: eq(variables['Agent.JobStatus'], 'SucceededWithIssues')
displayName: Coerce correct build result
- job: fuzzing
displayName: 'Fuzzing'
# Only run this job on schedules or PRs for non-forks.
condition: or(eq(variables['System.PullRequest.IsFork'], 'False'), eq(variables['Build.Reason'], 'Schedule') )
strategy:
matrix:
linux:
imageName: ubuntu-16.04
gorootDir: /usr/local
pool:
vmImage: $(imageName)
steps:
- bash: |
latestGo=$(curl "https://golang.org/VERSION?m=text")
echo "##vso[task.setvariable variable=LATEST_GO]$latestGo"
echo "Latest Go version: $latestGo"
displayName: "Get latest Go version"
- bash: |
sudo rm -f $(which go)
echo '##vso[task.prependpath]$(GOBIN)'
echo '##vso[task.prependpath]$(GOROOT)/bin'
mkdir -p '$(modulePath)'
shopt -s extglob
shopt -s dotglob
mv !(gopath) '$(modulePath)'
displayName: Remove old Go, set GOBIN/GOROOT, and move project into GOPATH
- bash: |
wget "https://dl.google.com/go/$(LATEST_GO).linux-amd64.tar.gz"
sudo tar -C $(gorootDir) -xzf "$(LATEST_GO).linux-amd64.tar.gz"
condition: eq( variables['Agent.OS'], 'Linux' )
displayName: Install Go on Linux
- bash: |
# Install Clang-7.0 because other versions seem to be missing the file libclang_rt.fuzzer-x86_64.a
sudo add-apt-repository "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-7 main"
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
sudo apt update && sudo apt install -y clang-7 lldb-7 lld-7
go get -v github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
wget -q -O fuzzit https://github.com/fuzzitdev/fuzzit/releases/download/v2.4.74/fuzzit_Linux_x86_64
chmod a+x fuzzit
mv fuzzit $(GOBIN)
displayName: Download go-fuzz tools and the Fuzzit CLI, and move Fuzzit CLI to GOBIN
condition: and(eq(variables['System.PullRequest.IsFork'], 'False') , eq( variables['Agent.OS'], 'Linux' ))
- bash: |
declare -A fuzzers_funcs=(\
["./admin_fuzz.go"]="FuzzAdmin" \
["./caddyconfig/httpcaddyfile/adapter_fuzz.go"]="FuzzHTTPCaddyfileAdapter" \
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="FuzzParseAddress" \
["./caddyconfig/caddyfile/parse_fuzz.go"]="FuzzParseCaddyfile" \
["./listeners_fuzz.go"]="FuzzParseNetworkAddress" \
["./replacer_fuzz.go"]="FuzzReplacer" \
)
declare -A fuzzers_targets=(\
["./admin_fuzz.go"]="admin" \
["./caddyconfig/httpcaddyfile/adapter_fuzz.go"]="caddyfile-adapter" \
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="parse-address" \
["./caddyconfig/caddyfile/parse_fuzz.go"]="parse-caddyfile" \
["./listeners_fuzz.go"]="parse-listen-addr" \
["./replacer_fuzz.go"]="replacer" \
)
fuzz_type="regression"
if [[ $(Build.Reason) == "Schedule" ]]; then
fuzz_type="fuzzing"
fi
for f in $(find . -name \*_fuzz.go); do
FUZZER_DIRECTORY=$(dirname $f)
echo "go-fuzz-build func ${fuzzers_funcs[$f]} residing in $f"
go-fuzz-build -func "${fuzzers_funcs[$f]}" -libfuzzer -o "$FUZZER_DIRECTORY/${fuzzers_targets[$f]}.a" $FUZZER_DIRECTORY
echo "Generating fuzzer binary of func ${fuzzers_funcs[$f]} which resides in $f"
clang-7 -fsanitize=fuzzer "$FUZZER_DIRECTORY/${fuzzers_targets[$f]}.a" -o "$FUZZER_DIRECTORY/${fuzzers_targets[$f]}"
fuzzit create job caddyserver/${fuzzers_targets[$f]} $FUZZER_DIRECTORY/${fuzzers_targets[$f]} --api-key ${FUZZIT_API_KEY} --type "${fuzz_type}" --branch "${SYSTEM_PULLREQUEST_SOURCEBRANCH}" --revision "${BUILD_SOURCEVERSION}"
echo "Completed $f"
done
env:
FUZZIT_API_KEY: $(FUZZIT_API_KEY)
workingDirectory: '$(modulePath)'
displayName: Generate fuzzers & submit them to Fuzzit
+156 -105
View File
@@ -29,16 +29,13 @@ import (
// Config represents a Caddy configuration.
type Config struct {
Admin *AdminConfig `json:"admin,omitempty"`
Admin *AdminConfig `json:"admin,omitempty"`
Logging *Logging `json:"logging,omitempty"`
StorageRaw json.RawMessage `json:"storage,omitempty"`
AppsRaw map[string]json.RawMessage `json:"apps,omitempty"`
StorageRaw json.RawMessage `json:"storage,omitempty"`
storage certmagic.Storage
AppsRaw map[string]json.RawMessage `json:"apps,omitempty"`
// apps stores the decoded Apps values,
// keyed by module name.
apps map[string]App
apps map[string]App
storage certmagic.Storage
cancelFunc context.CancelFunc
}
@@ -54,95 +51,10 @@ func Run(newCfg *Config) error {
currentCfgMu.Lock()
defer currentCfgMu.Unlock()
if newCfg != nil {
// 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
// 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 {
cancel() // clean up now
}
}()
newCfg.cancelFunc = cancel // clean up later
// set up storage and make it CertMagic's default storage, too
err = func() error {
if newCfg.StorageRaw != nil {
val, err := ctx.LoadModuleInline("module", "caddy.storage", 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
newCfg.StorageRaw = nil // allow GC to deallocate - TODO: Does this help?
}
if newCfg.storage == nil {
newCfg.storage = &certmagic.FileStorage{Path: dataDir()}
}
certmagic.Default.Storage = newCfg.storage
return nil
}()
if err != nil {
return err
}
// Load, Provision, Validate each app and their submodules
err = func() error {
for modName, rawMsg := range newCfg.AppsRaw {
val, err := ctx.LoadModule(modName, rawMsg)
if err != nil {
return fmt.Errorf("loading app module '%s': %v", modName, err)
}
newCfg.apps[modName] = val.(App)
}
return nil
}()
if err != nil {
return err
}
// Start
err = func() error {
var started []string
for name, a := range newCfg.apps {
err := a.Start()
if err != nil {
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
}()
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
@@ -155,6 +67,133 @@ func Run(newCfg *Config) error {
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.
func run(newCfg *Config, start bool) error {
if newCfg == nil {
return nil
}
// 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
// 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.LoadModuleInline("module", "caddy.storage", 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
newCfg.StorageRaw = nil // allow GC to deallocate
}
if newCfg.storage == nil {
newCfg.storage = &certmagic.FileStorage{Path: dataDir()}
}
certmagic.Default.Storage = newCfg.storage
return nil
}()
if err != nil {
return err
}
// Load, Provision, Validate each app and their submodules
err = func() error {
for modName, rawMsg := range newCfg.AppsRaw {
val, err := ctx.LoadModule(modName, rawMsg)
if err != nil {
return fmt.Errorf("loading app module '%s': %v", modName, err)
}
newCfg.apps[modName] = val.(App)
}
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
@@ -168,26 +207,38 @@ func Stop() error {
return nil
}
// unsyncedStop stops oldCfg from running, but if
// unsyncedStop stops cfg from running, but if
// applicable, you need to acquire locks yourself.
// It is a no-op if oldCfg is nil. If any app
// It is a no-op if cfg is nil. If any app
// returns an error when stopping, it is logged
// and the function continues with the next app.
func unsyncedStop(oldCfg *Config) {
if oldCfg == nil {
// This function assumes all apps in cfg were
// successfully started.
func unsyncedStop(cfg *Config) {
if cfg == nil {
return
}
// stop each app
for name, a := range oldCfg.apps {
for name, a := range cfg.apps {
err := a.Stop()
if err != nil {
log.Printf("[ERROR] stop %s: %v", name, err)
}
}
// clean up all old modules
oldCfg.cancelFunc()
// clean up all modules
cfg.cancelFunc()
}
// 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 is a JSON-string-unmarshable duration type.
+32 -19
View File
@@ -26,9 +26,10 @@ type (
// are separated by whitespace. A word can be enclosed
// in quotes if it contains whitespace.
lexer struct {
reader *bufio.Reader
token Token
line int
reader *bufio.Reader
token Token
line int
skippedLines int
}
// Token represents a single parsable unit.
@@ -91,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 and newlines
if ch != '"' && ch != '\n' {
val = append(val, '\\')
}
l.line += 1 + l.skippedLines
l.skippedLines = 0
}
val = append(val, ch)
escaped = false
continue
}
@@ -120,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 {
@@ -132,7 +141,6 @@ func (l *lexer) next() bool {
if ch == '#' {
comment = true
}
if comment {
continue
}
@@ -145,6 +153,11 @@ func (l *lexer) next() bool {
}
}
if escaped {
val = append(val, '\\')
escaped = false
}
val = append(val, ch)
}
}
+45 -3
View File
@@ -96,13 +96,49 @@ func TestLexer(t *testing.T) {
},
},
{
input: "A \"newline \\\ninside\" quotes",
input: "An escaped \"newline\\\ninside\" quotes",
expected: []Token{
{Line: 1, Text: "A"},
{Line: 1, Text: "newline \ninside"},
{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{
@@ -115,6 +151,12 @@ func TestLexer(t *testing.T) {
{Line: 1, Text: `don't\\escape`},
},
},
{
input: `un\escapable`,
expected: []Token{
{Line: 1, Text: `un\escapable`},
},
},
{
input: `A "quoted value with line
break inside" {
-2
View File
@@ -22,8 +22,6 @@ import (
"testing"
)
// TODO: re-enable all tests
func TestAllTokens(t *testing.T) {
input := strings.NewReader("a b c\nd e")
expected := []string{"a", "b", "c", "d", "e"}
+4 -4
View File
@@ -27,10 +27,10 @@ type Adapter interface {
// Warning represents a warning or notice related to conversion.
type Warning struct {
File string
Line int
Directive string
Message string
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
+49
View File
@@ -0,0 +1,49 @@
// 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
// +build gofuzz_libfuzzer
package httpcaddyfile
import (
"bytes"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func FuzzHTTPCaddyfileAdapter(data []byte) int {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
b, warns, err := adapter.Adapt(data, nil)
// Adapt func calls the Setup() func of the ServerType,
// thus it's going across multiple layers, each can
// return warnings or errors. Marking the presence of
// errors or warnings as interesting in this case
// could push the fuzzer towards a path where we only
// catch errors. Let's push the fuzzer to where it passes
// but breaks.
if (err != nil) || (warns != nil && len(warns) > 0) {
return 0
}
// adapted Caddyfile should be parseable by the configuration loader in admin.go
err = caddy.Load(bytes.NewReader(b))
if err != nil {
return 0
}
return 1
}
+4 -2
View File
@@ -161,7 +161,7 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
}
addr = addr.Normalize()
lnPort := defaultPort
lnPort := DefaultPort
if addr.Port != "" {
// port explicitly defined
lnPort = addr.Port
@@ -327,6 +327,8 @@ func (a Address) Key() string {
}
const (
defaultPort = "2015"
// DefaultPort is the default port to use.
DefaultPort = "2015"
caseSensitivePath = false // TODO: Used?
)
@@ -0,0 +1,29 @@
// 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
// +build gofuzz_libfuzzer
package httpcaddyfile
func FuzzParseAddress(data []byte) int {
addr, err := ParseAddress(string(data))
if err != nil {
if addr == (Address{}) {
return 1
}
return 0
}
return 1
}
+20 -2
View File
@@ -31,6 +31,7 @@ func init() {
RegisterDirective("root", parseRoot)
RegisterDirective("tls", parseTLS)
RegisterHandlerDirective("redir", parseRedir)
RegisterHandlerDirective("respond", parseRespond)
}
func parseBind(h Helper) ([]ConfigValue, error) {
@@ -85,6 +86,14 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
var mgr caddytls.ACMEManagerMaker
var off bool
// fill in global defaults, if configured
if email := h.Option("email"); email != nil {
mgr.Email = email.(string)
}
if acmeCA := h.Option("acme_ca"); acmeCA != nil {
mgr.CA = acmeCA.(string)
}
for h.Next() {
// file certificate loader
firstLine := h.RemainingArgs()
@@ -111,7 +120,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
hasBlock = true
switch h.Val() {
// connection policy
case "protocols":
args := h.RemainingArgs()
@@ -163,7 +171,8 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
}
mgr.CA = arg[0]
// TODO: other properties for automation manager
default:
return nil, h.Errf("unknown subdirective: %s", h.Val())
}
}
@@ -253,3 +262,12 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
Body: body,
}, nil
}
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
}
+8 -2
View File
@@ -25,16 +25,16 @@ import (
// defaultDirectiveOrder specifies the order
// to apply directives in HTTP routes.
// TODO: finish the ability to customize this
var defaultDirectiveOrder = []string{
"rewrite",
"try_files",
"basicauth",
"headers",
"request_header",
"encode",
"templates",
"redir",
"static_response", // TODO: "reply" or "respond"?
"respond",
"reverse_proxy",
"php_fastcgi",
"file_server",
@@ -81,11 +81,17 @@ func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
// Caddyfile tokens.
type Helper struct {
*caddyfile.Dispenser
options map[string]interface{}
warnings *[]caddyconfig.Warning
matcherDefs map[string]map[string]json.RawMessage
parentBlock caddyfile.ServerBlock
}
// 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 {
-56
View File
@@ -1,56 +0,0 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpcaddyfile
import (
"encoding/json"
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func (st *ServerType) parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]map[string]json.RawMessage, error) {
matchers := make(map[string]map[string]json.RawMessage)
for d.Next() {
definitionName := d.Val()
for nesting := d.Nesting(); d.NextBlock(nesting); {
matcherName := d.Val()
mod, err := caddy.GetModule("http.matchers." + matcherName)
if err != nil {
return nil, fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
}
err = unm.UnmarshalCaddyfile(d.NewFromNextTokens())
if err != nil {
return nil, err
}
rm, ok := unm.(caddyhttp.RequestMatcher)
if !ok {
return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
}
if _, ok := matchers[definitionName]; !ok {
matchers[definitionName] = make(map[string]json.RawMessage)
}
matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
}
}
return matchers, nil
}
+95 -17
View File
@@ -57,15 +57,23 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
dir := segment.Directive()
var val interface{}
var err error
disp := caddyfile.NewDispenser(segment)
// TODO: make this switch into a map
switch dir {
case "http_port":
val, err = parseHTTPPort(caddyfile.NewDispenser(segment))
val, err = parseOptHTTPPort(disp)
case "https_port":
val, err = parseHTTPSPort(caddyfile.NewDispenser(segment))
val, err = parseOptHTTPSPort(disp)
case "handler_order":
val, err = parseHandlerOrder(caddyfile.NewDispenser(segment))
val, err = parseOptHandlerOrder(disp)
case "experimental_http3":
val, err = parseExperimentalHTTP3(caddyfile.NewDispenser(segment))
val, err = parseOptExperimentalHTTP3(disp)
case "storage":
val, err = parseOptStorage(disp)
case "acme_ca":
val, err = parseOptACMECA(disp)
case "email":
val, err = parseOptEmail(disp)
default:
return nil, warnings, fmt.Errorf("unrecognized parameter name: %s", dir)
}
@@ -105,7 +113,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
// extract matcher definitions
d := sb.block.DispenseDirective("matcher")
matcherDefs, err := st.parseMatcherDefinitions(d)
matcherDefs, err := parseMatcherDefinitions(d)
if err != nil {
return nil, warnings, err
}
@@ -119,6 +127,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
if dirFunc, ok := registeredDirectives[dir]; ok {
results, err := dirFunc(Helper{
Dispenser: caddyfile.NewDispenser(segment),
options: options,
warnings: &warnings,
matcherDefs: matcherDefs,
parentBlock: sb.block,
@@ -163,7 +172,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
// now for the TLS app! (TODO: refactor into own func)
tlsApp := caddytls.TLS{Certificates: make(map[string]json.RawMessage)}
for _, p := range pairings {
for _, sblock := range p.serverBlocks {
for i, sblock := range p.serverBlocks {
// tls automation policies
if mmVals, ok := sblock.pile["tls.automation_manager"]; ok {
for _, mmVal := range mmVals {
@@ -172,10 +181,19 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
if err != nil {
return nil, warnings, err
}
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{
Hosts: sblockHosts,
ManagementRaw: caddyconfig.JSONModuleObject(mm, "module", mm.(caddy.Module).CaddyModule().ID(), &warnings),
})
if len(sblockHosts) > 0 {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{
Hosts: sblockHosts,
ManagementRaw: caddyconfig.JSONModuleObject(mm, "module", mm.(caddy.Module).CaddyModule().ID(), &warnings),
})
} else {
warnings = append(warnings, caddyconfig.Warning{
Message: fmt.Sprintf("Server block %d %v has no names that qualify for automatic HTTPS, so no TLS automation policy will be added.", i, sblock.block.Keys),
})
}
}
}
@@ -189,8 +207,31 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
}
}
}
// consolidate automation policies that are the exact same
tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
// if global ACME CA or email were set, append a catch-all automation
// policy that ensures they will be used if no tls directive was used
acmeCA, hasACMECA := options["acme_ca"]
email, hasEmail := options["email"]
if hasACMECA || hasEmail {
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
if !hasACMECA {
acmeCA = ""
}
if !hasEmail {
email = ""
}
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{
ManagementRaw: caddyconfig.JSONModuleObject(caddytls.ACMEManagerMaker{
CA: acmeCA.(string),
Email: email.(string),
}, "module", "acme", &warnings),
})
}
if tlsApp.Automation != nil {
// consolidate automation policies that are the exact same
tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
}
// if experimental HTTP/3 is enabled, enable it on each server
if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
@@ -204,9 +245,15 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
if !reflect.DeepEqual(httpApp, caddyhttp.App{}) {
cfg.AppsRaw["http"] = caddyconfig.JSON(httpApp, &warnings)
}
if !reflect.DeepEqual(tlsApp, caddytls.TLS{}) {
if !reflect.DeepEqual(tlsApp, caddytls.TLS{Certificates: make(map[string]json.RawMessage)}) {
cfg.AppsRaw["tls"] = caddyconfig.JSON(tlsApp, &warnings)
}
if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok {
cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr,
"module",
storageCvtr.(caddy.Module).CaddyModule().ID(),
&warnings)
}
return cfg, warnings, nil
}
@@ -266,7 +313,7 @@ func (st *ServerType) serversFromPairings(
if err != nil {
return nil, err
}
if _, ok := sblock.pile["tls.off"]; ok {
if _, ok := sblock.pile["tls.off"]; ok && len(autoHTTPSQualifiedHosts) > 0 {
// tls off: disable TLS (and automatic HTTPS) for server block's names
if srv.AutoHTTPS == nil {
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
@@ -406,10 +453,10 @@ func consolidateAutomationPolicies(aps []caddytls.AutomationPolicy) []caddytls.A
}
if reflect.DeepEqual(aps[i].ManagementRaw, aps[j].ManagementRaw) {
aps[i].Hosts = append(aps[i].Hosts, aps[j].Hosts...)
aps = append(aps[:j], aps[j+1:]...)
i--
break
}
aps = append(aps[:j], aps[j+1:]...)
i--
break
}
}
return aps
@@ -522,6 +569,37 @@ func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([
return matcherSetsEnc, nil
}
func parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]map[string]json.RawMessage, error) {
matchers := make(map[string]map[string]json.RawMessage)
for d.Next() {
definitionName := d.Val()
for nesting := d.Nesting(); d.NextBlock(nesting); {
matcherName := d.Val()
mod, err := caddy.GetModule("http.matchers." + matcherName)
if err != nil {
return nil, fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
}
err = unm.UnmarshalCaddyfile(d.NewFromNextTokens())
if err != nil {
return nil, err
}
rm, ok := unm.(caddyhttp.RequestMatcher)
if !ok {
return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
}
if _, ok := matchers[definitionName]; !ok {
matchers[definitionName] = make(map[string]json.RawMessage)
}
matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
}
}
return matchers, nil
}
func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (map[string]json.RawMessage, error) {
msEncoded := make(map[string]json.RawMessage)
for matcherName, val := range matchers {
+58 -4
View File
@@ -15,12 +15,14 @@
package httpcaddyfile
import (
"fmt"
"strconv"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func parseHTTPPort(d *caddyfile.Dispenser) (int, error) {
func parseOptHTTPPort(d *caddyfile.Dispenser) (int, error) {
var httpPort int
for d.Next() {
var httpPortStr string
@@ -36,7 +38,7 @@ func parseHTTPPort(d *caddyfile.Dispenser) (int, error) {
return httpPort, nil
}
func parseHTTPSPort(d *caddyfile.Dispenser) (int, error) {
func parseOptHTTPSPort(d *caddyfile.Dispenser) (int, error) {
var httpsPort int
for d.Next() {
var httpsPortStr string
@@ -52,11 +54,11 @@ func parseHTTPSPort(d *caddyfile.Dispenser) (int, error) {
return httpsPort, nil
}
func parseExperimentalHTTP3(d *caddyfile.Dispenser) (bool, error) {
func parseOptExperimentalHTTP3(d *caddyfile.Dispenser) (bool, error) {
return true, nil
}
func parseHandlerOrder(d *caddyfile.Dispenser) ([]string, error) {
func parseOptHandlerOrder(d *caddyfile.Dispenser) ([]string, error) {
if !d.Next() {
return nil, d.ArgErr()
}
@@ -78,3 +80,55 @@ func parseHandlerOrder(d *caddyfile.Dispenser) ([]string, error) {
}
return order, 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, fmt.Errorf("getting storage module '%s': %v", modName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, fmt.Errorf("storage module '%s' is not a Caddyfile unmarshaler", mod.Name)
}
err = unm.UnmarshalCaddyfile(d.NewFromNextTokens())
if err != nil {
return nil, err
}
storage, ok := unm.(caddy.StorageConverter)
if !ok {
return nil, fmt.Errorf("module %s is not a StorageConverter", mod.Name)
}
return storage, nil
}
func parseOptACMECA(d *caddyfile.Dispenser) (string, error) {
d.Next() // consume parameter name
if !d.Next() {
return "", d.ArgErr()
}
val := d.Val()
if d.Next() {
return "", d.ArgErr()
}
return val, nil
}
func parseOptEmail(d *caddyfile.Dispenser) (string, error) {
d.Next() // consume parameter name
if !d.Next() {
return "", d.ArgErr()
}
val := d.Val()
if d.Next() {
return "", d.ArgErr()
}
return val, nil
}
+8
View File
@@ -19,21 +19,29 @@ import (
// this is where modules get plugged in
_ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
_ "github.com/caddyserver/caddy/v2/caddyconfig/json5"
_ "github.com/caddyserver/caddy/v2/caddyconfig/jsonc"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/caddyauth"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/brotli"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/gzip"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/zstd"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/httpcache"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/markdown"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/requestbody"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/fastcgi"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/starlarkmw"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
_ "github.com/caddyserver/caddy/v2/modules/caddytls"
_ "github.com/caddyserver/caddy/v2/modules/caddytls/distributedstek"
_ "github.com/caddyserver/caddy/v2/modules/caddytls/standardstek"
_ "github.com/caddyserver/caddy/v2/modules/filestorage"
_ "github.com/caddyserver/caddy/v2/modules/logging"
)
func main() {
+523
View File
@@ -0,0 +1,523 @@
// 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/debug"
"sort"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/keybase/go-ps"
"github.com/mholt/certmagic"
)
func cmdStart(fl Flags) (int, error) {
startCmdConfigFlag := fl.String("config")
startCmdConfigAdapterFlag := fl.String("adapter")
// 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)
}
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 sychronously 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)\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")
runCmdPrintEnvFlag := fl.Bool("environ")
runCmdPingbackFlag := fl.String("pingback")
// if we are supposed to print the environment, do that first
if runCmdPrintEnvFlag {
printEnvironment()
}
// get the config in caddy's native format
config, err := loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// set a fitting User-Agent for ACME requests
goModule := caddy.GoModule()
cleanModVersion := strings.TrimPrefix(goModule.Version, "v")
certmagic.UserAgent = "Caddy/" + cleanModVersion
// start the admin endpoint along with any initial config
err = caddy.StartAdmin(config)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("starting caddy administration endpoint: %v", err)
}
defer caddy.StopAdmin()
// 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)
}
}
select {}
}
func cmdStop(_ Flags) (int, error) {
processList, err := ps.Processes()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("listing processes: %v", err)
}
thisProcName := getProcessName()
var found bool
for _, p := range processList {
// the process we're looking for should have the same name but different PID
if p.Executable() == thisProcName && p.Pid() != os.Getpid() {
found = true
fmt.Printf("pid=%d\n", p.Pid())
if err := gracefullyStopProcess(p.Pid()); err != nil {
return caddy.ExitCodeFailedStartup, err
}
}
}
if !found {
return caddy.ExitCodeFailedStartup, fmt.Errorf("Caddy is not running")
}
fmt.Println(" success")
return caddy.ExitCodeSuccess, nil
}
func cmdReload(fl Flags) (int, error) {
reloadCmdConfigFlag := fl.String("config")
reloadCmdConfigAdapterFlag := fl.String("adapter")
reloadCmdAddrFlag := fl.String("address")
// a configuration is required
if reloadCmdConfigFlag == "" {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("no configuration to load (use --config)")
}
// get the config in caddy's native format
config, err := loadConfig(reloadCmdConfigFlag, reloadCmdConfigAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// get the address of the admin listener and craft endpoint URL
adminAddr := reloadCmdAddrFlag
if adminAddr == "" {
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
}
adminEndpoint := fmt.Sprintf("http://%s/load", adminAddr)
// send the configuration to the instance
resp, err := http.Post(adminEndpoint, "application/json", bytes.NewReader(config))
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("sending configuration to instance: %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 caddy.ExitCodeFailedStartup,
fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
}
return caddy.ExitCodeFailedStartup,
fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
}
return caddy.ExitCodeSuccess, nil
}
func cmdVersion(_ Flags) (int, error) {
goModule := caddy.GoModule()
if goModule.Sum != "" {
// a build with a known version will also have a checksum
fmt.Printf("%s %s\n", goModule.Version, goModule.Sum)
} else {
fmt.Println(goModule.Version)
}
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 _, modName := range caddy.Modules() {
modInfo, err := caddy.GetModule(modName)
if err != nil {
// that's weird
fmt.Println(modName)
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 name instead
if matched == nil {
fmt.Println(modName)
continue
}
fmt.Printf("%s %s\n", modName, 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 adaptCmdAdapterFlag == "" || adaptCmdInputFlag == "" {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("--adapter and --config flags are required")
}
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"
}
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 := ioutil.ReadFile(validateCmdConfigFlag)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading input file: %v", err)
}
if validateCmdAdapterFlag != "" {
cfgAdapter := caddyconfig.GetAdapter(validateCmdAdapterFlag)
if cfgAdapter == nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("unrecognized config adapter: %s", validateCmdAdapterFlag)
}
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, nil)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// print warnings to stderr
for _, warn := range warnings {
msg := warn.Message
if warn.Directive != "" {
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
}
fmt.Fprintf(os.Stderr, "[WARNING][%s] %s:%d: %s\n", validateCmdAdapterFlag, warn.File, warn.Line, msg)
}
input = adaptedConfig
}
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 cmdHelp(fl Flags) (int, error) {
const fullDocs = `Full documentation is available at:
https://github.com/caddyserver/caddy/wiki/v2:-Documentation`
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
}
+225 -317
View File
@@ -15,343 +15,251 @@
package caddycmd
import (
"bytes"
"crypto/rand"
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"os/exec"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/mholt/certmagic"
"github.com/mitchellh/go-ps"
"regexp"
)
func cmdStart() (int, error) {
startCmd := flag.NewFlagSet("start", flag.ExitOnError)
startCmdConfigFlag := startCmd.String("config", "", "Configuration file")
startCmdConfigAdapterFlag := startCmd.String("config-adapter", "", "Name of config adapter to apply")
startCmd.Parse(os.Args[2:])
// 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
// 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()
// Run is a function that executes a subcommand using
// the parsed flags. It returns an exit code and any
// associated error.
// Required.
Func CommandFunc
// 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, "--config-adapter", *startCmdConfigAdapterFlag)
}
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
// 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
// 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)
}
// Short is a one-line message explaining what the
// command does. Should not end with punctuation.
// Required.
Short string
// begin writing the confirmation bytes to the child's
// stdin; use a goroutine since the child hasn't been
// started yet, and writing sychronously would result
// in a deadlock
go func() {
stdinpipe.Write(expect)
stdinpipe.Close()
}()
// Long is the full help text shown to the user.
// Will be trimmed of whitespace on both ends before
// being printed.
Long string
// 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.Println("Successfully started Caddy")
case err := <-exit:
return caddy.ExitCodeFailedStartup,
fmt.Errorf("caddy process exited with error: %v", err)
}
return caddy.ExitCodeSuccess, nil
// Flags is the flagset for command.
Flags *flag.FlagSet
}
func cmdRun() (int, error) {
runCmd := flag.NewFlagSet("run", flag.ExitOnError)
runCmdConfigFlag := runCmd.String("config", "", "Configuration file")
runCmdConfigAdapterFlag := runCmd.String("config-adapter", "", "Name of config adapter to apply")
runCmdPrintEnvFlag := runCmd.Bool("print-env", false, "Print environment")
runCmdPingbackFlag := runCmd.String("pingback", "", "Echo confirmation bytes to this address on success")
runCmd.Parse(os.Args[2:])
// 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)
// if we are supposed to print the environment, do that first
if *runCmdPrintEnvFlag {
exitCode, err := cmdEnviron()
if err != nil {
return exitCode, err
}
}
var commands = make(map[string]Command)
// get the config in caddy's native format
config, err := loadConfig(*runCmdConfigFlag, *runCmdConfigAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
func init() {
RegisterCommand(Command{
Name: "help",
Func: cmdHelp,
Usage: "<command>",
Short: "Shows help for a Caddy subcommand",
})
// set a fitting User-Agent for ACME requests
goModule := caddy.GoModule()
cleanModVersion := strings.TrimPrefix(goModule.Version, "v")
certmagic.UserAgent = "Caddy/" + cleanModVersion
RegisterCommand(Command{
Name: "start",
Func: cmdStart,
Usage: "[--config <path> [[--adapter <name>]]",
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.
// start the admin endpoint along with any initial config
err = caddy.StartAdmin(config)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("starting caddy administration endpoint: %v", err)
}
defer caddy.StopAdmin()
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")
return fs
}(),
})
// 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)
}
}
RegisterCommand(Command{
Name: "run",
Func: cmdRun,
Usage: "[--config <path> [--adapter <name>]] [--environ]",
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.`,
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.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.
On Windows, this stop is forceful and Caddy will not have an opportunity to
clean up any active locks; for a graceful shutdown on Windows, use Ctrl+C
or the /stop API endpoint.
Note: this will stop any process named the same as the executable (os.Args[0]).`,
})
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: "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
}(),
})
select {}
}
func cmdStop() (int, error) {
processList, err := ps.Processes()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("listing processes: %v", err)
// 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.
func RegisterCommand(cmd Command) {
if cmd.Name == "" {
panic("command name is required")
}
thisProcName := getProcessName()
var found bool
for _, p := range processList {
// the process we're looking for should have the same name but different PID
if p.Executable() == thisProcName && p.Pid() != os.Getpid() {
found = true
fmt.Printf("pid=%d\n", p.Pid())
if err := gracefullyStopProcess(p.Pid()); err != nil {
return caddy.ExitCodeFailedStartup, err
}
}
if cmd.Func == nil {
panic("command function missing")
}
if !found {
return caddy.ExitCodeFailedStartup, fmt.Errorf("Caddy is not running")
if cmd.Short == "" {
panic("command short string is required")
}
fmt.Println(" success")
return caddy.ExitCodeSuccess, nil
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
}
func cmdReload() (int, error) {
reloadCmd := flag.NewFlagSet("load", flag.ExitOnError)
reloadCmdConfigFlag := reloadCmd.String("config", "", "Configuration file")
reloadCmdConfigAdapterFlag := reloadCmd.String("config-adapter", "", "Name of config adapter to apply")
reloadCmdAddrFlag := reloadCmd.String("address", "", "Address of the administration listener, if different from config")
reloadCmd.Parse(os.Args[2:])
// a configuration is required
if *reloadCmdConfigFlag == "" {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("no configuration to load (use --config)")
}
// get the config in caddy's native format
config, err := loadConfig(*reloadCmdConfigFlag, *reloadCmdConfigAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// get the address of the admin listener and craft endpoint URL
adminAddr := *reloadCmdAddrFlag
if adminAddr == "" {
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
}
adminEndpoint := fmt.Sprintf("http://%s/load", adminAddr)
// send the configuration to the instance
resp, err := http.Post(adminEndpoint, "application/json", bytes.NewReader(config))
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("sending configuration to instance: %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 caddy.ExitCodeFailedStartup,
fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
}
return caddy.ExitCodeFailedStartup,
fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
}
return caddy.ExitCodeSuccess, nil
}
func cmdVersion() (int, error) {
goModule := caddy.GoModule()
if goModule.Sum != "" {
// a build with a known version will also have a checksum
fmt.Printf("%s %s\n", goModule.Version, goModule.Sum)
} else {
fmt.Println(goModule.Version)
}
return caddy.ExitCodeSuccess, nil
}
func cmdListModules() (int, error) {
for _, m := range caddy.Modules() {
fmt.Println(m)
}
return caddy.ExitCodeSuccess, nil
}
func cmdEnviron() (int, error) {
for _, v := range os.Environ() {
fmt.Println(v)
}
return caddy.ExitCodeSuccess, nil
}
func cmdAdaptConfig() (int, error) {
adaptCmd := flag.NewFlagSet("adapt", flag.ExitOnError)
adaptCmdAdapterFlag := adaptCmd.String("adapter", "", "Name of config adapter")
adaptCmdInputFlag := adaptCmd.String("input", "", "Configuration file to adapt")
adaptCmdPrettyFlag := adaptCmd.Bool("pretty", false, "Format the output for human readability")
adaptCmd.Parse(os.Args[2:])
if *adaptCmdAdapterFlag == "" || *adaptCmdInputFlag == "" {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("usage: caddy adapt-config --adapter <name> --input <file>")
}
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"
}
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)
}
log.Printf("[WARNING][%s] %s:%d: %s", *adaptCmdAdapterFlag, warn.File, warn.Line, msg)
}
// print result to stdout
fmt.Println(string(adaptedConfig))
return caddy.ExitCodeSuccess, nil
}
var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)
+102 -32
View File
@@ -20,9 +20,11 @@ import (
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"strconv"
"strings"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -33,45 +35,43 @@ import (
func Main() {
caddy.TrapSignals()
if len(os.Args) < 2 {
fmt.Println(usageString())
return
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")
}
subcommand, ok := commands[os.Args[1]]
subcommandName := os.Args[1]
subcommand, ok := commands[subcommandName]
if !ok {
fmt.Printf("%q is not a valid command\n", os.Args[1])
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)
}
if exitCode, err := subcommand(); err != nil {
log.Println(err)
os.Exit(exitCode)
fs := subcommand.Flags
if fs == nil {
fs = flag.NewFlagSet(subcommand.Name, flag.ExitOnError)
}
}
// commandFunc is a function that executes
// a subcommand. It returns an exit code and
// any associated error.
type commandFunc func() (int, error)
err := fs.Parse(os.Args[2:])
if err != nil {
fmt.Println(err)
os.Exit(caddy.ExitCodeFailedStartup)
}
var commands = map[string]commandFunc{
"start": cmdStart,
"run": cmdRun,
"stop": cmdStop,
"reload": cmdReload,
"version": cmdVersion,
"list-modules": cmdListModules,
"environ": cmdEnviron,
"adapt-config": cmdAdaptConfig,
}
exitCode, err := subcommand.Func(Flags{fs})
if err != nil {
fmt.Printf("%s: %v\n", subcommand.Name, err)
}
func usageString() string {
buf := new(bytes.Buffer)
buf.WriteString("usage: caddy <command> [<args>]")
flag.CommandLine.SetOutput(buf)
flag.CommandLine.PrintDefaults()
return buf.String()
os.Exit(exitCode)
}
// handlePingbackConn reads from conn and ensures it matches
@@ -117,7 +117,6 @@ func loadConfig(configFile, adapterName string) ([]byte, error) {
if os.IsNotExist(err) {
// okay, no default Caddyfile; pretend like this never happened
cfgAdapter = nil
err = nil
} else if err != nil {
// default Caddyfile exists, but error reading it
return nil, fmt.Errorf("reading default Caddyfile: %v", err)
@@ -149,10 +148,81 @@ func loadConfig(configFile, adapterName string) ([]byte, error) {
if warn.Directive != "" {
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
}
fmt.Printf("[WARNING][%s] %s:%d: %s", adapterName, warn.File, warn.Line, msg)
fmt.Printf("[WARNING][%s] %s:%d: %s\n", adapterName, warn.File, warn.Line, msg)
}
config = adaptedConfig
}
return config, nil
}
// 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() {
for _, v := range os.Environ() {
fmt.Println(v)
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ import (
)
func gracefullyStopProcess(pid int) error {
fmt.Printf("Graceful stop...")
fmt.Printf("Graceful stop...\n")
err := syscall.Kill(pid, syscall.SIGINT)
if err != nil {
return fmt.Errorf("kill: %v", err)
+1 -1
View File
@@ -23,7 +23,7 @@ import (
)
func gracefullyStopProcess(pid int) error {
fmt.Printf("Forceful Stop...")
fmt.Printf("Forceful Stop...\n")
// 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 {
+16 -1
View File
@@ -22,6 +22,7 @@ import (
"reflect"
"github.com/mholt/certmagic"
"go.uber.org/zap"
)
// Context is a type which defines the lifetime of modules that
@@ -30,7 +31,7 @@ import (
// with care and wrapped with derivation functions from the
// standard context package only if you don't need the Caddy
// specific features. These contexts are cancelled when the
// lifetime of the modules loaded from it are over.
// lifetime of the modules loaded from it is over.
//
// Use NewContext() to get a valid value (but most modules will
// not actually need to do this).
@@ -131,6 +132,14 @@ func (ctx Context) LoadModule(name string, rawMsg json.RawMessage) (interface{},
if prov, ok := val.(Provisioner); ok {
err := prov.Provision(ctx)
if err != nil {
// incomplete provisioning could have left state
// dangling, so make sure it gets cleaned up
if cleanerUpper, ok := val.(CleanerUpper); ok {
err2 := cleanerUpper.Cleanup()
if err2 != nil {
err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2)
}
}
return nil, fmt.Errorf("provision %s: %v", mod.Name, err)
}
}
@@ -138,6 +147,7 @@ func (ctx Context) LoadModule(name string, rawMsg json.RawMessage) (interface{},
if validator, ok := val.(Validator); ok {
err := validator.Validate()
if err != nil {
// since the module was already provisioned, make sure we clean up
if cleanerUpper, ok := val.(CleanerUpper); ok {
err2 := cleanerUpper.Cleanup()
if err2 != nil {
@@ -197,3 +207,8 @@ func (ctx Context) App(name string) (interface{}, error) {
func (ctx Context) Storage() certmagic.Storage {
return ctx.cfg.storage
}
// Logger returns a logger that can be used by mod.
func (ctx Context) Logger(mod Module) *zap.Logger {
return ctx.cfg.Logging.Logger(mod)
}
+358
View File
@@ -0,0 +1,358 @@
// 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"
"encoding/json"
"fmt"
"log"
"net/http"
"path"
"strconv"
"strings"
"sync"
)
func init() {
RegisterModule(router{})
}
type router []AdminRoute
// CaddyModule returns the Caddy module information.
func (router) CaddyModule() ModuleInfo {
return ModuleInfo{
Name: "admin.routers.dynamic_config",
New: func() Module {
return router{
{
Pattern: "/" + rawConfigKey + "/",
Handler: http.HandlerFunc(handleConfig),
},
{
Pattern: "/id/",
Handler: http.HandlerFunc(handleConfigID),
},
}
},
}
}
func (r router) Routes() []AdminRoute { return r }
// handleConfig handles config changes or exports according to r.
// This function is safe for concurrent use.
func handleConfig(w http.ResponseWriter, r *http.Request) {
rawCfgMu.Lock()
defer rawCfgMu.Unlock()
unsyncedHandleConfig(w, r)
}
// handleConfigID accesses the config through a user-assigned ID
// that is mapped to its full/expanded path in the JSON structure.
// It is the same as handleConfig except it replaces the ID in
// the request path with the full, expanded URL path.
// This function is safe for concurrent use.
func handleConfigID(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(r.URL.Path, "/")
if len(parts) < 3 || parts[2] == "" {
http.Error(w, "request path is missing object ID", http.StatusBadRequest)
return
}
id := parts[2]
rawCfgMu.Lock()
defer rawCfgMu.Unlock()
// map the ID to the expanded path
expanded, ok := rawCfgIndex[id]
if !ok {
http.Error(w, "unknown object ID: "+id, http.StatusBadRequest)
return
}
// piece the full URL path back together
parts = append([]string{expanded}, parts[3:]...)
r.URL.Path = path.Join(parts...)
unsyncedHandleConfig(w, r)
}
// configIndex recurisvely 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; use rawCfgMu.
func configIndex(ptr interface{}, configPath string, index map[string]string) error {
switch val := ptr.(type) {
case map[string]interface{}:
for k, v := range val {
if k == "@id" {
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: @id field must be a string or number", configPath)
}
delete(val, "@id") // field is no longer needed, and will break config if not removed
continue
}
// traverse this object property recursively
err := configIndex(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 := configIndex(val[i], path.Join(configPath, strconv.Itoa(i)), index)
if err != nil {
return err
}
}
}
return nil
}
// unsycnedHandleConfig handles config accesses without a lock
// on rawCfgMu. This is NOT safe for concurrent use, so be sure
// to acquire a lock on rawCfgMu before calling this.
func unsyncedHandleConfig(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
default:
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
// perform the mutation with our decoded representation
// (the map), which may change pointers deep within it
err := mutateConfig(w, r)
if err != nil {
http.Error(w, "mutating config: "+err.Error(), http.StatusBadRequest)
return
}
if r.Method != http.MethodGet {
// find any IDs in this config and index them
idx := make(map[string]string)
err = configIndex(rawCfg[rawConfigKey], "/config", idx)
if err != nil {
http.Error(w, "indexing config: "+err.Error(), http.StatusInternalServerError)
return
}
// the mutation is complete, so encode the entire config as JSON
newCfg, err := json.Marshal(rawCfg[rawConfigKey])
if err != nil {
http.Error(w, "encoding new config: "+err.Error(), http.StatusBadRequest)
return
}
// if nothing changed, no need to do a whole reload unless the client forces it
if r.Header.Get("Cache-Control") != "must-revalidate" && bytes.Equal(rawCfgJSON, newCfg) {
log.Printf("[ADMIN][INFO] Config is unchanged")
return
}
// load this new config; if it fails, we need to revert to
// our old representation of caddy's actual config
err = Load(bytes.NewReader(newCfg))
if err != nil {
// 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
// report error
log.Printf("[ADMIN][ERROR] loading config: %v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 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
}
}
// mutateConfig changes the rawCfg according to r. It is NOT
// safe for concurrent use; use rawCfgMu. If the request's
// method is GET, the config will not be changed.
func mutateConfig(w http.ResponseWriter, r *http.Request) error {
var err error
var val interface{}
// if there is a request body, make sure we recognize its content-type and decode it
if r.Method != http.MethodGet && r.Method != http.MethodDelete {
if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
return fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct)
}
err = json.NewDecoder(r.Body).Decode(&val)
if err != nil {
return fmt.Errorf("decoding request body: %v", err)
}
}
buf := new(bytes.Buffer)
enc := json.NewEncoder(buf)
cleanPath := strings.Trim(r.URL.Path, "/")
if cleanPath == "" {
return fmt.Errorf("no traversable path")
}
parts := strings.Split(cleanPath, "/")
if len(parts) == 0 {
return fmt.Errorf("path missing")
}
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 r.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",
r.URL.Path, idxStr, err)
}
if idx < 0 || idx >= len(arr) {
return fmt.Errorf("[%s] array index out of bounds: %s", r.URL.Path, idxStr)
}
}
switch r.Method {
case http.MethodGet:
err = enc.Encode(arr[idx])
if err != nil {
return fmt.Errorf("encoding config: %v", err)
}
case http.MethodPost:
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", r.Method)
}
break traverseLoop
}
if i == len(parts)-1 {
switch r.Method {
case http.MethodGet:
err = enc.Encode(v[part])
if err != nil {
return fmt.Errorf("encoding config: %v", err)
}
case http.MethodPost:
if arr, ok := v[part].([]interface{}); ok {
// if the part is an existing list, POST appends to it
// TODO: Do we ever reach this point, since we handle arrays
// separately above?
v[part] = append(arr, val)
} else {
// otherwise, it simply sets the value
v[part] = val
}
case http.MethodPut:
if _, ok := v[part]; ok {
return fmt.Errorf("[%s] key already exists: %s", r.URL.Path, part)
}
v[part] = val
case http.MethodPatch:
if _, ok := v[part]; !ok {
return fmt.Errorf("[%s] key does not exist: %s", r.URL.Path, part)
}
v[part] = val
case http.MethodDelete:
delete(v, part)
default:
return fmt.Errorf("unrecognized method %s", r.Method)
}
} else {
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 path: %s", parts[:i+1])
}
}
if r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.Write(buf.Bytes())
}
return nil
}
var (
// 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 []byte // keeping the encoded form avoids an extra Marshal on changes
rawCfgIndex map[string]string // map of user-assigned ID to expanded path
rawCfgMu sync.Mutex // protects rawCfg, rawCfgJSON, and rawCfgIndex
)
const rawConfigKey = "config"
+123
View File
@@ -0,0 +1,123 @@
// 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"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
)
func TestMutateConfig(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"]}`,
},
} {
req, err := http.NewRequest(tc.method, rawConfigKey+tc.path, strings.NewReader(tc.payload))
if err != nil {
t.Fatalf("Test %d: making test request: %v", i, err)
}
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
err = mutateConfig(w, req)
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)
}
if tc.shouldErr && w.Code == http.StatusOK {
t.Fatalf("Test %d: Expected error, but got HTTP %d: %s",
i, w.Code, w.Body.String())
}
if !tc.shouldErr && w.Code != http.StatusOK {
t.Fatalf("Test %d: Should not have errored, but got HTTP %d: %s",
i, w.Code, w.Body.String())
}
// 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])
}
}
}
+23 -19
View File
@@ -3,28 +3,32 @@ module github.com/caddyserver/caddy/v2
go 1.13
require (
github.com/DataDog/zstd v1.4.1 // indirect
github.com/Masterminds/goutils v1.1.0 // indirect
github.com/Masterminds/semver v1.4.2 // indirect
github.com/Masterminds/sprig v2.20.0+incompatible
github.com/andybalholm/brotli v0.0.0-20190704151324-71eb68cc467c
github.com/Masterminds/sprig/v3 v3.0.0
github.com/andybalholm/brotli v0.0.0-20190821151343-b60f0d972eeb
github.com/dustin/go-humanize v1.0.0
github.com/go-acme/lego/v3 v3.0.2
github.com/google/go-cmp v0.3.1 // indirect
github.com/huandu/xstrings v1.2.0 // indirect
github.com/dvyukov/go-fuzz v0.0.0-20191022152526-8cb203812681 // indirect
github.com/go-acme/lego/v3 v3.1.0
github.com/golang/groupcache v0.0.0-20191002201903-404acd9df4cc
github.com/ilibs/json5 v1.0.1
github.com/imdario/mergo v0.3.7 // indirect
github.com/klauspost/compress v1.7.1-0.20190613161414-0b31f265a57b
github.com/imdario/mergo v0.3.8 // indirect
github.com/jsternberg/zap-logfmt v1.2.0
github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19
github.com/klauspost/compress v1.8.6
github.com/klauspost/cpuid v1.2.1
github.com/lucas-clemente/quic-go v0.7.1-0.20190908032346-fc962d18373a
github.com/mholt/certmagic v0.7.1
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20190902132743-e4903c4dea48
github.com/rs/cors v1.6.0
github.com/lucas-clemente/quic-go v0.12.1
github.com/mholt/certmagic v0.8.3
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20190906142622-1265e9b150c6
github.com/onsi/ginkgo v1.8.0 // indirect
github.com/onsi/gomega v1.5.0 // indirect
github.com/rs/cors v1.7.0
github.com/russross/blackfriday/v2 v2.0.1
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/starlight-go/starlight v0.0.0-20181207205707-b06f321544f3
go.starlark.net v0.0.0-20190604130855-6ddc71c0ba77
golang.org/x/net v0.0.0-20190603091049-60506f45cf65
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
github.com/vulcand/oxy v1.0.0
go.starlark.net v0.0.0-20190919145610-979af19b165c
go.uber.org/multierr v1.2.0 // indirect
go.uber.org/zap v1.10.0
golang.org/x/crypto v0.0.0-20191010185427-af544f31c8ac
golang.org/x/net v0.0.0-20191009170851-d66e71096ffb
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)
+110 -56
View File
@@ -15,40 +15,41 @@ github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocm
github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.20.0+incompatible h1:dJTKKuUkYW3RMFdQFXPU/s6hg10RgctmTjRcbZ98Ap8=
github.com/Masterminds/sprig v2.20.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/Masterminds/semver/v3 v3.0.1 h1:2kKm5lb7dKVrt5TYUiAavE6oFc1cFT0057UVGT+JqLk=
github.com/Masterminds/semver/v3 v3.0.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/sprig/v3 v3.0.0 h1:KSQz7Nb08/3VU9E4ns29dDxcczhOD1q7O1UfM4G3t3g=
github.com/Masterminds/sprig/v3 v3.0.0/go.mod h1:NEUY/Qq8Gdm2xgYA+NwJM6wmfdRV9xkh8h/Rld20R0U=
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.0/go.mod h1:zpDJeKyp9ScW4NNrbdr+Eyxvry3ilGPewKoXw3XGN1k=
github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75 h1:3ILjVyslFbc4jl1w5TWuvvslFD/nDfR2H8tVaMVLrEY=
github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75/go.mod h1:uAXEEpARkRhCZfEvy/y0Jcc888f9tHCc1W7/UeEtreE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190808125512-07798873deee/go.mod h1:myCDvQSzCW+wB1WAlocEru4wMGJxy+vlxHdhegi1CDQ=
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/andybalholm/brotli v0.0.0-20190704151324-71eb68cc467c h1:pBKtfXLqKZ+GPHGjlBheGaXK2lddydUG3XhWGrYjxOA=
github.com/andybalholm/brotli v0.0.0-20190704151324-71eb68cc467c/go.mod h1:+lx6/Aqd1kLJ1GQfkvOnaZ1WGmLpMpbprPuIOOZX30U=
github.com/andybalholm/brotli v0.0.0-20190821151343-b60f0d972eeb h1:ZSlUsEd11C/uRzhZHOgANARJ03fkwmjJEa6g2Cqjlo4=
github.com/andybalholm/brotli v0.0.0-20190821151343-b60f0d972eeb/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/aws/aws-sdk-go v1.23.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c=
github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.10.0/go.mod h1:fOESqHl/jzAmCtEyjceLkw3v0rVjzl8V9iehxZPynXY=
github.com/cloudflare/cloudflare-go v0.10.2/go.mod h1:qhVI5MKwBGhdNU89ZRz2plgYutcJ5PCekLxXn56w6SY=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/cpu/goacmedns v0.0.1/go.mod h1:sesf/pNnCYwUevQEQfEwY0Y3DydlQWSGZbaMElOWxok=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -59,6 +60,8 @@ github.com/dnaeon/go-vcr v0.0.0-20180814043457-aafff18a5cc2/go.mod h1:aBB1+wY4s9
github.com/dnsimple/dnsimple-go v0.30.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dvyukov/go-fuzz v0.0.0-20191022152526-8cb203812681 h1:3WV5aRRj1ELP3RcLlBp/v0WJTuy47OQMkL9GIQq8QEE=
github.com/dvyukov/go-fuzz v0.0.0-20191022152526-8cb203812681/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
@@ -67,35 +70,37 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-acme/lego/v3 v3.0.2 h1:cnS+URiPzkt2pd7I2WlZtFyt2ihQ762nouBybY4djjw=
github.com/go-acme/lego/v3 v3.0.2/go.mod h1:sMoLjf8BUo4Jexg+6Xw5KeFx98KVZ7Nfczh9tzLyhJU=
github.com/go-acme/lego/v3 v3.1.0 h1:yanYFoYW8azFkCvJfIk7edWWfjkYkhDxe45ZsxoW4Xk=
github.com/go-acme/lego/v3 v3.1.0/go.mod h1:074uqt+JS6plx+c9Xaiz6+L+GBb+7itGtzfcDM2AhEE=
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-ini/ini v1.44.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721 h1:KRMr9A3qfbVM7iV/WcLY/rL5LICqwMHLhwRXKu99fXw=
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20191002201903-404acd9df4cc h1:55rEp52jU6bkyslZ1+C/7NGfpQsEc6pxGLAGDOctqbw=
github.com/golang/groupcache v0.0.0-20191002201903-404acd9df4cc/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
@@ -106,6 +111,9 @@ github.com/gophercloud/gophercloud v0.3.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEo
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gravitational/trace v0.0.0-20190726142706-a535a178675f/go.mod h1:RvdOUHE4SHqR3oXlFFKnGzms8a5dugHygGw1bqDstYI=
github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -118,21 +126,30 @@ github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
github.com/ilibs/json5 v1.0.1 h1:3e14wUQM8PyK6Hf1bM+zAQFxfG+N5oZj35x5vCNeQ58=
github.com/ilibs/json5 v1.0.1/go.mod h1:kXsGuzHMPuZZTN15l0IQzy5PR8DrDhPB24tFgwpdKME=
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jsternberg/zap-logfmt v1.2.0 h1:1v+PK4/B48cy8cfQbxL4FmmNZrjnIMr2BsnyEmXqv2o=
github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19 h1:WjT3fLi9n8YWh/Ih8Q1LHAPsTqGddPcHqscN+PJ3i68=
github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19/go.mod h1:hY+WOq6m2FpbvyrI93sMaypsttvaIL5nhVR92dTMUcQ=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.7.1-0.20190613161414-0b31f265a57b h1:LHpBANNM/cw1PAXJtKV9dgfp6ztOKfdGXcltGmqU9aE=
github.com/klauspost/compress v1.7.1-0.20190613161414-0b31f265a57b/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.8.6 h1:970MQcQdxX7hfgc/aqmB4a3grW0ivUVV6i1TLkP8CiE=
github.com/klauspost/compress v1.8.6/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/kolo/xmlrpc v0.0.0-20190717152603-07c4ee3fd181/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@@ -140,35 +157,43 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labbsr0x/bindman-dns-webhook v1.0.0/go.mod h1:pn4jcNjxSywRWDPDyGkFzgSnwty18OFdiUFc6S6fpgc=
github.com/labbsr0x/goh v0.0.0-20190417202808-8b16b4848295/go.mod h1:RBxeaayaaMmp7GxwHiKANjkg9e+rxjOm4mB5vD5rt/I=
github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA=
github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w=
github.com/linode/linodego v0.10.0/go.mod h1:cziNP7pbvE3mXIPneHj0oRY8L1WtGEIKlZ8LANE4eXA=
github.com/lucas-clemente/quic-go v0.7.1-0.20190908032346-fc962d18373a h1:4NDhBYEqjdNTQhlkLK8u4FGq1mMDQv4V4MLDSS5oCS0=
github.com/lucas-clemente/quic-go v0.7.1-0.20190908032346-fc962d18373a/go.mod h1:iP3S21xvg0qlsEAz+goZ/qfptsfhshjzkbhEK6Ka3Fg=
github.com/marten-seemann/chacha20 v0.2.0 h1:f40vqzzx+3GdOmzQoItkLX5WLvHgPgyYqFFIO5Gh4hQ=
github.com/marten-seemann/chacha20 v0.2.0/go.mod h1:HSdjFau7GzYRj+ahFNwsO3ouVJr1HFkWoEwNDb4TMtE=
github.com/liquidweb/liquidweb-go v1.6.0/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ=
github.com/lucas-clemente/quic-go v0.12.1 h1:BPITli+6KnKogtTxBk2aS4okr5dUHz2LtIDAP1b8UL4=
github.com/lucas-clemente/quic-go v0.12.1/go.mod h1:UXJJPE4RfFef/xPO5wQm0tITK8gNfqwTxjbE7s3Vb8s=
github.com/mailgun/minheap v0.0.0-20170619185613-3dbe6c6bf55f/go.mod h1:V3EvCedtJTvUYzJF2GZMRB0JMlai+6cBu3VCTQz33GQ=
github.com/mailgun/multibuf v0.0.0-20150714184110-565402cd71fb/go.mod h1:E0vRBBIQUHcRtmL/oR6w/jehh4FJqJFxe86gBnw9gXc=
github.com/mailgun/timetools v0.0.0-20141028012446-7e6055773c51 h1:Kg/NPZLLC3aAFr1YToMs98dbCdhootQ1hZIvZU28hAQ=
github.com/mailgun/timetools v0.0.0-20141028012446-7e6055773c51/go.mod h1:RYmqHbhWwIz3z9eVmQ2rx82rulEMG0t+Q1bzfc9DYN4=
github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f/go.mod h1:8heskWJ5c0v5J9WH89ADhyal1DOZcayll8fSbhB+/9A=
github.com/marten-seemann/qpack v0.1.0 h1:/0M7lkda/6mus9B8u34Asqm8ZhHAAt9Ho0vniNuVSVg=
github.com/marten-seemann/qpack v0.1.0/go.mod h1:LFt1NU/Ptjip0C2CPkhimBz5CGE3WGDAUWqna+CNTrI=
github.com/marten-seemann/qtls v0.4.0 h1:HM9ftULNeuhGiCliIfPKvp5VDJw6pvi/Ghq6PYf7B0E=
github.com/marten-seemann/qtls v0.4.0/go.mod h1:pxVXcHHw1pNIt8Qo0pwSYQEoZ8yYOOPXTCZLQQunvRc=
github.com/marten-seemann/qtls v0.3.2 h1:O7awy4bHEzSX/K3h+fZig3/Vo03s/RxlxgsAk9sYamI=
github.com/marten-seemann/qtls v0.3.2/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mholt/certmagic v0.7.1 h1:nbSSVwvlDE3+vttD/RBikBIkxrlKVkIQOz449gCrG5Q=
github.com/mholt/certmagic v0.7.1/go.mod h1:hqHzDsY32TwZpj/KswVylheSISjquF/eOVOaJTYV15w=
github.com/mholt/certmagic v0.8.3 h1:JOUiX9IAZbbgyjNP2GY6v/6lorH+9GkZsc7ktMpGCSo=
github.com/mholt/certmagic v0.8.3/go.mod h1:91uJzK5K8IWtYQqTi5R2tsxV1pCde+wdGfaRaOZi6aQ=
github.com/miekg/dns v1.1.15 h1:CSSIDtllwGLMoA6zjdKnaE6Tx6eVUxQ29LUgGetiDCI=
github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936 h1:kw1v0NlnN+GZcU8Ma8CLF2Zzgjfx95gs3/GN3vYAPpo=
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20190902132743-e4903c4dea48 h1:BM/fjd7MfvZuyoHXLv3YlWNIuNb47PLp6EyFBL1KIMg=
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20190902132743-e4903c4dea48/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20190906142622-1265e9b150c6 h1:EajWCEv0scxMWyMHWxJbFK70brsPIl4TLQJ0zaOeOiI=
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20190906142622-1265e9b150c6/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
@@ -179,28 +204,40 @@ github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXW
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/oracle/oci-go-sdk v7.0.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014/go.mod h1:joRatxRJaZBsY3JAOEMcoOp05CnZzsx4scTxi95DHyQ=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sacloud/libsacloud v1.26.1/go.mod h1:79ZwATmHLIFZIMd7sxA3LwzVy/B77uj3LDoToVTxDoQ=
@@ -208,21 +245,28 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/starlight-go/starlight v0.0.0-20181207205707-b06f321544f3 h1:/fBh1Ot84ILt/ociFHO98wJ9LxIMA3UG8B0unUJPFpY=
github.com/starlight-go/starlight v0.0.0-20181207205707-b06f321544f3/go.mod h1:pxOc2ZuBV+CNlQgzq/HJ9Z9G/eoEMHFeuGohOvva4Co=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY=
github.com/transip/gotransip v0.0.0-20190812104329-6d8d9179b66f/go.mod h1:i0f4R4o2HM0m3DZYQWsj6/MEowD57VzoH0v3d7igeFY=
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/vulcand/oxy v1.0.0 h1:7vL5/pjDFzHGbtBEhmlHITUi6KLH4xXTDF33/wrdRKw=
github.com/vulcand/oxy v1.0.0/go.mod h1:6EXgOAl6CRa46/2ZGcDJKf3ywJUp5WtT7vSlGSkvecI=
github.com/vulcand/predicate v1.1.0/go.mod h1:mlccC5IRBoc2cIFmCB8ZM62I3VDb6p2GXESMHa3CnZg=
github.com/vultr/govultr v0.1.4/go.mod h1:9H008Uxr/C4vFNGLqKx232C206GL0PBHzOP0809bGNA=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
@@ -230,21 +274,27 @@ github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4m
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.starlark.net v0.0.0-20190604130855-6ddc71c0ba77 h1:KPzANX1mXqnSWenqVWkSTsQWiaUSpTY5GyGZKI6lStw=
go.starlark.net v0.0.0-20190604130855-6ddc71c0ba77/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg=
go.starlark.net v0.0.0-20190919145610-979af19b165c h1:WR7X1xgXJlXhQBdorVc9Db3RhwG+J/kp6bLuMyJjfVw=
go.starlark.net v0.0.0-20190919145610-979af19b165c/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.2.0 h1:6I+W7f5VwC5SV9dNrZ3qXrDB9mD0dyGOi/ZJmYw03T4=
go.uber.org/multierr v1.2.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472 h1:Gv7RPwsi3eZ2Fgewe3CBsuOebPwO27PoXzRpJPsvSSM=
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 h1:0hQKqeLdqlt5iIwVOBErRisrHJAN57yOiPRQItI20fU=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191010185427-af544f31c8ac h1:/b4NMZurYfBIQyRMqaPGMDeUrSW6gU7/7Hv6owY1Vjk=
golang.org/x/crypto v0.0.0-20191010185427-af544f31c8ac/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -264,14 +314,16 @@ golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191009170851-d66e71096ffb h1:TR699M2v0qoKTOHxeLgp6zPqaQNs74f01a/ob9W0qko=
golang.org/x/net v0.0.0-20191009170851-d66e71096ffb/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
@@ -284,22 +336,22 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0 h1:xQwXv67TxFo9nC1GJFyab5eq/5B590r6RlnL/G8Sz7w=
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -324,7 +376,6 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -333,6 +384,9 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc h1:GAcf+t0o8gdJAdSFYdE9wChu4bIyguMVqz0RHiFL5VY=
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw=
gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+4 -3
View File
@@ -286,9 +286,10 @@ func JoinNetworkAddress(network, host, port string) string {
if network != "" {
a = network + "/"
}
a += host
if port != "" {
a += ":" + port
if host != "" && port == "" {
a += host
} else if port != "" {
a += net.JoinHostPort(host, port)
}
return a
}
+26
View File
@@ -0,0 +1,26 @@
// 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
// +build gofuzz_libfuzzer
package caddy
func FuzzParseNetworkAddress(data []byte) int {
_, _, err := ParseNetworkAddress(string(data))
if err != nil {
return 0
}
return 1
}
+4
View File
@@ -138,6 +138,10 @@ func TestJoinNetworkAddress(t *testing.T) {
network: "unix", host: "/foo/bar", port: "",
expect: "unix//foo/bar",
},
{
network: "", host: "::1", port: "1234",
expect: "[::1]:1234",
},
} {
actual := JoinNetworkAddress(tc.network, tc.host, tc.port)
if actual != tc.expect {
+611
View File
@@ -0,0 +1,611 @@
// 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"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strings"
"sync"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func init() {
RegisterModule(StdoutWriter{})
RegisterModule(StderrWriter{})
RegisterModule(DiscardWriter{})
}
// Logging facilitates logging within Caddy.
type Logging struct {
Sink *StandardLibLog `json:"sink,omitempty"`
Logs map[string]*CustomLog `json:"logs,omitempty"`
// a list of all keys for open writers; all writers
// that are opened to provision this logging config
// must have their keys added to this list so they
// can be closed when cleaning up
writerKeys []string
}
// openLogs sets up the config and opens all the configured writers.
// It closes its logs when ctx is cancelled, so it should clean up
// after itself.
func (logging *Logging) openLogs(ctx Context) error {
// make sure to deallocate resources when context is done
ctx.OnCancel(func() {
err := logging.closeLogs()
if err != nil {
Log().Error("closing logs", zap.Error(err))
}
})
// set up the "sink" log first (std lib's default global logger)
if logging.Sink != nil {
err := logging.Sink.provision(ctx, logging)
if err != nil {
return fmt.Errorf("setting up sink log: %v", err)
}
}
// as a special case, set up the default structured Caddy log next
if err := logging.setupNewDefault(ctx); err != nil {
return err
}
// then set up any other custom logs
for name, l := range logging.Logs {
// the default log is already set up
if name == "default" {
continue
}
err := l.provision(ctx, logging)
if err != nil {
return fmt.Errorf("setting up custom log '%s': %v", name, err)
}
// Any other logs that use the discard writer can be deleted
// entirely. This avoids encoding and processing of each
// log entry that would just be thrown away anyway. Notably,
// we do not reach this point for the default log, which MUST
// exist, otherwise core log emissions would panic because
// they use the Log() function directly which expects a non-nil
// logger. Even if we keep logs with a discard writer, they
// have a nop core, and keeping them at all seems unnecessary.
if _, ok := l.writerOpener.(*DiscardWriter); ok {
delete(logging.Logs, name)
continue
}
}
return nil
}
func (logging *Logging) setupNewDefault(ctx Context) error {
if logging.Logs == nil {
logging.Logs = make(map[string]*CustomLog)
}
// extract the user-defined default log, if any
newDefault := new(defaultCustomLog)
if userDefault, ok := logging.Logs["default"]; ok {
newDefault.CustomLog = userDefault
} else {
// if none, make one with our own default settings
var err error
newDefault, err = newDefaultProductionLog()
if err != nil {
return fmt.Errorf("setting up default Caddy log: %v", err)
}
logging.Logs["default"] = newDefault.CustomLog
}
// set up this new log
err := newDefault.CustomLog.provision(ctx, logging)
if err != nil {
return fmt.Errorf("setting up default log: %v", err)
}
newDefault.logger = zap.New(newDefault.CustomLog.core)
// redirect the default caddy logs
defaultLoggerMu.Lock()
oldDefault := defaultLogger
defaultLogger = newDefault
defaultLoggerMu.Unlock()
// if the new writer is different, indicate it in the logs for convenience
var newDefaultLogWriterKey, currentDefaultLogWriterKey string
var newDefaultLogWriterStr, currentDefaultLogWriterStr string
if newDefault.writerOpener != nil {
newDefaultLogWriterKey = newDefault.writerOpener.WriterKey()
newDefaultLogWriterStr = newDefault.writerOpener.String()
}
if oldDefault.writerOpener != nil {
currentDefaultLogWriterKey = oldDefault.writerOpener.WriterKey()
currentDefaultLogWriterStr = oldDefault.writerOpener.String()
}
if newDefaultLogWriterKey != currentDefaultLogWriterKey {
oldDefault.logger.Info("redirected default logger",
zap.String("from", currentDefaultLogWriterStr),
zap.String("to", newDefaultLogWriterStr),
)
}
return nil
}
// closeLogs cleans up resources allocated during openLogs.
// A successful call to openLogs calls this automatically
// when the context is cancelled.
func (logging *Logging) closeLogs() error {
for _, key := range logging.writerKeys {
_, err := writers.Delete(key)
if err != nil {
log.Printf("[ERROR] Closing log writer %v: %v", key, err)
}
}
return nil
}
// Logger returns a logger that is ready for the module to use.
func (logging *Logging) Logger(mod Module) *zap.Logger {
modName := mod.CaddyModule().Name
var cores []zapcore.Core
if logging != nil {
for _, l := range logging.Logs {
if l.matchesModule(modName) {
if len(l.Include) == 0 && len(l.Exclude) == 0 {
cores = append(cores, l.core)
continue
}
cores = append(cores, &filteringCore{Core: l.core, cl: l})
}
}
}
multiCore := zapcore.NewTee(cores...)
return zap.New(multiCore).Named(modName)
}
// openWriter opens a writer using opener, and returns true if
// the writer is new, or false if the writer already exists.
func (logging *Logging) openWriter(opener WriterOpener) (io.WriteCloser, bool, error) {
key := opener.WriterKey()
writer, loaded, err := writers.LoadOrNew(key, func() (Destructor, error) {
w, err := opener.OpenWriter()
return writerDestructor{w}, err
})
if err == nil {
logging.writerKeys = append(logging.writerKeys, key)
}
return writer.(io.WriteCloser), !loaded, err
}
// WriterOpener is a module that can open a log writer.
// It can return a human-readable string representation
// of itself so that operators can understand where
// the logs are going.
type WriterOpener interface {
fmt.Stringer
// WriterKey is a string that uniquely identifies this
// writer configuration. It is not shown to humans.
WriterKey() string
// OpenWriter opens a log for writing. The writer
// should be safe for concurrent use but need not
// be synchronous.
OpenWriter() (io.WriteCloser, error)
}
type writerDestructor struct {
io.WriteCloser
}
func (wdest writerDestructor) Destruct() error {
return wdest.Close()
}
// StandardLibLog configures the default Go standard library
// global logger in the log package. This is necessary because
// module dependencies which are not built specifically for
// Caddy will use the standard logger.
type StandardLibLog struct {
WriterRaw json.RawMessage `json:"writer,omitempty"`
writer io.WriteCloser
}
func (sll *StandardLibLog) provision(ctx Context, logging *Logging) error {
if sll.WriterRaw != nil {
val, err := ctx.LoadModuleInline("output", "caddy.logging.writers", sll.WriterRaw)
if err != nil {
return fmt.Errorf("loading sink log writer module: %v", err)
}
wo := val.(WriterOpener)
sll.WriterRaw = nil // allow GC to deallocate
var isNew bool
sll.writer, isNew, err = logging.openWriter(wo)
if err != nil {
return fmt.Errorf("opening sink log writer %#v: %v", val, err)
}
if isNew {
log.Printf("[INFO] Redirecting sink to: %s", wo)
log.SetOutput(sll.writer)
log.Printf("[INFO] Redirected sink to here (%s)", wo)
}
}
return nil
}
// CustomLog represents a custom logger configuration.
type CustomLog struct {
WriterRaw json.RawMessage `json:"writer,omitempty"`
EncoderRaw json.RawMessage `json:"encoder,omitempty"`
Level string `json:"level,omitempty"`
Sampling *LogSampling `json:"sampling,omitempty"`
Include []string `json:"include,omitempty"`
Exclude []string `json:"exclude,omitempty"`
writerOpener WriterOpener
writer io.WriteCloser
encoder zapcore.Encoder
levelEnabler zapcore.LevelEnabler
core zapcore.Core
}
func (cl *CustomLog) provision(ctx Context, logging *Logging) error {
// set up the log level
switch cl.Level {
case "debug":
cl.levelEnabler = zapcore.DebugLevel
case "", "info":
cl.levelEnabler = zapcore.InfoLevel
case "warn":
cl.levelEnabler = zapcore.WarnLevel
case "error":
cl.levelEnabler = zapcore.ErrorLevel
case "panic":
cl.levelEnabler = zapcore.PanicLevel
case "fatal":
cl.levelEnabler = zapcore.FatalLevel
default:
return fmt.Errorf("unrecognized log level: %s", cl.Level)
}
// If both Include and Exclude lists are populated, then each item must
// be a superspace or subspace of an item in the other list, because
// populating both lists means that any given item is either a rule
// or an exception to another rule. But if the item is not a super-
// or sub-space of any item in the other list, it is neither a rule
// nor an exception, and is a contradiction. Ensure, too, that the
// sets do not intersect, which is also a contradiction.
if len(cl.Include) > 0 && len(cl.Exclude) > 0 {
// prevent intersections
for _, allow := range cl.Include {
for _, deny := range cl.Exclude {
if allow == deny {
return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow)
}
}
}
// ensure namespaces are nested
outer:
for _, allow := range cl.Include {
for _, deny := range cl.Exclude {
if strings.HasPrefix(allow+".", deny+".") ||
strings.HasPrefix(deny+".", allow+".") {
continue outer
}
}
return fmt.Errorf("when both include and exclude are populated, each element must be a superspace or subspace of one in the other list; check '%s' in include", allow)
}
}
if cl.EncoderRaw != nil {
val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", cl.EncoderRaw)
if err != nil {
return fmt.Errorf("loading log encoder module: %v", err)
}
cl.EncoderRaw = nil // allow GC to deallocate
cl.encoder = val.(zapcore.Encoder)
}
if cl.encoder == nil {
cl.encoder = zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
}
if cl.WriterRaw != nil {
val, err := ctx.LoadModuleInline("output", "caddy.logging.writers", cl.WriterRaw)
if err != nil {
return fmt.Errorf("loading log writer module: %v", err)
}
cl.WriterRaw = nil // allow GC to deallocate
cl.writerOpener = val.(WriterOpener)
}
if cl.writerOpener == nil {
cl.writerOpener = StderrWriter{}
}
var err error
cl.writer, _, err = logging.openWriter(cl.writerOpener)
if err != nil {
return fmt.Errorf("opening log writer using %#v: %v", cl.writerOpener, err)
}
cl.buildCore()
return nil
}
func (cl *CustomLog) buildCore() {
// logs which only discard their output don't need
// to perform encoding or any other processing steps
// at all, so just shorcut to a nop core instead
if _, ok := cl.writerOpener.(*DiscardWriter); ok {
cl.core = zapcore.NewNopCore()
return
}
c := zapcore.NewCore(
cl.encoder,
zapcore.AddSync(cl.writer),
cl.levelEnabler,
)
if cl.Sampling != nil {
if cl.Sampling.Interval == 0 {
cl.Sampling.Interval = 1 * time.Second
}
if cl.Sampling.First == 0 {
cl.Sampling.First = 100
}
if cl.Sampling.Thereafter == 0 {
cl.Sampling.Thereafter = 100
}
c = zapcore.NewSampler(c, cl.Sampling.Interval,
cl.Sampling.First, cl.Sampling.Thereafter)
}
cl.core = c
}
func (cl *CustomLog) matchesModule(moduleName string) bool {
return cl.loggerAllowed(moduleName, true)
}
// loggerAllowed returns true if name is allowed to emit
// to cl. isModule should be true if name is the name of
// a module and you want to see if ANY of that module's
// logs would be permitted.
func (cl *CustomLog) loggerAllowed(name string, isModule bool) bool {
// accept all loggers by default
if len(cl.Include) == 0 && len(cl.Exclude) == 0 {
return true
}
// append a dot so that partial names don't match
// (i.e. we don't want "foo.b" to match "foo.bar"); we
// will also have to append a dot when we do HasPrefix
// below to compensate for when when namespaces are equal
if name != "" && name != "*" && name != "." {
name += "."
}
var longestAccept, longestReject int
if len(cl.Include) > 0 {
for _, namespace := range cl.Include {
var hasPrefix bool
if isModule {
hasPrefix = strings.HasPrefix(namespace+".", name)
} else {
hasPrefix = strings.HasPrefix(name, namespace+".")
}
if hasPrefix && len(namespace) > longestAccept {
longestAccept = len(namespace)
}
}
// the include list was populated, meaning that
// a match in this list is absolutely required
// if we are to accept the entry
if longestAccept == 0 {
return false
}
}
if len(cl.Exclude) > 0 {
for _, namespace := range cl.Exclude {
// * == all logs emitted by modules
// . == all logs emitted by core
if (namespace == "*" && name != ".") ||
(namespace == "." && name == ".") {
return false
}
if strings.HasPrefix(name, namespace+".") &&
len(namespace) > longestReject {
longestReject = len(namespace)
}
}
// the reject list is populated, so we have to
// reject this entry if its match is better
// than the best from the accept list
if longestReject > longestAccept {
return false
}
}
return (longestAccept > longestReject) ||
(len(cl.Include) == 0 && longestReject == 0)
}
// filteringCore filters log entries based on logger name,
// according to the rules of a CustomLog.
type filteringCore struct {
zapcore.Core
cl *CustomLog
}
// With properly wraps With.
func (fc *filteringCore) With(fields []zapcore.Field) zapcore.Core {
return &filteringCore{
Core: fc.Core.With(fields),
cl: fc.cl,
}
}
// Check only allows the log entry if its logger name
// is allowed from the include/exclude rules of fc.cl.
func (fc *filteringCore) Check(e zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if fc.cl.loggerAllowed(e.LoggerName, false) {
return fc.Core.Check(e, ce)
}
return ce
}
// LogSampling configures log entry sampling.
type LogSampling struct {
Interval time.Duration `json:"interval,omitempty"`
First int `json:"first,omitempty"`
Thereafter int `json:"thereafter,omitempty"`
}
type (
// StdoutWriter can write logs to stdout.
StdoutWriter struct{}
// StderrWriter can write logs to stdout.
StderrWriter struct{}
// DiscardWriter discards all writes.
DiscardWriter struct{}
)
// CaddyModule returns the Caddy module information.
func (StdoutWriter) CaddyModule() ModuleInfo {
return ModuleInfo{
Name: "caddy.logging.writers.stdout",
New: func() Module { return new(StdoutWriter) },
}
}
// CaddyModule returns the Caddy module information.
func (StderrWriter) CaddyModule() ModuleInfo {
return ModuleInfo{
Name: "caddy.logging.writers.stderr",
New: func() Module { return new(StderrWriter) },
}
}
// CaddyModule returns the Caddy module information.
func (DiscardWriter) CaddyModule() ModuleInfo {
return ModuleInfo{
Name: "caddy.logging.writers.discard",
New: func() Module { return new(DiscardWriter) },
}
}
func (StdoutWriter) String() string { return "stdout" }
func (StderrWriter) String() string { return "stderr" }
func (DiscardWriter) String() string { return "discard" }
// WriterKey returns a unique key representing stdout.
func (StdoutWriter) WriterKey() string { return "std:out" }
// WriterKey returns a unique key representing stderr.
func (StderrWriter) WriterKey() string { return "std:err" }
// WriterKey returns a unique key representing discard.
func (DiscardWriter) WriterKey() string { return "discard" }
// OpenWriter returns os.Stdout that can't be closed.
func (StdoutWriter) OpenWriter() (io.WriteCloser, error) {
return notClosable{os.Stdout}, nil
}
// OpenWriter returns os.Stderr that can't be closed.
func (StderrWriter) OpenWriter() (io.WriteCloser, error) {
return notClosable{os.Stderr}, nil
}
// OpenWriter returns ioutil.Discard that can't be closed.
func (DiscardWriter) OpenWriter() (io.WriteCloser, error) {
return notClosable{ioutil.Discard}, nil
}
// notClosable is an io.WriteCloser that can't be closed.
type notClosable struct{ io.Writer }
func (fc notClosable) Close() error { return nil }
type defaultCustomLog struct {
*CustomLog
logger *zap.Logger
}
// newDefaultProductionLog configures a custom log that is
// intended for use by default if no other log is specified
// in a config. It writes to stderr, uses the console encoder,
// and enables INFO-level logs and higher.
func newDefaultProductionLog() (*defaultCustomLog, error) {
cl := new(CustomLog)
cl.writerOpener = StderrWriter{}
var err error
cl.writer, err = cl.writerOpener.OpenWriter()
if err != nil {
return nil, err
}
encCfg := zap.NewProductionEncoderConfig()
encCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
cl.encoder = zapcore.NewConsoleEncoder(encCfg)
cl.levelEnabler = zapcore.InfoLevel
cl.buildCore()
return &defaultCustomLog{
CustomLog: cl,
logger: zap.New(cl.core),
}, nil
}
// Log returns the current default logger.
func Log() *zap.Logger {
defaultLoggerMu.RLock()
defer defaultLoggerMu.RUnlock()
return defaultLogger.logger
}
var (
defaultLogger, _ = newDefaultProductionLog()
defaultLoggerMu sync.RWMutex
)
var writers = NewUsagePool()
// Interface guards
var (
_ io.WriteCloser = (*notClosable)(nil)
_ WriterOpener = (*StdoutWriter)(nil)
_ WriterOpener = (*StderrWriter)(nil)
)
+3 -2
View File
@@ -253,9 +253,10 @@ type Validator interface {
// CleanerUpper is implemented by modules which may have side-effects
// such as opened files, spawned goroutines, or allocated some sort
// of non-local state when they were provisioned. This method should
// of non-stack state when they were provisioned. This method should
// deallocate/cleanup those resources to prevent memory leaks. Cleanup
// should be fast and efficient.
// should be fast and efficient. Cleanup should work even if Provision
// returns an error, to allow cleaning up from partial provisionings.
type CleanerUpper interface {
Cleanup() error
}
+164
View File
@@ -0,0 +1,164 @@
// 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 caddyauth
import (
"crypto/sha256"
"crypto/subtle"
"encoding/json"
"fmt"
"net/http"
"github.com/caddyserver/caddy/v2"
)
func init() {
caddy.RegisterModule(HTTPBasicAuth{})
}
// HTTPBasicAuth facilitates HTTP basic authentication.
type HTTPBasicAuth struct {
HashRaw json.RawMessage `json:"hash,omitempty"`
AccountList []Account `json:"accounts,omitempty"`
Realm string `json:"realm,omitempty"`
Accounts map[string]Account `json:"-"`
Hash Comparer `json:"-"`
}
// CaddyModule returns the Caddy module information.
func (HTTPBasicAuth) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "http.handlers.authentication.providers.http_basic",
New: func() caddy.Module { return new(HTTPBasicAuth) },
}
}
// Provision provisions the HTTP basic auth provider.
func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
if hba.HashRaw == nil {
return fmt.Errorf("passwords must be hashed, so a hash must be defined")
}
// load password hasher
hashIface, err := ctx.LoadModuleInline("algorithm", "http.handlers.authentication.hashes", hba.HashRaw)
if err != nil {
return fmt.Errorf("loading password hasher module: %v", err)
}
hba.Hash = hashIface.(Comparer)
hba.HashRaw = nil // allow GC to deallocate
if hba.Hash == nil {
return fmt.Errorf("hash is required")
}
// load account list
hba.Accounts = make(map[string]Account)
for _, acct := range hba.AccountList {
if _, ok := hba.Accounts[acct.Username]; ok {
return fmt.Errorf("username is not unique: %s", acct.Username)
}
hba.Accounts[acct.Username] = acct
}
hba.AccountList = nil // allow GC to deallocate
return nil
}
// Authenticate validates the user credentials in req and returns the user, if valid.
func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request) (User, bool, error) {
username, plaintextPasswordStr, ok := req.BasicAuth()
// if basic auth is missing or invalid, prompt for credentials
if !ok {
// browsers show a message that says something like:
// "The website says: <realm>"
// which is kinda dumb, but whatever.
realm := hba.Realm
if realm == "" {
realm = "restricted"
}
w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm))
return User{}, false, nil
}
plaintextPassword := []byte(plaintextPasswordStr)
account, accountExists := hba.Accounts[username]
// don't return early if account does not exist; we want
// to try to avoid side-channels that leak existence
same, err := hba.Hash.Compare(account.Password, plaintextPassword, account.Salt)
if err != nil {
return User{}, false, err
}
if !same || !accountExists {
return User{}, false, nil
}
return User{ID: username}, true, nil
}
// Comparer is a type that can securely compare
// a plaintext password with a hashed password
// in constant-time. Comparers should hash the
// plaintext password and then use constant-time
// comparison.
type Comparer interface {
// Compare returns true if the result of hashing
// plaintextPassword with salt is hashedPassword,
// false otherwise. An error is returned only if
// there is a technical/configuration error.
Compare(hashedPassword, plaintextPassword, salt []byte) (bool, error)
}
type quickComparer struct{}
func (quickComparer) Compare(theirHash, plaintext, _ []byte) (bool, error) {
ourHash := quickHash(plaintext)
return hashesMatch(ourHash, theirHash), nil
}
func hashesMatch(pwdHash1, pwdHash2 []byte) bool {
return subtle.ConstantTimeCompare(pwdHash1, pwdHash2) == 1
}
// quickHash returns the SHA-256 of v. It
// is not secure for password storage, but
// it is useful for efficiently normalizing
// the length of plaintext passwords for
// constant-time comparisons.
//
// Errors are discarded.
func quickHash(v []byte) []byte {
h := sha256.New()
h.Write([]byte(v))
return h.Sum(nil)
}
// Account contains a username, password, and salt (if applicable).
type Account struct {
Username string `json:"username"`
Password []byte `json:"password"`
Salt []byte `json:"salt,omitempty"` // for algorithms where external salt is needed
}
// Interface guards
var (
_ caddy.Provisioner = (*HTTPBasicAuth)(nil)
_ Authenticator = (*HTTPBasicAuth)(nil)
)
+102
View File
@@ -0,0 +1,102 @@
// 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 caddyauth
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func init() {
caddy.RegisterModule(Authentication{})
}
// Authentication is a middleware which provides user authentication.
type Authentication struct {
ProvidersRaw map[string]json.RawMessage `json:"providers,omitempty"`
Providers map[string]Authenticator `json:"-"`
}
// CaddyModule returns the Caddy module information.
func (Authentication) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "http.handlers.authentication",
New: func() caddy.Module { return new(Authentication) },
}
}
// Provision sets up a.
func (a *Authentication) Provision(ctx caddy.Context) error {
a.Providers = make(map[string]Authenticator)
for modName, rawMsg := range a.ProvidersRaw {
val, err := ctx.LoadModule("http.handlers.authentication.providers."+modName, rawMsg)
if err != nil {
return fmt.Errorf("loading authentication provider module '%s': %v", modName, err)
}
a.Providers[modName] = val.(Authenticator)
}
a.ProvidersRaw = nil // allow GC to deallocate
return nil
}
func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
var user User
var authed bool
var err error
for provName, prov := range a.Providers {
user, authed, err = prov.Authenticate(w, r)
if err != nil {
log.Printf("[ERROR] Authenticating with %s: %v", provName, err)
continue
}
if authed {
break
}
}
if !authed {
return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated"))
}
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
repl.Set("http.handlers.authentication.user.id", user.ID)
return next.ServeHTTP(w, r)
}
// Authenticator is a type which can authenticate a request.
// If a request was not authenticated, it returns false. An
// error is only returned if authenticating the request fails
// for a technical reason (not for bad/missing credentials).
type Authenticator interface {
Authenticate(http.ResponseWriter, *http.Request) (User, bool, error)
}
// User represents an authenticated user.
type User struct {
ID string
}
// Interface guards
var (
_ caddy.Provisioner = (*Authentication)(nil)
_ caddyhttp.MiddlewareHandler = (*Authentication)(nil)
)
+104
View File
@@ -0,0 +1,104 @@
// 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 caddyauth
import (
"encoding/base64"
"encoding/json"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func init() {
httpcaddyfile.RegisterHandlerDirective("basicauth", parseCaddyfile)
}
// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax:
//
// basicauth [<matcher>] [<hash_algorithm>] {
// <username> <hashed_password_base64> [<salt_base64>]
// ...
// }
//
// If no hash algorithm is supplied, bcrypt will be assumed.
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var ba HTTPBasicAuth
for h.Next() {
var cmp Comparer
args := h.RemainingArgs()
var hashName string
switch len(args) {
case 0:
hashName = "bcrypt"
case 1:
hashName = args[0]
default:
return nil, h.ArgErr()
}
switch hashName {
case "bcrypt":
cmp = BcryptHash{}
case "scrypt":
cmp = ScryptHash{}
default:
return nil, h.Errf("unrecognized hash algorithm: %s", hashName)
}
ba.HashRaw = caddyconfig.JSONModuleObject(cmp, "algorithm", hashName, nil)
for h.NextBlock(0) {
username := h.Val()
var b64Pwd, b64Salt string
h.Args(&b64Pwd, &b64Salt)
if h.NextArg() {
return nil, h.ArgErr()
}
if username == "" || b64Pwd == "" {
return nil, h.Err("username and password cannot be empty or missing")
}
pwd, err := base64.StdEncoding.DecodeString(b64Pwd)
if err != nil {
return nil, h.Errf("decoding password: %v", err)
}
var salt []byte
if b64Salt != "" {
salt, err = base64.StdEncoding.DecodeString(b64Salt)
if err != nil {
return nil, h.Errf("decoding salt: %v", err)
}
}
ba.AccountList = append(ba.AccountList, Account{
Username: username,
Password: pwd,
Salt: salt,
})
}
}
return Authentication{
ProvidersRaw: map[string]json.RawMessage{
"http_basic": caddyconfig.JSON(ba, nil),
},
}, nil
}
+84
View File
@@ -0,0 +1,84 @@
// 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 caddyauth
import (
"encoding/base64"
"flag"
"fmt"
"github.com/caddyserver/caddy/v2"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/scrypt"
)
func init() {
caddycmd.RegisterCommand(caddycmd.Command{
Name: "hash-password",
Func: cmdHashPassword,
Usage: "--plaintext <password> [--salt <string>] [--algorithm <name>]",
Short: "Hashes a password and writes base64",
Long: `
Convenient way to hash a plaintext password. The resulting
hash is written to stdout as a base64 string.
--algorithm may be bcrypt or scrypt. If script, the default
parameters are used.
Use the --salt flag for algorithms which require a salt to
be provided (scrypt).
`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("hash-password", flag.ExitOnError)
fs.String("algorithm", "bcrypt", "Name of the hash algorithm")
fs.String("plaintext", "", "The plaintext password")
fs.String("salt", "", "The password salt")
return fs
}(),
})
}
func cmdHashPassword(fs caddycmd.Flags) (int, error) {
algorithm := fs.String("algorithm")
plaintext := []byte(fs.String("plaintext"))
salt := []byte(fs.String("salt"))
if len(plaintext) == 0 {
return caddy.ExitCodeFailedStartup, fmt.Errorf("password is required")
}
var hash []byte
var err error
switch algorithm {
case "bcrypt":
hash, err = bcrypt.GenerateFromPassword(plaintext, bcrypt.DefaultCost)
case "scrypt":
def := ScryptHash{}
def.SetDefaults()
hash, err = scrypt.Key(plaintext, salt, def.N, def.R, def.P, def.KeyLength)
default:
return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm)
}
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
hashBase64 := base64.StdEncoding.EncodeToString([]byte(hash))
fmt.Println(hashBase64)
return 0, nil
}
+111
View File
@@ -0,0 +1,111 @@
// 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 caddyauth
import (
"github.com/caddyserver/caddy/v2"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/scrypt"
)
func init() {
caddy.RegisterModule(BcryptHash{})
caddy.RegisterModule(ScryptHash{})
}
// BcryptHash implements the bcrypt hash.
type BcryptHash struct{}
// CaddyModule returns the Caddy module information.
func (BcryptHash) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "http.handlers.authentication.hashes.bcrypt",
New: func() caddy.Module { return new(BcryptHash) },
}
}
// Compare compares passwords.
func (BcryptHash) Compare(hashed, plaintext, _ []byte) (bool, error) {
err := bcrypt.CompareHashAndPassword(hashed, plaintext)
if err == bcrypt.ErrMismatchedHashAndPassword {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
// ScryptHash implements the scrypt KDF as a hash.
type ScryptHash struct {
N int `json:"N,omitempty"`
R int `json:"r,omitempty"`
P int `json:"p,omitempty"`
KeyLength int `json:"key_length,omitempty"`
}
// CaddyModule returns the Caddy module information.
func (ScryptHash) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "http.handlers.authentication.hashes.scrypt",
New: func() caddy.Module { return new(ScryptHash) },
}
}
// Provision sets up s.
func (s *ScryptHash) Provision(_ caddy.Context) error {
s.SetDefaults()
return nil
}
// SetDefaults sets safe default parameters, but does
// not overwrite existing values. Each default parameter
// is set independently; it does not check to ensure
// that r*p < 2^30. The defaults chosen are those as
// recommended in 2019 by
// https://godoc.org/golang.org/x/crypto/scrypt.
func (s *ScryptHash) SetDefaults() {
if s.N == 0 {
s.N = 32768
}
if s.R == 0 {
s.R = 8
}
if s.P == 0 {
s.P = 1
}
if s.KeyLength == 0 {
s.KeyLength = 32
}
}
// Compare compares passwords.
func (s ScryptHash) Compare(hashed, plaintext, salt []byte) (bool, error) {
ourHash, err := scrypt.Key(plaintext, salt, s.N, s.R, s.P, s.KeyLength)
if err != nil {
return false, err
}
if hashesMatch(hashed, ourHash) {
return true, nil
}
return false, nil
}
// Interface guards
var (
_ Comparer = (*BcryptHash)(nil)
_ Comparer = (*ScryptHash)(nil)
_ caddy.Provisioner = (*ScryptHash)(nil)
)
+145 -91
View File
@@ -21,7 +21,6 @@ import (
"encoding/json"
"fmt"
"io"
"log"
weakrand "math/rand"
"net"
"net/http"
@@ -33,6 +32,7 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/lucas-clemente/quic-go/http3"
"github.com/mholt/certmagic"
"go.uber.org/zap"
)
func init() {
@@ -40,7 +40,7 @@ func init() {
err := caddy.RegisterModule(App{})
if err != nil {
log.Fatal(err)
caddy.Log().Fatal(err.Error())
}
}
@@ -55,7 +55,8 @@ type App struct {
h3servers []*http3.Server
h3listeners []net.PacketConn
ctx caddy.Context
ctx caddy.Context
logger *zap.Logger
}
// CaddyModule returns the Caddy module information.
@@ -69,40 +70,54 @@ func (App) CaddyModule() caddy.ModuleInfo {
// Provision sets up the app.
func (app *App) Provision(ctx caddy.Context) error {
app.ctx = ctx
app.logger = ctx.Logger(app)
repl := caddy.NewReplacer()
for _, srv := range app.Servers {
for srvName, srv := range app.Servers {
srv.logger = app.logger.Named("log")
srv.accessLogger = app.logger.Named("log.access")
srv.errorLogger = app.logger.Named("log.error")
if srv.AutoHTTPS == nil {
// avoid nil pointer dereferences
srv.AutoHTTPS = new(AutoHTTPSConfig)
}
// disallow TLS client auth bypass which could
// otherwise be exploited by sending an unprotected
// SNI value during TLS handshake, then a protected
// Host header during HTTP request later on that
// connection
if srv.hasTLSClientAuth() {
srv.StrictSNIHost = true
// if not explicitly configured by the user, disallow TLS
// client auth bypass (domain fronting) which could
// otherwise be exploited by sending an unprotected SNI
// value during a TLS handshake, then putting a protected
// domain in the Host header after establishing connection;
// this is a safe default, but we allow users to override
// it for example in the case of running a proxy where
// domain fronting is desired and access is not restricted
// based on hostname
if srv.StrictSNIHost == nil && srv.hasTLSClientAuth() {
trueBool := true
srv.StrictSNIHost = &trueBool
}
// TODO: Test this function to ensure these replacements are performed
for i := range srv.Listen {
srv.Listen[i] = repl.ReplaceAll(srv.Listen[i], "")
lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true)
if err != nil {
return fmt.Errorf("server %s, listener %d: %v",
srvName, i, err)
}
srv.Listen[i] = lnOut
}
if srv.Routes != nil {
err := srv.Routes.Provision(ctx)
if err != nil {
return fmt.Errorf("setting up server routes: %v", err)
return fmt.Errorf("server %s: setting up server routes: %v", srvName, err)
}
}
if srv.Errors != nil {
err := srv.Errors.Routes.Provision(ctx)
if err != nil {
return fmt.Errorf("setting up server error handling routes: %v", err)
return fmt.Errorf("server %s: setting up server error handling routes: %v", srvName, err)
}
}
@@ -179,12 +194,8 @@ func (app *App) Start() error {
}
// enable TLS
httpPort := app.HTTPPort
if httpPort == 0 {
httpPort = DefaultHTTPPort
}
_, port, _ := net.SplitHostPort(addr)
if len(srv.TLSConnPolicies) > 0 && port != strconv.Itoa(httpPort) {
if len(srv.TLSConnPolicies) > 0 && port != strconv.Itoa(app.httpPort()) {
tlsCfg, err := srv.TLSConnPolicies.TLSConfig(app.ctx)
if err != nil {
return fmt.Errorf("%s/%s: making TLS configuration: %v", network, addr, err)
@@ -194,7 +205,9 @@ func (app *App) Start() error {
/////////
// TODO: HTTP/3 support is experimental for now
if srv.ExperimentalHTTP3 {
log.Printf("[INFO] Enabling experimental HTTP/3 listener on %s", addr)
app.logger.Info("enabling experimental HTTP/3 listener",
zap.String("addr", addr),
)
h3ln, err := caddy.ListenPacket("udp", addr)
if err != nil {
return fmt.Errorf("getting HTTP/3 UDP listener: %v", err)
@@ -269,8 +282,11 @@ func (app *App) automaticHTTPS() error {
}
tlsApp := tlsAppIface.(*caddytls.TLS)
lnAddrMap := make(map[string]struct{})
var redirRoutes RouteList
// this map will store associations of HTTP listener
// addresses to the routes that do HTTP->HTTPS redirects
lnAddrRedirRoutes := make(map[string]Route)
repl := caddy.NewReplacer()
for srvName, srv := range app.Servers {
srv.tlsApp = tlsApp
@@ -280,19 +296,26 @@ func (app *App) automaticHTTPS() error {
}
// skip if all listeners use the HTTP port
if !srv.listenersUseAnyPortOtherThan(app.HTTPPort) {
log.Printf("[INFO] Server %v is only listening on the HTTP port %d, so no automatic HTTPS will be applied to this server",
srv.Listen, app.HTTPPort)
if !srv.listenersUseAnyPortOtherThan(app.httpPort()) {
app.logger.Info("server is only listening on the HTTP port, so no automatic HTTPS will be applied to this server",
zap.String("server_name", srvName),
zap.Int("http_port", app.httpPort()),
)
continue
}
// find all qualifying domain names, de-duplicated
domainSet := make(map[string]struct{})
for _, route := range srv.Routes {
for _, matcherSet := range route.MatcherSets {
for _, m := range matcherSet {
for routeIdx, route := range srv.Routes {
for matcherSetIdx, matcherSet := range route.MatcherSets {
for matcherIdx, m := range matcherSet {
if hm, ok := m.(*MatchHost); ok {
for _, d := range *hm {
for hostMatcherIdx, d := range *hm {
d, err = repl.ReplaceOrErr(d, true, false)
if err != nil {
return fmt.Errorf("%s: route %d, matcher set %d, matcher %d, host matcher %d: %v",
srvName, routeIdx, matcherSetIdx, matcherIdx, hostMatcherIdx, err)
}
if certmagic.HostQualifies(d) &&
!srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) {
domainSet[d] = struct{}{}
@@ -314,7 +337,10 @@ func (app *App) automaticHTTPS() error {
// supposed to ignore loaded certificates
if !srv.AutoHTTPS.IgnoreLoadedCerts &&
len(tlsApp.AllMatchingCertificates(d)) > 0 {
log.Printf("[INFO][%s] Skipping automatic certificate management because one or more matching certificates are already loaded", d)
app.logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded",
zap.String("domain", d),
zap.String("server_name", srvName),
)
continue
}
domainsForCerts = append(domainsForCerts, d)
@@ -328,15 +354,18 @@ func (app *App) automaticHTTPS() error {
// to tell the TLS app to manage these certs by honoring
// those port configurations
acmeManager := &caddytls.ACMEManagerMaker{
Challenges: caddytls.ChallengesConfig{
HTTP: caddytls.HTTPChallengeConfig{
AlternatePort: app.HTTPPort,
Challenges: &caddytls.ChallengesConfig{
HTTP: &caddytls.HTTPChallengeConfig{
AlternatePort: app.HTTPPort, // we specifically want the user-configured port, if any
},
TLSALPN: caddytls.TLSALPNChallengeConfig{
AlternatePort: app.HTTPSPort,
TLSALPN: &caddytls.TLSALPNChallengeConfig{
AlternatePort: app.HTTPSPort, // we specifically want the user-configured port, if any
},
},
}
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies,
caddytls.AutomationPolicy{
Hosts: domainsForCerts,
@@ -344,7 +373,9 @@ func (app *App) automaticHTTPS() error {
})
// manage their certificates
log.Printf("[INFO] Enabling automatic HTTPS certificates for %v", domainsForCerts)
app.logger.Info("enabling automatic TLS certificate management",
zap.Strings("domains", domainsForCerts),
)
err := tlsApp.Manage(domainsForCerts)
if err != nil {
return fmt.Errorf("%s: managing certificate for %s: %s", srvName, domains, err)
@@ -361,13 +392,9 @@ func (app *App) automaticHTTPS() error {
continue
}
log.Printf("[INFO] Enabling automatic HTTP->HTTPS redirects for %v", domains)
// notify user if their config might override the HTTP->HTTPS redirects
if srv.listenersIncludePort(app.HTTPPort) {
log.Printf("[WARNING] Server %v is listening on HTTP port %d, so automatic HTTP->HTTPS redirects may be overridden by your own configuration",
srv.Listen, app.HTTPPort)
}
app.logger.Info("enabling automatic HTTP->HTTPS redirects",
zap.Strings("domains", domains),
)
// create HTTP->HTTPS redirects
for _, addr := range srv.Listen {
@@ -376,28 +403,22 @@ func (app *App) automaticHTTPS() error {
return fmt.Errorf("%s: invalid listener address: %v", srvName, addr)
}
httpPort := app.HTTPPort
if httpPort == 0 {
httpPort = DefaultHTTPPort
}
httpRedirLnAddr := caddy.JoinNetworkAddress(netw, host, strconv.Itoa(httpPort))
lnAddrMap[httpRedirLnAddr] = struct{}{}
if parts := strings.SplitN(port, "-", 2); len(parts) == 2 {
port = parts[0]
}
redirTo := "https://{http.request.host}"
httpsPort := app.HTTPSPort
if httpsPort == 0 {
httpsPort = DefaultHTTPSPort
}
if port != strconv.Itoa(httpsPort) {
if port != strconv.Itoa(app.httpsPort()) {
redirTo += ":" + port
}
redirTo += "{http.request.uri}"
redirRoutes = append(redirRoutes, Route{
// build the plaintext HTTP variant of this address
httpRedirLnAddr := caddy.JoinNetworkAddress(netw, host, strconv.Itoa(app.httpPort()))
// create the route that does the redirect and associate
// it with the listener address it will be served from
lnAddrRedirRoutes[httpRedirLnAddr] = Route{
MatcherSets: []MatcherSet{
{
MatchProtocol("http"),
@@ -414,52 +435,72 @@ func (app *App) automaticHTTPS() error {
Close: true,
},
},
})
}
}
}
}
if len(lnAddrMap) > 0 {
var lnAddrs []string
mapLoop:
for addr := range lnAddrMap {
netw, addrs, err := caddy.ParseNetworkAddress(addr)
if err != nil {
continue
}
for _, a := range addrs {
if app.listenerTaken(netw, a) {
continue mapLoop
// if there are HTTP->HTTPS redirects to add, do so now
if len(lnAddrRedirRoutes) > 0 {
var redirServerAddrs []string
var redirRoutes []Route
// for each redirect listener, see if there's already a
// server configured to listen on that exact address; if
// so, simply the redirect route to the end of its route
// list; otherwise, we'll create a new server for all the
// listener addresses that are unused and serve the
// remaining redirects from it
redirRoutesLoop:
for addr, redirRoute := range lnAddrRedirRoutes {
for srvName, srv := range app.Servers {
if srv.hasListenerAddress(addr) {
// user has configured a server for the same address
// that the redirect runs from; simply append our
// redirect route to the existing routes, with a
// caveat that their config might override ours
app.logger.Warn("server is listening on same interface as redirects, so automatic HTTP->HTTPS redirects might be overridden by your own configuration",
zap.String("server_name", srvName),
zap.String("interface", addr),
)
srv.Routes = append(srv.Routes, redirRoute)
continue redirRoutesLoop
}
}
lnAddrs = append(lnAddrs, addr)
// no server with this listener address exists;
// save this address and route for custom server
redirServerAddrs = append(redirServerAddrs, addr)
redirRoutes = append(redirRoutes, redirRoute)
}
app.Servers["auto_https_redirects"] = &Server{
Listen: lnAddrs,
Routes: redirRoutes,
AutoHTTPS: &AutoHTTPSConfig{Disabled: true},
tlsApp: tlsApp, // required to solve HTTP challenge
// if there are routes remaining which do not belong
// in any existing server, make our own to serve the
// rest of the redirects
if len(redirServerAddrs) > 0 {
app.Servers["remaining_auto_https_redirects"] = &Server{
Listen: redirServerAddrs,
Routes: redirRoutes,
tlsApp: tlsApp, // required to solve HTTP challenge
}
}
}
return nil
}
func (app *App) listenerTaken(network, address string) bool {
for _, srv := range app.Servers {
for _, addr := range srv.Listen {
netw, addrs, err := caddy.ParseNetworkAddress(addr)
if err != nil || netw != network {
continue
}
for _, a := range addrs {
if a == address {
return true
}
}
}
func (app *App) httpPort() int {
if app.HTTPPort == 0 {
return DefaultHTTPPort
}
return false
return app.HTTPPort
}
func (app *App) httpsPort() int {
if app.HTTPSPort == 0 {
return DefaultHTTPSPort
}
return app.HTTPSPort
}
var defaultALPN = []string{"h2", "http/1.1"}
@@ -511,8 +552,8 @@ var emptyHandler HandlerFunc = func(http.ResponseWriter, *http.Request) error {
// WeakString is a type that unmarshals any JSON value
// as a string literal, with the following exceptions:
// 1) actual string values are decoded as strings, and
// 2) null is decoded as empty string
// 1) actual string values are decoded as strings; and
// 2) null is decoded as empty string;
// and provides methods for getting the value as various
// primitive types. However, using this type removes any
// type safety as far as deserializing JSON is concerned.
@@ -580,6 +621,19 @@ func (ws WeakString) String() string {
return string(ws)
}
// CopyHeader copies HTTP headers by completely
// replacing dest with src. (This allows deletions
// to be propagated, assuming src started as a
// consistent copy of dest.)
func CopyHeader(dest, src http.Header) {
for field := range dest {
delete(dest, field)
}
for field, val := range src {
dest[field] = val
}
}
// StatusCodeMatches returns true if a real HTTP status code matches
// the configured status code, which may be either a real HTTP status
// code or an integer representing a class of codes (e.g. 4 for all
-68
View File
@@ -1,68 +0,0 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddylog
import (
"log"
"net/http"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func init() {
caddy.RegisterModule(Log{})
}
// Log implements a simple logging middleware.
type Log struct {
Filename string
counter int
}
// CaddyModule returns the Caddy module information.
func (Log) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "http.handlers.log",
New: func() caddy.Module { return new(Log) },
}
}
func (l *Log) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
start := time.Now()
// TODO: An example of returning errors
// return caddyhttp.Error(http.StatusBadRequest, fmt.Errorf("this is a basic error"))
// return caddyhttp.Error(http.StatusBadGateway, caddyhttp.HandlerError{
// Err: fmt.Errorf("this is a detailed error"),
// Message: "We had trouble doing the thing.",
// Recommendations: []string{
// "Try reconnecting the gizbop.",
// "Turn off the Internet.",
// },
// })
if err := next.ServeHTTP(w, r); err != nil {
return err
}
log.Println("latency:", time.Now().Sub(start), l.counter)
return nil
}
// Interface guard
var _ caddyhttp.MiddlewareHandler = (*Log)(nil)
+1 -4
View File
@@ -29,7 +29,6 @@ func init() {
httpcaddyfile.RegisterHandlerDirective("encode", parseCaddyfile)
}
// TODO: This is a good example of why UnmarshalCaddyfile is still a good idea... hmm.
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
enc := new(Encode)
err := enc.UnmarshalCaddyfile(h.Dispenser)
@@ -39,8 +38,6 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
return enc, nil
}
// TODO: Keep UnmarshalCaddyfile pattern?
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
//
// encode [<matcher>] <formats...> {
@@ -71,7 +68,7 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
name := d.Val()
mod, err := caddy.GetModule("http.encoders." + name)
if err != nil {
return fmt.Errorf("getting encoder module '%s': %v", mod.Name, err)
return fmt.Errorf("getting encoder module '%s': %v", name, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
+1 -4
View File
@@ -68,7 +68,7 @@ func (enc *Encode) Provision(ctx caddy.Context) error {
return err
}
}
enc.EncodingsRaw = nil // allow GC to deallocate - TODO: Does this help?
enc.EncodingsRaw = nil // allow GC to deallocate
if enc.MinLength == 0 {
enc.MinLength = defaultMinLength
@@ -162,9 +162,6 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
if err != nil {
return 0, err
}
if rw.buf.Len() < rw.config.MinLength {
return len(p), nil
}
rw.init()
p = rw.buf.Bytes()
defer func() {
+2 -4
View File
@@ -53,10 +53,8 @@ func Error(statusCode int, err error) HandlerError {
// HandlerError is a serializable representation of
// an error from within an HTTP handler.
type HandlerError struct {
Err error // the original error value and message
StatusCode int // the HTTP status code to associate with this error
Message string // an optional message that can be shown to the user
Recommendations []string // an optional list of things to try to resolve the error
Err error // the original error value and message
StatusCode int // the HTTP status code to associate with this error
ID string // generated; for identifying this error in logs
Trace string // produced from call stack
+108
View File
@@ -0,0 +1,108 @@
// 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 fileserver
import (
"encoding/json"
"flag"
"log"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func init() {
caddycmd.RegisterCommand(caddycmd.Command{
Name: "file-server",
Func: cmdFileServer,
Usage: "[--domain <example.com>] [--path <path>] [--listen <addr>] [--browse]",
Short: "Spins up a production-ready file server",
Long: `
A simple but production-ready file server. Useful for quick deployments,
demos, and development.
If a qualifying hostname is specified with --domain, the server will use
HTTPS if domain validation succeeds. Ensure A/AAAA records are properly
configured before using this option.
The listener's socket address can be customized with the --listen flag.
If --browse is enabled, requests for folders without an index file will
respond with a file listing.`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("file-server", flag.ExitOnError)
fs.String("domain", "", "Domain name at which to serve the files")
fs.String("root", "", "The path to the root of the site")
fs.String("listen", "", "The address to which to bind the listener")
fs.Bool("browse", false, "Whether to enable directory browsing")
return fs
}(),
})
}
func cmdFileServer(fs caddycmd.Flags) (int, error) {
domain := fs.String("domain")
root := fs.String("root")
listen := fs.String("listen")
browse := fs.Bool("browse")
handler := FileServer{Root: root}
if browse {
handler.Browse = new(Browse)
}
route := caddyhttp.Route{
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(handler, "handler", "file_server", nil),
},
}
if domain != "" {
route.MatcherSetsRaw = []map[string]json.RawMessage{
map[string]json.RawMessage{
"host": caddyconfig.JSON(caddyhttp.MatchHost{domain}, nil),
},
}
}
server := &caddyhttp.Server{
Routes: caddyhttp.RouteList{route},
}
if listen == "" {
listen = ":" + httpcaddyfile.DefaultPort
}
server.Listen = []string{listen}
httpApp := caddyhttp.App{
Servers: map[string]*caddyhttp.Server{"static": server},
}
cfg := &caddy.Config{
AppsRaw: map[string]json.RawMessage{
"http": caddyconfig.JSON(httpApp, nil),
},
}
err := caddy.Run(cfg)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
log.Printf("Caddy 2 serving static files on %s", listen)
select {}
}
@@ -18,6 +18,7 @@ import (
"bytes"
"fmt"
"html/template"
"io"
weakrand "math/rand"
"mime"
"net/http"
@@ -191,6 +192,24 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, _ cadd
}
}
// if this handler exists in an error context (i.e. is
// part of a handler chain that is supposed to handle
// a previous error), we have to serve the content
// manually in order to write the correct status code
if reqErr, ok := r.Context().Value(caddyhttp.ErrorCtxKey).(error); ok {
statusCode := http.StatusInternalServerError
if handlerErr, ok := reqErr.(caddyhttp.HandlerError); ok {
if handlerErr.StatusCode > 0 {
statusCode = handlerErr.StatusCode
}
}
w.WriteHeader(statusCode)
if r.Method != "HEAD" {
io.Copy(w, file)
}
return nil
}
// let the standard library do what it does best; note, however,
// that errors generated by ServeContent are written immediately
// to the response, so we cannot handle them (but errors there
@@ -16,14 +16,15 @@ package fileserver
import (
"net/url"
"path/filepath"
"testing"
)
func TestSanitizedPathJoin(t *testing.T) {
// For easy reference:
// %2E = .
// %2F = /
// %5C = \
// %2e = .
// %2f = /
// %5c = \
for i, tc := range []struct {
inputRoot string
inputPath string
@@ -43,12 +44,12 @@ func TestSanitizedPathJoin(t *testing.T) {
},
{
inputPath: "/foo/bar",
expect: "foo/bar",
expect: filepath.Join("foo", "bar"),
},
{
inputRoot: "/a",
inputPath: "/foo/bar",
expect: "/a/foo/bar",
expect: filepath.Join("/", "a", "foo", "bar"),
},
{
inputPath: "/foo/../bar",
@@ -57,24 +58,29 @@ func TestSanitizedPathJoin(t *testing.T) {
{
inputRoot: "/a/b",
inputPath: "/foo/../bar",
expect: "/a/b/bar",
expect: filepath.Join("/", "a", "b", "bar"),
},
{
inputRoot: "/a/b",
inputPath: "/..%2fbar",
expect: "/a/b/bar",
expect: filepath.Join("/", "a", "b", "bar"),
},
{
inputRoot: "/a/b",
inputPath: "/%2e%2e%2fbar",
expect: "/a/b/bar",
expect: filepath.Join("/", "a", "b", "bar"),
},
{
inputRoot: "/a/b",
inputPath: "/%2e%2e%2f%2e%2e%2f",
expect: "/a/b",
expect: filepath.Join("/", "a", "b"),
},
// TODO: test windows paths... on windows... sigh.
{
inputRoot: "C:\\www",
inputPath: "/foo/bar",
expect: filepath.Join("C:\\www", "foo", "bar"),
},
// TODO: test more windows paths... on windows... sigh.
} {
// we don't *need* to use an actual parsed URL, but it
// adds some authenticity to the tests since real-world
+64 -38
View File
@@ -30,26 +30,40 @@ func init() {
// parseCaddyfile sets up the handler for response headers from
// Caddyfile tokens. Syntax:
//
// headers [<matcher>] [[+|-]<field> <value>] {
// [+][<field>] [<value>]
// [-<field>]
// headers [<matcher>] [[+|-]<field> [<value|regexp>] [<replacement>]] {
// [+]<field> [<value|regexp> [<replacement>]]
// -<field>
// }
//
// Either a block can be opened or a single header field can be configured
// in the first line, but not both in the same directive.
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
hdr := new(Headers)
hdr := new(Handler)
makeResponseOps := func() {
if hdr.Response == nil {
hdr.Response = &RespHeaderOps{
HeaderOps: new(HeaderOps),
Deferred: true,
}
}
}
for h.Next() {
// first see if headers are in the initial line
var hasArgs bool
if h.NextArg() {
hasArgs = true
field := h.Val()
var value string
var value, replacement string
if h.NextArg() {
value = h.Val()
}
processCaddyfileLineRespHdr(hdr, field, value)
if h.NextArg() {
replacement = h.Val()
}
makeResponseOps()
CaddyfileHeaderOp(hdr.Response.HeaderOps, field, value, replacement)
}
// if not, they should be in a block
@@ -58,49 +72,45 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
return nil, h.Err("cannot specify headers in both arguments and block")
}
field := h.Val()
var value string
var value, replacement string
if h.NextArg() {
value = h.Val()
}
processCaddyfileLineRespHdr(hdr, field, value)
if h.NextArg() {
replacement = h.Val()
}
makeResponseOps()
CaddyfileHeaderOp(hdr.Response.HeaderOps, field, value, replacement)
}
}
return hdr, nil
}
// parseReqHdrCaddyfile sets up the handler for request headers
// from Caddyfile tokens. Syntax:
//
// request_header [<matcher>] [[+|-]<field> <value>]
// request_header [<matcher>] [[+|-]<field> [<value|regexp>] [<replacement>]]
//
func parseReqHdrCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
hdr := new(Headers)
hdr := new(Handler)
for h.Next() {
if !h.NextArg() {
return nil, h.ArgErr()
}
field := h.Val()
var value string
var value, replacement string
if h.NextArg() {
value = h.Val()
}
if h.NextArg() {
replacement = h.Val()
}
if hdr.Request == nil {
hdr.Request = new(HeaderOps)
}
if strings.HasPrefix(field, "+") {
if hdr.Request.Add == nil {
hdr.Request.Add = make(http.Header)
}
hdr.Request.Add.Set(field[1:], value)
} else if strings.HasPrefix(field, "-") {
hdr.Request.Delete = append(hdr.Request.Delete, field[1:])
} else {
if hdr.Request.Set == nil {
hdr.Request.Set = make(http.Header)
}
hdr.Request.Set.Set(field, value)
}
CaddyfileHeaderOp(hdr.Request, field, value, replacement)
if h.NextArg() {
return nil, h.ArgErr()
@@ -109,24 +119,40 @@ func parseReqHdrCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler,
return hdr, nil
}
func processCaddyfileLineRespHdr(hdr *Headers, field, value string) {
if hdr.Response == nil {
hdr.Response = &RespHeaderOps{
HeaderOps: new(HeaderOps),
Deferred: true,
}
}
// CaddyfileHeaderOp applies a new header operation according to
// field, value, and replacement. The field can be prefixed with
// "+" or "-" to specify adding or removing; otherwise, the value
// will be set (overriding any previous value). If replacement is
// non-empty, value will be treated as a regular expression which
// will be used to search and then replacement will be used to
// complete the substring replacement; in that case, any + or -
// prefix to field will be ignored.
func CaddyfileHeaderOp(ops *HeaderOps, field, value, replacement string) {
if strings.HasPrefix(field, "+") {
if hdr.Response.Add == nil {
hdr.Response.Add = make(http.Header)
if ops.Add == nil {
ops.Add = make(http.Header)
}
hdr.Response.Add.Set(field[1:], value)
ops.Add.Set(field[1:], value)
} else if strings.HasPrefix(field, "-") {
hdr.Response.Delete = append(hdr.Response.Delete, field[1:])
ops.Delete = append(ops.Delete, field[1:])
} else {
if hdr.Response.Set == nil {
hdr.Response.Set = make(http.Header)
if replacement == "" {
if ops.Set == nil {
ops.Set = make(http.Header)
}
ops.Set.Set(field, value)
} else {
if ops.Replace == nil {
ops.Replace = make(map[string][]Replacement)
}
field = strings.TrimLeft(field, "+-")
ops.Replace[field] = append(
ops.Replace[field],
Replacement{
SearchRegexp: value,
Replace: replacement,
},
)
}
hdr.Response.Set.Set(field, value)
}
}
+184 -35
View File
@@ -15,7 +15,9 @@
package headers
import (
"fmt"
"net/http"
"regexp"
"strings"
"github.com/caddyserver/caddy/v2"
@@ -23,51 +25,62 @@ import (
)
func init() {
caddy.RegisterModule(Headers{})
caddy.RegisterModule(Handler{})
}
// Headers is a middleware which can mutate HTTP headers.
type Headers struct {
// Handler is a middleware which can mutate HTTP headers.
type Handler struct {
Request *HeaderOps `json:"request,omitempty"`
Response *RespHeaderOps `json:"response,omitempty"`
}
// CaddyModule returns the Caddy module information.
func (Headers) CaddyModule() caddy.ModuleInfo {
func (Handler) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "http.handlers.headers",
New: func() caddy.Module { return new(Headers) },
New: func() caddy.Module { return new(Handler) },
}
}
// HeaderOps defines some operations to
// perform on HTTP headers.
type HeaderOps struct {
Add http.Header `json:"add,omitempty"`
Set http.Header `json:"set,omitempty"`
Delete []string `json:"delete,omitempty"`
// Provision sets up h's configuration.
func (h *Handler) Provision(_ caddy.Context) error {
if h.Request != nil {
err := h.Request.provision()
if err != nil {
return err
}
}
if h.Response != nil {
err := h.Response.provision()
if err != nil {
return err
}
}
return nil
}
// RespHeaderOps is like HeaderOps, but
// optionally deferred until response time.
type RespHeaderOps struct {
*HeaderOps
Require *caddyhttp.ResponseMatcher `json:"require,omitempty"`
Deferred bool `json:"deferred,omitempty"`
// Validate ensures h's configuration is valid.
func (h Handler) Validate() error {
if h.Request != nil {
err := h.Request.validate()
if err != nil {
return err
}
}
if h.Response != nil {
err := h.Response.validate()
if err != nil {
return err
}
}
return nil
}
func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
apply(h.Request, r.Header, repl)
// request header's Host is handled specially by the
// Go standard library, so if that header was changed,
// change it in the Host field since the Header won't
// be used
if intendedHost := r.Header.Get("Host"); intendedHost != "" {
r.Host = intendedHost
r.Header.Del("Host")
if h.Request != nil {
h.Request.ApplyToRequest(r)
}
if h.Response != nil {
@@ -79,32 +92,165 @@ func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhtt
headerOps: h.Response.HeaderOps,
}
} else {
apply(h.Response.HeaderOps, w.Header(), repl)
h.Response.ApplyTo(w.Header(), repl)
}
}
return next.ServeHTTP(w, r)
}
func apply(ops *HeaderOps, hdr http.Header, repl caddy.Replacer) {
if ops == nil {
return
// HeaderOps defines some operations to
// perform on HTTP headers.
type HeaderOps struct {
Add http.Header `json:"add,omitempty"`
Set http.Header `json:"set,omitempty"`
Delete []string `json:"delete,omitempty"`
Replace map[string][]Replacement `json:"replace,omitempty"`
}
func (ops *HeaderOps) provision() error {
for fieldName, replacements := range ops.Replace {
for i, r := range replacements {
if r.SearchRegexp != "" {
re, err := regexp.Compile(r.SearchRegexp)
if err != nil {
return fmt.Errorf("replacement %d for header field '%s': %v", i, fieldName, err)
}
replacements[i].re = re
}
}
}
return nil
}
func (ops HeaderOps) validate() error {
for fieldName, replacements := range ops.Replace {
for _, r := range replacements {
if r.Search != "" && r.SearchRegexp != "" {
return fmt.Errorf("cannot specify both a substring search and a regular expression search for field '%s'", fieldName)
}
}
}
return nil
}
// Replacement describes a string replacement,
// either a simple and fast sugbstring search
// or a slower but more powerful regex search.
type Replacement struct {
Search string `json:"search,omitempty"`
SearchRegexp string `json:"search_regexp,omitempty"`
Replace string `json:"replace,omitempty"`
re *regexp.Regexp
}
// RespHeaderOps is like HeaderOps, but
// optionally deferred until response time.
type RespHeaderOps struct {
*HeaderOps
Require *caddyhttp.ResponseMatcher `json:"require,omitempty"`
Deferred bool `json:"deferred,omitempty"`
}
// ApplyTo applies ops to hdr using repl.
func (ops HeaderOps) ApplyTo(hdr http.Header, repl caddy.Replacer) {
// add
for fieldName, vals := range ops.Add {
fieldName = repl.ReplaceAll(fieldName, "")
for _, v := range vals {
hdr.Add(fieldName, repl.ReplaceAll(v, ""))
}
}
// set
for fieldName, vals := range ops.Set {
fieldName = repl.ReplaceAll(fieldName, "")
var newVals []string
for i := range vals {
vals[i] = repl.ReplaceAll(vals[i], "")
// append to new slice so we don't overwrite
// the original values in ops.Set
newVals = append(newVals, repl.ReplaceAll(vals[i], ""))
}
hdr.Set(fieldName, strings.Join(vals, ","))
hdr.Set(fieldName, strings.Join(newVals, ","))
}
// delete
for _, fieldName := range ops.Delete {
hdr.Del(repl.ReplaceAll(fieldName, ""))
}
// replace
for fieldName, replacements := range ops.Replace {
fieldName = repl.ReplaceAll(fieldName, "")
// all fields...
if fieldName == "*" {
for _, r := range replacements {
search := repl.ReplaceAll(r.Search, "")
replace := repl.ReplaceAll(r.Replace, "")
for fieldName, vals := range hdr {
for i := range vals {
if r.re != nil {
hdr[fieldName][i] = r.re.ReplaceAllString(hdr[fieldName][i], replace)
} else {
hdr[fieldName][i] = strings.ReplaceAll(hdr[fieldName][i], search, replace)
}
}
}
}
continue
}
// ...or only with the named field
for _, r := range replacements {
search := repl.ReplaceAll(r.Search, "")
replace := repl.ReplaceAll(r.Replace, "")
for i := range hdr[fieldName] {
if r.re != nil {
hdr[fieldName][i] = r.re.ReplaceAllString(hdr[fieldName][i], replace)
} else {
hdr[fieldName][i] = strings.ReplaceAll(hdr[fieldName][i], search, replace)
}
}
}
}
}
// ApplyToRequest applies ops to r, specially handling the Host
// header which the standard library does not include with the
// header map with all the others. This method mutates r.Host.
func (ops HeaderOps) ApplyToRequest(r *http.Request) {
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
// capture the current Host header so we can
// reset to it when we're done
origHost, hadHost := r.Header["Host"]
// append r.Host; this way, we know that our value
// was last in the list, and if an Add operation
// appended something else after it, that's probably
// fine because it's weird to have multiple Host
// headers anyway and presumably the one they added
// is the one they wanted
r.Header["Host"] = append(r.Header["Host"], r.Host)
// apply header operations
ops.ApplyTo(r.Header, repl)
// retrieve the last Host value (likely the one we appended)
if len(r.Header["Host"]) > 0 {
r.Host = r.Header["Host"][len(r.Header["Host"])-1]
} else {
r.Host = ""
}
// reset the Host header slice
if hadHost {
r.Header["Host"] = origHost
} else {
delete(r.Header, "Host")
}
}
// responseWriterWrapper defers response header
@@ -123,7 +269,9 @@ func (rww *responseWriterWrapper) WriteHeader(status int) {
}
rww.wroteHeader = true
if rww.require == nil || rww.require.Match(status, rww.ResponseWriterWrapper.Header()) {
apply(rww.headerOps, rww.ResponseWriterWrapper.Header(), rww.replacer)
if rww.headerOps != nil {
rww.headerOps.ApplyTo(rww.ResponseWriterWrapper.Header(), rww.replacer)
}
}
rww.ResponseWriterWrapper.WriteHeader(status)
}
@@ -137,6 +285,7 @@ func (rww *responseWriterWrapper) Write(d []byte) (int, error) {
// Interface guards
var (
_ caddyhttp.MiddlewareHandler = (*Headers)(nil)
_ caddy.Provisioner = (*Handler)(nil)
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
_ caddyhttp.HTTPInterfaces = (*responseWriterWrapper)(nil)
)
+214
View File
@@ -0,0 +1,214 @@
// 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 httpcache
import (
"bytes"
"encoding/gob"
"fmt"
"io"
"log"
"net/http"
"sync"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/golang/groupcache"
)
func init() {
caddy.RegisterModule(Cache{})
}
// Cache implements a simple distributed cache.
type Cache struct {
Self string `json:"self,omitempty"`
Peers []string `json:"peers,omitempty"`
MaxSize int64 `json:"max_size,omitempty"`
group *groupcache.Group
}
// CaddyModule returns the Caddy module information.
func (Cache) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "http.handlers.cache",
New: func() caddy.Module { return new(Cache) },
}
}
// Provision provisions c.
func (c *Cache) Provision(ctx caddy.Context) error {
// TODO: use UsagePool so that cache survives config reloads - TODO: a single cache for whole process?
maxSize := c.MaxSize
if maxSize == 0 {
const maxMB = 512
maxSize = int64(maxMB << 20)
}
poolMu.Lock()
if pool == nil {
pool = groupcache.NewHTTPPool(c.Self)
c.group = groupcache.NewGroup(groupName, maxSize, groupcache.GetterFunc(c.getter))
} else {
c.group = groupcache.GetGroup(groupName)
}
pool.Set(append(c.Peers, c.Self)...)
poolMu.Unlock()
return nil
}
// Validate validates c.
func (c *Cache) Validate() error {
if c.Self == "" {
return fmt.Errorf("address of this instance (self) is required")
}
if c.MaxSize < 0 {
return fmt.Errorf("size must be greater than 0")
}
return nil
}
func (c *Cache) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
// TODO: proper RFC implementation of cache control headers...
if r.Header.Get("Cache-Control") == "no-cache" || (r.Method != "GET" && r.Method != "HEAD") {
return next.ServeHTTP(w, r)
}
ctx := getterContext{w, r, next}
// TODO: rigorous performance testing
// TODO: pretty much everything else to handle the nuances of HTTP caching...
// TODO: groupcache has no explicit cache eviction, so we need to embed
// all information related to expiring cache entries into the key; right
// now we just use the request URI as a proof-of-concept
key := r.RequestURI
var cachedBytes []byte
err := c.group.Get(ctx, key, groupcache.AllocatingByteSliceSink(&cachedBytes))
if err == errUncacheable {
return nil
}
if err != nil {
return err
}
// the cached bytes consists of two parts: first a
// gob encoding of the status and header, immediately
// followed by the raw bytes of the response body
rdr := bytes.NewReader(cachedBytes)
// read the header and status first
var hs headerAndStatus
err = gob.NewDecoder(rdr).Decode(&hs)
if err != nil {
return err
}
// set and write the cached headers
for k, v := range hs.Header {
w.Header()[k] = v
}
w.WriteHeader(hs.Status)
// write the cached response body
io.Copy(w, rdr)
return nil
}
func (c *Cache) getter(ctx groupcache.Context, key string, dest groupcache.Sink) error {
combo := ctx.(getterContext)
// the buffer will store the gob-encoded header, then the body
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
// we need to record the response if we are to cache it; only cache if
// request is successful (TODO: there's probably much more nuance needed here)
rr := caddyhttp.NewResponseRecorder(combo.rw, buf, func(status int, header http.Header) bool {
shouldBuf := status < 300
if shouldBuf {
// store the header before the body, so we can efficiently
// and conveniently use a single buffer for both; gob
// decoder will only read up to end of gob message, and
// the rest will be the body, which will be written
// implicitly for us by the recorder
err := gob.NewEncoder(buf).Encode(headerAndStatus{
Header: header,
Status: status,
})
if err != nil {
log.Printf("[ERROR] Encoding headers for cache entry: %v; not caching this request", err)
return false
}
}
return shouldBuf
})
// execute next handlers in chain
err := combo.next.ServeHTTP(rr, combo.req)
if err != nil {
return err
}
// if response body was not buffered, response was
// already written and we are unable to cache
if !rr.Buffered() {
return errUncacheable
}
// add to cache
dest.SetBytes(buf.Bytes())
return nil
}
type headerAndStatus struct {
Header http.Header
Status int
}
type getterContext struct {
rw http.ResponseWriter
req *http.Request
next caddyhttp.Handler
}
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
var (
pool *groupcache.HTTPPool
poolMu sync.Mutex
)
var errUncacheable = fmt.Errorf("uncacheable")
const groupName = "http_requests"
// Interface guards
var (
_ caddy.Provisioner = (*Cache)(nil)
_ caddy.Validator = (*Cache)(nil)
_ caddyhttp.MiddlewareHandler = (*Cache)(nil)
)
+4 -2
View File
@@ -48,8 +48,8 @@ func (m Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
buf.Reset()
defer bufPool.Put(buf)
shouldBuf := func(status int) bool {
return strings.HasPrefix(w.Header().Get("Content-Type"), "text/")
shouldBuf := func(status int, header http.Header) bool {
return strings.HasPrefix(header.Get("Content-Type"), "text/")
}
rec := caddyhttp.NewResponseRecorder(w, buf, shouldBuf)
@@ -62,6 +62,8 @@ func (m Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
return nil
}
caddyhttp.CopyHeader(w.Header(), rec.Header())
output := blackfriday.Run(buf.Bytes())
w.Header().Set("Content-Length", strconv.Itoa(len(output)))
+89
View File
@@ -0,0 +1,89 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyhttp
import (
"crypto/tls"
"net/http"
"go.uber.org/zap/zapcore"
)
// LoggableHTTPRequest makes an HTTP request loggable with zap.Object().
type LoggableHTTPRequest struct{ *http.Request }
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
enc.AddString("method", r.Method)
enc.AddString("uri", r.RequestURI)
enc.AddString("proto", r.Proto)
enc.AddString("remote_addr", r.RemoteAddr)
enc.AddString("host", r.Host)
enc.AddObject("headers", LoggableHTTPHeader(r.Header))
if r.TLS != nil {
enc.AddObject("tls", LoggableTLSConnState(*r.TLS))
}
return nil
}
// LoggableHTTPHeader makes an HTTP header loggable with zap.Object().
type LoggableHTTPHeader http.Header
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
func (h LoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error {
if h == nil {
return nil
}
for key, val := range h {
enc.AddArray(key, LoggableStringArray(val))
}
return nil
}
// LoggableStringArray makes a slice of strings marshalable for logging.
type LoggableStringArray []string
// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface.
func (sa LoggableStringArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
if sa == nil {
return nil
}
for _, s := range sa {
enc.AppendString(s)
}
return nil
}
// LoggableTLSConnState makes a TLS connection state loggable with zap.Object().
type LoggableTLSConnState tls.ConnectionState
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
func (t LoggableTLSConnState) MarshalLogObject(enc zapcore.ObjectEncoder) error {
enc.AddBool("resumed", t.DidResume)
enc.AddUint16("version", t.Version)
enc.AddUint16("resumed", t.CipherSuite)
enc.AddString("proto", t.NegotiatedProtocol)
enc.AddBool("proto_mutual", t.NegotiatedProtocolIsMutual)
enc.AddString("server_name", t.ServerName)
return nil
}
// Interface guards
var (
_ zapcore.ObjectMarshaler = (*LoggableHTTPRequest)(nil)
_ zapcore.ObjectMarshaler = (*LoggableHTTPHeader)(nil)
_ zapcore.ArrayMarshaler = (*LoggableStringArray)(nil)
_ zapcore.ObjectMarshaler = (*LoggableTLSConnState)(nil)
)
+58 -8
View File
@@ -66,9 +66,9 @@ type (
// MatchNegate matches requests by negating its matchers' results.
MatchNegate struct {
matchersRaw map[string]json.RawMessage
MatchersRaw map[string]json.RawMessage `json:"-"`
matchers MatcherSet
Matchers MatcherSet `json:"-"`
}
// MatchStarlarkExpr matches requests by evaluating a Starlark expression.
@@ -112,10 +112,17 @@ func (m MatchHost) Match(r *http.Request) bool {
if err != nil {
// OK; probably didn't have a port
reqHost = r.Host
// make sure we strip the brackets from IPv6 addresses
reqHost = strings.TrimPrefix(reqHost, "[")
reqHost = strings.TrimSuffix(reqHost, "]")
}
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
outer:
for _, host := range m {
host = repl.ReplaceAll(host, "")
if strings.Contains(host, "*") {
patternParts := strings.Split(host, ".")
incomingParts := strings.Split(reqHost, ".")
@@ -400,32 +407,72 @@ func (MatchNegate) CaddyModule() caddy.ModuleInfo {
// the struct, but we need a struct because we need another
// field just for the provisioned modules.
func (m *MatchNegate) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &m.matchersRaw)
return json.Unmarshal(data, &m.MatchersRaw)
}
// MarshalJSON marshals m's matchers.
func (m MatchNegate) MarshalJSON() ([]byte, error) {
return json.Marshal(m.MatchersRaw)
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (m *MatchNegate) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// TODO: figure out how this will work
// first, unmarshal each matcher in the set from its tokens
matcherMap := make(map[string]RequestMatcher)
for d.Next() {
for d.NextBlock(0) {
matcherName := d.Val()
mod, err := caddy.GetModule("http.matchers." + matcherName)
if err != nil {
return d.Errf("getting matcher module '%s': %v", matcherName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return d.Errf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
}
err = unm.UnmarshalCaddyfile(d.NewFromNextTokens())
if err != nil {
return err
}
rm := unm.(RequestMatcher)
m.Matchers = append(m.Matchers, rm)
matcherMap[matcherName] = rm
}
}
// we should now be functional, but we also need
// to be able to marshal as JSON, otherwise config
// adaptation won't work properly
m.MatchersRaw = make(map[string]json.RawMessage)
for name, matchers := range matcherMap {
jsonBytes, err := json.Marshal(matchers)
if err != nil {
return fmt.Errorf("marshaling matcher %s: %v", name, err)
}
m.MatchersRaw[name] = jsonBytes
}
return nil
}
// Provision loads the matcher modules to be negated.
func (m *MatchNegate) Provision(ctx caddy.Context) error {
for modName, rawMsg := range m.matchersRaw {
for modName, rawMsg := range m.MatchersRaw {
val, err := ctx.LoadModule("http.matchers."+modName, rawMsg)
if err != nil {
return fmt.Errorf("loading matcher module '%s': %v", modName, err)
}
m.matchers = append(m.matchers, val.(RequestMatcher))
m.Matchers = append(m.Matchers, val.(RequestMatcher))
}
m.matchersRaw = nil // allow GC to deallocate - TODO: Does this help?
m.MatchersRaw = nil // allow GC to deallocate
return nil
}
// Match returns true if r matches m. Since this matcher negates the
// embedded matchers, false is returned if any of its matchers match.
func (m MatchNegate) Match(r *http.Request) bool {
return !m.matchers.Match(r)
return !m.Matchers.Match(r)
}
// CaddyModule returns the Caddy module information.
@@ -686,4 +733,7 @@ var (
_ caddyfile.Unmarshaler = (*MatchHeaderRE)(nil)
_ caddyfile.Unmarshaler = (*MatchProtocol)(nil)
_ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
_ json.Marshaler = (*MatchNegate)(nil)
_ json.Unmarshaler = (*MatchNegate)(nil)
)
+52
View File
@@ -20,12 +20,18 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"github.com/caddyserver/caddy/v2"
)
func TestHostMatcher(t *testing.T) {
err := os.Setenv("GO_BENCHMARK_DOMAIN", "localhost")
if err != nil {
t.Errorf("error while setting up environment: %v", err)
}
for i, tc := range []struct {
match MatchHost
input string
@@ -106,8 +112,22 @@ func TestHostMatcher(t *testing.T) {
input: "example.com:5555",
expect: true,
},
{
match: MatchHost{"{env.GO_BENCHMARK_DOMAIN}"},
input: "localhost",
expect: true,
},
{
match: MatchHost{"{env.GO_NONEXISTENT}"},
input: "localhost",
expect: false,
},
} {
req := &http.Request{Host: tc.input}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
actual := tc.match.Match(req)
if actual != tc.expect {
t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
@@ -518,3 +538,35 @@ func TestResponseMatcher(t *testing.T) {
}
}
}
func BenchmarkHostMatcherWithoutPlaceholder(b *testing.B) {
req := &http.Request{Host: "localhost"}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
match := MatchHost{"localhost"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
match.Match(req)
}
}
func BenchmarkHostMatcherWithPlaceholder(b *testing.B) {
err := os.Setenv("GO_BENCHMARK_DOMAIN", "localhost")
if err != nil {
b.Errorf("error while setting up environment: %v", err)
}
req := &http.Request{Host: "localhost"}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
match := MatchHost{"{env.GO_BENCHMARK_DOMAIN}"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
match.Match(req)
}
}
+60 -12
View File
@@ -59,24 +59,39 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
}
switch key {
case "http.request.method":
return req.Method, true
case "http.request.scheme":
if req.TLS != nil {
return "https", true
}
return "http", true
case "http.request.proto":
return req.Proto, true
case "http.request.host":
host, _, err := net.SplitHostPort(req.Host)
if err != nil {
return req.Host, true // OK; there probably was no port
}
return host, true
case "http.request.hostport":
return req.Host, true
case "http.request.method":
return req.Method, true
case "http.request.port":
_, port, _ := net.SplitHostPort(req.Host)
return port, true
case "http.request.scheme":
if req.TLS != nil {
return "https", true
case "http.request.hostport":
return req.Host, true
case "http.request.remote":
return req.RemoteAddr, true
case "http.request.remote.host":
host, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return req.RemoteAddr, true
}
return "http", true
return host, true
case "http.request.remote.port":
_, port, _ := net.SplitHostPort(req.RemoteAddr)
return port, true
// current URI, including any internal rewrites
case "http.request.uri":
return req.URL.RequestURI(), true
case "http.request.uri.path":
@@ -95,6 +110,35 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
qs = "?" + qs
}
return qs, true
// original request, before any internal changes
case "http.request.orig_method":
or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
return or.Method, true
case "http.request.orig_uri":
or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
return or.RequestURI, true
case "http.request.orig_uri.path":
or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
return or.URL.Path, true
case "http.request.orig_uri.path.file":
or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
_, file := path.Split(or.URL.Path)
return file, true
case "http.request.orig_uri.path.dir":
or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
dir, _ := path.Split(or.URL.Path)
return dir, true
case "http.request.orig_uri.query":
or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
return or.URL.RawQuery, true
case "http.request.orig_uri.query_string":
or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
qs := or.URL.Query().Encode()
if qs != "" {
qs = "?" + qs
}
return qs, true
}
// hostname labels
@@ -104,14 +148,18 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
if err != nil {
return "", false
}
hostLabels := strings.Split(req.Host, ".")
reqHost, _, err := net.SplitHostPort(req.Host)
if err != nil {
reqHost = req.Host // OK; assume there was no port
}
hostLabels := strings.Split(reqHost, ".")
if idx < 0 {
return "", false
}
if idx >= len(hostLabels) {
if idx > len(hostLabels) {
return "", true
}
return hostLabels[idx], true
return hostLabels[len(hostLabels)-idx-1], true
}
// path parts
@@ -137,7 +185,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
// middleware variables
if strings.HasPrefix(key, varsReplPrefix) {
varName := key[len(varsReplPrefix):]
tbl := req.Context().Value(VarCtxKey).(map[string]interface{})
tbl := req.Context().Value(VarsCtxKey).(map[string]interface{})
raw, ok := tbl[varName]
if !ok {
// variables can be dynamic, so always return true
+79
View File
@@ -0,0 +1,79 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyhttp
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/caddyserver/caddy/v2"
)
func TestHTTPVarReplacement(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
req.Host = "example.com:80"
req.RemoteAddr = "localhost:1234"
res := httptest.NewRecorder()
addHTTPVarsToReplacer(repl, req, res)
for i, tc := range []struct {
input string
expect string
}{
{
input: "{http.request.scheme}",
expect: "http",
},
{
input: "{http.request.host}",
expect: "example.com",
},
{
input: "{http.request.port}",
expect: "80",
},
{
input: "{http.request.hostport}",
expect: "example.com:80",
},
{
input: "{http.request.remote.host}",
expect: "localhost",
},
{
input: "{http.request.remote.port}",
expect: "1234",
},
{
input: "{http.request.host.labels.0}",
expect: "com",
},
{
input: "{http.request.host.labels.1}",
expect: "example",
},
} {
actual := repl.ReplaceAll(tc.input, "<empty>")
if actual != tc.expect {
t.Errorf("Test %d: Expected placeholder %s to be '%s' but got '%s'",
i, tc.input, tc.expect, actual)
}
}
}
+107 -32
View File
@@ -18,6 +18,7 @@ import (
"bufio"
"bytes"
"fmt"
"io"
"net"
"net/http"
)
@@ -78,49 +79,89 @@ type responseRecorder struct {
wroteHeader bool
statusCode int
buf *bytes.Buffer
shouldBuffer func(status int) bool
shouldBuffer ShouldBufferFunc
stream bool
size int
header http.Header
}
// NewResponseRecorder returns a new ResponseRecorder that can be
// used instead of a real http.ResponseWriter. The recorder is useful
// for middlewares which need to buffer a responder's response and
// process it in its entirety before actually allowing the response to
// be written. Of course, this has a performance overhead, but
// sometimes there is no way to avoid buffering the whole response.
// Still, if at all practical, middlewares should strive to stream
// used instead of a standard http.ResponseWriter. The recorder is
// useful for middlewares which need to buffer a response and
// potentially process its entire body before actually writing the
// response to the underlying writer. Of course, buffering the entire
// body has a memory overhead, but sometimes there is no way to avoid
// buffering the whole response, hence the existence of this type.
// Still, if at all practical, handlers should strive to stream
// responses by wrapping Write and WriteHeader methods instead of
// buffering whole response bodies.
//
// Recorders optionally buffer the response. When the headers are
// to be written, shouldBuffer will be called with the status
// code that is being written. The rest of the headers can be read
// from w.Header(). If shouldBuffer returns true, the response
// will be buffered. You can know the response was buffered if
// the Buffered() method returns true. If the response was not
// buffered, Buffered() will return false and that means the
// response bypassed the recorder and was written directly to the
// underlying writer.
// Buffering is actually optional. The shouldBuffer function will
// be called just before the headers are written. If it returns
// true, the headers and body will be buffered by this recorder
// and not written to the underlying writer; if false, the headers
// will be written immediately and the body will be streamed out
// directly to the underlying writer. If shouldBuffer is nil,
// the response will never be buffered and will always be streamed
// directly to the writer.
//
// Before calling this function in a middleware handler, make a
// new buffer or obtain one from a pool (use the sync.Pool) type.
// Using a pool is generally recommended for performance gains;
// do profiling to ensure this is the case. If using a pool, be
// sure to reset the buffer before using it.
// You can know if shouldBuffer returned true by calling Buffered().
//
// The returned recorder can be used in place of w when calling
// the next handler in the chain. When that handler returns, you
// can read the status code from the recorder's Status() method.
// The response body fills buf if it was buffered, and the headers
// are available via w.Header().
func NewResponseRecorder(w http.ResponseWriter, buf *bytes.Buffer, shouldBuffer func(status int) bool) ResponseRecorder {
// The provided buffer buf should be obtained from a pool for best
// performance (see the sync.Pool type).
//
// Proper usage of a recorder looks like this:
//
// rec := caddyhttp.NewResponseRecorder(w, buf, shouldBuffer)
// err := next.ServeHTTP(rec, req)
// if err != nil {
// return err
// }
// if !rec.Buffered() {
// return nil
// }
// // process the buffered response here
//
// After a response has been buffered, remember that any upstream header
// manipulations are only manifest in the recorder's Header(), not the
// Header() of the underlying ResponseWriter. Thus if you wish to inspect
// or change response headers, you either need to use rec.Header(), or
// copy rec.Header() into w.Header() first (see caddyhttp.CopyHeader).
//
// Once you are ready to write the response, there are two ways you can do
// it. The easier way is to have the recorder do it:
//
// rec.WriteResponse()
//
// This writes the recorded response headers as well as the buffered body.
// Or, you may wish to do it yourself, especially if you manipulated the
// buffered body. First you will need to copy the recorded headers, then
// write the headers with the recorded status code, then write the body
// (this example writes the recorder's body buffer, but you might have
// your own body to write instead):
//
// caddyhttp.CopyHeader(w.Header(), rec.Header())
// w.WriteHeader(rec.Status())
// io.Copy(w, rec.Buffer())
//
func NewResponseRecorder(w http.ResponseWriter, buf *bytes.Buffer, shouldBuffer ShouldBufferFunc) ResponseRecorder {
// copy the current response header into this buffer so
// that any header manipulations on the buffered header
// are consistent with what would be written out
hdr := make(http.Header)
CopyHeader(hdr, w.Header())
return &responseRecorder{
ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: w},
buf: buf,
shouldBuffer: shouldBuffer,
header: hdr,
}
}
func (rr *responseRecorder) Header() http.Header {
return rr.header
}
func (rr *responseRecorder) WriteHeader(statusCode int) {
if rr.wroteHeader {
return
@@ -130,20 +171,31 @@ func (rr *responseRecorder) WriteHeader(statusCode int) {
// decide whether we should buffer the response
if rr.shouldBuffer == nil {
return
rr.stream = true
} else {
rr.stream = !rr.shouldBuffer(rr.statusCode, rr.header)
}
rr.stream = !rr.shouldBuffer(rr.statusCode)
// if not buffered, immediately write header
if rr.stream {
CopyHeader(rr.ResponseWriterWrapper.Header(), rr.header)
rr.ResponseWriterWrapper.WriteHeader(rr.statusCode)
}
}
func (rr *responseRecorder) Write(data []byte) (int, error) {
rr.WriteHeader(http.StatusOK)
var n int
var err error
if rr.stream {
return rr.ResponseWriterWrapper.Write(data)
n, err = rr.ResponseWriterWrapper.Write(data)
} else {
n, err = rr.buf.Write(data)
}
return rr.buf.Write(data)
if err == nil {
rr.size += n
}
return n, err
}
// Status returns the status code that was written, if any.
@@ -151,6 +203,12 @@ func (rr *responseRecorder) Status() int {
return rr.statusCode
}
// Size returns the number of bytes written,
// not including the response headers.
func (rr *responseRecorder) Size() int {
return rr.size
}
// Buffer returns the body buffer that rr was created with.
// You should still have your original pointer, though.
func (rr *responseRecorder) Buffer() *bytes.Buffer {
@@ -162,15 +220,32 @@ func (rr *responseRecorder) Buffered() bool {
return !rr.stream
}
func (rr *responseRecorder) WriteResponse() error {
if rr.stream {
return nil
}
CopyHeader(rr.ResponseWriterWrapper.Header(), rr.header)
rr.ResponseWriterWrapper.WriteHeader(rr.statusCode)
_, err := io.Copy(rr.ResponseWriterWrapper, rr.buf)
return err
}
// ResponseRecorder is a http.ResponseWriter that records
// responses instead of writing them to the client.
// responses instead of writing them to the client. See
// docs for NewResponseRecorder for proper usage.
type ResponseRecorder interface {
HTTPInterfaces
Status() int
Buffer() *bytes.Buffer
Buffered() bool
Size() int
WriteResponse() error
}
// ShouldBufferFunc is a function that returns true if the
// response should be buffered, given the pending HTTP status
// code and response headers.
type ShouldBufferFunc func(status int, header http.Header) bool
// Interface guards
var (
_ HTTPInterfaces = (*ResponseWriterWrapper)(nil)
+49 -6
View File
@@ -25,6 +25,7 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
"github.com/dustin/go-humanize"
)
@@ -67,6 +68,10 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// unhealthy_status <status>
// unhealthy_latency <duration>
//
// # header manipulation
// header_up [+|-]<field> [<value|regexp> [<replacement>]]
// header_down [+|-]<field> [<value|regexp> [<replacement>]]
//
// # round trip
// transport <name> {
// ...
@@ -76,9 +81,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
for _, up := range d.RemainingArgs() {
h.Upstreams = append(h.Upstreams, &Upstream{
Dial: up,
})
h.Upstreams = append(h.Upstreams, &Upstream{Dial: up})
}
for d.NextBlock(0) {
@@ -89,9 +92,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.ArgErr()
}
for _, up := range args {
h.Upstreams = append(h.Upstreams, &Upstream{
Dial: up,
})
h.Upstreams = append(h.Upstreams, &Upstream{Dial: up})
}
case "lb_policy":
@@ -327,6 +328,46 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
h.HealthChecks.Passive.UnhealthyLatency = caddy.Duration(dur)
case "header_up":
if h.Headers == nil {
h.Headers = new(headers.Handler)
}
if h.Headers.Request == nil {
h.Headers.Request = new(headers.HeaderOps)
}
args := d.RemainingArgs()
switch len(args) {
case 1:
headers.CaddyfileHeaderOp(h.Headers.Request, args[0], "", "")
case 2:
headers.CaddyfileHeaderOp(h.Headers.Request, args[0], args[1], "")
case 3:
headers.CaddyfileHeaderOp(h.Headers.Request, args[0], args[1], args[2])
default:
return d.ArgErr()
}
case "header_down":
if h.Headers == nil {
h.Headers = new(headers.Handler)
}
if h.Headers.Response == nil {
h.Headers.Response = &headers.RespHeaderOps{
HeaderOps: new(headers.HeaderOps),
}
}
args := d.RemainingArgs()
switch len(args) {
case 1:
headers.CaddyfileHeaderOp(h.Headers.Response.HeaderOps, args[0], "", "")
case 2:
headers.CaddyfileHeaderOp(h.Headers.Response.HeaderOps, args[0], args[1], "")
case 3:
headers.CaddyfileHeaderOp(h.Headers.Response.HeaderOps, args[0], args[1], args[2])
default:
return d.ArgErr()
}
case "transport":
if !d.NextArg() {
return d.ArgErr()
@@ -457,6 +498,7 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if d.Val() == "off" {
var disable bool
h.KeepAlive.Enabled = &disable
break
}
dur, err := time.ParseDuration(d.Val())
if err != nil {
@@ -476,6 +518,7 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
h.KeepAlive = new(KeepAlive)
}
h.KeepAlive.MaxIdleConns = num
h.KeepAlive.MaxIdleConnsPerHost = num
default:
return d.Errf("unrecognized subdirective %s", d.Val())
@@ -0,0 +1,152 @@
// 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 reverseproxy
import (
"fmt"
"sync/atomic"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/vulcand/oxy/memmetrics"
)
func init() {
caddy.RegisterModule(localCircuitBreaker{})
}
// localCircuitBreaker implements circuit breaking functionality
// for requests within this process over a sliding time window.
type localCircuitBreaker struct {
tripped int32
cbType int32
threshold float64
metrics *memmetrics.RTMetrics
tripTime time.Duration
Config
}
// CaddyModule returns the Caddy module information.
func (localCircuitBreaker) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "http.handlers.reverse_proxy.circuit_breakers.local",
New: func() caddy.Module { return new(localCircuitBreaker) },
}
}
// Provision sets up a configured circuit breaker.
func (c *localCircuitBreaker) Provision(ctx caddy.Context) error {
t, ok := typeCB[c.Type]
if !ok {
return fmt.Errorf("type is not defined")
}
if c.TripTime == "" {
c.TripTime = defaultTripTime
}
tw, err := time.ParseDuration(c.TripTime)
if err != nil {
return fmt.Errorf("cannot parse trip_time duration, %v", err.Error())
}
mt, err := memmetrics.NewRTMetrics()
if err != nil {
return fmt.Errorf("cannot create new metrics: %v", err.Error())
}
c.cbType = t
c.tripTime = tw
c.threshold = c.Threshold
c.metrics = mt
c.tripped = 0
return nil
}
// Ok returns whether the circuit breaker is tripped or not.
func (c *localCircuitBreaker) Ok() bool {
tripped := atomic.LoadInt32(&c.tripped)
return tripped == 0
}
// RecordMetric records a response status code and execution time of a request. This function should be run in a separate goroutine.
func (c *localCircuitBreaker) RecordMetric(statusCode int, latency time.Duration) {
c.metrics.Record(statusCode, latency)
c.checkAndSet()
}
// Ok checks our metrics to see if we should trip our circuit breaker, or if the fallback duration has completed.
func (c *localCircuitBreaker) checkAndSet() {
var isTripped bool
switch c.cbType {
case typeErrorRatio:
// check if amount of network errors exceed threshold over sliding window, threshold for comparison should be < 1.0 i.e. .5 = 50th percentile
if c.metrics.NetworkErrorRatio() > c.threshold {
isTripped = true
}
case typeLatency:
// check if threshold in milliseconds is reached and trip
hist, err := c.metrics.LatencyHistogram()
if err != nil {
return
}
l := hist.LatencyAtQuantile(c.threshold)
if l.Nanoseconds()/int64(time.Millisecond) > int64(c.threshold) {
isTripped = true
}
case typeStatusCodeRatio:
// check ratio of error status codes of sliding window, threshold for comparison should be < 1.0 i.e. .5 = 50th percentile
if c.metrics.ResponseCodeRatio(500, 600, 0, 600) > c.threshold {
isTripped = true
}
}
if isTripped {
c.metrics.Reset()
atomic.AddInt32(&c.tripped, 1)
// wait tripTime amount before allowing operations to resume.
t := time.NewTimer(c.tripTime)
<-t.C
atomic.AddInt32(&c.tripped, -1)
}
}
// Config represents the configuration of a circuit breaker.
type Config struct {
Threshold float64 `json:"threshold"`
Type string `json:"type"`
TripTime string `json:"trip_time"`
}
const (
typeLatency = iota + 1
typeErrorRatio
typeStatusCodeRatio
defaultTripTime = "5s"
)
var (
// typeCB handles converting a Config Type value to the internal circuit breaker types.
typeCB = map[string]int32{
"latency": typeLatency,
"error_ratio": typeErrorRatio,
"status_ratio": typeStatusCodeRatio,
}
)
+138
View File
@@ -0,0 +1,138 @@
// 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 reverseproxy
import (
"encoding/json"
"flag"
"log"
"net/http"
"net/url"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
"github.com/mholt/certmagic"
)
func init() {
caddycmd.RegisterCommand(caddycmd.Command{
Name: "reverse-proxy",
Func: cmdReverseProxy,
Usage: "[--from <addr>] [--to <addr>]",
Short: "A quick and production-ready reverse proxy",
Long: `
A simple but production-ready reverse proxy. Useful for quick deployments,
demos, and development.
Simply shuttles HTTP traffic from the --from address to the --to address.
If the --from address has a domain name, Caddy will attempt to serve the
proxy over HTTPS with a certificate.
`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("file-server", flag.ExitOnError)
fs.String("from", "", "Address to receive traffic on")
fs.String("to", "", "Upstream address to proxy traffic to")
return fs
}(),
})
}
func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
from := fs.String("from")
to := fs.String("to")
if from == "" {
from = "localhost:" + httpcaddyfile.DefaultPort
}
if !strings.Contains(from, "://") {
from = "http://" + from
}
fromURL, err := url.Parse(from)
if err != nil {
fromURL.Host = from
}
toURL, err := url.Parse(to)
if err != nil {
toURL.Host = to
}
ht := HTTPTransport{}
if toURL.Scheme == "https" {
ht.TLS = new(TLSConfig)
}
handler := Handler{
TransportRaw: caddyconfig.JSONModuleObject(ht, "protocol", "http", nil),
Upstreams: UpstreamPool{{Dial: toURL.Host}},
Headers: &headers.Handler{
Request: &headers.HeaderOps{
Set: http.Header{
"Host": []string{"{http.handlers.reverse_proxy.upstream.host}"},
},
},
},
}
route := caddyhttp.Route{
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", nil),
},
}
if fromURL.Hostname() != "" {
route.MatcherSetsRaw = []map[string]json.RawMessage{
map[string]json.RawMessage{
"host": caddyconfig.JSON(caddyhttp.MatchHost{fromURL.Hostname()}, nil),
},
}
}
listen := ":" + httpcaddyfile.DefaultPort
if certmagic.HostQualifies(fromURL.Hostname()) {
listen = ":443"
}
server := &caddyhttp.Server{
Routes: caddyhttp.RouteList{route},
Listen: []string{listen},
}
httpApp := caddyhttp.App{
Servers: map[string]*caddyhttp.Server{"proxy": server},
}
cfg := &caddy.Config{
AppsRaw: map[string]json.RawMessage{
"http": caddyconfig.JSON(httpApp, nil),
},
}
err = caddy.Run(cfg)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
log.Printf("Caddy 2 proxying from %s to %s", from, to)
select {}
}
@@ -16,6 +16,7 @@ package fastcgi
import (
"encoding/json"
"net/http"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -81,12 +82,17 @@ func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
//
// is equivalent to:
//
// matcher indexFiles {
// matcher canonicalPath {
// file {
// try_files {path} index.php
// try_files {path}/index.php
// }
// not {
// path */
// }
// }
// rewrite match:indexFiles {http.matchers.file.relative}
// redir match:canonicalPath {path}/ 308
//
// try_files {path} {path}/index.php index.php
//
// matcher phpFiles {
// path *.php
@@ -100,8 +106,8 @@ func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Thus, this directive produces multiple routes, each with a different
// matcher because multiple consecutive routes are necessary to support
// the common PHP use case. If this "common" config is not compatible
// with a user's PHP requirements, they can use the manual approach as
// above to configure it precisely as they need.
// with a user's PHP requirements, they can use a manual approach based
// on the example above to configure it precisely as they need.
//
// If a matcher is specified by the user, for example:
//
@@ -114,10 +120,30 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
return nil, h.ArgErr()
}
// route to redirect to canonical path if index PHP file
redirMatcherSet := map[string]json.RawMessage{
"file": h.JSON(fileserver.MatchFile{
TryFiles: []string{"{http.request.uri.path}/index.php"},
}, nil),
"not": h.JSON(caddyhttp.MatchNegate{
MatchersRaw: map[string]json.RawMessage{
"path": h.JSON(caddyhttp.MatchPath{"*/"}, nil),
},
}, nil),
}
redirHandler := caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString("308"),
Headers: http.Header{"Location": []string{"{http.request.uri.path}/"}},
}
redirRoute := caddyhttp.Route{
MatcherSetsRaw: []map[string]json.RawMessage{redirMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
}
// route to rewrite to PHP index file
rewriteMatcherSet := map[string]json.RawMessage{
"file": h.JSON(fileserver.MatchFile{
TryFiles: []string{"{http.request.uri.path}", "index.php"},
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php", "index.php"},
}, nil),
}
rewriteHandler := rewrite.Rewrite{
@@ -175,7 +201,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
// wrap ours in a subroute and return that
if hasUserMatcher {
subroute := caddyhttp.Subroute{
Routes: caddyhttp.RouteList{rewriteRoute, rpRoute},
Routes: caddyhttp.RouteList{redirRoute, rewriteRoute, rpRoute},
}
return []httpcaddyfile.ConfigValue{
{
@@ -191,6 +217,10 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
// if the user did not specify a matcher, then
// we can just use our own matchers
return []httpcaddyfile.ConfigValue{
{
Class: "route",
Value: redirRoute,
},
{
Class: "route",
Value: rewriteRoute,
@@ -19,7 +19,6 @@ import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
"path"
"path/filepath"
"strconv"
@@ -53,6 +52,9 @@ type Transport struct {
// with the value of SplitPath. The first piece will be assumed as the
// actual resource (CGI script) name, and the second piece will be set to
// PATH_INFO for the CGI script to use.
// Future enhancements should be careful to avoid CVE-2019-11043,
// which can be mitigated with use of a try_files-like behavior
// that 404's if the fastcgi path info is not found.
SplitPath string `json:"split_path,omitempty"`
// Extra environment variables
@@ -110,6 +112,7 @@ func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) {
fcgiBackend, err := DialContext(ctx, network, address)
if err != nil {
// TODO: wrap in a special error type if the dial failed, so retries can happen if enabled
return nil, fmt.Errorf("dialing backend: %v", err)
}
// fcgiBackend gets closed when response body is closed (see clientCloser)
@@ -190,12 +193,13 @@ func (t Transport) buildEnv(r *http.Request) (map[string]string, error) {
// original URI in as the value of REQUEST_URI (the user can overwrite this
// if desired). Most PHP apps seem to want the original URI. Besides, this is
// how nginx defaults: http://stackoverflow.com/a/12485156/1048862
reqURL, ok := r.Context().Value(caddyhttp.OriginalURLCtxKey).(url.URL)
origReq, ok := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
if !ok {
// some requests, like active health checks, don't add this to
// the request context, so we can just use the current URL
reqURL = *r.URL
origReq = *r
}
reqURL := origReq.URL
requestScheme := "http"
if r.TLS != nil {
@@ -43,6 +43,7 @@ type HealthChecks struct {
type ActiveHealthChecks struct {
Path string `json:"path,omitempty"`
Port int `json:"port,omitempty"`
Headers http.Header `json:"headers,omitempty"`
Interval caddy.Duration `json:"interval,omitempty"`
Timeout caddy.Duration `json:"timeout,omitempty"`
MaxSize int64 `json:"max_size,omitempty"`
@@ -111,10 +112,10 @@ func (h *Handler) doActiveHealthChecksForAllHosts() {
if network == "unix" || network == "unixgram" || network == "unixpacket" {
// this will be used as the Host portion of a http.Request URL, and
// paths to socket files would produce an error when creating URL,
// so use a fake Host value instead
hostAddr = network
// so use a fake Host value instead; unix sockets are usually local
hostAddr = "localhost"
}
err = h.doActiveHealthCheck(DialInfo{network, addrs[0]}, hostAddr, host)
err = h.doActiveHealthCheck(DialInfo{Network: network, Address: addrs[0]}, hostAddr, host)
if err != nil {
log.Printf("[ERROR] reverse_proxy: active health check for host %s: %v", networkAddr, err)
}
@@ -163,6 +164,9 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, host H
if err != nil {
return fmt.Errorf("making request: %v", err)
}
for key, hdrs := range h.HealthChecks.Active.Headers {
req.Header[key] = hdrs
}
// do the request, being careful to tame the response body
resp, err := h.HealthChecks.Active.httpClient.Do(req)
@@ -255,7 +259,7 @@ func (h *Handler) countFailure(upstream *Upstream) {
err := upstream.Host.CountFail(1)
if err != nil {
log.Printf("[ERROR] proxy: upstream %s: counting failure: %v",
upstream.dialInfo, err)
upstream.Dial, err)
}
// forget it later
@@ -264,7 +268,7 @@ func (h *Handler) countFailure(upstream *Upstream) {
err := host.CountFail(-1)
if err != nil {
log.Printf("[ERROR] proxy: upstream %s: expiring failure: %v",
upstream.dialInfo, err)
upstream.Dial, err)
}
}(upstream.Host, failDuration)
}
+41 -6
View File
@@ -16,6 +16,8 @@ package reverseproxy
import (
"fmt"
"net"
"strings"
"sync/atomic"
"github.com/caddyserver/caddy/v2"
@@ -70,7 +72,6 @@ type Upstream struct {
healthCheckPolicy *PassiveHealthChecks
cb CircuitBreaker
dialInfo DialInfo
}
// Available returns true if the remote host
@@ -147,8 +148,7 @@ func (uh *upstreamHost) CountFail(delta int) error {
}
// SetHealthy sets the upstream has healthy or unhealthy
// and returns true if the value was different from before,
// or an error if the adjustment failed.
// and returns true if the new value is different.
func (uh *upstreamHost) SetHealthy(healthy bool) (bool, error) {
var unhealthy, compare int32 = 1, 0
if healthy {
@@ -165,21 +165,56 @@ func (uh *upstreamHost) SetHealthy(healthy bool) (bool, error) {
// a host that can be represented in a URL, but
// they certainly have a network name and address).
type DialInfo struct {
// The network to use. This should be one of the
// values that is accepted by net.Dial:
// Upstream is the Upstream associated with
// this DialInfo. It may be nil.
Upstream *Upstream
// The network to use. This should be one of
// the values that is accepted by net.Dial:
// https://golang.org/pkg/net/#Dial
Network string
// The address to dial. Follows the same
// semantics and rules as net.Dial.
Address string
// Host and Port are components of Address.
Host, Port string
}
// String returns the Caddy network address form
// by joining the network and address with a
// forward slash.
func (di DialInfo) String() string {
return di.Network + "/" + di.Address
return caddy.JoinNetworkAddress(di.Network, di.Host, di.Port)
}
// fillDialInfo returns a filled DialInfo for the given upstream, using
// the given Replacer. Note that the returned value is not a pointer.
func fillDialInfo(upstream *Upstream, repl caddy.Replacer) (DialInfo, error) {
dial := repl.ReplaceAll(upstream.Dial, "")
netw, addrs, err := caddy.ParseNetworkAddress(dial)
if err != nil {
return DialInfo{}, fmt.Errorf("upstream %s: invalid dial address %s: %v", upstream.Dial, dial, err)
}
if len(addrs) != 1 {
return DialInfo{}, fmt.Errorf("upstream %s: dial address must represent precisely one socket: %s represents %d",
upstream.Dial, dial, len(addrs))
}
var dialHost, dialPort string
if !strings.Contains(netw, "unix") {
dialHost, dialPort, err = net.SplitHostPort(addrs[0])
if err != nil {
dialHost = addrs[0] // assume there was no port
}
}
return DialInfo{
Upstream: upstream,
Network: netw,
Address: addrs[0],
Host: dialHost,
Port: dialPort,
}, nil
}
// DialInfoCtxKey is used to store a DialInfo
@@ -26,6 +26,7 @@ import (
"time"
"github.com/caddyserver/caddy/v2"
"golang.org/x/net/http2"
)
func init() {
@@ -79,7 +80,14 @@ func (h *HTTPTransport) Provision(_ caddy.Context) error {
network = dialInfo.Network
address = dialInfo.Address
}
return dialer.DialContext(ctx, network, address)
conn, err := dialer.DialContext(ctx, network, address)
if err != nil {
// identify this error as one that occurred during
// dialing, which can be important when trying to
// decide whether to retry a request
return nil, DialError{err}
}
return conn, nil
},
MaxConnsPerHost: h.MaxConnsPerHost,
ResponseHeaderTimeout: time.Duration(h.ResponseHeaderTimeout),
@@ -113,6 +121,10 @@ func (h *HTTPTransport) Provision(_ caddy.Context) error {
rt.DisableCompression = !*h.Compression
}
if err := http2.ConfigureTransport(rt); err != nil {
return err
}
h.RoundTripper = rt
return nil
@@ -123,6 +135,14 @@ func (h HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return h.RoundTripper.RoundTrip(req)
}
// Cleanup implements caddy.CleanerUpper and closes any idle connections.
func (h HTTPTransport) Cleanup() error {
if ht, ok := h.RoundTripper.(*http.Transport); ok {
ht.CloseIdleConnections()
}
return nil
}
// TLSConfig holds configuration related to the
// TLS configuration for the transport/client.
type TLSConfig struct {
@@ -132,6 +152,7 @@ type TLSConfig struct {
ClientCertificateKeyFile string `json:"client_certificate_key_file,omitempty"`
InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty"`
HandshakeTimeout caddy.Duration `json:"handshake_timeout,omitempty"`
ServerName string `json:"server_name,omitempty"`
}
// MakeTLSClientConfig returns a tls.Config usable by a client to a backend.
@@ -167,6 +188,9 @@ func (t TLSConfig) MakeTLSClientConfig() (*tls.Config, error) {
cfg.RootCAs = rootPool
}
// custom SNI
cfg.ServerName = t.ServerName
// throw all security out the window
cfg.InsecureSkipVerify = t.InsecureSkipVerify
@@ -203,6 +227,7 @@ type KeepAlive struct {
// Interface guards
var (
_ caddy.Provisioner = (*HTTPTransport)(nil)
_ http.RoundTripper = (*HTTPTransport)(nil)
_ caddy.Provisioner = (*HTTPTransport)(nil)
_ http.RoundTripper = (*HTTPTransport)(nil)
_ caddy.CleanerUpper = (*HTTPTransport)(nil)
)
+171 -126
View File
@@ -21,11 +21,13 @@ import (
"net"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
"golang.org/x/net/http/httpguts"
)
@@ -35,12 +37,13 @@ func init() {
// Handler implements a highly configurable and production-ready reverse proxy.
type Handler struct {
TransportRaw json.RawMessage `json:"transport,omitempty"`
CBRaw json.RawMessage `json:"circuit_breaker,omitempty"`
LoadBalancing *LoadBalancing `json:"load_balancing,omitempty"`
HealthChecks *HealthChecks `json:"health_checks,omitempty"`
Upstreams UpstreamPool `json:"upstreams,omitempty"`
FlushInterval caddy.Duration `json:"flush_interval,omitempty"`
TransportRaw json.RawMessage `json:"transport,omitempty"`
CBRaw json.RawMessage `json:"circuit_breaker,omitempty"`
LoadBalancing *LoadBalancing `json:"load_balancing,omitempty"`
HealthChecks *HealthChecks `json:"health_checks,omitempty"`
Upstreams UpstreamPool `json:"upstreams,omitempty"`
FlushInterval caddy.Duration `json:"flush_interval,omitempty"`
Headers *headers.Handler `json:"headers,omitempty"`
Transport http.RoundTripper `json:"-"`
CB CircuitBreaker `json:"-"`
@@ -63,7 +66,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
return fmt.Errorf("loading transport module: %s", err)
}
h.Transport = val.(http.RoundTripper)
h.TransportRaw = nil // allow GC to deallocate - TODO: Does this help?
h.TransportRaw = nil // allow GC to deallocate
}
if h.LoadBalancing != nil && h.LoadBalancing.SelectionPolicyRaw != nil {
val, err := ctx.LoadModuleInline("policy",
@@ -73,7 +76,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
return fmt.Errorf("loading load balancing selection module: %s", err)
}
h.LoadBalancing.SelectionPolicy = val.(Selector)
h.LoadBalancing.SelectionPolicyRaw = nil // allow GC to deallocate - TODO: Does this help?
h.LoadBalancing.SelectionPolicyRaw = nil // allow GC to deallocate
}
if h.CBRaw != nil {
val, err := ctx.LoadModuleInline("type", "http.handlers.reverse_proxy.circuit_breakers", h.CBRaw)
@@ -81,14 +84,16 @@ func (h *Handler) Provision(ctx caddy.Context) error {
return fmt.Errorf("loading circuit breaker module: %s", err)
}
h.CB = val.(CircuitBreaker)
h.CBRaw = nil // allow GC to deallocate - TODO: Does this help?
h.CBRaw = nil // allow GC to deallocate
}
// set up transport
if h.Transport == nil {
t := &HTTPTransport{
KeepAlive: &KeepAlive{
ProbeInterval: caddy.Duration(30 * time.Second),
IdleConnTimeout: caddy.Duration(2 * time.Minute),
ProbeInterval: caddy.Duration(30 * time.Second),
IdleConnTimeout: caddy.Duration(2 * time.Minute),
MaxIdleConnsPerHost: 32,
},
DialTimeout: caddy.Duration(10 * time.Second),
}
@@ -99,6 +104,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
h.Transport = t
}
// set up load balancing
if h.LoadBalancing == nil {
h.LoadBalancing = new(LoadBalancing)
}
@@ -112,6 +118,12 @@ func (h *Handler) Provision(ctx caddy.Context) error {
// defaulting to a sane wait period between attempts
h.LoadBalancing.TryInterval = caddy.Duration(250 * time.Millisecond)
}
lbMatcherSets, err := h.LoadBalancing.RetryMatchRaw.Setup(ctx)
if err != nil {
return err
}
h.LoadBalancing.RetryMatch = lbMatcherSets
h.LoadBalancing.RetryMatchRaw = nil // allow GC to deallocate
// if active health checks are enabled, configure them and start a worker
if h.HealthChecks != nil &&
@@ -143,85 +155,40 @@ func (h *Handler) Provision(ctx caddy.Context) error {
go h.activeHealthChecker()
}
var allUpstreams []*Upstream
// set up upstreams
for _, upstream := range h.Upstreams {
// if a port was not specified (and the network type uses
// ports), then maybe we can figure out the default port
netw, host, port, err := caddy.SplitNetworkAddress(upstream.Dial)
if err != nil && port == "" && !strings.Contains(netw, "unix") {
if host == "" {
// assume all that was given was the host, no port
host = upstream.Dial
}
// a port was not specified, but we may be able to
// infer it if we know the standard ports on which
// the transport protocol operates
if ht, ok := h.Transport.(*HTTPTransport); ok {
defaultPort := "80"
if ht.TLS != nil {
defaultPort = "443"
}
upstream.Dial = caddy.JoinNetworkAddress(netw, host, defaultPort)
}
// create or get the host representation for this upstream
var host Host = new(upstreamHost)
existingHost, loaded := hosts.LoadOrStore(upstream.Dial, host)
if loaded {
host = existingHost.(Host)
}
upstream.Host = host
// give it the circuit breaker, if any
upstream.cb = h.CB
// if the passive health checker has a non-zero UnhealthyRequestCount
// but the upstream has no MaxRequests set (they are the same thing,
// but the passive health checker is a default value for for upstreams
// without MaxRequests), copy the value into this upstream, since the
// value in the upstream (MaxRequests) is what is used during
// availability checks
if h.HealthChecks != nil &&
h.HealthChecks.Passive != nil &&
h.HealthChecks.Passive.UnhealthyRequestCount > 0 &&
upstream.MaxRequests == 0 {
upstream.MaxRequests = h.HealthChecks.Passive.UnhealthyRequestCount
}
// upstreams are allowed to map to only a single host,
// but an upstream's address may semantically represent
// multiple addresses, so make sure to handle each
// one in turn based on this one upstream config
network, addresses, err := caddy.ParseNetworkAddress(upstream.Dial)
if err != nil {
return fmt.Errorf("parsing dial address: %v", err)
}
for _, addr := range addresses {
// make a new upstream based on the original
// that has a singular dial address
upstreamCopy := *upstream
upstreamCopy.dialInfo = DialInfo{network, addr}
upstreamCopy.Dial = upstreamCopy.dialInfo.String()
upstreamCopy.cb = h.CB
// if host already exists from a current config,
// use that instead; otherwise, add it
// TODO: make hosts modular, so that their state can be distributed in enterprise for example
// TODO: If distributed, the pool should be stored in storage...
var host Host = new(upstreamHost)
activeHost, loaded := hosts.LoadOrStore(upstreamCopy.Dial, host)
if loaded {
host = activeHost.(Host)
}
upstreamCopy.Host = host
// if the passive health checker has a non-zero "unhealthy
// request count" but the upstream has no MaxRequests set
// (they are the same thing, but one is a default value for
// for upstreams with a zero MaxRequests), copy the default
// value into this upstream, since the value in the upstream
// (MaxRequests) is what is used during availability checks
if h.HealthChecks != nil &&
h.HealthChecks.Passive != nil &&
h.HealthChecks.Passive.UnhealthyRequestCount > 0 &&
upstreamCopy.MaxRequests == 0 {
upstreamCopy.MaxRequests = h.HealthChecks.Passive.UnhealthyRequestCount
}
// upstreams need independent access to the passive
// health check policy because they run outside of the
// scope of a request handler
if h.HealthChecks != nil {
upstreamCopy.healthCheckPolicy = h.HealthChecks.Passive
}
allUpstreams = append(allUpstreams, &upstreamCopy)
// upstreams need independent access to the passive
// health check policy because passive health checks
// run without access to h.
if h.HealthChecks != nil {
upstream.healthCheckPolicy = h.HealthChecks.Passive
}
}
// replace the unmarshaled upstreams (possible 1:many
// address mapping) with our list, which is mapped 1:1,
// thus may have expanded the original list
h.Upstreams = allUpstreams
return nil
}
@@ -234,15 +201,19 @@ func (h *Handler) Cleanup() error {
close(h.HealthChecks.Active.stopChan)
}
// TODO: Close keepalive connections on reload? https://github.com/caddyserver/caddy/pull/2507/files#diff-70219fd88fe3f36834f474ce6537ed26R762
// remove hosts from our config from the pool
for _, upstream := range h.Upstreams {
hosts.Delete(upstream.dialInfo.String())
hosts.Delete(upstream.Dial)
}
return nil
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
// prepare the request for proxying; this is needed only once
err := h.prepareRequest(r)
if err != nil {
@@ -250,6 +221,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
fmt.Errorf("preparing request for upstream round-trip: %v", err))
}
// we will need the original headers and Host
// value if header operations are configured
reqHeader := r.Header
reqHost := r.Host
start := time.Now()
var proxyErr error
@@ -258,25 +234,53 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
upstream := h.LoadBalancing.SelectionPolicy.Select(h.Upstreams, r)
if upstream == nil {
if proxyErr == nil {
proxyErr = fmt.Errorf("no available upstreams")
proxyErr = fmt.Errorf("no upstreams available")
}
if !h.tryAgain(start, proxyErr) {
if !h.LoadBalancing.tryAgain(start, proxyErr, r) {
break
}
continue
}
// the dial address may vary per-request if placeholders are
// used, so perform those replacements here; the resulting
// DialInfo struct should have valid network address syntax
dialInfo, err := fillDialInfo(upstream, repl)
if err != nil {
return fmt.Errorf("making dial info: %v", err)
}
// attach to the request information about how to dial the upstream;
// this is necessary because the information cannot be sufficiently
// or satisfactorily represented in a URL
ctx := context.WithValue(r.Context(), DialInfoCtxKey, upstream.dialInfo)
ctx := context.WithValue(r.Context(), DialInfoCtxKey, dialInfo)
r = r.WithContext(ctx)
// set placeholders with information about this upstream
repl.Set("http.handlers.reverse_proxy.upstream.address", dialInfo.String())
repl.Set("http.handlers.reverse_proxy.upstream.hostport", dialInfo.Address)
repl.Set("http.handlers.reverse_proxy.upstream.host", dialInfo.Host)
repl.Set("http.handlers.reverse_proxy.upstream.port", dialInfo.Port)
repl.Set("http.handlers.reverse_proxy.upstream.requests", strconv.Itoa(upstream.Host.NumRequests()))
repl.Set("http.handlers.reverse_proxy.upstream.max_requests", strconv.Itoa(upstream.MaxRequests))
repl.Set("http.handlers.reverse_proxy.upstream.fails", strconv.Itoa(upstream.Host.Fails()))
// mutate request headers according to this upstream;
// because we're in a retry loop, we have to copy
// headers (and the r.Host value) from the original
// so that each retry is identical to the first
if h.Headers != nil && h.Headers.Request != nil {
r.Header = make(http.Header)
copyHeader(r.Header, reqHeader)
r.Host = reqHost
h.Headers.Request.ApplyToRequest(r)
}
// proxy the request to that upstream
proxyErr = h.reverseProxy(w, r, upstream)
proxyErr = h.reverseProxy(w, r, dialInfo)
if proxyErr == nil || proxyErr == context.Canceled {
// context.Canceled happens when the downstream client
// cancels the request; we don't have to worry about that
// cancels the request, which is not our failure
return nil
}
@@ -284,7 +288,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
h.countFailure(upstream)
// if we've tried long enough, break
if !h.tryAgain(start, proxyErr) {
if !h.LoadBalancing.tryAgain(start, proxyErr, r) {
break
}
}
@@ -367,12 +371,12 @@ func (h Handler) prepareRequest(req *http.Request) error {
// reverseProxy performs a round-trip to the given backend and processes the response with the client.
// (This method is mostly the beginning of what was borrowed from the net/http/httputil package in the
// Go standard library which was used as the foundation.)
func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, upstream *Upstream) error {
upstream.Host.CountRequest(1)
defer upstream.Host.CountRequest(-1)
func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di DialInfo) error {
di.Upstream.Host.CountRequest(1)
defer di.Upstream.Host.CountRequest(-1)
// point the request to this upstream
h.directRequest(req, upstream)
h.directRequest(req, di)
// do the round-trip
start := time.Now()
@@ -383,8 +387,8 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, upstre
}
// update circuit breaker on current conditions
if upstream.cb != nil {
upstream.cb.RecordMetric(res.StatusCode, latency)
if di.Upstream.cb != nil {
di.Upstream.cb.RecordMetric(res.StatusCode, latency)
}
// perform passive health checks (if enabled)
@@ -392,14 +396,14 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, upstre
// strike if the status code matches one that is "bad"
for _, badStatus := range h.HealthChecks.Passive.UnhealthyStatus {
if caddyhttp.StatusCodeMatches(res.StatusCode, badStatus) {
h.countFailure(upstream)
h.countFailure(di.Upstream)
}
}
// strike if the roundtrip took too long
if h.HealthChecks.Passive.UnhealthyLatency > 0 &&
latency >= time.Duration(h.HealthChecks.Passive.UnhealthyLatency) {
h.countFailure(upstream)
h.countFailure(di.Upstream)
}
}
@@ -428,6 +432,24 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, upstre
rw.Header().Add("Trailer", strings.Join(trailerKeys, ", "))
}
// apply any response header operations
if h.Headers != nil && h.Headers.Response != nil {
if h.Headers.Response.Require == nil ||
h.Headers.Response.Require.Match(res.StatusCode, rw.Header()) {
repl := req.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
h.Headers.Response.ApplyTo(rw.Header(), repl)
}
}
// TODO: there should be an option to return an error if the response
// matches some criteria; would solve https://github.com/caddyserver/caddy/issues/1447
// by allowing the backend to determine whether this server should treat
// a 400+ status code as an error -- but we might need to be careful that
// we do not affect the health status of the backend... still looking into
// that; if we need to avoid that, we should return a particular error type
// that the caller of this function checks for and only applies health
// status changes if the error is not this special type
rw.WriteHeader(res.StatusCode)
err = h.copyResponse(rw, res.Body, h.flushInterval(req, res))
@@ -471,42 +493,57 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, upstre
}
// tryAgain takes the time that the handler was initially invoked
// as well as any error currently obtained and returns true if
// another attempt should be made at proxying the request. If
// true is returned, it has already blocked long enough before
// the next retry (i.e. no more sleeping is needed). If false is
// returned, the handler should stop trying to proxy the request.
func (h Handler) tryAgain(start time.Time, proxyErr error) bool {
// if downstream has canceled the request, break
if proxyErr == context.Canceled {
return false
}
// as well as any error currently obtained, and the request being
// tried, and returns true if another attempt should be made at
// proxying the request. If true is returned, it has already blocked
// long enough before the next retry (i.e. no more sleeping is
// needed). If false is returned, the handler should stop trying to
// proxy the request.
func (lb LoadBalancing) tryAgain(start time.Time, proxyErr error, req *http.Request) bool {
// if we've tried long enough, break
if time.Since(start) >= time.Duration(h.LoadBalancing.TryDuration) {
if time.Since(start) >= time.Duration(lb.TryDuration) {
return false
}
// if the error occurred while dialing (i.e. a connection
// could not even be established to the upstream), then it
// should be safe to retry, since without a connection, no
// HTTP request can be transmitted; but if the error is not
// specifically a dialer error, we need to be careful
if _, ok := proxyErr.(DialError); proxyErr != nil && !ok {
// if the error occurred after a connection was established,
// we have to assume the upstream received the request, and
// retries need to be carefully decided, because some requests
// are not idempotent
if lb.RetryMatch == nil && req.Method != "GET" {
// by default, don't retry requests if they aren't GET
return false
}
if !lb.RetryMatch.AnyMatch(req) {
return false
}
}
// otherwise, wait and try the next available host
time.Sleep(time.Duration(h.LoadBalancing.TryInterval))
time.Sleep(time.Duration(lb.TryInterval))
return true
}
// directRequest modifies only req.URL so that it points to the
// given upstream host. It must modify ONLY the request URL.
func (h Handler) directRequest(req *http.Request, upstream *Upstream) {
// directRequest modifies only req.URL so that it points to the upstream
// in the given DialInfo. It must modify ONLY the request URL.
func (h Handler) directRequest(req *http.Request, di DialInfo) {
if req.URL.Host == "" {
// we need a host, so set the upstream's host address
fullHost := upstream.dialInfo.Address
reqHost := di.Address
// but if the port matches the scheme, strip the port because
// if the port equates to the scheme, strip the port because
// it's weird to make a request like http://example.com:80/.
host, port, err := net.SplitHostPort(fullHost)
if err == nil &&
(req.URL.Scheme == "http" && port == "80") ||
(req.URL.Scheme == "https" && port == "443") {
fullHost = host
if (req.URL.Scheme == "http" && di.Port == "80") ||
(req.URL.Scheme == "https" && di.Port == "443") {
reqHost = di.Host
}
req.URL.Host = fullHost
req.URL.Host = reqHost
}
}
@@ -582,11 +619,13 @@ func removeConnectionHeaders(h http.Header) {
// LoadBalancing has parameters related to load balancing.
type LoadBalancing struct {
SelectionPolicyRaw json.RawMessage `json:"selection_policy,omitempty"`
TryDuration caddy.Duration `json:"try_duration,omitempty"`
TryInterval caddy.Duration `json:"try_interval,omitempty"`
SelectionPolicyRaw json.RawMessage `json:"selection_policy,omitempty"`
TryDuration caddy.Duration `json:"try_duration,omitempty"`
TryInterval caddy.Duration `json:"try_interval,omitempty"`
RetryMatchRaw caddyhttp.RawMatcherSets `json:"retry_match,omitempty"`
SelectionPolicy Selector `json:"-"`
SelectionPolicy Selector `json:"-"`
RetryMatch caddyhttp.MatcherSets `json:"-"`
}
// Selector selects an available upstream from the pool.
@@ -611,6 +650,12 @@ var hopHeaders = []string{
"Upgrade",
}
// DialError is an error that specifically occurs
// in a call to Dial or DialContext.
type DialError struct {
error
}
// TODO: see if we can use this
// var bufPool = sync.Pool{
// New: func() interface{} {
+6
View File
@@ -31,7 +31,13 @@ func init() {
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var rewr Rewrite
for h.Next() {
if !h.NextArg() {
return nil, h.ArgErr()
}
rewr.URI = h.Val()
if h.NextArg() {
return nil, h.ArgErr()
}
}
rewr.Rehandle = true
return rewr, nil
+73 -8
View File
@@ -15,12 +15,15 @@
package rewrite
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"go.uber.org/zap"
)
func init() {
@@ -29,9 +32,16 @@ func init() {
// Rewrite is a middleware which can rewrite HTTP requests.
type Rewrite struct {
Method string `json:"method,omitempty"`
URI string `json:"uri,omitempty"`
Rehandle bool `json:"rehandle,omitempty"`
Method string `json:"method,omitempty"`
URI string `json:"uri,omitempty"`
StripPathPrefix string `json:"strip_path_prefix,omitempty"`
StripPathSuffix string `json:"strip_path_suffix,omitempty"`
HTTPRedirect caddyhttp.WeakString `json:"http_redirect,omitempty"`
Rehandle bool `json:"rehandle,omitempty"`
logger *zap.Logger
}
// CaddyModule returns the Caddy module information.
@@ -42,18 +52,38 @@ func (Rewrite) CaddyModule() caddy.ModuleInfo {
}
}
// Provision sets up rewr.
func (rewr *Rewrite) Provision(ctx caddy.Context) error {
rewr.logger = ctx.Logger(rewr)
return nil
}
// Validate ensures rewr's configuration is valid.
func (rewr Rewrite) Validate() error {
if rewr.HTTPRedirect != "" && rewr.Rehandle {
return fmt.Errorf("cannot be configured to both write a redirect response and rehandle internally")
}
return nil
}
func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
var rehandleNeeded bool
var changed bool
logger := rewr.logger.With(
zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}),
)
// rewrite the method
if rewr.Method != "" {
method := r.Method
r.Method = strings.ToUpper(repl.ReplaceAll(rewr.Method, ""))
if r.Method != method {
rehandleNeeded = true
changed = true
}
}
// rewrite the URI
if rewr.URI != "" {
oldURI := r.RequestURI
newURI := repl.ReplaceAll(rewr.URI, "")
@@ -73,12 +103,47 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
}
if newURI != oldURI {
rehandleNeeded = true
changed = true
}
}
if rehandleNeeded && rewr.Rehandle {
return caddyhttp.ErrRehandle
// strip path prefix or suffix
if rewr.StripPathPrefix != "" {
prefix := repl.ReplaceAll(rewr.StripPathPrefix, "")
r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix)
newURI := r.URL.String()
if newURI != r.RequestURI {
changed = true
}
r.RequestURI = newURI
}
if rewr.StripPathSuffix != "" {
suffix := repl.ReplaceAll(rewr.StripPathSuffix, "")
r.URL.Path = strings.TrimSuffix(r.URL.Path, suffix)
newURI := r.URL.String()
if newURI != r.RequestURI {
changed = true
}
r.RequestURI = newURI
}
if changed {
logger.Debug("rewrote request",
zap.String("method", r.Method),
zap.String("uri", r.RequestURI),
)
if rewr.Rehandle {
return caddyhttp.ErrRehandle
}
if rewr.HTTPRedirect != "" {
statusCode, err := strconv.Atoi(repl.ReplaceAll(rewr.HTTPRedirect.String(), ""))
if err != nil {
return caddyhttp.Error(http.StatusInternalServerError, err)
}
w.Header().Set("Location", r.RequestURI)
w.WriteHeader(statusCode)
return nil
}
}
return next.ServeHTTP(w, r)
+73 -44
View File
@@ -26,13 +26,13 @@ import (
// middlewares, and a responder for handling HTTP
// requests.
type Route struct {
Group string `json:"group,omitempty"`
MatcherSetsRaw []map[string]json.RawMessage `json:"match,omitempty"`
HandlersRaw []json.RawMessage `json:"handle,omitempty"`
Terminal bool `json:"terminal,omitempty"`
Group string `json:"group,omitempty"`
MatcherSetsRaw RawMatcherSets `json:"match,omitempty"`
HandlersRaw []json.RawMessage `json:"handle,omitempty"`
Terminal bool `json:"terminal,omitempty"`
// decoded values
MatcherSets []MatcherSet `json:"-"`
MatcherSets MatcherSets `json:"-"`
Handlers []MiddlewareHandler `json:"-"`
}
@@ -46,32 +46,6 @@ func (r Route) Empty() bool {
r.Group == ""
}
func (r Route) anyMatcherSetMatches(req *http.Request) bool {
for _, ms := range r.MatcherSets {
if ms.Match(req) {
return true
}
}
// if no matchers, always match
return len(r.MatcherSets) == 0
}
// MatcherSet is a set of matchers which
// must all match in order for the request
// to be matched successfully.
type MatcherSet []RequestMatcher
// Match returns true if the request matches all
// matchers in mset.
func (mset MatcherSet) Match(r *http.Request) bool {
for _, m := range mset {
if !m.Match(r) {
return false
}
}
return true
}
// RouteList is a list of server routes that can
// create a middleware chain.
type RouteList []Route
@@ -80,18 +54,12 @@ type RouteList []Route
func (routes RouteList) Provision(ctx caddy.Context) error {
for i, route := range routes {
// matchers
for _, matcherSet := range route.MatcherSetsRaw {
var matchers MatcherSet
for modName, rawMsg := range matcherSet {
val, err := ctx.LoadModule("http.matchers."+modName, rawMsg)
if err != nil {
return fmt.Errorf("loading matcher module '%s': %v", modName, err)
}
matchers = append(matchers, val.(RequestMatcher))
}
routes[i].MatcherSets = append(routes[i].MatcherSets, matchers)
matcherSets, err := route.MatcherSetsRaw.Setup(ctx)
if err != nil {
return err
}
routes[i].MatcherSetsRaw = nil // allow GC to deallocate - TODO: Does this help?
routes[i].MatcherSets = matcherSets
routes[i].MatcherSetsRaw = nil // allow GC to deallocate
// handlers
for j, rawMsg := range route.HandlersRaw {
@@ -101,7 +69,7 @@ func (routes RouteList) Provision(ctx caddy.Context) error {
}
routes[i].Handlers = append(routes[i].Handlers, mh.(MiddlewareHandler))
}
routes[i].HandlersRaw = nil // allow GC to deallocate - TODO: Does this help?
routes[i].HandlersRaw = nil // allow GC to deallocate
}
return nil
}
@@ -118,7 +86,7 @@ func (routes RouteList) BuildCompositeRoute(req *http.Request) Handler {
for _, route := range routes {
// route must match at least one of the matcher sets
if !route.anyMatcherSetMatches(req) {
if !route.MatcherSets.AnyMatch(req) {
continue
}
@@ -173,6 +141,9 @@ func (routes RouteList) BuildCompositeRoute(req *http.Request) Handler {
func wrapMiddleware(mh MiddlewareHandler) Middleware {
return func(next HandlerFunc) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
// TODO: We could wait to evaluate matchers here, just eval
// the next matcher and choose the next route...
// TODO: This is where request tracing could be implemented; also
// see below to trace the responder as well
// TODO: Trace a diff of the request, would be cool too! see what changed since the last middleware (host, headers, URI...)
@@ -181,3 +152,61 @@ func wrapMiddleware(mh MiddlewareHandler) Middleware {
}
}
}
// MatcherSet is a set of matchers which
// must all match in order for the request
// to be matched successfully.
type MatcherSet []RequestMatcher
// Match returns true if the request matches all
// matchers in mset.
func (mset MatcherSet) Match(r *http.Request) bool {
for _, m := range mset {
if !m.Match(r) {
return false
}
}
return true
}
// RawMatcherSets is a group of matcher sets
// in their raw, JSON form.
type RawMatcherSets []map[string]json.RawMessage
// Setup sets up all matcher sets by loading each matcher module
// and returning the group of provisioned matcher sets.
func (rm RawMatcherSets) Setup(ctx caddy.Context) (MatcherSets, error) {
if rm == nil {
return nil, nil
}
var ms MatcherSets
for _, matcherSet := range rm {
var matchers MatcherSet
for modName, rawMsg := range matcherSet {
val, err := ctx.LoadModule("http.matchers."+modName, rawMsg)
if err != nil {
return nil, fmt.Errorf("loading matcher module '%s': %v", modName, err)
}
matchers = append(matchers, val.(RequestMatcher))
}
ms = append(ms, matchers)
}
return ms, nil
}
// MatcherSets is a group of matcher sets capable
// of checking whether a request matches any of
// the sets.
type MatcherSets []MatcherSet
// AnyMatch returns true if req matches any of the
// matcher sets in mss or if there are no matchers,
// in which case the request always matches.
func (mss MatcherSets) AnyMatch(req *http.Request) bool {
for _, ms := range mss {
if ms.Match(req) {
return true
}
}
return len(mss) == 0
}
+189 -52
View File
@@ -17,16 +17,18 @@ package caddyhttp
import (
"context"
"fmt"
"log"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/lucas-clemente/quic-go/http3"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Server is an HTTP server.
@@ -42,12 +44,16 @@ type Server struct {
TLSConnPolicies caddytls.ConnectionPolicies `json:"tls_connection_policies,omitempty"`
AutoHTTPS *AutoHTTPSConfig `json:"automatic_https,omitempty"`
MaxRehandles *int `json:"max_rehandles,omitempty"`
StrictSNIHost bool `json:"strict_sni_host,omitempty"`
StrictSNIHost *bool `json:"strict_sni_host,omitempty"`
Logs *ServerLogConfig `json:"logs,omitempty"`
// This field is not subject to compatibility promises
ExperimentalHTTP3 bool `json:"experimental_http3,omitempty"`
tlsApp *caddytls.TLS
tlsApp *caddytls.TLS
logger *zap.Logger
accessLogger *zap.Logger
errorLogger *zap.Logger
h3server *http3.Server
}
@@ -59,58 +65,106 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if s.h3server != nil {
err := s.h3server.SetQuicHeaders(w.Header())
if err != nil {
log.Printf("[ERROR] Setting HTTP/3 Alt-Svc header: %v", err)
s.logger.Error("setting HTTP/3 Alt-Svc header", zap.Error(err))
}
}
if s.tlsApp.HandleHTTPChallenge(w, r) {
return
}
// set up the context for the request
repl := caddy.NewReplacer()
ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
ctx = context.WithValue(ctx, ServerCtxKey, s)
ctx = context.WithValue(ctx, VarCtxKey, make(map[string]interface{}))
ctx = context.WithValue(ctx, OriginalURLCtxKey, cloneURL(r.URL))
ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]interface{}))
var url2 url.URL // avoid letting this escape to the heap
ctx = context.WithValue(ctx, OriginalRequestCtxKey, originalRequest(r, &url2))
r = r.WithContext(ctx)
// once the pointer to the request won't change
// anymore, finish setting up the replacer
addHTTPVarsToReplacer(repl, r, w)
// build and execute the main handler chain
loggableReq := LoggableHTTPRequest{r}
errLog := s.errorLogger.With(
// encode the request for logging purposes before
// it enters any handler chain; this is necessary
// to capture the original request in case it gets
// modified during handling
zap.Object("request", loggableReq),
)
if s.Logs != nil {
wrec := NewResponseRecorder(w, nil, nil)
w = wrec
accLog := s.accessLogger.With(
// capture the original version of the request
zap.Object("request", loggableReq),
)
start := time.Now()
defer func() {
latency := time.Since(start)
repl.Set("http.response.status", strconv.Itoa(wrec.Status()))
repl.Set("http.response.size", strconv.Itoa(wrec.Size()))
repl.Set("http.response.latency", latency.String())
logger := accLog
if s.Logs.LoggerNames != nil {
logger = logger.Named(s.Logs.LoggerNames[r.Host])
}
log := logger.Info
if wrec.Status() >= 400 {
log = logger.Error
}
log("request",
zap.String("common_log", repl.ReplaceAll(CommonLogFormat, "-")),
zap.Duration("latency", latency),
zap.Int("size", wrec.Size()),
zap.Int("status", wrec.Status()),
)
}()
}
// guarantee ACME HTTP challenges; handle them
// separately from any user-defined handlers
if s.tlsApp.HandleHTTPChallenge(w, r) {
return
}
// build and execute the primary handler chain
err := s.executeCompositeRoute(w, r, s.Routes)
if err != nil {
// add the raw error value to the request context
// so it can be accessed by error handlers
c := context.WithValue(r.Context(), ErrorCtxKey, err)
r = r.WithContext(c)
// add error values to the replacer
repl.Set("http.error", err.Error())
if handlerErr, ok := err.(HandlerError); ok {
repl.Set("http.error.status_code", strconv.Itoa(handlerErr.StatusCode))
repl.Set("http.error.status_text", http.StatusText(handlerErr.StatusCode))
repl.Set("http.error.message", handlerErr.Message)
repl.Set("http.error.trace", handlerErr.Trace)
repl.Set("http.error.id", handlerErr.ID)
// prepare the error log
logger := errLog
if s.Logs != nil && s.Logs.LoggerNames != nil {
logger = logger.Named(s.Logs.LoggerNames[r.Host])
}
// get the values that will be used to log the error
errStatus, errMsg, errFields := errLogValues(err)
// add HTTP error information to request context
r = s.Errors.WithError(r, err)
if s.Errors != nil && len(s.Errors.Routes) > 0 {
err := s.executeCompositeRoute(w, r, s.Errors.Routes)
if err != nil {
// TODO: what should we do if the error handler has an error?
log.Printf("[ERROR] [%s %s] handling error: %v", r.Method, r.RequestURI, err)
// execute user-defined error handling route
err2 := s.executeCompositeRoute(w, r, s.Errors.Routes)
if err2 == nil {
// user's error route handled the error response
// successfully, so now just log the error
logger.Error(errMsg, errFields...)
} else {
// well... this is awkward
errFields = append([]zapcore.Field{
zap.String("error", err2.Error()),
zap.Namespace("first_error"),
zap.String("msg", errMsg),
}, errFields...)
logger.Error("error handling handler error", errFields...)
}
} else {
// TODO: polish the default error handling
log.Printf("[ERROR] [%s %s] %v", r.Method, r.RequestURI, err)
if handlerErr, ok := err.(HandlerError); ok {
w.WriteHeader(handlerErr.StatusCode)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
logger.Error(errMsg, errFields...)
w.WriteHeader(errStatus)
}
}
}
@@ -164,12 +218,12 @@ func (s *Server) enforcementHandler(w http.ResponseWriter, r *http.Request, next
// servers that rely on TLS ClientAuth sharing a listener
// with servers that do not; if not enforced, client could
// bypass by sending benign SNI then restricted Host header
if s.StrictSNIHost && r.TLS != nil {
if s.StrictSNIHost != nil && *s.StrictSNIHost && r.TLS != nil {
hostname, _, err := net.SplitHostPort(r.Host)
if err != nil {
hostname = r.Host // OK; probably lacked port
}
if strings.ToLower(r.TLS.ServerName) != strings.ToLower(hostname) {
if !strings.EqualFold(r.TLS.ServerName, hostname) {
err := fmt.Errorf("strict host matching: TLS ServerName (%s) and HTTP Host (%s) values differ",
r.TLS.ServerName, hostname)
r.Close = true
@@ -196,17 +250,26 @@ func (s *Server) listenersUseAnyPortOtherThan(otherPort int) bool {
return false
}
// listenersIncludePort returns true if there are any
// listeners in s that use otherPort.
func (s *Server) listenersIncludePort(otherPort int) bool {
func (s *Server) hasListenerAddress(fullAddr string) bool {
netw, addrs, err := caddy.ParseNetworkAddress(fullAddr)
if err != nil {
return false
}
if len(addrs) != 1 {
return false
}
addr := addrs[0]
for _, lnAddr := range s.Listen {
_, addrs, err := caddy.ParseNetworkAddress(lnAddr)
if err == nil {
for _, a := range addrs {
_, port, err := net.SplitHostPort(a)
if err == nil && port == strconv.Itoa(otherPort) {
return true
}
thisNetw, thisAddrs, err := caddy.ParseNetworkAddress(lnAddr)
if err != nil {
continue
}
if thisNetw != netw {
continue
}
for _, a := range thisAddrs {
if a == addr {
return true
}
}
}
@@ -269,26 +332,100 @@ type HTTPErrorConfig struct {
Routes RouteList `json:"routes,omitempty"`
}
// WithError makes a shallow copy of r to add the error to its
// context, and sets placeholders on the request's replacer
// related to err. It returns the modified request which has
// the error information in its context and replacer. It
// overwrites any existing error values that are stored.
func (*HTTPErrorConfig) WithError(r *http.Request, err error) *http.Request {
// add the raw error value to the request context
// so it can be accessed by error handlers
c := context.WithValue(r.Context(), ErrorCtxKey, err)
r = r.WithContext(c)
// add error values to the replacer
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
repl.Set("http.error", err.Error())
if handlerErr, ok := err.(HandlerError); ok {
repl.Set("http.error.status_code", strconv.Itoa(handlerErr.StatusCode))
repl.Set("http.error.status_text", http.StatusText(handlerErr.StatusCode))
repl.Set("http.error.trace", handlerErr.Trace)
repl.Set("http.error.id", handlerErr.ID)
}
return r
}
// ServerLogConfig describes a server's logging configuration.
type ServerLogConfig struct {
LoggerNames map[string]string `json:"logger_names,omitempty"`
}
// errLogValues inspects err and returns the status code
// to use, the error log message, and any extra fields.
// If err is a HandlerError, the returned values will
// have richer information.
func errLogValues(err error) (status int, msg string, fields []zapcore.Field) {
if handlerErr, ok := err.(HandlerError); ok {
status = handlerErr.StatusCode
msg = handlerErr.Err.Error()
fields = []zapcore.Field{
zap.Int("status", handlerErr.StatusCode),
zap.String("err_id", handlerErr.ID),
zap.String("err_trace", handlerErr.Trace),
}
return
}
status = http.StatusInternalServerError
msg = err.Error()
return
}
// originalRequest returns a partial, shallow copy of
// req, including: req.Method, deep copy of req.URL
// (into the urlCopy parameter, which should be on the
// stack), and req.RequestURI. Notably, headers are not
// copied. This function is designed to be very fast
// and efficient, and useful primarly for read-only
// logging purposes.
func originalRequest(req *http.Request, urlCopy *url.URL) http.Request {
urlCopy = cloneURL(req.URL)
return http.Request{
Method: req.Method,
RequestURI: req.RequestURI,
URL: urlCopy,
}
}
// cloneURL makes a copy of r.URL and returns a
// new value that doesn't reference the original.
func cloneURL(u *url.URL) url.URL {
func cloneURL(u *url.URL) *url.URL {
urlCopy := *u
if u.User != nil {
userInfo := new(url.Userinfo)
*userInfo = *u.User
urlCopy.User = userInfo
}
return urlCopy
return &urlCopy
}
const (
// CommonLogFormat is the common log format. https://en.wikipedia.org/wiki/Common_Log_Format
CommonLogFormat = `{http.request.remote.host} ` + CommonLogEmptyValue + ` {http.handlers.authentication.user.id} [{time.now.common_log}] "{http.request.orig_method} {http.request.orig_uri} {http.request.proto}" {http.response.status} {http.response.size}`
// CommonLogEmptyValue is the common empty log value.
CommonLogEmptyValue = "-"
)
// Context keys for HTTP request context values.
const (
// For referencing the server instance
ServerCtxKey caddy.CtxKey = "server"
// For the request's variable table
VarCtxKey caddy.CtxKey = "vars"
VarsCtxKey caddy.CtxKey = "vars"
// For the unmodified URL that originally came in with a request
OriginalURLCtxKey caddy.CtxKey = "original_url"
// For a partial copy of the unmodified request that
// originally came into the server's entry handler
OriginalRequestCtxKey caddy.CtxKey = "original_request"
)
@@ -0,0 +1,19 @@
{
"apps": {
"http": {
"servers": {
"MY_SERVER": {
"listen": [":3001"],
"routes": [
{
"handle": {
"handler": "starlark",
"script": "def setup(r):\n\t# create some middlewares specific to this request\n\ttemplates = loadModule('http.handlers.templates', {'include_root': './includes'})\n\tmidChain = execute([templates])\n\ndef serveHTTP (rw, r):\n\trw.Write('Hello world, from Starlark!')\n"
}
}
]
}
}
}
}
}
@@ -0,0 +1,165 @@
package lib
import (
"encoding/json"
"fmt"
"strings"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2"
"go.starlark.net/starlark"
)
// ResponderModule represents a module that satisfies the caddyhttp handler.
type ResponderModule struct {
Name string
Cfg json.RawMessage
Instance caddyhttp.Handler
}
func (r ResponderModule) Freeze() {}
func (r ResponderModule) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: responder module") }
func (r ResponderModule) String() string { return "responder module" }
func (r ResponderModule) Type() string { return "responder module" }
func (r ResponderModule) Truth() starlark.Bool { return true }
// Middleware represents a module that satisfies the starlark Value interface.
type Middleware struct {
Name string
Cfg json.RawMessage
Instance caddyhttp.MiddlewareHandler
}
func (r Middleware) Freeze() {}
func (r Middleware) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: middleware") }
func (r Middleware) String() string { return "middleware" }
func (r Middleware) Type() string { return "middleware" }
func (r Middleware) Truth() starlark.Bool { return true }
// LoadMiddleware represents the method exposed to starlark to load a Caddy module.
type LoadMiddleware struct {
Middleware Middleware
Ctx caddy.Context
}
func (r LoadMiddleware) Freeze() {}
func (r LoadMiddleware) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: loadMiddleware") }
func (r LoadMiddleware) String() string { return "loadMiddleware" }
func (r LoadMiddleware) Type() string { return "function: loadMiddleware" }
func (r LoadMiddleware) Truth() starlark.Bool { return true }
// Run is the method bound to the starlark loadMiddleware function.
func (r *LoadMiddleware) Run(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var name string
var cfg *starlark.Dict
err := starlark.UnpackPositionalArgs(fn.Name(), args, kwargs, 2, &name, &cfg)
if err != nil {
return starlark.None, fmt.Errorf("unpacking arguments: %v", err.Error())
}
js := json.RawMessage(cfg.String())
if strings.Index(name, "http.handlers.") == -1 {
name = fmt.Sprintf("http.handlers.%s", name)
}
inst, err := r.Ctx.LoadModule(name, js)
if err != nil {
return starlark.None, err
}
mid, ok := inst.(caddyhttp.MiddlewareHandler)
if !ok {
return starlark.None, fmt.Errorf("could not assert as middleware handler")
}
m := Middleware{
Name: name,
Cfg: js,
Instance: mid,
}
r.Middleware = m
return m, nil
}
// LoadResponder represents the method exposed to starlark to load a Caddy middleware responder.
type LoadResponder struct {
Module ResponderModule
Ctx caddy.Context
}
func (r LoadResponder) Freeze() {}
func (r LoadResponder) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: loadModule") }
func (r LoadResponder) String() string { return "loadModule" }
func (r LoadResponder) Type() string { return "function: loadModule" }
func (r LoadResponder) Truth() starlark.Bool { return true }
// Run is the method bound to the starlark loadResponder function.
func (r *LoadResponder) Run(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var name string
var cfg *starlark.Dict
err := starlark.UnpackPositionalArgs(fn.Name(), args, kwargs, 2, &name, &cfg)
if err != nil {
return starlark.None, fmt.Errorf("unpacking arguments: %v", err.Error())
}
js := json.RawMessage(cfg.String())
if strings.Index(name, "http.handlers.") == -1 {
name = fmt.Sprintf("http.handlers.%s", name)
}
inst, err := r.Ctx.LoadModule(name, js)
if err != nil {
return starlark.None, err
}
res, ok := inst.(caddyhttp.Handler)
if !ok {
return starlark.None, fmt.Errorf("could not assert as responder")
}
m := ResponderModule{
Name: name,
Cfg: js,
Instance: res,
}
r.Module = m
return m, nil
}
// Execute represents the method exposed to starlark to build a middleware chain.
type Execute struct {
Modules []Middleware
}
func (r Execute) Freeze() {}
func (r Execute) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: execute") }
func (r Execute) String() string { return "execute" }
func (r Execute) Type() string { return "function: execute" }
func (r Execute) Truth() starlark.Bool { return true }
// Run is the method bound to the starlark execute function.
func (r *Execute) Run(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var mids *starlark.List
err := starlark.UnpackPositionalArgs(fn.Name(), args, kwargs, 1, &mids)
if err != nil {
return starlark.None, fmt.Errorf("unpacking arguments: %v", err.Error())
}
for i := 0; i < mids.Len(); i++ {
val, ok := mids.Index(i).(Middleware)
if !ok {
return starlark.None, fmt.Errorf("cannot get module from execute")
}
r.Modules = append(r.Modules, val)
}
return starlark.None, nil
}
+172
View File
@@ -0,0 +1,172 @@
package starlarkmw
import (
"context"
"fmt"
"net/http"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
caddyscript "github.com/caddyserver/caddy/v2/pkg/caddyscript/lib"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/starlarkmw/internal/lib"
"github.com/starlight-go/starlight/convert"
"go.starlark.net/starlark"
)
func init() {
caddy.RegisterModule(StarlarkMW{})
}
// StarlarkMW represents a middleware responder written in starlark
type StarlarkMW struct {
Script string `json:"script"`
serveHTTP *starlark.Function
setup *starlark.Function
thread *starlark.Thread
loadMiddleware *lib.LoadMiddleware
execute *lib.Execute
globals *starlark.StringDict
gctx caddy.Context
rctx caddy.Context
rcancel context.CancelFunc
}
// CaddyModule returns the Caddy module information.
func (StarlarkMW) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "http.handlers.starlark",
New: func() caddy.Module { return new(StarlarkMW) },
}
}
// ServeHTTP responds to an http request with starlark.
func (s *StarlarkMW) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
var mwcancel context.CancelFunc
var mwctx caddy.Context
// call setup() to prepare the middleware chain if it is defined
if s.setup != nil {
mwctx, mwcancel = caddy.NewContext(s.gctx)
defer mwcancel()
s.loadMiddleware.Ctx = mwctx
args := starlark.Tuple{caddyscript.HTTPRequest{Req: r}}
_, err := starlark.Call(new(starlark.Thread), s.setup, args, nil)
if err != nil {
return fmt.Errorf("starlark setup(), %v", err)
}
}
// dynamically build middleware chain for each request
stack := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
wr, err := convert.ToValue(w)
if err != nil {
return fmt.Errorf("cannot convert response writer to starlark value")
}
args := starlark.Tuple{wr, caddyscript.HTTPRequest{Req: r}}
v, err := starlark.Call(new(starlark.Thread), s.serveHTTP, args, nil)
if err != nil {
return fmt.Errorf("starlark serveHTTP(), %v", err)
}
// if a responder type was returned from starlark we should run it otherwise it
// is expected to handle the request
if resp, ok := v.(lib.ResponderModule); ok {
return resp.Instance.ServeHTTP(w, r)
}
return nil
})
// TODO :- make middlewareResponseWriter exported and wrap w with that
var mid []caddyhttp.Middleware
for _, m := range s.execute.Modules {
mid = append(mid, func(next caddyhttp.HandlerFunc) caddyhttp.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
return m.Instance.ServeHTTP(w, r, next)
}
})
}
for i := len(mid) - 1; i >= 0; i-- {
stack = mid[i](stack)
}
s.execute.Modules = nil
return stack(w, r)
}
// Cleanup cleans up any modules loaded during the creation of a starlark route.
func (s *StarlarkMW) Cleanup() error {
s.rcancel()
return nil
}
// Provision sets up the starlark env.
func (s *StarlarkMW) Provision(ctx caddy.Context) error {
// store global context
s.gctx = ctx
// setup context for cleaning up any modules loaded during starlark script parsing phase
rctx, cancel := caddy.NewContext(ctx)
s.rcancel = cancel
// setup starlark global env
env := make(starlark.StringDict)
loadMiddleware := lib.LoadMiddleware{}
loadResponder := lib.LoadResponder{
Ctx: rctx,
}
execute := lib.Execute{}
lr := starlark.NewBuiltin("loadResponder", loadResponder.Run)
lr = lr.BindReceiver(&loadResponder)
env["loadResponder"] = lr
lm := starlark.NewBuiltin("loadMiddleware", loadMiddleware.Run)
lm = lm.BindReceiver(&loadMiddleware)
env["loadMiddleware"] = lm
ex := starlark.NewBuiltin("execute", execute.Run)
ex = ex.BindReceiver(&execute)
env["execute"] = ex
// import caddyscript lib
env["time"] = caddyscript.Time{}
env["regexp"] = caddyscript.Regexp{}
// configure starlark
thread := new(starlark.Thread)
s.thread = thread
// run starlark script
globals, err := starlark.ExecFile(thread, "", s.Script, env)
if err != nil {
return fmt.Errorf("starlark exec file: %v", err.Error())
}
// extract defined methods to setup middleware chain and responder, setup is not required
var setup *starlark.Function
if _, ok := globals["setup"]; ok {
setup, ok = globals["setup"].(*starlark.Function)
if !ok {
return fmt.Errorf("setup function not defined in starlark script")
}
}
serveHTTP, ok := globals["serveHTTP"].(*starlark.Function)
if !ok {
return fmt.Errorf("serveHTTP function not defined in starlark script")
}
s.setup = setup
s.serveHTTP = serveHTTP
s.loadMiddleware = &loadMiddleware
s.execute = &execute
s.globals = &globals
return nil
}
@@ -0,0 +1,40 @@
# any module that provisions resources
proxyConfig = {
'load_balance_type': 'round_robin',
'upstreams': [
{
'host': 'http://localhost:8080',
'circuit_breaker': {
'type': 'status_ratio',
'threshold': 0.5
}
},
{
'host': 'http://localhost:8081'
}
]
}
sfConfig = {
'root': '/Users/dev/Desktop',
'browse': {},
}
proxy = loadResponder('reverse_proxy', proxyConfig)
static_files = loadResponder('file_server', sfConfig)
def setup(r):
# create some middlewares specific to this request
mid = []
if r.query.get('log') == 'true':
logMid = loadMiddleware('log', {'file': 'access.log'})
mid.append(logMid)
execute(mid)
def serveHTTP(w, r):
if r.url.find('static') > 0:
return static_files
return proxy
+7 -5
View File
@@ -25,7 +25,6 @@ import (
func init() {
caddy.RegisterModule(StaticResponse{})
// TODO: Caddyfile directive
}
// StaticResponse implements a simple responder for static responses.
@@ -46,7 +45,7 @@ func (StaticResponse) CaddyModule() caddy.ModuleInfo {
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
//
// static_response [<matcher>] <status> {
// respond [<matcher>] <status> {
// body <text>
// close
// }
@@ -113,11 +112,14 @@ func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Hand
// write response body
if s.Body != "" {
fmt.Fprint(w, repl.ReplaceAll(s.Body, ""))
fmt.Fprint(w, repl.ReplaceKnown(s.Body, ""))
}
return nil
}
// Interface guard
var _ MiddlewareHandler = (*StaticResponse)(nil)
// Interface guards
var (
_ MiddlewareHandler = (*StaticResponse)(nil)
_ caddyfile.Unmarshaler = (*StaticResponse)(nil)
)
+22 -3
View File
@@ -30,8 +30,15 @@ func init() {
// matchers, or for routes with matchers that must be have deferred
// evaluation (e.g. if they depend on placeholders created by other
// matchers that need to be evaluated first).
//
// You can also use subroutes to handle errors from specific handlers.
// First the primary Routes will be executed, and if they return an
// error, the Errors routes will be executed; in that case, an error
// is only returned to the entry point at the server if there is an
// additional error returned from the errors routes.
type Subroute struct {
Routes RouteList `json:"routes,omitempty"`
Routes RouteList `json:"routes,omitempty"`
Errors *HTTPErrorConfig `json:"errors,omitempty"`
}
// CaddyModule returns the Caddy module information.
@@ -47,7 +54,13 @@ func (sr *Subroute) Provision(ctx caddy.Context) error {
if sr.Routes != nil {
err := sr.Routes.Provision(ctx)
if err != nil {
return fmt.Errorf("setting up routes: %v", err)
return fmt.Errorf("setting up subroutes: %v", err)
}
if sr.Errors != nil {
err := sr.Errors.Routes.Provision(ctx)
if err != nil {
return fmt.Errorf("setting up error subroutes: %v", err)
}
}
}
return nil
@@ -55,7 +68,13 @@ func (sr *Subroute) Provision(ctx caddy.Context) error {
func (sr *Subroute) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error {
subroute := sr.Routes.BuildCompositeRoute(r)
return subroute.ServeHTTP(w, r)
err := subroute.ServeHTTP(w, r)
if err != nil && sr.Errors != nil {
r = sr.Errors.WithError(r, err)
errRoute := sr.Errors.Routes.BuildCompositeRoute(r)
return errRoute.ServeHTTP(w, r)
}
return err
}
// Interface guards
+8 -10
View File
@@ -17,7 +17,6 @@ package templates
import (
"bytes"
"fmt"
"io"
"net/http"
"strconv"
"strings"
@@ -71,8 +70,8 @@ func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
// shouldBuf determines whether to execute templates on this response,
// since generally we will not want to execute for images or CSS, etc.
shouldBuf := func(status int) bool {
ct := w.Header().Get("Content-Type")
shouldBuf := func(status int, header http.Header) bool {
ct := header.Get("Content-Type")
for _, mt := range t.MIMETypes {
if strings.Contains(ct, mt) {
return true
@@ -96,18 +95,17 @@ func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
return err
}
w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
w.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-created content
w.Header().Del("Last-Modified") // useless for dynamic content since it's always changing
rec.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
rec.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-created content
rec.Header().Del("Last-Modified") // useless for dynamic content since it's always changing
// we don't know a way to guickly generate etag for dynamic content,
// but we can convert this to a weak etag to kind of indicate that
if etag := w.Header().Get("ETag"); etag != "" {
w.Header().Set("ETag", "W/"+etag)
if etag := rec.Header().Get("Etag"); etag != "" {
rec.Header().Set("Etag", "W/"+etag)
}
w.WriteHeader(rec.Status())
io.Copy(w, buf)
rec.WriteResponse()
return nil
}
+18 -4
View File
@@ -22,10 +22,11 @@ import (
"net"
"net/http"
"path"
"strconv"
"strings"
"sync"
"github.com/Masterminds/sprig"
"github.com/Masterminds/sprig/v3"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/russross/blackfriday/v2"
)
@@ -79,8 +80,18 @@ func (c templateContext) Include(filename string, args ...interface{}) (template
// are NOT escaped, so you should only include trusted resources.
// If it is not trusted, be sure to use escaping functions yourself.
func (c templateContext) HTTPInclude(uri string) (template.HTML, error) {
if c.Req.Header.Get(recursionPreventionHeader) == "1" {
return "", fmt.Errorf("virtual include cycle")
// prevent virtual request loops by counting how many levels
// deep we are; and if we get too deep, return an error
recursionCount := 1
if numStr := c.Req.Header.Get(recursionPreventionHeader); numStr != "" {
num, err := strconv.Atoi(numStr)
if err != nil {
return "", fmt.Errorf("parsing %s: %v", recursionPreventionHeader, err)
}
if num >= 3 {
return "", fmt.Errorf("virtual request cycle")
}
recursionCount = num + 1
}
buf := bufPool.Get().(*bytes.Buffer)
@@ -91,7 +102,10 @@ func (c templateContext) HTTPInclude(uri string) (template.HTML, error) {
if err != nil {
return "", err
}
virtReq.Header.Set(recursionPreventionHeader, "1")
virtReq.Host = c.Req.Host
virtReq.Header = c.Req.Header.Clone()
virtReq.Trailer = c.Req.Trailer.Clone()
virtReq.Header.Set(recursionPreventionHeader, strconv.Itoa(recursionCount))
vrw := &virtualResponseWriter{body: buf, header: make(http.Header)}
server := c.Req.Context().Value(caddyhttp.ServerCtxKey).(http.Handler)
+2 -2
View File
@@ -38,7 +38,7 @@ func (VarsMiddleware) CaddyModule() caddy.ModuleInfo {
}
func (t VarsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error {
vars := r.Context().Value(VarCtxKey).(map[string]interface{})
vars := r.Context().Value(VarsCtxKey).(map[string]interface{})
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
for k, v := range t {
keyExpanded := repl.ReplaceAll(k, "")
@@ -62,7 +62,7 @@ func (VarsMatcher) CaddyModule() caddy.ModuleInfo {
// Match matches a request based on variables in the context.
func (m VarsMatcher) Match(r *http.Request) bool {
vars := r.Context().Value(VarCtxKey).(map[string]string)
vars := r.Context().Value(VarsCtxKey).(map[string]string)
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
for k, v := range m {
keyExpanded := repl.ReplaceAll(k, "")
+61 -37
View File
@@ -15,8 +15,10 @@
package caddytls
import (
"crypto/x509"
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"time"
@@ -38,17 +40,19 @@ func init() {
// after you have configured this struct
// to your liking.
type ACMEManagerMaker struct {
CA string `json:"ca,omitempty"`
Email string `json:"email,omitempty"`
RenewAhead caddy.Duration `json:"renew_ahead,omitempty"`
KeyType string `json:"key_type,omitempty"`
ACMETimeout caddy.Duration `json:"acme_timeout,omitempty"`
MustStaple bool `json:"must_staple,omitempty"`
Challenges ChallengesConfig `json:"challenges,omitempty"`
OnDemand bool `json:"on_demand,omitempty"`
Storage json.RawMessage `json:"storage,omitempty"`
CA string `json:"ca,omitempty"`
Email string `json:"email,omitempty"`
RenewAhead caddy.Duration `json:"renew_ahead,omitempty"`
KeyType string `json:"key_type,omitempty"`
ACMETimeout caddy.Duration `json:"acme_timeout,omitempty"`
MustStaple bool `json:"must_staple,omitempty"`
Challenges *ChallengesConfig `json:"challenges,omitempty"`
OnDemand bool `json:"on_demand,omitempty"`
Storage json.RawMessage `json:"storage,omitempty"`
TrustedRootsPEMFiles []string `json:"trusted_roots_pem_files,omitempty"`
storage certmagic.Storage
storage certmagic.Storage
rootPool *x509.CertPool
}
// CaddyModule returns the Caddy module information.
@@ -68,13 +72,13 @@ func (m ACMEManagerMaker) NewManager(interactive bool) (certmagic.Manager, error
// Provision sets up m.
func (m *ACMEManagerMaker) Provision(ctx caddy.Context) error {
// DNS providers
if m.Challenges.DNSRaw != nil {
if m.Challenges != nil && m.Challenges.DNSRaw != nil {
val, err := ctx.LoadModuleInline("provider", "tls.dns", m.Challenges.DNSRaw)
if err != nil {
return fmt.Errorf("loading DNS provider module: %s", err)
}
m.Challenges.DNS = val.(challenge.Provider)
m.Challenges.DNSRaw = nil // allow GC to deallocate - TODO: Does this help?
m.Challenges.DNSRaw = nil // allow GC to deallocate
}
// policy-specific storage implementation
@@ -88,7 +92,21 @@ func (m *ACMEManagerMaker) Provision(ctx caddy.Context) error {
return fmt.Errorf("creating TLS storage configuration: %v", err)
}
m.storage = cmStorage
m.Storage = nil // allow GC to deallocate - TODO: Does this help?
m.Storage = nil // allow GC to deallocate
}
// add any custom CAs to trust store
if len(m.TrustedRootsPEMFiles) > 0 {
m.rootPool = x509.NewCertPool()
for _, pemFile := range m.TrustedRootsPEMFiles {
pemData, err := ioutil.ReadFile(pemFile)
if err != nil {
return fmt.Errorf("loading trusted root CA's PEM file: %s: %v", pemFile, err)
}
if !m.rootPool.AppendCertsFromPEM(pemData) {
return fmt.Errorf("unable to add %s to trust pool: %v", pemFile, err)
}
}
}
return nil
@@ -107,7 +125,7 @@ func (m *ACMEManagerMaker) makeCertMagicConfig(ctx caddy.Context) certmagic.Conf
if m.OnDemand {
var onDemand *OnDemandConfig
appVal, err := ctx.App("tls")
if err == nil {
if err == nil && appVal.(*TLS).Automation != nil {
onDemand = appVal.(*TLS).Automation.OnDemand
}
@@ -120,14 +138,10 @@ func (m *ACMEManagerMaker) makeCertMagicConfig(ctx caddy.Context) certmagic.Conf
return err
}
}
// check the rate limiter last, since
// even checking consumes a token; so
// don't even bother checking if the
// other regulations fail anyway
if onDemand.RateLimit != nil {
if !onDemandRateLimiter.Allow() {
return fmt.Errorf("on-demand rate limit exceeded")
}
// check the rate limiter last because
// doing so makes a reservation
if !onDemandRateLimiter.Allow() {
return fmt.Errorf("on-demand rate limit exceeded")
}
}
return nil
@@ -135,23 +149,33 @@ func (m *ACMEManagerMaker) makeCertMagicConfig(ctx caddy.Context) certmagic.Conf
}
}
return certmagic.Config{
CA: m.CA,
Email: m.Email,
Agreed: true,
DisableHTTPChallenge: m.Challenges.HTTP.Disabled,
DisableTLSALPNChallenge: m.Challenges.TLSALPN.Disabled,
RenewDurationBefore: time.Duration(m.RenewAhead),
AltHTTPPort: m.Challenges.HTTP.AlternatePort,
AltTLSALPNPort: m.Challenges.TLSALPN.AlternatePort,
DNSProvider: m.Challenges.DNS,
KeyType: supportedCertKeyTypes[m.KeyType],
CertObtainTimeout: time.Duration(m.ACMETimeout),
OnDemand: ond,
MustStaple: m.MustStaple,
Storage: storage,
cfg := certmagic.Config{
CA: m.CA,
Email: m.Email,
Agreed: true,
RenewDurationBefore: time.Duration(m.RenewAhead),
KeyType: supportedCertKeyTypes[m.KeyType],
CertObtainTimeout: time.Duration(m.ACMETimeout),
OnDemand: ond,
MustStaple: m.MustStaple,
Storage: storage,
TrustedRoots: m.rootPool,
// TODO: listenHost
}
if m.Challenges != nil {
if m.Challenges.HTTP != nil {
cfg.DisableHTTPChallenge = m.Challenges.HTTP.Disabled
cfg.AltHTTPPort = m.Challenges.HTTP.AlternatePort
}
if m.Challenges.TLSALPN != nil {
cfg.DisableTLSALPNChallenge = m.Challenges.TLSALPN.Disabled
cfg.AltTLSALPNPort = m.Challenges.TLSALPN.AlternatePort
}
cfg.DNSProvider = m.Challenges.DNS
}
return cfg
}
// onDemandAskRequest makes a request to the ask URL
+71
View File
@@ -0,0 +1,71 @@
package caddytls
import (
"crypto/tls"
"crypto/x509"
"fmt"
"math/big"
"github.com/caddyserver/caddy/v2"
"github.com/mholt/certmagic"
)
func init() {
caddy.RegisterModule(Policy{})
}
// Policy represents a policy for selecting the certificate used to
// complete a handshake when there may be multiple options. All fields
// specified must match the candidate certificate for it to be chosen.
// This was needed to solve https://github.com/caddyserver/caddy/issues/2588.
type Policy struct {
SerialNumber *big.Int `json:"serial_number,omitempty"`
SubjectOrganization string `json:"subject_organization,omitempty"`
PublicKeyAlgorithm PublicKeyAlgorithm `json:"public_key_algorithm,omitempty"`
Tag string `json:"tag,omitempty"`
}
// CaddyModule returns the Caddy module information.
func (Policy) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "tls.certificate_selection.custom",
New: func() caddy.Module { return new(Policy) },
}
}
// SelectCertificate implements certmagic.CertificateSelector.
func (p Policy) SelectCertificate(_ *tls.ClientHelloInfo, choices []certmagic.Certificate) (certmagic.Certificate, error) {
for _, cert := range choices {
if p.SerialNumber != nil && cert.SerialNumber.Cmp(p.SerialNumber) != 0 {
continue
}
if p.PublicKeyAlgorithm != PublicKeyAlgorithm(x509.UnknownPublicKeyAlgorithm) &&
PublicKeyAlgorithm(cert.PublicKeyAlgorithm) != p.PublicKeyAlgorithm {
continue
}
if p.SubjectOrganization != "" {
var matchOrg bool
for _, org := range cert.Subject.Organization {
if p.SubjectOrganization == org {
matchOrg = true
break
}
}
if !matchOrg {
continue
}
}
if p.Tag != "" && !cert.HasTag(p.Tag) {
continue
}
return cert, nil
}
return certmagic.Certificate{}, fmt.Errorf("no certificates matched custom selection policy")
}
// Interface guard
var _ certmagic.CertificateSelector = (*Policy)(nil)
+14 -12
View File
@@ -46,7 +46,7 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) (*tls.Config, error) {
}
cp[i].matchers = append(cp[i].matchers, val.(ConnectionMatcher))
}
cp[i].Matchers = nil // allow GC to deallocate - TODO: Does this help?
cp[i].Matchers = nil // allow GC to deallocate
// certificate selector
if pol.CertSelection != nil {
@@ -55,7 +55,7 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) (*tls.Config, error) {
return nil, fmt.Errorf("loading certificate selection module: %s", err)
}
cp[i].certSelector = val.(certmagic.CertificateSelector)
cp[i].CertSelection = nil // allow GC to deallocate - TODO: Does this help?
cp[i].CertSelection = nil // allow GC to deallocate
}
}
@@ -155,17 +155,19 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
}
// session tickets support
cfg.SessionTicketsDisabled = tlsApp.SessionTickets.Disabled
if tlsApp.SessionTickets != nil {
cfg.SessionTicketsDisabled = tlsApp.SessionTickets.Disabled
// session ticket key rotation
tlsApp.SessionTickets.register(cfg)
ctx.OnCancel(func() {
// do cleanup when the context is cancelled because,
// though unlikely, it is possible that a context
// needing a TLS server config could exist for less
// than the lifetime of the whole app
tlsApp.SessionTickets.unregister(cfg)
})
// session ticket key rotation
tlsApp.SessionTickets.register(cfg)
ctx.OnCancel(func() {
// do cleanup when the context is cancelled because,
// though unlikely, it is possible that a context
// needing a TLS server config could exist for less
// than the lifetime of the whole app
tlsApp.SessionTickets.unregister(cfg)
})
}
// TODO: Clean up session ticket active locks in storage if app (or process) is being closed!
@@ -0,0 +1,228 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package distributedstek provides TLS session ticket ephemeral
// keys (STEKs) in a distributed fashion by utilizing configured
// storage for locking and key sharing. This allows a cluster of
// machines to optimally resume TLS sessions in a load-balanced
// environment without any hassle. This is similar to what
// Twitter does, but without needing to rely on SSH, as it is
// built into the web server this way:
// https://blog.twitter.com/engineering/en_us/a/2013/forward-secrecy-at-twitter.html
package distributedstek
import (
"bytes"
"encoding/gob"
"encoding/json"
"fmt"
"log"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/mholt/certmagic"
)
func init() {
caddy.RegisterModule(Provider{})
}
// Provider implements a distributed STEK provider.
type Provider struct {
Storage json.RawMessage `json:"storage,omitempty"`
storage certmagic.Storage
stekConfig *caddytls.SessionTicketService
timer *time.Timer
}
// CaddyModule returns the Caddy module information.
func (Provider) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "tls.stek.distributed",
New: func() caddy.Module { return new(Provider) },
}
}
// Provision provisions s.
func (s *Provider) Provision(ctx caddy.Context) error {
// unpack the storage module to use, if different from the default
if s.Storage != nil {
val, err := ctx.LoadModuleInline("module", "caddy.storage", s.Storage)
if err != nil {
return fmt.Errorf("loading TLS storage module: %s", err)
}
cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage()
if err != nil {
return fmt.Errorf("creating TLS storage configuration: %v", err)
}
s.storage = cmStorage
s.Storage = nil // allow GC to deallocate
}
// otherwise, use default storage
if s.storage == nil {
s.storage = ctx.Storage()
}
return nil
}
// Initialize sets the configuration for s and returns the starting keys.
func (s *Provider) Initialize(config *caddytls.SessionTicketService) ([][32]byte, error) {
// keep a reference to the config; we'll need it when rotating keys
s.stekConfig = config
dstek, err := s.getSTEK()
if err != nil {
return nil, err
}
// create timer for the remaining time on the interval;
// this timer is cleaned up only when rotate() returns
s.timer = time.NewTimer(time.Until(dstek.NextRotation))
return dstek.Keys, nil
}
// Next returns a channel which transmits the latest session ticket keys.
func (s *Provider) Next(doneChan <-chan struct{}) <-chan [][32]byte {
keysChan := make(chan [][32]byte)
go s.rotate(doneChan, keysChan)
return keysChan
}
func (s *Provider) loadSTEK() (distributedSTEK, error) {
var sg distributedSTEK
gobBytes, err := s.storage.Load(stekFileName)
if err != nil {
return sg, err // don't wrap, in case error is certmagic.ErrNotExist
}
dec := gob.NewDecoder(bytes.NewReader(gobBytes))
err = dec.Decode(&sg)
if err != nil {
return sg, fmt.Errorf("STEK gob corrupted: %v", err)
}
return sg, nil
}
func (s *Provider) storeSTEK(dstek distributedSTEK) error {
var buf bytes.Buffer
err := gob.NewEncoder(&buf).Encode(dstek)
if err != nil {
return fmt.Errorf("encoding STEK gob: %v", err)
}
err = s.storage.Store(stekFileName, buf.Bytes())
if err != nil {
return fmt.Errorf("storing STEK gob: %v", err)
}
return nil
}
// getSTEK locks and loads the current STEK from storage. If none
// currently exists, a new STEK is created and persisted. If the
// current STEK is outdated (NextRotation time is in the past),
// then it is rotated and persisted. The resulting STEK is returned.
func (s *Provider) getSTEK() (distributedSTEK, error) {
s.storage.Lock(stekLockName)
defer s.storage.Unlock(stekLockName)
// load the current STEKs from storage
dstek, err := s.loadSTEK()
if _, isNotExist := err.(certmagic.ErrNotExist); isNotExist {
// if there is none, then make some right away
dstek, err = s.rotateKeys(dstek)
if err != nil {
return dstek, fmt.Errorf("creating new STEK: %v", err)
}
} else if err != nil {
// some other error, that's a problem
return dstek, fmt.Errorf("loading STEK: %v", err)
} else if time.Now().After(dstek.NextRotation) {
// if current STEKs are outdated, rotate them
dstek, err = s.rotateKeys(dstek)
if err != nil {
return dstek, fmt.Errorf("rotating keys: %v", err)
}
}
return dstek, nil
}
// rotateKeys rotates the keys of oldSTEK and returns the new distributedSTEK
// with updated keys and timestamps. It stores the returned STEK in storage,
// so this function must only be called in a storage-provided lock.
func (s *Provider) rotateKeys(oldSTEK distributedSTEK) (distributedSTEK, error) {
var newSTEK distributedSTEK
var err error
newSTEK.Keys, err = s.stekConfig.RotateSTEKs(oldSTEK.Keys)
if err != nil {
return newSTEK, err
}
now := time.Now()
newSTEK.LastRotation = now
newSTEK.NextRotation = now.Add(time.Duration(s.stekConfig.RotationInterval))
err = s.storeSTEK(newSTEK)
if err != nil {
return newSTEK, err
}
return newSTEK, nil
}
// rotate rotates keys on a regular basis, sending each updated set of
// keys down keysChan, until doneChan is closed.
func (s *Provider) rotate(doneChan <-chan struct{}, keysChan chan<- [][32]byte) {
for {
select {
case <-s.timer.C:
dstek, err := s.getSTEK()
if err != nil {
// TODO: improve this handling
log.Printf("[ERROR] Loading STEK: %v", err)
continue
}
// send the updated keys to the service
keysChan <- dstek.Keys
// timer channel is already drained, so reset directly (see godoc)
s.timer.Reset(time.Until(dstek.NextRotation))
case <-doneChan:
// again, see godocs for why timer is stopped this way
if !s.timer.Stop() {
<-s.timer.C
}
return
}
}
}
type distributedSTEK struct {
Keys [][32]byte
LastRotation, NextRotation time.Time
}
const (
stekLockName = "stek_check"
stekFileName = "stek/stek.bin"
)
// Interface guard
var _ caddytls.STEKProvider = (*Provider)(nil)
+65
View File
@@ -0,0 +1,65 @@
// 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 caddytls
import (
"crypto/tls"
"fmt"
"github.com/caddyserver/caddy/v2"
)
func init() {
caddy.RegisterModule(PEMLoader{})
}
// PEMLoader loads certificates and their associated keys by
// decoding their PEM blocks directly. This has the advantage
// of not needing to store them on disk at all.
type PEMLoader []CertKeyPEMPair
// CaddyModule returns the Caddy module information.
func (PEMLoader) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "tls.certificates.load_pem",
New: func() caddy.Module { return PEMLoader{} },
}
}
// CertKeyPEMPair pairs certificate and key PEM blocks.
type CertKeyPEMPair struct {
CertificatePEM string `json:"certificate"`
KeyPEM string `json:"key"`
Tags []string `json:"tags,omitempty"`
}
// LoadCertificates returns the certificates contained in pl.
func (pl PEMLoader) LoadCertificates() ([]Certificate, error) {
var certs []Certificate
for i, pair := range pl {
cert, err := tls.X509KeyPair([]byte(pair.CertificatePEM), []byte(pair.KeyPEM))
if err != nil {
return nil, fmt.Errorf("PEM pair %d: %v", i, err)
}
certs = append(certs, Certificate{
Certificate: cert,
Tags: pair.Tags,
})
}
return certs, nil
}
// Interface guard
var _ CertificateLoader = (PEMLoader)(nil)
+1 -1
View File
@@ -62,7 +62,7 @@ func (s *SessionTicketService) provision(ctx caddy.Context) error {
return fmt.Errorf("loading TLS session ticket ephemeral keys provider module: %s", err)
}
s.keySource = val.(STEKProvider)
s.KeySource = nil // allow GC to deallocate - TODO: Does this help?
s.KeySource = nil // allow GC to deallocate
// if session tickets or just rotation are
// disabled, no need to start service
+149 -42
View File
@@ -19,12 +19,13 @@ import (
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/go-acme/lego/v3/challenge"
"github.com/mholt/certmagic"
"golang.org/x/time/rate"
"go.uber.org/zap"
)
func init() {
@@ -34,12 +35,15 @@ func init() {
// TLS represents a process-wide TLS configuration.
type TLS struct {
Certificates map[string]json.RawMessage `json:"certificates,omitempty"`
Automation AutomationConfig `json:"automation"`
SessionTickets SessionTicketService `json:"session_tickets"`
Automation *AutomationConfig `json:"automation,omitempty"`
SessionTickets *SessionTicketService `json:"session_tickets,omitempty"`
certificateLoaders []CertificateLoader
certCache *certmagic.Cache
ctx caddy.Context
storageCleanTicker *time.Ticker
storageCleanStop chan struct{}
logger *zap.Logger
}
// CaddyModule returns the Caddy module information.
@@ -53,25 +57,30 @@ func (TLS) CaddyModule() caddy.ModuleInfo {
// Provision sets up the configuration for the TLS app.
func (t *TLS) Provision(ctx caddy.Context) error {
t.ctx = ctx
t.logger = ctx.Logger(t)
// set up the certificate cache
// TODO: this makes a new cache every time; better to only make a new
// cache (or even better, add/remove only what is necessary) if the
// certificates config has been updated
t.certCache = certmagic.NewCache(certmagic.CacheOptions{
// set up a new certificate cache; this (re)loads all certificates
cacheOpts := certmagic.CacheOptions{
GetConfigForCert: func(cert certmagic.Certificate) (certmagic.Config, error) {
return t.getConfigForName(cert.Names[0])
},
})
}
if t.Automation != nil {
cacheOpts.OCSPCheckInterval = time.Duration(t.Automation.OCSPCheckInterval)
cacheOpts.RenewCheckInterval = time.Duration(t.Automation.RenewCheckInterval)
}
t.certCache = certmagic.NewCache(cacheOpts)
// automation/management policies
for i, ap := range t.Automation.Policies {
val, err := ctx.LoadModuleInline("module", "tls.management", ap.ManagementRaw)
if err != nil {
return fmt.Errorf("loading TLS automation management module: %s", err)
if t.Automation != nil {
for i, ap := range t.Automation.Policies {
val, err := ctx.LoadModuleInline("module", "tls.management", ap.ManagementRaw)
if err != nil {
return fmt.Errorf("loading TLS automation management module: %s", err)
}
t.Automation.Policies[i].Management = val.(ManagerMaker)
t.Automation.Policies[i].ManagementRaw = nil // allow GC to deallocate
}
t.Automation.Policies[i].Management = val.(ManagerMaker)
t.Automation.Policies[i].ManagementRaw = nil // allow GC to deallocate - TODO: Does this help?
}
// certificate loaders
@@ -87,19 +96,21 @@ func (t *TLS) Provision(ctx caddy.Context) error {
}
// session ticket ephemeral keys (STEK) service and provider
err := t.SessionTickets.provision(ctx)
if err != nil {
return fmt.Errorf("provisioning session tickets configuration: %v", err)
if t.SessionTickets != nil {
err := t.SessionTickets.provision(ctx)
if err != nil {
return fmt.Errorf("provisioning session tickets configuration: %v", err)
}
}
// on-demand rate limiting
if t.Automation.OnDemand != nil && t.Automation.OnDemand.RateLimit != nil {
limit := rate.Every(time.Duration(t.Automation.OnDemand.RateLimit.Interval))
// TODO: Burst size is not updated, see https://github.com/golang/go/issues/23575
onDemandRateLimiter.SetLimit(limit)
if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.RateLimit != nil {
onDemandRateLimiter.SetMaxEvents(t.Automation.OnDemand.RateLimit.Burst)
onDemandRateLimiter.SetWindow(time.Duration(t.Automation.OnDemand.RateLimit.Interval))
} else {
// if no rate limit is specified, be sure to remove any existing limit
onDemandRateLimiter.SetLimit(0)
// remove any existing rate limiter
onDemandRateLimiter.SetMaxEvents(0)
onDemandRateLimiter.SetWindow(0)
}
// load manual/static (unmanaged) certificates - we do this in
@@ -138,18 +149,37 @@ func (t *TLS) Start() error {
return fmt.Errorf("automate: managing %v: %v", names, err)
}
}
t.Certificates = nil // allow GC to deallocate - TODO: Does this help?
t.Certificates = nil // allow GC to deallocate
t.keepStorageClean()
return nil
}
// Stop stops the TLS module and cleans up any allocations.
func (t *TLS) Stop() error {
// stop the storage cleaner goroutine and ticker
if t.storageCleanStop != nil {
close(t.storageCleanStop)
}
if t.storageCleanTicker != nil {
t.storageCleanTicker.Stop()
}
return nil
}
// Cleanup frees up resources allocated during Provision.
func (t *TLS) Cleanup() error {
// stop the certificate cache
if t.certCache != nil {
// TODO: ensure locks are cleaned up too... maybe in certmagic though
t.certCache.Stop()
}
t.SessionTickets.stop()
// stop the session ticket rotation goroutine
if t.SessionTickets != nil {
t.SessionTickets.stop()
}
return nil
}
@@ -159,7 +189,12 @@ func (t *TLS) Manage(names []string) error {
for _, name := range names {
ap := t.getAutomationPolicyForName(name)
magic := certmagic.New(t.certCache, ap.makeCertMagicConfig(t.ctx))
err := magic.Manage([]string{name})
var err error
if ap.ManageSync {
err = magic.ManageSync([]string{name})
} else {
err = magic.ManageAsync(t.ctx.Context, []string{name})
}
if err != nil {
return fmt.Errorf("automate: manage %s: %v", name, err)
}
@@ -184,15 +219,17 @@ func (t *TLS) getConfigForName(name string) (certmagic.Config, error) {
}
func (t *TLS) getAutomationPolicyForName(name string) AutomationPolicy {
for _, ap := range t.Automation.Policies {
if len(ap.Hosts) == 0 {
// no host filter is an automatic match
return ap
}
for _, h := range ap.Hosts {
if h == name {
if t.Automation != nil {
for _, ap := range t.Automation.Policies {
if len(ap.Hosts) == 0 {
// no host filter is an automatic match
return ap
}
for _, h := range ap.Hosts {
if h == name {
return ap
}
}
}
}
@@ -200,12 +237,64 @@ func (t *TLS) getAutomationPolicyForName(name string) AutomationPolicy {
return AutomationPolicy{Management: new(ACMEManagerMaker)}
}
// CertificatesForSAN returns the list of all certificates in
// AllMatchingCertificates returns the list of all certificates in
// the cache which could be used to satisfy the given SAN.
func (t *TLS) AllMatchingCertificates(san string) []certmagic.Certificate {
return t.certCache.AllMatchingCertificates(san)
}
// keepStorageClean immediately cleans up all known storage units
// if it was not recently done, and starts a goroutine that runs
// the operation at every tick from t.storageCleanTicker.
func (t *TLS) keepStorageClean() {
t.storageCleanTicker = time.NewTicker(storageCleanInterval)
t.storageCleanStop = make(chan struct{})
go func() {
for {
select {
case <-t.storageCleanStop:
return
case <-t.storageCleanTicker.C:
t.cleanStorageUnits()
}
}
}()
t.cleanStorageUnits()
}
func (t *TLS) cleanStorageUnits() {
storageCleanMu.Lock()
defer storageCleanMu.Unlock()
if !storageClean.IsZero() && time.Since(storageClean) < storageCleanInterval {
return
}
options := certmagic.CleanStorageOptions{
OCSPStaples: true,
ExpiredCerts: true,
ExpiredCertGracePeriod: 24 * time.Hour * 14,
}
// start with the default storage
certmagic.CleanStorage(t.ctx.Storage(), options)
// then clean each storage defined in ACME automation policies
if t.Automation != nil {
for _, ap := range t.Automation.Policies {
if acmeMgmt, ok := ap.Management.(ACMEManagerMaker); ok {
if acmeMgmt.storage != nil {
certmagic.CleanStorage(acmeMgmt.storage, options)
}
}
}
}
storageClean = time.Now()
t.logger.Info("cleaned up storage units")
}
// CertificateLoader is a type that can load certificates.
// Certificates can optionally be associated with tags.
type CertificateLoader interface {
@@ -222,8 +311,10 @@ type Certificate struct {
// AutomationConfig designates configuration for the
// construction and use of ACME clients.
type AutomationConfig struct {
Policies []AutomationPolicy `json:"policies,omitempty"`
OnDemand *OnDemandConfig `json:"on_demand,omitempty"`
Policies []AutomationPolicy `json:"policies,omitempty"`
OnDemand *OnDemandConfig `json:"on_demand,omitempty"`
OCSPCheckInterval caddy.Duration `json:"ocsp_interval,omitempty"`
RenewCheckInterval caddy.Duration `json:"renew_interval,omitempty"`
}
// AutomationPolicy designates the policy for automating the
@@ -231,6 +322,7 @@ type AutomationConfig struct {
type AutomationPolicy struct {
Hosts []string `json:"hosts,omitempty"`
ManagementRaw json.RawMessage `json:"management,omitempty"`
ManageSync bool `json:"manage_sync,omitempty"`
Management ManagerMaker `json:"-"`
}
@@ -253,9 +345,9 @@ func (ap AutomationPolicy) makeCertMagicConfig(ctx caddy.Context) certmagic.Conf
// ChallengesConfig configures the ACME challenges.
type ChallengesConfig struct {
HTTP HTTPChallengeConfig `json:"http"`
TLSALPN TLSALPNChallengeConfig `json:"tls-alpn"`
DNSRaw json.RawMessage `json:"dns,omitempty"`
HTTP *HTTPChallengeConfig `json:"http,omitempty"`
TLSALPN *TLSALPNChallengeConfig `json:"tls-alpn,omitempty"`
DNSRaw json.RawMessage `json:"dns,omitempty"`
DNS challenge.Provider `json:"-"`
}
@@ -292,7 +384,7 @@ type ManagerMaker interface {
// These perpetual values are used for on-demand TLS.
var (
onDemandRateLimiter = rate.NewLimiter(0, 1)
onDemandRateLimiter = certmagic.NewRateLimiter(0, 0)
onDemandAskClient = &http.Client{
Timeout: 10 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
@@ -301,4 +393,19 @@ var (
}
)
// Variables related to storage cleaning.
var (
storageCleanInterval = 12 * time.Hour
storageClean time.Time
storageCleanMu sync.Mutex
)
// Interface guards
var (
_ caddy.App = (*TLS)(nil)
_ caddy.Provisioner = (*TLS)(nil)
_ caddy.CleanerUpper = (*TLS)(nil)
)
const automateKey = "automate"
+63
View File
@@ -0,0 +1,63 @@
// 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 filestorage
import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/mholt/certmagic"
)
func init() {
caddy.RegisterModule(FileStorage{})
}
// FileStorage is a certmagic.Storage wrapper for certmagic.FileStorage.
type FileStorage struct {
Root string `json:"root,omitempty"`
}
// CaddyModule returns the Caddy module information.
func (FileStorage) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "caddy.storage.file_system",
New: func() caddy.Module { return new(FileStorage) },
}
}
// CertMagicStorage converts s to a certmagic.Storage instance.
func (s FileStorage) CertMagicStorage() (certmagic.Storage, error) {
return &certmagic.FileStorage{Path: s.Root}, nil
}
// UnmarshalCaddyfile sets up the storage module from Caddyfile tokens.
func (s *FileStorage) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if !d.Next() {
return d.Err("expected tokens")
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
if !d.NextArg() {
return d.ArgErr()
}
s.Root = d.Val()
if d.NextArg() {
return d.ArgErr()
}
}
return nil
}
// Interface guard
var _ caddy.StorageConverter = (*FileStorage)(nil)
+268
View File
@@ -0,0 +1,268 @@
// 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 logging
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/caddyserver/caddy/v2"
zaplogfmt "github.com/jsternberg/zap-logfmt"
"go.uber.org/zap"
"go.uber.org/zap/buffer"
"go.uber.org/zap/zapcore"
)
func init() {
caddy.RegisterModule(ConsoleEncoder{})
caddy.RegisterModule(JSONEncoder{})
caddy.RegisterModule(LogfmtEncoder{})
caddy.RegisterModule(StringEncoder{})
}
// ConsoleEncoder encodes log entries that are mostly human-readable.
type ConsoleEncoder struct {
zapcore.Encoder
LogEncoderConfig
}
// CaddyModule returns the Caddy module information.
func (ConsoleEncoder) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "caddy.logging.encoders.console",
New: func() caddy.Module { return new(ConsoleEncoder) },
}
}
// Provision sets up the encoder.
func (ce *ConsoleEncoder) Provision(_ caddy.Context) error {
ce.Encoder = zapcore.NewConsoleEncoder(ce.ZapcoreEncoderConfig())
return nil
}
// JSONEncoder encodes entries as JSON.
type JSONEncoder struct {
zapcore.Encoder
*LogEncoderConfig
}
// CaddyModule returns the Caddy module information.
func (JSONEncoder) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "caddy.logging.encoders.json",
New: func() caddy.Module { return new(JSONEncoder) },
}
}
// Provision sets up the encoder.
func (je *JSONEncoder) Provision(_ caddy.Context) error {
je.Encoder = zapcore.NewJSONEncoder(je.ZapcoreEncoderConfig())
return nil
}
// LogfmtEncoder encodes log entries as logfmt:
// https://www.brandur.org/logfmt
type LogfmtEncoder struct {
zapcore.Encoder
LogEncoderConfig
}
// CaddyModule returns the Caddy module information.
func (LogfmtEncoder) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "caddy.logging.encoders.logfmt",
New: func() caddy.Module { return new(LogfmtEncoder) },
}
}
// Provision sets up the encoder.
func (lfe *LogfmtEncoder) Provision(_ caddy.Context) error {
lfe.Encoder = zaplogfmt.NewEncoder(lfe.ZapcoreEncoderConfig())
return nil
}
// StringEncoder writes a log entry that consists entirely
// of a single string field in the log entry. This is useful
// for custom, self-encoded log entries that consist of a
// single field in the structured log entry.
type StringEncoder struct {
zapcore.Encoder
FieldName string `json:"field,omitempty"`
FallbackRaw json.RawMessage `json:"fallback,omitempty"`
}
// CaddyModule returns the Caddy module information.
func (StringEncoder) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "caddy.logging.encoders.string",
New: func() caddy.Module { return new(StringEncoder) },
}
}
// Provision sets up the encoder.
func (se *StringEncoder) Provision(ctx caddy.Context) error {
if se.FallbackRaw != nil {
val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", se.FallbackRaw)
if err != nil {
return fmt.Errorf("loading fallback encoder module: %v", err)
}
se.FallbackRaw = nil // allow GC to deallocate
se.Encoder = val.(zapcore.Encoder)
}
if se.Encoder == nil {
se.Encoder = nopEncoder{}
}
return nil
}
// Clone wraps the underlying encoder's Clone. This is
// necessary because we implement our own EncodeEntry,
// and if we simply let the embedded encoder's Clone
// be promoted, it would return a clone of that, and
// we'd lose our StringEncoder's EncodeEntry.
func (se StringEncoder) Clone() zapcore.Encoder {
return StringEncoder{
Encoder: se.Encoder.Clone(),
FieldName: se.FieldName,
}
}
// EncodeEntry partially implements the zapcore.Encoder interface.
func (se StringEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
for _, f := range fields {
if f.Key == se.FieldName {
buf := bufferpool.Get()
buf.AppendString(f.String)
if !strings.HasSuffix(f.String, "\n") {
buf.AppendByte('\n')
}
return buf, nil
}
}
if se.Encoder == nil {
return nil, fmt.Errorf("no fallback encoder defined")
}
return se.Encoder.EncodeEntry(ent, fields)
}
// LogEncoderConfig holds configuration common to most encoders.
type LogEncoderConfig struct {
MessageKey *string `json:"message_key,omitempty"`
LevelKey *string `json:"level_key,omitempty"`
TimeKey *string `json:"time_key,omitempty"`
NameKey *string `json:"name_key,omitempty"`
CallerKey *string `json:"caller_key,omitempty"`
StacktraceKey *string `json:"stacktrace_key,omitempty"`
LineEnding *string `json:"line_ending,omitempty"`
TimeFormat string `json:"time_format,omitempty"`
DurationFormat string `json:"duration_format,omitempty"`
LevelFormat string `json:"level_format,omitempty"`
}
// ZapcoreEncoderConfig returns the equivalent zapcore.EncoderConfig.
// If lec is nil, zap.NewProductionEncoderConfig() is returned.
func (lec *LogEncoderConfig) ZapcoreEncoderConfig() zapcore.EncoderConfig {
cfg := zap.NewProductionEncoderConfig()
if lec == nil {
lec = new(LogEncoderConfig)
}
if lec.MessageKey != nil {
cfg.MessageKey = *lec.MessageKey
}
if lec.TimeKey != nil {
cfg.TimeKey = *lec.TimeKey
}
if lec.NameKey != nil {
cfg.NameKey = *lec.NameKey
}
if lec.CallerKey != nil {
cfg.CallerKey = *lec.CallerKey
}
if lec.StacktraceKey != nil {
cfg.StacktraceKey = *lec.StacktraceKey
}
if lec.LineEnding != nil {
cfg.LineEnding = *lec.LineEnding
}
// time format
var timeFormatter zapcore.TimeEncoder
switch lec.TimeFormat {
case "", "unix_seconds_float":
timeFormatter = zapcore.EpochTimeEncoder
case "unix_milli_float":
timeFormatter = zapcore.EpochMillisTimeEncoder
case "unix_nano":
timeFormatter = zapcore.EpochNanosTimeEncoder
case "iso8601":
timeFormatter = zapcore.ISO8601TimeEncoder
default:
timeFormat := lec.TimeFormat
switch lec.TimeFormat {
case "rfc3339":
timeFormat = time.RFC3339
case "rfc3339_nano":
timeFormat = time.RFC3339Nano
case "wall":
timeFormat = "2006/01/02 15:04:05"
case "wall_milli":
timeFormat = "2006/01/02 15:04:05.000"
case "wall_nano":
timeFormat = "2006/01/02 15:04:05.000000000"
}
timeFormatter = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(ts.UTC().Format(timeFormat))
}
}
cfg.EncodeTime = timeFormatter
// duration format
var durFormatter zapcore.DurationEncoder
switch lec.DurationFormat {
case "", "seconds":
durFormatter = zapcore.SecondsDurationEncoder
case "nano":
durFormatter = zapcore.NanosDurationEncoder
case "string":
durFormatter = zapcore.StringDurationEncoder
}
cfg.EncodeDuration = durFormatter
// level format
var levelFormatter zapcore.LevelEncoder
switch lec.LevelFormat {
case "", "lower":
levelFormatter = zapcore.LowercaseLevelEncoder
case "upper":
levelFormatter = zapcore.CapitalLevelEncoder
case "color":
levelFormatter = zapcore.CapitalColorLevelEncoder
}
cfg.EncodeLevel = levelFormatter
return cfg
}
var bufferpool = buffer.NewPool()
// Interface guards
var (
_ zapcore.Encoder = (*ConsoleEncoder)(nil)
_ zapcore.Encoder = (*JSONEncoder)(nil)
_ zapcore.Encoder = (*LogfmtEncoder)(nil)
_ zapcore.Encoder = (*StringEncoder)(nil)
)
+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 logging
import (
"io"
"os"
"path/filepath"
"github.com/caddyserver/caddy/v2"
"gopkg.in/natefinch/lumberjack.v2"
)
func init() {
caddy.RegisterModule(FileWriter{})
}
// FileWriter can write logs to files.
type FileWriter struct {
Filename string `json:"filename,omitempty"`
Roll *bool `json:"roll,omitempty"`
RollSizeMB int `json:"roll_size_mb,omitempty"`
RollCompress *bool `json:"roll_gzip,omitempty"`
RollLocalTime bool `json:"roll_local_time,omitempty"`
RollKeep int `json:"roll_keep,omitempty"`
RollKeepDays int `json:"roll_keep_days,omitempty"`
}
// CaddyModule returns the Caddy module information.
func (FileWriter) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "caddy.logging.writers.file",
New: func() caddy.Module { return new(FileWriter) },
}
}
func (fw FileWriter) String() string {
fpath, err := filepath.Abs(fw.Filename)
if err == nil {
return fpath
}
return fw.Filename
}
// WriterKey returns a unique key representing this fw.
func (fw FileWriter) WriterKey() string {
return "file:" + fw.Filename
}
// OpenWriter opens a new file writer.
func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
// roll log files by default
if fw.Roll == nil || *fw.Roll == true {
if fw.RollSizeMB == 0 {
fw.RollSizeMB = 100
}
if fw.RollCompress == nil {
compress := true
fw.RollCompress = &compress
}
if fw.RollKeep == 0 {
fw.RollKeep = 10
}
if fw.RollKeepDays == 0 {
fw.RollKeepDays = 90
}
return &lumberjack.Logger{
Filename: fw.Filename,
MaxSize: fw.RollSizeMB,
MaxAge: fw.RollKeepDays,
MaxBackups: fw.RollKeep,
LocalTime: fw.RollLocalTime,
Compress: *fw.RollCompress,
}, nil
}
// otherwise just open a regular file
return os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
}
+321
View File
@@ -0,0 +1,321 @@
// 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 logging
import (
"encoding/json"
"fmt"
"time"
"github.com/caddyserver/caddy/v2"
"go.uber.org/zap"
"go.uber.org/zap/buffer"
"go.uber.org/zap/zapcore"
)
func init() {
caddy.RegisterModule(FilterEncoder{})
}
// FilterEncoder wraps an underlying encoder. It does
// not do any encoding itself, but it can manipulate
// (filter) fields before they are actually encoded.
// A wrapped encoder is required.
type FilterEncoder struct {
WrappedRaw json.RawMessage `json:"wrap,omitempty"`
FieldsRaw map[string]json.RawMessage `json:"fields,omitempty"`
wrapped zapcore.Encoder
Fields map[string]LogFieldFilter `json:"-"`
// used to keep keys unique across nested objects
keyPrefix string
}
// CaddyModule returns the Caddy module information.
func (FilterEncoder) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "caddy.logging.encoders.filter",
New: func() caddy.Module { return new(FilterEncoder) },
}
}
// Provision sets up the encoder.
func (fe *FilterEncoder) Provision(ctx caddy.Context) error {
if fe.WrappedRaw == nil {
return fmt.Errorf("missing \"wrap\" (must specify an underlying encoder)")
}
// set up wrapped encoder (required)
val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", fe.WrappedRaw)
if err != nil {
return fmt.Errorf("loading fallback encoder module: %v", err)
}
fe.WrappedRaw = nil // allow GC to deallocate
fe.wrapped = val.(zapcore.Encoder)
// set up each field filter
if fe.Fields == nil {
fe.Fields = make(map[string]LogFieldFilter)
}
for field, filterRaw := range fe.FieldsRaw {
if filterRaw == nil {
continue
}
val, err := ctx.LoadModuleInline("filter", "caddy.logging.encoders.filter", filterRaw)
if err != nil {
return fmt.Errorf("loading log filter module: %v", err)
}
fe.Fields[field] = val.(LogFieldFilter)
}
fe.FieldsRaw = nil // allow GC to deallocate
return nil
}
// AddArray is part of the zapcore.ObjectEncoder interface.
// Array elements do not get filtered.
func (fe FilterEncoder) AddArray(key string, marshaler zapcore.ArrayMarshaler) error {
if filter, ok := fe.Fields[fe.keyPrefix+key]; ok {
filter.Filter(zap.Array(key, marshaler)).AddTo(fe.wrapped)
return nil
}
return fe.wrapped.AddArray(key, marshaler)
}
// AddObject is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddObject(key string, marshaler zapcore.ObjectMarshaler) error {
fe.keyPrefix += key + ">"
return fe.wrapped.AddObject(key, logObjectMarshalerWrapper{
enc: fe,
marsh: marshaler,
})
}
// AddBinary is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddBinary(key string, value []byte) {
if !fe.filtered(key, value) {
fe.wrapped.AddBinary(key, value)
}
}
// AddByteString is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddByteString(key string, value []byte) {
if !fe.filtered(key, value) {
fe.wrapped.AddByteString(key, value)
}
}
// AddBool is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddBool(key string, value bool) {
if !fe.filtered(key, value) {
fe.wrapped.AddBool(key, value)
}
}
// AddComplex128 is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddComplex128(key string, value complex128) {
if !fe.filtered(key, value) {
fe.wrapped.AddComplex128(key, value)
}
}
// AddComplex64 is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddComplex64(key string, value complex64) {
if !fe.filtered(key, value) {
fe.wrapped.AddComplex64(key, value)
}
}
// AddDuration is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddDuration(key string, value time.Duration) {
if !fe.filtered(key, value) {
fe.wrapped.AddDuration(key, value)
}
}
// AddFloat64 is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddFloat64(key string, value float64) {
if !fe.filtered(key, value) {
fe.wrapped.AddFloat64(key, value)
}
}
// AddFloat32 is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddFloat32(key string, value float32) {
if !fe.filtered(key, value) {
fe.wrapped.AddFloat32(key, value)
}
}
// AddInt is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddInt(key string, value int) {
if !fe.filtered(key, value) {
fe.wrapped.AddInt(key, value)
}
}
// AddInt64 is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddInt64(key string, value int64) {
if !fe.filtered(key, value) {
fe.wrapped.AddInt64(key, value)
}
}
// AddInt32 is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddInt32(key string, value int32) {
if !fe.filtered(key, value) {
fe.wrapped.AddInt32(key, value)
}
}
// AddInt16 is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddInt16(key string, value int16) {
if !fe.filtered(key, value) {
fe.wrapped.AddInt16(key, value)
}
}
// AddInt8 is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddInt8(key string, value int8) {
if !fe.filtered(key, value) {
fe.wrapped.AddInt8(key, value)
}
}
// AddString is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddString(key, value string) {
if !fe.filtered(key, value) {
fe.wrapped.AddString(key, value)
}
}
// AddTime is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddTime(key string, value time.Time) {
if !fe.filtered(key, value) {
fe.wrapped.AddTime(key, value)
}
}
// AddUint is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddUint(key string, value uint) {
if !fe.filtered(key, value) {
fe.wrapped.AddUint(key, value)
}
}
// AddUint64 is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddUint64(key string, value uint64) {
if !fe.filtered(key, value) {
fe.wrapped.AddUint64(key, value)
}
}
// AddUint32 is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddUint32(key string, value uint32) {
if !fe.filtered(key, value) {
fe.wrapped.AddUint32(key, value)
}
}
// AddUint16 is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddUint16(key string, value uint16) {
if !fe.filtered(key, value) {
fe.wrapped.AddUint16(key, value)
}
}
// AddUint8 is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddUint8(key string, value uint8) {
if !fe.filtered(key, value) {
fe.wrapped.AddUint8(key, value)
}
}
// AddUintptr is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddUintptr(key string, value uintptr) {
if !fe.filtered(key, value) {
fe.wrapped.AddUintptr(key, value)
}
}
// AddReflected is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) AddReflected(key string, value interface{}) error {
if !fe.filtered(key, value) {
return fe.wrapped.AddReflected(key, value)
}
return nil
}
// OpenNamespace is part of the zapcore.ObjectEncoder interface.
func (fe FilterEncoder) OpenNamespace(key string) {
fe.wrapped.OpenNamespace(key)
}
// Clone is part of the zapcore.ObjectEncoder interface.
// We don't use it as of Oct 2019 (v2 beta 7), I'm not
// really sure what it'd be useful for in our case.
func (fe FilterEncoder) Clone() zapcore.Encoder {
return FilterEncoder{
Fields: fe.Fields,
wrapped: fe.wrapped.Clone(),
keyPrefix: fe.keyPrefix,
}
}
// EncodeEntry partially implements the zapcore.Encoder interface.
func (fe FilterEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
// without this clone and storing it to fe.wrapped, fields
// from subsequent log entries get appended to previous
// ones, and I'm not 100% sure why; see end of
// https://github.com/uber-go/zap/issues/750
fe.wrapped = fe.wrapped.Clone()
for _, field := range fields {
field.AddTo(fe)
}
return fe.wrapped.EncodeEntry(ent, nil)
}
// filtered returns true if the field was filtered.
// If true is returned, the field was filtered and
// added to the underlying encoder (so do not do
// that again). If false was returned, the field has
// not yet been added to the underlying encoder.
func (fe FilterEncoder) filtered(key string, value interface{}) bool {
filter, ok := fe.Fields[fe.keyPrefix+key]
if !ok {
return false
}
filter.Filter(zap.Any(key, value)).AddTo(fe.wrapped)
return true
}
// logObjectMarshalerWrapper allows us to recursively
// filter fields of objects as they get encoded.
type logObjectMarshalerWrapper struct {
enc FilterEncoder
marsh zapcore.ObjectMarshaler
}
// MarshalLogObject implements the zapcore.ObjectMarshaler interface.
func (mom logObjectMarshalerWrapper) MarshalLogObject(_ zapcore.ObjectEncoder) error {
return mom.marsh.MarshalLogObject(mom.enc)
}
// Interface guards
var (
_ zapcore.Encoder = (*FilterEncoder)(nil)
_ zapcore.ObjectMarshaler = (*logObjectMarshalerWrapper)(nil)
)
+94
View File
@@ -0,0 +1,94 @@
// 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 logging
import (
"net"
"github.com/caddyserver/caddy/v2"
"go.uber.org/zap/zapcore"
)
func init() {
caddy.RegisterModule(DeleteFilter{})
caddy.RegisterModule(IPMaskFilter{})
}
// LogFieldFilter can filter (or manipulate)
// a field in a log entry. If delete is true,
// out will be ignored and the field will be
// removed from the output.
type LogFieldFilter interface {
Filter(zapcore.Field) zapcore.Field
}
// DeleteFilter is a Caddy log field filter that
// deletes the field.
type DeleteFilter struct{}
// CaddyModule returns the Caddy module information.
func (DeleteFilter) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "caddy.logging.encoders.filter.delete",
New: func() caddy.Module { return new(DeleteFilter) },
}
}
// Filter filters the input field.
func (DeleteFilter) Filter(in zapcore.Field) zapcore.Field {
in.Type = zapcore.SkipType
return in
}
// IPMaskFilter is a Caddy log field filter that
// masks IP addresses.
type IPMaskFilter struct {
IPv4CIDR int `json:"ipv4_cidr,omitempty"`
IPv6CIDR int `json:"ipv6_cidr,omitempty"`
}
// CaddyModule returns the Caddy module information.
func (IPMaskFilter) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "caddy.logging.encoders.filter.ip_mask",
New: func() caddy.Module { return new(IPMaskFilter) },
}
}
// Filter filters the input field.
func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
host, port, err := net.SplitHostPort(in.String)
if err != nil {
host = in.String // assume whole thing was IP address
}
ipAddr := net.ParseIP(host)
if ipAddr == nil {
return in
}
bitLen := 32
cidrPrefix := m.IPv4CIDR
if ipAddr.To16() != nil {
bitLen = 128
cidrPrefix = m.IPv6CIDR
}
mask := net.CIDRMask(cidrPrefix, bitLen)
masked := ipAddr.Mask(mask)
if port == "" {
in.String = masked.String()
} else {
in.String = net.JoinHostPort(masked.String(), port)
}
return in
}
+114
View File
@@ -0,0 +1,114 @@
// 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 logging
import (
"time"
"go.uber.org/zap/buffer"
"go.uber.org/zap/zapcore"
)
// nopEncoder is a zapcore.Encoder that does nothing.
type nopEncoder struct{}
// AddArray is part of the zapcore.ObjectEncoder interface.
// Array elements do not get filtered.
func (nopEncoder) AddArray(key string, marshaler zapcore.ArrayMarshaler) error { return nil }
// AddObject is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddObject(key string, marshaler zapcore.ObjectMarshaler) error { return nil }
// AddBinary is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddBinary(key string, value []byte) {}
// AddByteString is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddByteString(key string, value []byte) {}
// AddBool is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddBool(key string, value bool) {}
// AddComplex128 is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddComplex128(key string, value complex128) {}
// AddComplex64 is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddComplex64(key string, value complex64) {}
// AddDuration is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddDuration(key string, value time.Duration) {}
// AddFloat64 is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddFloat64(key string, value float64) {}
// AddFloat32 is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddFloat32(key string, value float32) {}
// AddInt is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddInt(key string, value int) {}
// AddInt64 is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddInt64(key string, value int64) {}
// AddInt32 is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddInt32(key string, value int32) {}
// AddInt16 is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddInt16(key string, value int16) {}
// AddInt8 is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddInt8(key string, value int8) {}
// AddString is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddString(key, value string) {}
// AddTime is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddTime(key string, value time.Time) {}
// AddUint is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddUint(key string, value uint) {}
// AddUint64 is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddUint64(key string, value uint64) {}
// AddUint32 is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddUint32(key string, value uint32) {}
// AddUint16 is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddUint16(key string, value uint16) {}
// AddUint8 is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddUint8(key string, value uint8) {}
// AddUintptr is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddUintptr(key string, value uintptr) {}
// AddReflected is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) AddReflected(key string, value interface{}) error { return nil }
// OpenNamespace is part of the zapcore.ObjectEncoder interface.
func (nopEncoder) OpenNamespace(key string) {}
// Clone is part of the zapcore.ObjectEncoder interface.
// We don't use it as of Oct 2019 (v2 beta 7), I'm not
// really sure what it'd be useful for in our case.
func (ne nopEncoder) Clone() zapcore.Encoder { return ne }
// EncodeEntry partially implements the zapcore.Encoder interface.
func (nopEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
return bufferpool.Get(), nil
}
// Interface guard
var _ zapcore.Encoder = (*nopEncoder)(nil)
+60 -14
View File
@@ -15,10 +15,12 @@
package caddy
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"time"
)
// Replacer can replace values in strings.
@@ -27,6 +29,8 @@ type Replacer interface {
Delete(variable string)
Map(ReplacementFunc)
ReplaceAll(input, empty string) string
ReplaceKnown(input, empty string) string
ReplaceOrErr(input string, errOnEmpty, errOnUnknown bool) (string, error)
}
// NewReplacer returns a new Replacer.
@@ -69,12 +73,34 @@ func (r *replacer) fromStatic(key string) (val string, ok bool) {
return
}
// ReplaceOrErr is like ReplaceAll, but any placeholders
// that are empty or not recognized will cause an error to
// be returned.
func (r *replacer) ReplaceOrErr(input string, errOnEmpty, errOnUnknown bool) (string, error) {
return r.replace(input, "", false, errOnEmpty, errOnUnknown)
}
// ReplaceKnown is like ReplaceAll but only replaces
// placeholders that are known (recognized). Unrecognized
// placeholders will remain in the output.
func (r *replacer) ReplaceKnown(input, empty string) string {
out, _ := r.replace(input, empty, false, false, false)
return out
}
// ReplaceAll efficiently replaces placeholders in input with
// their values. Unrecognized placeholders will not be replaced.
// Values that are empty string will be substituted with empty.
// their values. All placeholders are replaced in the output
// whether they are recognized or not. Values that are empty
// string will be substituted with empty.
func (r *replacer) ReplaceAll(input, empty string) string {
out, _ := r.replace(input, empty, true, false, false)
return out
}
func (r *replacer) replace(input, empty string,
treatUnknownAsEmpty, errOnEmpty, errOnUnknown bool) (string, error) {
if !strings.Contains(input, string(phOpen)) {
return input
return input, nil
}
var sb strings.Builder
@@ -99,24 +125,39 @@ func (r *replacer) ReplaceAll(input, empty string) string {
// trim opening bracket
key := input[i+1 : end]
// try to get a value for this key; if
// the key is not recognized, do not
// perform any replacement
// try to get a value for this key,
// handle empty values accordingly
var found bool
for _, mapFunc := range r.providers {
if val, ok := mapFunc(key); ok {
found = true
if val != "" {
if val == "" {
if errOnEmpty {
return "", fmt.Errorf("evaluated placeholder %s%s%s is empty",
string(phOpen), key, string(phClose))
} else if empty != "" {
sb.WriteString(empty)
}
} else {
sb.WriteString(val)
} else if empty != "" {
sb.WriteString(empty)
}
break
}
}
if !found {
lastWriteCursor = i
continue
// placeholder is unknown (unrecognized), handle accordingly
switch {
case errOnUnknown:
return "", fmt.Errorf("unrecognized placeholder %s%s%s",
string(phOpen), key, string(phClose))
case treatUnknownAsEmpty:
if empty != "" {
sb.WriteString(empty)
}
default:
lastWriteCursor = i
continue
}
}
// advance cursor to end of placeholder
@@ -127,7 +168,7 @@ func (r *replacer) ReplaceAll(input, empty string) string {
// flush any unwritten remainder
sb.WriteString(input[lastWriteCursor:])
return sb.String()
return sb.String(), nil
}
// ReplacementFunc is a function that returns a replacement
@@ -141,8 +182,7 @@ func globalDefaultReplacements(key string) (string, bool) {
// check environment variable
const envPrefix = "env."
if strings.HasPrefix(key, envPrefix) {
val := os.Getenv(key[len(envPrefix):])
return val, val != ""
return os.Getenv(key[len(envPrefix):]), true
}
switch key {
@@ -156,11 +196,17 @@ func globalDefaultReplacements(key string) (string, bool) {
return runtime.GOOS, true
case "system.arch":
return runtime.GOARCH, true
case "time.now.common_log":
return nowFunc().Format("02/Jan/2006:15:04:05 -0700"), true
}
return "", false
}
// nowFunc is a variable so tests can change it
// in order to obtain a deterministic time.
var nowFunc = time.Now
// ReplacerCtxKey is the context key for a replacer.
const ReplacerCtxKey CtxKey = "replacer"
+26
View File
@@ -0,0 +1,26 @@
// 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
// +build gofuzz_libfuzzer
package caddy
func FuzzReplacer(data []byte) (score int) {
NewReplacer().ReplaceAll(string(data), "")
NewReplacer().ReplaceAll(NewReplacer().ReplaceAll(string(data), ""), "")
NewReplacer().ReplaceAll(NewReplacer().ReplaceAll(string(data), ""), NewReplacer().ReplaceAll(string(data), ""))
NewReplacer().ReplaceAll(string(data[:len(data)/2]), string(data[len(data)/2:]))
return 0
}
+2 -2
View File
@@ -85,7 +85,7 @@ func TestReplacerSet(t *testing.T) {
}
}
func TestReplacerReplaceAll(t *testing.T) {
func TestReplacerReplaceKnown(t *testing.T) {
rep := replacer{
providers: []ReplacementFunc{
// split our possible vars to two functions (to test if both functions are called)
@@ -148,7 +148,7 @@ func TestReplacerReplaceAll(t *testing.T) {
expected: "val1 {nope} test-123 ",
},
} {
actual := rep.ReplaceAll(tc.testInput, "EMPTY")
actual := rep.ReplaceKnown(tc.testInput, "EMPTY")
// test if all are replaced as expected
if actual != tc.expected {
+12 -8
View File
@@ -15,9 +15,10 @@
package caddy
import (
"log"
"os"
"os/signal"
"go.uber.org/zap"
)
// TrapSignals create signal/interrupt handlers as best it can for the
@@ -41,11 +42,11 @@ func trapSignalsCrossPlatform() {
<-shutdown
if i > 0 {
log.Println("[INFO] SIGINT: Force quit")
Log().Warn("force quit", zap.String("signal", "SIGINT"))
os.Exit(ExitCodeForceQuit)
}
log.Println("[INFO] SIGINT: Shutting down")
Log().Info("shutting down", zap.String("signal", "SIGINT"))
go gracefulStop("SIGINT")
}
}()
@@ -57,17 +58,20 @@ func gracefulStop(sigName string) {
err := stopAndCleanup()
if err != nil {
log.Printf("[ERROR] %s stop: %v", sigName, err)
Log().Error("stopping",
zap.String("signal", sigName),
zap.Error(err),
)
exitCode = ExitCodeFailedQuit
}
log.Printf("[INFO] %s: Shutdown done", sigName)
Log().Info("shutdown done", zap.String("signal", sigName))
os.Exit(exitCode)
}
// Exit codes. Generally, you will want to avoid
// automatically restarting the process if the
// exit code is 1.
// Exit codes. Generally, you should NOT
// automatically restart the process if the
// exit code is ExitCodeFailedStartup (1).
const (
ExitCodeSuccess = iota
ExitCodeFailedStartup
+6 -6
View File
@@ -17,12 +17,12 @@
package caddy
import (
"log"
"os"
"os/signal"
"syscall"
"github.com/mholt/certmagic"
"go.uber.org/zap"
)
// trapSignalsPosix captures POSIX-only signals.
@@ -34,23 +34,23 @@ func trapSignalsPosix() {
for sig := range sigchan {
switch sig {
case syscall.SIGQUIT:
log.Println("[INFO] SIGQUIT: Quitting process immediately")
Log().Info("quitting process immediately", zap.String("signal", "SIGQUIT"))
certmagic.CleanUpOwnLocks() // try to clean up locks anyway, it's important
os.Exit(ExitCodeForceQuit)
case syscall.SIGTERM:
log.Println("[INFO] SIGTERM: Shutting down apps then terminating")
Log().Info("shutting down apps then terminating", zap.String("signal", "SIGTERM"))
gracefulStop("SIGTERM")
case syscall.SIGUSR1:
log.Println("[INFO] SIGUSR1: Not implemented")
Log().Info("not implemented", zap.String("signal", "SIGUSR1"))
case syscall.SIGUSR2:
log.Println("[INFO] SIGUSR2: Not implemented")
Log().Info("not implemented", zap.String("signal", "SIGUSR2"))
case syscall.SIGHUP:
// ignore; this signal is sometimes sent outside of the user's control
log.Println("[INFO] SIGHUP: Not implemented")
Log().Info("not implemented", zap.String("signal", "SIGHUP"))
}
}
}()
+2 -23
View File
@@ -22,10 +22,6 @@ import (
"github.com/mholt/certmagic"
)
func init() {
RegisterModule(fileStorage{})
}
// StorageConverter is a type that can convert itself
// to a valid, usable certmagic.Storage value. (The
// value might be short-lived.) This interface allows
@@ -35,23 +31,6 @@ type StorageConverter interface {
CertMagicStorage() (certmagic.Storage, error)
}
// fileStorage is a certmagic.Storage wrapper for certmagic.FileStorage.
type fileStorage struct {
Root string `json:"root"`
}
// CaddyModule returns the Caddy module information.
func (fileStorage) CaddyModule() ModuleInfo {
return ModuleInfo{
Name: "caddy.storage.file_system",
New: func() Module { return new(fileStorage) },
}
}
func (s fileStorage) CertMagicStorage() (certmagic.Storage, error) {
return &certmagic.FileStorage{Path: s.Root}, nil
}
// homeDir returns the best guess of the current user's home
// directory from environment variables. If unknown, "." (the
// current directory) is returned instead.
@@ -81,5 +60,5 @@ func dataDir() string {
return filepath.Join(baseDir, "caddy")
}
// Interface guard
var _ StorageConverter = fileStorage{}
// TODO: Consider using Go 1.13's os.UserConfigDir() (https://golang.org/pkg/os/#UserConfigDir)
// if we are going to store the last-loaded config anywhere
+157 -47
View File
@@ -21,23 +21,136 @@ import (
)
// UsagePool is a thread-safe map that pools values
// based on usage; a LoadOrStore operation increments
// the usage, and a Delete decrements from the usage.
// If the usage count reaches 0, the value will be
// removed from the map. There is no way to overwrite
// existing keys in the pool without first deleting
// it as many times as it was stored. Deleting too
// many times will panic.
// based on usage (reference counting). Values are
// only inserted if they do not already exist. There
// are two ways to add values to the pool:
//
// 1) LoadOrStore will increment usage and store the
// value immediately if it does not already exist
// 2) LoadOrNew will increment usage and construct the
// value immediately if it does not already exist,
// then store that value in the pool. When the
// constructed value is finally deleted from the
// pool (after its usage reaches 0), it will be
// cleaned up by calling its Destruct method.
//
// The use of LoadOrNew allows values to be created
// and reused and finally cleaned up only once, even
// though they may have many references throughout
// their lifespan. This is helpful, for example, when
// sharing thread-safe io.Writers that you only want
// to open and close once.
//
// There is no way to overwrite existing keys in the
// pool without first deleting it as many times as it
// was stored. Deleting too many times will panic.
//
// The implementation does not use a sync.Pool because
// UsagePool needs additional atomicity to run the
// constructor functions when creating a new value when
// LoadOrNew is used. (We could probably use sync.Pool
// but we'd still have to layer our own additional locks
// on top.)
//
// An empty UsagePool is NOT safe to use; always call
// NewUsagePool() to make a new value.
// NewUsagePool() to make a new one.
type UsagePool struct {
pool *sync.Map
sync.RWMutex
pool map[interface{}]*usagePoolVal
}
// NewUsagePool returns a new usage pool.
// NewUsagePool returns a new usage pool that is ready to use.
func NewUsagePool() *UsagePool {
return &UsagePool{pool: new(sync.Map)}
return &UsagePool{
pool: make(map[interface{}]*usagePoolVal),
}
}
// LoadOrNew loads the value associated with key from the pool if it
// already exists. If the key doesn't exist, it will call construct
// to create a new value and then stores that in the pool. An error
// is only returned if the constructor returns an error. The loaded
// or constructed value is returned. The loaded return value is true
// if the value already existed and was loaded, or false if it was
// newly constructed.
func (up *UsagePool) LoadOrNew(key interface{}, construct Constructor) (value interface{}, loaded bool, err error) {
var upv *usagePoolVal
up.Lock()
upv, loaded = up.pool[key]
if loaded {
atomic.AddInt32(&upv.refs, 1)
up.Unlock()
upv.RLock()
value = upv.value
err = upv.err
upv.RUnlock()
} else {
upv = &usagePoolVal{refs: 1}
upv.Lock()
up.pool[key] = upv
up.Unlock()
value, err = construct()
if err == nil {
upv.value = value
} else {
// TODO: remove error'ed entries from map
upv.err = err
}
upv.Unlock()
}
return
}
// LoadOrStore loads the value associated with key from the pool if it
// already exists, or stores it if it does not exist. It returns the
// value that was either loaded or stored, and true if the value already
// existed and was
func (up *UsagePool) LoadOrStore(key, val interface{}) (value interface{}, loaded bool) {
var upv *usagePoolVal
up.Lock()
upv, loaded = up.pool[key]
if loaded {
atomic.AddInt32(&upv.refs, 1)
up.Unlock()
upv.Lock()
if upv.err == nil {
value = upv.value
} else {
upv.value = val
upv.err = nil
}
upv.Unlock()
} else {
upv = &usagePoolVal{refs: 1, value: val}
up.pool[key] = upv
up.Unlock()
value = val
}
return
}
// Range iterates the pool similarly to how sync.Map.Range() does:
// it calls f for every key in the pool, and if f returns false,
// iteration is stopped. Ranging does not affect usage counts.
//
// This method is somewhat naive and acquires a read lock on the
// entire pool during iteration, so do your best to make f() really
// fast, m'kay?
func (up *UsagePool) Range(f func(key, value interface{}) bool) {
up.RLock()
defer up.RUnlock()
for key, upv := range up.pool {
upv.RLock()
if upv.err != nil {
upv.RUnlock()
continue
}
val := upv.value
upv.RUnlock()
if !f(key, val) {
break
}
}
}
// Delete decrements the usage count for key and removes the
@@ -45,50 +158,47 @@ func NewUsagePool() *UsagePool {
// true if the usage count reached 0 and the value was deleted.
// It panics if the usage count drops below 0; always call
// Delete precisely as many times as LoadOrStore.
func (up *UsagePool) Delete(key interface{}) (deleted bool) {
usageVal, ok := up.pool.Load(key)
func (up *UsagePool) Delete(key interface{}) (deleted bool, err error) {
up.Lock()
upv, ok := up.pool[key]
if !ok {
return false
up.Unlock()
return false, nil
}
upv := usageVal.(*usagePoolVal)
newUsage := atomic.AddInt32(&upv.usage, -1)
if newUsage == 0 {
up.pool.Delete(key)
return true
} else if newUsage < 0 {
panic(fmt.Sprintf("deleted more than stored: %#v (usage: %d)",
upv.value, upv.usage))
}
return false
}
// LoadOrStore puts val in the pool and returns false if key does
// not already exist; otherwise if the key exists, it loads the
// existing value, increments the usage for that value, and returns
// the value along with true.
func (up *UsagePool) LoadOrStore(key, val interface{}) (actual interface{}, loaded bool) {
usageVal := &usagePoolVal{
usage: 1,
value: val,
}
actual, loaded = up.pool.LoadOrStore(key, usageVal)
if loaded {
upv := actual.(*usagePoolVal)
actual = upv.value
atomic.AddInt32(&upv.usage, 1)
refs := atomic.AddInt32(&upv.refs, -1)
if refs == 0 {
delete(up.pool, key)
up.Unlock()
upv.RLock()
val := upv.value
upv.RUnlock()
if destructor, ok := val.(Destructor); ok {
err = destructor.Destruct()
}
deleted = true
} else {
up.Unlock()
if refs < 0 {
panic(fmt.Sprintf("deleted more than stored: %#v (usage: %d)",
upv.value, upv.refs))
}
}
return
}
// Range iterates the pool the same way sync.Map.Range does.
// This does not affect usage counts.
func (up *UsagePool) Range(f func(key, value interface{}) bool) {
up.pool.Range(func(key, value interface{}) bool {
return f(key, value.(*usagePoolVal).value)
})
// Constructor is a function that returns a new value
// that can destruct itself when it is no longer needed.
type Constructor func() (Destructor, error)
// Destructor is a value that can clean itself up when
// it is deallocated.
type Destructor interface {
Destruct() error
}
type usagePoolVal struct {
usage int32 // accessed atomically; must be 64-bit aligned for 32-bit systems
refs int32 // accessed atomically; must be 64-bit aligned for 32-bit systems
value interface{}
err error
sync.RWMutex
}