0
Fork 0
mirror of https://github.com/caddyserver/caddy.git synced 2024-12-23 22:27:38 -05:00
caddy/caddytls/filestorage.go
Chad Retz 88a2811e2a Pluggable TLS Storage (#913)
* Initial concept for pluggable storage (sans tests and docs)

* Add TLS storage docs, test harness, and minor clean up from code review

* Fix issue with caddymain's temporary moveStorage

* Formatting improvement on struct array literal by removing struct name

* Pluggable storage changes:

* Change storage interface to persist all site or user data in one call
* Add lock/unlock calls for renewal and cert obtaining

* Key fields on composite literals
2016-07-08 07:32:31 -06:00

239 lines
7.1 KiB
Go

package caddytls
import (
"github.com/mholt/caddy"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
)
// storageBasePath is the root path in which all TLS/ACME assets are
// stored. Do not change this value during the lifetime of the program.
var storageBasePath = filepath.Join(caddy.AssetsPath(), "acme")
// FileStorageCreator creates a new Storage instance backed by the local
// disk. The resulting Storage instance is guaranteed to be non-nil if
// there is no error. This can be used by "middleware" implementations that
// may want to proxy the disk storage.
func FileStorageCreator(caURL *url.URL) (Storage, error) {
return FileStorage(filepath.Join(storageBasePath, caURL.Host)), nil
}
// FileStorage is a root directory and facilitates forming file paths derived
// from it. It is used to get file paths in a consistent, cross- platform way
// for persisting ACME assets on the file system.
type FileStorage string
// sites gets the directory that stores site certificate and keys.
func (s FileStorage) sites() string {
return filepath.Join(string(s), "sites")
}
// site returns the path to the folder containing assets for domain.
func (s FileStorage) site(domain string) string {
domain = strings.ToLower(domain)
return filepath.Join(s.sites(), domain)
}
// siteCertFile returns the path to the certificate file for domain.
func (s FileStorage) siteCertFile(domain string) string {
domain = strings.ToLower(domain)
return filepath.Join(s.site(domain), domain+".crt")
}
// siteKeyFile returns the path to domain's private key file.
func (s FileStorage) siteKeyFile(domain string) string {
domain = strings.ToLower(domain)
return filepath.Join(s.site(domain), domain+".key")
}
// siteMetaFile returns the path to the domain's asset metadata file.
func (s FileStorage) siteMetaFile(domain string) string {
domain = strings.ToLower(domain)
return filepath.Join(s.site(domain), domain+".json")
}
// users gets the directory that stores account folders.
func (s FileStorage) users() string {
return filepath.Join(string(s), "users")
}
// user gets the account folder for the user with email
func (s FileStorage) user(email string) string {
if email == "" {
email = emptyEmail
}
email = strings.ToLower(email)
return filepath.Join(s.users(), email)
}
// 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
} else if at == 0 {
return email[1:]
}
return email[:at]
}
// userRegFile gets the path to the registration file for the user with the
// given email address.
func (s FileStorage) userRegFile(email string) string {
if email == "" {
email = emptyEmail
}
email = strings.ToLower(email)
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 FileStorage) userKeyFile(email string) string {
if email == "" {
email = emptyEmail
}
email = strings.ToLower(email)
fileName := emailUsername(email)
if fileName == "" {
fileName = "private"
}
return filepath.Join(s.user(email), fileName+".key")
}
// readFile abstracts a simple ioutil.ReadFile, making sure to return an
// ErrStorageNotFound instance when the file is not found.
func (s FileStorage) readFile(file string) ([]byte, error) {
byts, err := ioutil.ReadFile(file)
if os.IsNotExist(err) {
return nil, ErrStorageNotFound
}
return byts, err
}
// SiteExists implements Storage.SiteExists by checking for the presence of
// cert and key files.
func (s FileStorage) SiteExists(domain string) bool {
_, err := os.Stat(s.siteCertFile(domain))
if err != nil {
return false
}
_, err = os.Stat(s.siteKeyFile(domain))
if err != nil {
return false
}
return true
}
// LoadSite implements Storage.LoadSite by loading it from disk. If it is not
// present, the ErrStorageNotFound error instance is returned.
func (s FileStorage) LoadSite(domain string) (*SiteData, error) {
var err error
siteData := new(SiteData)
siteData.Cert, err = s.readFile(s.siteCertFile(domain))
if err == nil {
siteData.Key, err = s.readFile(s.siteKeyFile(domain))
}
if err == nil {
siteData.Meta, err = s.readFile(s.siteMetaFile(domain))
}
return siteData, err
}
// StoreSite implements Storage.StoreSite by writing it to disk. The base
// directories needed for the file are automatically created as needed.
func (s FileStorage) StoreSite(domain string, data *SiteData) error {
err := os.MkdirAll(s.site(domain), 0700)
if err != nil {
return err
}
err = ioutil.WriteFile(s.siteCertFile(domain), data.Cert, 0600)
if err == nil {
err = ioutil.WriteFile(s.siteKeyFile(domain), data.Key, 0600)
}
if err == nil {
err = ioutil.WriteFile(s.siteMetaFile(domain), data.Meta, 0600)
}
return err
}
// DeleteSite implements Storage.DeleteSite by deleting just the cert from
// disk. If it is not present, the ErrStorageNotFound error instance is
// returned.
func (s FileStorage) DeleteSite(domain string) error {
err := os.Remove(s.siteCertFile(domain))
if os.IsNotExist(err) {
return ErrStorageNotFound
}
return err
}
// LockRegister implements Storage.LockRegister by just returning true because
// it is not a multi-server storage implementation.
func (s FileStorage) LockRegister(domain string) (bool, error) {
return true, nil
}
// UnlockRegister implements Storage.UnlockRegister as a no-op because it is
// not a multi-server storage implementation.
func (s FileStorage) UnlockRegister(domain string) error {
return nil
}
// LoadUser implements Storage.LoadUser by loading it from disk. If it is not
// present, the ErrStorageNotFound error instance is returned.
func (s FileStorage) LoadUser(email string) (*UserData, error) {
var err error
userData := new(UserData)
userData.Reg, err = s.readFile(s.userRegFile(email))
if err == nil {
userData.Key, err = s.readFile(s.userKeyFile(email))
}
return userData, err
}
// StoreUser implements Storage.StoreUser by writing it to disk. The base
// directories needed for the file are automatically created as needed.
func (s FileStorage) StoreUser(email string, data *UserData) error {
err := os.MkdirAll(s.user(email), 0700)
if err != nil {
return err
}
err = ioutil.WriteFile(s.userRegFile(email), data.Reg, 0600)
if err == nil {
err = ioutil.WriteFile(s.userKeyFile(email), data.Key, 0600)
}
return err
}
// MostRecentUserEmail implements Storage.MostRecentUserEmail by finding the
// most recently written sub directory in the users' directory. It is named
// after the email address. This corresponds to the most recent call to
// StoreUser.
func (s FileStorage) MostRecentUserEmail() string {
userDirs, err := ioutil.ReadDir(s.users())
if err != nil {
return ""
}
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 {
return mostRecent.Name()
}
return ""
}