1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
|
package selfupdate
import (
"bytes"
"crypto"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/kardianos/osext"
)
// Apply performs an update of the current executable or opts.TargetFile, with
// the contents of the given io.Reader. When the update fails, it is unlikely
// that old executable is corrupted, but still, applications need to check the
// returned error with RollbackError() and notify the user of the bad news and
// ask them to recover manually.
func Apply(update io.Reader, opts Options) error {
err := PrepareAndCheckBinary(update, opts)
if err != nil {
return err
}
return CommitBinary(opts)
}
// PrepareAndCheckBinary reads the new binary content from io.Reader and performs the following actions:
// 1. If configured, applies the contents of the update io.Reader as a binary patch.
// 2. If configured, computes the checksum of the 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
func PrepareAndCheckBinary(update io.Reader, opts Options) error {
// get target path
targetPath, err := opts.getPath()
if err != nil {
return err
}
var newBytes []byte
if opts.Patcher != nil {
if newBytes, err = opts.applyPatch(update, targetPath); 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 opts.Verifier != nil {
if err = opts.Verifier.Verify(newBytes); err != nil {
return err
}
}
// get the directory the executable exists in
updateDir := filepath.Dir(targetPath)
filename := filepath.Base(targetPath)
// Copy the contents of newbinary to a new executable file
newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, opts.getMode())
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()
return nil
}
// CommitBinary moves the new executable to the location of the current executable or opts.TargetPath
// if specified. It performs the following operations:
// 1. Renames /path/to/target to /path/to/.target.old
// 2. Renames /path/to/.target.new to /path/to/target
// 3. 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.
// 4. 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 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 CommitBinary(opts Options) error {
// get the directory the file exists in
targetPath, err := opts.getPath()
if err != nil {
return err
}
updateDir := filepath.Dir(targetPath)
filename := filepath.Base(targetPath)
newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
// 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(targetPath, oldPath)
if err != nil {
return err
}
// move the new exectuable in to become the new program
err = os.Rename(newPath, 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, 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
// Verifier for signature verification. If nil, no signature verification is done.
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.check-perm", fileName))
fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, o.getMode())
if err != nil {
return err
}
fp.Close()
_ = os.Remove(newPath)
return nil
}
func (o *Options) getPath() (string, error) {
if o.TargetPath == "" {
return osext.Executable()
} else {
return o.TargetPath, nil
}
}
func (o *Options) getMode() os.FileMode {
if o.TargetMode == 0 {
return 0755
}
return o.TargetMode
}
func (o *Options) getHash() crypto.Hash {
if o.Hash == 0 {
o.Hash = crypto.SHA256
}
return o.Hash
}
func (o *Options) applyPatch(patch io.Reader, targetPath string) ([]byte, error) {
// open the file to patch
old, err := os.Open(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.getHash(), 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 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
}
|