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