summaryrefslogtreecommitdiff
path: root/caddyconfig
diff options
context:
space:
mode:
authorFrancis Lavoie <lavofr@gmail.com>2020-11-23 14:46:50 -0500
committerGitHub <noreply@github.com>2020-11-23 12:46:50 -0700
commit3cfefeb0f71d54f1d9a76a63be7b97d0943c88ef (patch)
treeefc01573221ab12af054df6b1b7832a88adf6fd3 /caddyconfig
parent4a641f6c6fb441a8301eeb8cab0c0b61de70ff7f (diff)
httpcaddyfile: Configure servers via global options (#3836)
* httpcaddyfile: First pass at implementing server options * httpcaddyfile: Add listener wrapper support * httpcaddyfile: Sort sbaddrs to make adapt output more deterministic * httpcaddyfile: Add server options adapt tests * httpcaddyfile: Windows line endings lol * caddytest: More windows line endings lol (sorry Matt) * Update caddyconfig/httpcaddyfile/serveroptions.go Co-authored-by: Matt Holt <mholt@users.noreply.github.com> * httpcaddyfile: Reword listener address "matcher" * Apply suggestions from code review Co-authored-by: Matt Holt <mholt@users.noreply.github.com> * httpcaddyfile: Deprecate experimental_http3 option (moved to servers) * httpcaddyfile: Remove validation step, no longer needed Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Diffstat (limited to 'caddyconfig')
-rw-r--r--caddyconfig/httpcaddyfile/addresses.go8
-rw-r--r--caddyconfig/httpcaddyfile/httptype.go55
-rw-r--r--caddyconfig/httpcaddyfile/options.go5
-rw-r--r--caddyconfig/httpcaddyfile/serveroptions.go235
4 files changed, 290 insertions, 13 deletions
diff --git a/caddyconfig/httpcaddyfile/addresses.go b/caddyconfig/httpcaddyfile/addresses.go
index 51411a9..7105320 100644
--- a/caddyconfig/httpcaddyfile/addresses.go
+++ b/caddyconfig/httpcaddyfile/addresses.go
@@ -18,6 +18,7 @@ import (
"fmt"
"net"
"reflect"
+ "sort"
"strconv"
"strings"
"unicode"
@@ -163,6 +164,13 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
sbaddrs = append(sbaddrs, a)
}
+
+ // sort them by their first address (we know there will always be at least one)
+ // to avoid problems with non-deterministic ordering (makes tests flaky)
+ sort.Slice(sbaddrs, func(i, j int) bool {
+ return sbaddrs[i].addresses[0] < sbaddrs[j].addresses[0]
+ })
+
return sbaddrs
}
diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go
index 35eab90..e4e40b2 100644
--- a/caddyconfig/httpcaddyfile/httptype.go
+++ b/caddyconfig/httpcaddyfile/httptype.go
@@ -218,13 +218,6 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
return nil, warnings, err
}
- // if experimental HTTP/3 is enabled, enable it on each server
- if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
- for _, srv := range httpApp.Servers {
- srv.ExperimentalHTTP3 = true
- }
- }
-
// extract any custom logs, and enforce configured levels
var customLogs []namedCustomLog
var hasDefaultLog bool
@@ -311,23 +304,54 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
}
for _, segment := range serverBlocks[0].block.Segments {
- dir := segment.Directive()
+ opt := segment.Directive()
var val interface{}
var err error
disp := caddyfile.NewDispenser(segment)
- dirFunc, ok := registeredGlobalOptions[dir]
+ optFunc, ok := registeredGlobalOptions[opt]
if !ok {
tkn := segment[0]
- return nil, fmt.Errorf("%s:%d: unrecognized global option: %s", tkn.File, tkn.Line, dir)
+ return nil, fmt.Errorf("%s:%d: unrecognized global option: %s", tkn.File, tkn.Line, opt)
}
- val, err = dirFunc(disp)
+ val, err = optFunc(disp)
if err != nil {
- return nil, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err)
+ return nil, fmt.Errorf("parsing caddyfile tokens for '%s': %v", opt, err)
+ }
+
+ // As a special case, fold multiple "servers" options together
+ // in an array instead of overwriting a possible existing value
+ if opt == "servers" {
+ existingOpts, ok := options[opt].([]serverOptions)
+ if !ok {
+ existingOpts = []serverOptions{}
+ }
+ serverOpts, ok := val.(serverOptions)
+ if !ok {
+ return nil, fmt.Errorf("unexpected type from 'servers' global options")
+ }
+ options[opt] = append(existingOpts, serverOpts)
+ continue
}
- options[dir] = val
+ options[opt] = val
+ }
+
+ // If we got "servers" options, we'll sort them by their listener address
+ if serverOpts, ok := options["servers"].([]serverOptions); ok {
+ sort.Slice(serverOpts, func(i, j int) bool {
+ return len(serverOpts[i].ListenerAddress) > len(serverOpts[j].ListenerAddress)
+ })
+
+ // Reject the config if there are duplicate listener address
+ seen := make(map[string]bool)
+ for _, entry := range serverOpts {
+ if _, alreadySeen := seen[entry.ListenerAddress]; alreadySeen {
+ return nil, fmt.Errorf("cannot have 'servers' global options with duplicate listener addresses: %s", entry.ListenerAddress)
+ }
+ seen[entry.ListenerAddress] = true
+ }
}
return serverBlocks[1:], nil
@@ -602,6 +626,11 @@ func (st *ServerType) serversFromPairings(
servers[fmt.Sprintf("srv%d", i)] = srv
}
+ err := applyServerOptions(servers, options, warnings)
+ if err != nil {
+ return nil, err
+ }
+
return servers, nil
}
diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go
index 7d34805..5001974 100644
--- a/caddyconfig/httpcaddyfile/options.go
+++ b/caddyconfig/httpcaddyfile/options.go
@@ -43,6 +43,7 @@ func init() {
RegisterGlobalOption("local_certs", parseOptTrue)
RegisterGlobalOption("key_type", parseOptSingleString)
RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
+ RegisterGlobalOption("servers", parseServerOptions)
}
func parseOptTrue(d *caddyfile.Dispenser) (interface{}, error) {
@@ -361,3 +362,7 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser) (interface{}, error) {
}
return val, nil
}
+
+func parseServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
+ return unmarshalCaddyfileServerOptions(d)
+}
diff --git a/caddyconfig/httpcaddyfile/serveroptions.go b/caddyconfig/httpcaddyfile/serveroptions.go
new file mode 100644
index 0000000..38fa0f1
--- /dev/null
+++ b/caddyconfig/httpcaddyfile/serveroptions.go
@@ -0,0 +1,235 @@
+// 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"
+ "github.com/dustin/go-humanize"
+)
+
+// serverOptions collects server config overrides parsed from Caddyfile global options
+type serverOptions struct {
+ // If set, will only apply these options to servers that contain a
+ // listener address that matches exactly. If empty, will apply to all
+ // servers that were not already matched by another serverOptions.
+ ListenerAddress string
+
+ // These will all map 1:1 to the caddyhttp.Server struct
+ ListenerWrappersRaw []json.RawMessage
+ ReadTimeout caddy.Duration
+ ReadHeaderTimeout caddy.Duration
+ WriteTimeout caddy.Duration
+ IdleTimeout caddy.Duration
+ MaxHeaderBytes int
+ AllowH2C bool
+ ExperimentalHTTP3 bool
+ StrictSNIHost *bool
+}
+
+func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
+ serverOpts := serverOptions{}
+ for d.Next() {
+ if d.NextArg() {
+ serverOpts.ListenerAddress = d.Val()
+ if d.NextArg() {
+ return nil, d.ArgErr()
+ }
+ }
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
+ switch d.Val() {
+ case "listener_wrappers":
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
+ mod, err := caddy.GetModule("caddy.listeners." + d.Val())
+ if err != nil {
+ return nil, fmt.Errorf("finding listener module '%s': %v", d.Val(), err)
+ }
+ unm, ok := mod.New().(caddyfile.Unmarshaler)
+ if !ok {
+ return nil, fmt.Errorf("listener module '%s' is not a Caddyfile unmarshaler", mod)
+ }
+ err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
+ if err != nil {
+ return nil, err
+ }
+ listenerWrapper, ok := unm.(caddy.ListenerWrapper)
+ if !ok {
+ return nil, fmt.Errorf("module %s is not a listener wrapper", mod)
+ }
+ jsonListenerWrapper := caddyconfig.JSONModuleObject(
+ listenerWrapper,
+ "wrapper",
+ listenerWrapper.(caddy.Module).CaddyModule().ID.Name(),
+ nil,
+ )
+ serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper)
+ }
+
+ case "timeouts":
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
+ switch d.Val() {
+ case "read_body":
+ if !d.NextArg() {
+ return nil, d.ArgErr()
+ }
+ dur, err := caddy.ParseDuration(d.Val())
+ if err != nil {
+ return nil, d.Errf("parsing read_body timeout duration: %v", err)
+ }
+ serverOpts.ReadTimeout = caddy.Duration(dur)
+
+ case "read_header":
+ if !d.NextArg() {
+ return nil, d.ArgErr()
+ }
+ dur, err := caddy.ParseDuration(d.Val())
+ if err != nil {
+ return nil, d.Errf("parsing read_header timeout duration: %v", err)
+ }
+ serverOpts.ReadHeaderTimeout = caddy.Duration(dur)
+
+ case "write":
+ if !d.NextArg() {
+ return nil, d.ArgErr()
+ }
+ dur, err := caddy.ParseDuration(d.Val())
+ if err != nil {
+ return nil, d.Errf("parsing write timeout duration: %v", err)
+ }
+ serverOpts.WriteTimeout = caddy.Duration(dur)
+
+ case "idle":
+ if !d.NextArg() {
+ return nil, d.ArgErr()
+ }
+ dur, err := caddy.ParseDuration(d.Val())
+ if err != nil {
+ return nil, d.Errf("parsing idle timeout duration: %v", err)
+ }
+ serverOpts.IdleTimeout = caddy.Duration(dur)
+
+ default:
+ return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
+ }
+ }
+
+ case "max_header_size":
+ var sizeStr string
+ if !d.AllArgs(&sizeStr) {
+ return nil, d.ArgErr()
+ }
+ size, err := humanize.ParseBytes(sizeStr)
+ if err != nil {
+ return nil, d.Errf("parsing max_header_size: %v", err)
+ }
+ serverOpts.MaxHeaderBytes = int(size)
+
+ case "protocol":
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
+ switch d.Val() {
+ case "allow_h2c":
+ if d.NextArg() {
+ return nil, d.ArgErr()
+ }
+ serverOpts.AllowH2C = true
+
+ case "experimental_http3":
+ if d.NextArg() {
+ return nil, d.ArgErr()
+ }
+ serverOpts.ExperimentalHTTP3 = true
+
+ case "strict_sni_host":
+ if d.NextArg() {
+ return nil, d.ArgErr()
+ }
+ trueBool := true
+ serverOpts.StrictSNIHost = &trueBool
+
+ default:
+ return nil, d.Errf("unrecognized protocol option '%s'", d.Val())
+ }
+ }
+
+ default:
+ return nil, d.Errf("unrecognized servers option '%s'", d.Val())
+ }
+ }
+ }
+ return serverOpts, nil
+}
+
+// applyServerOptions sets the server options on the appropriate servers
+func applyServerOptions(
+ servers map[string]*caddyhttp.Server,
+ options map[string]interface{},
+ warnings *[]caddyconfig.Warning,
+) error {
+ // If experimental HTTP/3 is enabled, enable it on each server.
+ // We already know there won't be a conflict with serverOptions because
+ // we validated earlier that "experimental_http3" cannot be set at the same
+ // time as "servers"
+ if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
+ *warnings = append(*warnings, caddyconfig.Warning{Message: "the 'experimental_http3' global option is deprecated, please use the 'servers > protocol > experimental_http3' option instead"})
+ for _, srv := range servers {
+ srv.ExperimentalHTTP3 = true
+ }
+ }
+
+ serverOpts, ok := options["servers"].([]serverOptions)
+ if !ok {
+ return nil
+ }
+
+ for _, server := range servers {
+ // find the options that apply to this server
+ opts := func() *serverOptions {
+ for _, entry := range serverOpts {
+ if entry.ListenerAddress == "" {
+ return &entry
+ }
+ for _, listener := range server.Listen {
+ if entry.ListenerAddress == listener {
+ return &entry
+ }
+ }
+ }
+ return nil
+ }()
+
+ // if none apply, then move to the next server
+ if opts == nil {
+ continue
+ }
+
+ // set all the options
+ server.ListenerWrappersRaw = opts.ListenerWrappersRaw
+ server.ReadTimeout = opts.ReadTimeout
+ server.ReadHeaderTimeout = opts.ReadHeaderTimeout
+ server.WriteTimeout = opts.WriteTimeout
+ server.IdleTimeout = opts.IdleTimeout
+ server.MaxHeaderBytes = opts.MaxHeaderBytes
+ server.AllowH2C = opts.AllowH2C
+ server.ExperimentalHTTP3 = opts.ExperimentalHTTP3
+ server.StrictSNIHost = opts.StrictSNIHost
+ }
+
+ return nil
+}