2015-10-17 21:17:24 -05:00
|
|
|
package letsencrypt
|
|
|
|
|
|
|
|
import (
|
2015-10-17 21:44:33 -05:00
|
|
|
"bufio"
|
2015-10-17 21:17:24 -05:00
|
|
|
"crypto/rand"
|
|
|
|
"crypto/rsa"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
2015-10-17 21:44:33 -05:00
|
|
|
"fmt"
|
2015-10-18 13:09:06 -05:00
|
|
|
"io"
|
2015-10-17 21:17:24 -05:00
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
2015-10-17 21:44:33 -05:00
|
|
|
"strings"
|
2015-10-17 21:17:24 -05:00
|
|
|
|
2015-10-17 21:44:33 -05:00
|
|
|
"github.com/mholt/caddy/server"
|
2015-10-17 21:17:24 -05:00
|
|
|
"github.com/xenolf/lego/acme"
|
|
|
|
)
|
|
|
|
|
2015-10-18 13:09:06 -05:00
|
|
|
// User represents a Let's Encrypt user account.
|
2015-10-17 21:17:24 -05:00
|
|
|
type User struct {
|
|
|
|
Email string
|
|
|
|
Registration *acme.RegistrationResource
|
|
|
|
key *rsa.PrivateKey
|
|
|
|
}
|
|
|
|
|
2015-10-18 13:09:06 -05:00
|
|
|
// GetEmail gets u's email.
|
2015-10-17 21:17:24 -05:00
|
|
|
func (u User) GetEmail() string {
|
|
|
|
return u.Email
|
|
|
|
}
|
2015-10-18 13:09:06 -05:00
|
|
|
|
|
|
|
// GetRegistration gets u's registration resource.
|
2015-10-17 21:17:24 -05:00
|
|
|
func (u User) GetRegistration() *acme.RegistrationResource {
|
|
|
|
return u.Registration
|
|
|
|
}
|
2015-10-18 13:09:06 -05:00
|
|
|
|
|
|
|
// GetPrivateKey gets u's private key.
|
2015-10-17 21:17:24 -05:00
|
|
|
func (u User) GetPrivateKey() *rsa.PrivateKey {
|
|
|
|
return u.key
|
|
|
|
}
|
|
|
|
|
|
|
|
// getUser loads the user with the given email from disk.
|
2015-10-18 13:09:06 -05:00
|
|
|
// If the user does not exist, it will create a new one,
|
|
|
|
// but it does NOT save new users to the disk or register
|
|
|
|
// them via ACME.
|
2015-10-17 21:17:24 -05:00
|
|
|
func getUser(email string) (User, error) {
|
|
|
|
var user User
|
|
|
|
|
|
|
|
// open user file
|
|
|
|
regFile, err := os.Open(storage.UserRegFile(email))
|
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
// create a new user
|
|
|
|
return newUser(email)
|
|
|
|
}
|
|
|
|
return user, err
|
|
|
|
}
|
|
|
|
defer regFile.Close()
|
|
|
|
|
|
|
|
// load user information
|
|
|
|
err = json.NewDecoder(regFile).Decode(&user)
|
|
|
|
if err != nil {
|
|
|
|
return user, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// load their private key
|
2015-11-01 12:58:58 -05:00
|
|
|
user.key, err = loadRSAPrivateKey(storage.UserKeyFile(email))
|
2015-10-17 21:17:24 -05:00
|
|
|
if err != nil {
|
|
|
|
return user, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return user, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// saveUser persists a user's key and account registration
|
2015-10-17 21:44:33 -05:00
|
|
|
// to the file system. It does NOT register the user via ACME.
|
2015-10-17 21:17:24 -05:00
|
|
|
func saveUser(user User) error {
|
|
|
|
// make user account folder
|
|
|
|
err := os.MkdirAll(storage.User(user.Email), 0700)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// save private key file
|
2015-11-01 12:58:58 -05:00
|
|
|
err = saveRSAPrivateKey(user.key, storage.UserKeyFile(user.Email))
|
2015-10-17 21:17:24 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// save registration file
|
|
|
|
jsonBytes, err := json.MarshalIndent(&user, "", "\t")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return ioutil.WriteFile(storage.UserRegFile(user.Email), jsonBytes, 0600)
|
|
|
|
}
|
|
|
|
|
|
|
|
// newUser creates a new User for the given email address
|
2015-10-17 21:44:33 -05:00
|
|
|
// with a new private key. This function does NOT save the
|
|
|
|
// user to disk or register it via ACME. If you want to use
|
|
|
|
// a user account that might already exist, call getUser
|
|
|
|
// instead.
|
2015-10-17 21:17:24 -05:00
|
|
|
func newUser(email string) (User, error) {
|
|
|
|
user := User{Email: email}
|
2015-10-18 13:09:06 -05:00
|
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySizeToUse)
|
2015-10-17 21:17:24 -05:00
|
|
|
if err != nil {
|
|
|
|
return user, errors.New("error generating private key: " + err.Error())
|
|
|
|
}
|
|
|
|
user.key = privateKey
|
|
|
|
return user, nil
|
|
|
|
}
|
2015-10-17 21:44:33 -05:00
|
|
|
|
|
|
|
// getEmail does everything it can to obtain an email
|
|
|
|
// address from the user to use for TLS for cfg. If it
|
|
|
|
// cannot get an email address, it returns empty string.
|
2015-10-31 00:44:00 -05:00
|
|
|
// (It will warn the user of the consequences of an
|
|
|
|
// empty email.)
|
2015-10-17 21:44:33 -05:00
|
|
|
func getEmail(cfg server.Config) string {
|
|
|
|
// First try the tls directive from the Caddyfile
|
|
|
|
leEmail := cfg.TLS.LetsEncryptEmail
|
|
|
|
if leEmail == "" {
|
|
|
|
// Then try memory (command line flag or typed by user previously)
|
|
|
|
leEmail = DefaultEmail
|
|
|
|
}
|
|
|
|
if leEmail == "" {
|
|
|
|
// Then try to get most recent user email ~/.caddy/users file
|
|
|
|
userDirs, err := ioutil.ReadDir(storage.Users())
|
|
|
|
if err == nil {
|
|
|
|
var mostRecent os.FileInfo
|
|
|
|
for _, dir := range userDirs {
|
|
|
|
if !dir.IsDir() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if mostRecent == nil || dir.ModTime().After(mostRecent.ModTime()) {
|
|
|
|
mostRecent = dir
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if mostRecent != nil {
|
|
|
|
leEmail = mostRecent.Name()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if leEmail == "" {
|
2015-10-31 14:15:47 -05:00
|
|
|
// Alas, we must bother the user and ask for an email address;
|
|
|
|
// if they proceed they also agree to the SA.
|
2015-10-18 13:09:06 -05:00
|
|
|
reader := bufio.NewReader(stdin)
|
2015-10-31 00:44:00 -05:00
|
|
|
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:")
|
2015-11-02 13:06:42 -05:00
|
|
|
fmt.Println(" " + saURL) // TODO: Show current SA link
|
2015-10-31 00:44:00 -05:00
|
|
|
fmt.Println("Please enter your email address so you can recover your account if needed.")
|
2015-11-02 13:06:42 -05:00
|
|
|
fmt.Println("You can leave it blank, but you'll lose the ability to recover your account.")
|
2015-10-31 00:44:00 -05:00
|
|
|
fmt.Print("Email address: ")
|
2015-10-17 21:44:33 -05:00
|
|
|
var err error
|
|
|
|
leEmail, err = reader.ReadString('\n')
|
|
|
|
if err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
DefaultEmail = leEmail
|
2015-10-31 14:15:47 -05:00
|
|
|
Agreed = true
|
2015-10-17 21:44:33 -05:00
|
|
|
}
|
|
|
|
return strings.TrimSpace(leEmail)
|
|
|
|
}
|
2015-10-18 13:09:06 -05:00
|
|
|
|
2015-10-28 19:12:07 -05:00
|
|
|
// promptUserAgreement prompts the user to agree to the agreement
|
|
|
|
// at agreementURL via stdin. If the agreement has changed, then pass
|
|
|
|
// true as the second argument. If this is the user's first time
|
|
|
|
// agreeing, pass false. It returns whether the user agreed or not.
|
|
|
|
func promptUserAgreement(agreementURL string, changed bool) bool {
|
|
|
|
if changed {
|
2015-11-02 13:06:42 -05:00
|
|
|
fmt.Printf("The Let's Encrypt Subscriber Agreement has changed:\n %s\n", agreementURL)
|
2015-10-28 19:12:07 -05:00
|
|
|
fmt.Print("Do you agree to the new terms? (y/n): ")
|
|
|
|
} else {
|
2015-11-02 13:06:42 -05:00
|
|
|
fmt.Printf("To continue, you must agree to the Let's Encrypt Subscriber Agreement:\n %s\n", agreementURL)
|
2015-10-28 19:12:07 -05:00
|
|
|
fmt.Print("Do you agree to the terms? (y/n): ")
|
|
|
|
}
|
|
|
|
|
2015-10-31 00:44:00 -05:00
|
|
|
reader := bufio.NewReader(stdin)
|
2015-10-28 19:12:07 -05:00
|
|
|
answer, err := reader.ReadString('\n')
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
answer = strings.ToLower(strings.TrimSpace(answer))
|
|
|
|
|
|
|
|
return answer == "y" || answer == "yes"
|
|
|
|
}
|
|
|
|
|
2015-10-18 13:09:06 -05:00
|
|
|
// stdin is used to read the user's input if prompted;
|
|
|
|
// this is changed by tests during tests.
|
|
|
|
var stdin = io.ReadWriter(os.Stdin)
|
2015-10-31 00:44:00 -05:00
|
|
|
|
|
|
|
// The name of the folder for accounts where the email
|
|
|
|
// address was not provided; default 'username' if you will.
|
|
|
|
const emptyEmail = "default"
|
2015-11-02 13:06:42 -05:00
|
|
|
|
|
|
|
// TODO: Use latest
|
|
|
|
const saURL = "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"
|