github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/persist/json.go (about)

     1  package persist
     2  
     3  // NOTE: The safe json files include a checksum that is allowed to be manually
     4  // overwritten by the user. This temporarily exposes the user to corruption, not
     5  // just from a json file that has the wrong values, but if the disk fails right
     6  // after the user has manually modified their json file, there are edge cases
     7  // where because of the manual checksum, the saver will not be able to detect
     8  // corruption.
     9  
    10  import (
    11  	"bytes"
    12  	"encoding/json"
    13  	"io/ioutil"
    14  	"os"
    15  	"strings"
    16  
    17  	"SiaPrime/build"
    18  	"SiaPrime/crypto"
    19  
    20  	"gitlab.com/NebulousLabs/errors"
    21  )
    22  
    23  // verifyChecksum will disregard the metadata of the saved file, and just verify
    24  // that the checksum matches the data below the checksum to be certain that the
    25  // file is correct.
    26  func verifyChecksum(filename string) bool {
    27  	// Open the file.
    28  	file, err := os.Open(filename)
    29  	if os.IsNotExist(err) {
    30  		// No file at all means that everything is okay. This is a condition we
    31  		// are going to hit the first time that we ever save a file.
    32  		return true
    33  	}
    34  	if err != nil {
    35  		// An error opening the file means that the checksum verification has
    36  		// failed, we don't have confidence that this a a good file.
    37  		return false
    38  	}
    39  	defer file.Close()
    40  
    41  	// Read the metadata from the file. This is not covered by the checksum but
    42  	// we have to read it anyway to get to the checksum.
    43  	var header, version string
    44  	dec := json.NewDecoder(file)
    45  	if err := dec.Decode(&header); err != nil {
    46  		return false
    47  	}
    48  	if err := dec.Decode(&version); err != nil {
    49  		return false
    50  	}
    51  
    52  	// Read everything else.
    53  	remainingBytes, err := ioutil.ReadAll(dec.Buffered())
    54  	if err != nil {
    55  		return false
    56  	}
    57  	// The buffer may or may not have read the rest of the file, read the rest
    58  	// of the file to be certain.
    59  	remainingBytesExtra, err := ioutil.ReadAll(file)
    60  	if err != nil {
    61  		return false
    62  	}
    63  	remainingBytes = append(remainingBytes, remainingBytesExtra...)
    64  
    65  	// Determine whether the leading bytes contain a checksum. A proper checksum
    66  	// will be 67 bytes (quote, 64 byte checksum, quote, newline). A manual
    67  	// checksum will be the characters "manual\n" (9 characters). If neither
    68  	// decode correctly, it is assumed that there is no checksum at all.
    69  	var checksum crypto.Hash
    70  	if len(remainingBytes) >= 67 {
    71  		err = json.Unmarshal(remainingBytes[:67], &checksum)
    72  		if err == nil {
    73  			// The checksum was read successfully. Return 'true' if the checksum
    74  			// matches the remaining data, and false otherwise.
    75  			return checksum == crypto.HashBytes(remainingBytes[68:])
    76  		}
    77  	}
    78  
    79  	// The checksum was not read correctly, check if the next few bytes are
    80  	// the "manual" checksum.
    81  	var manualChecksum string
    82  	if len(remainingBytes) >= 9 {
    83  		err = json.Unmarshal(remainingBytes[:9], &manualChecksum)
    84  		if err == nil && manualChecksum == "manual" {
    85  			return true
    86  		}
    87  	}
    88  
    89  	// The checksum could not be decoded. Older versions of the file did not
    90  	// have a checksum, but the remaining data would still need to be valid
    91  	// JSON. If we are this far, it means that either the file is corrupt, or it
    92  	// is an old file where all remaining bytes should be valid json.
    93  	return json.Valid(remainingBytes)
    94  }
    95  
    96  // readJSON will try to read a persisted json object from a file.
    97  func readJSON(meta Metadata, object interface{}, filename string) error {
    98  	// Open the file.
    99  	file, err := os.Open(filename)
   100  	if os.IsNotExist(err) {
   101  		return err
   102  	}
   103  	if err != nil {
   104  		return build.ExtendErr("unable to open persisted json object file", err)
   105  	}
   106  	defer file.Close()
   107  
   108  	// Read the metadata from the file.
   109  	var header, version string
   110  	dec := json.NewDecoder(file)
   111  	if err := dec.Decode(&header); err != nil {
   112  		return build.ExtendErr("unable to read header from persisted json object file", err)
   113  	}
   114  	if header != meta.Header {
   115  		return ErrBadHeader
   116  	}
   117  	if err := dec.Decode(&version); err != nil {
   118  		return build.ExtendErr("unable to read version from persisted json object file", err)
   119  	}
   120  	if version != meta.Version {
   121  		return ErrBadVersion
   122  	}
   123  
   124  	// Read everything else.
   125  	remainingBytes, err := ioutil.ReadAll(dec.Buffered())
   126  	if err != nil {
   127  		return build.ExtendErr("unable to read persisted json object data", err)
   128  	}
   129  	// The buffer may or may not have read the rest of the file, read the rest
   130  	// of the file to be certain.
   131  	remainingBytesExtra, err := ioutil.ReadAll(file)
   132  	if err != nil {
   133  		return build.ExtendErr("unable to read persisted json object data", err)
   134  	}
   135  	remainingBytes = append(remainingBytes, remainingBytesExtra...)
   136  
   137  	// Determine whether the leading bytes contain a checksum. A proper checksum
   138  	// will be 67 bytes (quote, 64 byte checksum, quote, newline). A manual
   139  	// checksum will be the characters "manual\n" (9 characters). If neither
   140  	// decode correctly, it is assumed that there is no checksum at all.
   141  	checkManual := len(remainingBytes) >= 9
   142  	if len(remainingBytes) >= 67 {
   143  		var checksum crypto.Hash
   144  		err = json.Unmarshal(remainingBytes[:67], &checksum)
   145  		checkManual = checkManual && err != nil
   146  		if err == nil && checksum != crypto.HashBytes(remainingBytes[68:]) {
   147  			return errors.New("loading a file with a bad checksum")
   148  		} else if err == nil {
   149  			remainingBytes = remainingBytes[68:]
   150  		}
   151  	}
   152  
   153  	// checkManual will be set to true so long as the remainingBytes is at least
   154  	// 9 bytes long, and also there was an error when parsing the checksum. The
   155  	// manual checksum is considered correct if the json unmarshalling parses
   156  	// correctly, and also the bytes match the string "manual".
   157  	if checkManual {
   158  		var manualChecksum string
   159  		err := json.Unmarshal(remainingBytes[:9], &manualChecksum)
   160  		if err == nil && manualChecksum != "manual" {
   161  			return errors.New("loading a file with a bad checksum")
   162  		} else if err == nil {
   163  			remainingBytes = remainingBytes[10:]
   164  		}
   165  	}
   166  
   167  	// Any valid checksum has been stripped off. If there was an invalid
   168  	// checksum, an error has been returned. There is also the case that no
   169  	// checksum was written at all, which is ignored as a case - it's needed to
   170  	// preserve compatibility with previous persist files.
   171  
   172  	// Parse the json object.
   173  	return json.Unmarshal(remainingBytes, &object)
   174  }
   175  
   176  // LoadJSON will load a persisted json object from disk.
   177  func LoadJSON(meta Metadata, object interface{}, filename string) error {
   178  	// Verify that the filename does not have the persist temp suffix.
   179  	if strings.HasSuffix(filename, tempSuffix) {
   180  		return ErrBadFilenameSuffix
   181  	}
   182  
   183  	// Verify that no other thread is using this filename.
   184  	err := func() error {
   185  		activeFilesMu.Lock()
   186  		defer activeFilesMu.Unlock()
   187  
   188  		_, exists := activeFiles[filename]
   189  		if exists {
   190  			build.Critical(ErrFileInUse, filename)
   191  			return ErrFileInUse
   192  		}
   193  		activeFiles[filename] = struct{}{}
   194  		return nil
   195  	}()
   196  	if err != nil {
   197  		return err
   198  	}
   199  	// Release the lock at the end of the function.
   200  	defer func() {
   201  		activeFilesMu.Lock()
   202  		delete(activeFiles, filename)
   203  		activeFilesMu.Unlock()
   204  	}()
   205  
   206  	// Try opening the primary file.
   207  	err = readJSON(meta, object, filename)
   208  	if err == ErrBadHeader || err == ErrBadVersion || os.IsNotExist(err) {
   209  		return err
   210  	}
   211  	if err != nil {
   212  		// Try opening the temp file.
   213  		err := readJSON(meta, object, filename+tempSuffix)
   214  		if err != nil {
   215  			return build.ExtendErr("unable to read persisted json object from disk", err)
   216  		}
   217  	}
   218  
   219  	// Success.
   220  	return nil
   221  }
   222  
   223  // SaveJSON will save a json object to disk in a durable, atomic way. The
   224  // resulting file will have a checksum of the data as the third line. If
   225  // manually editing files, the checksum line can be replaced with the 8
   226  // characters "manual". This will cause the reader to accept the checksum even
   227  // though the file has been changed.
   228  func SaveJSON(meta Metadata, object interface{}, filename string) error {
   229  	// Verify that the filename does not have the persist temp suffix.
   230  	if strings.HasSuffix(filename, tempSuffix) {
   231  		return ErrBadFilenameSuffix
   232  	}
   233  
   234  	// Verify that no other thread is using this filename.
   235  	err := func() error {
   236  		activeFilesMu.Lock()
   237  		defer activeFilesMu.Unlock()
   238  
   239  		_, exists := activeFiles[filename]
   240  		if exists {
   241  			build.Critical(ErrFileInUse, filename)
   242  			return ErrFileInUse
   243  		}
   244  		activeFiles[filename] = struct{}{}
   245  		return nil
   246  	}()
   247  	if err != nil {
   248  		return err
   249  	}
   250  	// Release the lock at the end of the function.
   251  	defer func() {
   252  		activeFilesMu.Lock()
   253  		delete(activeFiles, filename)
   254  		activeFilesMu.Unlock()
   255  	}()
   256  
   257  	// Write the metadata to the buffer.
   258  	buf := new(bytes.Buffer)
   259  	enc := json.NewEncoder(buf)
   260  	if err := enc.Encode(meta.Header); err != nil {
   261  		return build.ExtendErr("unable to encode metadata header", err)
   262  	}
   263  	if err := enc.Encode(meta.Version); err != nil {
   264  		return build.ExtendErr("unable to encode metadata version", err)
   265  	}
   266  
   267  	// Marshal the object into json and write the checksum + result to the
   268  	// buffer.
   269  	objBytes, err := json.MarshalIndent(object, "", "\t")
   270  	if err != nil {
   271  		return build.ExtendErr("unable to marshal the provided object", err)
   272  	}
   273  	checksum := crypto.HashBytes(objBytes)
   274  	if err := enc.Encode(checksum); err != nil {
   275  		return build.ExtendErr("unable to encode checksum", err)
   276  	}
   277  	buf.Write(objBytes)
   278  	data := buf.Bytes()
   279  
   280  	// Write out the data to the temp file, with a sync.
   281  	err = func() (err error) {
   282  		// Verify the checksum of the real file. If the real file does not have
   283  		// a valid checksum, we do not want to risk overwriting the temp file,
   284  		// which may be the only good version of the persistence remaining.
   285  		// We'll skip writing the temp file to make sure it stays intact, and go
   286  		// straight to over-writing the real file.
   287  		if !verifyChecksum(filename) {
   288  			return nil
   289  		}
   290  
   291  		file, err := os.OpenFile(filename+tempSuffix, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0600)
   292  		if err != nil {
   293  			return build.ExtendErr("unable to open temp file", err)
   294  		}
   295  		defer func() {
   296  			err = build.ComposeErrors(err, file.Close())
   297  		}()
   298  
   299  		// Write and sync.
   300  		_, err = file.Write(data)
   301  		if err != nil {
   302  			return build.ExtendErr("unable to write temp file", err)
   303  		}
   304  		err = file.Sync()
   305  		if err != nil {
   306  			return build.ExtendErr("unable to sync temp file", err)
   307  		}
   308  		return nil
   309  	}()
   310  	if err != nil {
   311  		return err
   312  	}
   313  
   314  	// Write out the data to the real file, with a sync.
   315  	err = func() (err error) {
   316  		file, err := os.OpenFile(filename, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0600)
   317  		if err != nil {
   318  			return build.ExtendErr("unable to open file", err)
   319  		}
   320  		defer func() {
   321  			err = build.ComposeErrors(err, file.Close())
   322  		}()
   323  
   324  		// Write and sync.
   325  		_, err = file.Write(data)
   326  		if err != nil {
   327  			return build.ExtendErr("unable to write file", err)
   328  		}
   329  		err = file.Sync()
   330  		if err != nil {
   331  			return build.ExtendErr("unable to sync temp file", err)
   332  		}
   333  		return nil
   334  	}()
   335  	if err != nil {
   336  		return err
   337  	}
   338  
   339  	// Success
   340  	return nil
   341  }