// 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 ( "fmt" "io/ioutil" "log" "net/url" "os" "path/filepath" "strings" "github.com/mholt/caddy" ) func init() { RegisterStorageProvider("file", NewFileStorage) } // 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) { // 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") storage := &FileStorage{Path: filepath.Join(storageBasePath, caURL.Host)} storage.Locker = &fileStorageLock{caURL: caURL.Host, storage: storage} return storage, nil } // 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 { Path string Locker } // sites gets the directory that stores site certificate and keys. func (s *FileStorage) sites() string { return filepath.Join(s.Path, "sites") } // site returns the path to the folder containing assets for domain. func (s *FileStorage) site(domain string) string { domain = fileSafe(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 = fileSafe(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 = fileSafe(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 = fileSafe(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(s.Path, "users") } // user gets the account folder for the user with email func (s *FileStorage) user(email string) string { if email == "" { email = emptyEmail } email = fileSafe(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" } fileName = fileSafe(fileName) 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" } fileName = fileSafe(fileName) return filepath.Join(s.user(email), fileName+".key") } // readFile abstracts a simple ioutil.ReadFile, making sure to return an // ErrNotExist instance when the file is not found. func (s *FileStorage) readFile(file string) ([]byte, error) { b, err := ioutil.ReadFile(file) if os.IsNotExist(err) { return nil, ErrNotExist(err) } return b, err } // SiteExists implements Storage.SiteExists by checking for the presence of // cert and key files. func (s *FileStorage) SiteExists(domain string) (bool, error) { _, err := os.Stat(s.siteCertFile(domain)) if os.IsNotExist(err) { return false, nil } else if err != nil { return false, err } _, err = os.Stat(s.siteKeyFile(domain)) if err != nil { return false, err } return true, nil } // LoadSite implements Storage.LoadSite by loading it from disk. If it is not // present, an instance of ErrNotExist 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 { return nil, err } siteData.Key, err = s.readFile(s.siteKeyFile(domain)) if err != nil { return nil, err } siteData.Meta, err = s.readFile(s.siteMetaFile(domain)) if err != nil { return nil, err } return siteData, nil } // 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 fmt.Errorf("making site directory: %v", err) } err = ioutil.WriteFile(s.siteCertFile(domain), data.Cert, 0600) if err != nil { return fmt.Errorf("writing certificate file: %v", err) } err = ioutil.WriteFile(s.siteKeyFile(domain), data.Key, 0600) if err != nil { return fmt.Errorf("writing key file: %v", err) } err = ioutil.WriteFile(s.siteMetaFile(domain), data.Meta, 0600) if err != nil { return fmt.Errorf("writing cert meta file: %v", err) } log.Printf("[INFO][%v] Certificate written to disk: %v", domain, s.siteCertFile(domain)) return nil } // DeleteSite implements Storage.DeleteSite by deleting just the cert from // disk. If it is not present, an instance of ErrNotExist is returned. func (s *FileStorage) DeleteSite(domain string) error { err := os.Remove(s.siteCertFile(domain)) if err != nil { if os.IsNotExist(err) { return ErrNotExist(err) } return err } return nil } // LoadUser implements Storage.LoadUser by loading it from disk. If it is not // present, an instance of ErrNotExist 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 { return nil, err } userData.Key, err = s.readFile(s.userKeyFile(email)) if err != nil { return nil, err } return userData, nil } // 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 fmt.Errorf("making user directory: %v", err) } err = ioutil.WriteFile(s.userRegFile(email), data.Reg, 0600) if err != nil { return fmt.Errorf("writing user registration file: %v", err) } err = ioutil.WriteFile(s.userKeyFile(email), data.Key, 0600) if err != nil { return fmt.Errorf("writing user key file: %v", err) } return nil } // 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 "" } // 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) }