-
Notifications
You must be signed in to change notification settings - Fork 42
/
apply.go
294 lines (250 loc) · 8.76 KB
/
apply.go
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/minio/selfupdate/internal/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
}