2018-03-05 19:54:04 +00:00
package acme
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
fmtlog "log"
"net"
"net/http"
"os"
"reflect"
"strings"
"sync"
"time"
"github.com/BurntSushi/ty/fun"
"github.com/containous/flaeg"
"github.com/containous/mux"
"github.com/containous/traefik/log"
"github.com/containous/traefik/rules"
"github.com/containous/traefik/safe"
traefikTLS "github.com/containous/traefik/tls"
"github.com/containous/traefik/types"
"github.com/pkg/errors"
2018-03-27 14:18:03 +00:00
"github.com/xenolf/lego/acmev2"
2018-03-05 19:54:04 +00:00
"github.com/xenolf/lego/providers/dns"
)
var (
// OSCPMustStaple enables OSCP stapling as from https://github.com/xenolf/lego/issues/270
OSCPMustStaple = false
provider = & Provider { }
)
// Configuration holds ACME configuration provided by users
type Configuration struct {
Email string ` description:"Email address used for registration" `
ACMELogging bool ` description:"Enable debug logging of ACME actions." `
CAServer string ` description:"CA server to use." `
Storage string ` description:"Storage to use." `
EntryPoint string ` description:"EntryPoint to use." `
OnHostRule bool ` description:"Enable certificate generation on frontends Host rules." `
OnDemand bool ` description:"Enable on demand certificate generation. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate." ` //deprecated
DNSChallenge * DNSChallenge ` description:"Activate DNS-01 Challenge" `
HTTPChallenge * HTTPChallenge ` description:"Activate HTTP-01 Challenge" `
2018-03-26 12:12:03 +00:00
Domains [ ] types . Domain ` description:"CN and SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='*.main.net'. No SANs for wildcards domain. Wildcard domains only accepted with DNSChallenge" `
2018-03-05 19:54:04 +00:00
}
// Provider holds configurations of the provider.
type Provider struct {
* Configuration
Store Store
certificates [ ] * Certificate
account * Account
client * acme . Client
certsChan chan * Certificate
configurationChan chan <- types . ConfigMessage
dynamicCerts * safe . Safe
staticCerts map [ string ] * tls . Certificate
clientMutex sync . Mutex
configFromListenerChan chan types . Configuration
pool * safe . Pool
}
// Certificate is a struct which contains all data needed from an ACME certificate
type Certificate struct {
Domain types . Domain
Certificate [ ] byte
Key [ ] byte
}
// DNSChallenge contains DNS challenge Configuration
type DNSChallenge struct {
Provider string ` description:"Use a DNS-01 based challenge provider rather than HTTPS." `
DelayBeforeCheck flaeg . Duration ` description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers." `
}
// HTTPChallenge contains HTTP challenge Configuration
type HTTPChallenge struct {
EntryPoint string ` description:"HTTP challenge EntryPoint" `
}
// Get returns the provider instance
func Get ( ) * Provider {
return provider
}
// IsEnabled returns true if the provider instance and its configuration are not nil, otherwise false
func IsEnabled ( ) bool {
return provider != nil && provider . Configuration != nil
}
// SetConfigListenerChan initializes the configFromListenerChan
func ( p * Provider ) SetConfigListenerChan ( configFromListenerChan chan types . Configuration ) {
p . configFromListenerChan = configFromListenerChan
}
func ( p * Provider ) init ( ) error {
if p . ACMELogging {
acme . Logger = fmtlog . New ( os . Stderr , "legolog: " , fmtlog . LstdFlags )
} else {
acme . Logger = fmtlog . New ( ioutil . Discard , "" , 0 )
}
var err error
if p . Store == nil {
err = errors . New ( "no store found for the ACME provider" )
return err
}
p . account , err = p . Store . GetAccount ( )
if err != nil {
2018-03-06 09:12:04 +00:00
return fmt . Errorf ( "unable to get ACME account : %v" , err )
2018-03-05 19:54:04 +00:00
}
p . certificates , err = p . Store . GetCertificates ( )
if err != nil {
2018-03-06 09:12:04 +00:00
return fmt . Errorf ( "unable to get ACME account : %v" , err )
2018-03-05 19:54:04 +00:00
}
p . watchCertificate ( )
p . watchNewDomains ( )
return nil
}
func ( p * Provider ) initAccount ( ) ( * Account , error ) {
if p . account == nil || len ( p . account . Email ) == 0 {
var err error
p . account , err = NewAccount ( p . Email )
if err != nil {
return nil , err
}
}
return p . account , nil
}
// ListenConfiguration sets a new Configuration into the configFromListenerChan
func ( p * Provider ) ListenConfiguration ( config types . Configuration ) {
p . configFromListenerChan <- config
}
2018-03-26 12:12:03 +00:00
// ListenRequest resolves new certificates for a domain from an incoming request and return a valid Certificate to serve (onDemand option)
2018-03-05 19:54:04 +00:00
func ( p * Provider ) ListenRequest ( domain string ) ( * tls . Certificate , error ) {
2018-03-26 12:12:03 +00:00
acmeCert , err := p . resolveCertificate ( types . Domain { Main : domain } , false )
2018-03-05 19:54:04 +00:00
if acmeCert == nil || err != nil {
return nil , err
}
certificate , err := tls . X509KeyPair ( acmeCert . Certificate , acmeCert . PrivateKey )
return & certificate , err
}
func ( p * Provider ) watchNewDomains ( ) {
p . pool . Go ( func ( stop chan bool ) {
for {
select {
case config := <- p . configFromListenerChan :
for _ , frontend := range config . Frontends {
for _ , route := range frontend . Routes {
domainRules := rules . Rules { }
domains , err := domainRules . ParseDomains ( route . Rule )
if err != nil {
log . Errorf ( "Error parsing domains in provider ACME: %v" , err )
continue
}
if len ( domains ) == 0 {
log . Debugf ( "No domain parsed in rule %q" , route . Rule )
continue
}
log . Debugf ( "Try to challenge certificate for domain %v founded in Host rule" , domains )
var domain types . Domain
if len ( domains ) > 0 {
domain = types . Domain { Main : domains [ 0 ] }
if len ( domains ) > 1 {
domain . SANs = domains [ 1 : ]
}
safe . Go ( func ( ) {
2018-03-26 12:12:03 +00:00
if _ , err := p . resolveCertificate ( domain , false ) ; err != nil {
2018-03-05 19:54:04 +00:00
log . Errorf ( "Unable to obtain ACME certificate for domains %q detected thanks to rule %q : %v" , strings . Join ( domains , "," ) , route . Rule , err )
}
} )
}
}
}
case <- stop :
return
}
}
} )
}
// SetDynamicCertificates allow to initialize dynamicCerts map
func ( p * Provider ) SetDynamicCertificates ( safe * safe . Safe ) {
p . dynamicCerts = safe
}
// SetStaticCertificates allow to initialize staticCerts map
func ( p * Provider ) SetStaticCertificates ( staticCerts map [ string ] * tls . Certificate ) {
p . staticCerts = staticCerts
}
2018-03-26 12:12:03 +00:00
func ( p * Provider ) resolveCertificate ( domain types . Domain , domainFromConfigurationFile bool ) ( * acme . CertificateResource , error ) {
domains , err := p . getValidDomains ( domain , domainFromConfigurationFile )
if err != nil {
return nil , err
2018-03-05 19:54:04 +00:00
}
2018-03-06 09:12:04 +00:00
// Check provided certificates
2018-03-26 12:12:03 +00:00
uncheckedDomains := p . getUncheckedDomains ( domains , ! domainFromConfigurationFile )
2018-03-06 09:12:04 +00:00
if len ( uncheckedDomains ) == 0 {
2018-03-05 19:54:04 +00:00
return nil , nil
}
2018-03-06 09:12:04 +00:00
log . Debugf ( "Loading ACME certificates %+v..." , uncheckedDomains )
2018-03-05 19:54:04 +00:00
client , err := p . getClient ( )
if err != nil {
return nil , fmt . Errorf ( "cannot get ACME client %v" , err )
}
bundle := true
2018-03-06 09:12:04 +00:00
certificate , failures := client . ObtainCertificate ( uncheckedDomains , bundle , nil , OSCPMustStaple )
2018-03-05 19:54:04 +00:00
if len ( failures ) > 0 {
return nil , fmt . Errorf ( "cannot obtain certificates %+v" , failures )
}
2018-03-06 09:12:04 +00:00
log . Debugf ( "Certificates obtained for domain %+v" , uncheckedDomains )
if len ( uncheckedDomains ) > 1 {
domain = types . Domain { Main : uncheckedDomains [ 0 ] , SANs : uncheckedDomains [ 1 : ] }
} else {
domain = types . Domain { Main : uncheckedDomains [ 0 ] }
}
2018-03-05 19:54:04 +00:00
p . addCertificateForDomain ( domain , certificate . Certificate , certificate . PrivateKey )
return & certificate , nil
}
func ( p * Provider ) getClient ( ) ( * acme . Client , error ) {
p . clientMutex . Lock ( )
defer p . clientMutex . Unlock ( )
var account * Account
if p . client == nil {
var err error
account , err = p . initAccount ( )
if err != nil {
return nil , err
}
log . Debug ( "Building ACME client..." )
2018-03-26 12:12:03 +00:00
caServer := "https://acme-v02.api.letsencrypt.org/directory"
2018-03-05 19:54:04 +00:00
if len ( p . CAServer ) > 0 {
caServer = p . CAServer
}
log . Debugf ( caServer )
client , err := acme . NewClient ( caServer , account , acme . RSA4096 )
if err != nil {
return nil , err
}
if account . GetRegistration ( ) == nil {
// New users will need to register; be sure to save it
log . Info ( "Register..." )
2018-03-26 12:12:03 +00:00
reg , err := client . Register ( true )
2018-03-05 19:54:04 +00:00
if err != nil {
return nil , err
}
account . Registration = reg
}
// Save the account once before all the certificates generation/storing
// No certificate can be generated if account is not initialized
err = p . Store . SaveAccount ( account )
if err != nil {
return nil , err
}
if p . DNSChallenge != nil && len ( p . DNSChallenge . Provider ) > 0 {
log . Debugf ( "Using DNS Challenge provider: %s" , p . DNSChallenge . Provider )
err = dnsOverrideDelay ( p . DNSChallenge . DelayBeforeCheck )
if err != nil {
return nil , err
}
var provider acme . ChallengeProvider
provider , err = dns . NewDNSChallengeProviderByName ( p . DNSChallenge . Provider )
if err != nil {
return nil , err
}
2018-03-26 12:12:03 +00:00
client . ExcludeChallenges ( [ ] acme . Challenge { acme . HTTP01 } )
2018-03-05 19:54:04 +00:00
err = client . SetChallengeProvider ( acme . DNS01 , provider )
if err != nil {
return nil , err
}
} else if p . HTTPChallenge != nil && len ( p . HTTPChallenge . EntryPoint ) > 0 {
log . Debug ( "Using HTTP Challenge provider." )
2018-03-26 12:12:03 +00:00
client . ExcludeChallenges ( [ ] acme . Challenge { acme . DNS01 } )
2018-03-05 19:54:04 +00:00
err = client . SetChallengeProvider ( acme . HTTP01 , p )
if err != nil {
return nil , err
}
} else {
2018-03-06 13:50:03 +00:00
return nil , errors . New ( "ACME challenge not specified, please select HTTP or DNS Challenge" )
2018-03-05 19:54:04 +00:00
}
p . client = client
}
return p . client , nil
}
// Present presents a challenge to obtain new ACME certificate
func ( p * Provider ) Present ( domain , token , keyAuth string ) error {
2018-03-06 13:50:03 +00:00
return presentHTTPChallenge ( domain , token , keyAuth , p . Store )
2018-03-05 19:54:04 +00:00
}
// CleanUp cleans the challenges when certificate is obtained
func ( p * Provider ) CleanUp ( domain , token , keyAuth string ) error {
2018-03-06 13:50:03 +00:00
return cleanUpHTTPChallenge ( domain , token , p . Store )
2018-03-05 19:54:04 +00:00
}
// Provide allows the file provider to provide configurations to traefik
// using the given Configuration channel.
func ( p * Provider ) Provide ( configurationChan chan <- types . ConfigMessage , pool * safe . Pool , constraints types . Constraints ) error {
p . pool = pool
err := p . init ( )
if err != nil {
return err
}
p . configurationChan = configurationChan
p . refreshCertificates ( )
2018-03-26 12:12:03 +00:00
p . deleteUnnecessaryDomains ( )
for i := 0 ; i < len ( p . Domains ) ; i ++ {
domain := p . Domains [ i ]
2018-03-05 19:54:04 +00:00
safe . Go ( func ( ) {
2018-03-26 12:12:03 +00:00
if _ , err := p . resolveCertificate ( domain , true ) ; err != nil {
log . Errorf ( "Unable to obtain ACME certificate for domains %q : %v" , strings . Join ( domain . ToStrArray ( ) , "," ) , err )
} else {
2018-03-05 19:54:04 +00:00
}
} )
}
p . renewCertificates ( )
ticker := time . NewTicker ( 24 * time . Hour )
pool . Go ( func ( stop chan bool ) {
for {
select {
case <- ticker . C :
p . renewCertificates ( )
case <- stop :
ticker . Stop ( )
return
}
}
} )
return nil
}
func ( p * Provider ) addCertificateForDomain ( domain types . Domain , certificate [ ] byte , key [ ] byte ) {
p . certsChan <- & Certificate { Certificate : certificate , Key : key , Domain : domain }
}
func ( p * Provider ) watchCertificate ( ) {
p . certsChan = make ( chan * Certificate )
p . pool . Go ( func ( stop chan bool ) {
for {
select {
case cert := <- p . certsChan :
certUpdated := false
for _ , domainsCertificate := range p . certificates {
if reflect . DeepEqual ( cert . Domain , domainsCertificate . Domain ) {
domainsCertificate . Certificate = cert . Certificate
domainsCertificate . Key = cert . Key
certUpdated = true
break
}
}
if ! certUpdated {
p . certificates = append ( p . certificates , cert )
}
p . saveCertificates ( )
case <- stop :
return
}
}
} )
}
func ( p * Provider ) deleteCertificateForDomain ( domain types . Domain ) {
for k , cert := range p . certificates {
if reflect . DeepEqual ( cert . Domain , domain ) {
p . certificates = append ( p . certificates [ : k ] , p . certificates [ k + 1 : ] ... )
}
}
p . saveCertificates ( )
}
func ( p * Provider ) saveCertificates ( ) {
err := p . Store . SaveCertificates ( p . certificates )
if err != nil {
log . Error ( err )
}
p . refreshCertificates ( )
}
func ( p * Provider ) refreshCertificates ( ) {
config := types . ConfigMessage {
ProviderName : "ACME" ,
Configuration : & types . Configuration {
Backends : map [ string ] * types . Backend { } ,
Frontends : map [ string ] * types . Frontend { } ,
TLS : [ ] * traefikTLS . Configuration { } ,
} ,
}
for _ , cert := range p . certificates {
certificate := & traefikTLS . Certificate { CertFile : traefikTLS . FileOrContent ( cert . Certificate ) , KeyFile : traefikTLS . FileOrContent ( cert . Key ) }
config . Configuration . TLS = append ( config . Configuration . TLS , & traefikTLS . Configuration { Certificate : certificate } )
}
p . configurationChan <- config
}
// Timeout calculates the maximum of time allowed to resolved an ACME challenge
func ( p * Provider ) Timeout ( ) ( timeout , interval time . Duration ) {
return 60 * time . Second , 5 * time . Second
}
func ( p * Provider ) renewCertificates ( ) {
log . Info ( "Testing certificate renew..." )
for _ , certificate := range p . certificates {
crt , err := getX509Certificate ( certificate )
// If there's an error, we assume the cert is broken, and needs update
// <= 30 days left, renew certificate
if err != nil || crt == nil || crt . NotAfter . Before ( time . Now ( ) . Add ( 24 * 30 * time . Hour ) ) {
client , err := p . getClient ( )
if err != nil {
log . Infof ( "Error renewing certificate from LE : %+v, %v" , certificate . Domain , err )
continue
}
log . Infof ( "Renewing certificate from LE : %+v" , certificate . Domain )
renewedCert , err := client . RenewCertificate ( acme . CertificateResource {
Domain : certificate . Domain . Main ,
PrivateKey : certificate . Key ,
Certificate : certificate . Certificate ,
} , true , OSCPMustStaple )
if err != nil {
log . Errorf ( "Error renewing certificate from LE: %v, %v" , certificate . Domain , err )
continue
}
p . addCertificateForDomain ( certificate . Domain , renewedCert . Certificate , renewedCert . PrivateKey )
}
}
}
// AddRoutes add routes on internal router
func ( p * Provider ) AddRoutes ( router * mux . Router ) {
router . Methods ( http . MethodGet ) .
Path ( acme . HTTP01ChallengePath ( "{token}" ) ) .
Handler ( http . HandlerFunc ( func ( rw http . ResponseWriter , req * http . Request ) {
vars := mux . Vars ( req )
if token , ok := vars [ "token" ] ; ok {
domain , _ , err := net . SplitHostPort ( req . Host )
if err != nil {
log . Debugf ( "Unable to split host and port: %v. Fallback to request host." , err )
domain = req . Host
}
tokenValue := getTokenValue ( token , domain , p . Store )
if len ( tokenValue ) > 0 {
rw . WriteHeader ( http . StatusOK )
_ , err = rw . Write ( tokenValue )
if err != nil {
log . Errorf ( "Unable to write token : %v" , err )
}
return
}
}
rw . WriteHeader ( http . StatusNotFound )
} ) )
}
2018-03-06 09:12:04 +00:00
// Get provided certificate which check a domains list (Main and SANs)
// from static and dynamic provided certificates
2018-03-26 12:12:03 +00:00
func ( p * Provider ) getUncheckedDomains ( domainsToCheck [ ] string , checkConfigurationDomains bool ) [ ] string {
log . Debugf ( "Looking for provided certificate(s) to validate %q..." , domainsToCheck )
var allCerts [ ] string
2018-03-06 09:12:04 +00:00
// Get static certificates
2018-03-26 12:12:03 +00:00
for domains := range p . staticCerts {
allCerts = append ( allCerts , domains )
2018-03-06 09:12:04 +00:00
}
// Get dynamic certificates
if p . dynamicCerts != nil && p . dynamicCerts . Get ( ) != nil {
2018-03-26 12:12:03 +00:00
for domains := range p . dynamicCerts . Get ( ) . ( map [ string ] * tls . Certificate ) {
allCerts = append ( allCerts , domains )
}
}
// Get ACME certificates
for _ , certificate := range p . certificates {
allCerts = append ( allCerts , strings . Join ( certificate . Domain . ToStrArray ( ) , "," ) )
}
// Get Configuration Domains
if checkConfigurationDomains {
for i := 0 ; i < len ( p . Domains ) ; i ++ {
allCerts = append ( allCerts , strings . Join ( p . Domains [ i ] . ToStrArray ( ) , "," ) )
2018-03-06 09:12:04 +00:00
}
}
2018-03-26 12:12:03 +00:00
return searchUncheckedDomains ( domainsToCheck , allCerts )
2018-03-06 09:12:04 +00:00
}
2018-03-26 12:12:03 +00:00
func searchUncheckedDomains ( domainsToCheck [ ] string , existentDomains [ ] string ) [ ] string {
2018-03-27 14:18:03 +00:00
var uncheckedDomains [ ] string
2018-03-26 12:12:03 +00:00
for _ , domainToCheck := range domainsToCheck {
if ! isDomainAlreadyChecked ( domainToCheck , existentDomains ) {
2018-03-06 09:12:04 +00:00
uncheckedDomains = append ( uncheckedDomains , domainToCheck )
2018-03-05 19:54:04 +00:00
}
}
2018-03-06 09:12:04 +00:00
if len ( uncheckedDomains ) == 0 {
2018-03-26 12:12:03 +00:00
log . Debugf ( "No ACME certificate to generate for domains %q." , domainsToCheck )
2018-03-06 09:12:04 +00:00
} else {
2018-03-26 12:12:03 +00:00
log . Debugf ( "Domains %q need ACME certificates generation for domains %q." , domainsToCheck , strings . Join ( uncheckedDomains , "," ) )
2018-03-06 09:12:04 +00:00
}
return uncheckedDomains
2018-03-05 19:54:04 +00:00
}
func getX509Certificate ( certificate * Certificate ) ( * x509 . Certificate , error ) {
var crt * x509 . Certificate
tlsCert , err := tls . X509KeyPair ( certificate . Certificate , certificate . Key )
if err != nil {
log . Errorf ( "Failed to load TLS keypair from ACME certificate for domain %q (SAN : %q), certificate will be renewed : %v" , certificate . Domain . Main , strings . Join ( certificate . Domain . SANs , "," ) , err )
return nil , err
}
crt = tlsCert . Leaf
if crt == nil {
crt , err = x509 . ParseCertificate ( tlsCert . Certificate [ 0 ] )
if err != nil {
log . Errorf ( "Failed to parse TLS keypair from ACME certificate for domain %q (SAN : %q), certificate will be renewed : %v" , certificate . Domain . Main , strings . Join ( certificate . Domain . SANs , "," ) , err )
}
}
return crt , err
}
2018-03-26 12:12:03 +00:00
// getValidDomains checks if given domain is allowed to generate a ACME certificate and return it
func ( p * Provider ) getValidDomains ( domain types . Domain , wildcardAllowed bool ) ( [ ] string , error ) {
domains := domain . ToStrArray ( )
if len ( domains ) == 0 {
return nil , errors . New ( "unable to generate a certificate in ACME provider when no domain is given" )
}
if strings . HasPrefix ( domain . Main , "*" ) {
if ! wildcardAllowed {
return nil , fmt . Errorf ( "unable to generate a wildcard certificate in ACME provider for domain %q from a 'Host' rule" , strings . Join ( domains , "," ) )
}
if p . DNSChallenge == nil {
return nil , fmt . Errorf ( "unable to generate a wildcard certificate in ACME provider for domain %q : ACME needs a DNSChallenge" , strings . Join ( domains , "," ) )
}
if len ( domain . SANs ) > 0 {
return nil , fmt . Errorf ( "unable to generate a wildcard certificate in ACME provider for domain %q : SANs are not allowed" , strings . Join ( domains , "," ) )
}
} else {
for _ , san := range domain . SANs {
if strings . HasPrefix ( san , "*" ) {
return nil , fmt . Errorf ( "unable to generate a certificate in ACME provider for domains %q: SANs can not be a wildcard domain" , strings . Join ( domains , "," ) )
}
}
}
domains = fun . Map ( types . CanonicalDomain , domains ) . ( [ ] string )
return domains , nil
}
func isDomainAlreadyChecked ( domainToCheck string , existentDomains [ ] string ) bool {
for _ , certDomains := range existentDomains {
for _ , certDomain := range strings . Split ( certDomains , "," ) {
2018-03-27 14:18:03 +00:00
if types . MatchDomain ( domainToCheck , certDomain ) {
2018-03-26 12:12:03 +00:00
return true
}
}
}
return false
}
// deleteUnnecessaryDomains deletes from the configuration :
// - Duplicated domains
// - Domains which are checked by wildcard domain
func ( p * Provider ) deleteUnnecessaryDomains ( ) {
var newDomains [ ] types . Domain
for idxDomainToCheck , domainToCheck := range p . Domains {
keepDomain := true
for idxDomain , domain := range p . Domains {
if idxDomainToCheck == idxDomain {
continue
}
if reflect . DeepEqual ( domain , domainToCheck ) {
if idxDomainToCheck > idxDomain {
log . Warnf ( "The domain %v is duplicated in the configuration but will be process by ACME provider only once." , domainToCheck )
keepDomain = false
}
break
} else if strings . HasPrefix ( domain . Main , "*" ) && domain . SANs == nil {
// Check if domains can be validated by the wildcard domain
var newDomainsToCheck [ ] string
for _ , domainProcessed := range domainToCheck . ToStrArray ( ) {
if isDomainAlreadyChecked ( domainProcessed , domain . ToStrArray ( ) ) {
log . Warnf ( "Domain %q will not be processed by ACME provider because it is validated by the wildcard %q" , domainProcessed , domain . Main )
continue
}
newDomainsToCheck = append ( newDomainsToCheck , domainProcessed )
}
// Delete the domain if both Main and SANs can be validated by the wildcard domain
// otherwise keep the unchecked values
if newDomainsToCheck == nil {
keepDomain = false
break
}
domainToCheck . Set ( newDomainsToCheck )
}
}
if keepDomain {
newDomains = append ( newDomains , domainToCheck )
}
}
p . Domains = newDomains
}