github.com/RuishanTech/selfupdate@v1.0.0/apply.go (about)

     1  package selfupdate
     2  
     3  import (
     4  	"bytes"
     5  	"crypto"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"os"
    11  	"path/filepath"
    12  
    13  	"github.com/RuishanTech/selfupdate/internal/osext"
    14  )
    15  
    16  // Apply performs an update of the current executable or opts.TargetFile, with
    17  // the contents of the given io.Reader. When the update fails, it is unlikely
    18  // that old executable is corrupted, but still, applications need to check the
    19  // returned error with RollbackError() and notify the user of the bad news and
    20  // ask them to recover manually.
    21  func Apply(update io.Reader, opts Options) error {
    22  	err := PrepareAndCheckBinary(update, opts)
    23  	if err != nil {
    24  		return err
    25  	}
    26  	return CommitBinary(opts)
    27  }
    28  
    29  // PrepareAndCheckBinary reads the new binary content from io.Reader and performs the following actions:
    30  //  1. If configured, applies the contents of the update io.Reader as a binary patch.
    31  //  2. If configured, computes the checksum of the executable and verifies it matches.
    32  //  3. If configured, verifies the signature with a public key.
    33  //  4. Creates a new file, /path/to/.target.new with the TargetMode with the contents of the updated file
    34  func PrepareAndCheckBinary(update io.Reader, opts Options) error {
    35  	// get target path
    36  	targetPath, err := opts.getPath()
    37  	if err != nil {
    38  		return err
    39  	}
    40  
    41  	var newBytes []byte
    42  	if opts.Patcher != nil {
    43  		if newBytes, err = opts.applyPatch(update, targetPath); err != nil {
    44  			return err
    45  		}
    46  	} else {
    47  		// no patch to apply, go on through
    48  		if newBytes, err = ioutil.ReadAll(update); err != nil {
    49  			return err
    50  		}
    51  	}
    52  
    53  	// verify checksum if requested
    54  	if opts.Checksum != nil {
    55  		if err = opts.verifyChecksum(newBytes); err != nil {
    56  			return err
    57  		}
    58  	}
    59  
    60  	// get the directory the executable exists in
    61  	updateDir := filepath.Dir(targetPath)
    62  	filename := filepath.Base(targetPath)
    63  
    64  	// Copy the contents of newbinary to a new executable file
    65  	newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
    66  	fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, opts.getMode())
    67  	if err != nil {
    68  		return err
    69  	}
    70  	defer fp.Close()
    71  
    72  	_, err = io.Copy(fp, bytes.NewReader(newBytes))
    73  	if err != nil {
    74  		return err
    75  	}
    76  
    77  	// if we don't call fp.Close(), windows won't let us move the new executable
    78  	// because the file will still be "in use"
    79  	fp.Close()
    80  	return nil
    81  }
    82  
    83  // CommitBinary moves the new executable to the location of the current executable or opts.TargetPath
    84  // if specified. It performs the following operations:
    85  //  1. Renames /path/to/target to /path/to/.target.old
    86  //  2. Renames /path/to/.target.new to /path/to/target
    87  //  3. If the final rename is successful, deletes /path/to/.target.old, returns no error. On Windows,
    88  //     the removal of /path/to/target.old always fails, so instead Apply hides the old file instead.
    89  //  4. If the final rename fails, attempts to roll back by renaming /path/to/.target.old
    90  //     back to /path/to/target.
    91  //
    92  // If the roll back operation fails, the file system is left in an inconsistent state where there is
    93  // no new executable file and the old executable file could not be be moved to its original location.
    94  // In this case you should notify the user of the bad news and ask them to recover manually. Applications
    95  // can determine whether the rollback failed by calling RollbackError, see the documentation on that function
    96  // for additional detail.
    97  func CommitBinary(opts Options) error {
    98  	// get the directory the file exists in
    99  	targetPath, err := opts.getPath()
   100  	if err != nil {
   101  		return err
   102  	}
   103  
   104  	updateDir := filepath.Dir(targetPath)
   105  	filename := filepath.Base(targetPath)
   106  	newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
   107  
   108  	// this is where we'll move the executable to so that we can swap in the updated replacement
   109  	oldPath := opts.OldSavePath
   110  	removeOld := opts.OldSavePath == ""
   111  	if removeOld {
   112  		oldPath = filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))
   113  	}
   114  
   115  	// delete any existing old exec file - this is necessary on Windows for two reasons:
   116  	// 1. after a successful update, Windows can't remove the .old file because the process is still running
   117  	// 2. windows rename operations fail if the destination file already exists
   118  	_ = os.Remove(oldPath)
   119  
   120  	// move the existing executable to a new file in the same directory
   121  	err = os.Rename(targetPath, oldPath)
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	// move the new exectuable in to become the new program
   127  	err = os.Rename(newPath, targetPath)
   128  
   129  	if err != nil {
   130  		// move unsuccessful
   131  		//
   132  		// The filesystem is now in a bad state. We have successfully
   133  		// moved the existing binary to a new location, but we couldn't move the new
   134  		// binary to take its place. That means there is no file where the current executable binary
   135  		// used to be!
   136  		// Try to rollback by restoring the old binary to its original path.
   137  		rerr := os.Rename(oldPath, targetPath)
   138  		if rerr != nil {
   139  			return &rollbackErr{err, rerr}
   140  		}
   141  
   142  		return err
   143  	}
   144  
   145  	// move successful, remove the old binary if needed
   146  	if removeOld {
   147  		errRemove := os.Remove(oldPath)
   148  
   149  		// windows has trouble with removing old binaries, so hide it instead
   150  		if errRemove != nil {
   151  			_ = hideFile(oldPath)
   152  		}
   153  	}
   154  
   155  	return nil
   156  }
   157  
   158  // RollbackError takes an error value returned by Apply and returns the error, if any,
   159  // that occurred when attempting to roll back from a failed update. Applications should
   160  // always call this function on any non-nil errors returned by Apply.
   161  //
   162  // If no rollback was needed or if the rollback was successful, RollbackError returns nil,
   163  // otherwise it returns the error encountered when trying to roll back.
   164  func RollbackError(err error) error {
   165  	if err == nil {
   166  		return nil
   167  	}
   168  	if rerr, ok := err.(*rollbackErr); ok {
   169  		return rerr.rollbackErr
   170  	}
   171  	return nil
   172  }
   173  
   174  type rollbackErr struct {
   175  	error             // original error
   176  	rollbackErr error // error encountered while rolling back
   177  }
   178  
   179  type Options struct {
   180  	// TargetPath defines the path to the file to update.
   181  	// The emptry string means 'the executable file of the running program'.
   182  	TargetPath string
   183  
   184  	// Create TargetPath replacement with this file mode. If zero, defaults to 0755.
   185  	TargetMode os.FileMode
   186  
   187  	// Checksum of the new binary to verify against. If nil, no checksum or signature verification is done.
   188  	Checksum []byte
   189  
   190  	// Use this hash function to generate the checksum. If not set, SHA256 is used.
   191  	Hash crypto.Hash
   192  
   193  	// If nil, treat the update as a complete replacement for the contents of the file at TargetPath.
   194  	// If non-nil, treat the update contents as a patch and use this object to apply the patch.
   195  	Patcher Patcher
   196  
   197  	// Store the old executable file at this path after a successful update.
   198  	// The empty string means the old executable file will be removed after the update.
   199  	OldSavePath string
   200  }
   201  
   202  // CheckPermissions determines whether the process has the correct permissions to
   203  // perform the requested update. If the update can proceed, it returns nil, otherwise
   204  // it returns the error that would occur if an update were attempted.
   205  func (o *Options) CheckPermissions() error {
   206  	// get the directory the file exists in
   207  	path, err := o.getPath()
   208  	if err != nil {
   209  		return err
   210  	}
   211  
   212  	fileDir := filepath.Dir(path)
   213  	fileName := filepath.Base(path)
   214  
   215  	// attempt to open a file in the file's directory
   216  	newPath := filepath.Join(fileDir, fmt.Sprintf(".%s.check-perm", fileName))
   217  	fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, o.getMode())
   218  	if err != nil {
   219  		return err
   220  	}
   221  	fp.Close()
   222  
   223  	_ = os.Remove(newPath)
   224  	return nil
   225  }
   226  
   227  func (o *Options) getPath() (string, error) {
   228  	if o.TargetPath == "" {
   229  		return osext.Executable()
   230  	} else {
   231  		return o.TargetPath, nil
   232  	}
   233  }
   234  
   235  func (o *Options) getMode() os.FileMode {
   236  	if o.TargetMode == 0 {
   237  		return 0755
   238  	}
   239  	return o.TargetMode
   240  }
   241  
   242  func (o *Options) getHash() crypto.Hash {
   243  	if o.Hash == 0 {
   244  		o.Hash = crypto.SHA256
   245  	}
   246  	return o.Hash
   247  }
   248  
   249  func (o *Options) applyPatch(patch io.Reader, targetPath string) ([]byte, error) {
   250  	// open the file to patch
   251  	old, err := os.Open(targetPath)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  	defer old.Close()
   256  
   257  	// apply the patch
   258  	var applied bytes.Buffer
   259  	if err = o.Patcher.Patch(old, &applied, patch); err != nil {
   260  		return nil, err
   261  	}
   262  
   263  	return applied.Bytes(), nil
   264  }
   265  
   266  func (o *Options) verifyChecksum(updated []byte) error {
   267  	checksum, err := checksumFor(o.getHash(), updated)
   268  	if err != nil {
   269  		return err
   270  	}
   271  
   272  	if !bytes.Equal(o.Checksum, checksum) {
   273  		return fmt.Errorf("Updated file has wrong checksum. Expected: %x, got: %x", o.Checksum, checksum)
   274  	}
   275  	return nil
   276  }
   277  
   278  func checksumFor(h crypto.Hash, payload []byte) ([]byte, error) {
   279  	if !h.Available() {
   280  		return nil, errors.New("requested hash function not available")
   281  	}
   282  	hash := h.New()
   283  	hash.Write(payload) // guaranteed not to error
   284  	return hash.Sum([]byte{}), nil
   285  }