]> ruderich.org/simon Gitweb - nsscash/nsscash.git/commitdiff
nsscash: store and check hash of deployed files
authorSimon Ruderich <simon@ruderich.org>
Thu, 13 Jun 2019 06:25:09 +0000 (08:25 +0200)
committerSimon Ruderich <simon@ruderich.org>
Thu, 13 Jun 2019 06:25:09 +0000 (08:25 +0200)
The goal is to detect manual modifications of the deployed files. As we
store only the last modification in the state file and don't check the
deployed file itself, modifications go unnoticed.

An alternative would be to check the last modification time of the
deployed files. But a hash is safer as possible corruptions to the file
are detected as well.

README
file.go
state.go

diff --git a/README b/README
index 510ba33b02e7c51d43e19b8493e576e8a1ab2c24..d9c255fbefd3b14c9aa95ff17290475ecda62929 100644 (file)
--- a/README
+++ b/README
@@ -24,8 +24,8 @@ Nsscash is very careful when deploying the changes:
   message and a non-zero exit status. This prevents hiding possibly important
   errors. In addition all files are fetched first and then deployed to try to
   prevent inconsistent state if only one file can be downloaded. The state
-  file (containing last file modifications) is only updated when all
-  operations were successful.
+  file (containing last file modification and content hash) is only updated
+  when all operations were successful.
 - To prevent unexpected permissions, `nsscash` does not create new files. The
   user must create them first and `nsscash` will then re-use the permissions
   and owner/group when updating the file (see examples below).
@@ -127,9 +127,11 @@ typical configuration looks like this:
 
 The following global keys are available:
 
-- `statepath`: Path to a JSON file which stores the last modification time of
-  each file; automatically updated by `nsscash`. Used to fetch data only when
-  something has changed to reduce the required traffic.
+- `statepath`: Path to a JSON file which stores the last modification time and
+  hash of each file; automatically updated by `nsscash`. Used to fetch data
+  only when something has changed to reduce the required traffic, via
+  `If-Modified-Since`. When the hash of a file has changed the download is
+  forced.
 
 Each `file` block describes a single file to download/write. The following
 keys are available:
diff --git a/file.go b/file.go
index b77cbb692de37f02df499c7af5254e34b2bf8de4..0857dd50f14dde2fa1d0a7507fbe9e3ac17ad499 100644 (file)
--- a/file.go
+++ b/file.go
@@ -19,6 +19,8 @@ package main
 
 import (
        "bytes"
+       "crypto/sha512"
+       "encoding/hex"
        "fmt"
        "io/ioutil"
        "log"
@@ -26,6 +28,7 @@ import (
        "os"
        "path/filepath"
        "syscall"
+       "time"
 
        "github.com/pkg/errors"
 )
@@ -58,8 +61,34 @@ func handleFiles(cfg *Config, state *State) error {
        return nil
 }
 
+func checksumFile(file *File) (string, error) {
+       x, err := ioutil.ReadFile(file.Path)
+       if err != nil {
+               return "", err
+       }
+       return checksumBytes(x), nil
+}
+
+func checksumBytes(x []byte) string {
+       h := sha512.New()
+       h.Write(x)
+       return hex.EncodeToString(h.Sum(nil))
+}
+
 func fetchFile(file *File, state *State) error {
        t := state.LastModified[file.Url]
+
+       hash, err := checksumFile(file)
+       if err != nil {
+               // See below in deployFile() for the reason
+               return errors.Wrapf(err, "file.path %q must exist", file.Path)
+       }
+       if hash != state.Checksum[file.Url] {
+               log.Printf("%q -> %q: hash has changed", file.Url, file.Path)
+               var zero time.Time
+               t = zero // force download
+       }
+
        status, body, err := fetchIfModified(file.Url, &t)
        if err != nil {
                return err
@@ -116,6 +145,8 @@ func fetchFile(file *File, state *State) error {
        } else {
                return fmt.Errorf("unsupported file type %v", file.Type)
        }
+
+       state.Checksum[file.Url] = checksumBytes(file.body)
        return nil
 }
 
index fa8dccd60cfb56974f6a03d58eedae5c06f76b0f..9b67d9a038d14f3b5e9708c0ffd3e5ca5a9a4895 100644 (file)
--- a/state.go
+++ b/state.go
@@ -26,7 +26,9 @@ import (
 )
 
 type State struct {
+       // Key is File.Url
        LastModified map[string]time.Time
+       Checksum     map[string]string // SHA512 in hex
 }
 
 func LoadState(path string) (*State, error) {
@@ -49,6 +51,9 @@ func LoadState(path string) (*State, error) {
        if state.LastModified == nil {
                state.LastModified = make(map[string]time.Time)
        }
+       if state.Checksum == nil {
+               state.Checksum = make(map[string]string)
+       }
 
        return &state, nil
 }