mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-31 02:27:19 -04:00 
			
		
		
		
	- Using xenolf/lego's likely-temporary acmev2 branch - Cleaned up vendor folder a little bit (probably more to do) - Temporarily set default CA URL to v2 staging endpoint - Refactored user management a bit; updated tests (biggest change is how we get the email address, which now requires being able to make an ACME client with a User with a private key so that we can get the current ToS URL) - Automatic HTTPS now allows specific wildcard pattern hostnames - Commented out (but kept) the TLS-SNI code, as the challenge type may return in the future in a similar form
		
			
				
	
	
		
			234 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			234 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2015 Light Code Labs, LLC
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| package caddytls
 | |
| 
 | |
| import (
 | |
| 	"bufio"
 | |
| 	"crypto"
 | |
| 	"crypto/ecdsa"
 | |
| 	"crypto/elliptic"
 | |
| 	"crypto/rand"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/xenolf/lego/acmev2"
 | |
| )
 | |
| 
 | |
| // 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
 | |
| }
 | |
| 
 | |
| // 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 within the scope of memory and storage to use
 | |
| // for ACME TLS. If it cannot get an email address, it returns
 | |
| // empty string. (If user is present, 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.
 | |
| // If the user is prompted, a new User will be created and
 | |
| // stored in storage according to the email address they
 | |
| // provided (which might be blank).
 | |
| func getEmail(cfg *Config, userPresent bool) (string, error) {
 | |
| 	storage, err := cfg.StorageFor(cfg.CAUrl)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	// First try memory (command line flag or typed by user previously)
 | |
| 	leEmail := DefaultEmail
 | |
| 
 | |
| 	// Then try to get most recent user email from storage
 | |
| 	if leEmail == "" {
 | |
| 		leEmail = storage.MostRecentUserEmail()
 | |
| 		DefaultEmail = leEmail // save for next time
 | |
| 	}
 | |
| 
 | |
| 	// Looks like there is no email address readily available,
 | |
| 	// so we will have to ask the user if we can.
 | |
| 	if leEmail == "" && userPresent {
 | |
| 		// evidently, no User data was present in storage;
 | |
| 		// thus we must make a new User so that we can get
 | |
| 		// the Terms of Service URL via our ACME client, phew!
 | |
| 		user, err := newUser("")
 | |
| 		if err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 
 | |
| 		// get the agreement URL
 | |
| 		agreementURL := agreementTestURL
 | |
| 		if agreementURL == "" {
 | |
| 			// we call acme.NewClient directly because newACMEClient
 | |
| 			// would require that we already know the user's email
 | |
| 			caURL := DefaultCAUrl
 | |
| 			if cfg.CAUrl != "" {
 | |
| 				caURL = cfg.CAUrl
 | |
| 			}
 | |
| 			tempClient, err := acme.NewClient(caURL, user, "")
 | |
| 			if err != nil {
 | |
| 				return "", fmt.Errorf("making ACME client to get ToS URL: %v", err)
 | |
| 			}
 | |
| 			agreementURL = tempClient.GetToSURL()
 | |
| 		}
 | |
| 
 | |
| 		// prompt the user for an email address and terms agreement
 | |
| 		reader := bufio.NewReader(stdin)
 | |
| 		promptUserAgreement(agreementURL)
 | |
| 		fmt.Println("Please enter your email address to signify agreement and to be notified")
 | |
| 		fmt.Println("in case of issues. You can leave it blank, but we don't recommend it.")
 | |
| 		fmt.Print("  Email address: ")
 | |
| 		leEmail, err = reader.ReadString('\n')
 | |
| 		if err != nil && err != io.EOF {
 | |
| 			return "", fmt.Errorf("reading email address: %v", err)
 | |
| 		}
 | |
| 		leEmail = strings.TrimSpace(leEmail)
 | |
| 		DefaultEmail = leEmail
 | |
| 		Agreed = true
 | |
| 
 | |
| 		// save the new user to preserve this for next time
 | |
| 		user.Email = leEmail
 | |
| 		err = saveUser(storage, user)
 | |
| 		if err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// lower-casing the email is important for consistency
 | |
| 	return strings.ToLower(leEmail), nil
 | |
| }
 | |
| 
 | |
| // getUser loads the user with the given email from disk
 | |
| // using the provided storage. 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(storage Storage, email string) (User, error) {
 | |
| 	var user User
 | |
| 
 | |
| 	// open user reg
 | |
| 	userData, err := storage.LoadUser(email)
 | |
| 	if err != nil {
 | |
| 		if _, ok := err.(ErrNotExist); ok {
 | |
| 			// create a new user
 | |
| 			return newUser(email)
 | |
| 		}
 | |
| 		return user, err
 | |
| 	}
 | |
| 
 | |
| 	// load user information
 | |
| 	err = json.Unmarshal(userData.Reg, &user)
 | |
| 	if err != nil {
 | |
| 		return user, err
 | |
| 	}
 | |
| 
 | |
| 	// load their private key
 | |
| 	user.key, err = loadPrivateKey(userData.Key)
 | |
| 	return user, err
 | |
| }
 | |
| 
 | |
| // 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. You must also pass in the storage
 | |
| // wherein the user should be saved. It should be the storage
 | |
| // for the CA with which user has an account.
 | |
| func saveUser(storage Storage, user User) error {
 | |
| 	// Save the private key and registration
 | |
| 	userData := new(UserData)
 | |
| 	var err error
 | |
| 	userData.Key, err = savePrivateKey(user.key)
 | |
| 	if err == nil {
 | |
| 		userData.Reg, err = json.MarshalIndent(&user, "", "\t")
 | |
| 	}
 | |
| 	if err == nil {
 | |
| 		err = storage.StoreUser(user.Email, userData)
 | |
| 	}
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| // promptUserAgreement simply outputs the standard user
 | |
| // agreement prompt with the given agreement URL.
 | |
| // It outputs a newline after the message.
 | |
| func promptUserAgreement(agreementURL string) {
 | |
| 	const userAgreementPrompt = `Your sites will be served over HTTPS automatically using Let's Encrypt.
 | |
| By continuing, you agree to the Let's Encrypt Subscriber Agreement at:`
 | |
| 	fmt.Printf("\n\n%s\n  %s\n", userAgreementPrompt, agreementURL)
 | |
| }
 | |
| 
 | |
| // askUserAgreement prompts the user to agree to the agreement
 | |
| // at the given agreement URL via stdin. It returns whether the
 | |
| // user agreed or not.
 | |
| func askUserAgreement(agreementURL string) bool {
 | |
| 	promptUserAgreement(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"
 | |
| }
 | |
| 
 | |
| // agreementTestURL is set during tests to skip requiring
 | |
| // setting up an entire ACME CA endpoint.
 | |
| var agreementTestURL string
 | |
| 
 | |
| // 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,
 | |
| // but only for local/storage use, not with the CA.
 | |
| const emptyEmail = "default"
 |