// 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)