From c9980fd3671d873a7197a5ac4d6ac9d6b046abb6 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 21 Aug 2019 10:46:35 -0600 Subject: Refactor Caddyfile adapter and module registration Use piles from which to draw config values. Module values can return their name, so now we can do two-way mapping from value to name and name to value; whereas before we could only map name to value. This was problematic with the Caddyfile adapter since it receives values and needs to know the name to put in the config. --- caddyconfig/httpcaddyfile/addresses.go | 162 ++++++------ caddyconfig/httpcaddyfile/addresses_test.go | 121 +++++---- caddyconfig/httpcaddyfile/builtins.go | 356 +++++++++++++-------------- caddyconfig/httpcaddyfile/directives.go | 182 ++++++++++++++ caddyconfig/httpcaddyfile/handlers.go | 36 --- caddyconfig/httpcaddyfile/httptype.go | 369 +++++++++++++--------------- 6 files changed, 682 insertions(+), 544 deletions(-) create mode 100644 caddyconfig/httpcaddyfile/directives.go (limited to 'caddyconfig/httpcaddyfile') diff --git a/caddyconfig/httpcaddyfile/addresses.go b/caddyconfig/httpcaddyfile/addresses.go index 6ecee26..2adb818 100644 --- a/caddyconfig/httpcaddyfile/addresses.go +++ b/caddyconfig/httpcaddyfile/addresses.go @@ -22,7 +22,7 @@ import ( "strconv" "strings" - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/mholt/certmagic" ) @@ -73,8 +73,8 @@ import ( // 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) +func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock) (map[string][]serverBlock, error) { + sbmap := make(map[string][]serverBlock) for i, sblock := range originalServerBlocks { // within a server block, we need to map all the listener addresses @@ -83,7 +83,7 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []caddyfile. // 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 { + for j, key := range sblock.block.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 @@ -105,9 +105,12 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []caddyfile. // 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, + sbmap[addr] = append(sbmap[addr], serverBlock{ + block: caddyfile.ServerBlock{ + Keys: keys, + Segments: sblock.block.Segments, + }, + pile: sblock.pile, }) } } @@ -123,7 +126,7 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []caddyfile. // 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 { +func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]serverBlock) []sbAddrAssociation { var sbaddrs []sbAddrAssociation for addr, sblocks := range addrToServerBlocks { // we start with knowing that at least this address @@ -151,11 +154,12 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]ca return sbaddrs } -func (st *ServerType) listenerAddrsForServerBlockKey(sblock caddyfile.ServerBlock, key string) ([]string, error) { - addr, err := standardizeAddress(key) +func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string) ([]string, error) { + addr, err := ParseAddress(key) if err != nil { return nil, fmt.Errorf("parsing key: %v", err) } + addr = addr.Normalize() lnPort := defaultPort if addr.Port != "" { @@ -168,11 +172,8 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock caddyfile.ServerBloc // 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) + for _, cfgVal := range sblock.pile["bind"] { + lnHosts = append(lnHosts, cfgVal.Value.([]string)...) } if len(lnHosts) == 0 { lnHosts = []string{""} @@ -205,7 +206,53 @@ type Address struct { Original, Scheme, Host, Port, Path string } -// String returns a human-friendly print of the address. +// ParseAddress parses an address string into a structured format with separate +// scheme, host, port, and path portions, as well as the original input string. +func ParseAddress(str string) (Address, error) { + httpPort, httpsPort := strconv.Itoa(certmagic.HTTPPort), strconv.Itoa(certmagic.HTTPSPort) + + input := str + + // Split input into components (prepend with // to force host portion 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 + } + } + + // 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) + } + + return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err +} + +// TODO: which of the methods on Address are even used? + +// String returns a human-readable form of a. It will +// be a cleaned-up and filled-out URL string. func (a Address) String() string { if a.Host == "" && a.Port == "" { return "" @@ -235,16 +282,7 @@ func (a Address) String() string { 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 +// Normalize returns a normalized version of a. func (a Address) Normalize() Address { path := a.Path if !caseSensitivePath { @@ -266,8 +304,8 @@ func (a Address) Normalize() Address { } } -// 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) +// Key returns a string form of a, much like String() does, but this +// method doesn't add anything default that wasn't in the original. func (a Address) Key() string { res := "" if a.Scheme != "" { @@ -276,11 +314,11 @@ func (a Address) Key() string { 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 - } + // insert port only if the original has its own explicit port + if a.Port != "" && + len(a.Original) >= len(res) && + strings.HasPrefix(a.Original[len(res):], ":"+a.Port) { + res += ":" + a.Port } if a.Path != "" { res += a.Path @@ -288,63 +326,7 @@ func (a Address) Key() string { 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 + caseSensitivePath = false // TODO: Used? ) diff --git a/caddyconfig/httpcaddyfile/addresses_test.go b/caddyconfig/httpcaddyfile/addresses_test.go index 7e03d29..d6aa6f6 100644 --- a/caddyconfig/httpcaddyfile/addresses_test.go +++ b/caddyconfig/httpcaddyfile/addresses_test.go @@ -1,22 +1,11 @@ -// 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" +import ( + "strings" + "testing" +) -func TestStandardizeAddress(t *testing.T) { +func TestParseAddress(t *testing.T) { for i, test := range []struct { input string scheme, host, port, path string @@ -31,14 +20,15 @@ func TestStandardizeAddress(t *testing.T) { {`[::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`, "", "", "", "", true}, + {`:https`, "", "", "", "", true}, + {`localhost:http`, "", "", "", "", true}, // using service name in port is verboten, as of Go 1.12.8 + {`localhost:https`, "", "", "", "", true}, {`http://localhost:https`, "", "", "", "", true}, // conflict {`http://localhost:http`, "", "", "", "", true}, // repeated scheme - {`http://localhost:443`, "", "", "", "", true}, // not conventional - {`https://localhost:80`, "", "", "", "", true}, // not conventional + {`host:https/path`, "", "", "", "", true}, + {`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}, @@ -58,10 +48,9 @@ func TestStandardizeAddress(t *testing.T) { {`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) + actual, err := ParseAddress(test.input) if err != nil && !test.shouldErr { t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err) @@ -88,24 +77,6 @@ func TestStandardizeAddress(t *testing.T) { } } -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 @@ -127,3 +98,69 @@ func TestAddressString(t *testing.T) { } } } + +func TestKeyNormalization(t *testing.T) { + testCases := []struct { + input string + expect string + }{ + { + input: "http://host:1234/path", + expect: "http://host:1234/path", + }, + { + input: "HTTP://A/ABCDEF", + expect: "http://a/ABCDEF", + }, + { + input: "A/ABCDEF", + expect: "a/ABCDEF", + }, + { + input: "A:2015/Path", + expect: "a:2015/Path", + }, + { + input: ":80", + expect: ":80", + }, + { + input: ":443", + expect: ":443", + }, + { + input: ":1234", + expect: ":1234", + }, + { + input: "", + expect: "", + }, + { + input: ":", + expect: "", + }, + { + input: "[::]", + expect: "::", + }, + } + for i, tc := range testCases { + addr, err := ParseAddress(tc.input) + if err != nil { + t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err) + continue + } + expect := tc.expect + if !caseSensitivePath { + // every other part of the address should be lowercased when normalized, + // so simply lower-case the whole thing to do case-insensitive comparison + // of the path as well + expect = strings.ToLower(expect) + } + if actual := addr.Normalize().Key(); actual != expect { + t.Errorf("Test %d: Normalized key for address '%s' was '%s' but expected '%s'", i, tc.input, actual, expect) + } + + } +} diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 7e51e46..0fdfcd5 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -19,239 +19,237 @@ import ( "fmt" "html" "net/http" + "reflect" - "github.com/caddyserver/caddy/v2/caddyconfig" - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/caddyconfig" + "github.com/caddyserver/caddy/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} - } +func init() { + RegisterDirective("bind", parseBind) + RegisterDirective("root", parseRoot) + RegisterDirective("tls", parseTLS) + RegisterHandlerDirective("redir", parseRedir) +} - routes = append(routes, route) +func parseBind(h Helper) ([]ConfigValue, error) { + var lnHosts []string + for h.Next() { + lnHosts = append(lnHosts, h.RemainingArgs()...) } - - return routes, nil + return h.NewBindAddresses(lnHosts), 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 +func parseRoot(h Helper) ([]ConfigValue, error) { + if !h.Next() { + return nil, h.ArgErr() + } - matchersAndTokens, err := st.tokensToMatcherSets(tkns, matcherDefs, warnings) + matcherSet, ok, err := h.MatcherToken() 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 = ` - - - Redirecting... - - - - Redirecting to %s... - -` - 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) + if !ok { + // no matcher token; oops + h.Dispenser.Prev() } - 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 - } - } + if !h.NextArg() { + return nil, h.ArgErr() + } + root := h.Val() + if h.NextArg() { + return nil, h.ArgErr() + } - // a naked tls directive is not allowed - if len(firstLine) == 0 && !hasBlock { - return m, d.ArgErr() - } + varsHandler := caddyhttp.VarsMiddleware{"root": root} + route := caddyhttp.Route{ + HandlersRaw: []json.RawMessage{ + caddyconfig.JSONModuleObject(varsHandler, "handler", "vars", nil), + }, + } + if matcherSet != nil { + route.MatcherSetsRaw = []map[string]json.RawMessage{matcherSet} } - return m, nil + return h.NewVarsRoute(route), nil } -func (st *ServerType) parseTLSCerts(d *caddyfile.Dispenser) (map[string]caddytls.CertificateLoader, error) { +func parseTLS(h Helper) ([]ConfigValue, error) { + var configVals []ConfigValue + + cp := new(caddytls.ConnectionPolicy) var fileLoader caddytls.FileLoader var folderLoader caddytls.FolderLoader - - for d.Next() { - // file loader - firstLine := d.RemainingArgs() - if len(firstLine) == 2 { + var mgr caddytls.ACMEManagerMaker + var off bool + + for h.Next() { + // file certificate loader + firstLine := h.RemainingArgs() + switch len(firstLine) { + case 0: + case 1: + if firstLine[0] == "off" { + off = true + } else { + mgr.Email = firstLine[0] + } + case 2: fileLoader = append(fileLoader, caddytls.CertKeyFilePair{ Certificate: firstLine[0], Key: firstLine[1], - // TODO: tags, for enterprise module's certificate selection + // TODO: add tags, for enterprise module's certificate selection }) + default: + return nil, h.ArgErr() } - // 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 -} + var hasBlock bool + for h.NextBlock() { + hasBlock = true -func (st *ServerType) parseTLSConnPolicy(d *caddyfile.Dispenser) (*caddytls.ConnectionPolicy, error) { - cp := new(caddytls.ConnectionPolicy) + switch h.Val() { - for d.Next() { - for d.NextBlock() { - switch d.Val() { + // connection policy case "protocols": - args := d.RemainingArgs() + args := h.RemainingArgs() if len(args) == 0 { - return nil, d.SyntaxErr("one or two protocols") + return nil, h.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]) + return nil, h.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]) + return nil, h.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()) + for h.NextArg() { + if _, ok := caddytls.SupportedCipherSuites[h.Val()]; !ok { + return nil, h.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", h.Val()) } - cp.CipherSuites = append(cp.CipherSuites, d.Val()) + cp.CipherSuites = append(cp.CipherSuites, h.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()) + for h.NextArg() { + if _, ok := caddytls.SupportedCurves[h.Val()]; !ok { + return nil, h.Errf("Wrong curve name or curve not supported: '%s'", h.Val()) } - cp.Curves = append(cp.Curves, d.Val()) + cp.Curves = append(cp.Curves, h.Val()) } case "alpn": - args := d.RemainingArgs() + args := h.RemainingArgs() if len(args) == 0 { - return nil, d.ArgErr() + return nil, h.ArgErr() } cp.ALPN = args + + // certificate folder loader + case "load": + folderLoader = append(folderLoader, h.RemainingArgs()...) + + // automation policy + case "ca": + arg := h.RemainingArgs() + if len(arg) != 1 { + return nil, h.ArgErr() + } + mgr.CA = arg[0] + + // TODO: other properties for automation manager } } + + // a naked tls directive is not allowed + if len(firstLine) == 0 && !hasBlock { + return nil, h.ArgErr() + } + } + + // connection policy + configVals = append(configVals, ConfigValue{ + Class: "tls.connection_policy", + Value: cp, + }) + + // certificate loaders + if len(fileLoader) > 0 { + configVals = append(configVals, ConfigValue{ + Class: "tls.certificate_loader", + Value: fileLoader, + }) + } + if len(folderLoader) > 0 { + configVals = append(configVals, ConfigValue{ + Class: "tls.certificate_loader", + Value: folderLoader, + }) + } + + // automation policy + if off { + configVals = append(configVals, ConfigValue{ + Class: "tls.off", + Value: true, + }) + } else if !reflect.DeepEqual(mgr, caddytls.ACMEManagerMaker{}) { + configVals = append(configVals, ConfigValue{ + Class: "tls.automation_manager", + Value: mgr, + }) + } + + return configVals, nil +} + +func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) { + if !h.Next() { + return nil, h.ArgErr() + } + + if !h.NextArg() { + return nil, h.ArgErr() + } + to := h.Val() + + var code string + if h.NextArg() { + code = h.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 = ` + + + Redirecting... + + + + Redirecting to %s... + +` + safeTo := html.EscapeString(to) + body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo) } - return cp, nil + return caddyhttp.StaticResponse{ + StatusCode: caddyhttp.WeakString(code), + Headers: http.Header{"Location": []string{to}}, + Body: body, + }, nil } diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go new file mode 100644 index 0000000..526ac87 --- /dev/null +++ b/caddyconfig/httpcaddyfile/directives.go @@ -0,0 +1,182 @@ +// 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" + + "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" +) + +// defaultDirectiveOrder specifies the order +// to apply directives in HTTP routes. +// TODO: finish the ability to customize this +var defaultDirectiveOrder = []string{ + "rewrite", + "try_files", + "headers", + "encode", + "templates", + "redir", + "static_response", // TODO: "reply" or "respond"? + "reverse_proxy", + "file_server", +} + +// RegisterDirective registers a unique directive dir with an +// associated unmarshaling (setup) function. When directive dir +// is encountered in a Caddyfile, setupFunc will be called to +// unmarshal its tokens. +func RegisterDirective(dir string, setupFunc UnmarshalFunc) { + if _, ok := registeredDirectives[dir]; ok { + panic("directive " + dir + " already registered") + } + registeredDirectives[dir] = setupFunc +} + +// RegisterHandlerDirective is like RegisterDirective, but for +// directives which specifically output only an HTTP handler. +func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) { + RegisterDirective(dir, func(h Helper) ([]ConfigValue, error) { + if !h.Next() { + return nil, h.ArgErr() + } + + matcherSet, ok, err := h.MatcherToken() + if err != nil { + return nil, err + } + if ok { + h.Dispenser.Delete() // strip matcher token + } + + h.Dispenser.Reset() // pretend this lookahead never happened + val, err := setupFunc(h) + if err != nil { + return nil, err + } + + return h.NewRoute(matcherSet, val), nil + }) +} + +// Helper is a type which helps setup a value from +// Caddyfile tokens. +type Helper struct { + *caddyfile.Dispenser + warnings *[]caddyconfig.Warning + matcherDefs map[string]map[string]json.RawMessage +} + +// JSON converts val into JSON. Any errors are added to warnings. +func (h Helper) JSON(val interface{}, warnings *[]caddyconfig.Warning) json.RawMessage { + return caddyconfig.JSON(val, h.warnings) +} + +// MatcherToken assumes the current token is (possibly) a matcher, and +// if so, returns the matcher set along with a true value. If the current +// token is not a matcher, nil and false is returned. Note that a true +// value may be returned with a nil matcher set if it is a catch-all. +func (h Helper) MatcherToken() (map[string]json.RawMessage, bool, error) { + if !h.NextArg() { + return nil, false, nil + } + return matcherSetFromMatcherToken(h.Dispenser.Token(), h.matcherDefs, h.warnings) +} + +// NewRoute returns config values relevant to creating a new HTTP route. +func (h Helper) NewRoute(matcherSet map[string]json.RawMessage, + handler caddyhttp.MiddlewareHandler) []ConfigValue { + mod, err := caddy.GetModule(caddy.GetModuleName(handler)) + if err != nil { + // TODO: append to warnings + } + var matcherSetsRaw []map[string]json.RawMessage + if matcherSet != nil { + matcherSetsRaw = append(matcherSetsRaw, matcherSet) + } + return []ConfigValue{ + { + Class: "route", + Value: caddyhttp.Route{ + MatcherSetsRaw: matcherSetsRaw, + HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", mod.ID(), h.warnings)}, + }, + }, + } +} + +// NewBindAddresses returns config values relevant to adding +// listener bind addresses to the config. +func (h Helper) NewBindAddresses(addrs []string) []ConfigValue { + return []ConfigValue{{Class: "bind", Value: addrs}} +} + +// NewVarsRoute returns config values relevant to adding a +// "vars" wrapper route to the config. +func (h Helper) NewVarsRoute(route caddyhttp.Route) []ConfigValue { + return []ConfigValue{{Class: "var", Value: route}} +} + +// ConfigValue represents a value to be added to the final +// configuration, or a value to be consulted when building +// the final configuration. +type ConfigValue struct { + // The kind of value this is. As the config is + // being built, the adapter will look in the + // "pile" for values belonging to a certain + // class when it is setting up a certain part + // of the config. The associated value will be + // type-asserted and placed accordingly. + Class string + + // The value to be used when building the config. + // Generally its type is associated with the + // name of the Class. + Value interface{} + + directive string +} + +// serverBlock pairs a Caddyfile server block +// with a "pile" of config values, keyed by class +// name. +type serverBlock struct { + block caddyfile.ServerBlock + pile map[string][]ConfigValue // config values obtained from directives +} + +type ( + // UnmarshalFunc is a function which can unmarshal Caddyfile + // tokens into zero or more config values using a Helper type. + // These are passed in a call to RegisterDirective. + UnmarshalFunc func(h Helper) ([]ConfigValue, error) + + // UnmarshalHandlerFunc is like UnmarshalFunc, except the + // output of the unmarshaling is an HTTP handler. This + // function does not need to deal with HTTP request matching + // which is abstracted away. Since writing HTTP handlers + // with Caddyfile support is very common, this is a more + // convenient way to add a handler to the chain since a lot + // of the details common to HTTP handlers are taken care of + // for you. These are passed to a call to + // RegisterHandlerDirective. + UnmarshalHandlerFunc func(h Helper) (caddyhttp.MiddlewareHandler, error) +) + +var registeredDirectives = make(map[string]UnmarshalFunc) diff --git a/caddyconfig/httpcaddyfile/handlers.go b/caddyconfig/httpcaddyfile/handlers.go index a90aa4a..9a29e97 100644 --- a/caddyconfig/httpcaddyfile/handlers.go +++ b/caddyconfig/httpcaddyfile/handlers.go @@ -17,7 +17,6 @@ package httpcaddyfile import ( "encoding/json" "fmt" - "log" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" @@ -55,38 +54,3 @@ func (st *ServerType) parseMatcherDefinitions(d *caddyfile.Dispenser) (map[strin } 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 index e5bf048..1c12ccd 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -17,17 +17,18 @@ package httpcaddyfile import ( "encoding/json" "fmt" + "log" "reflect" + "sort" "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" + "github.com/mholt/certmagic" ) func init() { @@ -38,24 +39,57 @@ func init() { 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 -} +// TODO: error on unrecognized directives // 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 + var serverBlocks []serverBlock + for _, sblock := range originalServerBlocks { + serverBlocks = append(serverBlocks, serverBlock{ + block: sblock, + pile: make(map[string][]ConfigValue), + }) + } + + for _, sb := range serverBlocks { + // extract matcher definitions + d := sb.block.DispenseDirective("matcher") + matcherDefs, err := st.parseMatcherDefinitions(d) + if err != nil { + return nil, nil, err + } + + for _, segment := range sb.block.Segments { + dir := segment.Directive() + if dir == "matcher" { + // TODO: This is a special case because we pre-processed it; handle this better + continue + } + if dirFunc, ok := registeredDirectives[dir]; ok { + results, err := dirFunc(Helper{ + Dispenser: segment.NewDispenser(), + warnings: &warnings, + matcherDefs: matcherDefs, + }) + if err != nil { + return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err) + } + for _, result := range results { + result.directive = dir + sb.pile[result.Class] = append(sb.pile[result.Class], result) + } + } else { + // TODO: this should be an error + log.Printf("%s not registered", dir) + } + } + } + // map - sbmap, err := st.mapAddressToServerBlocks(originalServerBlocks) + sbmap, err := st.mapAddressToServerBlocks(serverBlocks) if err != nil { return nil, warnings, err } @@ -63,6 +97,22 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock, // reduce pairings := st.consolidateAddrMappings(sbmap) + // TODO: shorthand placeholders + // for _, p := range pairings { + // for _, sblock := range p.serverBlocks { + // for _, tokens := range sblock.Tokens { + // for i := 0; i < len(tokens); i++ { + // switch tokens[i].Text { + // case "{uri}": + // tokens[i].Text = "{http.request.uri}" + // case "{path}": + // tokens[i].Text = "{http.request.uri.path}" + // } + // } + // } + // } + // } + // each pairing of listener addresses to list of server // blocks is basically a server definition servers, err := st.serversFromPairings(pairings, &warnings) @@ -81,45 +131,33 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock, 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) + // tls automation policies + if mmVals, ok := sblock.pile["tls.automation_manager"]; ok { + for _, mmVal := range mmVals { + mm := mmVal.Value.(caddytls.ManagerMaker) + sblockHosts, err := st.autoHTTPSHosts(sblock) if err != nil { - return nil, warnings, fmt.Errorf("parsing server block key: %v", err) + return nil, warnings, err } - hostMap[addr.Host] = struct{}{} - } - sblockHosts := make([]string, 0, len(hostMap)) - for host := range hostMap { - sblockHosts = append(sblockHosts, host) + tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{ + Hosts: sblockHosts, + ManagementRaw: caddyconfig.JSONModuleObject(mm, "module", mm.(caddy.Module).CaddyModule().ID(), &warnings), + }) } + } - // 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 { + // tls certificate loaders + if clVals, ok := sblock.pile["tls.certificate_loader"]; ok { + for _, clVal := range clVals { + loader := clVal.Value.(caddytls.CertificateLoader) + loaderName := caddy.GetModuleName(loader) tlsApp.Certificates[loaderName] = caddyconfig.JSON(loader, &warnings) } - } } } + // consolidate automation policies that are the exact same + tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies) // annnd the top-level config, then we're done! cfg := &caddy.Config{AppsRaw: make(map[string]json.RawMessage)} @@ -140,10 +178,11 @@ func (st *ServerType) hostsFromServerBlockKeys(sb caddyfile.ServerBlock) ([]stri // first get each unique hostname hostMap := make(map[string]struct{}) for _, sblockKey := range sb.Keys { - addr, err := standardizeAddress(sblockKey) + addr, err := ParseAddress(sblockKey) if err != nil { return nil, fmt.Errorf("parsing server block key: %v", err) } + addr = addr.Normalize() hostMap[addr.Host] = struct{}{} } @@ -167,121 +206,75 @@ func (st *ServerType) serversFromPairings(pairings []sbAddrAssociation, warnings } for _, sblock := range p.serverBlocks { - matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock) + matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock.block) 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 + return nil, fmt.Errorf("server block %v: compiling matcher sets: %v", sblock.block.Keys, err) } + // if there are user-defined variables, then siteVarSubroute will + // wrap the handlerSubroute; otherwise handlerSubroute will be the + // site's primary subroute. siteVarSubroute, handlerSubroute := new(caddyhttp.Subroute), new(caddyhttp.Subroute) - // built-in directives + // tls: connection policies and toggle auto HTTPS - // 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...) + autoHTTPSQualifiedHosts, err := st.autoHTTPSHosts(sblock) + if err != nil { + return nil, err } - - // 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 + if _, ok := sblock.pile["tls.off"]; ok { + // tls off: disable TLS (and automatic HTTPS) for server block's names + if srv.AutoHTTPS == nil { + srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) } - - // ...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)) + srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, autoHTTPSQualifiedHosts...) + } else if cpVals, ok := sblock.pile["tls.connection_policy"]; ok { + // tls connection policies + for _, cpVal := range cpVals { + cp := cpVal.Value.(*caddytls.ConnectionPolicy) + // only create if there is a non-empty policy + if !reflect.DeepEqual(cp, new(caddytls.ConnectionPolicy)) { + // make sure the policy covers all hostnames from the block + hosts, err := st.hostsFromServerBlockKeys(sblock.block) 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} + // 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 } - handlerSubroute.Routes = append(handlerSubroute.Routes, route) + srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp) } - } + // TODO: consolidate equal conn policies } - // 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 + // vars: special routes that will have to wrap the normal handlers + // so that these variables can be used across their matchers too + for _, cfgVal := range sblock.pile["var"] { + siteVarSubroute.Routes = append(siteVarSubroute.Routes, cfgVal.Value.(caddyhttp.Route)) + } + + // set up each handler directive + dirRoutes := sblock.pile["route"] + // TODO: The ordering here depends on... if there is a list of + // directives to use, then sort by that, otherwise just use in + // the order they appear in the slice (which is the order they + // appeared in the Caddyfile) + sortByList := true + if sortByList { + dirPositions := make(map[string]int) + for i, dir := range defaultDirectiveOrder { + dirPositions[dir] = i } - handlerSubroute.Routes = append(handlerSubroute.Routes, routes...) + sort.SliceStable(dirRoutes, func(i, j int) bool { + iDir, jDir := dirRoutes[i].directive, dirRoutes[j].directive + return dirPositions[iDir] < dirPositions[jDir] + }) + } + for _, r := range dirRoutes { + handlerSubroute.Routes = append(handlerSubroute.Routes, r.Value.(caddyhttp.Route)) } // the route that contains the site's handlers will @@ -298,7 +291,7 @@ func (st *ServerType) serversFromPairings(pairings []sbAddrAssociation, warnings siteSubroute.Routes = append( siteVarSubroute.Routes, caddyhttp.Route{ - Handle: []json.RawMessage{ + HandlersRaw: []json.RawMessage{ caddyconfig.JSONModuleObject(subSubRoute, "handler", "subroute", warnings), }, }, @@ -308,8 +301,8 @@ func (st *ServerType) serversFromPairings(pairings []sbAddrAssociation, warnings siteSubroute.Routes = consolidateRoutes(siteSubroute.Routes) srv.Routes = append(srv.Routes, caddyhttp.Route{ - MatcherSets: matcherSetsEnc, - Handle: []json.RawMessage{ + MatcherSetsRaw: matcherSetsEnc, + HandlersRaw: []json.RawMessage{ caddyconfig.JSONModuleObject(siteSubroute, "handler", "subroute", warnings), }, }) @@ -323,16 +316,32 @@ func (st *ServerType) serversFromPairings(pairings []sbAddrAssociation, warnings return servers, nil } +func (st ServerType) autoHTTPSHosts(sb serverBlock) ([]string, error) { + // get the hosts for this server block... + hosts, err := st.hostsFromServerBlockKeys(sb.block) + 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) + } + } + return autoHTTPSQualifiedHosts, 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) && + if reflect.DeepEqual(routes[i].MatcherSetsRaw, routes[i+1].MatcherSetsRaw) && 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[i].HandlersRaw = append(routes[i].HandlersRaw, routes[i+1].HandlersRaw...) routes = append(routes[:i+1], routes[i+2:]...) i-- } @@ -340,53 +349,26 @@ func consolidateRoutes(routes caddyhttp.RouteList) caddyhttp.RouteList { 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() +// consolidateAutomationPolicies combines automation policies that are the same, +// for a cleaner overall output. +func consolidateAutomationPolicies(aps []caddytls.AutomationPolicy) []caddytls.AutomationPolicy { + for i := 0; i < len(aps); i++ { + for j := 0; j < len(aps); j++ { + if j == i { + continue } - 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() + if reflect.DeepEqual(aps[i].ManagementRaw, aps[j].ManagementRaw) { + aps[i].Hosts = append(aps[i].Hosts, aps[j].Hosts...) } + aps = append(aps[:j], aps[j+1:]...) + i-- + break } - end := d.Cursor() + 1 - m[matcherToken.Text] = matcherSetAndTokens{ - matcherSet: matcherSet, - tokens: append(m[matcherToken.Text].tokens, tkns[:end]...), - } - tkns = tkns[end:] } - return m, nil + return aps } -func (st *ServerType) matcherSetFromMatcherToken( +func matcherSetFromMatcherToken( tkn caddyfile.Token, matcherDefs map[string]map[string]json.RawMessage, warnings *[]caddyconfig.Warning, @@ -424,10 +406,11 @@ func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([ var matcherPairs []*hostPathPair for _, key := range sblock.Keys { - addr, err := standardizeAddress(key) + addr, err := ParseAddress(key) if err != nil { return nil, fmt.Errorf("server block %v: parsing and standardizing address '%s': %v", sblock.Keys, key, err) } + addr = addr.Normalize() // choose a matcher pair that should be shared by this // server block; if none exists yet, create one @@ -504,14 +487,6 @@ func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (map[string] 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 { @@ -535,7 +510,7 @@ type matcherSetAndTokens struct { // served on those addresses. type sbAddrAssociation struct { addresses []string - serverBlocks []caddyfile.ServerBlock + serverBlocks []serverBlock } // Interface guard -- cgit v1.2.3