2017-09-23 00:56:58 -05:00
|
|
|
// 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.
|
|
|
|
|
2016-07-08 08:32:31 -05:00
|
|
|
package caddytls
|
|
|
|
|
|
|
|
import (
|
2016-09-08 19:48:32 -05:00
|
|
|
"fmt"
|
2016-07-08 08:32:31 -05:00
|
|
|
"io/ioutil"
|
2017-03-06 16:56:24 -05:00
|
|
|
"log"
|
2016-07-08 08:32:31 -05:00
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
2016-09-08 19:48:32 -05:00
|
|
|
|
|
|
|
"github.com/mholt/caddy"
|
2016-07-08 08:32:31 -05:00
|
|
|
)
|
|
|
|
|
2016-08-23 15:58:33 -05:00
|
|
|
func init() {
|
2016-09-19 18:24:34 -05:00
|
|
|
RegisterStorageProvider("file", NewFileStorage)
|
2016-08-23 15:58:33 -05:00
|
|
|
}
|
|
|
|
|
2016-09-19 18:24:34 -05:00
|
|
|
// NewFileStorage is a StorageConstructor function that 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.
|
|
|
|
func NewFileStorage(caURL *url.URL) (Storage, error) {
|
2018-03-14 22:44:08 -05:00
|
|
|
// storageBasePath is the root path in which all TLS/ACME assets are
|
|
|
|
// stored. Do not change this value during the lifetime of the program.
|
|
|
|
storageBasePath := filepath.Join(caddy.AssetsPath(), "acme")
|
|
|
|
|
2018-02-13 15:23:09 -05:00
|
|
|
storage := &FileStorage{Path: filepath.Join(storageBasePath, caURL.Host)}
|
|
|
|
storage.Locker = &fileStorageLock{caURL: caURL.Host, storage: storage}
|
|
|
|
return storage, nil
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
|
|
|
|
2016-09-19 18:24:34 -05:00
|
|
|
// FileStorage facilitates forming file paths derived from a root
|
|
|
|
// directory. It is used to get file paths in a consistent,
|
|
|
|
// cross-platform way or persisting ACME assets on the file system.
|
|
|
|
type FileStorage struct {
|
2017-11-06 11:43:41 -05:00
|
|
|
Path string
|
2018-02-13 15:23:09 -05:00
|
|
|
Locker
|
2016-09-19 18:24:34 -05:00
|
|
|
}
|
2016-07-08 08:32:31 -05:00
|
|
|
|
|
|
|
// sites gets the directory that stores site certificate and keys.
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) sites() string {
|
|
|
|
return filepath.Join(s.Path, "sites")
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// site returns the path to the folder containing assets for domain.
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) site(domain string) string {
|
2018-03-28 13:04:35 -05:00
|
|
|
domain = fileSafe(domain)
|
2016-07-08 08:32:31 -05:00
|
|
|
return filepath.Join(s.sites(), domain)
|
|
|
|
}
|
|
|
|
|
|
|
|
// siteCertFile returns the path to the certificate file for domain.
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) siteCertFile(domain string) string {
|
2018-03-28 13:04:35 -05:00
|
|
|
domain = fileSafe(domain)
|
2016-07-08 08:32:31 -05:00
|
|
|
return filepath.Join(s.site(domain), domain+".crt")
|
|
|
|
}
|
|
|
|
|
|
|
|
// siteKeyFile returns the path to domain's private key file.
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) siteKeyFile(domain string) string {
|
2018-03-28 13:04:35 -05:00
|
|
|
domain = fileSafe(domain)
|
2016-07-08 08:32:31 -05:00
|
|
|
return filepath.Join(s.site(domain), domain+".key")
|
|
|
|
}
|
|
|
|
|
|
|
|
// siteMetaFile returns the path to the domain's asset metadata file.
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) siteMetaFile(domain string) string {
|
2018-03-28 13:04:35 -05:00
|
|
|
domain = fileSafe(domain)
|
2016-07-08 08:32:31 -05:00
|
|
|
return filepath.Join(s.site(domain), domain+".json")
|
|
|
|
}
|
|
|
|
|
|
|
|
// users gets the directory that stores account folders.
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) users() string {
|
|
|
|
return filepath.Join(s.Path, "users")
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// user gets the account folder for the user with email
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) user(email string) string {
|
2016-07-08 08:32:31 -05:00
|
|
|
if email == "" {
|
|
|
|
email = emptyEmail
|
|
|
|
}
|
2018-03-28 13:04:35 -05:00
|
|
|
email = fileSafe(email)
|
2016-07-08 08:32:31 -05:00
|
|
|
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.
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) userRegFile(email string) string {
|
2016-07-08 08:32:31 -05:00
|
|
|
if email == "" {
|
|
|
|
email = emptyEmail
|
|
|
|
}
|
|
|
|
email = strings.ToLower(email)
|
|
|
|
fileName := emailUsername(email)
|
|
|
|
if fileName == "" {
|
|
|
|
fileName = "registration"
|
|
|
|
}
|
2018-03-28 13:04:35 -05:00
|
|
|
fileName = fileSafe(fileName)
|
2016-07-08 08:32:31 -05:00
|
|
|
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.
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) userKeyFile(email string) string {
|
2016-07-08 08:32:31 -05:00
|
|
|
if email == "" {
|
|
|
|
email = emptyEmail
|
|
|
|
}
|
|
|
|
email = strings.ToLower(email)
|
|
|
|
fileName := emailUsername(email)
|
|
|
|
if fileName == "" {
|
|
|
|
fileName = "private"
|
|
|
|
}
|
2018-03-28 13:04:35 -05:00
|
|
|
fileName = fileSafe(fileName)
|
2016-07-08 08:32:31 -05:00
|
|
|
return filepath.Join(s.user(email), fileName+".key")
|
|
|
|
}
|
|
|
|
|
|
|
|
// readFile abstracts a simple ioutil.ReadFile, making sure to return an
|
2016-09-08 19:48:32 -05:00
|
|
|
// ErrNotExist instance when the file is not found.
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) readFile(file string) ([]byte, error) {
|
2016-09-08 19:48:32 -05:00
|
|
|
b, err := ioutil.ReadFile(file)
|
2016-07-08 08:32:31 -05:00
|
|
|
if os.IsNotExist(err) {
|
2016-09-08 19:48:32 -05:00
|
|
|
return nil, ErrNotExist(err)
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
2016-09-08 19:48:32 -05:00
|
|
|
return b, err
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// SiteExists implements Storage.SiteExists by checking for the presence of
|
|
|
|
// cert and key files.
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) SiteExists(domain string) (bool, error) {
|
2016-07-08 08:32:31 -05:00
|
|
|
_, err := os.Stat(s.siteCertFile(domain))
|
2016-08-18 11:28:43 -05:00
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return false, nil
|
|
|
|
} else if err != nil {
|
|
|
|
return false, err
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
2016-08-18 11:28:43 -05:00
|
|
|
|
2016-07-08 08:32:31 -05:00
|
|
|
_, err = os.Stat(s.siteKeyFile(domain))
|
|
|
|
if err != nil {
|
2016-08-18 11:28:43 -05:00
|
|
|
return false, err
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
2016-08-18 11:28:43 -05:00
|
|
|
return true, nil
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// LoadSite implements Storage.LoadSite by loading it from disk. If it is not
|
2016-09-08 19:48:32 -05:00
|
|
|
// present, an instance of ErrNotExist is returned.
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) LoadSite(domain string) (*SiteData, error) {
|
2016-07-08 08:32:31 -05:00
|
|
|
var err error
|
|
|
|
siteData := new(SiteData)
|
|
|
|
siteData.Cert, err = s.readFile(s.siteCertFile(domain))
|
2016-09-08 19:48:32 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
siteData.Key, err = s.readFile(s.siteKeyFile(domain))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
2016-09-08 19:48:32 -05:00
|
|
|
siteData.Meta, err = s.readFile(s.siteMetaFile(domain))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
2016-09-08 19:48:32 -05:00
|
|
|
return siteData, nil
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// StoreSite implements Storage.StoreSite by writing it to disk. The base
|
|
|
|
// directories needed for the file are automatically created as needed.
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) StoreSite(domain string, data *SiteData) error {
|
2016-07-08 08:32:31 -05:00
|
|
|
err := os.MkdirAll(s.site(domain), 0700)
|
|
|
|
if err != nil {
|
2016-09-08 19:48:32 -05:00
|
|
|
return fmt.Errorf("making site directory: %v", err)
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
|
|
|
err = ioutil.WriteFile(s.siteCertFile(domain), data.Cert, 0600)
|
2016-09-08 19:48:32 -05:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("writing certificate file: %v", err)
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
2016-09-08 19:48:32 -05:00
|
|
|
err = ioutil.WriteFile(s.siteKeyFile(domain), data.Key, 0600)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("writing key file: %v", err)
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
2016-09-08 19:48:32 -05:00
|
|
|
err = ioutil.WriteFile(s.siteMetaFile(domain), data.Meta, 0600)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("writing cert meta file: %v", err)
|
|
|
|
}
|
2017-03-06 16:56:24 -05:00
|
|
|
log.Printf("[INFO][%v] Certificate written to disk: %v", domain, s.siteCertFile(domain))
|
2016-09-08 19:48:32 -05:00
|
|
|
return nil
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteSite implements Storage.DeleteSite by deleting just the cert from
|
2016-09-08 19:48:32 -05:00
|
|
|
// disk. If it is not present, an instance of ErrNotExist is returned.
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) DeleteSite(domain string) error {
|
2016-07-08 08:32:31 -05:00
|
|
|
err := os.Remove(s.siteCertFile(domain))
|
2016-09-08 19:48:32 -05:00
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return ErrNotExist(err)
|
|
|
|
}
|
|
|
|
return err
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
2016-09-08 19:48:32 -05:00
|
|
|
return nil
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// LoadUser implements Storage.LoadUser by loading it from disk. If it is not
|
2016-09-08 19:48:32 -05:00
|
|
|
// present, an instance of ErrNotExist is returned.
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) LoadUser(email string) (*UserData, error) {
|
2016-07-08 08:32:31 -05:00
|
|
|
var err error
|
|
|
|
userData := new(UserData)
|
|
|
|
userData.Reg, err = s.readFile(s.userRegFile(email))
|
2016-09-08 19:48:32 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
2016-09-08 19:48:32 -05:00
|
|
|
userData.Key, err = s.readFile(s.userKeyFile(email))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return userData, nil
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// StoreUser implements Storage.StoreUser by writing it to disk. The base
|
|
|
|
// directories needed for the file are automatically created as needed.
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) StoreUser(email string, data *UserData) error {
|
2016-07-08 08:32:31 -05:00
|
|
|
err := os.MkdirAll(s.user(email), 0700)
|
|
|
|
if err != nil {
|
2016-09-08 19:48:32 -05:00
|
|
|
return fmt.Errorf("making user directory: %v", err)
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
|
|
|
err = ioutil.WriteFile(s.userRegFile(email), data.Reg, 0600)
|
2016-09-08 19:48:32 -05:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("writing user registration file: %v", err)
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
2016-09-08 19:48:32 -05:00
|
|
|
err = ioutil.WriteFile(s.userKeyFile(email), data.Key, 0600)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("writing user key file: %v", err)
|
|
|
|
}
|
|
|
|
return nil
|
2016-07-08 08:32:31 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2016-09-19 18:24:34 -05:00
|
|
|
func (s *FileStorage) MostRecentUserEmail() string {
|
2016-07-08 08:32:31 -05:00
|
|
|
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 ""
|
|
|
|
}
|
2018-03-28 13:04:35 -05:00
|
|
|
|
|
|
|
// fileSafe standardizes and sanitizes str for use in a file path.
|
|
|
|
func fileSafe(str string) string {
|
|
|
|
str = strings.ToLower(str)
|
|
|
|
str = strings.TrimSpace(str)
|
|
|
|
repl := strings.NewReplacer("..", "",
|
|
|
|
"/", "",
|
|
|
|
"\\", "",
|
|
|
|
// TODO: Consider also replacing "@" with "_at_" (but migrate existing accounts...)
|
|
|
|
"+", "_plus_",
|
|
|
|
"%", "",
|
|
|
|
"$", "",
|
|
|
|
"`", "",
|
|
|
|
"~", "",
|
|
|
|
":", "",
|
|
|
|
";", "",
|
|
|
|
"=", "",
|
|
|
|
"!", "",
|
|
|
|
"#", "",
|
|
|
|
"&", "",
|
|
|
|
"|", "",
|
|
|
|
"\"", "",
|
|
|
|
"'", "",
|
|
|
|
"*", "wildcard_")
|
|
|
|
return repl.Replace(str)
|
|
|
|
}
|