2020-03-18 11:00:45 +08:00
// 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 {
2020-04-03 04:20:30 +08:00
for _ , addr := range sb . keys {
2020-03-18 11:00:45 +08:00
if addr . Host == "" {
serverBlocksWithHostlessKey ++
// this server block has a hostless key, now
// go through and add all the hosts to the set
2020-04-03 04:20:30 +08:00
for _ , otherAddr := range sb . keys {
if otherAddr . Original == addr . Original {
2020-03-18 11:00:45 +08:00
continue
}
2020-04-03 04:20:30 +08:00
if otherAddr . Host != "" {
2020-03-18 11:00:45 +08:00
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
2020-04-03 04:20:30 +08:00
sblockHosts := sblock . hostsFromKeys ( false , false )
2020-03-18 11:00:45 +08:00
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
}
}
2020-04-07 02:24:35 +08:00
if ap == catchAllAP && ! reflect . DeepEqual ( ap . Issuer , issuer ) {
return nil , warnings , fmt . Errorf ( "automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v" , ap . Issuer , issuer )
2020-03-18 11:00:45 +08:00
}
2020-04-07 02:24:35 +08:00
ap . Issuer = issuer
2020-03-18 11:00:45 +08:00
}
}
2020-04-07 02:24:35 +08:00
// custom bind host
for _ , cfgVal := range sblock . pile [ "bind" ] {
// either an existing issuer is already configured (and thus, ap is not
// nil), or we need to configure an issuer, so we need ap to be non-nil
if ap == nil {
ap , err = newBaseAutomationPolicy ( options , warnings , true )
if err != nil {
return nil , warnings , err
}
}
// if an issuer was already configured and it is NOT an ACME
// issuer, skip, since we intend to adjust only ACME issuers
var acmeIssuer * caddytls . ACMEIssuer
if ap . Issuer != nil {
var ok bool
if acmeIssuer , ok = ap . Issuer . ( * caddytls . ACMEIssuer ) ; ! ok {
break
}
}
// proceed to configure the ACME issuer's bind host, without
// overwriting any existing settings
if acmeIssuer == nil {
acmeIssuer = new ( caddytls . ACMEIssuer )
}
if acmeIssuer . Challenges == nil {
acmeIssuer . Challenges = new ( caddytls . ChallengesConfig )
}
if acmeIssuer . Challenges . BindHost == "" {
// only binding to one host is supported
var bindHost string
if bindHosts , ok := cfgVal . Value . ( [ ] string ) ; ok && len ( bindHosts ) > 0 {
bindHost = bindHosts [ 0 ]
}
acmeIssuer . Challenges . BindHost = bindHost
}
ap . Issuer = acmeIssuer // we'll encode it later
}
2020-03-18 11:00:45 +08:00
if ap != nil {
2020-04-07 02:24:35 +08:00
// encode issuer now that it's all set up
issuerName := ap . Issuer . ( caddy . Module ) . CaddyModule ( ) . ID . Name ( )
ap . IssuerRaw = caddyconfig . JSONModuleObject ( ap . Issuer , "module" , issuerName , & warnings )
2020-03-18 11:00:45 +08:00
// 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 {
2020-04-07 02:24:35 +08:00
// first, encode its issuer
issuerName := catchAllAP . Issuer . ( caddy . Module ) . CaddyModule ( ) . ID . Name ( )
catchAllAP . IssuerRaw = caddyconfig . JSONModuleObject ( catchAllAP . Issuer , "module" , issuerName , & warnings )
// then append it to the end of the policies list
2020-03-18 11:00:45 +08:00
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
2020-04-07 02:24:35 +08:00
ap . Issuer = new ( caddytls . InternalIssuer ) // we'll encode it later
2020-03-18 11:00:45 +08:00
} else {
if acmeCA == nil {
acmeCA = ""
}
if email == nil {
email = ""
}
2020-04-07 02:24:35 +08:00
mgr := & caddytls . ACMEIssuer {
2020-03-18 11:00:45 +08:00
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 ) }
}
2020-04-07 02:24:35 +08:00
ap . Issuer = mgr // we'll encode it later
2020-03-18 11:00:45 +08:00
}
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 &&
2020-03-27 04:02:29 +08:00
aps [ i ] . RenewalWindowRatio == aps [ j ] . RenewalWindowRatio {
2020-03-18 11:00:45 +08:00
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
}