Compare commits

...

46 Commits

Author SHA1 Message Date
Francis Lavoie 5968ebd0f4 reverseproxy: Add support for specifying IDs in Caddyfile 2021-09-13 00:21:54 -04:00
Francis Lavoie a5f4fae145 reverseproxy: Add ID field for upstreams 2021-09-12 23:50:04 -04:00
Francis Lavoie a779e1b383 fastcgi: Fix Caddyfile parsing when handle_response is used (#4342) 2021-09-11 14:12:21 -06:00
Matthew Holt 46ab93be51 go.mod: Update CertMagic
Adds one more debug log
2021-09-03 11:42:13 -06:00
Mohammed Al Sahaf e0fc46a911 ci: revert workaround implemented in #4306 (#4328) 2021-09-03 10:05:04 -04:00
peymaneh 9f6393c64c cmd: export CaddyVersion(), Commands() (#4316)
* cmd: Export CaddyVersion()

* cmd: Add getter Commands()
2021-09-01 18:08:02 -06:00
Francis Lavoie 105dac8c2a ci: Only test cross-build on latest Go version (#4319)
This generated way too many test jobs, which weren't really that useful. Cross-build is just to keep us posted on which architectures are building okay, so it's not necessary to do it twice. Only plan9 is not working at this point (see https://github.com/caddyserver/caddy/issues/3615)
2021-08-31 13:44:07 -06:00
Steffen Brüheim 4ebf100f09 encode: ignore flushing until after first write (#4318)
* encode: ignore flushing until after first write (fix #4314)

The first write will determine if encoding has to be done and will add an Content-Encoding. Until then Flushing has to be delayed so the Content-Encoding header can be added before headers and status code is written. (A passthrough flush would write header and status code)

* Update modules/caddyhttp/encode/encode.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2021-08-31 13:36:36 -06:00
Matthew Holt f43fd6f388 go.mod: Upgrade CertMagic to v0.14.4
Adds more debug logging
2021-08-30 13:14:42 -06:00
Matthew Holt 84b906a248 go.mod: Upgrade some dependencies 2021-08-26 15:00:25 -06:00
Francis Lavoie 403732c433 httpcaddyfile: Reorder some directives (#4311)
We realized we made some mistakes with the directive ordering, so we're making some minor adjustments.

`abort` and `error` don't really make sense to be after other handler directives, because you would expect to be able to "fail-fast" and throw an error before falling through to some `file_server` or `respond` typically. So we're moving them up to just before `respond`, i.e. before the common handler directives. 

This is also more consistent with our existing examples in the docs, which actually didn't work due to the directive ordering. See https://caddyserver.com/docs/caddyfile/directives/error#examples

Also, `push` doesn't quite make sense to be after `handle`/`route`, since its job is to read from response headers to push additional resources if necessary, and `handle`/`route` may be terminal so push would not be reached if it was declared outside those. And also, it would make sense to be _before_ `templates` because a template _could_ add a `Link` header to the response dynamically.
2021-08-26 14:31:55 -06:00
Francis Lavoie f6d5ec2fd6 chore: Upgrade smallstep libs (#4307)
See https://github.com/smallstep/nosql/issues/12 for context.
2021-08-25 12:16:55 -06:00
Mohammed Al Sahaf 19a55d6aeb chore: promote creating 'caddy-build' to the release action (#4306)
The commit goreleaser/goreleaser@013bd69126 of GoReleaser is now checking the `go version` prior to executing any of the pre-hooks, which involves setting the current dir of the command to the `build.dir` of the build config. At the time of version check, the buil dir does not exist. It's created in the pre-hook. As a workaround, the build-dir is now created in the Github Action prior to executing goreleaser action.
2021-08-25 17:30:24 +00:00
Matthew Holt bfbc459c0a httpcaddyfile: Improve unrecognized directive errors 2021-08-25 10:30:39 -06:00
Francis Lavoie f70a7578fa reverseproxy: Remove redundant flushing (#4299)
From reading through the code, I think this code path is now obsoleted by the changes made in https://github.com/caddyserver/caddy/pull/4266.

Basically, `h.flushInterval()` will set the flush interval to `-1` if we're in a bi-directional stream, and the recent PR ensured that `h.copyResponse()` properly flushes headers immediately when the flush interval is non-zero. So now there should be no need to call Flush before calling `h.copyResponse()`.
2021-08-23 11:54:28 -06:00
Francis Lavoie 51f125bd44 caddyfile: Better error message for missing site block braces (#4301)
Some new users mistakenly try to define two sites without braces around each. Doing this can yield a confusing error message saying that their site address is an "unknown directive".

We can do better by keeping track of whether the current site block was parsed with or without a brace, then changing the error message later based on that.

For example, now this invalid config:

```
foo.example.com
respond "foo"

bar.example.com
respond "bar"
```

Will yield this error message:

```
$ caddy adapt
2021/08/22 19:21:31.028 INFO    using adjacent Caddyfile
adapt: Caddyfile:4: unrecognized directive: bar.example.com
Did you mean to define a second site? If so, you must use curly braces around each site to separate their configurations.
```
2021-08-23 11:53:27 -06:00
Francis Lavoie d74913f871 caddyfile: Error on invalid site addresses containing comma (#4302)
Some users forget to use a comma between their site addresses. This is invalid (commas aren't a valid character in domains) and later parts of the code like certificate automation will try to use this otherwise, which doesn't make sense. Best to error as early as possible.

Example thread on the forums where this happened: https://caddy.community/t/simplify-caddyfile/13281/9
2021-08-23 11:26:07 -06:00
Pascal Zarrad ce5a45db45 cmd: Fix paths when using an env file (#4296)
* core: Fix paths when using an env file

* refactor: move path logic to loadFromEnv
2021-08-20 15:51:31 -06:00
Adam Weinberger e0a6a1efff chore: Update quic-go for go 1.17 support (#4297)
* Update quic-go for go 1.17 support

* Complete quic-go update (go mod tidy)
2021-08-20 10:19:16 -06:00
Scott Mebberson c1cd192ee7 caddyhttp: Updated the documentation for MatchQuery (#4295) 2021-08-19 22:44:28 -06:00
Francis Lavoie a056fcd7ba chore: Upgrade smallstep libs (#4291)
See https://github.com/smallstep/nosql/issues/12 for context.
2021-08-19 16:08:19 -06:00
M. Ángel Jimeno 9e333c39da cmd: use net.ErrClosed for matching returned error (#4289)
Implements #3805
2021-08-18 12:58:19 -06:00
Matthew Holt 8a974a4f8f logging: Warn for deprecated single_field encoder 2021-08-17 10:51:26 -06:00
Francis Lavoie 6bc87ea2ff ci: Start testing on Go 1.17, drop 1.15 (#4283) 2021-08-16 21:56:20 -06:00
Rainer Borene 1b1e625c20 core: Unix ns and Unix ms time placeholders (#4280) 2021-08-16 15:06:44 -06:00
Steven Angles a10910f398 admin: Sync server variables (fix #4260) (#4274)
* Synchronize server assignment/references to avoid data race

* only hold lock during var reassignment
2021-08-16 15:04:47 -06:00
Francis Lavoie ab32440b21 httpcaddyfile: Add shortcut for proxy hostport placeholder (#4263)
* httpcaddyfile: Add shortcut for proxy hostport placeholder

I've noticed that it's a pretty common pattern to write a proxy like this, when needing to proxy over HTTPS:

```
reverse_proxy https://example.com {
	header_up Host {http.reverse_proxy.upstream.hostport}
}
```

I find it pretty hard to remember the exact placeholder to use for this, and I continually need to refer to the docs when I need it. I think a simple fix for this is to add another Caddyfile placeholder for this one to shorten it:

```
reverse_proxy https://example.com {
	header_up Host {proxy_hostport}
}
```

* Switch the shortcut name
2021-08-12 12:08:37 -06:00
Francis Lavoie e6c29ce081 reverseproxy: Incorporate latest proxy changes from stdlib (#4266)
I went through the commits that touched stdlib's `reverseproxy.go` file, and copied over all the changes that are to code that was copied into Caddy.

The commits I pulled changes from:

- https://github.com/golang/go/commit/2cc347382f4df3fb40d8d81ec9331f0748b1c394
- https://github.com/golang/go/commit/a5cea062b305c8502bdc959c0eec279dbcd4391f
- https://github.com/golang/go/commit/ecdbffd4ec68b509998792f120868fec319de59b
- https://github.com/golang/go/commit/21898524f66c075d7cfb64a38f17684140e57675
-https://github.com/golang/go/commit/ca3c0df1f8e07337ba4048b191bf905118ebe251
- https://github.com/golang/go/commit/9c017ff30dd21bbdcdb11f39458d3944db530d7e

This may also fix https://github.com/caddyserver/caddy/issues/4247 because of the change to `copyResponse` to set `mlw.flushPending = true` right away.
2021-08-12 10:48:24 -06:00
Oleg 68c5c71659 cmd: New add-package and remove-package commands (#4226)
* adding package command

* add-package command name

* refactoring duplicate code

* fixed by review

* fixed by review

* remove-package command

* commands in different files, common utils

* fix add, remove, upgrade packages in 1 file

* copyright and downloadPath moved

* refactor

* downloadPath do no export

* adding/removing multiple packages

* addPackages/removePackages, comments, command-desc

* add-package, process case len(args) == 0

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2021-08-11 17:31:41 -06:00
Frederik Ring 569ecdbd02 httpcaddyfile: Ensure hosts to skip for logs can always be collected (#4258)
* httpcaddyfile: ensure hosts to skip can always be collected

Previously, some hosts that should be skipped in logging would
be missed as the current logic would only collect them after
encountering the first server that would log. This change makes sure
the ServerLogConfig is initialized before iterating over the server
blocks.

* httpcaddyfile: add test case for skip hosts behavior
2021-08-02 14:15:27 -06:00
王清雨 c131339c5c admin: Implement load_interval to pull config on a timer (#4246)
* feat: implement a simple timer to pull config

mostly referenced to the issue

re #4106

* Update admin.go

use `caddy.Duration`

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

* Update caddy.go

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

* Update admin.go

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

* fix: sync load config when no pull interval provided

try not to make break change

* fix: change PullInterval to LoadInterval

* fix: change pull_interval to load_interval

* Update caddy.go

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

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2021-07-28 15:39:08 -06:00
Ggicci b6f51254ea caddyfile: keep error chain info in Dispenser.Errf (#4233)
* caddyfile: Errf enable error chain unwrapping

* refactor: remove parseError
2021-07-19 08:35:14 -06:00
Francis Lavoie 124ba1ba71 logging: Prep for common_log removal (#4149)
See https://github.com/caddyserver/caddy/issues/4148#issuecomment-833207811
2021-07-14 11:07:38 -06:00
Francis Lavoie 1c6c7714a3 caddyhttp: Fix edgecase with auto HTTP->HTTPS logic (#4243) 2021-07-14 10:49:34 -06:00
Leo Di Donato 46d99aba85 logging: Add missing interface guards for replace filter (#4244)
Signed-off-by: Leonardo Di Donato <leodidonato@gmail.com>
2021-07-12 11:13:01 -04:00
diamondburned 9e16e80f3c fileserver: Fix browse name_dir_first sorting (#4218)
This commit fixes the `sortByNameDirFirst` variable inside fileserver to
match what browse's default template has.

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2021-07-07 02:54:54 -04:00
elnoro d882211080 reverseproxy: Keep path to unix socket as dial address (#4232) 2021-07-06 23:43:45 -04:00
hmol233 42e140b1b2 caddyhttp: Fix incorrect determination of gRPC protocol (#4236) 2021-07-06 12:09:44 -04:00
mritd 4245ceb67d fileserver: Add disable_canonical_uris Caddyfile subdirective (#4222)
* feat(fileserver): add 'canonical_uris' parameter to caddyfile

add 'canonical_uris' parameter to caddyfile

reference #2741

Signed-off-by: mritd <mritd@linux.com>

* feat(file_server): rename subdirective canonical_uris to disable_canonical_uris

rename subdirective canonical_uris to disable_canonical_uris

Signed-off-by: mritd <mritd@linux.com>

* test(caddyfile_adapt): add disable_canonical_uris subdirective test file

add disable_canonical_uris subdirective test file

Signed-off-by: mritd <mritd@linux.com>
2021-07-01 17:22:16 -06:00
Matthew Holt 0bdb8aa82d acmeserver: Don't set host for directory links by default
This makes the server more easily proxied.
2021-07-01 17:20:51 -06:00
Matthew Holt 191dc86f9e fileserver: Clarify docs about canonicalization
Related to https://github.com/caddyserver/caddy/issues/4205.
2021-06-25 11:33:18 -06:00
Matthew Holt 81e5318021 caddytls: Remove "IssuerRaw" field
Has been deprecated and printing warnings for about 8 months now.
Replaced by "IssuersRaw" field in v2.3.0.
2021-06-25 11:29:56 -06:00
Matthew Holt b3d35a4995 httpcaddyfile: Don't put localhost in public APs (fix #4220)
If an email is specified in global options, a site called 'localhost' shouldn't be bunched together with public DNS names in the automation policies, which get the default, public-CA issuers. Fix old test that did this.

I also noticed that these two:

    localhost {
    }
    example.com {
    }

and

    localhost, example.com {
    }

produce slightly different TLS automation policies. The former is what the new test case covers, and we have logic that removes the empty automation policy for localhost so that auto-HTTPS can implicitly create one. (We prefer that whenever possible.) But the latter case produces two automation policies, with the second one being for localhost, with an explicit internal issuer. It's not wrong, just more explicit than it needs to be.

I'd really like to completely rewrite the code from scratch that generates automation policies, hopefully there is a simpler, more correct algorithm.
2021-06-25 11:28:32 -06:00
Matthew Holt 2de7e14e1c acmeserver: Trim slashes from path prefix
See https://caddy.community/t/mtls-tls-internal-error/12807
2021-06-21 11:56:41 -06:00
Matthew Holt 885a9aaf48 go.mod: Update dependencies (close #4216) 2021-06-18 12:02:47 -06:00
Klaus Post 69c914483d encode: Tweak compression settings (#4215)
* Tweak compression settings

zstd: Limit window sizes to 128K to keep memory in control both server and client size.
zstd: Write 0 length frames. This may be needed for compatibility.
zstd: Create fewer encoders. Small memory improvement.
gzip: Allow -2 (Huffman only) and -3 (stateless) compression modes.

* Update modules/caddyhttp/encode/zstd/zstd.go

Update docs.

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

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2021-06-18 11:49:49 -06:00
48 changed files with 1910 additions and 563 deletions
+2 -1
View File
@@ -19,7 +19,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
go: [ '1.15', '1.16' ]
go: [ '1.16', '1.17' ]
# Set some variables per OS, usable via ${{ matrix.VAR }}
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
@@ -156,6 +156,7 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v2
- uses: goreleaser/goreleaser-action@v2
with:
version: latest
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
fail-fast: false
matrix:
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
go: [ '1.15', '1.16' ]
go: [ '1.17' ]
runs-on: ubuntu-latest
continue-on-error: true
steps:
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
os: [ ubuntu-latest ]
go: [ '1.16' ]
go: [ '1.17' ]
runs-on: ${{ matrix.os }}
steps:
+1 -1
View File
@@ -75,7 +75,7 @@ For other install options, see https://caddyserver.com/docs/install.
Requirements:
- [Go 1.15 or newer](https://golang.org/dl/)
- [Go 1.16 or newer](https://golang.org/dl/)
### For development
+19 -2
View File
@@ -109,6 +109,12 @@ type ConfigSettings struct {
//
// EXPERIMENTAL: Subject to change.
LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"`
// The interval to pull config. With a non-zero value, will pull config
// from config loader (eg. a http loader) with given interval.
//
// EXPERIMENTAL: Subject to change.
LoadInterval Duration `json:"load_interval,omitempty"`
}
// IdentityConfig configures management of this server's identity. An identity
@@ -329,6 +335,7 @@ func replaceLocalAdminServer(cfg *Config) error {
return err
}
serverMu.Lock()
localAdminServer = &http.Server{
Addr: addr.String(), // for logging purposes only
Handler: handler,
@@ -337,10 +344,14 @@ func replaceLocalAdminServer(cfg *Config) error {
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1024 * 64,
}
serverMu.Unlock()
adminLogger := Log().Named("admin")
go func() {
if err := localAdminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
serverMu.Lock()
server := localAdminServer
serverMu.Unlock()
if err := server.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
}
}()
@@ -468,6 +479,7 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
return err
}
serverMu.Lock()
// create secure HTTP server
remoteAdminServer = &http.Server{
Addr: addr.String(), // for logging purposes only
@@ -479,6 +491,7 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
MaxHeaderBytes: 1024 * 64,
ErrorLog: serverLogger,
}
serverMu.Unlock()
// start listener
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
@@ -488,7 +501,10 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
ln = tls.NewListener(ln, tlsConfig)
go func() {
if err := remoteAdminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
serverMu.Lock()
server := remoteAdminServer
serverMu.Unlock()
if err := server.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
remoteLogger.Error("admin remote server shutdown for unknown reason", zap.Error(err))
}
}()
@@ -1223,6 +1239,7 @@ var bufPool = sync.Pool{
// keep a reference to admin endpoint singletons while they're active
var (
serverMu sync.Mutex
localAdminServer, remoteAdminServer *http.Server
identityCertCache *certmagic.Cache
)
+36 -18
View File
@@ -17,9 +17,28 @@ package caddy
import (
"encoding/json"
"reflect"
"sync"
"testing"
)
var testCfg = []byte(`{
"apps": {
"http": {
"servers": {
"myserver": {
"listen": ["tcp/localhost:8080-8084"],
"read_timeout": "30s"
},
"yourserver": {
"listen": ["127.0.0.1:5000"],
"read_header_timeout": "15s"
}
}
}
}
}
`)
func TestUnsyncedConfigAccess(t *testing.T) {
// each test is performed in sequence, so
// each change builds on the previous ones;
@@ -108,25 +127,24 @@ func TestUnsyncedConfigAccess(t *testing.T) {
}
}
// TestLoadConcurrent exercises Load under concurrent conditions
// and is most useful under test with `-race` enabled.
func TestLoadConcurrent(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
_ = Load(testCfg, true)
wg.Done()
}()
}
wg.Wait()
}
func BenchmarkLoad(b *testing.B) {
for i := 0; i < b.N; i++ {
cfg := []byte(`{
"apps": {
"http": {
"servers": {
"myserver": {
"listen": ["tcp/localhost:8080-8084"],
"read_timeout": "30s"
},
"yourserver": {
"listen": ["127.0.0.1:5000"],
"read_header_timeout": "15s"
}
}
}
}
}
`)
Load(cfg, true)
Load(testCfg, true)
}
}
+32 -12
View File
@@ -268,8 +268,9 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
newCfg != nil &&
newCfg.Admin != nil &&
newCfg.Admin.Config != nil &&
newCfg.Admin.Config.LoadRaw != nil {
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs")
newCfg.Admin.Config.LoadRaw != nil &&
newCfg.Admin.Config.LoadInterval <= 0 {
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_interval")
}
// run the new config and start all its apps
@@ -480,23 +481,42 @@ func finishSettingUp(ctx Context, cfg *Config) error {
if err != nil {
return fmt.Errorf("loading config loader module: %s", err)
}
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
if err != nil {
return fmt.Errorf("loading dynamic config from %T: %v", val, err)
}
// do this in a goroutine so current config can finish being loaded; otherwise deadlock
go func() {
Log().Info("applying dynamically-loaded config", zap.String("loader_module", val.(Module).CaddyModule().ID.Name()))
runLoadedConfig := func(config []byte) {
Log().Info("applying dynamically-loaded config", zap.String("loader_module", val.(Module).CaddyModule().ID.Name()), zap.Int("pull_interval", int(cfg.Admin.Config.LoadInterval)))
currentCfgMu.Lock()
err := unsyncedDecodeAndRun(loadedConfig, false)
err := unsyncedDecodeAndRun(config, false)
currentCfgMu.Unlock()
if err == nil {
Log().Info("dynamically-loaded config applied successfully")
} else {
Log().Error("running dynamically-loaded config failed", zap.Error(err))
}
}()
}
if cfg.Admin.Config.LoadInterval > 0 {
go func() {
select {
// if LoadInterval is positive, will wait for the interval and then run with new config
case <-time.After(time.Duration(cfg.Admin.Config.LoadInterval)):
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
if err != nil {
Log().Error("loading dynamic config failed", zap.Error(err))
return
}
runLoadedConfig(loadedConfig)
case <-ctx.Done():
return
}
}()
} else {
// if no LoadInterval is provided, will load config synchronously
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
if err != nil {
return fmt.Errorf("loading dynamic config from %T: %v", val, err)
}
// do this in a goroutine so current config can finish being loaded; otherwise deadlock
go runLoadedConfig(loadedConfig)
}
}
return nil
+3 -3
View File
@@ -345,13 +345,13 @@ func (d *Dispenser) EOFErr() error {
// Err generates a custom parse-time error with a message of msg.
func (d *Dispenser) Err(msg string) error {
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
return errors.New(msg)
return d.Errf(msg)
}
// Errf is like Err, but for formatted error messages
func (d *Dispenser) Errf(format string, args ...interface{}) error {
return d.Err(fmt.Sprintf(format, args...))
err := fmt.Errorf(format, args...)
return fmt.Errorf("%s:%d - Error during parsing: %w", d.File(), d.Line(), err)
}
// Delete deletes the current token and returns the updated slice
+7
View File
@@ -15,6 +15,7 @@
package caddyfile
import (
"errors"
"reflect"
"strings"
"testing"
@@ -303,4 +304,10 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
if !strings.Contains(err.Error(), "foobar") {
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
}
var ErrBarIsFull = errors.New("bar is full")
bookingError := d.Errf("unable to reserve: %w", ErrBarIsFull)
if !errors.Is(bookingError, ErrBarIsFull) {
t.Errorf("Errf(): should be able to unwrap the error chain")
}
}
+16 -2
View File
@@ -211,6 +211,12 @@ func (p *parser) addresses() error {
if expectingAnother {
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
}
// Mark this server block as being defined with braces.
// This is used to provide a better error message when
// the user may have tried to define two server blocks
// without having used braces, which are required in
// that case.
p.block.HasBraces = true
break
}
@@ -229,6 +235,13 @@ func (p *parser) addresses() error {
expectingAnother = false // but we may still see another one on this line
}
// If there's a comma here, it's probably because they didn't use a space
// between their two domains, e.g. "foo.com,bar.com", which would not be
// parsed as two separate site addresses.
if strings.Contains(tkn, ",") {
return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", tkn)
}
p.block.Keys = append(p.block.Keys, tkn)
}
@@ -564,8 +577,9 @@ func (p *parser) snippetTokens() ([]Token, error) {
// head of the server block with tokens, which are
// grouped by segments.
type ServerBlock struct {
Keys []string
Segments []Segment
HasBraces bool
Keys []string
Segments []Segment
}
// DispenseDirective returns a dispenser that contains
+5 -5
View File
@@ -44,9 +44,9 @@ var directiveOrder = []string{
"request_body",
"redir",
"rewrite",
// URI manipulation
"rewrite",
"uri",
"try_files",
@@ -54,23 +54,23 @@ var directiveOrder = []string{
"basicauth",
"request_header",
"encode",
"push",
"templates",
// special routing & dispatching directives
"handle",
"handle_path",
"route",
"push",
// handlers that typically respond to requests
"abort",
"error",
"respond",
"metrics",
"reverse_proxy",
"php_fastcgi",
"file_server",
"acme_server",
"abort",
"error",
}
// directiveIsOrdered returns true if dir is
@@ -329,7 +329,7 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
dir := seg.Directive()
dirFunc, ok := registeredDirectives[dir]
if !ok {
return nil, h.Errf("unrecognized directive: %s", dir)
return nil, h.Errf("unrecognized directive: %s - are you sure your Caddyfile structure (nesting and braces) is correct?", dir)
}
subHelper := h
+16 -4
View File
@@ -113,6 +113,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
"{tls_client_serial}", "{http.request.tls.client.serial}",
"{tls_client_subject}", "{http.request.tls.client.subject}",
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
)
// these are placeholders that allow a user-defined final
@@ -169,7 +170,11 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
dirFunc, ok := registeredDirectives[dir]
if !ok {
tkn := segment[0]
return nil, warnings, fmt.Errorf("%s:%d: unrecognized directive: %s", tkn.File, tkn.Line, dir)
message := "%s:%d: unrecognized directive: %s"
if !sb.block.HasBraces {
message += "\nDid you mean to define a second site? If so, you must use curly braces around each site to separate their configurations."
}
return nil, warnings, fmt.Errorf(message, tkn.File, tkn.Line, dir)
}
h := Helper{
@@ -522,6 +527,16 @@ func (st *ServerType) serversFromPairings(
}
}
// if needed, the ServerLogConfig is initialized beforehand so
// that all server blocks can populate it with data, even when not
// coming with a log directive
for _, sblock := range p.serverBlocks {
if len(sblock.pile["custom_log"]) != 0 {
srv.Logs = new(caddyhttp.ServerLogConfig)
break
}
}
// create a subroute for each site in the server block
for _, sblock := range p.serverBlocks {
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
@@ -636,9 +651,6 @@ func (st *ServerType) serversFromPairings(
sblockLogHosts := sblock.hostsFromKeys(true)
for _, cval := range sblock.pile["custom_log"] {
ncl := cval.Value.(namedCustomLog)
if srv.Logs == nil {
srv.Logs = new(caddyhttp.ServerLogConfig)
}
if sblock.hasHostCatchAllKey() {
// all requests for hosts not able to be listed should use
// this log because it's a catch-all-hosts server block
+33 -4
View File
@@ -211,7 +211,7 @@ func (st ServerType) buildTLSApp(
// it that we would need to check here) since the hostname is known at handshake;
// and it is unexpected to switch to internal issuer when the user wants to get
// regular certificates on-demand for a class of certs like *.*.tld.
if !certmagic.SubjectIsIP(s) && !certmagic.SubjectIsInternal(s) && (strings.Count(s, "*.") < 2 || ap.OnDemand) {
if subjectQualifiesForPublicCert(ap, s) {
external = append(external, s)
} else {
internal = append(internal, s)
@@ -324,8 +324,12 @@ func (st ServerType) buildTLSApp(
globalPreferredChains := options["preferred_chains"]
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil
if hasGlobalACMEDefaults {
for _, ap := range tlsApp.Automation.Policies {
if len(ap.Issuers) == 0 {
// for _, ap := range tlsApp.Automation.Policies {
for i := 0; i < len(tlsApp.Automation.Policies); i++ {
ap := tlsApp.Automation.Policies[i]
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
// for public names, create default issuers which will later be filled in with configured global defaults
// (internal names will implicitly use the internal issuer at auto-https time)
ap.Issuers = caddytls.DefaultIssuers()
// if a specific endpoint is configured, can't use multiple default issuers
@@ -494,16 +498,23 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
})
emptyAPCount := 0
origLenAPs := len(aps)
// compute the number of empty policies (disregarding subjects) - see #4128
emptyAP := new(caddytls.AutomationPolicy)
for i := 0; i < len(aps); i++ {
emptyAP.Subjects = aps[i].Subjects
if reflect.DeepEqual(aps[i], emptyAP) {
emptyAPCount++
if !automationPolicyHasAllPublicNames(aps[i]) {
// if this automation policy has internal names, we might as well remove it
// so auto-https can implicitly use the internal issuer
aps = append(aps[:i], aps[i+1:]...)
i--
}
}
}
// If all policies are empty, we can return nil, as there is no need to set any policy
if emptyAPCount == len(aps) {
if emptyAPCount == origLenAPs {
return nil
}
@@ -601,3 +612,21 @@ func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int {
}
return -1
}
// subjectQualifiesForPublicCert is like certmagic.SubjectQualifiesForPublicCert() except
// that this allows domains with multiple wildcard levels like '*.*.example.com' to qualify
// if the automation policy has OnDemand enabled (i.e. this function is more lenient).
func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) bool {
return !certmagic.SubjectIsIP(subj) &&
!certmagic.SubjectIsInternal(subj) &&
(strings.Count(subj, "*.") < 2 || ap.OnDemand)
}
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
for _, subj := range ap.Subjects {
if !subjectQualifiesForPublicCert(ap, subj) {
return false
}
}
return true
}
+20
View File
@@ -103,3 +103,23 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) {
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Baz")
}
func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSite(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
local_certs
}
http://:9080 {
respond "Foo"
}
bar.localhost {
respond "Bar"
}
`, "caddyfile")
tester.AssertRedirect("http://bar.localhost:9080/", "https://bar.localhost/", http.StatusPermanentRedirect)
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Foo")
}
@@ -0,0 +1,138 @@
example.com {
root * /srv
# Trigger errors for certain paths
error /private* "Unauthorized" 403
error /hidden* "Not found" 404
# Handle the error by serving an HTML page
handle_errors {
rewrite * /{http.error.status_code}.html
file_server
}
file_server
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"handle": [
{
"error": "Unauthorized",
"handler": "error",
"status_code": 403
}
],
"match": [
{
"path": [
"/private*"
]
}
]
},
{
"handle": [
{
"error": "Not found",
"handler": "error",
"status_code": 404
}
],
"match": [
{
"path": [
"/hidden*"
]
}
]
},
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
]
}
]
}
]
}
],
"terminal": true
}
],
"errors": {
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"group": "group0",
"handle": [
{
"handler": "rewrite",
"uri": "/{http.error.status_code}.html"
}
]
},
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
}
@@ -0,0 +1,32 @@
:80
file_server {
disable_canonical_uris
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"canonical_uris": false,
"handler": "file_server",
"hide": [
"./Caddyfile"
]
}
]
}
]
}
}
}
}
}
@@ -2,7 +2,7 @@
preferred_chains smallest
}
localhost
example.com
----------
{
"apps": {
@@ -17,7 +17,7 @@ localhost
"match": [
{
"host": [
"localhost"
"example.com"
]
}
],
@@ -32,7 +32,7 @@ localhost
"policies": [
{
"subjects": [
"localhost"
"example.com"
],
"issuers": [
{
@@ -0,0 +1,75 @@
one.example.com {
log
}
two.example.com {
}
three.example.com {
}
example.com {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"three.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"one.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"two.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
],
"logs": {
"skip_hosts": [
"three.example.com",
"two.example.com",
"example.com"
]
}
}
}
}
}
}
@@ -0,0 +1,145 @@
:8881 {
php_fastcgi app:9000 {
env FOO bar
@error status 4xx
handle_response @error {
root * /errors
rewrite * /{http.reverse_proxy.status_code}.html
file_server
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"routes": [
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
],
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.uri.path}/"
]
},
"status_code": 308
}
]
},
{
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
],
"split_path": [
".php"
]
}
}
],
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
]
},
{
"match": [
{
"path": [
"*.php"
]
}
],
"handle": [
{
"handle_response": [
{
"match": {
"status_code": [
4
]
},
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/errors"
}
]
},
{
"group": "group0",
"handle": [
{
"handler": "rewrite",
"uri": "/{http.reverse_proxy.status_code}.html"
}
]
},
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
]
}
]
}
]
}
],
"handler": "reverse_proxy",
"transport": {
"env": {
"FOO": "bar"
},
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"upstreams": [
{
"dial": "app:9000"
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,46 @@
:8884
reverse_proxy one|http://localhost two|http://localhost {
to three|srv+http://localhost four|srv+http://localhost
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "localhost:80",
"id": "one"
},
{
"dial": "localhost:80",
"id": "two"
},
{
"id": "three",
"lookup_srv": "localhost"
},
{
"id": "four",
"lookup_srv": "localhost"
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,66 @@
{
email foo@bar
}
localhost {
}
example.com {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"example.com"
],
"issuers": [
{
"email": "foo@bar",
"module": "acme"
},
{
"email": "foo@bar",
"module": "zerossl"
}
]
}
]
}
}
}
}
+56 -2
View File
@@ -371,7 +371,7 @@ func TestReverseProxyHealthCheck(t *testing.T) {
reverse_proxy {
to localhost:2020
health_path /health
health_uri /health
health_port 2021
health_interval 2s
health_timeout 5s
@@ -426,7 +426,7 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
reverse_proxy {
to unix/%s
health_path /health
health_uri /health
health_port 2021
health_interval 2s
health_timeout 5s
@@ -436,3 +436,57 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
}
func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
if runtime.GOOS == "windows" {
t.SkipNow()
}
tester := caddytest.NewTester(t)
f, err := ioutil.TempFile("", "*.sock")
if err != nil {
t.Errorf("failed to create TempFile: %s", err)
return
}
// a hack to get a file name within a valid path to use as socket
socketName := f.Name()
os.Remove(f.Name())
server := http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if strings.HasPrefix(req.URL.Path, "/health") {
w.Write([]byte("ok"))
return
}
w.Write([]byte("Hello, World!"))
}),
}
unixListener, err := net.Listen("unix", socketName)
if err != nil {
t.Errorf("failed to listen on the socket: %s", err)
return
}
go server.Serve(unixListener)
t.Cleanup(func() {
server.Close()
})
runtime.Gosched() // Allow other goroutines to run
tester.InitServer(fmt.Sprintf(`
{
http_port 9080
https_port 9443
}
http://localhost:9080 {
reverse_proxy {
to unix/%s
health_uri /health
health_interval 2s
health_timeout 5s
}
}
`, socketName), "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
}
+3 -199
View File
@@ -19,16 +19,15 @@ import (
"context"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"reflect"
"runtime"
"runtime/debug"
"sort"
@@ -121,7 +120,7 @@ func cmdStart(fl Flags) (int, error) {
for {
conn, err := ln.Accept()
if err != nil {
if !strings.Contains(err.Error(), "use of closed network connection") {
if !errors.Is(err, net.ErrClosed) {
log.Println(err)
}
break
@@ -332,7 +331,7 @@ func cmdReload(fl Flags) (int, error) {
}
func cmdVersion(_ Flags) (int, error) {
fmt.Println(caddyVersion())
fmt.Println(CaddyVersion())
return caddy.ExitCodeSuccess, nil
}
@@ -570,151 +569,6 @@ func cmdFmt(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil
}
func cmdUpgrade(_ Flags) (int, error) {
l := caddy.Log()
thisExecPath, err := os.Executable()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("determining current executable path: %v", err)
}
thisExecStat, err := os.Stat(thisExecPath)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err)
}
l.Info("this executable will be replaced", zap.String("path", thisExecPath))
// get the list of nonstandard plugins
_, nonstandard, _, err := getModules()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
}
pluginPkgs := make(map[string]struct{})
for _, mod := range nonstandard {
if mod.goModule.Replace != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s",
mod.goModule.Path, mod.goModule.Replace.Path)
}
l.Info("found non-standard module",
zap.String("id", mod.caddyModuleID),
zap.String("package", mod.goModule.Path))
pluginPkgs[mod.goModule.Path] = struct{}{}
}
// build the request URL to download this custom build
qs := url.Values{
"os": {runtime.GOOS},
"arch": {runtime.GOARCH},
}
for pkg := range pluginPkgs {
qs.Add("p", pkg)
}
urlStr := fmt.Sprintf("https://caddyserver.com/api/download?%s", qs.Encode())
// initiate the build
l.Info("requesting build",
zap.String("os", qs.Get("os")),
zap.String("arch", qs.Get("arch")),
zap.Strings("packages", qs["p"]))
resp, err := http.Get(urlStr)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("secure request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
var details struct {
StatusCode int `json:"status_code"`
Error struct {
Message string `json:"message"`
ID string `json:"id"`
} `json:"error"`
}
err2 := json.NewDecoder(resp.Body).Decode(&details)
if err2 != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download and error decoding failed: HTTP %d: %v", resp.StatusCode, err2)
}
return caddy.ExitCodeFailedStartup, fmt.Errorf("download failed: HTTP %d: %s (id=%s)", resp.StatusCode, details.Error.Message, details.Error.ID)
}
// back up the current binary, in case something goes wrong we can replace it
backupExecPath := thisExecPath + ".tmp"
l.Info("build acquired; backing up current executable",
zap.String("current_path", thisExecPath),
zap.String("backup_path", backupExecPath))
err = os.Rename(thisExecPath, backupExecPath)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("backing up current binary: %v", err)
}
defer func() {
if err != nil {
err2 := os.Rename(backupExecPath, thisExecPath)
if err2 != nil {
l.Error("restoring original executable failed; will need to be restored manually",
zap.String("backup_path", backupExecPath),
zap.String("original_path", thisExecPath),
zap.Error(err2))
}
}
}()
// download the file; do this in a closure to close reliably before we execute it
writeFile := func() error {
destFile, err := os.OpenFile(thisExecPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, thisExecStat.Mode())
if err != nil {
return fmt.Errorf("unable to open destination file: %v", err)
}
defer destFile.Close()
l.Info("downloading binary", zap.String("source", urlStr), zap.String("destination", thisExecPath))
_, err = io.Copy(destFile, resp.Body)
if err != nil {
return fmt.Errorf("unable to download file: %v", err)
}
err = destFile.Sync()
if err != nil {
return fmt.Errorf("syncing downloaded file to device: %v", err)
}
return nil
}
err = writeFile()
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
l.Info("download successful; displaying new binary details", zap.String("location", thisExecPath))
// use the new binary to print out version and module info
fmt.Print("\nModule versions:\n\n")
cmd := exec.Command(thisExecPath, "list-modules", "--versions")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
}
fmt.Println("\nVersion:")
cmd = exec.Command(thisExecPath, "version")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
}
fmt.Println()
// clean up the backup file
err = os.Remove(backupExecPath)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to clean up backup binary: %v", err)
}
l.Info("upgrade successful; please restart any running Caddy instances", zap.String("executable", thisExecPath))
return caddy.ExitCodeSuccess, nil
}
func cmdHelp(fl Flags) (int, error) {
const fullDocs = `Full documentation is available at:
https://caddyserver.com/docs/command-line`
@@ -779,56 +633,6 @@ commands:
return caddy.ExitCodeSuccess, nil
}
func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
bi, ok := debug.ReadBuildInfo()
if !ok {
err = fmt.Errorf("no build info")
return
}
for _, modID := range caddy.Modules() {
modInfo, err := caddy.GetModule(modID)
if err != nil {
// that's weird, shouldn't happen
unknown = append(unknown, moduleInfo{caddyModuleID: modID, err: err})
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
}
}
}
caddyModGoMod := moduleInfo{caddyModuleID: modID, goModule: matched}
if strings.HasPrefix(modPkgPath, caddy.ImportPath) {
standard = append(standard, caddyModGoMod)
} else {
nonstandard = append(nonstandard, caddyModGoMod)
}
}
return
}
// apiRequest makes an API request to the endpoint adminAddr with the
// given HTTP method and request URI. If body is non-nil, it will be
// assumed to be Content-Type application/json.
+30
View File
@@ -61,6 +61,12 @@ type Command struct {
// any error that occurred.
type CommandFunc func(Flags) (int, error)
// Commands returns a list of commands initialised by
// RegisterCommand
func Commands() map[string]Command {
return commands
}
var commands = make(map[string]Command)
func init() {
@@ -291,6 +297,30 @@ Downloads an updated Caddy binary with the same modules/plugins at the
latest versions. EXPERIMENTAL: May be changed or removed.`,
})
RegisterCommand(Command{
Name: "add-package",
Func: cmdAddPackage,
Usage: "<packages...>",
Short: "Adds Caddy packages (EXPERIMENTAL)",
Long: `
Downloads an updated Caddy binary with the specified packages (module/plugin)
added. Retains existing packages. Returns an error if the any of packages are
already included. EXPERIMENTAL: May be changed or removed.
`,
})
RegisterCommand(Command{
Name: "remove-package",
Func: cmdRemovePackage,
Usage: "<packages...>",
Short: "Removes Caddy packages (EXPERIMENTAL)",
Long: `
Downloads an updated Caddy binaries without the specified packages (module/plugin).
Returns an error if any of the packages are not included.
EXPERIMENTAL: May be changed or removed.
`,
})
}
// RegisterCommand registers the command cmd.
+8 -3
View File
@@ -361,6 +361,11 @@ func loadEnvFromFile(envFile string) error {
}
}
// Update the storage paths to ensure they have the proper
// value after loading a specified env file.
caddy.ConfigAutosavePath = filepath.Join(caddy.AppConfigDir(), "autosave.json")
caddy.DefaultStorage = &certmagic.FileStorage{Path: caddy.AppDataDir()}
return nil
}
@@ -415,7 +420,7 @@ func printEnvironment() {
fmt.Printf("caddy.AppDataDir=%s\n", caddy.AppDataDir())
fmt.Printf("caddy.AppConfigDir=%s\n", caddy.AppConfigDir())
fmt.Printf("caddy.ConfigAutosavePath=%s\n", caddy.ConfigAutosavePath)
fmt.Printf("caddy.Version=%s\n", caddyVersion())
fmt.Printf("caddy.Version=%s\n", CaddyVersion())
fmt.Printf("runtime.GOOS=%s\n", runtime.GOOS)
fmt.Printf("runtime.GOARCH=%s\n", runtime.GOARCH)
fmt.Printf("runtime.Compiler=%s\n", runtime.Compiler)
@@ -432,8 +437,8 @@ func printEnvironment() {
}
}
// caddyVersion returns a detailed version string, if available.
func caddyVersion() string {
// CaddyVersion returns a detailed version string, if available.
func CaddyVersion() string {
goModule := caddy.GoModule()
ver := goModule.Version
if goModule.Sum != "" {
+306
View File
@@ -0,0 +1,306 @@
// 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 (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"reflect"
"runtime"
"runtime/debug"
"strings"
"github.com/caddyserver/caddy/v2"
"go.uber.org/zap"
)
func cmdUpgrade(_ Flags) (int, error) {
_, nonstandard, _, err := getModules()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
}
pluginPkgs, err := getPluginPackages(nonstandard)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
return upgradeBuild(pluginPkgs)
}
func cmdAddPackage(fl Flags) (int, error) {
if len(fl.Args()) == 0 {
return caddy.ExitCodeFailedStartup, fmt.Errorf("at least one package name must be specified")
}
_, nonstandard, _, err := getModules()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
}
pluginPkgs, err := getPluginPackages(nonstandard)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
for _, arg := range fl.Args() {
if _, ok := pluginPkgs[arg]; ok {
return caddy.ExitCodeFailedStartup, fmt.Errorf("package is already added")
}
pluginPkgs[arg] = struct{}{}
}
return upgradeBuild(pluginPkgs)
}
func cmdRemovePackage(fl Flags) (int, error) {
if len(fl.Args()) == 0 {
return caddy.ExitCodeFailedStartup, fmt.Errorf("at least one package name must be specified")
}
_, nonstandard, _, err := getModules()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
}
pluginPkgs, err := getPluginPackages(nonstandard)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
for _, arg := range fl.Args() {
if _, ok := pluginPkgs[arg]; !ok {
// package does not exist
return caddy.ExitCodeFailedStartup, fmt.Errorf("package is not added")
}
delete(pluginPkgs, arg)
}
return upgradeBuild(pluginPkgs)
}
func upgradeBuild(pluginPkgs map[string]struct{}) (int, error) {
l := caddy.Log()
thisExecPath, err := os.Executable()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("determining current executable path: %v", err)
}
thisExecStat, err := os.Stat(thisExecPath)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err)
}
l.Info("this executable will be replaced", zap.String("path", thisExecPath))
// build the request URL to download this custom build
qs := url.Values{
"os": {runtime.GOOS},
"arch": {runtime.GOARCH},
}
for pkg := range pluginPkgs {
qs.Add("p", pkg)
}
// initiate the build
resp, err := downloadBuild(qs)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download failed: %v", err)
}
defer resp.Body.Close()
// back up the current binary, in case something goes wrong we can replace it
backupExecPath := thisExecPath + ".tmp"
l.Info("build acquired; backing up current executable",
zap.String("current_path", thisExecPath),
zap.String("backup_path", backupExecPath))
err = os.Rename(thisExecPath, backupExecPath)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("backing up current binary: %v", err)
}
defer func() {
if err != nil {
err2 := os.Rename(backupExecPath, thisExecPath)
if err2 != nil {
l.Error("restoring original executable failed; will need to be restored manually",
zap.String("backup_path", backupExecPath),
zap.String("original_path", thisExecPath),
zap.Error(err2))
}
}
}()
// download the file; do this in a closure to close reliably before we execute it
err = writeCaddyBinary(thisExecPath, &resp.Body, thisExecStat)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
l.Info("download successful; displaying new binary details", zap.String("location", thisExecPath))
// use the new binary to print out version and module info
fmt.Print("\nModule versions:\n\n")
if err = listModules(thisExecPath); err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
}
fmt.Println("\nVersion:")
if err = showVersion(thisExecPath); err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
}
fmt.Println()
// clean up the backup file
if err = os.Remove(backupExecPath); err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to clean up backup binary: %v", err)
}
l.Info("upgrade successful; please restart any running Caddy instances", zap.String("executable", thisExecPath))
return caddy.ExitCodeSuccess, nil
}
func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
bi, ok := debug.ReadBuildInfo()
if !ok {
err = fmt.Errorf("no build info")
return
}
for _, modID := range caddy.Modules() {
modInfo, err := caddy.GetModule(modID)
if err != nil {
// that's weird, shouldn't happen
unknown = append(unknown, moduleInfo{caddyModuleID: modID, err: err})
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
}
}
}
caddyModGoMod := moduleInfo{caddyModuleID: modID, goModule: matched}
if strings.HasPrefix(modPkgPath, caddy.ImportPath) {
standard = append(standard, caddyModGoMod)
} else {
nonstandard = append(nonstandard, caddyModGoMod)
}
}
return
}
func listModules(path string) error {
cmd := exec.Command(path, "list-modules", "--versions")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return fmt.Errorf("download succeeded, but unable to execute: %v", err)
}
return nil
}
func showVersion(path string) error {
cmd := exec.Command(path, "version")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return fmt.Errorf("download succeeded, but unable to execute: %v", err)
}
return nil
}
func downloadBuild(qs url.Values) (*http.Response, error) {
l := caddy.Log()
l.Info("requesting build",
zap.String("os", qs.Get("os")),
zap.String("arch", qs.Get("arch")),
zap.Strings("packages", qs["p"]))
resp, err := http.Get(fmt.Sprintf("%s?%s", downloadPath, qs.Encode()))
if err != nil {
return nil, fmt.Errorf("secure request failed: %v", err)
}
if resp.StatusCode >= 400 {
var details struct {
StatusCode int `json:"status_code"`
Error struct {
Message string `json:"message"`
ID string `json:"id"`
} `json:"error"`
}
err2 := json.NewDecoder(resp.Body).Decode(&details)
if err2 != nil {
return nil, fmt.Errorf("download and error decoding failed: HTTP %d: %v", resp.StatusCode, err2)
}
return nil, fmt.Errorf("download failed: HTTP %d: %s (id=%s)", resp.StatusCode, details.Error.Message, details.Error.ID)
}
return resp, nil
}
func getPluginPackages(modules []moduleInfo) (map[string]struct{}, error) {
pluginPkgs := make(map[string]struct{})
for _, mod := range modules {
if mod.goModule.Replace != nil {
return nil, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s",
mod.goModule.Path, mod.goModule.Replace.Path)
}
pluginPkgs[mod.goModule.Path] = struct{}{}
}
return pluginPkgs, nil
}
func writeCaddyBinary(path string, body *io.ReadCloser, fileInfo os.FileInfo) error {
l := caddy.Log()
destFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileInfo.Mode())
if err != nil {
return fmt.Errorf("unable to open destination file: %v", err)
}
defer destFile.Close()
l.Info("downloading binary", zap.String("destination", path))
_, err = io.Copy(destFile, *body)
if err != nil {
return fmt.Errorf("unable to download file: %v", err)
}
err = destFile.Sync()
if err != nil {
return fmt.Errorf("syncing downloaded file to device: %v", err)
}
return nil
}
const downloadPath = "https://caddyserver.com/api/download"
+18 -18
View File
@@ -1,35 +1,35 @@
module github.com/caddyserver/caddy/v2
go 1.15
go 1.16
require (
github.com/Masterminds/sprig/v3 v3.2.2
github.com/alecthomas/chroma v0.9.1
github.com/alecthomas/chroma v0.9.2
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
github.com/caddyserver/certmagic v0.14.0
github.com/caddyserver/certmagic v0.14.5
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
github.com/go-chi/chi v4.1.2+incompatible
github.com/google/cel-go v0.7.3
github.com/google/uuid v1.2.0
github.com/klauspost/compress v1.13.0
github.com/klauspost/cpuid/v2 v2.0.6
github.com/lucas-clemente/quic-go v0.21.0
github.com/mholt/acmez v0.1.3
github.com/google/uuid v1.3.0
github.com/klauspost/compress v1.13.4
github.com/klauspost/cpuid/v2 v2.0.9
github.com/lucas-clemente/quic-go v0.23.0
github.com/mholt/acmez v1.0.0
github.com/naoina/go-stringutil v0.1.0 // indirect
github.com/naoina/toml v0.1.1
github.com/prometheus/client_golang v1.10.1-0.20210603120351-253906201bda
github.com/smallstep/certificates v0.15.15
github.com/smallstep/cli v0.15.16
github.com/smallstep/nosql v0.3.6
github.com/prometheus/client_golang v1.11.0
github.com/smallstep/certificates v0.16.4
github.com/smallstep/cli v0.16.1
github.com/smallstep/nosql v0.3.8
github.com/smallstep/truststore v0.9.6
github.com/yuin/goldmark v1.3.7
github.com/yuin/goldmark v1.4.0
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01
go.uber.org/zap v1.17.0
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
golang.org/x/net v0.0.0-20210525063256-abc453219eb5
go.uber.org/zap v1.19.0
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c
google.golang.org/protobuf v1.26.0
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08
google.golang.org/protobuf v1.27.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.4.0
)
+647 -218
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -182,6 +182,7 @@ type responseWriter struct {
buf *bytes.Buffer
config *Encode
statusCode int
wroteHeader bool
}
// WriteHeader stores the status to write when the time comes
@@ -195,6 +196,19 @@ func (enc *Encode) Match(rw *responseWriter) bool {
return enc.Matcher.Match(rw.statusCode, rw.Header())
}
// Flush implements http.Flusher. It delays the actual Flush of the underlying ResponseWriterWrapper
// until headers were written.
func (rw *responseWriter) Flush() {
if !rw.wroteHeader {
// flushing the underlying ResponseWriter will write header and status code,
// but we need to delay that until we can determine if we must encode and
// therefore add the Content-Encoding header; this happens in the first call
// to rw.Write (see bug in #4314)
return
}
rw.ResponseWriterWrapper.Flush()
}
// Write writes to the response. If the response qualifies,
// it is encoded using the encoder, which is initialized
// if not done so already.
@@ -225,6 +239,7 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
if rw.statusCode > 0 {
rw.ResponseWriter.WriteHeader(rw.statusCode)
rw.statusCode = 0
rw.wroteHeader = true
}
switch {
@@ -271,6 +286,7 @@ func (rw *responseWriter) Close() error {
// that rely on If-None-Match, for example
rw.ResponseWriter.WriteHeader(rw.statusCode)
rw.statusCode = 0
rw.wroteHeader = true
}
if rw.w != nil {
err2 := rw.w.Close()
+4 -5
View File
@@ -15,7 +15,6 @@
package caddygzip
import (
"compress/flate"
"fmt"
"strconv"
@@ -68,11 +67,11 @@ func (g *Gzip) Provision(ctx caddy.Context) error {
// Validate validates g's configuration.
func (g Gzip) Validate() error {
if g.Level < flate.NoCompression {
return fmt.Errorf("quality too low; must be >= %d", flate.NoCompression)
if g.Level < gzip.StatelessCompression {
return fmt.Errorf("quality too low; must be >= %d", gzip.StatelessCompression)
}
if g.Level > flate.BestCompression {
return fmt.Errorf("quality too high; must be <= %d", flate.BestCompression)
if g.Level > gzip.BestCompression {
return fmt.Errorf("quality too high; must be <= %d", gzip.BestCompression)
}
return nil
}
+4 -1
View File
@@ -47,7 +47,10 @@ func (Zstd) AcceptEncoding() string { return "zstd" }
// NewEncoder returns a new gzip writer.
func (z Zstd) NewEncoder() encode.Encoder {
writer, _ := zstd.NewWriter(nil)
// The default of 8MB for the window is
// too large for many clients, so we limit
// it to 128K to lighten their load.
writer, _ := zstd.NewWriter(nil, zstd.WithWindowSize(128<<10), zstd.WithEncoderConcurrency(1), zstd.WithZeroFrames(true))
return writer
}
@@ -264,7 +264,7 @@ func (l byTime) Less(i, j int) bool { return l.Items[i].ModTime.Before(l.Items[j
const (
sortByName = "name"
sortByNameDirFirst = "name_dir_first"
sortByNameDirFirst = "namedirfirst"
sortBySize = "size"
sortByTime = "time"
)
@@ -41,6 +41,7 @@ func init() {
// browse [<template_file>]
// precompressed <formats...>
// status <status>
// disable_canonical_uris
// }
//
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
@@ -112,6 +113,13 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
}
fsrv.StatusCode = caddyhttp.WeakString(h.Val())
case "disable_canonical_uris":
if h.NextArg() {
return nil, h.ArgErr()
}
falseBool := false
fsrv.CanonicalURIs = &falseBool
default:
return nil, h.Errf("unknown subdirective '%s'", h.Val())
}
@@ -70,6 +70,10 @@ type FileServer struct {
// Use redirects to enforce trailing slashes for directories, or to
// remove trailing slash from URIs for files. Default is true.
//
// Canonicalization will not happen if the last element of the request's
// path (the filename) is changed in an internal rewrite, to avoid
// clobbering the explicit rewrite with implicit behavior.
CanonicalURIs *bool `json:"canonical_uris,omitempty"`
// Override the status code written when successfully serving a file.
+14 -2
View File
@@ -82,7 +82,19 @@ type (
// MatchMethod matches requests by the method.
MatchMethod []string
// MatchQuery matches requests by URI's query string.
// MatchQuery matches requests by the URI's query string. It takes a JSON object
// keyed by the query keys, with an array of string values to match for that key.
// Query key matches are exact, but wildcards may be used for value matches. Both
// keys and values may be placeholders.
// An example of the structure to match `?key=value&topic=api&query=something` is:
//
// ```json
// {
// "key": ["value"],
// "topic": ["api"],
// "query": ["*"]
// }
// ```
MatchQuery url.Values
// MatchHeader matches requests by header fields. It performs fast,
@@ -667,7 +679,7 @@ func (MatchProtocol) CaddyModule() caddy.ModuleInfo {
func (m MatchProtocol) Match(r *http.Request) bool {
switch string(m) {
case "grpc":
return r.Header.Get("content-type") == "application/grpc"
return strings.HasPrefix(r.Header.Get("content-type"), "application/grpc")
case "https":
return r.TLS != nil
case "http":
+5 -3
View File
@@ -34,7 +34,8 @@ type adminUpstreams struct{}
// upstreamResults holds the status of a particular upstream
type upstreamStatus struct {
Address string `json:"address"`
ID string `json:"id"`
Address string `json:"address"` // Address is deprecated, should be removed in a future release.
Healthy bool `json:"healthy"`
NumRequests int `json:"num_requests"`
Fails int `json:"fails"`
@@ -78,7 +79,7 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er
// Iterate over the upstream pool (needs to be fast)
var rangeErr error
hosts.Range(func(key, val interface{}) bool {
address, ok := key.(string)
id, ok := key.(string)
if !ok {
rangeErr = caddy.APIError{
HTTPStatus: http.StatusInternalServerError,
@@ -97,7 +98,8 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er
}
results = append(results, upstreamStatus{
Address: address,
ID: id,
Address: id,
Healthy: !upstream.Unhealthy(),
NumRequests: upstream.NumRequests(),
Fails: upstream.Fails(),
+9 -3
View File
@@ -61,7 +61,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// lb_try_interval <interval>
//
// # active health checking
// health_path <path>
// health_uri <uri>
// health_port <port>
// health_interval <interval>
// health_timeout <duration>
@@ -219,6 +219,12 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// treated as a SRV-based upstream, and any port will be
// dropped.
appendUpstream := func(address string) error {
var id string
if strings.Contains(address, "|") {
parts := strings.SplitN(address, "|", 2)
id = parts[0]
address = parts[1]
}
isSRV := strings.HasPrefix(address, "srv+")
if isSRV {
address = strings.TrimPrefix(address, "srv+")
@@ -231,9 +237,9 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if host, _, err := net.SplitHostPort(dialAddr); err == nil {
dialAddr = host
}
h.Upstreams = append(h.Upstreams, &Upstream{LookupSRV: dialAddr})
h.Upstreams = append(h.Upstreams, &Upstream{ID: id, LookupSRV: dialAddr})
} else {
h.Upstreams = append(h.Upstreams, &Upstream{Dial: dialAddr})
h.Upstreams = append(h.Upstreams, &Upstream{ID: id, Dial: dialAddr})
}
return nil
}
@@ -192,7 +192,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
// NOTE: we delete the tokens as we go so that the reverse_proxy
// unmarshal doesn't see these subdirectives which it cannot handle
for dispenser.Next() {
for dispenser.NextBlock(0) {
for dispenser.NextBlock(0) && dispenser.Nesting() == 1 {
switch dispenser.Val() {
case "root":
if !dispenser.NextArg() {
@@ -189,13 +189,14 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
return
}
hostAddr := addr.JoinHostPort(0)
dialAddr := hostAddr
if addr.IsUnixNetwork() {
// 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; unix sockets are usually local
hostAddr = "localhost"
}
err = h.doActiveHealthCheck(DialInfo{Network: addr.Network, Address: hostAddr}, hostAddr, upstream.Host)
err = h.doActiveHealthCheck(DialInfo{Network: addr.Network, Address: dialAddr}, hostAddr, upstream.Host)
if err != nil {
h.HealthChecks.Active.logger.Error("active health check failed",
zap.String("address", hostAddr),
+9
View File
@@ -65,6 +65,12 @@ type UpstreamPool []*Upstream
type Upstream struct {
Host `json:"-"`
// The unique ID for this upstream, to disambiguate multiple
// upstreams with the same Dial address. This is optional,
// and only necessary if the upstream states need to be
// separate, such as having different health checking policies.
ID string `json:"id,omitempty"`
// The [network address](/docs/conventions#network-addresses)
// to dial to connect to the upstream. Must represent precisely
// one socket (i.e. no port ranges). A valid network address
@@ -98,6 +104,9 @@ type Upstream struct {
}
func (u Upstream) String() string {
if u.ID != "" {
return u.ID
}
if u.LookupSRV != "" {
return u.LookupSRV
}
+21 -20
View File
@@ -23,6 +23,7 @@ import (
"io"
"net"
"net/http"
"net/textproto"
"net/url"
"regexp"
"strconv"
@@ -80,10 +81,13 @@ type Handler struct {
// Upstreams is the list of backends to proxy to.
Upstreams UpstreamPool `json:"upstreams,omitempty"`
// Adjusts how often to flush the response buffer. A
// negative value disables response buffering.
// TODO: figure out good defaults and write docs for this
// (see https://github.com/caddyserver/caddy/issues/1460)
// Adjusts how often to flush the response buffer. By default,
// no periodic flushing is done. A negative value disables
// response buffering, and flushes immediately after each
// write to the client. This option is ignored when the upstream's
// response is recognized as a streaming response, or if its
// content length is -1; for such responses, writes are flushed
// to the client immediately.
FlushInterval caddy.Duration `json:"flush_interval,omitempty"`
// Headers manipulates headers between Caddy and the backend.
@@ -528,13 +532,19 @@ func (h Handler) prepareRequest(req *http.Request) error {
// If we aren't the first proxy retain prior
// X-Forwarded-For information as a comma+space
// separated list and fold multiple headers into one.
if prior, ok := req.Header["X-Forwarded-For"]; ok {
prior, ok := req.Header["X-Forwarded-For"]
omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
if len(prior) > 0 {
clientIP = strings.Join(prior, ", ") + ", " + clientIP
}
req.Header.Set("X-Forwarded-For", clientIP)
if !omit {
req.Header.Set("X-Forwarded-For", clientIP)
}
}
if req.Header.Get("X-Forwarded-Proto") == "" {
prior, ok := req.Header["X-Forwarded-Proto"]
omit := ok && prior == nil
if len(prior) == 0 && !omit {
// set X-Forwarded-Proto; many backend apps expect this too
proto := "https"
if req.TLS == nil {
@@ -681,15 +691,6 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
}
rw.WriteHeader(res.StatusCode)
// some apps need the response headers before starting to stream content with http2,
// so it's important to explicitly flush the headers to the client before streaming the data.
// (see https://github.com/caddyserver/caddy/issues/3556 for use case and nuances)
if h.isBidirectionalStream(req, res) {
if wf, ok := rw.(http.Flusher); ok {
wf.Flush()
}
}
err = h.copyResponse(rw, res.Body, h.flushInterval(req, res))
res.Body.Close() // close now, instead of defer, to populate res.Trailer
if err != nil {
@@ -827,10 +828,10 @@ func upgradeType(h http.Header) string {
// removeConnectionHeaders removes hop-by-hop headers listed in the "Connection" header of h.
// See RFC 7230, section 6.1
func removeConnectionHeaders(h http.Header) {
if c := h.Get("Connection"); c != "" {
for _, f := range strings.Split(c, ",") {
if f = strings.TrimSpace(f); f != "" {
h.Del(f)
for _, f := range h["Connection"] {
for _, sf := range strings.Split(f, ",") {
if sf = textproto.TrimString(sf); sf != "" {
h.Del(sf)
}
}
}
+16 -4
View File
@@ -32,6 +32,9 @@ import (
func (h Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWriter, req *http.Request, res *http.Response) {
reqUpType := upgradeType(req.Header)
resUpType := upgradeType(res.Header)
// TODO: Update to use "net/http/internal/ascii" once we bumped
// the minimum Go version to 1.17.
// See https://github.com/golang/go/commit/5c489514bc5e61ad9b5b07bd7d8ec65d66a0512a
if reqUpType != resUpType {
h.logger.Debug("backend tried to switch to unexpected protocol via Upgrade header",
zap.String("backend_upgrade", resUpType),
@@ -39,8 +42,6 @@ func (h Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWrite
return
}
copyHeader(res.Header, rw.Header())
hj, ok := rw.(http.Hijacker)
if !ok {
h.logger.Sugar().Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw)
@@ -78,6 +79,9 @@ func (h Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWrite
logger.Debug("connection closed", zap.Duration("duration", time.Since(start)))
}()
copyHeader(rw.Header(), res.Header)
res.Header = rw.Header()
res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above
if err := res.Write(brw); err != nil {
h.logger.Debug("response write", zap.Error(err))
@@ -107,13 +111,16 @@ func (h Handler) flushInterval(req *http.Request, res *http.Response) time.Durat
return -1 // negative means immediately
}
// We might have the case of streaming for which Content-Length might be unset.
if res.ContentLength == -1 {
return -1
}
// for h2 and h2c upstream streaming data to client (issues #3556 and #3606)
if h.isBidirectionalStream(req, res) {
return -1
}
// TODO: more specific cases? e.g. res.ContentLength == -1? (this TODO is from the std lib, but
// strangely similar to our isBidirectionalStream function that we implemented ourselves)
return time.Duration(h.FlushInterval)
}
@@ -142,6 +149,11 @@ func (h Handler) copyResponse(dst io.Writer, src io.Reader, flushInterval time.D
latency: flushInterval,
}
defer mlw.stop()
// set up initial timer so headers get flushed even if body writes are delayed
mlw.flushPending = true
mlw.t = time.AfterFunc(flushInterval, mlw.delayedFlush)
dst = mlw
}
}
+14
View File
@@ -184,8 +184,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log = logger.Error
}
userID, _ := repl.GetString("http.auth.user.id")
log("handled request",
zap.String("common_log", repl.ReplaceAll(commonLogFormat, commonLogEmptyValue)),
zap.String("user_id", userID),
zap.Duration("duration", duration),
zap.Int("size", wrec.Size()),
zap.Int("status", wrec.Status()),
@@ -379,7 +382,9 @@ func (s *Server) hasTLSClientAuth() bool {
// that it is after any other host matcher but before any "catch-all"
// route without a host matcher.
func (s *Server) findLastRouteWithHostMatcher() int {
foundHostMatcher := false
lastIndex := len(s.Routes)
for i, route := range s.Routes {
// since we want to break out of an inner loop, use a closure
// to allow us to use 'return' when we found a host matcher
@@ -388,6 +393,7 @@ func (s *Server) findLastRouteWithHostMatcher() int {
for _, matcher := range sets {
switch matcher.(type) {
case *MatchHost:
foundHostMatcher = true
return true
}
}
@@ -401,6 +407,14 @@ func (s *Server) findLastRouteWithHostMatcher() int {
lastIndex = i + 1
}
}
// If we didn't actually find a host matcher, return 0
// because that means every defined route was a "catch-all".
// See https://caddy.community/t/how-to-set-priority-in-caddyfile/13002/8
if !foundHostMatcher {
return 0
}
return lastIndex
}
+8 -15
View File
@@ -50,17 +50,16 @@ type Handler struct {
// The hostname or IP address by which ACME clients
// will access the server. This is used to populate
// the ACME directory endpoint. Default: localhost.
// the ACME directory endpoint. If not set, the Host
// header of the request will be used.
// COMPATIBILITY NOTE / TODO: This property may go away in the
// future, as it is currently only required due to
// limitations in the underlying library. Do not rely
// on this property long-term; check release notes.
// future. Do not rely on this property long-term; check release notes.
Host string `json:"host,omitempty"`
// The path prefix under which to serve all ACME
// endpoints. All other requests will not be served
// by this handler and will be passed through to
// the next one. Default: "/acme/"
// the next one. Default: "/acme/".
// COMPATIBILITY NOTE / TODO: This property may go away in the
// future, as it is currently only required due to
// limitations in the underlying library. Do not rely
@@ -93,9 +92,6 @@ func (ash *Handler) Provision(ctx caddy.Context) error {
if ash.CA == "" {
ash.CA = caddypki.DefaultCAID
}
if ash.Host == "" {
ash.Host = defaultHost
}
if ash.PathPrefix == "" {
ash.PathPrefix = defaultPathPrefix
}
@@ -150,9 +146,9 @@ func (ash *Handler) Provision(ctx caddy.Context) error {
// create the router for the ACME endpoints
acmeRouterHandler := acmeAPI.NewHandler(acmeAPI.HandlerOptions{
CA: auth,
DB: acmeDB, // stores all the server state
DNS: ash.Host, // used for directory links; TODO: not needed (follow-up upstream with step-ca)
Prefix: ash.PathPrefix, // used for directory links
DB: acmeDB, // stores all the server state
DNS: ash.Host, // used for directory links
Prefix: strings.Trim(ash.PathPrefix, "/"), // used for directory links
})
// extract its http.Handler so we can use it directly
@@ -219,10 +215,7 @@ func (ash Handler) openDatabase() (*db.AuthDB, error) {
return database.(databaseCloser).DB, err
}
const (
defaultHost = "localhost"
defaultPathPrefix = "/acme/"
)
const defaultPathPrefix = "/acme/"
var keyCleaner = regexp.MustCompile(`[^\w.-_]`)
var databasePool = caddy.NewUsagePool()
-10
View File
@@ -89,10 +89,6 @@ type AutomationPolicy struct {
// zerossl.
IssuersRaw []json.RawMessage `json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module"`
// DEPRECATED: Use `issuers` instead (November 2020). This field will
// be removed in the future.
IssuerRaw json.RawMessage `json:"issuer,omitempty" caddy:"namespace=tls.issuance inline_key=module"`
// If true, certificates will be requested with MustStaple. Not all
// CAs support this, and there are potentially serious consequences
// of enabling this feature without proper threat modeling.
@@ -180,12 +176,6 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
}
}
// TODO: IssuerRaw field deprecated as of November 2020 - remove this shim after deprecation is complete
if ap.IssuerRaw != nil {
tlsApp.logger.Warn("the 'issuer' field is deprecated and will be removed in the future; use 'issuers' instead; your issuer has been appended automatically for now")
ap.IssuersRaw = append(ap.IssuersRaw, ap.IssuerRaw)
}
// load and provision any explicitly-configured issuer modules
if ap.IssuersRaw != nil {
val, err := tlsApp.ctx.LoadModule(ap, "IssuersRaw")
+3
View File
@@ -135,6 +135,7 @@ func (SingleFieldEncoder) CaddyModule() caddy.ModuleInfo {
// Provision sets up the encoder.
func (se *SingleFieldEncoder) Provision(ctx caddy.Context) error {
caddy.Log().Named("caddy.logging.encoders.single_field").Warn("the 'single_field' encoder is deprecated and will be removed soon!")
if se.FallbackRaw != nil {
val, err := ctx.LoadModule(se, "FallbackRaw")
if err != nil {
@@ -307,6 +308,8 @@ func (lec *LogEncoderConfig) ZapcoreEncoderConfig() zapcore.EncoderConfig {
timeFormat = "2006/01/02 15:04:05.000"
case "wall_nano":
timeFormat = "2006/01/02 15:04:05.000000000"
case "common_log":
timeFormat = "02/Jan/2006:15:04:05 -0700"
}
timeFormatter = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(ts.UTC().Format(timeFormat))
+2
View File
@@ -188,9 +188,11 @@ func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
// Interface guards
var (
_ LogFieldFilter = (*DeleteFilter)(nil)
_ LogFieldFilter = (*ReplaceFilter)(nil)
_ LogFieldFilter = (*IPMaskFilter)(nil)
_ caddyfile.Unmarshaler = (*DeleteFilter)(nil)
_ caddyfile.Unmarshaler = (*ReplaceFilter)(nil)
_ caddyfile.Unmarshaler = (*IPMaskFilter)(nil)
_ caddy.Provisioner = (*IPMaskFilter)(nil)
+4
View File
@@ -301,6 +301,10 @@ func globalDefaultReplacements(key string) (interface{}, bool) {
return nowFunc().Format("02/Jan/2006:15:04:05 -0700"), true
case "time.now.year":
return strconv.Itoa(nowFunc().Year()), true
case "time.now.unix":
return strconv.FormatInt(nowFunc().Unix(), 10), true
case "time.now.unix_ms":
return strconv.FormatInt(nowFunc().UnixNano()/int64(time.Millisecond), 10), true
}
return nil, false