package https import ( "encoding/json" "errors" "fmt" "io/ioutil" "net" "sync" "time" "github.com/miekg/coredns/server" "github.com/xenolf/lego/acme" ) // acmeMu ensures that only one ACME challenge occurs at a time. var acmeMu sync.Mutex // ACMEClient is an acme.Client with custom state attached. type ACMEClient struct { *acme.Client AllowPrompts bool // if false, we assume AlternatePort must be used } // NewACMEClient creates a new ACMEClient given an email and whether // prompting the user is allowed. Clients should not be kept and // re-used over long periods of time, but immediate re-use is more // efficient than re-creating on every iteration. var NewACMEClient = func(email string, allowPrompts bool) (*ACMEClient, error) { // Look up or create the LE user account leUser, err := getUser(email) if err != nil { return nil, err } // The client facilitates our communication with the CA server. client, err := acme.NewClient(CAUrl, &leUser, KeyType) if err != nil { return nil, err } // If not registered, the user must register an account with the CA // and agree to terms if leUser.Registration == nil { reg, err := client.Register() if err != nil { return nil, errors.New("registration error: " + err.Error()) } leUser.Registration = reg if allowPrompts { // can't prompt a user who isn't there if !Agreed && reg.TosURL == "" { Agreed = promptUserAgreement(saURL, false) // TODO - latest URL } if !Agreed && reg.TosURL == "" { return nil, errors.New("user must agree to terms") } } err = client.AgreeToTOS() if err != nil { saveUser(leUser) // Might as well try, right? return nil, errors.New("error agreeing to terms: " + err.Error()) } // save user to the file system err = saveUser(leUser) if err != nil { return nil, errors.New("could not save user: " + err.Error()) } } return &ACMEClient{ Client: client, AllowPrompts: allowPrompts, }, nil } // NewACMEClientGetEmail creates a new ACMEClient and gets an email // address at the same time (a server config is required, since it // may contain an email address in it). func NewACMEClientGetEmail(config server.Config, allowPrompts bool) (*ACMEClient, error) { return NewACMEClient(getEmail(config, allowPrompts), allowPrompts) } // Configure configures c according to bindHost, which is the host (not // whole address) to bind the listener to in solving the http and tls-sni // challenges. func (c *ACMEClient) Configure(bindHost string) { // If we allow prompts, operator must be present. In our case, // that is synonymous with saying the server is not already // started. So if the user is still there, we don't use // AlternatePort because we don't need to proxy the challenges. // Conversely, if the operator is not there, the server has // already started and we need to proxy the challenge. if c.AllowPrompts { // Operator is present; server is not already listening c.SetHTTPAddress(net.JoinHostPort(bindHost, "")) c.SetTLSAddress(net.JoinHostPort(bindHost, "")) //c.ExcludeChallenges([]acme.Challenge{acme.DNS01}) } else { // Operator is not present; server is started, so proxy challenges c.SetHTTPAddress(net.JoinHostPort(bindHost, AlternatePort)) c.SetTLSAddress(net.JoinHostPort(bindHost, AlternatePort)) //c.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01}) } c.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01}) // TODO: can we proxy TLS challenges? and we should support DNS... } // Obtain obtains a single certificate for names. It stores the certificate // on the disk if successful. func (c *ACMEClient) Obtain(names []string) error { Attempts: for attempts := 0; attempts < 2; attempts++ { acmeMu.Lock() certificate, failures := c.ObtainCertificate(names, true, nil) acmeMu.Unlock() if len(failures) > 0 { // Error - try to fix it or report it to the user and abort var errMsg string // we'll combine all the failures into a single error message var promptedForAgreement bool // only prompt user for agreement at most once for errDomain, obtainErr := range failures { // TODO: Double-check, will obtainErr ever be nil? if tosErr, ok := obtainErr.(acme.TOSError); ok { // Terms of Service agreement error; we can probably deal with this if !Agreed && !promptedForAgreement && c.AllowPrompts { Agreed = promptUserAgreement(tosErr.Detail, true) // TODO: Use latest URL promptedForAgreement = true } if Agreed || !c.AllowPrompts { err := c.AgreeToTOS() if err != nil { return errors.New("error agreeing to updated terms: " + err.Error()) } continue Attempts } } // If user did not agree or it was any other kind of error, just append to the list of errors errMsg += "[" + errDomain + "] failed to get certificate: " + obtainErr.Error() + "\n" } return errors.New(errMsg) } // Success - immediately save the certificate resource err := saveCertResource(certificate) if err != nil { return fmt.Errorf("error saving assets for %v: %v", names, err) } break } return nil } // Renew renews the managed certificate for name. Right now our storage // mechanism only supports one name per certificate, so this function only // accepts one domain as input. It can be easily modified to support SAN // certificates if, one day, they become desperately needed enough that our // storage mechanism is upgraded to be more complex to support SAN certs. // // Anyway, this function is safe for concurrent use. func (c *ACMEClient) Renew(name string) error { // Prepare for renewal (load PEM cert, key, and meta) certBytes, err := ioutil.ReadFile(storage.SiteCertFile(name)) if err != nil { return err } keyBytes, err := ioutil.ReadFile(storage.SiteKeyFile(name)) if err != nil { return err } metaBytes, err := ioutil.ReadFile(storage.SiteMetaFile(name)) if err != nil { return err } var certMeta acme.CertificateResource err = json.Unmarshal(metaBytes, &certMeta) certMeta.Certificate = certBytes certMeta.PrivateKey = keyBytes // Perform renewal and retry if necessary, but not too many times. var newCertMeta acme.CertificateResource var success bool for attempts := 0; attempts < 2; attempts++ { acmeMu.Lock() newCertMeta, err = c.RenewCertificate(certMeta, true) acmeMu.Unlock() if err == nil { success = true break } // If the legal terms changed and need to be agreed to again, // we can handle that. if _, ok := err.(acme.TOSError); ok { err := c.AgreeToTOS() if err != nil { return err } continue } // For any other kind of error, wait 10s and try again. time.Sleep(10 * time.Second) } if !success { return errors.New("too many renewal attempts; last error: " + err.Error()) } return saveCertResource(newCertMeta) }