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>
This commit is contained in:
Francis Lavoie 2020-11-23 14:46:50 -05:00 committed by GitHub
parent 4a641f6c6f
commit 3cfefeb0f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1084 additions and 658 deletions

View file

@ -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
}

View file

@ -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)
}
options[dir] = val
// 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[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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -0,0 +1,83 @@
{
servers {
timeouts {
idle 90s
}
}
servers :80 {
timeouts {
idle 60s
}
}
servers :443 {
timeouts {
idle 30s
}
}
}
foo.com {
}
http://bar.com {
}
:8080 {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"idle_timeout": 30000000000,
"routes": [
{
"match": [
{
"host": [
"foo.com"
]
}
],
"terminal": true
}
]
},
"srv1": {
"listen": [
":80"
],
"idle_timeout": 60000000000,
"routes": [
{
"match": [
{
"host": [
"bar.com"
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"bar.com"
]
}
},
"srv2": {
"listen": [
":8080"
],
"idle_timeout": 90000000000
}
}
}
}
}

View file

@ -0,0 +1,62 @@
{
servers {
listener_wrappers {
tls
}
timeouts {
read_body 30s
read_header 30s
write 30s
idle 30s
}
max_header_size 100MB
protocol {
allow_h2c
experimental_http3
strict_sni_host
}
}
}
foo.com {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"listener_wrappers": [
{
"wrapper": "tls"
}
],
"read_timeout": 30000000000,
"read_header_timeout": 30000000000,
"write_timeout": 30000000000,
"idle_timeout": 30000000000,
"max_header_bytes": 100000000,
"routes": [
{
"match": [
{
"host": [
"foo.com"
]
}
],
"terminal": true
}
],
"strict_sni_host": true,
"experimental_http3": true,
"allow_h2c": true
}
}
}
}
}

View file

@ -23,6 +23,7 @@ import (
"strconv"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func init() {
@ -231,6 +232,8 @@ func (tlsPlaceholderWrapper) CaddyModule() caddy.ModuleInfo {
func (tlsPlaceholderWrapper) WrapListener(ln net.Listener) net.Listener { return ln }
func (tlsPlaceholderWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil }
const (
// DefaultHTTPPort is the default port for HTTP.
DefaultHTTPPort = 80
@ -241,3 +244,4 @@ const (
// Interface guard
var _ caddy.ListenerWrapper = (*tlsPlaceholderWrapper)(nil)
var _ caddyfile.Unmarshaler = (*tlsPlaceholderWrapper)(nil)