mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-25 16:22:36 -04:00
Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba0000678d | |||
| c4c45f8e01 | |||
| 54e458b756 | |||
| d803561212 | |||
| 813fff0584 | |||
| d2e7baed8d | |||
| d6dad04e96 | |||
| 442fd748f6 | |||
| b00dfd3965 | |||
| 6c533558a3 | |||
| 2fbe2ff40b | |||
| faf67b1067 | |||
| 208f2ff93c | |||
| 19e834cf36 | |||
| bce2edd22d | |||
| a458544d9f | |||
| 2f91b44587 | |||
| e3726588b4 | |||
| abf5ab340e | |||
| acf7dea68f | |||
| bc738991b6 | |||
| fcd8869f51 | |||
| 1e31be8de0 | |||
| 4aa3af4b78 | |||
| 8715a28320 | |||
| 715e6ddf51 | |||
| 9c0bf311f9 | |||
| 5300949e0d | |||
| 411152016e | |||
| 5c7640a8d9 | |||
| f8366c2f09 | |||
| fe36d26b63 | |||
| b38365ff3b | |||
| 26cc883708 | |||
| 93943a6ac2 | |||
| 85ce15a5ad | |||
| dedcfd4e3d | |||
| 20fe9cf024 | |||
| bcbe1c220d | |||
| a53b27c62e | |||
| 03306e646e | |||
| 53dd600b4d | |||
| ce1205239a | |||
| bc3e44c1a6 | |||
| 8c55167f71 | |||
| be7abda7d4 | |||
| 6fd28b81dc | |||
| 65c060f56e | |||
| 44cb804b9e | |||
| c11e3bffd6 | |||
| f29a9eee0d | |||
| 370b78c5c7 | |||
| 1ecb216001 | |||
| 94f98c0733 | |||
| 2c3657bb8a | |||
| 5b36424cf0 | |||
| 0006df6026 | |||
| c95db3551d | |||
| 8eb2c37251 | |||
| 1e66226217 | |||
| 7b4aa108c7 | |||
| 8b11ed347b | |||
| b249b45d10 | |||
| c12bf4054c | |||
| 735d6ce405 | |||
| 7b33c8db31 | |||
| 11696793bd | |||
| 3e8bff594a | |||
| 2f684e42d5 | |||
| ba29f9d41d | |||
| 40e05e5a01 | |||
| 39d61cad2d | |||
| bc9f944837 | |||
| 4c289fc6ad | |||
| 19f36667f7 | |||
| 484cee1ac1 | |||
| d030bfdae0 | |||
| db4c73dd58 | |||
| f15f0d5839 | |||
| e73b117332 | |||
| 2fd22139c6 | |||
| 5c9ebe3af1 | |||
| 2ab2d5bf9e | |||
| c09e86fddc | |||
| 46aaf02371 |
@@ -6,6 +6,10 @@ Caddyfile
|
||||
*.prof
|
||||
*.test
|
||||
|
||||
# build artifacts
|
||||
cmd/caddy/caddy
|
||||
cmd/caddy/caddy.exe
|
||||
|
||||
# mac specific
|
||||
.DS_Store
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -1,11 +1,14 @@
|
||||
Caddy 2 Development Branch
|
||||
===========================
|
||||
|
||||
[](https://dev.azure.com/mholt-dev/Caddy/_build/latest?definitionId=5&branchName=v2)
|
||||
[](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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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)))
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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{} {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, "")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user