diff options
author | Matthew Holt <mholt@users.noreply.github.com> | 2019-08-09 12:05:47 -0600 |
---|---|---|
committer | Matthew Holt <mholt@users.noreply.github.com> | 2019-08-09 12:05:47 -0600 |
commit | ab885f07b844fd60adb9d49ed7884f3cd2d939a7 (patch) | |
tree | 8827ad88cf3da8982154e2fda46f53274342785d /caddyconfig/httpcaddyfile | |
parent | 4950ce485f7d931890fcfd2ee287b6df1b5db435 (diff) |
Implement config adapters and beginning of Caddyfile adapter
Along with several other changes, such as renaming caddyhttp.ServerRoute
to caddyhttp.Route, exporting some types that were not exported before,
and tweaking the caddytls TLS values to be more consistent.
Notably, we also now disable automatic cert management for names which
already have a cert (manually) loaded into the cache. These names no
longer need to be specified in the "skip_certificates" field of the
automatic HTTPS config, because they will be skipped automatically.
Diffstat (limited to 'caddyconfig/httpcaddyfile')
-rw-r--r-- | caddyconfig/httpcaddyfile/addresses.go | 350 | ||||
-rw-r--r-- | caddyconfig/httpcaddyfile/addresses_test.go | 129 | ||||
-rw-r--r-- | caddyconfig/httpcaddyfile/builtins.go | 257 | ||||
-rw-r--r-- | caddyconfig/httpcaddyfile/handlers.go | 92 | ||||
-rw-r--r-- | caddyconfig/httpcaddyfile/httptype.go | 542 |
5 files changed, 1370 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 +) diff --git a/caddyconfig/httpcaddyfile/addresses_test.go b/caddyconfig/httpcaddyfile/addresses_test.go new file mode 100644 index 0000000..7e03d29 --- /dev/null +++ b/caddyconfig/httpcaddyfile/addresses_test.go @@ -0,0 +1,129 @@ +// 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 "testing" + +func TestStandardizeAddress(t *testing.T) { + for i, test := range []struct { + input string + scheme, host, port, path string + shouldErr bool + }{ + {`localhost`, "", "localhost", "", "", false}, + {`localhost:1234`, "", "localhost", "1234", "", false}, + {`localhost:`, "", "localhost", "", "", false}, + {`0.0.0.0`, "", "0.0.0.0", "", "", false}, + {`127.0.0.1:1234`, "", "127.0.0.1", "1234", "", false}, + {`:1234`, "", "", "1234", "", false}, + {`[::1]`, "", "::1", "", "", false}, + {`[::1]:1234`, "", "::1", "1234", "", false}, + {`:`, "", "", "", "", false}, + {`localhost:http`, "http", "localhost", "80", "", false}, + {`localhost:https`, "https", "localhost", "443", "", false}, + {`:http`, "http", "", "80", "", false}, + {`:https`, "https", "", "443", "", false}, + {`http://localhost:https`, "", "", "", "", true}, // conflict + {`http://localhost:http`, "", "", "", "", true}, // repeated scheme + {`http://localhost:443`, "", "", "", "", true}, // not conventional + {`https://localhost:80`, "", "", "", "", true}, // not conventional + {`http://localhost`, "http", "localhost", "80", "", false}, + {`https://localhost`, "https", "localhost", "443", "", false}, + {`http://127.0.0.1`, "http", "127.0.0.1", "80", "", false}, + {`https://127.0.0.1`, "https", "127.0.0.1", "443", "", false}, + {`http://[::1]`, "http", "::1", "80", "", false}, + {`http://localhost:1234`, "http", "localhost", "1234", "", false}, + {`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", "", false}, + {`http://[::1]:1234`, "http", "::1", "1234", "", false}, + {``, "", "", "", "", false}, + {`::1`, "", "::1", "", "", true}, + {`localhost::`, "", "localhost::", "", "", true}, + {`#$%@`, "", "", "", "", true}, + {`host/path`, "", "host", "", "/path", false}, + {`http://host/`, "http", "host", "80", "/", false}, + {`//asdf`, "", "asdf", "", "", false}, + {`:1234/asdf`, "", "", "1234", "/asdf", false}, + {`http://host/path`, "http", "host", "80", "/path", false}, + {`https://host:443/path/foo`, "https", "host", "443", "/path/foo", false}, + {`host:80/path`, "", "host", "80", "/path", false}, + {`host:https/path`, "https", "host", "443", "/path", false}, + {`/path`, "", "", "", "/path", false}, + } { + actual, err := standardizeAddress(test.input) + + if err != nil && !test.shouldErr { + t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err) + } + if err == nil && test.shouldErr { + t.Errorf("Test %d (%s): Expected error, but had none", i, test.input) + } + + if !test.shouldErr && actual.Original != test.input { + t.Errorf("Test %d (%s): Expected original '%s', got '%s'", i, test.input, test.input, actual.Original) + } + if actual.Scheme != test.scheme { + t.Errorf("Test %d (%s): Expected scheme '%s', got '%s'", i, test.input, test.scheme, actual.Scheme) + } + if actual.Host != test.host { + t.Errorf("Test %d (%s): Expected host '%s', got '%s'", i, test.input, test.host, actual.Host) + } + if actual.Port != test.port { + t.Errorf("Test %d (%s): Expected port '%s', got '%s'", i, test.input, test.port, actual.Port) + } + if actual.Path != test.path { + t.Errorf("Test %d (%s): Expected path '%s', got '%s'", i, test.input, test.path, actual.Path) + } + } +} + +func TestAddressVHost(t *testing.T) { + for i, test := range []struct { + addr Address + expected string + }{ + {Address{Original: "host:1234"}, "host:1234"}, + {Address{Original: "host:1234/foo"}, "host:1234/foo"}, + {Address{Original: "host/foo"}, "host/foo"}, + {Address{Original: "http://host/foo"}, "host/foo"}, + {Address{Original: "https://host/foo"}, "host/foo"}, + } { + actual := test.addr.VHost() + if actual != test.expected { + t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual) + } + } +} + +func TestAddressString(t *testing.T) { + for i, test := range []struct { + addr Address + expected string + }{ + {Address{Scheme: "http", Host: "host", Port: "1234", Path: "/path"}, "http://host:1234/path"}, + {Address{Scheme: "", Host: "host", Port: "", Path: ""}, "http://host"}, + {Address{Scheme: "", Host: "host", Port: "80", Path: ""}, "http://host"}, + {Address{Scheme: "", Host: "host", Port: "443", Path: ""}, "https://host"}, + {Address{Scheme: "https", Host: "host", Port: "443", Path: ""}, "https://host"}, + {Address{Scheme: "https", Host: "host", Port: "", Path: ""}, "https://host"}, + {Address{Scheme: "", Host: "host", Port: "80", Path: "/path"}, "http://host/path"}, + {Address{Scheme: "http", Host: "", Port: "1234", Path: ""}, "http://:1234"}, + {Address{Scheme: "", Host: "", Port: "", Path: ""}, ""}, + } { + actual := test.addr.String() + if actual != test.expected { + t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual) + } + } +} diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go new file mode 100644 index 0000000..7e51e46 --- /dev/null +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -0,0 +1,257 @@ +// 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" + "html" + "net/http" + + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/v2/modules/caddytls" +) + +func (st *ServerType) parseRoot( + tkns []caddyfile.Token, + matcherDefs map[string]map[string]json.RawMessage, + warnings *[]caddyconfig.Warning, +) ([]caddyhttp.Route, error) { + var routes []caddyhttp.Route + + matchersAndTokens, err := st.tokensToMatcherSets(tkns, matcherDefs, warnings) + if err != nil { + return nil, err + } + + for _, mst := range matchersAndTokens { + d := caddyfile.NewDispenser("Caddyfile", mst.tokens) + + var root string + for d.Next() { + if !d.NextArg() { + return nil, d.ArgErr() + } + root = d.Val() + if d.NextArg() { + return nil, d.ArgErr() + } + } + + varsHandler := caddyhttp.VarsMiddleware{"root": root} + route := caddyhttp.Route{ + Handle: []json.RawMessage{ + caddyconfig.JSONModuleObject(varsHandler, "handler", "vars", warnings), + }, + } + if mst.matcherSet != nil { + route.MatcherSets = []map[string]json.RawMessage{mst.matcherSet} + } + + routes = append(routes, route) + } + + return routes, nil +} + +func (st *ServerType) parseRedir( + tkns []caddyfile.Token, + matcherDefs map[string]map[string]json.RawMessage, + warnings *[]caddyconfig.Warning, +) ([]caddyhttp.Route, error) { + var routes []caddyhttp.Route + + matchersAndTokens, err := st.tokensToMatcherSets(tkns, matcherDefs, warnings) + if err != nil { + return nil, err + } + + for _, mst := range matchersAndTokens { + var route caddyhttp.Route + + d := caddyfile.NewDispenser("Caddyfile", mst.tokens) + + for d.Next() { + if !d.NextArg() { + return nil, d.ArgErr() + } + to := d.Val() + + var code string + if d.NextArg() { + code = d.Val() + } + if code == "permanent" { + code = "301" + } + if code == "temporary" || code == "" { + code = "307" + } + var body string + if code == "meta" { + // Script tag comes first since that will better imitate a redirect in the browser's + // history, but the meta tag is a fallback for most non-JS clients. + const metaRedir = `<!DOCTYPE html> +<html> + <head> + <title>Redirecting...</title> + <script>window.location.replace("%s");</script> + <meta http-equiv="refresh" content="0; URL='%s'"> + </head> + <body>Redirecting to <a href="%s">%s</a>...</body> +</html> +` + safeTo := html.EscapeString(to) + body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo) + } + + handler := caddyhttp.StaticResponse{ + StatusCode: caddyhttp.WeakString(code), + Headers: http.Header{"Location": []string{to}}, + Body: body, + } + + route.Handle = append(route.Handle, + caddyconfig.JSONModuleObject(handler, "handler", "static_response", warnings)) + } + + if mst.matcherSet != nil { + route.MatcherSets = []map[string]json.RawMessage{mst.matcherSet} + } + + routes = append(routes, route) + } + + return routes, nil +} + +func (st *ServerType) parseTLSAutomationManager(d *caddyfile.Dispenser) (caddytls.ACMEManagerMaker, error) { + var m caddytls.ACMEManagerMaker + + for d.Next() { + firstLine := d.RemainingArgs() + if len(firstLine) == 1 && firstLine[0] != "off" { + m.Email = firstLine[0] + } + + var hasBlock bool + for d.NextBlock() { + hasBlock = true + switch d.Val() { + case "ca": + arg := d.RemainingArgs() + if len(arg) != 1 { + return m, d.ArgErr() + } + m.CA = arg[0] + // TODO: other properties + } + } + + // a naked tls directive is not allowed + if len(firstLine) == 0 && !hasBlock { + return m, d.ArgErr() + } + } + + return m, nil +} + +func (st *ServerType) parseTLSCerts(d *caddyfile.Dispenser) (map[string]caddytls.CertificateLoader, error) { + var fileLoader caddytls.FileLoader + var folderLoader caddytls.FolderLoader + + for d.Next() { + // file loader + firstLine := d.RemainingArgs() + if len(firstLine) == 2 { + fileLoader = append(fileLoader, caddytls.CertKeyFilePair{ + Certificate: firstLine[0], + Key: firstLine[1], + // TODO: tags, for enterprise module's certificate selection + }) + } + + // folder loader + for d.NextBlock() { + if d.Val() == "load" { + folderLoader = append(folderLoader, d.RemainingArgs()...) + } + } + } + + // put configured loaders into the map + loaders := make(map[string]caddytls.CertificateLoader) + if len(fileLoader) > 0 { + loaders["load_files"] = fileLoader + } + if len(folderLoader) > 0 { + loaders["load_folders"] = folderLoader + } + + return loaders, nil +} + +func (st *ServerType) parseTLSConnPolicy(d *caddyfile.Dispenser) (*caddytls.ConnectionPolicy, error) { + cp := new(caddytls.ConnectionPolicy) + + for d.Next() { + for d.NextBlock() { + switch d.Val() { + case "protocols": + args := d.RemainingArgs() + if len(args) == 0 { + return nil, d.SyntaxErr("one or two protocols") + } + if len(args) > 0 { + if _, ok := caddytls.SupportedProtocols[args[0]]; !ok { + return nil, d.Errf("Wrong protocol name or protocol not supported: '%s'", args[0]) + } + cp.ProtocolMin = args[0] + } + if len(args) > 1 { + if _, ok := caddytls.SupportedProtocols[args[1]]; !ok { + return nil, d.Errf("Wrong protocol name or protocol not supported: '%s'", args[1]) + } + cp.ProtocolMax = args[1] + } + case "ciphers": + for d.NextArg() { + if _, ok := caddytls.SupportedCipherSuites[d.Val()]; !ok { + return nil, d.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", d.Val()) + } + cp.CipherSuites = append(cp.CipherSuites, d.Val()) + } + case "curves": + for d.NextArg() { + if _, ok := caddytls.SupportedCurves[d.Val()]; !ok { + return nil, d.Errf("Wrong curve name or curve not supported: '%s'", d.Val()) + } + cp.Curves = append(cp.Curves, d.Val()) + } + case "alpn": + args := d.RemainingArgs() + if len(args) == 0 { + return nil, d.ArgErr() + } + cp.ALPN = args + } + } + } + + return cp, nil +} diff --git a/caddyconfig/httpcaddyfile/handlers.go b/caddyconfig/httpcaddyfile/handlers.go new file mode 100644 index 0000000..a90aa4a --- /dev/null +++ b/caddyconfig/httpcaddyfile/handlers.go @@ -0,0 +1,92 @@ +// 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" + "log" + + "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" +) + +func (st *ServerType) parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]map[string]json.RawMessage, error) { + matchers := make(map[string]map[string]json.RawMessage) + for d.Next() { + definitionName := d.Val() + for d.NextBlock() { + matcherName := d.Val() + mod, err := caddy.GetModule("http.matchers." + matcherName) + if err != nil { + return nil, fmt.Errorf("getting matcher module '%s': %v", matcherName, err) + } + unm, ok := mod.New().(caddyfile.Unmarshaler) + if !ok { + return nil, fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName) + } + err = unm.UnmarshalCaddyfile(d.NewFromNextTokens()) + if err != nil { + return nil, err + } + rm, ok := unm.(caddyhttp.RequestMatcher) + if !ok { + return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) + } + if _, ok := matchers[definitionName]; !ok { + matchers[definitionName] = make(map[string]json.RawMessage) + } + matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil) + } + } + return matchers, nil +} + +// directiveBuckets returns a list of middleware/handler directives. +// Buckets are ordered, and directives should be evaluated in their +// bucket order. Within a bucket, directives are not ordered. Hence, +// the return value has a slice of buckets, where each bucket is a +// map, which is a strongly-typed reminder that directives within a +// bucket are not ordered. +func directiveBuckets() []map[string]struct{} { + directiveBuckets := []map[string]struct{}{ + // prefer odd-numbered buckets; evens are there for contingencies + {}, // 0 + {}, // 1 - keep empty unless necessary + {}, // 2 + {}, // 3 - first handlers, last responders + {}, // 4 + {}, // 5 - middle of chain + {}, // 6 + {}, // 7 - last handlers, first responders + {}, // 8 + {}, // 9 - keep empty unless necessary + {}, // 10 + } + for _, mod := range caddy.GetModules("http.handlers") { + if hd, ok := mod.New().(HandlerDirective); ok { + bucket := hd.Bucket() + if bucket < 0 || bucket >= len(directiveBuckets) { + log.Printf("[ERROR] directive %s: bucket out of range [0-%d): %d; skipping", + mod.Name, len(directiveBuckets), bucket) + continue + } + directiveBuckets[bucket][mod.ID()] = struct{}{} + } + } + return directiveBuckets +} diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go new file mode 100644 index 0000000..e5bf048 --- /dev/null +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -0,0 +1,542 @@ +// 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" + "reflect" + "strconv" + "strings" + + "github.com/mholt/certmagic" + + "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/caddyserver/caddy/v2/modules/caddytls" +) + +func init() { + caddyconfig.RegisterAdapter("caddyfile", caddyfile.Adapter{ServerType: ServerType{}}) +} + +// ServerType can set up a config from an HTTP Caddyfile. +type ServerType struct { +} + +// ValidDirectives returns the list of known directives. +func (ServerType) ValidDirectives() []string { + dirs := []string{"matcher", "root", "tls", "redir"} // TODO: put special-case (hard-coded, or non-handler) directives here + for _, mod := range caddy.GetModules("http.handlers") { + if _, ok := mod.New().(HandlerDirective); ok { + dirs = append(dirs, mod.ID()) + } + } + return dirs +} + +// Setup makes a config from the tokens. +func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock, + options map[string]string) (*caddy.Config, []caddyconfig.Warning, error) { + var warnings []caddyconfig.Warning + + // map + sbmap, err := st.mapAddressToServerBlocks(originalServerBlocks) + if err != nil { + return nil, warnings, err + } + + // reduce + pairings := st.consolidateAddrMappings(sbmap) + + // each pairing of listener addresses to list of server + // blocks is basically a server definition + servers, err := st.serversFromPairings(pairings, &warnings) + if err != nil { + return nil, warnings, err + } + + // now that each server is configured, make the HTTP app + httpApp := caddyhttp.App{ + HTTPPort: tryInt(options["http-port"], &warnings), + HTTPSPort: tryInt(options["https-port"], &warnings), + Servers: servers, + } + + // now for the TLS app! (TODO: refactor into own func) + tlsApp := caddytls.TLS{Certificates: make(map[string]json.RawMessage)} + for _, p := range pairings { + for _, sblock := range p.serverBlocks { + if tkns, ok := sblock.Tokens["tls"]; ok { + // extract all unique hostnames from the server block + // keys, then convert to a slice for use in the TLS app + hostMap := make(map[string]struct{}) + for _, sblockKey := range sblock.Keys { + addr, err := standardizeAddress(sblockKey) + if err != nil { + return nil, warnings, fmt.Errorf("parsing server block key: %v", err) + } + hostMap[addr.Host] = struct{}{} + } + sblockHosts := make([]string, 0, len(hostMap)) + for host := range hostMap { + sblockHosts = append(sblockHosts, host) + } + + // parse tokens to get ACME manager config + acmeMgr, err := st.parseTLSAutomationManager(caddyfile.NewDispenser("Caddyfile", tkns)) + if err != nil { + return nil, warnings, err + } + + tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{ + Hosts: sblockHosts, + ManagementRaw: caddyconfig.JSONModuleObject(acmeMgr, "module", "acme", &warnings), + }) + + // parse tokens to get certificates to be loaded manually + certLoaders, err := st.parseTLSCerts(caddyfile.NewDispenser("Caddyfile", tkns)) + if err != nil { + return nil, nil, err + } + for loaderName, loader := range certLoaders { + tlsApp.Certificates[loaderName] = caddyconfig.JSON(loader, &warnings) + } + + } + } + } + + // annnd the top-level config, then we're done! + cfg := &caddy.Config{AppsRaw: make(map[string]json.RawMessage)} + if !reflect.DeepEqual(httpApp, caddyhttp.App{}) { + cfg.AppsRaw["http"] = caddyconfig.JSON(httpApp, &warnings) + } + if !reflect.DeepEqual(tlsApp, caddytls.TLS{}) { + cfg.AppsRaw["tls"] = caddyconfig.JSON(tlsApp, &warnings) + } + + return cfg, warnings, nil +} + +// hostsFromServerBlockKeys returns a list of all the +// hostnames found in the keys of the server block sb. +// The list may not be in a consistent order. +func (st *ServerType) hostsFromServerBlockKeys(sb caddyfile.ServerBlock) ([]string, error) { + // first get each unique hostname + hostMap := make(map[string]struct{}) + for _, sblockKey := range sb.Keys { + addr, err := standardizeAddress(sblockKey) + if err != nil { + return nil, fmt.Errorf("parsing server block key: %v", err) + } + 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 +// to server blocks. Each pairing is essentially a server definition. +func (st *ServerType) serversFromPairings(pairings []sbAddrAssociation, warnings *[]caddyconfig.Warning) (map[string]*caddyhttp.Server, error) { + servers := make(map[string]*caddyhttp.Server) + + for i, p := range pairings { + srv := &caddyhttp.Server{ + Listen: p.addresses, + } + + for _, sblock := range p.serverBlocks { + matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock) + if err != nil { + return nil, fmt.Errorf("server block %v: compiling matcher sets: %v", sblock.Keys, err) + } + + // extract matcher definitions + d := caddyfile.NewDispenser("Caddyfile", sblock.Tokens["matcher"]) + matcherDefs, err := st.parseMatcherDefinitions(d) + if err != nil { + return nil, err + } + + siteVarSubroute, handlerSubroute := new(caddyhttp.Subroute), new(caddyhttp.Subroute) + + // built-in directives + + // root: path to root of site + if tkns, ok := sblock.Tokens["root"]; ok { + routes, err := st.parseRoot(tkns, matcherDefs, warnings) + if err != nil { + return nil, err + } + siteVarSubroute.Routes = append(siteVarSubroute.Routes, routes...) + } + + // tls: off and conn policies + if tkns, ok := sblock.Tokens["tls"]; ok { + // get the hosts for this server block... + hosts, err := st.hostsFromServerBlockKeys(sblock) + if err != nil { + return nil, err + } + + // ...and of those, which ones qualify for auto HTTPS + var autoHTTPSQualifiedHosts []string + for _, h := range hosts { + if certmagic.HostQualifies(h) { + autoHTTPSQualifiedHosts = append(autoHTTPSQualifiedHosts, h) + } + } + + if len(tkns) == 2 && tkns[1].Text == "off" { + // tls off: disable TLS (and automatic HTTPS) for server block's names + if srv.AutoHTTPS == nil { + srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) + } + srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, autoHTTPSQualifiedHosts...) + } else { + // tls connection policies + cp, err := st.parseTLSConnPolicy(caddyfile.NewDispenser("Caddyfile", tkns)) + if err != nil { + return nil, err + } + // TODO: are matchers needed if every hostname of the config is matched? + cp.Matchers = map[string]json.RawMessage{ + "sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones + } + srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp) + } + } + + // set up each handler directive + for _, dirBucket := range directiveBuckets() { + for dir := range dirBucket { + // keep in mind that multiple occurrences of the directive may appear here + tkns, ok := sblock.Tokens[dir] + if !ok { + continue + } + + // extract matcher sets from matcher tokens, if any + matcherSetsMap, err := st.tokensToMatcherSets(tkns, matcherDefs, warnings) + + mod, err := caddy.GetModule("http.handlers." + dir) + if err != nil { + return nil, fmt.Errorf("getting handler module '%s': %v", mod.Name, err) + } + + // the tokens have been divided by matcher set for us, + // so iterate each one and set them up + for _, mst := range matcherSetsMap { + unm, ok := mod.New().(caddyfile.Unmarshaler) + if !ok { + return nil, fmt.Errorf("handler module '%s' is not a Caddyfile unmarshaler", mod.Name) + } + err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(d.File(), mst.tokens)) + if err != nil { + return nil, err + } + handler, ok := unm.(caddyhttp.MiddlewareHandler) + if !ok { + return nil, fmt.Errorf("handler module '%s' does not implement caddyhttp.MiddlewareHandler interface", mod.Name) + } + + route := caddyhttp.Route{ + Handle: []json.RawMessage{ + caddyconfig.JSONModuleObject(handler, "handler", dir, warnings), + }, + } + if mst.matcherSet != nil { + route.MatcherSets = []map[string]json.RawMessage{mst.matcherSet} + } + handlerSubroute.Routes = append(handlerSubroute.Routes, route) + } + + } + } + + // redir: static responses that redirect + if tkns, ok := sblock.Tokens["redir"]; ok { + routes, err := st.parseRedir(tkns, matcherDefs, warnings) + if err != nil { + return nil, err + } + handlerSubroute.Routes = append(handlerSubroute.Routes, routes...) + } + + // the route that contains the site's handlers will + // be assumed to be the sub-route for this site... + siteSubroute := handlerSubroute + + // ... unless, of course, there are variables that might + // be used by the site's matchers or handlers, in which + // case we need to nest the handlers in a sub-sub-route, + // and the variables go in the sub-route so the variables + // get evaluated first + if len(siteVarSubroute.Routes) > 0 { + subSubRoute := caddyhttp.Subroute{Routes: siteSubroute.Routes} + siteSubroute.Routes = append( + siteVarSubroute.Routes, + caddyhttp.Route{ + Handle: []json.RawMessage{ + caddyconfig.JSONModuleObject(subSubRoute, "handler", "subroute", warnings), + }, + }, + ) + } + + siteSubroute.Routes = consolidateRoutes(siteSubroute.Routes) + + srv.Routes = append(srv.Routes, caddyhttp.Route{ + MatcherSets: matcherSetsEnc, + Handle: []json.RawMessage{ + caddyconfig.JSONModuleObject(siteSubroute, "handler", "subroute", warnings), + }, + }) + } + + srv.Routes = consolidateRoutes(srv.Routes) + + servers[fmt.Sprintf("srv%d", i)] = srv + } + + return servers, nil +} + +// consolidateRoutes combines routes with the same properties +// (same matchers, same Terminal and Group settings) for a +// cleaner overall output. +func consolidateRoutes(routes caddyhttp.RouteList) caddyhttp.RouteList { + for i := 0; i < len(routes)-1; i++ { + if reflect.DeepEqual(routes[i].MatcherSets, routes[i+1].MatcherSets) && + routes[i].Terminal == routes[i+1].Terminal && + routes[i].Group == routes[i+1].Group { + // keep the handlers in the same order, then splice out repetitive route + routes[i].Handle = append(routes[i].Handle, routes[i+1].Handle...) + routes = append(routes[:i+1], routes[i+2:]...) + i-- + } + } + return routes +} + +func (st *ServerType) tokensToMatcherSets( + tkns []caddyfile.Token, + matcherDefs map[string]map[string]json.RawMessage, + warnings *[]caddyconfig.Warning, +) (map[string]matcherSetAndTokens, error) { + m := make(map[string]matcherSetAndTokens) + + for len(tkns) > 0 { + d := caddyfile.NewDispenser("Caddyfile", tkns) + d.Next() // consume directive token + + // look for matcher; it should be the next argument + var matcherToken caddyfile.Token + var matcherSet map[string]json.RawMessage + if d.NextArg() { + var ok bool + var err error + matcherSet, ok, err = st.matcherSetFromMatcherToken(d.Token(), matcherDefs, warnings) + if err != nil { + return nil, err + } + if ok { + // found a matcher; save it, then splice it out + // since we don't want to parse it again + matcherToken = d.Token() + tkns = d.Delete() + } + d.RemainingArgs() // advance to end of line + } + for d.NextBlock() { + // skip entire block including any nested blocks; all + // we care about is accessing next directive occurrence + for d.Nested() { + d.NextBlock() + } + } + end := d.Cursor() + 1 + m[matcherToken.Text] = matcherSetAndTokens{ + matcherSet: matcherSet, + tokens: append(m[matcherToken.Text].tokens, tkns[:end]...), + } + tkns = tkns[end:] + } + return m, nil +} + +func (st *ServerType) matcherSetFromMatcherToken( + tkn caddyfile.Token, + matcherDefs map[string]map[string]json.RawMessage, + warnings *[]caddyconfig.Warning, +) (map[string]json.RawMessage, bool, error) { + // matcher tokens can be wildcards, simple path matchers, + // or refer to a pre-defined matcher by some name + if tkn.Text == "*" { + // match all requests == no matchers, so nothing to do + return nil, true, nil + } else if strings.HasPrefix(tkn.Text, "/") { + // convenient way to specify a single path match + return map[string]json.RawMessage{ + "path": caddyconfig.JSON(caddyhttp.MatchPath{tkn.Text}, warnings), + }, true, nil + } else if strings.HasPrefix(tkn.Text, "match:") { + // pre-defined matcher + matcherName := strings.TrimPrefix(tkn.Text, "match:") + m, ok := matcherDefs[matcherName] + if !ok { + return nil, false, fmt.Errorf("unrecognized matcher name: %+v", matcherName) + } + return m, true, nil + } + + return nil, false, nil +} + +func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([]map[string]json.RawMessage, error) { + type hostPathPair struct { + hostm caddyhttp.MatchHost + pathm caddyhttp.MatchPath + } + + // keep routes with common host and path matchers together + var matcherPairs []*hostPathPair + + for _, key := range sblock.Keys { + addr, err := standardizeAddress(key) + if err != nil { + return nil, fmt.Errorf("server block %v: parsing and standardizing address '%s': %v", sblock.Keys, key, err) + } + + // choose a matcher pair that should be shared by this + // server block; if none exists yet, create one + var chosenMatcherPair *hostPathPair + for _, mp := range matcherPairs { + if (len(mp.pathm) == 0 && addr.Path == "") || + (len(mp.pathm) == 1 && mp.pathm[0] == addr.Path) { + chosenMatcherPair = mp + break + } + } + if chosenMatcherPair == nil { + chosenMatcherPair = new(hostPathPair) + if addr.Path != "" { + chosenMatcherPair.pathm = []string{addr.Path} + } + matcherPairs = append(matcherPairs, chosenMatcherPair) + } + + // add this server block's keys to the matcher + // pair if it doesn't already exist + if addr.Host != "" { + var found bool + for _, h := range chosenMatcherPair.hostm { + if h == addr.Host { + found = true + break + } + } + if !found { + chosenMatcherPair.hostm = append(chosenMatcherPair.hostm, addr.Host) + } + } + } + + // iterate each pairing of host and path matchers and + // put them into a map for JSON encoding + var matcherSets []map[string]caddyhttp.RequestMatcher + for _, mp := range matcherPairs { + matcherSet := make(map[string]caddyhttp.RequestMatcher) + if len(mp.hostm) > 0 { + matcherSet["host"] = mp.hostm + } + if len(mp.pathm) > 0 { + matcherSet["path"] = mp.pathm + } + if len(matcherSet) > 0 { + matcherSets = append(matcherSets, matcherSet) + } + } + + // finally, encode each of the matcher sets + var matcherSetsEnc []map[string]json.RawMessage + for _, ms := range matcherSets { + msEncoded, err := encodeMatcherSet(ms) + if err != nil { + return nil, fmt.Errorf("server block %v: %v", sblock.Keys, err) + } + matcherSetsEnc = append(matcherSetsEnc, msEncoded) + } + + return matcherSetsEnc, nil +} + +func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (map[string]json.RawMessage, error) { + msEncoded := make(map[string]json.RawMessage) + for matcherName, val := range matchers { + jsonBytes, err := json.Marshal(val) + if err != nil { + return nil, fmt.Errorf("marshaling matcher set %#v: %v", matchers, err) + } + msEncoded[matcherName] = jsonBytes + } + return msEncoded, nil +} + +// HandlerDirective implements a directive for an HTTP handler, +// in that it can unmarshal its own configuration from Caddyfile +// tokens and also specify which directive bucket it belongs in. +type HandlerDirective interface { + caddyfile.Unmarshaler + Bucket() int +} + +// tryInt tries to convert str to an integer. If it fails, it downgrades +// the error to a warning and returns 0. +func tryInt(str string, warnings *[]caddyconfig.Warning) int { + if str == "" { + return 0 + } + val, err := strconv.Atoi(str) + if err != nil && warnings != nil { + *warnings = append(*warnings, caddyconfig.Warning{Message: err.Error()}) + } + return val +} + +type matcherSetAndTokens struct { + matcherSet map[string]json.RawMessage + tokens []caddyfile.Token +} + +// sbAddrAssocation is a mapping from a list of +// addresses to a list of server blocks that are +// served on those addresses. +type sbAddrAssociation struct { + addresses []string + serverBlocks []caddyfile.ServerBlock +} + +// Interface guard +var _ caddyfile.ServerType = (*ServerType)(nil) |