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 }