httpcaddyfile: Refactor site key parsing; detect conflicting schemes

We now store the parsed site/server block keys with the server block,
rather than parsing the addresses every time we read them.

Also detect conflicting schemes, i.e. TLS and non-TLS cannot be served
from the same server (natively -- modules could be built for it).

Also do not add site subroutes (subroutes generated specifically from
site blocks in the Caddyfile) that are empty.
This commit is contained in:
Matthew Holt 2020-04-02 14:20:30 -06:00
parent 3634c4593f
commit 1c190b001b
No known key found for this signature in database
GPG key ID: 2A349DD577D586A5
4 changed files with 155 additions and 101 deletions

View file

@ -106,12 +106,22 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBloc
// server block are only the ones which use the address; but // server block are only the ones which use the address; but
// the contents (tokens) are of course the same // the contents (tokens) are of course the same
for addr, keys := range addrToKeys { for addr, keys := range addrToKeys {
// parse keys so that we only have to do it once
parsedKeys := make([]Address, 0, len(keys))
for _, key := range keys {
addr, err := ParseAddress(key)
if err != nil {
return nil, fmt.Errorf("parsing key '%s': %v", key, err)
}
parsedKeys = append(parsedKeys, addr.Normalize())
}
sbmap[addr] = append(sbmap[addr], serverBlock{ sbmap[addr] = append(sbmap[addr], serverBlock{
block: caddyfile.ServerBlock{ block: caddyfile.ServerBlock{
Keys: keys, Keys: keys,
Segments: sblock.block.Segments, Segments: sblock.block.Segments,
}, },
pile: sblock.pile, pile: sblock.pile,
keys: parsedKeys,
}) })
} }
} }
@ -165,7 +175,7 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
// figure out the HTTP and HTTPS ports; either // figure out the HTTP and HTTPS ports; either
// use defaults, or override with user config // use defaults, or override with user config
httpPort, httpsPort := strconv.Itoa(certmagic.HTTPPort), strconv.Itoa(certmagic.HTTPSPort) httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort)
if hport, ok := options["http_port"]; ok { if hport, ok := options["http_port"]; ok {
httpPort = strconv.Itoa(hport.(int)) httpPort = strconv.Itoa(hport.(int))
} }

View file

