summaryrefslogtreecommitdiff
path: root/caddyconfig/httpcaddyfile/addresses.go
diff options
context:
space:
mode:
Diffstat (limited to 'caddyconfig/httpcaddyfile/addresses.go')
-rw-r--r--caddyconfig/httpcaddyfile/addresses.go350
1 files changed, 350 insertions, 0 deletions
diff --git a/caddyconfig/httpcaddyfile/addresses.go b/caddyconfig/httpcaddyfile/addresses.go
new file mode 100644
index 0000000..6ecee26
--- /dev/null
+++ b/caddyconfig/httpcaddyfile/addresses.go
@@ -0,0 +1,350 @@
+// 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/url"
+ "reflect"
+ "strconv"
+ "strings"
+
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
+ "github.com/mholt/certmagic"
+)
+
+// 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
+// beceause 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) mapAddressToServerBlocks(originalServerBlocks []caddyfile.ServerBlock) (map[string][]caddyfile.ServerBlock, error) {
+ sbmap := make(map[string][]caddyfile.ServerBlock)
+
+ 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
+ addrToKeys := make(map[string][]string)
+ for j, key := range sblock.Keys {
+ // 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)
+ addrs, err := st.listenerAddrsForServerBlockKey(sblock, key)
+ if err != nil {
+ return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key, err)
+ }
+
+ // associate this key with each listener address it is served on
+ for _, addr := range addrs {
+ addrToKeys[addr] = append(addrToKeys[addr], key)
+ }
+ }
+
+ // 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, keys := range addrToKeys {
+ sbmap[addr] = append(sbmap[addr], caddyfile.ServerBlock{
+ Keys: keys,
+ Tokens: sblock.Tokens,
+ })
+ }
+ }
+
+ return sbmap, nil
+}
+
+// consolidateAddrMappings eliminates repetition of identical server blocks in a mapping of
+// single listener addresses to lists of server blocks. Since multiple addresses may serve
+// 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(addrToServerBlocks map[string][]caddyfile.ServerBlock) []sbAddrAssociation {
+ var sbaddrs []sbAddrAssociation
+ for addr, sblocks := range addrToServerBlocks {
+ // we start with knowing that at least this address
+ // maps to these server blocks
+ a := sbAddrAssociation{
+ addresses: []string{addr},
+ serverBlocks: sblocks,
+ }
+
+ // now find other addresses that map to identical
+ // server blocks and add them to our list of
+ // addresses, while removing them from the map
+ for otherAddr, otherSblocks := range addrToServerBlocks {
+ if addr == otherAddr {
+ continue
+ }
+ if reflect.DeepEqual(sblocks, otherSblocks) {
+ a.addresses = append(a.addresses, otherAddr)
+ delete(addrToServerBlocks, otherAddr)
+ }
+ }
+
+ sbaddrs = append(sbaddrs, a)
+ }
+ return sbaddrs
+}
+
+func (st *ServerType) listenerAddrsForServerBlockKey(sblock caddyfile.ServerBlock, key string) ([]string, error) {
+ addr, err := standardizeAddress(key)
+ if err != nil {
+ return nil, fmt.Errorf("parsing key: %v", err)
+ }
+
+ lnPort := defaultPort
+ if addr.Port != "" {
+ // port explicitly defined
+ lnPort = addr.Port
+ } else if certmagic.HostQualifies(addr.Host) {
+ // automatic HTTPS
+ lnPort = strconv.Itoa(certmagic.HTTPSPort)
+ }
+
+ // the bind directive specifies hosts, but is optional
+ var lnHosts []string
+ for i, token := range sblock.Tokens["bind"] {
+ if i == 0 {
+ continue
+ }
+ lnHosts = append(lnHosts, token.Text)
+ }
+ if len(lnHosts) == 0 {
+ lnHosts = []string{""}
+ }
+
+ // use a map to prevent duplication
+ listeners := make(map[string]struct{})
+ for _, host := range lnHosts {
+ listeners[net.JoinHostPort(host, lnPort)] = struct{}{}
+ }
+
+ // now turn map into list
+ var listenersList []string
+ for lnStr := range listeners {
+ listenersList = append(listenersList, lnStr)
+ }
+ // sort.Strings(listenersList) // TODO: is sorting necessary?
+
+ return listenersList, nil
+}
+
+// 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
+}
+
+// String returns a human-friendly print of the address.
+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
+}
+
+// VHost returns a sensible concatenation of Host:Port/Path from a.
+// It's basically the a.Original but without the scheme.
+func (a Address) VHost() string {
+ if idx := strings.Index(a.Original, "://"); idx > -1 {
+ return a.Original[idx+3:]
+ }
+ return a.Original
+}
+
+// Normalize normalizes URL: turn scheme and host names into lower case
+func (a Address) Normalize() Address {
+ path := a.Path
+ if !caseSensitivePath {
+ path = strings.ToLower(path)
+ }
+
+ // ensure host is normalized if it's an IP address
+ host := a.Host
+ if ip := net.ParseIP(host); ip != nil {
+ host = ip.String()
+ }
+
+ return Address{
+ Original: a.Original,
+ Scheme: strings.ToLower(a.Scheme),
+ Host: strings.ToLower(host),
+ Port: a.Port,
+ Path: path,
+ }
+}
+
+// Key is similar to String, just replaces scheme and host values with modified values.
+// Unlike String it doesn't add anything default (scheme, port, etc)
+func (a Address) Key() string {
+ res := ""
+ if a.Scheme != "" {
+ res += a.Scheme + "://"
+ }
+ if a.Host != "" {
+ res += a.Host
+ }
+ if a.Port != "" {
+ if strings.HasPrefix(a.Original[len(res):], ":"+a.Port) {
+ // insert port only if the original has its own explicit port
+ res += ":" + a.Port
+ }
+ }
+ if a.Path != "" {
+ res += a.Path
+ }
+ return res
+}
+
+// standardizeAddress parses an address string into a structured format with separate
+// scheme, host, port, and path portions, as well as the original input string.
+func standardizeAddress(str string) (Address, error) {
+ httpPort, httpsPort := strconv.Itoa(certmagic.HTTPPort), strconv.Itoa(certmagic.HTTPSPort)
+
+ input := str
+
+ // Split input into components (prepend with // to assert host by default)
+ if !strings.Contains(str, "//") && !strings.HasPrefix(str, "/") {
+ str = "//" + str
+ }
+ u, err := url.Parse(str)
+ if err != nil {
+ return Address{}, err
+ }
+
+ // separate host and port
+ host, port, err := net.SplitHostPort(u.Host)
+ if err != nil {
+ host, port, err = net.SplitHostPort(u.Host + ":")
+ if err != nil {
+ host = u.Host
+ }
+ }
+
+ // see if we can set port based off scheme
+ if port == "" {
+ if u.Scheme == "http" {
+ port = httpPort
+ } else if u.Scheme == "https" {
+ port = httpsPort
+ }
+ }
+
+ // repeated or conflicting scheme is confusing, so error
+ if u.Scheme != "" && (port == "http" || port == "https") {
+ return Address{}, fmt.Errorf("[%s] scheme specified twice in address", input)
+ }
+
+ // error if scheme and port combination violate convention
+ if (u.Scheme == "http" && port == httpsPort) || (u.Scheme == "https" && port == httpPort) {
+ return Address{}, fmt.Errorf("[%s] scheme and port violate convention", input)
+ }
+
+ // standardize http and https ports to their respective port numbers
+ if port == "http" {
+ u.Scheme = "http"
+ port = httpPort
+ } else if port == "https" {
+ u.Scheme = "https"
+ port = httpsPort
+ }
+
+ return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err
+}
+
+const (
+ defaultPort = "2015"
+ caseSensitivePath = false
+)