mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-27 06:03:48 +03:00
4b1a9b6cc1
* caddy adapt for listen_protocols * adapt listen_socket * allow multiple listen sockets for port ranges and readd socket fd listen logic * readd logic to start servers according to listener protocols * gofmt * adapt caddytest * gosec * fmt and rename listen to listenWithSocket * fmt and rename listen to listenWithSocket * more consistent error msg * non unix listenReusableWithSocketFile * remove unused func * doc comment typo * nonosec * commit * doc comments * more doc comments * comment was misleading, cardinality did not change * addressesWithProtocols * update test * fd/ and fdgram/ * rm addr * actually write... * i guess we doin' "skip": now * wrong var in placeholder * wrong var in placeholder II * update param name in comment * dont save nil file pointers * windows * key -> parsedKey * osx * multiple default_bind with protocols * check for h1 and h2 listener netw
497 lines
17 KiB
Go
497 lines
17 KiB
Go
// 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 (
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/caddyserver/certmagic"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
)
|
|
|
|
// mapAddressToServerBlocks returns a map of listener address to list of server
|
|
// blocks that will be served on that address. To do this, each server block is
|
|
// expanded so that each one is considered individually, although keys of a
|
|
// server block that share the same address stay grouped together so the config
|
|
// isn't repeated unnecessarily. For example, this Caddyfile:
|
|
//
|
|
// example.com {
|
|
// bind 127.0.0.1
|
|
// }
|
|
// www.example.com, example.net/path, localhost:9999 {
|
|
// bind 127.0.0.1 1.2.3.4
|
|
// }
|
|
//
|
|
// has two server blocks to start with. But expressed in this Caddyfile are
|
|
// actually 4 listener addresses: 127.0.0.1:443, 1.2.3.4:443, 127.0.0.1:9999,
|
|
// and 127.0.0.1:9999. This is because the bind directive is applied to each
|
|
// key of its server block (specifying the host part), and each key may have
|
|
// a different port. And we definitely need to be sure that a site which is
|
|
// bound to be served on a specific interface is not served on others just
|
|
// because that is more convenient: it would be a potential security risk
|
|
// if the difference between interfaces means private vs. public.
|
|
//
|
|
// So what this function does for the example above is iterate each server
|
|
// block, and for each server block, iterate its keys. For the first, it
|
|
// finds one key (example.com) and determines its listener address
|
|
// (127.0.0.1:443 - because of 'bind' and automatic HTTPS). It then adds
|
|
// the listener address to the map value returned by this function, with
|
|
// the first server block as one of its associations.
|
|
//
|
|
// It then iterates each key on the second server block and associates them
|
|
// with one or more listener addresses. Indeed, each key in this block has
|
|
// two listener addresses because of the 'bind' directive. Once we know
|
|
// which addresses serve which keys, we can create a new server block for
|
|
// each address containing the contents of the server block and only those
|
|
// specific keys of the server block which use that address.
|
|
//
|
|
// It is possible and even likely that some keys in the returned map have
|
|
// the exact same list of server blocks (i.e. they are identical). This
|
|
// happens when multiple hosts are declared with a 'bind' directive and
|
|
// the resulting listener addresses are not shared by any other server
|
|
// block (or the other server blocks are exactly identical in their token
|
|
// contents). This happens with our example above because 1.2.3.4:443
|
|
// and 1.2.3.4:9999 are used exclusively with the second server block. This
|
|
// repetition may be undesirable, so call consolidateAddrMappings() to map
|
|
// multiple addresses to the same lists of server blocks (a many:many mapping).
|
|
// (Doing this is essentially a map-reduce technique.)
|
|
func (st *ServerType) mapAddressToProtocolToServerBlocks(originalServerBlocks []serverBlock,
|
|
options map[string]any,
|
|
) (map[string]map[string][]serverBlock, error) {
|
|
addrToProtocolToServerBlocks := map[string]map[string][]serverBlock{}
|
|
|
|
type keyWithParsedKey struct {
|
|
key caddyfile.Token
|
|
parsedKey Address
|
|
}
|
|
|
|
for i, sblock := range originalServerBlocks {
|
|
// within a server block, we need to map all the listener addresses
|
|
// implied by the server block to the keys of the server block which
|
|
// will be served by them; this has the effect of treating each
|
|
// key of a server block as its own, but without having to repeat its
|
|
// contents in cases where multiple keys really can be served together
|
|
addrToProtocolToKeyWithParsedKeys := map[string]map[string][]keyWithParsedKey{}
|
|
for j, key := range sblock.block.Keys {
|
|
parsedKey, err := ParseAddress(key.Text)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing key: %v", err)
|
|
}
|
|
parsedKey = parsedKey.Normalize()
|
|
|
|
// a key can have multiple listener addresses if there are multiple
|
|
// arguments to the 'bind' directive (although they will all have
|
|
// the same port, since the port is defined by the key or is implicit
|
|
// through automatic HTTPS)
|
|
listeners, err := st.listenersForServerBlockAddress(sblock, parsedKey, options)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key.Text, err)
|
|
}
|
|
|
|
// associate this key with its protocols and each listener address served with them
|
|
kwpk := keyWithParsedKey{key, parsedKey}
|
|
for addr, protocols := range listeners {
|
|
protocolToKeyWithParsedKeys, ok := addrToProtocolToKeyWithParsedKeys[addr]
|
|
if !ok {
|
|
protocolToKeyWithParsedKeys = map[string][]keyWithParsedKey{}
|
|
addrToProtocolToKeyWithParsedKeys[addr] = protocolToKeyWithParsedKeys
|
|
}
|
|
|
|
// an empty protocol indicates the default, a nil or empty value in the ListenProtocols array
|
|
if len(protocols) == 0 {
|
|
protocols[""] = struct{}{}
|
|
}
|
|
for prot := range protocols {
|
|
protocolToKeyWithParsedKeys[prot] = append(
|
|
protocolToKeyWithParsedKeys[prot],
|
|
kwpk)
|
|
}
|
|
}
|
|
}
|
|
|
|
// make a slice of the map keys so we can iterate in sorted order
|
|
addrs := make([]string, 0, len(addrToProtocolToKeyWithParsedKeys))
|
|
for addr := range addrToProtocolToKeyWithParsedKeys {
|
|
addrs = append(addrs, addr)
|
|
}
|
|
sort.Strings(addrs)
|
|
|
|
// now that we know which addresses serve which keys of this
|
|
// server block, we iterate that mapping and create a list of
|
|
// new server blocks for each address where the keys of the
|
|
// server block are only the ones which use the address; but
|
|
// the contents (tokens) are of course the same
|
|
for _, addr := range addrs {
|
|
protocolToKeyWithParsedKeys := addrToProtocolToKeyWithParsedKeys[addr]
|
|
|
|
prots := make([]string, 0, len(protocolToKeyWithParsedKeys))
|
|
for prot := range protocolToKeyWithParsedKeys {
|
|
prots = append(prots, prot)
|
|
}
|
|
sort.Strings(prots)
|
|
|
|
protocolToServerBlocks, ok := addrToProtocolToServerBlocks[addr]
|
|
if !ok {
|
|
protocolToServerBlocks = map[string][]serverBlock{}
|
|
addrToProtocolToServerBlocks[addr] = protocolToServerBlocks
|
|
}
|
|
|
|
for _, prot := range prots {
|
|
keyWithParsedKeys := protocolToKeyWithParsedKeys[prot]
|
|
|
|
keys := make([]caddyfile.Token, len(keyWithParsedKeys))
|
|
parsedKeys := make([]Address, len(keyWithParsedKeys))
|
|
|
|
for k, keyWithParsedKey := range keyWithParsedKeys {
|
|
keys[k] = keyWithParsedKey.key
|
|
parsedKeys[k] = keyWithParsedKey.parsedKey
|
|
}
|
|
|
|
protocolToServerBlocks[prot] = append(protocolToServerBlocks[prot], serverBlock{
|
|
block: caddyfile.ServerBlock{
|
|
Keys: keys,
|
|
Segments: sblock.block.Segments,
|
|
},
|
|
pile: sblock.pile,
|
|
parsedKeys: parsedKeys,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return addrToProtocolToServerBlocks, nil
|
|
}
|
|
|
|
// consolidateAddrMappings eliminates repetition of identical server blocks in a mapping of
|
|
// single listener addresses to protocols to lists of server blocks. Since multiple addresses
|
|
// may serve multiple protocols to identical sites (server block contents), this function turns
|
|
// a 1:many mapping into a many:many mapping. Server block contents (tokens) must be
|
|
// exactly identical so that reflect.DeepEqual returns true in order for the addresses to be combined.
|
|
// Identical entries are deleted from the addrToServerBlocks map. Essentially, each pairing (each
|
|
// association from multiple addresses to multiple server blocks; i.e. each element of
|
|
// the returned slice) becomes a server definition in the output JSON.
|
|
func (st *ServerType) consolidateAddrMappings(addrToProtocolToServerBlocks map[string]map[string][]serverBlock) []sbAddrAssociation {
|
|
sbaddrs := make([]sbAddrAssociation, 0, len(addrToProtocolToServerBlocks))
|
|
|
|
addrs := make([]string, 0, len(addrToProtocolToServerBlocks))
|
|
for addr := range addrToProtocolToServerBlocks {
|
|
addrs = append(addrs, addr)
|
|
}
|
|
sort.Strings(addrs)
|
|
|
|
for _, addr := range addrs {
|
|
protocolToServerBlocks := addrToProtocolToServerBlocks[addr]
|
|
|
|
prots := make([]string, 0, len(protocolToServerBlocks))
|
|
for prot := range protocolToServerBlocks {
|
|
prots = append(prots, prot)
|
|
}
|
|
sort.Strings(prots)
|
|
|
|
for _, prot := range prots {
|
|
serverBlocks := protocolToServerBlocks[prot]
|
|
|
|
// now find other addresses that map to identical
|
|
// server blocks and add them to our map of listener
|
|
// addresses and protocols, while removing them from
|
|
// the original map
|
|
listeners := map[string]map[string]struct{}{}
|
|
|
|
for otherAddr, otherProtocolToServerBlocks := range addrToProtocolToServerBlocks {
|
|
for otherProt, otherServerBlocks := range otherProtocolToServerBlocks {
|
|
if addr == otherAddr && prot == otherProt || reflect.DeepEqual(serverBlocks, otherServerBlocks) {
|
|
listener, ok := listeners[otherAddr]
|
|
if !ok {
|
|
listener = map[string]struct{}{}
|
|
listeners[otherAddr] = listener
|
|
}
|
|
listener[otherProt] = struct{}{}
|
|
delete(otherProtocolToServerBlocks, otherProt)
|
|
}
|
|
}
|
|
}
|
|
|
|
addresses := make([]string, 0, len(listeners))
|
|
for lnAddr := range listeners {
|
|
addresses = append(addresses, lnAddr)
|
|
}
|
|
sort.Strings(addresses)
|
|
|
|
addressesWithProtocols := make([]addressWithProtocols, 0, len(listeners))
|
|
|
|
for _, lnAddr := range addresses {
|
|
lnProts := listeners[lnAddr]
|
|
prots := make([]string, 0, len(lnProts))
|
|
for prot := range lnProts {
|
|
prots = append(prots, prot)
|
|
}
|
|
sort.Strings(prots)
|
|
|
|
addressesWithProtocols = append(addressesWithProtocols, addressWithProtocols{
|
|
address: lnAddr,
|
|
protocols: prots,
|
|
})
|
|
}
|
|
|
|
sbaddrs = append(sbaddrs, sbAddrAssociation{
|
|
addressesWithProtocols: addressesWithProtocols,
|
|
serverBlocks: serverBlocks,
|
|
})
|
|
}
|
|
}
|
|
|
|
return sbaddrs
|
|
}
|
|
|
|
// listenersForServerBlockAddress essentially converts the Caddyfile site addresses to a map from
|
|
// Caddy listener addresses and the protocols to serve them with to the parsed address for each server block.
|
|
func (st *ServerType) listenersForServerBlockAddress(sblock serverBlock, addr Address,
|
|
options map[string]any,
|
|
) (map[string]map[string]struct{}, error) {
|
|
switch addr.Scheme {
|
|
case "wss":
|
|
return nil, fmt.Errorf("the scheme wss:// is only supported in browsers; use https:// instead")
|
|
case "ws":
|
|
return nil, fmt.Errorf("the scheme ws:// is only supported in browsers; use http:// instead")
|
|
case "https", "http", "":
|
|
// Do nothing or handle the valid schemes
|
|
default:
|
|
return nil, fmt.Errorf("unsupported URL scheme %s://", addr.Scheme)
|
|
}
|
|
|
|
// figure out the HTTP and HTTPS ports; either
|
|
// use defaults, or override with user config
|
|
httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort)
|
|
if hport, ok := options["http_port"]; ok {
|
|
httpPort = strconv.Itoa(hport.(int))
|
|
}
|
|
if hsport, ok := options["https_port"]; ok {
|
|
httpsPort = strconv.Itoa(hsport.(int))
|
|
}
|
|
|
|
// default port is the HTTPS port
|
|
lnPort := httpsPort
|
|
if addr.Port != "" {
|
|
// port explicitly defined
|
|
lnPort = addr.Port
|
|
} else if addr.Scheme == "http" {
|
|
// port inferred from scheme
|
|
lnPort = httpPort
|
|
}
|
|
|
|
// error if scheme and port combination violate convention
|
|
if (addr.Scheme == "http" && lnPort == httpsPort) || (addr.Scheme == "https" && lnPort == httpPort) {
|
|
return nil, fmt.Errorf("[%s] scheme and port violate convention", addr.String())
|
|
}
|
|
|
|
// the bind directive specifies hosts (and potentially network), and the protocols to serve them with, but is optional
|
|
lnCfgVals := make([]addressesWithProtocols, 0, len(sblock.pile["bind"]))
|
|
for _, cfgVal := range sblock.pile["bind"] {
|
|
if val, ok := cfgVal.Value.(addressesWithProtocols); ok {
|
|
lnCfgVals = append(lnCfgVals, val)
|
|
}
|
|
}
|
|
if len(lnCfgVals) == 0 {
|
|
if defaultBindValues, ok := options["default_bind"].([]ConfigValue); ok {
|
|
for _, defaultBindValue := range defaultBindValues {
|
|
lnCfgVals = append(lnCfgVals, defaultBindValue.Value.(addressesWithProtocols))
|
|
}
|
|
} else {
|
|
lnCfgVals = []addressesWithProtocols{{
|
|
addresses: []string{""},
|
|
protocols: nil,
|
|
}}
|
|
}
|
|
}
|
|
|
|
// use a map to prevent duplication
|
|
listeners := map[string]map[string]struct{}{}
|
|
for _, lnCfgVal := range lnCfgVals {
|
|
for _, lnHost := range lnCfgVal.addresses {
|
|
networkAddr, err := caddy.ParseNetworkAddressFromHostPort(lnHost, lnPort)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing network address: %v", err)
|
|
}
|
|
if _, ok := listeners[addr.String()]; !ok {
|
|
listeners[networkAddr.String()] = map[string]struct{}{}
|
|
}
|
|
for _, protocol := range lnCfgVal.protocols {
|
|
listeners[networkAddr.String()][protocol] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
return listeners, nil
|
|
}
|
|
|
|
// addressesWithProtocols associates a list of listen addresses
|
|
// with a list of protocols to serve them with
|
|
type addressesWithProtocols struct {
|
|
addresses []string
|
|
protocols []string
|
|
}
|
|
|
|
// Address represents a site address. It contains
|
|
// the original input value, and the component
|
|
// parts of an address. The component parts may be
|
|
// updated to the correct values as setup proceeds,
|
|
// but the original value should never be changed.
|
|
//
|
|
// The Host field must be in a normalized form.
|
|
type Address struct {
|
|
Original, Scheme, Host, Port, Path string
|
|
}
|
|
|
|
// ParseAddress parses an address string into a structured format with separate
|
|
// scheme, host, port, and path portions, as well as the original input string.
|
|
func ParseAddress(str string) (Address, error) {
|
|
const maxLen = 4096
|
|
if len(str) > maxLen {
|
|
str = str[:maxLen]
|
|
}
|
|
remaining := strings.TrimSpace(str)
|
|
a := Address{Original: remaining}
|
|
|
|
// extract scheme
|
|
splitScheme := strings.SplitN(remaining, "://", 2)
|
|
switch len(splitScheme) {
|
|
case 0:
|
|
return a, nil
|
|
case 1:
|
|
remaining = splitScheme[0]
|
|
case 2:
|
|
a.Scheme = splitScheme[0]
|
|
remaining = splitScheme[1]
|
|
}
|
|
|
|
// extract host and port
|
|
hostSplit := strings.SplitN(remaining, "/", 2)
|
|
if len(hostSplit) > 0 {
|
|
host, port, err := net.SplitHostPort(hostSplit[0])
|
|
if err != nil {
|
|
host, port, err = net.SplitHostPort(hostSplit[0] + ":")
|
|
if err != nil {
|
|
host = hostSplit[0]
|
|
}
|
|
}
|
|
a.Host = host
|
|
a.Port = port
|
|
}
|
|
if len(hostSplit) == 2 {
|
|
// all that remains is the path
|
|
a.Path = "/" + hostSplit[1]
|
|
}
|
|
|
|
// make sure port is valid
|
|
if a.Port != "" {
|
|
if portNum, err := strconv.Atoi(a.Port); err != nil {
|
|
return Address{}, fmt.Errorf("invalid port '%s': %v", a.Port, err)
|
|
} else if portNum < 0 || portNum > 65535 {
|
|
return Address{}, fmt.Errorf("port %d is out of range", portNum)
|
|
}
|
|
}
|
|
|
|
return a, nil
|
|
}
|
|
|
|
// String returns a human-readable form of a. It will
|
|
// be a cleaned-up and filled-out URL string.
|
|
func (a Address) String() string {
|
|
if a.Host == "" && a.Port == "" {
|
|
return ""
|
|
}
|
|
scheme := a.Scheme
|
|
if scheme == "" {
|
|
if a.Port == strconv.Itoa(certmagic.HTTPSPort) {
|
|
scheme = "https"
|
|
} else {
|
|
scheme = "http"
|
|
}
|
|
}
|
|
s := scheme
|
|
if s != "" {
|
|
s += "://"
|
|
}
|
|
if a.Port != "" &&
|
|
((scheme == "https" && a.Port != strconv.Itoa(caddyhttp.DefaultHTTPSPort)) ||
|
|
(scheme == "http" && a.Port != strconv.Itoa(caddyhttp.DefaultHTTPPort))) {
|
|
s += net.JoinHostPort(a.Host, a.Port)
|
|
} else {
|
|
s += a.Host
|
|
}
|
|
if a.Path != "" {
|
|
s += a.Path
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Normalize returns a normalized version of a.
|
|
func (a Address) Normalize() Address {
|
|
path := a.Path
|
|
|
|
// ensure host is normalized if it's an IP address
|
|
host := strings.TrimSpace(a.Host)
|
|
if ip, err := netip.ParseAddr(host); err == nil {
|
|
if ip.Is6() && !ip.Is4() && !ip.Is4In6() {
|
|
host = ip.String()
|
|
}
|
|
}
|
|
|
|
return Address{
|
|
Original: a.Original,
|
|
Scheme: lowerExceptPlaceholders(a.Scheme),
|
|
Host: lowerExceptPlaceholders(host),
|
|
Port: a.Port,
|
|
Path: path,
|
|
}
|
|
}
|
|
|
|
// lowerExceptPlaceholders lowercases s except within
|
|
// placeholders (substrings in non-escaped '{ }' spans).
|
|
// See https://github.com/caddyserver/caddy/issues/3264
|
|
func lowerExceptPlaceholders(s string) string {
|
|
var sb strings.Builder
|
|
var escaped, inPlaceholder bool
|
|
for _, ch := range s {
|
|
if ch == '\\' && !escaped {
|
|
escaped = true
|
|
sb.WriteRune(ch)
|
|
continue
|
|
}
|
|
if ch == '{' && !escaped {
|
|
inPlaceholder = true
|
|
}
|
|
if ch == '}' && inPlaceholder && !escaped {
|
|
inPlaceholder = false
|
|
}
|
|
if inPlaceholder {
|
|
sb.WriteRune(ch)
|
|
} else {
|
|
sb.WriteRune(unicode.ToLower(ch))
|
|
}
|
|
escaped = false
|
|
}
|
|
return sb.String()
|
|
}
|