323 lines
9.1 KiB
Go
323 lines
9.1 KiB
Go
|
package update
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"crypto"
|
||
|
"crypto/x509"
|
||
|
"encoding/pem"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
|
||
|
"github.com/equinox-io/equinox/internal/go-update/internal/osext"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
openFile = os.OpenFile
|
||
|
)
|
||
|
|
||
|
// Apply performs an update of the current executable (or opts.TargetFile, if set) with the contents of the given io.Reader.
|
||
|
//
|
||
|
// Apply performs the following actions to ensure a safe cross-platform update:
|
||
|
//
|
||
|
// 1. If configured, applies the contents of the update io.Reader as a binary patch.
|
||
|
//
|
||
|
// 2. If configured, computes the checksum of the new executable and verifies it matches.
|
||
|
//
|
||
|
// 3. If configured, verifies the signature with a public key.
|
||
|
//
|
||
|
// 4. Creates a new file, /path/to/.target.new with the TargetMode with the contents of the updated file
|
||
|
//
|
||
|
// 5. Renames /path/to/target to /path/to/.target.old
|
||
|
//
|
||
|
// 6. Renames /path/to/.target.new to /path/to/target
|
||
|
//
|
||
|
// 7. If the final rename is successful, deletes /path/to/.target.old, returns no error. On Windows,
|
||
|
// the removal of /path/to/target.old always fails, so instead Apply hides the old file instead.
|
||
|
//
|
||
|
// 8. If the final rename fails, attempts to roll back by renaming /path/to/.target.old
|
||
|
// back to /path/to/target.
|
||
|
//
|
||
|
// If the roll back operation fails, the file system is left in an inconsistent state (betweet steps 5 and 6) where
|
||
|
// there is no new executable file and the old executable file could not be be moved to its original location. In this
|
||
|
// case you should notify the user of the bad news and ask them to recover manually. Applications can determine whether
|
||
|
// the rollback failed by calling RollbackError, see the documentation on that function for additional detail.
|
||
|
func Apply(update io.Reader, opts Options) error {
|
||
|
// validate
|
||
|
verify := false
|
||
|
switch {
|
||
|
case opts.Signature != nil && opts.PublicKey != nil:
|
||
|
// okay
|
||
|
verify = true
|
||
|
case opts.Signature != nil:
|
||
|
return errors.New("no public key to verify signature with")
|
||
|
case opts.PublicKey != nil:
|
||
|
return errors.New("No signature to verify with")
|
||
|
}
|
||
|
|
||
|
// set defaults
|
||
|
if opts.Hash == 0 {
|
||
|
opts.Hash = crypto.SHA256
|
||
|
}
|
||
|
if opts.Verifier == nil {
|
||
|
opts.Verifier = NewECDSAVerifier()
|
||
|
}
|
||
|
if opts.TargetMode == 0 {
|
||
|
opts.TargetMode = 0755
|
||
|
}
|
||
|
|
||
|
// get target path
|
||
|
var err error
|
||
|
opts.TargetPath, err = opts.getPath()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
var newBytes []byte
|
||
|
if opts.Patcher != nil {
|
||
|
if newBytes, err = opts.applyPatch(update); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
} else {
|
||
|
// no patch to apply, go on through
|
||
|
if newBytes, err = ioutil.ReadAll(update); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// verify checksum if requested
|
||
|
if opts.Checksum != nil {
|
||
|
if err = opts.verifyChecksum(newBytes); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if verify {
|
||
|
if err = opts.verifySignature(newBytes); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// get the directory the executable exists in
|
||
|
updateDir := filepath.Dir(opts.TargetPath)
|
||
|
filename := filepath.Base(opts.TargetPath)
|
||
|
|
||
|
// Copy the contents of newbinary to a new executable file
|
||
|
newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
|
||
|
fp, err := openFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, opts.TargetMode)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer fp.Close()
|
||
|
|
||
|
_, err = io.Copy(fp, bytes.NewReader(newBytes))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// if we don't call fp.Close(), windows won't let us move the new executable
|
||
|
// because the file will still be "in use"
|
||
|
fp.Close()
|
||
|
|
||
|
// this is where we'll move the executable to so that we can swap in the updated replacement
|
||
|
oldPath := opts.OldSavePath
|
||
|
removeOld := opts.OldSavePath == ""
|
||
|
if removeOld {
|
||
|
oldPath = filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))
|
||
|
}
|
||
|
|
||
|
// delete any existing old exec file - this is necessary on Windows for two reasons:
|
||
|
// 1. after a successful update, Windows can't remove the .old file because the process is still running
|
||
|
// 2. windows rename operations fail if the destination file already exists
|
||
|
_ = os.Remove(oldPath)
|
||
|
|
||
|
// move the existing executable to a new file in the same directory
|
||
|
err = os.Rename(opts.TargetPath, oldPath)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// move the new exectuable in to become the new program
|
||
|
err = os.Rename(newPath, opts.TargetPath)
|
||
|
|
||
|
if err != nil {
|
||
|
// move unsuccessful
|
||
|
//
|
||
|
// The filesystem is now in a bad state. We have successfully
|
||
|
// moved the existing binary to a new location, but we couldn't move the new
|
||
|
// binary to take its place. That means there is no file where the current executable binary
|
||
|
// used to be!
|
||
|
// Try to rollback by restoring the old binary to its original path.
|
||
|
rerr := os.Rename(oldPath, opts.TargetPath)
|
||
|
if rerr != nil {
|
||
|
return &rollbackErr{err, rerr}
|
||
|
}
|
||
|
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// move successful, remove the old binary if needed
|
||
|
if removeOld {
|
||
|
errRemove := os.Remove(oldPath)
|
||
|
|
||
|
// windows has trouble with removing old binaries, so hide it instead
|
||
|
if errRemove != nil {
|
||
|
_ = hideFile(oldPath)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// RollbackError takes an error value returned by Apply and returns the error, if any,
|
||
|
// that occurred when attempting to roll back from a failed update. Applications should
|
||
|
// always call this function on any non-nil errors returned by Apply.
|
||
|
//
|
||
|
// If no rollback was needed or if the rollback was successful, RollbackError returns nil,
|
||
|
// otherwise it returns the error encountered when trying to roll back.
|
||
|
func RollbackError(err error) error {
|
||
|
if err == nil {
|
||
|
return nil
|
||
|
}
|
||
|
if rerr, ok := err.(*rollbackErr); ok {
|
||
|
return rerr.rollbackErr
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
type rollbackErr struct {
|
||
|
error // original error
|
||
|
rollbackErr error // error encountered while rolling back
|
||
|
}
|
||
|
|
||
|
type Options struct {
|
||
|
// TargetPath defines the path to the file to update.
|
||
|
// The emptry string means 'the executable file of the running program'.
|
||
|
TargetPath string
|
||
|
|
||
|
// Create TargetPath replacement with this file mode. If zero, defaults to 0755.
|
||
|
TargetMode os.FileMode
|
||
|
|
||
|
// Checksum of the new binary to verify against. If nil, no checksum or signature verification is done.
|
||
|
Checksum []byte
|
||
|
|
||
|
// Public key to use for signature verification. If nil, no signature verification is done.
|
||
|
PublicKey crypto.PublicKey
|
||
|
|
||
|
// Signature to verify the updated file. If nil, no signature verification is done.
|
||
|
Signature []byte
|
||
|
|
||
|
// Pluggable signature verification algorithm. If nil, ECDSA is used.
|
||
|
Verifier Verifier
|
||
|
|
||
|
// Use this hash function to generate the checksum. If not set, SHA256 is used.
|
||
|
Hash crypto.Hash
|
||
|
|
||
|
// If nil, treat the update as a complete replacement for the contents of the file at TargetPath.
|
||
|
// If non-nil, treat the update contents as a patch and use this object to apply the patch.
|
||
|
Patcher Patcher
|
||
|
|
||
|
// Store the old executable file at this path after a successful update.
|
||
|
// The empty string means the old executable file will be removed after the update.
|
||
|
OldSavePath string
|
||
|
}
|
||
|
|
||
|
// CheckPermissions determines whether the process has the correct permissions to
|
||
|
// perform the requested update. If the update can proceed, it returns nil, otherwise
|
||
|
// it returns the error that would occur if an update were attempted.
|
||
|
func (o *Options) CheckPermissions() error {
|
||
|
// get the directory the file exists in
|
||
|
path, err := o.getPath()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
fileDir := filepath.Dir(path)
|
||
|
fileName := filepath.Base(path)
|
||
|
|
||
|
// attempt to open a file in the file's directory
|
||
|
newPath := filepath.Join(fileDir, fmt.Sprintf(".%s.new", fileName))
|
||
|
fp, err := openFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, o.TargetMode)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
fp.Close()
|
||
|
|
||
|
_ = os.Remove(newPath)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// SetPublicKeyPEM is a convenience method to set the PublicKey property
|
||
|
// used for checking a completed update's signature by parsing a
|
||
|
// Public Key formatted as PEM data.
|
||
|
func (o *Options) SetPublicKeyPEM(pembytes []byte) error {
|
||
|
block, _ := pem.Decode(pembytes)
|
||
|
if block == nil {
|
||
|
return errors.New("couldn't parse PEM data")
|
||
|
}
|
||
|
|
||
|
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
o.PublicKey = pub
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (o *Options) getPath() (string, error) {
|
||
|
if o.TargetPath == "" {
|
||
|
return osext.Executable()
|
||
|
} else {
|
||
|
return o.TargetPath, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (o *Options) applyPatch(patch io.Reader) ([]byte, error) {
|
||
|
// open the file to patch
|
||
|
old, err := os.Open(o.TargetPath)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer old.Close()
|
||
|
|
||
|
// apply the patch
|
||
|
var applied bytes.Buffer
|
||
|
if err = o.Patcher.Patch(old, &applied, patch); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return applied.Bytes(), nil
|
||
|
}
|
||
|
|
||
|
func (o *Options) verifyChecksum(updated []byte) error {
|
||
|
checksum, err := checksumFor(o.Hash, updated)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if !bytes.Equal(o.Checksum, checksum) {
|
||
|
return fmt.Errorf("Updated file has wrong checksum. Expected: %x, got: %x", o.Checksum, checksum)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (o *Options) verifySignature(updated []byte) error {
|
||
|
checksum, err := checksumFor(o.Hash, updated)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return o.Verifier.VerifySignature(checksum, o.Signature, o.Hash, o.PublicKey)
|
||
|
}
|
||
|
|
||
|
func checksumFor(h crypto.Hash, payload []byte) ([]byte, error) {
|
||
|
if !h.Available() {
|
||
|
return nil, errors.New("requested hash function not available")
|
||
|
}
|
||
|
hash := h.New()
|
||
|
hash.Write(payload) // guaranteed not to error
|
||
|
return hash.Sum([]byte{}), nil
|
||
|
}
|