aboutsummaryrefslogtreecommitdiff
path: root/core/https/user.go
diff options
context:
space:
mode:
Diffstat (limited to 'core/https/user.go')
-rw-r--r--core/https/user.go200
1 files changed, 200 insertions, 0 deletions
diff --git a/core/https/user.go b/core/https/user.go
new file mode 100644
index 000000000..13d93b1da
--- /dev/null
+++ b/core/https/user.go
@@ -0,0 +1,200 @@
+package https
+
+import (
+ "bufio"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "strings"
+
+ "github.com/miekg/coredns/server"
+ "github.com/xenolf/lego/acme"
+)
+
+// User represents a Let's Encrypt user account.
+type User struct {
+ Email string
+ Registration *acme.RegistrationResource
+ key crypto.PrivateKey
+}
+
+// GetEmail gets u's email.
+func (u User) GetEmail() string {
+ return u.Email
+}
+
+// GetRegistration gets u's registration resource.
+func (u User) GetRegistration() *acme.RegistrationResource {
+ return u.Registration
+}
+
+// GetPrivateKey gets u's private key.
+func (u User) GetPrivateKey() crypto.PrivateKey {
+ return u.key
+}
+
+// getUser loads the user with the given email from disk.
+// 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. It does NOT prompt the user.
+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
+ user.key, err = loadPrivateKey(storage.UserKeyFile(email))
+ if err != nil {
+ return user, err
+ }
+
+ return user, nil
+}
+
+// saveUser persists a user's key and account registration
+// to the file system. It does NOT register the user via ACME
+// or prompt the user.
+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
+ err = savePrivateKey(user.key, storage.UserKeyFile(user.Email))
+ 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
+// 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. It does NOT prompt the user.
+func newUser(email string) (User, error) {
+ user := User{Email: email}
+ privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
+ if err != nil {
+ return user, errors.New("error generating private key: " + err.Error())
+ }
+ user.key = privateKey
+ return user, nil
+}
+
+// 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.
+// (It will warn the user of the consequences of an
+// empty email.) This function MAY prompt the user for
+// input. If userPresent is false, the operator will
+// NOT be prompted and an empty email may be returned.
+func getEmail(cfg server.Config, userPresent bool) 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()) {
+ leEmail = dir.Name()
+ DefaultEmail = leEmail // save for next time
+ }
+ }
+ }
+ }
+ if leEmail == "" && userPresent {
+ // Alas, we must bother the user and ask for an email address;
+ // if they proceed they also agree to the SA.
+ reader := bufio.NewReader(stdin)
+ fmt.Println("\nYour 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(" " + 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'll lose the ability to recover your account.")
+ fmt.Print("Email address: ")
+ var err error
+ leEmail, err = reader.ReadString('\n')
+ if err != nil {
+ return ""
+ }
+ leEmail = strings.TrimSpace(leEmail)
+ DefaultEmail = leEmail
+ Agreed = true
+ }
+ return leEmail
+}
+
+// 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 {
+ 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.Print("Do you agree to the terms? (y/n): ")
+ }
+
+ reader := bufio.NewReader(stdin)
+ answer, err := reader.ReadString('\n')
+ if err != nil {
+ return false
+ }
+ answer = strings.ToLower(strings.TrimSpace(answer))
+
+ return answer == "y" || answer == "yes"
+}
+
+// stdin is used to read the user's input if prompted;
+// this is changed by tests during tests.
+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"