diff options
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) |