@ -16,7 +16,9 @@ package httpcaddyfile
import ( import (
"encoding/json" "encoding/json"
"net"
"sort" "sort"
"strconv"
"strings" "strings"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
@ -381,12 +383,49 @@ func parseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
return buildSubroute(allResults, h.groupCounter) return buildSubroute(allResults, h.groupCounter)
} }
// serverBlock pairs a Caddyfile server block // serverBlock pairs a Caddyfile server block with
// with a "pile" of config values, keyed by class // a "pile" of config values, keyed by class name,
// name. // as well as its parsed keys for convenience.
type serverBlock struct { type serverBlock struct {
block caddyfile.ServerBlock block caddyfile.ServerBlock
pile map[string][]ConfigValue // config values obtained from directives pile map[string][]ConfigValue // config values obtained from directives
keys []Address
}
// hostsFromKeys returns a list of all the non-empty hostnames found in
// the keys of the server block sb, unless allowEmpty is true, in which
// case a key with no host (e.g. ":443") will be added to the list as an
// empty string. Otherwise, if allowEmpty is false, and if sb has a key
// that omits the hostname (i.e. is a catch-all/empty host), then the returned
// list is empty, because the server block effectively matches ALL hosts.
// The list may not be in a consistent order. If includePorts is true, then
// any non-empty, non-standard ports will be included.
func (sb serverBlock) hostsFromKeys(allowEmpty, includePorts bool) []string {
// first get each unique hostname
hostMap := make(map[string]struct{})
for _, addr := range sb.keys {
if addr.Host == "" && !allowEmpty {
// server block contains a key like ":443", i.e. the host portion
// is empty / catch-all, which means to match all hosts
return []string{}
}
if includePorts &&
addr.Port != "" &&
addr.Port != strconv.Itoa(caddyhttp.DefaultHTTPPort) &&
addr.Port != strconv.Itoa(caddyhttp.DefaultHTTPSPort) {
hostMap[net.JoinHostPort(addr.Host, addr.Port)] = struct{}{}
} else {
hostMap[addr.Host] = struct{}{}
}
}
// convert map to slice
sblockHosts := make([]string, 0, len(hostMap))
for host := range hostMap {
sblockHosts = append(sblockHosts, host)
}
return sblockHosts
} }
type ( type (

View file

@ -17,7 +17,6 @@ package httpcaddyfile
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"reflect" "reflect"
"sort" "sort"
"strconv" "strconv"
@ -320,47 +319,6 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
return serverBlocks[1:], nil return serverBlocks[1:], nil
} }
// hostsFromServerBlockKeys returns a list of all the non-empty hostnames
// found in the keys of the server block sb, unless allowEmpty is true, in
// which case a key with no host (e.g. ":443") will be added to the list as
// an empty string. Otherwise, if allowEmpty is false, and if sb has a key
// that omits the hostname (i.e. is a catch-all/empty host), then the returned
// list is empty, because the server block effectively matches ALL hosts.
// The list may not be in a consistent order. If includePorts is true, then
// any non-empty, non-standard ports will be included.
func (st *ServerType) hostsFromServerBlockKeys(sb caddyfile.ServerBlock, allowEmpty, includePorts bool) ([]string, error) {
// first get each unique hostname
hostMap := make(map[string]struct{})
for _, sblockKey := range sb.Keys {
addr, err := ParseAddress(sblockKey)
if err != nil {
return nil, fmt.Errorf("parsing server block key: %v", err)
}
addr = addr.Normalize()
if addr.Host == "" && !allowEmpty {
// server block contains a key like ":443", i.e. the host portion
// is empty / catch-all, which means to match all hosts
return []string{}, nil
}
if includePorts &&
addr.Port != "" &&
addr.Port != strconv.Itoa(caddyhttp.DefaultHTTPPort) &&
addr.Port != strconv.Itoa(caddyhttp.DefaultHTTPSPort) {
hostMap[net.JoinHostPort(addr.Host, addr.Port)] = struct{}{}
} else {
hostMap[addr.Host] = struct{}{}
}
}
// convert map to slice
sblockHosts := make([]string, 0, len(hostMap))
for host := range hostMap {
sblockHosts = append(sblockHosts, host)
}
return sblockHosts, nil
}
// serversFromPairings creates the servers for each pairing of addresses // serversFromPairings creates the servers for each pairing of addresses
// to server blocks. Each pairing is essentially a server definition. // to server blocks. Each pairing is essentially a server definition.
func (st *ServerType) serversFromPairings( func (st *ServerType) serversFromPairings(
@ -384,11 +342,10 @@ func (st *ServerType) serversFromPairings(
// descending sort by length of host then path // descending sort by length of host then path
sort.SliceStable(p.serverBlocks, func(i, j int) bool { sort.SliceStable(p.serverBlocks, func(i, j int) bool {
// TODO: we could pre-process the specificities for efficiency, // TODO: we could pre-process the specificities for efficiency,
// but I don't expect many blocks will have SO many keys... // but I don't expect many blocks will have THAT many keys...
var iLongestPath, jLongestPath string var iLongestPath, jLongestPath string
var iLongestHost, jLongestHost string var iLongestHost, jLongestHost string
for _, key := range p.serverBlocks[i].block.Keys { for _, addr := range p.serverBlocks[i].keys {
addr, _ := ParseAddress(key)
if specificity(addr.Host) > specificity(iLongestHost) { if specificity(addr.Host) > specificity(iLongestHost) {
iLongestHost = addr.Host iLongestHost = addr.Host
} }
@ -396,8 +353,7 @@ func (st *ServerType) serversFromPairings(
iLongestPath = addr.Path iLongestPath = addr.Path
} }
} }
for _, key := range p.serverBlocks[j].block.Keys { for _, addr := range p.serverBlocks[j].keys {
addr, _ := ParseAddress(key)
if specificity(addr.Host) > specificity(jLongestHost) { if specificity(addr.Host) > specificity(jLongestHost) {
jLongestHost = addr.Host jLongestHost = addr.Host
} }
@ -415,15 +371,12 @@ func (st *ServerType) serversFromPairings(
// create a subroute for each site in the server block // create a subroute for each site in the server block
for _, sblock := range p.serverBlocks { for _, sblock := range p.serverBlocks {
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock.block) matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
if err != nil { if err != nil {
return nil, fmt.Errorf("server block %v: compiling matcher sets: %v", sblock.block.Keys, err) return nil, fmt.Errorf("server block %v: compiling matcher sets: %v", sblock.block.Keys, err)
} }
hosts, err := st.hostsFromServerBlockKeys(sblock.block, false, false) hosts := sblock.hostsFromKeys(false, false)
if err != nil {
return nil, err
}
// tls: connection policies // tls: connection policies
if cpVals, ok := sblock.pile["tls.connection_policy"]; ok { if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
@ -455,12 +408,7 @@ func (st *ServerType) serversFromPairings(
// exclude any hosts that were defined explicitly with // exclude any hosts that were defined explicitly with
// "http://" in the key from automated cert management (issue #2998) // "http://" in the key from automated cert management (issue #2998)
for _, key := range sblock.block.Keys { for _, addr := range sblock.keys {
addr, err := ParseAddress(key)
if err != nil {
return nil, err
}
addr = addr.Normalize()
if addr.Scheme == "http" && addr.Host != "" { if addr.Scheme == "http" && addr.Host != "" {
if srv.AutoHTTPS == nil { if srv.AutoHTTPS == nil {
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
@ -500,16 +448,19 @@ func (st *ServerType) serversFromPairings(
LoggerNames: make(map[string]string), LoggerNames: make(map[string]string),
} }
} }
hosts, err := st.hostsFromServerBlockKeys(sblock.block, true, true) for _, h := range sblock.hostsFromKeys(true, true) {
if err != nil {
return nil, err
}
for _, h := range hosts {
srv.Logs.LoggerNames[h] = ncl.name srv.Logs.LoggerNames[h] = ncl.name
} }
} }
} }
// a server cannot (natively) serve both HTTP and HTTPS at the
// same time, so make sure the configuration isn't in conflict
err := detectConflictingSchemes(srv, p.serverBlocks, options)
if err != nil {
return nil, err
}
// a catch-all TLS conn policy is necessary to ensure TLS can // a catch-all TLS conn policy is necessary to ensure TLS can
// be offered to all hostnames of the server; even though only // be offered to all hostnames of the server; even though only
// one policy is needed to enable TLS for the server, that // one policy is needed to enable TLS for the server, that
@ -527,7 +478,6 @@ func (st *ServerType) serversFromPairings(
} }
// tidy things up a bit // tidy things up a bit
var err error
srv.TLSConnPolicies, err = consolidateConnPolicies(srv.TLSConnPolicies) srv.TLSConnPolicies, err = consolidateConnPolicies(srv.TLSConnPolicies)
if err != nil { if err != nil {
return nil, fmt.Errorf("consolidating TLS connection policies for server %d: %v", i, err) return nil, fmt.Errorf("consolidating TLS connection policies for server %d: %v", i, err)
@ -540,6 +490,64 @@ func (st *ServerType) serversFromPairings(
return servers, nil return servers, nil
} }
func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock, options map[string]interface{}) error {
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp)
}
httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
if hsp, ok := options["https_port"].(int); ok {
httpsPort = strconv.Itoa(hsp)
}
var httpOrHTTPS string
checkAndSetHTTP := func(addr Address) error {
if httpOrHTTPS == "HTTPS" {
errMsg := fmt.Errorf("server listening on %v is configured for HTTPS and cannot natively multiplex HTTP and HTTPS: %s",
srv.Listen, addr.Original)
if addr.Scheme == "" && addr.Host == "" {
errMsg = fmt.Errorf("%s (try specifying https:// in the address)", errMsg)
}
return errMsg
}
if len(srv.TLSConnPolicies) > 0 {
// any connection policies created for an HTTP server
// is a logical conflict, as it would enable HTTPS
return fmt.Errorf("server listening on %v is HTTP, but attempts to configure TLS connection policies", srv.Listen)
}
httpOrHTTPS = "HTTP"
return nil
}
checkAndSetHTTPS := func(addr Address) error {
if httpOrHTTPS == "HTTP" {
return fmt.Errorf("server listening on %v is configured for HTTP and cannot natively multiplex HTTP and HTTPS: %s",
srv.Listen, addr.Original)
}
httpOrHTTPS = "HTTPS"
return nil
}
for _, sblock := range serverBlocks {
for _, addr := range sblock.keys {
if addr.Scheme == "http" || addr.Port == httpPort {
if err := checkAndSetHTTP(addr); err != nil {
return err
}
} else if addr.Scheme == "https" || addr.Port == httpsPort {
if err := checkAndSetHTTPS(addr); err != nil {
return err
}
} else if addr.Host == "" {
if err := checkAndSetHTTP(addr); err != nil {
return err
}
}
}
}
return nil
}
// consolidateConnPolicies combines TLS connection policies that are the same, // consolidateConnPolicies combines TLS connection policies that are the same,
// for a cleaner overall output. // for a cleaner overall output.
func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.ConnectionPolicies, error) { func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.ConnectionPolicies, error) {
@ -664,18 +672,34 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
matcherSetsEnc []caddy.ModuleMap, matcherSetsEnc []caddy.ModuleMap,
p sbAddrAssociation, p sbAddrAssociation,
warnings *[]caddyconfig.Warning) caddyhttp.RouteList { warnings *[]caddyconfig.Warning) caddyhttp.RouteList {
// nothing to do if... there's nothing to do
if len(matcherSetsEnc) == 0 && len(subroute.Routes) == 0 && subroute.Errors == nil {
return routeList
}
if len(matcherSetsEnc) == 0 && len(p.serverBlocks) == 1 { if len(matcherSetsEnc) == 0 && len(p.serverBlocks) == 1 {
// no need to wrap the handlers in a subroute if this is // no need to wrap the handlers in a subroute if this is
// the only server block and there is no matcher for it // the only server block and there is no matcher for it
routeList = append(routeList, subroute.Routes...) routeList = append(routeList, subroute.Routes...)
} else { } else {
routeList = append(routeList, caddyhttp.Route{ route := caddyhttp.Route{
MatcherSetsRaw: matcherSetsEnc, // the semantics of a site block in the Caddyfile dictate
HandlersRaw: []json.RawMessage{ // that only the first matching one is evaluated, since
// site blocks do not cascade nor inherit
Terminal: true,
}
if len(matcherSetsEnc) > 0 {
route.MatcherSetsRaw = matcherSetsEnc
}
if len(subroute.Routes) > 0 || subroute.Errors != nil {
route.HandlersRaw = []json.RawMessage{
caddyconfig.JSONModuleObject(subroute, "handler", "subroute", warnings), caddyconfig.JSONModuleObject(subroute, "handler", "subroute", warnings),
}, }
Terminal: true, // only first matching site block should be evaluated }
}) if len(route.MatcherSetsRaw) > 0 || len(route.HandlersRaw) > 0 {
routeList = append(routeList, route)
}
} }
return routeList return routeList
} }
@ -822,7 +846,7 @@ func matcherSetFromMatcherToken(
return nil, false, nil return nil, false, nil
} }
func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([]caddy.ModuleMap, error) { func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.ModuleMap, error) {
type hostPathPair struct { type hostPathPair struct {
hostm caddyhttp.MatchHost hostm caddyhttp.MatchHost
pathm caddyhttp.MatchPath pathm caddyhttp.MatchPath
@ -832,13 +856,7 @@ func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([
var matcherPairs []*hostPathPair var matcherPairs []*hostPathPair
var catchAllHosts bool var catchAllHosts bool
for _, key := range sblock.Keys { for _, addr := range sblock.keys {
addr, err := ParseAddress(key)
if err != nil {
return nil, fmt.Errorf("server block %v: parsing and standardizing address '%s': %v", sblock.Keys, key, err)
}
addr = addr.Normalize()
// choose a matcher pair that should be shared by this // choose a matcher pair that should be shared by this
// server block; if none exists yet, create one // server block; if none exists yet, create one
var chosenMatcherPair *hostPathPair var chosenMatcherPair *hostPathPair
@ -905,7 +923,7 @@ func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([
for _, ms := range matcherSets { for _, ms := range matcherSets {
msEncoded, err := encodeMatcherSet(ms) msEncoded, err := encodeMatcherSet(ms)
if err != nil { if err != nil {
return nil, fmt.Errorf("server block %v: %v", sblock.Keys, err) return nil, fmt.Errorf("server block %v: %v", sblock.block.Keys, err)
} }
matcherSetsEnc = append(matcherSetsEnc, msEncoded) matcherSetsEnc = append(matcherSetsEnc, msEncoded)
} }

View file

@ -43,26 +43,16 @@ func (st ServerType) buildTLSApp(
hostsSharedWithHostlessKey := make(map[string]struct{}) hostsSharedWithHostlessKey := make(map[string]struct{})
for _, pair := range pairings { for _, pair := range pairings {
for _, sb := range pair.serverBlocks { for _, sb := range pair.serverBlocks {
for _, key := range sb.block.Keys { for _, addr := range sb.keys {
addr, err := ParseAddress(key)
if err != nil {
return nil, warnings, err
}
addr = addr.Normalize()
if addr.Host == "" { if addr.Host == "" {
serverBlocksWithHostlessKey++ serverBlocksWithHostlessKey++
// this server block has a hostless key, now // this server block has a hostless key, now
// go through and add all the hosts to the set // go through and add all the hosts to the set
for _, otherKey := range sb.block.Keys { for _, otherAddr := range sb.keys {
if otherKey == key { if otherAddr.Original == addr.Original {
continue continue
} }
addr, err := ParseAddress(otherKey) if otherAddr.Host != "" {
if err != nil {
return nil, warnings, err
}
addr = addr.Normalize()
if addr.Host != "" {
hostsSharedWithHostlessKey[addr.Host] = struct{}{} hostsSharedWithHostlessKey[addr.Host] = struct{}{}
} }
} }
@ -82,10 +72,7 @@ func (st ServerType) buildTLSApp(
// get values that populate an automation policy for this block // get values that populate an automation policy for this block
var ap *caddytls.AutomationPolicy var ap *caddytls.AutomationPolicy
sblockHosts, err := st.hostsFromServerBlockKeys(sblock.block, false, false) sblockHosts := sblock.hostsFromKeys(false, false)
if err != nil {
return nil, warnings, err
}
if len(sblockHosts) == 0 { if len(sblockHosts) == 0 {
ap = catchAllAP ap = catchAllAP
} }