github.com/staktrace/go-update@v0.0.0-20210525161054-fc019945f9a2/apply.go (about)

     1  package update
     2  
     3  import (
     4  	"bytes"
     5  	"crypto"
     6  	"crypto/x509"
     7  	"encoding/pem"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"os"
    13  	"path/filepath"
    14  
    15  	"github.com/staktrace/go-update/internal/osext"
    16  )
    17  
    18  var (
    19  	openFile = os.OpenFile
    20  )
    21  
    22  // Apply performs an update of the current executable (or opts.TargetFile, if set) with the contents of the given io.Reader.
    23  //
    24  // Apply performs the following actions to ensure a safe cross-platform update:
    25  //
    26  // 1. If configured, applies the contents of the update io.Reader as a binary patch.
    27  //
    28  // 2. If configured, computes the checksum of the new executable and verifies it matches.
    29  //
    30  // 3. If configured, verifies the signature with a public key.
    31  //
    32  // 4. Creates a new file, /path/to/.target.new with the TargetMode with the contents of the updated file
    33  //
    34  // 5. Renames /path/to/target to /path/to/.target.old
    35  //
    36  // 6. Renames /path/to/.target.new to /path/to/target
    37  //
    38  // 7. If the final rename is successful, deletes /path/to/.target.old, returns no error. On Windows,
    39  // the removal of /path/to/target.old always fails, so instead Apply hides the old file instead.
    40  //
    41  // 8. If the final rename fails, attempts to roll back by renaming /path/to/.target.old
    42  // back to /path/to/target.
    43  //
    44  // If the roll back operation fails, the file system is left in an inconsistent state (betweet steps 5 and 6) where
    45  // there is no new executable file and the old executable file could not be be moved to its original location. In this
    46  // case you should notify the user of the bad news and ask them to recover manually. Applications can determine whether
    47  // the rollback failed by calling RollbackError, see the documentation on that function for additional detail.
    48  func Apply(update io.Reader, opts Options) error {
    49  	// validate
    50  	verify := false
    51  	switch {
    52  	case opts.Signature != nil && opts.PublicKey != nil:
    53  		// okay
    54  		verify = true
    55  	case opts.Signature != nil:
    56  		return errors.New("no public key to verify signature with")
    57  	case opts.PublicKey != nil:
    58  		return errors.New("No signature to verify with")
    59  	}
    60  
    61  	// set defaults
    62  	if opts.Hash == 0 {
    63  		opts.Hash = crypto.SHA256
    64  	}
    65  	if opts.Verifier == nil {
    66  		opts.Verifier = NewECDSAVerifier()
    67  	}
    68  	if opts.TargetMode == 0 {
    69  		opts.TargetMode = 0755
    70  	}
    71  
    72  	// get target path
    73  	var err error
    74  	opts.TargetPath, err = opts.getPath()
    75  	if err != nil {
    76  		return err
    77  	}
    78  
    79  	var newBytes []byte
    80  	if opts.Patcher != nil {
    81  		if newBytes, err = opts.applyPatch(update); err != nil {
    82  			return err
    83  		}
    84  	} else {
    85  		// no patch to apply, go on through
    86  		if newBytes, err = ioutil.ReadAll(update); err != nil {
    87  			return err
    88  		}
    89  	}
    90  
    91  	// verify checksum if requested
    92  	if opts.Checksum != nil {
    93  		if err = opts.verifyChecksum(newBytes); err != nil {
    94  			return err
    95  		}
    96  	}
    97  
    98  	if verify {
    99  		if err = opts.verifySignature(newBytes); err != nil {
   100  			return err
   101  		}
   102  	}
   103  
   104  	// get the directory the executable exists in
   105  	updateDir := filepath.Dir(opts.TargetPath)
   106  	filename := filepath.Base(opts.TargetPath)
   107  
   108  	// Copy the contents of newbinary to a new executable file
   109  	newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
   110  	fp, err := openFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, opts.TargetMode)
   111  	if err != nil {
   112  		return err
   113  	}
   114  	os.Chmod(newPath, opts.TargetMode)
   115  	defer fp.Close()
   116  
   117  	_, err = io.Copy(fp, bytes.NewReader(newBytes))
   118  	if err != nil {
   119  		return err
   120  	}
   121  	//don't call fp.Sync(). system power off ,file will lost
   122  	fp.Sync()
   123  	// if we don't call fp.Close(), windows won't let us move the new executable
   124  	// because the file will still be "in use"
   125  	fp.Close()
   126  
   127  	// if there is a callback for testing the new binary, run that now
   128  	if opts.TestBinary != nil {
   129  		err = opts.TestBinary(newPath)
   130  		if err != nil {
   131  			return err
   132  		}
   133  	}
   134  
   135  	// this is where we'll move the executable to so that we can swap in the updated replacement
   136  	oldPath := opts.OldSavePath
   137  	removeOld := opts.OldSavePath == ""
   138  	if removeOld {
   139  		oldPath = filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))
   140  	}
   141  
   142  	// delete any existing old exec file - this is necessary on Windows for two reasons:
   143  	// 1. after a successful update, Windows can't remove the .old file because the process is still running
   144  	// 2. windows rename operations fail if the destination file already exists
   145  	_ = os.Remove(oldPath)
   146  
   147  	// move the existing executable to a new file in the same directory
   148  	err = os.Rename(opts.TargetPath, oldPath)
   149  	if err != nil {
   150  		return err
   151  	}
   152  
   153  	// move the new exectuable in to become the new program
   154  	err = os.Rename(newPath, opts.TargetPath)
   155  
   156  	if err != nil {
   157  		// move unsuccessful
   158  		//
   159  		// The filesystem is now in a bad state. We have successfully
   160  		// moved the existing binary to a new location, but we couldn't move the new
   161  		// binary to take its place. That means there is no file where the current executable binary
   162  		// used to be!
   163  		// Try to rollback by restoring the old binary to its original path.
   164  		rerr := os.Rename(oldPath, opts.TargetPath)
   165  		if rerr != nil {
   166  			return &rollbackErr{err, rerr}
   167  		}
   168  
   169  		return err
   170  	}
   171  
   172  	// move successful, remove the old binary if needed
   173  	if removeOld {
   174  		errRemove := os.Remove(oldPath)
   175  
   176  		// windows has trouble with removing old binaries, so hide it instead
   177  		if errRemove != nil {
   178  			_ = hideFile(oldPath)
   179  		}
   180  	}
   181  
   182  	return nil
   183  }
   184  
   185  // RollbackError takes an error value returned by Apply and returns the error, if any,
   186  // that occurred when attempting to roll back from a failed update. Applications should
   187  // always call this function on any non-nil errors returned by Apply.
   188  //
   189  // If no rollback was needed or if the rollback was successful, RollbackError returns nil,
   190  // otherwise it returns the error encountered when trying to roll back.
   191  func RollbackError(err error) error {
   192  	if err == nil {
   193  		return nil
   194  	}
   195  	if rerr, ok := err.(*rollbackErr); ok {
   196  		return rerr.rollbackErr
   197  	}
   198  	return nil
   199  }
   200  
   201  type rollbackErr struct {
   202  	error             // original error
   203  	rollbackErr error // error encountered while rolling back
   204  }
   205  
   206  type Options struct {
   207  	// TargetPath defines the path to the file to update.
   208  	// The emptry string means 'the executable file of the running program'.
   209  	TargetPath string
   210  
   211  	// Create TargetPath replacement with this file mode. If zero, defaults to 0755.
   212  	TargetMode os.FileMode
   213  
   214  	// Checksum of the new binary to verify against. If nil, no checksum or signature verification is done.
   215  	Checksum []byte
   216  
   217  	// Public key to use for signature verification. If nil, no signature verification is done.
   218  	PublicKey crypto.PublicKey
   219  
   220  	// Signature to verify the updated file. If nil, no signature verification is done.
   221  	Signature []byte
   222  
   223  	// Pluggable signature verification algorithm. If nil, ECDSA is used.
   224  	Verifier Verifier
   225  
   226  	// Use this hash function to generate the checksum. If not set, SHA256 is used.
   227  	Hash crypto.Hash
   228  
   229  	// If nil, treat the update as a complete replacement for the contents of the file at TargetPath.
   230  	// If non-nil, treat the update contents as a patch and use this object to apply the patch.
   231  	Patcher Patcher
   232  
   233  	// Store the old executable file at this path after a successful update.
   234  	// The empty string means the old executable file will be removed after the update.
   235  	OldSavePath string
   236  
   237  	// If specified, this callback function will be invoked after downloading the new binary but before
   238  	// moving it into place to replace the old one. This can be used to do some sort of validation that
   239  	// the new binary runs as expected. Any errors returned here are propagated back out to the caller
   240  	// of the Apply function.
   241  	TestBinary func(binary string) error
   242  }
   243  
   244  // CheckPermissions determines whether the process has the correct permissions to
   245  // perform the requested update. If the update can proceed, it returns nil, otherwise
   246  // it returns the error that would occur if an update were attempted.
   247  func (o *Options) CheckPermissions() error {
   248  	// get the directory the file exists in
   249  	path, err := o.getPath()
   250  	if err != nil {
   251  		return err
   252  	}
   253  
   254  	fileDir := filepath.Dir(path)
   255  	fileName := filepath.Base(path)
   256  
   257  	// attempt to open a file in the file's directory
   258  	newPath := filepath.Join(fileDir, fmt.Sprintf(".%s.new", fileName))
   259  	fp, err := openFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, o.TargetMode)
   260  	if err != nil {
   261  		return err
   262  	}
   263  	fp.Close()
   264  
   265  	_ = os.Remove(newPath)
   266  	return nil
   267  }
   268  
   269  // SetPublicKeyPEM is a convenience method to set the PublicKey property
   270  // used for checking a completed update's signature by parsing a
   271  // Public Key formatted as PEM data.
   272  func (o *Options) SetPublicKeyPEM(pembytes []byte) error {
   273  	block, _ := pem.Decode(pembytes)
   274  	if block == nil {
   275  		return errors.New("couldn't parse PEM data")
   276  	}
   277  
   278  	pub, err := x509.ParsePKIXPublicKey(block.Bytes)
   279  	if err != nil {
   280  		return err
   281  	}
   282  	o.PublicKey = pub
   283  	return nil
   284  }
   285  
   286  func (o *Options) getPath() (string, error) {
   287  	if o.TargetPath == "" {
   288  		exe, err := osext.Executable()
   289  		if err != nil {
   290  			return "", err
   291  		}
   292  
   293  		exe, err = filepath.EvalSymlinks(exe)
   294  		if err != nil {
   295  			return "", err
   296  		}
   297  
   298  		return exe, nil
   299  	} else {
   300  		return o.TargetPath, nil
   301  	}
   302  }
   303  
   304  func (o *Options) applyPatch(patch io.Reader) ([]byte, error) {
   305  	// open the file to patch
   306  	old, err := os.Open(o.TargetPath)
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  	defer old.Close()
   311  
   312  	// apply the patch
   313  	var applied bytes.Buffer
   314  	if err = o.Patcher.Patch(old, &applied, patch); err != nil {
   315  		return nil, err
   316  	}
   317  
   318  	return applied.Bytes(), nil
   319  }
   320  
   321  func (o *Options) verifyChecksum(updated []byte) error {
   322  	checksum, err := checksumFor(o.Hash, updated)
   323  	if err != nil {
   324  		return err
   325  	}
   326  
   327  	if !bytes.Equal(o.Checksum, checksum) {
   328  		return fmt.Errorf("Updated file has wrong checksum. Expected: %x, got: %x", o.Checksum, checksum)
   329  	}
   330  	return nil
   331  }
   332  
   333  func (o *Options) verifySignature(updated []byte) error {
   334  	checksum, err := checksumFor(o.Hash, updated)
   335  	if err != nil {
   336  		return err
   337  	}
   338  	return o.Verifier.VerifySignature(checksum, o.Signature, o.Hash, o.PublicKey)
   339  }
   340  
   341  func checksumFor(h crypto.Hash, payload []byte) ([]byte, error) {
   342  	if !h.Available() {
   343  		return nil, errors.New("requested hash function not available")
   344  	}
   345  	hash := h.New()
   346  	hash.Write(payload) // guaranteed not to error
   347  	return hash.Sum([]byte{}), nil
   348  }