2016-03-21 10:10:18 +00:00
package acme
import (
"crypto/tls"
"errors"
"fmt"
2016-10-14 14:04:09 +00:00
"github.com/BurntSushi/ty/fun"
2016-09-23 16:27:01 +00:00
"github.com/cenk/backoff"
2016-08-18 12:20:11 +00:00
"github.com/containous/staert"
"github.com/containous/traefik/cluster"
"github.com/containous/traefik/log"
"github.com/containous/traefik/safe"
2016-10-14 14:04:09 +00:00
"github.com/containous/traefik/types"
2016-08-18 12:20:11 +00:00
"github.com/xenolf/lego/acme"
"golang.org/x/net/context"
2016-03-21 10:10:18 +00:00
"io/ioutil"
fmtlog "log"
"os"
2016-05-25 15:06:34 +00:00
"strings"
2016-03-21 10:10:18 +00:00
"time"
)
// ACME allows to connect to lets encrypt and retrieve certs
type ACME struct {
2016-08-18 12:20:11 +00:00
Email string ` description:"Email address used for registration" `
Domains [ ] Domain ` description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'" `
Storage string ` description:"File or key used for certificates storage." `
2016-09-23 16:27:01 +00:00
StorageFile string // deprecated
2016-08-18 12:20:11 +00:00
OnDemand bool ` description:"Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate." `
OnHostRule bool ` description:"Enable certificate generation on frontends Host rules." `
CAServer string ` description:"CA server to use." `
EntryPoint string ` description:"Entrypoint to proxy acme challenge to." `
client * acme . Client
defaultCertificate * tls . Certificate
store cluster . Store
challengeProvider * challengeProvider
checkOnDemandDomain func ( domain string ) bool
2016-03-21 10:10:18 +00:00
}
2016-05-25 15:06:34 +00:00
//Domains parse []Domain
type Domains [ ] Domain
//Set []Domain
func ( ds * Domains ) Set ( str string ) error {
fargs := func ( c rune ) bool {
return c == ',' || c == ';'
}
// get function
slice := strings . FieldsFunc ( str , fargs )
2016-05-27 09:13:34 +00:00
if len ( slice ) < 1 {
2016-05-25 15:06:34 +00:00
return fmt . Errorf ( "Parse error ACME.Domain. Imposible to parse %s" , str )
}
d := Domain {
Main : slice [ 0 ] ,
2016-05-27 09:13:34 +00:00
SANs : [ ] string { } ,
}
if len ( slice ) > 1 {
d . SANs = slice [ 1 : ]
2016-05-25 15:06:34 +00:00
}
* ds = append ( * ds , d )
return nil
}
//Get []Domain
func ( ds * Domains ) Get ( ) interface { } { return [ ] Domain ( * ds ) }
//String returns []Domain in string
func ( ds * Domains ) String ( ) string { return fmt . Sprintf ( "%+v" , * ds ) }
//SetValue sets []Domain into the parser
func ( ds * Domains ) SetValue ( val interface { } ) {
* ds = Domains ( val . ( [ ] Domain ) )
}
2016-03-21 10:10:18 +00:00
// Domain holds a domain name with SANs
type Domain struct {
Main string
SANs [ ] string
}
2016-08-16 17:13:18 +00:00
func ( a * ACME ) init ( ) error {
2016-03-21 10:10:18 +00:00
acme . Logger = fmtlog . New ( ioutil . Discard , "" , 0 )
2016-08-16 17:13:18 +00:00
// no certificates in TLS config, so we add a default one
cert , err := generateDefaultCertificate ( )
if err != nil {
return err
}
a . defaultCertificate = cert
2016-09-23 16:27:01 +00:00
// TODO: to remove in the futurs
if len ( a . StorageFile ) > 0 && len ( a . Storage ) == 0 {
log . Warnf ( "ACME.StorageFile is deprecated, use ACME.Storage instead" )
a . Storage = a . StorageFile
}
2016-08-16 17:13:18 +00:00
return nil
}
2016-03-21 10:10:18 +00:00
2016-08-18 12:20:11 +00:00
// CreateClusterConfig creates a tls.config using ACME configuration in cluster mode
func ( a * ACME ) CreateClusterConfig ( leadership * cluster . Leadership , tlsConfig * tls . Config , checkOnDemandDomain func ( domain string ) bool ) error {
2016-08-16 17:13:18 +00:00
err := a . init ( )
if err != nil {
return err
2016-03-21 10:10:18 +00:00
}
2016-08-18 12:20:11 +00:00
if len ( a . Storage ) == 0 {
return errors . New ( "Empty Store, please provide a key for certs storage" )
2016-08-16 17:13:18 +00:00
}
2016-08-18 12:20:11 +00:00
a . checkOnDemandDomain = checkOnDemandDomain
2016-08-16 17:13:18 +00:00
tlsConfig . Certificates = append ( tlsConfig . Certificates , * a . defaultCertificate )
2016-08-18 12:20:11 +00:00
tlsConfig . GetCertificate = a . getCertificate
2016-09-29 13:36:52 +00:00
listener := func ( object cluster . Object ) error {
account := object . ( * Account )
account . Init ( )
if ! leadership . IsLeader ( ) {
a . client , err = a . buildACMEClient ( account )
if err != nil {
log . Errorf ( "Error building ACME client %+v: %s" , object , err . Error ( ) )
}
}
return nil
}
2016-08-18 12:20:11 +00:00
datastore , err := cluster . NewDataStore (
staert . KvSource {
Store : leadership . Store ,
2016-09-29 13:36:52 +00:00
Prefix : a . Storage ,
2016-08-18 12:20:11 +00:00
} ,
leadership . Pool . Ctx ( ) , & Account { } ,
2016-09-29 13:36:52 +00:00
listener )
2016-08-18 12:20:11 +00:00
if err != nil {
return err
}
a . store = datastore
2016-09-29 13:36:52 +00:00
a . challengeProvider = & challengeProvider { store : a . store }
2016-08-18 12:20:11 +00:00
ticker := time . NewTicker ( 24 * time . Hour )
leadership . Pool . AddGoCtx ( func ( ctx context . Context ) {
log . Infof ( "Starting ACME renew job..." )
defer log . Infof ( "Stopped ACME renew job..." )
2016-10-27 14:09:28 +00:00
for {
select {
case <- ctx . Done ( ) :
return
case <- ticker . C :
if err := a . renewCertificates ( ) ; err != nil {
log . Errorf ( "Error renewing ACME certificate: %s" , err . Error ( ) )
}
2016-08-18 12:20:11 +00:00
}
}
} )
leadership . AddListener ( func ( elected bool ) error {
if elected {
object , err := a . store . Load ( )
if err != nil {
return err
}
transaction , object , err := a . store . Begin ( )
if err != nil {
return err
}
account := object . ( * Account )
account . Init ( )
var needRegister bool
if account == nil || len ( account . Email ) == 0 {
account , err = NewAccount ( a . Email )
if err != nil {
return err
}
needRegister = true
}
if err != nil {
return err
}
a . client , err = a . buildACMEClient ( account )
if err != nil {
return err
}
if needRegister {
// New users will need to register; be sure to save it
log . Debugf ( "Register..." )
reg , err := a . client . Register ( )
if err != nil {
return err
}
account . Registration = reg
}
// The client has a URL to the current Let's Encrypt Subscriber
// Agreement. The user will need to agree to it.
log . Debugf ( "AgreeToTOS..." )
err = a . client . AgreeToTOS ( )
if err != nil {
2016-09-23 16:27:01 +00:00
// Let's Encrypt Subscriber Agreement renew ?
reg , err := a . client . QueryRegistration ( )
if err != nil {
return err
}
account . Registration = reg
err = a . client . AgreeToTOS ( )
if err != nil {
log . Errorf ( "Error sending ACME agreement to TOS: %+v: %s" , account , err . Error ( ) )
}
2016-08-18 12:20:11 +00:00
}
err = transaction . Commit ( account )
if err != nil {
return err
}
safe . Go ( func ( ) {
a . retrieveCertificates ( )
if err := a . renewCertificates ( ) ; err != nil {
log . Errorf ( "Error renewing ACME certificate %+v: %s" , account , err . Error ( ) )
}
} )
}
return nil
} )
2016-08-16 17:13:18 +00:00
return nil
}
2016-03-21 10:10:18 +00:00
2016-08-18 12:20:11 +00:00
// CreateLocalConfig creates a tls.config using local ACME configuration
func ( a * ACME ) CreateLocalConfig ( tlsConfig * tls . Config , checkOnDemandDomain func ( domain string ) bool ) error {
2016-08-16 17:13:18 +00:00
err := a . init ( )
if err != nil {
return err
2016-03-21 10:10:18 +00:00
}
2016-08-18 12:20:11 +00:00
if len ( a . Storage ) == 0 {
return errors . New ( "Empty Store, please provide a filename for certs storage" )
2016-08-16 17:13:18 +00:00
}
2016-08-18 12:20:11 +00:00
a . checkOnDemandDomain = checkOnDemandDomain
2016-08-16 17:13:18 +00:00
tlsConfig . Certificates = append ( tlsConfig . Certificates , * a . defaultCertificate )
2016-08-18 12:20:11 +00:00
tlsConfig . GetCertificate = a . getCertificate
localStore := NewLocalStore ( a . Storage )
a . store = localStore
2016-09-29 13:36:52 +00:00
a . challengeProvider = & challengeProvider { store : a . store }
2016-08-16 17:13:18 +00:00
2016-03-21 10:10:18 +00:00
var needRegister bool
2016-08-18 12:20:11 +00:00
var account * Account
2016-03-21 10:10:18 +00:00
2016-08-18 12:20:11 +00:00
if fileInfo , fileErr := os . Stat ( a . Storage ) ; fileErr == nil && fileInfo . Size ( ) != 0 {
log . Infof ( "Loading ACME Account..." )
2016-03-21 10:10:18 +00:00
// load account
2016-08-18 12:20:11 +00:00
object , err := localStore . Load ( )
2016-03-21 10:10:18 +00:00
if err != nil {
return err
}
2016-08-18 12:20:11 +00:00
account = object . ( * Account )
2016-03-21 10:10:18 +00:00
} else {
log . Infof ( "Generating ACME Account..." )
2016-08-18 12:20:11 +00:00
account , err = NewAccount ( a . Email )
2016-03-21 10:10:18 +00:00
if err != nil {
return err
}
needRegister = true
}
2016-08-18 12:20:11 +00:00
a . client , err = a . buildACMEClient ( account )
2016-08-16 17:13:18 +00:00
if err != nil {
return err
}
2016-03-21 10:10:18 +00:00
if needRegister {
// New users will need to register; be sure to save it
2016-08-18 12:20:11 +00:00
log . Infof ( "Register..." )
2016-08-05 18:42:45 +00:00
reg , err := a . client . Register ( )
2016-03-21 10:10:18 +00:00
if err != nil {
return err
}
2016-08-18 12:20:11 +00:00
account . Registration = reg
2016-03-21 10:10:18 +00:00
}
// The client has a URL to the current Let's Encrypt Subscriber
// Agreement. The user will need to agree to it.
2016-11-07 20:51:23 +00:00
log . Debugf ( "AgreeToTOS..." )
2016-08-05 18:42:45 +00:00
err = a . client . AgreeToTOS ( )
2016-08-02 09:48:44 +00:00
if err != nil {
// Let's Encrypt Subscriber Agreement renew ?
2016-09-19 17:58:34 +00:00
reg , err := a . client . QueryRegistration ( )
2016-08-02 09:48:44 +00:00
if err != nil {
return err
}
2016-08-18 12:20:11 +00:00
account . Registration = reg
2016-09-19 17:58:34 +00:00
err = a . client . AgreeToTOS ( )
2016-08-02 09:48:44 +00:00
if err != nil {
2016-08-18 12:20:11 +00:00
log . Errorf ( "Error sending ACME agreement to TOS: %+v: %s" , account , err . Error ( ) )
2016-08-02 09:48:44 +00:00
}
}
// save account
2016-08-18 12:20:11 +00:00
transaction , _ , err := a . store . Begin ( )
if err != nil {
return err
}
err = transaction . Commit ( account )
2016-03-21 10:10:18 +00:00
if err != nil {
return err
}
2016-03-31 16:57:08 +00:00
safe . Go ( func ( ) {
2016-08-18 12:20:11 +00:00
a . retrieveCertificates ( )
if err := a . renewCertificates ( ) ; err != nil {
log . Errorf ( "Error renewing ACME certificate %+v: %s" , account , err . Error ( ) )
2016-06-20 11:55:50 +00:00
}
2016-03-31 16:57:08 +00:00
} )
2016-03-21 10:10:18 +00:00
ticker := time . NewTicker ( 24 * time . Hour )
2016-03-31 16:57:08 +00:00
safe . Go ( func ( ) {
2016-08-16 17:13:18 +00:00
for range ticker . C {
2016-08-18 12:20:11 +00:00
if err := a . renewCertificates ( ) ; err != nil {
2016-08-16 17:13:18 +00:00
log . Errorf ( "Error renewing ACME certificate %+v: %s" , account , err . Error ( ) )
2016-03-21 10:10:18 +00:00
}
}
2016-03-31 16:57:08 +00:00
} )
2016-03-21 10:10:18 +00:00
return nil
}
2016-08-18 12:20:11 +00:00
func ( a * ACME ) getCertificate ( clientHello * tls . ClientHelloInfo ) ( * tls . Certificate , error ) {
2016-10-14 14:04:09 +00:00
domain := types . CanonicalDomain ( clientHello . ServerName )
2016-08-18 12:20:11 +00:00
account := a . store . Get ( ) . ( * Account )
2016-10-14 14:04:09 +00:00
if challengeCert , ok := a . challengeProvider . getCertificate ( domain ) ; ok {
log . Debugf ( "ACME got challenge %s" , domain )
2016-08-18 12:20:11 +00:00
return challengeCert , nil
}
2016-10-14 14:04:09 +00:00
if domainCert , ok := account . DomainsCertificate . getCertificateForDomain ( domain ) ; ok {
log . Debugf ( "ACME got domain cert %s" , domain )
2016-08-18 12:20:11 +00:00
return domainCert . tlsCert , nil
}
if a . OnDemand {
2016-10-14 14:04:09 +00:00
if a . checkOnDemandDomain != nil && ! a . checkOnDemandDomain ( domain ) {
2016-08-18 12:20:11 +00:00
return nil , nil
}
return a . loadCertificateOnDemand ( clientHello )
}
2016-10-14 14:04:09 +00:00
log . Debugf ( "ACME got nothing %s" , domain )
2016-08-18 12:20:11 +00:00
return nil , nil
}
func ( a * ACME ) retrieveCertificates ( ) {
2016-03-22 00:32:02 +00:00
log . Infof ( "Retrieving ACME certificates..." )
for _ , domain := range a . Domains {
// check if cert isn't already loaded
2016-08-18 12:20:11 +00:00
account := a . store . Get ( ) . ( * Account )
if _ , exists := account . DomainsCertificate . exists ( domain ) ; ! exists {
2016-03-22 00:32:02 +00:00
domains := [ ] string { }
domains = append ( domains , domain . Main )
domains = append ( domains , domain . SANs ... )
2016-08-18 12:20:11 +00:00
certificateResource , err := a . getDomainsCertificates ( domains )
2016-03-22 00:32:02 +00:00
if err != nil {
log . Errorf ( "Error getting ACME certificate for domain %s: %s" , domains , err . Error ( ) )
continue
}
2016-08-18 12:20:11 +00:00
transaction , object , err := a . store . Begin ( )
if err != nil {
log . Errorf ( "Error creating ACME store transaction from domain %s: %s" , domain , err . Error ( ) )
continue
}
account = object . ( * Account )
_ , err = account . DomainsCertificate . addCertificateForDomains ( certificateResource , domain )
2016-03-22 00:32:02 +00:00
if err != nil {
log . Errorf ( "Error adding ACME certificate for domain %s: %s" , domains , err . Error ( ) )
continue
}
2016-08-18 12:20:11 +00:00
if err = transaction . Commit ( account ) ; err != nil {
log . Errorf ( "Error Saving ACME account %+v: %s" , account , err . Error ( ) )
2016-03-22 00:32:02 +00:00
continue
}
}
}
log . Infof ( "Retrieved ACME certificates" )
}
2016-08-18 12:20:11 +00:00
func ( a * ACME ) renewCertificates ( ) error {
2016-06-20 11:55:50 +00:00
log . Debugf ( "Testing certificate renew..." )
2016-08-18 12:20:11 +00:00
account := a . store . Get ( ) . ( * Account )
for _ , certificateResource := range account . DomainsCertificate . Certs {
2016-03-31 11:43:48 +00:00
if certificateResource . needRenew ( ) {
2016-03-21 10:10:18 +00:00
log . Debugf ( "Renewing certificate %+v" , certificateResource . Domains )
2016-08-18 12:20:11 +00:00
renewedCert , err := a . client . RenewCertificate ( acme . CertificateResource {
2016-03-21 10:10:18 +00:00
Domain : certificateResource . Certificate . Domain ,
CertURL : certificateResource . Certificate . CertURL ,
CertStableURL : certificateResource . Certificate . CertStableURL ,
PrivateKey : certificateResource . Certificate . PrivateKey ,
Certificate : certificateResource . Certificate . Certificate ,
2016-06-20 11:55:50 +00:00
} , true )
2016-03-21 10:10:18 +00:00
if err != nil {
2016-06-20 11:55:50 +00:00
log . Errorf ( "Error renewing certificate: %v" , err )
continue
2016-03-21 10:10:18 +00:00
}
log . Debugf ( "Renewed certificate %+v" , certificateResource . Domains )
renewedACMECert := & Certificate {
Domain : renewedCert . Domain ,
CertURL : renewedCert . CertURL ,
CertStableURL : renewedCert . CertStableURL ,
PrivateKey : renewedCert . PrivateKey ,
Certificate : renewedCert . Certificate ,
}
2016-11-07 20:51:23 +00:00
transaction , object , err := a . store . Begin ( )
if err != nil {
return err
}
account = object . ( * Account )
2016-08-18 12:20:11 +00:00
err = account . DomainsCertificate . renewCertificates ( renewedACMECert , certificateResource . Domains )
2016-03-21 10:10:18 +00:00
if err != nil {
2016-06-20 11:55:50 +00:00
log . Errorf ( "Error renewing certificate: %v" , err )
continue
2016-03-21 10:10:18 +00:00
}
2016-08-18 12:20:11 +00:00
if err = transaction . Commit ( account ) ; err != nil {
log . Errorf ( "Error Saving ACME account %+v: %s" , account , err . Error ( ) )
2016-06-20 11:55:50 +00:00
continue
2016-03-21 10:10:18 +00:00
}
}
}
return nil
}
2016-08-18 12:20:11 +00:00
func ( a * ACME ) buildACMEClient ( account * Account ) ( * acme . Client , error ) {
log . Debugf ( "Building ACME client..." )
2016-03-21 10:10:18 +00:00
caServer := "https://acme-v01.api.letsencrypt.org/directory"
if len ( a . CAServer ) > 0 {
caServer = a . CAServer
}
2016-08-18 12:20:11 +00:00
client , err := acme . NewClient ( caServer , account , acme . RSA4096 )
if err != nil {
return nil , err
}
client . ExcludeChallenges ( [ ] acme . Challenge { acme . HTTP01 , acme . DNS01 } )
err = client . SetChallengeProvider ( acme . TLSSNI01 , a . challengeProvider )
2016-03-21 10:10:18 +00:00
if err != nil {
return nil , err
}
return client , nil
}
2016-08-05 18:42:45 +00:00
func ( a * ACME ) loadCertificateOnDemand ( clientHello * tls . ClientHelloInfo ) ( * tls . Certificate , error ) {
2016-10-14 14:04:09 +00:00
domain := types . CanonicalDomain ( clientHello . ServerName )
2016-08-18 12:20:11 +00:00
account := a . store . Get ( ) . ( * Account )
2016-10-14 14:04:09 +00:00
if certificateResource , ok := account . DomainsCertificate . getCertificateForDomain ( domain ) ; ok {
2016-03-21 10:10:18 +00:00
return certificateResource . tlsCert , nil
}
2016-10-14 14:04:09 +00:00
certificate , err := a . getDomainsCertificates ( [ ] string { domain } )
2016-03-21 10:10:18 +00:00
if err != nil {
return nil , err
}
2016-10-14 14:04:09 +00:00
log . Debugf ( "Got certificate on demand for domain %s" , domain )
2016-08-18 12:20:11 +00:00
transaction , object , err := a . store . Begin ( )
2016-03-21 10:10:18 +00:00
if err != nil {
return nil , err
}
2016-08-18 12:20:11 +00:00
account = object . ( * Account )
2016-10-14 14:04:09 +00:00
cert , err := account . DomainsCertificate . addCertificateForDomains ( certificate , Domain { Main : domain } )
2016-08-18 12:20:11 +00:00
if err != nil {
return nil , err
}
if err = transaction . Commit ( account ) ; err != nil {
2016-03-21 10:10:18 +00:00
return nil , err
}
return cert . tlsCert , nil
}
2016-08-05 18:42:45 +00:00
// LoadCertificateForDomains loads certificates from ACME for given domains
func ( a * ACME ) LoadCertificateForDomains ( domains [ ] string ) {
2016-10-14 14:04:09 +00:00
domains = fun . Map ( types . CanonicalDomain , domains ) . ( [ ] string )
2016-08-05 18:42:45 +00:00
safe . Go ( func ( ) {
2016-09-23 16:27:01 +00:00
operation := func ( ) error {
if a . client == nil {
return fmt . Errorf ( "ACME client still not built" )
}
return nil
}
notify := func ( err error , time time . Duration ) {
log . Errorf ( "Error getting ACME client: %v, retrying in %s" , err , time )
}
ebo := backoff . NewExponentialBackOff ( )
ebo . MaxElapsedTime = 30 * time . Second
err := backoff . RetryNotify ( operation , ebo , notify )
if err != nil {
log . Errorf ( "Error getting ACME client: %v" , err )
return
}
2016-08-18 12:20:11 +00:00
account := a . store . Get ( ) . ( * Account )
2016-08-05 18:42:45 +00:00
var domain Domain
if len ( domains ) == 0 {
// no domain
return
} else if len ( domains ) > 1 {
domain = Domain { Main : domains [ 0 ] , SANs : domains [ 1 : ] }
} else {
domain = Domain { Main : domains [ 0 ] }
}
2016-08-18 12:20:11 +00:00
if _ , exists := account . DomainsCertificate . exists ( domain ) ; exists {
2016-08-05 18:42:45 +00:00
// domain already exists
return
}
2016-08-18 12:20:11 +00:00
certificate , err := a . getDomainsCertificates ( domains )
2016-08-05 18:42:45 +00:00
if err != nil {
log . Errorf ( "Error getting ACME certificates %+v : %v" , domains , err )
return
}
log . Debugf ( "Got certificate for domains %+v" , domains )
2016-08-18 12:20:11 +00:00
transaction , object , err := a . store . Begin ( )
if err != nil {
log . Errorf ( "Error creating transaction %+v : %v" , domains , err )
return
}
account = object . ( * Account )
_ , err = account . DomainsCertificate . addCertificateForDomains ( certificate , domain )
2016-08-05 18:42:45 +00:00
if err != nil {
log . Errorf ( "Error adding ACME certificates %+v : %v" , domains , err )
return
}
2016-08-18 12:20:11 +00:00
if err = transaction . Commit ( account ) ; err != nil {
log . Errorf ( "Error Saving ACME account %+v: %v" , account , err )
2016-08-05 18:42:45 +00:00
return
}
} )
}
2016-08-18 12:20:11 +00:00
func ( a * ACME ) getDomainsCertificates ( domains [ ] string ) ( * Certificate , error ) {
2016-10-14 14:04:09 +00:00
domains = fun . Map ( types . CanonicalDomain , domains ) . ( [ ] string )
2016-03-21 10:10:18 +00:00
log . Debugf ( "Loading ACME certificates %s..." , domains )
2016-05-03 00:01:10 +00:00
bundle := true
2016-08-18 12:20:11 +00:00
certificate , failures := a . client . ObtainCertificate ( domains , bundle , nil )
2016-03-21 10:10:18 +00:00
if len ( failures ) > 0 {
log . Error ( failures )
return nil , fmt . Errorf ( "Cannot obtain certificates %s+v" , failures )
}
log . Debugf ( "Loaded ACME certificates %s" , domains )
return & Certificate {
Domain : certificate . Domain ,
CertURL : certificate . CertURL ,
CertStableURL : certificate . CertStableURL ,
PrivateKey : certificate . PrivateKey ,
Certificate : certificate . Certificate ,
} , nil
}