Matthew Holt
5 years ago
10 changed files with 599 additions and 241 deletions
@ -0,0 +1,386 @@ |
|||
// 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 ( |
|||
"bytes" |
|||
"fmt" |
|||
"reflect" |
|||
"sort" |
|||
|
|||
"github.com/caddyserver/caddy/v2" |
|||
"github.com/caddyserver/caddy/v2/caddyconfig" |
|||
"github.com/caddyserver/caddy/v2/modules/caddytls" |
|||
"github.com/caddyserver/certmagic" |
|||
) |
|||
|
|||
func (st ServerType) buildTLSApp( |
|||
pairings []sbAddrAssociation, |
|||
options map[string]interface{}, |
|||
warnings []caddyconfig.Warning, |
|||
) (*caddytls.TLS, []caddyconfig.Warning, error) { |
|||
|
|||
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)} |
|||
var certLoaders []caddytls.CertificateLoader |
|||
|
|||
// count how many server blocks have a key with no host,
|
|||
// and find all hosts that share a server block with a
|
|||
// hostless key, so that they don't get forgotten/omitted
|
|||
// by auto-HTTPS (since they won't appear in route matchers)
|
|||
var serverBlocksWithHostlessKey int |
|||
hostsSharedWithHostlessKey := make(map[string]struct{}) |
|||
for _, pair := range pairings { |
|||
for _, sb := range pair.serverBlocks { |
|||
for _, key := range sb.block.Keys { |
|||
addr, err := ParseAddress(key) |
|||
if err != nil { |
|||
return nil, warnings, err |
|||
} |
|||
addr = addr.Normalize() |
|||
if addr.Host == "" { |
|||
serverBlocksWithHostlessKey++ |
|||
// this server block has a hostless key, now
|
|||
// go through and add all the hosts to the set
|
|||
for _, otherKey := range sb.block.Keys { |
|||
if otherKey == key { |
|||
continue |
|||
} |
|||
addr, err := ParseAddress(otherKey) |
|||
if err != nil { |
|||
return nil, warnings, err |
|||
} |
|||
addr = addr.Normalize() |
|||
if addr.Host != "" { |
|||
hostsSharedWithHostlessKey[addr.Host] = struct{}{} |
|||
} |
|||
} |
|||
break |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
catchAllAP, err := newBaseAutomationPolicy(options, warnings, false) |
|||
if err != nil { |
|||
return nil, warnings, err |
|||
} |
|||
|
|||
for _, p := range pairings { |
|||
for _, sblock := range p.serverBlocks { |
|||
// get values that populate an automation policy for this block
|
|||
var ap *caddytls.AutomationPolicy |
|||
|
|||
sblockHosts, err := st.hostsFromServerBlockKeys(sblock.block) |
|||
if err != nil { |
|||
return nil, warnings, err |
|||
} |
|||
if len(sblockHosts) == 0 { |
|||
ap = catchAllAP |
|||
} |
|||
|
|||
// on-demand tls
|
|||
if _, ok := sblock.pile["tls.on_demand"]; ok { |
|||
if ap == nil { |
|||
var err error |
|||
ap, err = newBaseAutomationPolicy(options, warnings, true) |
|||
if err != nil { |
|||
return nil, warnings, err |
|||
} |
|||
} |
|||
ap.OnDemand = true |
|||
} |
|||
|
|||
// certificate issuers
|
|||
if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok { |
|||
for _, issuerVal := range issuerVals { |
|||
issuer := issuerVal.Value.(certmagic.Issuer) |
|||
if ap == nil { |
|||
var err error |
|||
ap, err = newBaseAutomationPolicy(options, warnings, true) |
|||
if err != nil { |
|||
return nil, warnings, err |
|||
} |
|||
} |
|||
encoded := caddyconfig.JSONModuleObject(issuer, "module", issuer.(caddy.Module).CaddyModule().ID.Name(), &warnings) |
|||
if ap == catchAllAP && ap.IssuerRaw != nil && !bytes.Equal(ap.IssuerRaw, encoded) { |
|||
return nil, warnings, fmt.Errorf("conflicting issuer configuration: %s != %s", ap.IssuerRaw, encoded) |
|||
} |
|||
ap.IssuerRaw = encoded |
|||
} |
|||
} |
|||
|
|||
if ap != nil { |
|||
// first make sure this block is allowed to create an automation policy;
|
|||
// doing so is forbidden if it has a key with no host (i.e. ":443")
|
|||
// and if there is a different server block that also has a key with no
|
|||
// host -- since a key with no host matches any host, we need its
|
|||
// associated automation policy to have an empty Subjects list, i.e. no
|
|||
// host filter, which is indistinguishable between the two server blocks
|
|||
// because automation is not done in the context of a particular server...
|
|||
// this is an example of a poor mapping from Caddyfile to JSON but that's
|
|||
// the least-leaky abstraction I could figure out
|
|||
if len(sblockHosts) == 0 { |
|||
if serverBlocksWithHostlessKey > 1 { |
|||
// this server block and at least one other has a key with no host,
|
|||
// making the two indistinguishable; it is misleading to define such
|
|||
// a policy within one server block since it actually will apply to
|
|||
// others as well
|
|||
return nil, warnings, fmt.Errorf("cannot make a TLS automation policy from a server block that has a host-less address when there are other server block addresses lacking a host") |
|||
} |
|||
if catchAllAP == nil { |
|||
// this server block has a key with no hosts, but there is not yet
|
|||
// a catch-all automation policy (probably because no global options
|
|||
// were set), so this one becomes it
|
|||
catchAllAP = ap |
|||
} |
|||
} |
|||
|
|||
// associate our new automation policy with this server block's hosts,
|
|||
// unless, of course, the server block has a key with no hosts, in which
|
|||
// case its automation policy becomes or blends with the default/global
|
|||
// automation policy because, of necessity, it applies to all hostnames
|
|||
// (i.e. it has no Subjects filter) -- in that case, we'll append it last
|
|||
if ap != catchAllAP { |
|||
ap.Subjects = sblockHosts |
|||
|
|||
// if a combination of public and internal names were given
|
|||
// for this same server block and no issuer was specified, we
|
|||
// need to separate them out in the automation policies so
|
|||
// that the internal names can use the internal issuer and
|
|||
// the other names can use the default/public/ACME issuer
|
|||
var ap2 *caddytls.AutomationPolicy |
|||
if ap.Issuer == nil { |
|||
var internal, external []string |
|||
for _, s := range ap.Subjects { |
|||
if certmagic.SubjectQualifiesForPublicCert(s) { |
|||
external = append(external, s) |
|||
} else { |
|||
internal = append(internal, s) |
|||
} |
|||
} |
|||
if len(external) > 0 && len(internal) > 0 { |
|||
ap.Subjects = external |
|||
apCopy := *ap |
|||
ap2 = &apCopy |
|||
ap2.Subjects = internal |
|||
ap2.IssuerRaw = caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings) |
|||
} |
|||
} |
|||
if tlsApp.Automation == nil { |
|||
tlsApp.Automation = new(caddytls.AutomationConfig) |
|||
} |
|||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap) |
|||
if ap2 != nil { |
|||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap2) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// certificate loaders
|
|||
if clVals, ok := sblock.pile["tls.certificate_loader"]; ok { |
|||
for _, clVal := range clVals { |
|||
certLoaders = append(certLoaders, clVal.Value.(caddytls.CertificateLoader)) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// group certificate loaders by module name, then add to config
|
|||
if len(certLoaders) > 0 { |
|||
loadersByName := make(map[string]caddytls.CertificateLoader) |
|||
for _, cl := range certLoaders { |
|||
name := caddy.GetModuleName(cl) |
|||
// ugh... technically, we may have multiple FileLoader and FolderLoader
|
|||
// modules (because the tls directive returns one per occurrence), but
|
|||
// the config structure expects only one instance of each kind of loader
|
|||
// module, so we have to combine them... instead of enumerating each
|
|||
// possible cert loader module in a type switch, we can use reflection,
|
|||
// which works on any cert loaders that are slice types
|
|||
if reflect.TypeOf(cl).Kind() == reflect.Slice { |
|||
combined := reflect.ValueOf(loadersByName[name]) |
|||
if !combined.IsValid() { |
|||
combined = reflect.New(reflect.TypeOf(cl)).Elem() |
|||
} |
|||
clVal := reflect.ValueOf(cl) |
|||
for i := 0; i < clVal.Len(); i++ { |
|||
combined = reflect.Append(reflect.Value(combined), clVal.Index(i)) |
|||
} |
|||
loadersByName[name] = combined.Interface().(caddytls.CertificateLoader) |
|||
} |
|||
} |
|||
for certLoaderName, loaders := range loadersByName { |
|||
tlsApp.CertificatesRaw[certLoaderName] = caddyconfig.JSON(loaders, &warnings) |
|||
} |
|||
} |
|||
|
|||
// set any of the on-demand options, for if/when on-demand TLS is enabled
|
|||
if onDemand, ok := options["on_demand_tls"].(*caddytls.OnDemandConfig); ok { |
|||
if tlsApp.Automation == nil { |
|||
tlsApp.Automation = new(caddytls.AutomationConfig) |
|||
} |
|||
tlsApp.Automation.OnDemand = onDemand |
|||
} |
|||
|
|||
// if there is a global/catch-all automation policy, ensure it goes last
|
|||
if catchAllAP != nil { |
|||
if tlsApp.Automation == nil { |
|||
tlsApp.Automation = new(caddytls.AutomationConfig) |
|||
} |
|||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP) |
|||
} |
|||
|
|||
// if any hostnames appear on the same server block as a key with
|
|||
// no host, they will not be used with route matchers because the
|
|||
// hostless key matches all hosts, therefore, it wouldn't be
|
|||
// considered for auto-HTTPS, so we need to make sure those hosts
|
|||
// are manually considered for managed certificates
|
|||
var al caddytls.AutomateLoader |
|||
for h := range hostsSharedWithHostlessKey { |
|||
al = append(al, h) |
|||
} |
|||
if len(al) > 0 { |
|||
tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings) |
|||
} |
|||
|
|||
// do a little verification & cleanup
|
|||
if tlsApp.Automation != nil { |
|||
// ensure automation policies don't overlap subjects (this should be
|
|||
// an error at provision-time as well, but catch it in the adapt phase
|
|||
// for convenience)
|
|||
automationHostSet := make(map[string]struct{}) |
|||
for _, ap := range tlsApp.Automation.Policies { |
|||
for _, s := range ap.Subjects { |
|||
if _, ok := automationHostSet[s]; ok { |
|||
return nil, warnings, fmt.Errorf("hostname appears in more than one automation policy, making certificate management ambiguous: %s", s) |
|||
} |
|||
automationHostSet[s] = struct{}{} |
|||
} |
|||
} |
|||
|
|||
// consolidate automation policies that are the exact same
|
|||
tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies) |
|||
} |
|||
|
|||
return tlsApp, warnings, nil |
|||
} |
|||
|
|||
// newBaseAutomationPolicy returns a new TLS automation policy that gets
|
|||
// its values from the global options map. It should be used as the base
|
|||
// for any other automation policies. A nil policy (and no error) will be
|
|||
// returned if there are no default/global options. However, if always is
|
|||
// true, a non-nil value will always be returned (unless there is an error).
|
|||
func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) { |
|||
acmeCA, hasACMECA := options["acme_ca"] |
|||
acmeDNS, hasACMEDNS := options["acme_dns"] |
|||
acmeCARoot, hasACMECARoot := options["acme_ca_root"] |
|||
email, hasEmail := options["email"] |
|||
localCerts, hasLocalCerts := options["local_certs"] |
|||
|
|||
hasGlobalAutomationOpts := hasACMECA || hasACMEDNS || hasACMECARoot || hasEmail || hasLocalCerts |
|||
|
|||
// if there are no global options related to automation policies
|
|||
// set, then we can just return right away
|
|||
if !hasGlobalAutomationOpts { |
|||
if always { |
|||
return new(caddytls.AutomationPolicy), nil |
|||
} |
|||
return nil, nil |
|||
} |
|||
|
|||
ap := new(caddytls.AutomationPolicy) |
|||
|
|||
if localCerts != nil { |
|||
// internal issuer enabled trumps any ACME configurations; useful in testing
|
|||
ap.IssuerRaw = caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings) |
|||
} else { |
|||
if acmeCA == nil { |
|||
acmeCA = "" |
|||
} |
|||
if email == nil { |
|||
email = "" |
|||
} |
|||
mgr := caddytls.ACMEIssuer{ |
|||
CA: acmeCA.(string), |
|||
Email: email.(string), |
|||
} |
|||
if acmeDNS != nil { |
|||
provName := acmeDNS.(string) |
|||
dnsProvModule, err := caddy.GetModule("tls.dns." + provName) |
|||
if err != nil { |
|||
return nil, fmt.Errorf("getting DNS provider module named '%s': %v", provName, err) |
|||
} |
|||
mgr.Challenges = &caddytls.ChallengesConfig{ |
|||
DNSRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "provider", provName, &warnings), |
|||
} |
|||
} |
|||
if acmeCARoot != nil { |
|||
mgr.TrustedRootsPEMFiles = []string{acmeCARoot.(string)} |
|||
} |
|||
ap.IssuerRaw = caddyconfig.JSONModuleObject(mgr, "module", "acme", &warnings) |
|||
} |
|||
|
|||
return ap, nil |
|||
} |
|||
|
|||
// 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 |
|||
} |
|||
|
|||
// if they're exactly equal in every way, just keep one of them
|
|||
if reflect.DeepEqual(aps[i], aps[j]) { |
|||
aps = append(aps[:j], aps[j+1:]...) |
|||
i-- |
|||
break |
|||
} |
|||
|
|||
// if the policy is the same, we can keep just one, but we have
|
|||
// to be careful which one we keep; if only one has any hostnames
|
|||
// defined, then we need to keep the one without any hostnames,
|
|||
// otherwise the one without any subjects (a catch-all) would be
|
|||
// eaten up by the one with subjects; and if both have subjects, we
|
|||
// need to combine their lists
|
|||
if bytes.Equal(aps[i].IssuerRaw, aps[j].IssuerRaw) && |
|||
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) && |
|||
aps[i].MustStaple == aps[j].MustStaple && |
|||
aps[i].KeyType == aps[j].KeyType && |
|||
aps[i].OnDemand == aps[j].OnDemand && |
|||
aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio && |
|||
aps[i].ManageSync == aps[j].ManageSync { |
|||
if len(aps[i].Subjects) == 0 && len(aps[j].Subjects) > 0 { |
|||
aps = append(aps[:j], aps[j+1:]...) |
|||
} else if len(aps[i].Subjects) > 0 && len(aps[j].Subjects) == 0 { |
|||
aps = append(aps[:i], aps[i+1:]...) |
|||
} else { |
|||
aps[i].Subjects = append(aps[i].Subjects, aps[j].Subjects...) |
|||
aps = append(aps[:j], aps[j+1:]...) |
|||
} |
|||
i-- |
|||
break |
|||
} |
|||
} |
|||
} |
|||
|
|||
// ensure any catch-all policies go last
|
|||
sort.SliceStable(aps, func(i, j int) bool { |
|||
return len(aps[i].Subjects) > len(aps[j].Subjects) |
|||
}) |
|||
|
|||
return aps |
|||
} |
Loading…
Reference in new issue