certspotter/monitor/statedir.go

156 lines
4.1 KiB
Go

// Copyright (C) 2023 Opsmate, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License, v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
// See the Mozilla Public License for details.
package monitor
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"software.sslmate.com/src/certspotter/ct"
"software.sslmate.com/src/certspotter/merkletree"
"strconv"
"strings"
"time"
)
func readVersion(stateDir string) (int, error) {
path := filepath.Join(stateDir, "version")
fileBytes, err := os.ReadFile(path)
if errors.Is(err, fs.ErrNotExist) {
if fileExists(filepath.Join(stateDir, "evidence")) {
return 0, nil
} else {
return -1, nil
}
} else if err != nil {
return -1, err
}
version, err := strconv.Atoi(strings.TrimSpace(string(fileBytes)))
if err != nil {
return -1, fmt.Errorf("version file %q is malformed: %w", path, err)
}
return version, nil
}
func writeVersion(stateDir string) error {
return writeFile(filepath.Join(stateDir, "version"), []byte{'2', '\n'}, 0666)
}
func migrateLogStateDirV1(dir string) error {
var sth ct.SignedTreeHead
var tree merkletree.CollapsedTree
sthPath := filepath.Join(dir, "sth.json")
sthData, err := os.ReadFile(sthPath)
if errors.Is(err, fs.ErrNotExist) {
return nil
} else if err != nil {
return err
}
treePath := filepath.Join(dir, "tree.json")
treeData, err := os.ReadFile(treePath)
if errors.Is(err, fs.ErrNotExist) {
return nil
} else if err != nil {
return err
}
if err := json.Unmarshal(sthData, &sth); err != nil {
return fmt.Errorf("error unmarshaling %s: %w", sthPath, err)
}
if err := json.Unmarshal(treeData, &tree); err != nil {
return fmt.Errorf("error unmarshaling %s: %w", treePath, err)
}
stateFile := LogState{
DownloadPosition: &tree,
VerifiedPosition: &tree,
VerifiedSTH: &sth,
LastSuccess: time.Now().UTC(),
}
if err := writeJSONFile(filepath.Join(dir, "state.json"), stateFile, 0666); err != nil {
return err
}
if err := os.Remove(sthPath); err != nil {
return err
}
if err := os.Remove(treePath); err != nil {
return err
}
return nil
}
func migrateStateDirV1(stateDir string) error {
if lockfile := filepath.Join(stateDir, "lock"); fileExists(lockfile) {
return fmt.Errorf("directory is locked by another instance of certspotter; remove %s if this is not the case", lockfile)
}
if logDirs, err := os.ReadDir(filepath.Join(stateDir, "logs")); err == nil {
for _, logDir := range logDirs {
if strings.HasPrefix(logDir.Name(), ".") || !logDir.IsDir() {
continue
}
if err := migrateLogStateDirV1(filepath.Join(stateDir, "logs", logDir.Name())); err != nil {
return fmt.Errorf("error migrating log state: %w", err)
}
}
} else if !errors.Is(err, fs.ErrNotExist) {
return err
}
if err := writeVersion(stateDir); err != nil {
return err
}
if err := os.Remove(filepath.Join(stateDir, "once")); err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
return nil
}
func prepareStateDir(stateDir string) error {
if err := os.Mkdir(stateDir, 0777); err != nil && !errors.Is(err, fs.ErrExist) {
return err
}
if version, err := readVersion(stateDir); err != nil {
return err
} else if version == -1 {
if err := writeVersion(stateDir); err != nil {
return err
}
} else if version == 0 {
return fmt.Errorf("%s was created by a very old version of certspotter; run any version of certspotter after 0.2 and before 0.15.0 to upgrade this directory, or remove it to start from scratch", stateDir)
} else if version == 1 {
if err := migrateStateDirV1(stateDir); err != nil {
return err
}
} else if version > 2 {
return fmt.Errorf("%s was created by a newer version of certspotter; upgrade to the latest version of certspotter or remove this directory to start from scratch", stateDir)
}
for _, subdir := range []string{"certs", "logs", "healthchecks"} {
if err := os.Mkdir(filepath.Join(stateDir, subdir), 0777); err != nil && !errors.Is(err, fs.ErrExist) {
return err
}
}
return nil
}