mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-22 10:25:46 +03:00
Refactor letsencrypt code into its own package
This commit is contained in:
parent
307c2ffe3c
commit
a3a826572f
6 changed files with 318 additions and 168 deletions
|
@ -8,6 +8,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/mholt/caddy/app"
|
"github.com/mholt/caddy/app"
|
||||||
|
"github.com/mholt/caddy/config/letsencrypt"
|
||||||
"github.com/mholt/caddy/config/parse"
|
"github.com/mholt/caddy/config/parse"
|
||||||
"github.com/mholt/caddy/config/setup"
|
"github.com/mholt/caddy/config/setup"
|
||||||
"github.com/mholt/caddy/middleware"
|
"github.com/mholt/caddy/middleware"
|
||||||
|
@ -102,7 +103,7 @@ func Load(filename string, input io.Reader) (Group, error) {
|
||||||
log.SetFlags(flags)
|
log.SetFlags(flags)
|
||||||
|
|
||||||
// secure all the things
|
// secure all the things
|
||||||
configs, err = initiateLetsEncrypt(configs)
|
configs, err = letsencrypt.Activate(configs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -272,12 +273,6 @@ var (
|
||||||
|
|
||||||
// Site port
|
// Site port
|
||||||
Port = DefaultPort
|
Port = DefaultPort
|
||||||
|
|
||||||
// Let's Encrypt account email
|
|
||||||
LetsEncryptEmail string
|
|
||||||
|
|
||||||
// Agreement to Let's Encrypt terms
|
|
||||||
LetsEncryptAgree bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Group maps network addresses to their configurations.
|
// Group maps network addresses to their configurations.
|
||||||
|
|
43
config/letsencrypt/crypto.go
Normal file
43
config/letsencrypt/crypto.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package letsencrypt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// saveCertificate saves a DER-encoded (binary format) certificate
|
||||||
|
// to file.
|
||||||
|
func saveCertificate(certBytes []byte, file string) error {
|
||||||
|
pemCert := pem.Block{Type: "CERTIFICATE", Bytes: certBytes}
|
||||||
|
certOut, err := os.Create(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pem.Encode(certOut, &pemCert)
|
||||||
|
certOut.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadRSAPrivateKey loads a PEM-encoded RSA private key from file.
|
||||||
|
func loadRSAPrivateKey(file string) (*rsa.PrivateKey, error) {
|
||||||
|
keyBytes, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keyBlock, _ := pem.Decode(keyBytes)
|
||||||
|
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveRSAPrivateKey saves a PEM-encoded RSA private key to file.
|
||||||
|
func saveRSAPrivateKey(key *rsa.PrivateKey, file string) error {
|
||||||
|
pemKey := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
|
||||||
|
keyOut, err := os.Create(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer keyOut.Close()
|
||||||
|
return pem.Encode(keyOut, &pemKey)
|
||||||
|
}
|
|
@ -1,47 +1,26 @@
|
||||||
package config
|
package letsencrypt
|
||||||
|
|
||||||
// TODO: This code is a mess but I'm cleaning it up locally and
|
|
||||||
// refactoring a bunch. It will have tests, too. Don't worry. :)
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mholt/caddy/app"
|
|
||||||
"github.com/mholt/caddy/middleware"
|
"github.com/mholt/caddy/middleware"
|
||||||
"github.com/mholt/caddy/middleware/redirect"
|
"github.com/mholt/caddy/middleware/redirect"
|
||||||
"github.com/mholt/caddy/server"
|
"github.com/mholt/caddy/server"
|
||||||
"github.com/xenolf/lego/acme"
|
"github.com/xenolf/lego/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Some essential values related to the Let's Encrypt process
|
// Activate sets up TLS for each server config in configs
|
||||||
const (
|
// as needed. It only skips the config if the cert and key
|
||||||
// Size of RSA keys in bits
|
// are already provided or if plaintext http is explicitly
|
||||||
rsaKeySize = 2048
|
// specified as the port.
|
||||||
|
func Activate(configs []server.Config) ([]server.Config, error) {
|
||||||
// The base URL to the Let's Encrypt CA
|
|
||||||
caURL = "http://192.168.99.100:4000"
|
|
||||||
|
|
||||||
// The port to expose to the CA server for Simple HTTP Challenge
|
|
||||||
exposePort = "5001"
|
|
||||||
)
|
|
||||||
|
|
||||||
// initiateLetsEncrypt sets up TLS for each server config
|
|
||||||
// in configs as needed. It only skips the config if the
|
|
||||||
// cert and key are already specified or if plaintext http
|
|
||||||
// is explicitly specified as the port.
|
|
||||||
func initiateLetsEncrypt(configs []server.Config) ([]server.Config, error) {
|
|
||||||
// populate map of email address to server configs that use that email address for TLS.
|
// populate map of email address to server configs that use that email address for TLS.
|
||||||
// this will help us reduce roundtrips when getting the certs.
|
// this will help us reduce roundtrips when getting the certs.
|
||||||
initMap := make(map[string][]*server.Config)
|
initMap := make(map[string][]*server.Config)
|
||||||
|
@ -59,7 +38,7 @@ func initiateLetsEncrypt(configs []server.Config) ([]server.Config, error) {
|
||||||
// than one certificate per email address, and still save them individually.
|
// than one certificate per email address, and still save them individually.
|
||||||
for leEmail, serverConfigs := range initMap {
|
for leEmail, serverConfigs := range initMap {
|
||||||
// Look up or create the LE user account
|
// Look up or create the LE user account
|
||||||
leUser, err := getLetsEncryptUser(leEmail)
|
leUser, err := getUser(leEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return configs, err
|
return configs, err
|
||||||
}
|
}
|
||||||
|
@ -79,11 +58,11 @@ func initiateLetsEncrypt(configs []server.Config) ([]server.Config, error) {
|
||||||
// TODO: we can just do the agreement once, when registering, right?
|
// TODO: we can just do the agreement once, when registering, right?
|
||||||
err = client.AgreeToTos()
|
err = client.AgreeToTos()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
saveLetsEncryptUser(leUser) // TODO: Might as well try, right? Error check?
|
saveUser(leUser) // TODO: Might as well try, right? Error check?
|
||||||
return configs, errors.New("error agreeing to terms: " + err.Error())
|
return configs, errors.New("error agreeing to terms: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = saveLetsEncryptUser(leUser)
|
err = saveUser(leUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return configs, errors.New("could not save user: " + err.Error())
|
return configs, errors.New("could not save user: " + err.Error())
|
||||||
}
|
}
|
||||||
|
@ -103,17 +82,16 @@ func initiateLetsEncrypt(configs []server.Config) ([]server.Config, error) {
|
||||||
|
|
||||||
// ... that's it. save the certs, keys, and update server configs.
|
// ... that's it. save the certs, keys, and update server configs.
|
||||||
for _, cert := range certificates {
|
for _, cert := range certificates {
|
||||||
certFolder := filepath.Join(app.DataFolder(), "letsencrypt", "sites", cert.Domain)
|
os.MkdirAll(storage.Site(cert.Domain), 0700)
|
||||||
os.MkdirAll(certFolder, 0700)
|
|
||||||
|
|
||||||
// Save cert
|
// Save cert
|
||||||
err = saveCertificate(cert.Certificate, filepath.Join(certFolder, cert.Domain+".crt"))
|
err = saveCertificate(cert.Certificate, storage.SiteCertFile(cert.Domain))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return configs, err
|
return configs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save private key
|
// Save private key
|
||||||
err = ioutil.WriteFile(filepath.Join(certFolder, cert.Domain+".key"), cert.PrivateKey, 0600)
|
err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return configs, err
|
return configs, err
|
||||||
}
|
}
|
||||||
|
@ -123,7 +101,7 @@ func initiateLetsEncrypt(configs []server.Config) ([]server.Config, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return configs, err
|
return configs, err
|
||||||
}
|
}
|
||||||
err = ioutil.WriteFile(filepath.Join(certFolder, cert.Domain+".json"), jsonBytes, 0600)
|
err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return configs, err
|
return configs, err
|
||||||
}
|
}
|
||||||
|
@ -131,8 +109,8 @@ func initiateLetsEncrypt(configs []server.Config) ([]server.Config, error) {
|
||||||
|
|
||||||
// it all comes down to this: filling in the file path of a valid certificate automatically
|
// it all comes down to this: filling in the file path of a valid certificate automatically
|
||||||
for _, cfg := range serverConfigs {
|
for _, cfg := range serverConfigs {
|
||||||
cfg.TLS.Certificate = filepath.Join(app.DataFolder(), "letsencrypt", "sites", cfg.Host, cfg.Host+".crt")
|
cfg.TLS.Certificate = storage.SiteCertFile(cfg.Host)
|
||||||
cfg.TLS.Key = filepath.Join(app.DataFolder(), "letsencrypt", "sites", cfg.Host, cfg.Host+".key")
|
cfg.TLS.Key = storage.SiteKeyFile(cfg.Host)
|
||||||
cfg.TLS.Enabled = true
|
cfg.TLS.Enabled = true
|
||||||
cfg.Port = "https"
|
cfg.Port = "https"
|
||||||
|
|
||||||
|
@ -188,12 +166,12 @@ func getEmail(cfg server.Config) string {
|
||||||
leEmail := cfg.TLS.LetsEncryptEmail
|
leEmail := cfg.TLS.LetsEncryptEmail
|
||||||
if leEmail == "" {
|
if leEmail == "" {
|
||||||
// Then try memory (command line flag or typed by user previously)
|
// Then try memory (command line flag or typed by user previously)
|
||||||
leEmail = LetsEncryptEmail
|
leEmail = DefaultEmail
|
||||||
}
|
}
|
||||||
if leEmail == "" {
|
if leEmail == "" {
|
||||||
// Then try to get most recent user email ~/.caddy/users file
|
// Then try to get most recent user email ~/.caddy/users file
|
||||||
// TODO: Probably better to open the user's json file and read the email out of there...
|
// TODO: Probably better to open the user's json file and read the email out of there...
|
||||||
userDirs, err := ioutil.ReadDir(filepath.Join(app.DataFolder(), "letsencrypt", "users"))
|
userDirs, err := ioutil.ReadDir(storage.Users())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
var mostRecent os.FileInfo
|
var mostRecent os.FileInfo
|
||||||
for _, dir := range userDirs {
|
for _, dir := range userDirs {
|
||||||
|
@ -204,7 +182,9 @@ func getEmail(cfg server.Config) string {
|
||||||
mostRecent = dir
|
mostRecent = dir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
leEmail = mostRecent.Name()
|
if mostRecent != nil {
|
||||||
|
leEmail = mostRecent.Name()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if leEmail == "" {
|
if leEmail == "" {
|
||||||
|
@ -216,135 +196,41 @@ func getEmail(cfg server.Config) string {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
LetsEncryptEmail = leEmail
|
DefaultEmail = leEmail
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(leEmail)
|
return strings.TrimSpace(leEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveLetsEncryptUser(user LetsEncryptUser) error {
|
var (
|
||||||
// make user account folder
|
// Let's Encrypt account email to use if none provided
|
||||||
userFolder := filepath.Join(app.DataFolder(), "letsencrypt", "users", user.Email)
|
DefaultEmail string
|
||||||
err := os.MkdirAll(userFolder, 0700)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// save private key file
|
// Whether user has agreed to the Let's Encrypt SA
|
||||||
user.KeyFile = filepath.Join(userFolder, emailUsername(user.Email)+".key")
|
Agreed bool
|
||||||
err = savePrivateKey(user.key, user.KeyFile)
|
)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// save registration file
|
// Some essential values related to the Let's Encrypt process
|
||||||
jsonBytes, err := json.MarshalIndent(&user, "", "\t")
|
const (
|
||||||
if err != nil {
|
// Size of RSA keys in bits
|
||||||
return err
|
rsaKeySize = 2048
|
||||||
}
|
|
||||||
|
|
||||||
return ioutil.WriteFile(filepath.Join(userFolder, "registration.json"), jsonBytes, 0600)
|
// The base URL to the Let's Encrypt CA
|
||||||
}
|
caURL = "http://192.168.99.100:4000"
|
||||||
|
|
||||||
func getLetsEncryptUser(email string) (LetsEncryptUser, error) {
|
// The port to expose to the CA server for Simple HTTP Challenge
|
||||||
var user LetsEncryptUser
|
exposePort = "5001"
|
||||||
|
)
|
||||||
|
|
||||||
userFolder := filepath.Join(app.DataFolder(), "letsencrypt", "users", email)
|
// KeySize represents the length of a key in bits
|
||||||
regFile, err := os.Open(filepath.Join(userFolder, "registration.json"))
|
type KeySize int
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
// create a new user
|
|
||||||
return newLetsEncryptUser(email)
|
|
||||||
}
|
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewDecoder(regFile).Decode(&user)
|
// Key sizes
|
||||||
if err != nil {
|
const (
|
||||||
return user, err
|
ECC_224 KeySize = 224
|
||||||
}
|
ECC_256 = 256
|
||||||
|
RSA_2048 = 2048
|
||||||
user.key, err = loadPrivateKey(user.KeyFile)
|
RSA_4096 = 4096
|
||||||
if err != nil {
|
)
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newLetsEncryptUser(email string) (LetsEncryptUser, error) {
|
|
||||||
user := LetsEncryptUser{Email: email}
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize)
|
|
||||||
if err != nil {
|
|
||||||
return user, errors.New("error generating private key: " + err.Error())
|
|
||||||
}
|
|
||||||
user.key = privateKey
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func emailUsername(email string) string {
|
|
||||||
at := strings.Index(email, "@")
|
|
||||||
if at == -1 {
|
|
||||||
return email
|
|
||||||
}
|
|
||||||
return email[:at]
|
|
||||||
}
|
|
||||||
|
|
||||||
type LetsEncryptUser struct {
|
|
||||||
Email string
|
|
||||||
Registration *acme.RegistrationResource
|
|
||||||
KeyFile string
|
|
||||||
key *rsa.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u LetsEncryptUser) GetEmail() string {
|
|
||||||
return u.Email
|
|
||||||
}
|
|
||||||
func (u LetsEncryptUser) GetRegistration() *acme.RegistrationResource {
|
|
||||||
return u.Registration
|
|
||||||
}
|
|
||||||
func (u LetsEncryptUser) GetPrivateKey() *rsa.PrivateKey {
|
|
||||||
return u.key
|
|
||||||
}
|
|
||||||
|
|
||||||
// savePrivateKey saves an RSA private key to file.
|
|
||||||
//
|
|
||||||
// Borrowed from Sebastian Erhart
|
|
||||||
// https://github.com/xenolf/lego/blob/34910bd541315993224af1f04f9b2877513e5477/crypto.go
|
|
||||||
func savePrivateKey(key *rsa.PrivateKey, file string) error {
|
|
||||||
pemKey := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
|
|
||||||
keyOut, err := os.Create(file)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
pem.Encode(keyOut, &pemKey)
|
|
||||||
keyOut.Close()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Check file permission
|
|
||||||
func saveCertificate(certBytes []byte, file string) error {
|
|
||||||
pemCert := pem.Block{Type: "CERTIFICATE", Bytes: certBytes}
|
|
||||||
certOut, err := os.Create(file)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
pem.Encode(certOut, &pemCert)
|
|
||||||
certOut.Close()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadPrivateKey loads an RSA private key from filename.
|
|
||||||
//
|
|
||||||
// Borrowed from Sebastian Erhart
|
|
||||||
// https://github.com/xenolf/lego/blob/34910bd541315993224af1f04f9b2877513e5477/crypto.go
|
|
||||||
func loadPrivateKey(file string) (*rsa.PrivateKey, error) {
|
|
||||||
keyBytes, err := ioutil.ReadFile(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
keyBlock, _ := pem.Decode(keyBytes)
|
|
||||||
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CertificateMeta struct {
|
type CertificateMeta struct {
|
||||||
Domain, URL string
|
Domain, URL string
|
128
config/letsencrypt/storage.go
Normal file
128
config/letsencrypt/storage.go
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
package letsencrypt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
// storage is used to get file paths in a consistent,
|
||||||
|
// cross-platform way for persisting Let's Encrypt assets
|
||||||
|
// on the file system.
|
||||||
|
var storage = Storage(filepath.Join(app.DataFolder(), "letsencrypt"))
|
||||||
|
|
||||||
|
// Storage is a root directory and facilitates
|
||||||
|
// forming file paths derived from it.
|
||||||
|
type Storage string
|
||||||
|
|
||||||
|
func (s Storage) Path(parts ...string) string {
|
||||||
|
return filepath.Join(append([]string{string(s)}, parts...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sites gets the directory that stores site certificate and keys.
|
||||||
|
func (s Storage) Sites() string {
|
||||||
|
return filepath.Join(string(s), "sites")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Site returns the path to the folder containing assets for domain.
|
||||||
|
func (s Storage) Site(domain string) string {
|
||||||
|
return filepath.Join(s.Sites(), domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CertFile returns the path to the certificate file for domain.
|
||||||
|
func (s Storage) SiteCertFile(domain string) string {
|
||||||
|
return filepath.Join(s.Site(domain), domain+".crt")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SiteKeyFile returns the path to domain's private key file.
|
||||||
|
func (s Storage) SiteKeyFile(domain string) string {
|
||||||
|
return filepath.Join(s.Site(domain), domain+".key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SiteMetaFile returns the path to the domain's asset metadata file.
|
||||||
|
func (s Storage) SiteMetaFile(domain string) string {
|
||||||
|
return filepath.Join(s.Site(domain), domain+".json")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users gets the directory that stores account folders.
|
||||||
|
func (s Storage) Users() string {
|
||||||
|
return filepath.Join(string(s), "users")
|
||||||
|
}
|
||||||
|
|
||||||
|
// User gets the account folder for the user with email.
|
||||||
|
func (s Storage) User(email string) string {
|
||||||
|
return filepath.Join(s.Users(), email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserRegFile gets the path to the registration file for
|
||||||
|
// the user with the given email address.
|
||||||
|
func (s Storage) UserRegFile(email string) string {
|
||||||
|
fileName := emailUsername(email)
|
||||||
|
if fileName == "" {
|
||||||
|
fileName = "registration"
|
||||||
|
}
|
||||||
|
return filepath.Join(s.User(email), fileName+".json")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserKeyFile gets the path to the private key file for
|
||||||
|
// the user with the given email address.
|
||||||
|
func (s Storage) UserKeyFile(email string) string {
|
||||||
|
// TODO: Read the KeyFile property in the registration file instead?
|
||||||
|
fileName := emailUsername(email)
|
||||||
|
if fileName == "" {
|
||||||
|
fileName = "private"
|
||||||
|
}
|
||||||
|
return filepath.Join(s.User(email), fileName+".key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// emailUsername returns the username portion of an
|
||||||
|
// email address (part before '@') or the original
|
||||||
|
// input if it can't find the "@" symbol.
|
||||||
|
func emailUsername(email string) string {
|
||||||
|
at := strings.Index(email, "@")
|
||||||
|
if at == -1 {
|
||||||
|
return email
|
||||||
|
}
|
||||||
|
return email[:at]
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// StorageDir is the full path to the folder where this Let's
|
||||||
|
// Encrypt client will set up camp. In other words, where it
|
||||||
|
// stores user account information, keys, and certificates.
|
||||||
|
// All files will be contained in a 'letsencrypt' folder
|
||||||
|
// within StorageDir.
|
||||||
|
//
|
||||||
|
// Changing this after the program has accessed this folder
|
||||||
|
// will result in undefined behavior.
|
||||||
|
var StorageDir = "."
|
||||||
|
|
||||||
|
// Values related to persisting things on the file system
|
||||||
|
const (
|
||||||
|
// ContainerDir is the name of the folder within StorageDir
|
||||||
|
// in which files or folders are placed.
|
||||||
|
ContainerDir = "letsencrypt"
|
||||||
|
|
||||||
|
// File that contains information about the user's LE account
|
||||||
|
UserRegistrationFile = "registration.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BaseDir returns the full path to the base directory in which
|
||||||
|
// files or folders may be placed, e.g. "<StorageDir>/letsencrypt".
|
||||||
|
func BaseDir() string {
|
||||||
|
return filepath.Join(StorageDir, ContainerDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountsDir returns the full path to the directory where account
|
||||||
|
// information is stored for LE users.
|
||||||
|
func AccountsDir() string {
|
||||||
|
return filepath.Join(BaseDir(), "users")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountsDir gets the full path to the directory for a certain
|
||||||
|
// user with the email address email.
|
||||||
|
func AccountDir(email string) string {
|
||||||
|
return filepath.Join(AccountsDir(), email)
|
||||||
|
}
|
||||||
|
*/
|
97
config/letsencrypt/user.go
Normal file
97
config/letsencrypt/user.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
package letsencrypt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/xenolf/lego/acme"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Email string
|
||||||
|
Registration *acme.RegistrationResource
|
||||||
|
KeyFile string
|
||||||
|
key *rsa.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u User) GetEmail() string {
|
||||||
|
return u.Email
|
||||||
|
}
|
||||||
|
func (u User) GetRegistration() *acme.RegistrationResource {
|
||||||
|
return u.Registration
|
||||||
|
}
|
||||||
|
func (u User) GetPrivateKey() *rsa.PrivateKey {
|
||||||
|
return u.key
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUser loads the user with the given email from disk.
|
||||||
|
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 = loadRSAPrivateKey(user.KeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveUser persists a user's key and account registration
|
||||||
|
// to the file system.
|
||||||
|
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
|
||||||
|
user.KeyFile = storage.UserKeyFile(user.Email)
|
||||||
|
err = saveRSAPrivateKey(user.key, user.KeyFile)
|
||||||
|
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 register
|
||||||
|
// the user via ACME.
|
||||||
|
func newUser(email string) (User, error) {
|
||||||
|
user := User{Email: email}
|
||||||
|
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize)
|
||||||
|
if err != nil {
|
||||||
|
return user, errors.New("error generating private key: " + err.Error())
|
||||||
|
}
|
||||||
|
user.key = privateKey
|
||||||
|
return user, nil
|
||||||
|
}
|
5
main.go
5
main.go
|
@ -15,6 +15,7 @@ import (
|
||||||
|
|
||||||
"github.com/mholt/caddy/app"
|
"github.com/mholt/caddy/app"
|
||||||
"github.com/mholt/caddy/config"
|
"github.com/mholt/caddy/config"
|
||||||
|
"github.com/mholt/caddy/config/letsencrypt"
|
||||||
"github.com/mholt/caddy/server"
|
"github.com/mholt/caddy/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,8 +34,8 @@ func init() {
|
||||||
flag.StringVar(&config.Host, "host", config.DefaultHost, "Default host")
|
flag.StringVar(&config.Host, "host", config.DefaultHost, "Default host")
|
||||||
flag.StringVar(&config.Port, "port", config.DefaultPort, "Default port")
|
flag.StringVar(&config.Port, "port", config.DefaultPort, "Default port")
|
||||||
flag.BoolVar(&version, "version", false, "Show version")
|
flag.BoolVar(&version, "version", false, "Show version")
|
||||||
flag.BoolVar(&config.LetsEncryptAgree, "agree", false, "Agree to Let's Encrypt Subscriber Agreement")
|
flag.BoolVar(&letsencrypt.Agreed, "agree", false, "Agree to Let's Encrypt Subscriber Agreement")
|
||||||
flag.StringVar(&config.LetsEncryptEmail, "email", "", "Email address to use for Let's Encrypt account")
|
flag.StringVar(&letsencrypt.DefaultEmail, "email", "", "Default email address to use for Let's Encrypt transactions")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
Loading…
Reference in a new issue