Skip to content

Commit

Permalink
httpcaddyfile: Configure servers via global options (#3836)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
francislavoie and mholt authored Nov 23, 2020
1 parent 4a641f6 commit 3cfefeb
Show file tree
Hide file tree
Showing 15 changed files with 1,084 additions and 658 deletions.
8 changes: 8 additions & 0 deletions caddyconfig/httpcaddyfile/addresses.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"fmt"
"net"
"reflect"
"sort"
"strconv"
"strings"
"unicode"
Expand Down Expand Up @@ -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
}

Expand Down
55 changes: 42 additions & 13 deletions caddyconfig/httpcaddyfile/httptype.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
5 changes: 5 additions & 0 deletions caddyconfig/httpcaddyfile/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -361,3 +362,7 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser) (interface{}, error) {
}
return val, nil
}

func parseServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
return unmarshalCaddyfileServerOptions(d)
}
235 changes: 235 additions & 0 deletions caddyconfig/httpcaddyfile/serveroptions.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 3cfefeb

Please sign in to comment.