From be0fb0053d8523a8781f8e8dd43690caf4777dc6 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 2 Nov 2015 11:06:42 -0700 Subject: [PATCH] letsencrypt: Re-prompt user if obtaining certs fails due to updated SA --- caddy/letsencrypt/letsencrypt.go | 31 +++++++++++++++++++++++++++---- caddy/letsencrypt/maintain.go | 13 +++++++++++-- caddy/letsencrypt/user.go | 11 +++++++---- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/caddy/letsencrypt/letsencrypt.go b/caddy/letsencrypt/letsencrypt.go index 35b24d78..fa19a81f 100644 --- a/caddy/letsencrypt/letsencrypt.go +++ b/caddy/letsencrypt/letsencrypt.go @@ -7,7 +7,6 @@ import ( "encoding/json" "errors" "io/ioutil" - "log" "net/http" "os" "strings" @@ -84,11 +83,35 @@ func Activate(configs []server.Config) ([]server.Config, error) { } // client is ready, so let's get free, trusted SSL certificates! yeah! + Obtain: certificates, failures := obtainCertificates(client, serverConfigs) if len(failures) > 0 { - for k, v := range failures { - log.Printf("[%s] Failed to get a certificate: %s", k, v) + // Build an error string to return, using all the failures in the list. + var errMsg string + + // An agreement error means we need to prompt the user (once) with updated terms + // while they're still here. + var promptedUpdatedTerms bool + + for domain, obtainErr := range failures { + // If the failure was simply because the terms have changed, re-prompt and re-try + if tosErr, ok := obtainErr.(acme.TOSError); ok && !promptedUpdatedTerms { + Agreed = promptUserAgreement(tosErr.Detail, true) // TODO: Use latest URL + promptedUpdatedTerms = true + if Agreed { + err := client.AgreeToTOS() + if err != nil { + return configs, errors.New("error agreeing to updated terms: " + err.Error()) + } + goto Obtain + } + } + + // If user did not agree or it was any other kind of error, just append to the list of errors + errMsg += "[" + domain + "] failed to get certificate: " + obtainErr.Error() + "\n" } + + return configs, errors.New(errMsg) } // ... that's it. save the certs, keys, and metadata files to disk @@ -213,7 +236,7 @@ func newClient(leEmail string) (*acme.Client, error) { leUser.Registration = reg if !Agreed && reg.TosURL == "" { - Agreed = promptUserAgreement("", false) // TODO + Agreed = promptUserAgreement(saURL, false) // TODO - latest URL } if !Agreed && reg.TosURL == "" { return nil, errors.New("user must agree to terms") diff --git a/caddy/letsencrypt/maintain.go b/caddy/letsencrypt/maintain.go index ae364749..7642a238 100644 --- a/caddy/letsencrypt/maintain.go +++ b/caddy/letsencrypt/maintain.go @@ -97,8 +97,8 @@ func renewCertificates(configs []server.Config) (int, []error) { // Directly convert it to days for the following checks. daysLeft := int(expTime.Sub(time.Now().UTC()).Hours() / 24) - // Renew with a week or less remaining. - if daysLeft <= 7 { + // Renew with two weeks or less remaining. + if daysLeft <= 14 { log.Printf("[INFO] There are %d days left on the certificate of %s. Trying to renew now.", daysLeft, cfg.Host) client, err := newClient("") // email not used for renewal if err != nil { @@ -127,8 +127,17 @@ func renewCertificates(configs []server.Config) (int, []error) { // Renew certificate. // TODO: revokeOld should be an option in the caddyfile // TODO: bundle should be an option in the caddyfile as well :) + Renew: newCertMeta, err := client.RenewCertificate(certMeta, true, true) if err != nil { + if _, ok := err.(acme.TOSError); ok { + err := client.AgreeToTOS() + if err != nil { + errs = append(errs, err) + } + goto Renew + } + time.Sleep(10 * time.Second) newCertMeta, err = client.RenewCertificate(certMeta, true, true) if err != nil { diff --git a/caddy/letsencrypt/user.go b/caddy/letsencrypt/user.go index 4d4d4829..7fae3bb4 100644 --- a/caddy/letsencrypt/user.go +++ b/caddy/letsencrypt/user.go @@ -146,9 +146,9 @@ func getEmail(cfg server.Config) string { reader := bufio.NewReader(stdin) fmt.Println("Your sites will be served over HTTPS automatically using Let's Encrypt.") fmt.Println("By continuing, you agree to the Let's Encrypt Subscriber Agreement at:") - fmt.Println(" https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf") // TODO: Show current SA link + fmt.Println(" " + saURL) // TODO: Show current SA link fmt.Println("Please enter your email address so you can recover your account if needed.") - fmt.Println("You can leave it blank, but you lose the ability to recover your account.") + fmt.Println("You can leave it blank, but you'll lose the ability to recover your account.") fmt.Print("Email address: ") var err error leEmail, err = reader.ReadString('\n') @@ -167,10 +167,10 @@ func getEmail(cfg server.Config) string { // agreeing, pass false. It returns whether the user agreed or not. func promptUserAgreement(agreementURL string, changed bool) bool { if changed { - fmt.Printf("The Let's Encrypt Subscriber Agreement has changed:\n%s\n", agreementURL) + fmt.Printf("The Let's Encrypt Subscriber Agreement has changed:\n %s\n", agreementURL) fmt.Print("Do you agree to the new terms? (y/n): ") } else { - fmt.Printf("To continue, you must agree to the Let's Encrypt Subscriber Agreement:\n%s\n", agreementURL) + fmt.Printf("To continue, you must agree to the Let's Encrypt Subscriber Agreement:\n %s\n", agreementURL) fmt.Print("Do you agree to the terms? (y/n): ") } @@ -191,3 +191,6 @@ var stdin = io.ReadWriter(os.Stdin) // The name of the folder for accounts where the email // address was not provided; default 'username' if you will. const emptyEmail = "default" + +// TODO: Use latest +const saURL = "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"