From d710b2df4a0b48aa4d0970ac8e2a48832b57e718 Mon Sep 17 00:00:00 2001 From: Matteo Suppo Date: Tue, 1 Mar 2016 09:55:07 +0100 Subject: [PATCH 1/4] Use a POST request to trigger update. The updater code has been moved to its own package. The scheduler has been remove. Some functions about unzipping files went into utilities.go --- main.go | 22 +-- update.go | 364 ++------------------------------------------- updater/updater.go | 264 ++++++++++++++++++++++++++++++++ utilities.go | 67 +++++++++ 4 files changed, 345 insertions(+), 372 deletions(-) create mode 100644 updater/updater.go diff --git a/main.go b/main.go index 9cc38346d..bd654d7b2 100755 --- a/main.go +++ b/main.go @@ -14,7 +14,6 @@ import ( "time" log "github.com/Sirupsen/logrus" - "github.com/carlescere/scheduler" "github.com/gin-gonic/gin" "github.com/itsjamie/gin-cors" "github.com/kardianos/osext" @@ -25,7 +24,6 @@ import ( var ( version = "x.x.x-dev" //don't modify it, Jenkins will take care git_revision = "xxxxxxxx" //don't modify it, Jenkins will take care - embedded_autoupdate = true embedded_autoextract = false hibernate = flag.Bool("hibernate", false, "start hibernated") verbose = flag.Bool("v", true, "show debug logging") @@ -141,25 +139,6 @@ func main() { launchSelfLater() } - if embedded_autoupdate { - - var updater = &Updater{ - CurrentVersion: version, - ApiURL: *updateUrl, - BinURL: *updateUrl, - DiffURL: "", - Dir: "update/", - CmdName: *appName, - } - - if updater != nil { - updater_job := func() { - go updater.BackgroundRun() - } - scheduler.Every(5).Minutes().Run(updater_job) - } - } - log.Println("Version:" + version) // hostname @@ -244,6 +223,7 @@ func main() { r.GET("/info", infoHandler) r.POST("/killbrowser", killBrowserHandler) r.POST("/pause", pauseHandler) + r.POST("/update", updateHandler) go func() { // check if certificates exist; if not, use plain http diff --git a/update.go b/update.go index f6d85b882..0ee6b605b 100644 --- a/update.go +++ b/update.go @@ -30,366 +30,28 @@ package main import ( - "archive/zip" - "bytes" - "compress/gzip" - "crypto/sha256" - "encoding/json" - "errors" - "fmt" - log "github.com/Sirupsen/logrus" + "github.com/arduino/arduino-create-agent/updater" + "github.com/gin-gonic/gin" "github.com/kardianos/osext" - "github.com/kr/binarydist" - "github.com/pivotal-golang/archiver/extractor" - "gopkg.in/inconshreveable/go-update.v0" - "io" - "io/ioutil" - "math/rand" - "net/http" - "os" - "path" - "path/filepath" - "runtime" - "strings" - "time" ) -func UnzipWrapper(src, dest string) error { - e := extractor.NewDetectable() - return e.Extract(src, dest) -} - -func IsZip(path string) bool { - r, err := zip.OpenReader(path) - if err == nil { - r.Close() - return true +func updateHandler(c *gin.Context) { + var up = &updater.Updater{ + CurrentVersion: version, + APIURL: *updateUrl, + BinURL: *updateUrl, + DiffURL: "", + Dir: "update/", + CmdName: *appName, } - return false -} -func Unzip(zippath string, destination string) (err error) { - r, err := zip.OpenReader(zippath) - if err != nil { - return err - } - for _, f := range r.File { - fullname := path.Join(destination, f.Name) - if f.FileInfo().IsDir() { - os.MkdirAll(fullname, f.FileInfo().Mode().Perm()) - } else { - os.MkdirAll(filepath.Dir(fullname), 0755) - perms := f.FileInfo().Mode().Perm() - out, err := os.OpenFile(fullname, os.O_CREATE|os.O_RDWR, perms) - if err != nil { - return err - } - rc, err := f.Open() - if err != nil { - return err - } - _, err = io.CopyN(out, rc, f.FileInfo().Size()) - if err != nil { - return err - } - rc.Close() - out.Close() - - mtime := f.FileInfo().ModTime() - err = os.Chtimes(fullname, mtime, mtime) - if err != nil { - return err - } - } - } - return -} + err := up.BackgroundRun() -func UnzipList(path string) (list []string, err error) { - r, err := zip.OpenReader(path) if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) return } - for _, f := range r.File { - list = append(list, f.Name) - } - return -} - -// Update protocol: -// -// GET hk.heroku.com/hk/linux-amd64.json -// -// 200 ok -// { -// "Version": "2", -// "Sha256": "..." // base64 -// } -// -// then -// -// GET hkpatch.s3.amazonaws.com/hk/1/2/linux-amd64 -// -// 200 ok -// [bsdiff data] -// -// or -// -// GET hkdist.s3.amazonaws.com/hk/2/linux-amd64.gz -// -// 200 ok -// [gzipped executable data] -// -// - -const ( - plat = runtime.GOOS + "-" + runtime.GOARCH -) - -const devValidTime = 7 * 24 * time.Hour - -var ErrHashMismatch = errors.New("new file hash mismatch after patch") -var up = update.New() - -// Updater is the configuration and runtime data for doing an update. -// -// Note that ApiURL, BinURL and DiffURL should have the same value if all files are available at the same location. -// -// Example: -// -// updater := &selfupdate.Updater{ -// CurrentVersion: version, -// ApiURL: "http://updates.yourdomain.com/", -// BinURL: "http://updates.yourdownmain.com/", -// DiffURL: "http://updates.yourdomain.com/", -// Dir: "update/", -// CmdName: "myapp", // app name -// } -// if updater != nil { -// go updater.BackgroundRun() -// } -type Updater struct { - CurrentVersion string // Currently running version. - ApiURL string // Base URL for API requests (json files). - CmdName string // Command name is appended to the ApiURL like http://apiurl/CmdName/. This represents one binary. - BinURL string // Base URL for full binary downloads. - DiffURL string // Base URL for diff downloads. - Dir string // Directory to store selfupdate state. - Info struct { - Version string - Sha256 []byte - } -} - -func (u *Updater) getExecRelativeDir(dir string) string { - filename, _ := osext.Executable() - path := filepath.Join(filepath.Dir(filename), dir) - return path -} - -// BackgroundRun starts the update check and apply cycle. -func (u *Updater) BackgroundRun() error { - os.MkdirAll(u.getExecRelativeDir(u.Dir), 0777) - if u.wantUpdate() { - if err := up.CanUpdate(); err != nil { - log.Println(err) - return err - } - //self, err := osext.Executable() - //if err != nil { - // fail update, couldn't figure out path to self - //return - //} - // TODO(bgentry): logger isn't on Windows. Replace w/ proper error reports. - if err := u.update(); err != nil { - return err - } - } - return nil -} - -func (u *Updater) wantUpdate() bool { - if strings.Contains(u.CurrentVersion, "dev") { - return false - } else { - return true - } -} - -func (u *Updater) update() error { - path, err := osext.Executable() - if err != nil { - return err - } - old, err := os.Open(path) - if err != nil { - return err - } - defer old.Close() - - err = u.fetchInfo() - if err != nil { - log.Println(err) - return err - } - if u.Info.Version == u.CurrentVersion { - return nil - } - bin, err := u.fetchAndVerifyPatch(old) - if err != nil { - if err == ErrHashMismatch { - log.Println("update: hash mismatch from patched binary") - } else { - if u.DiffURL != "" { - log.Println("update: patching binary,", err) - } - } - - bin, err = u.fetchAndVerifyFullBin() - if err != nil { - if err == ErrHashMismatch { - log.Println("update: hash mismatch from full binary") - } else { - log.Println("update: fetching full binary,", err) - } - return err - } - } - - // close the old binary before installing because on windows - // it can't be renamed if a handle to the file is still open - old.Close() - - err, errRecover := up.FromStream(bytes.NewBuffer(bin)) - if errRecover != nil { - log.Errorf("update and recovery errors: %q %q", err, errRecover) - return fmt.Errorf("update and recovery errors: %q %q", err, errRecover) - } - if err != nil { - return err - } - - // remove config.ini so at restart the package will extract again - //shutil.CopyFile(*configIni, *configIni+".bak", false) - //os.Remove(*configIni) - - log.Println("Restarting because update!") + path, _ := osext.Executable() restart(path) - //addRebootTrayElement() - - // update done, we should decide if we need to restart ASAP (maybe a field in update json?) - // BIG issue: the file has been renamed in the meantime - - return nil -} - -func (u *Updater) fetchInfo() error { - r, err := fetch(u.ApiURL + u.CmdName + "/" + plat + ".json") - if err != nil { - return err - } - defer r.Close() - err = json.NewDecoder(r).Decode(&u.Info) - if err != nil { - return err - } - if len(u.Info.Sha256) != sha256.Size { - return errors.New("bad cmd hash in info") - } - return nil -} - -func (u *Updater) fetchAndVerifyPatch(old io.Reader) ([]byte, error) { - bin, err := u.fetchAndApplyPatch(old) - if err != nil { - return nil, err - } - if !verifySha(bin, u.Info.Sha256) { - return nil, ErrHashMismatch - } - return bin, nil -} - -func (u *Updater) fetchAndApplyPatch(old io.Reader) ([]byte, error) { - r, err := fetch(u.DiffURL + u.CmdName + "/" + u.CurrentVersion + "/" + u.Info.Version + "/" + plat) - if err != nil { - return nil, err - } - defer r.Close() - var buf bytes.Buffer - err = binarydist.Patch(old, &buf, r) - return buf.Bytes(), err -} - -func (u *Updater) fetchAndVerifyFullBin() ([]byte, error) { - bin, err := u.fetchBin() - if err != nil { - return nil, err - } - verified := verifySha(bin, u.Info.Sha256) - if !verified { - return nil, ErrHashMismatch - } - return bin, nil -} - -func (u *Updater) fetchBin() ([]byte, error) { - r, err := fetch(u.BinURL + u.CmdName + "/" + u.Info.Version + "/" + plat + ".gz") - if err != nil { - return nil, err - } - defer r.Close() - buf := new(bytes.Buffer) - gz, err := gzip.NewReader(r) - if err != nil { - return nil, err - } - if _, err = io.Copy(buf, gz); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -// returns a random duration in [0,n). -func randDuration(n time.Duration) time.Duration { - return time.Duration(rand.Int63n(int64(n))) -} - -func fetch(url string) (io.ReadCloser, error) { - resp, err := http.Get(url) - if err != nil { - return nil, err - } - if resp.StatusCode != 200 { - log.Errorf("bad http status from %s: %v", url, resp.Status) - return nil, fmt.Errorf("bad http status from %s: %v", url, resp.Status) - } - return resp.Body, nil -} - -func readTime(path string) time.Time { - p, err := ioutil.ReadFile(path) - if os.IsNotExist(err) { - return time.Time{} - } - if err != nil { - return time.Now().Add(1000 * time.Hour) - } - t, err := time.Parse(time.RFC3339, string(p)) - if err != nil { - return time.Now().Add(1000 * time.Hour) - } - return t -} - -func verifySha(bin []byte, sha []byte) bool { - h := sha256.New() - h.Write(bin) - return bytes.Equal(h.Sum(nil), sha) -} - -func writeTime(path string, t time.Time) bool { - return ioutil.WriteFile(path, []byte(t.Format(time.RFC3339)), 0644) == nil } diff --git a/updater/updater.go b/updater/updater.go new file mode 100644 index 000000000..239dc20d5 --- /dev/null +++ b/updater/updater.go @@ -0,0 +1,264 @@ +package updater + +import ( + "bytes" + "compress/gzip" + "crypto/sha256" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + log "github.com/Sirupsen/logrus" + "github.com/kr/binarydist" + + "github.com/inconshreveable/go-update" + "github.com/kardianos/osext" +) + +// Update protocol: +// +// GET hk.heroku.com/hk/linux-amd64.json +// +// 200 ok +// { +// "Version": "2", +// "Sha256": "..." // base64 +// } +// +// then +// +// GET hkpatch.s3.amazonaws.com/hk/1/2/linux-amd64 +// +// 200 ok +// [bsdiff data] +// +// or +// +// GET hkdist.s3.amazonaws.com/hk/2/linux-amd64.gz +// +// 200 ok +// [gzipped executable data] +// +// + +const ( + plat = runtime.GOOS + "-" + runtime.GOARCH +) + +const devValidTime = 7 * 24 * time.Hour + +var errHashMismatch = errors.New("new file hash mismatch after patch") +var up = update.New() + +// Updater is the configuration and runtime data for doing an update. +// +// Note that ApiURL, BinURL and DiffURL should have the same value if all files are available at the same location. +// +// Example: +// +// updater := &selfupdate.Updater{ +// CurrentVersion: version, +// ApiURL: "http://updates.yourdomain.com/", +// BinURL: "http://updates.yourdownmain.com/", +// DiffURL: "http://updates.yourdomain.com/", +// Dir: "update/", +// CmdName: "myapp", // app name +// } +// if updater != nil { +// go updater.BackgroundRun() +// } +type Updater struct { + CurrentVersion string // Currently running version. + APIURL string // Base URL for API requests (json files). + CmdName string // Command name is appended to the ApiURL like http://apiurl/CmdName/. This represents one binary. + BinURL string // Base URL for full binary downloads. + DiffURL string // Base URL for diff downloads. + Dir string // Directory to store selfupdate state. + Info struct { + Version string + Sha256 []byte + } +} + +// BackgroundRun starts the update check and apply cycle. +func (u *Updater) BackgroundRun() error { + os.MkdirAll(u.getExecRelativeDir(u.Dir), 0777) + if u.wantUpdate() { + if err := up.CanUpdate(); err != nil { + log.Println(err) + return err + } + //self, err := osext.Executable() + //if err != nil { + // fail update, couldn't figure out path to self + //return + //} + // TODO(bgentry): logger isn't on Windows. Replace w/ proper error reports. + if err := u.update(); err != nil { + return err + } + } + return errors.New("Won't update because it's a development daemon. Change the version in main.go") +} + +func fetch(url string) (io.ReadCloser, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + if resp.StatusCode != 200 { + log.Errorf("bad http status from %s: %v", url, resp.Status) + return nil, fmt.Errorf("bad http status from %s: %v", url, resp.Status) + } + return resp.Body, nil +} + +func verifySha(bin []byte, sha []byte) bool { + h := sha256.New() + h.Write(bin) + return bytes.Equal(h.Sum(nil), sha) +} + +func (u *Updater) fetchAndApplyPatch(old io.Reader) ([]byte, error) { + r, err := fetch(u.DiffURL + u.CmdName + "/" + u.CurrentVersion + "/" + u.Info.Version + "/" + plat) + if err != nil { + return nil, err + } + defer r.Close() + var buf bytes.Buffer + err = binarydist.Patch(old, &buf, r) + return buf.Bytes(), err +} + +func (u *Updater) fetchAndVerifyPatch(old io.Reader) ([]byte, error) { + bin, err := u.fetchAndApplyPatch(old) + if err != nil { + return nil, err + } + if !verifySha(bin, u.Info.Sha256) { + return nil, errHashMismatch + } + return bin, nil +} + +func (u *Updater) fetchAndVerifyFullBin() ([]byte, error) { + bin, err := u.fetchBin() + if err != nil { + return nil, err + } + verified := verifySha(bin, u.Info.Sha256) + if !verified { + return nil, errHashMismatch + } + return bin, nil +} + +func (u *Updater) fetchBin() ([]byte, error) { + r, err := fetch(u.BinURL + u.CmdName + "/" + u.Info.Version + "/" + plat + ".gz") + if err != nil { + return nil, err + } + defer r.Close() + buf := new(bytes.Buffer) + gz, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + if _, err = io.Copy(buf, gz); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func (u *Updater) fetchInfo() error { + r, err := fetch(u.APIURL + u.CmdName + "/" + plat + ".json") + if err != nil { + return err + } + defer r.Close() + err = json.NewDecoder(r).Decode(&u.Info) + if err != nil { + return err + } + if len(u.Info.Sha256) != sha256.Size { + return errors.New("bad cmd hash in info") + } + return nil +} + +func (u *Updater) getExecRelativeDir(dir string) string { + filename, _ := osext.Executable() + path := filepath.Join(filepath.Dir(filename), dir) + return path +} + +func (u *Updater) update() error { + path, err := osext.Executable() + if err != nil { + return err + } + old, err := os.Open(path) + if err != nil { + return err + } + defer old.Close() + + err = u.fetchInfo() + if err != nil { + log.Println(err) + return err + } + if u.Info.Version == u.CurrentVersion { + return nil + } + bin, err := u.fetchAndVerifyPatch(old) + if err != nil { + if err == errHashMismatch { + log.Println("update: hash mismatch from patched binary") + } else { + if u.DiffURL != "" { + log.Println("update: patching binary,", err) + } + } + + bin, err = u.fetchAndVerifyFullBin() + if err != nil { + if err == errHashMismatch { + log.Println("update: hash mismatch from full binary") + } else { + log.Println("update: fetching full binary,", err) + } + return err + } + } + + // close the old binary before installing because on windows + // it can't be renamed if a handle to the file is still open + old.Close() + + err, errRecover := up.FromStream(bytes.NewBuffer(bin)) + if errRecover != nil { + log.Errorf("update and recovery errors: %q %q", err, errRecover) + return fmt.Errorf("update and recovery errors: %q %q", err, errRecover) + } + if err != nil { + return err + } + + return nil +} + +func (u *Updater) wantUpdate() bool { + if strings.Contains(u.CurrentVersion, "dev") { + return false + } + return true +} diff --git a/utilities.go b/utilities.go index e6ef73a85..198473ead 100644 --- a/utilities.go +++ b/utilities.go @@ -3,11 +3,16 @@ package main import ( + "archive/zip" "bytes" "crypto/md5" "io" "os" "os/exec" + "path" + "path/filepath" + + "github.com/pivotal-golang/archiver/extractor" ) func computeMd5(filePath string) ([]byte, error) { @@ -76,3 +81,65 @@ func Filter(vs []OsSerialPort, f func(OsSerialPort) bool) []OsSerialPort { } return vsf } + +func UnzipWrapper(src, dest string) error { + e := extractor.NewDetectable() + return e.Extract(src, dest) +} + +func IsZip(path string) bool { + r, err := zip.OpenReader(path) + if err == nil { + r.Close() + return true + } + return false +} + +func Unzip(zippath string, destination string) (err error) { + r, err := zip.OpenReader(zippath) + if err != nil { + return err + } + for _, f := range r.File { + fullname := path.Join(destination, f.Name) + if f.FileInfo().IsDir() { + os.MkdirAll(fullname, f.FileInfo().Mode().Perm()) + } else { + os.MkdirAll(filepath.Dir(fullname), 0755) + perms := f.FileInfo().Mode().Perm() + out, err := os.OpenFile(fullname, os.O_CREATE|os.O_RDWR, perms) + if err != nil { + return err + } + rc, err := f.Open() + if err != nil { + return err + } + _, err = io.CopyN(out, rc, f.FileInfo().Size()) + if err != nil { + return err + } + rc.Close() + out.Close() + + mtime := f.FileInfo().ModTime() + err = os.Chtimes(fullname, mtime, mtime) + if err != nil { + return err + } + } + } + return +} + +func UnzipList(path string) (list []string, err error) { + r, err := zip.OpenReader(path) + if err != nil { + return + } + for _, f := range r.File { + list = append(list, f.Name) + } + return +} From 272cd49301413e49af24c4e9061ef02929b74bda Mon Sep 17 00:00:00 2001 From: Matteo Suppo Date: Tue, 1 Mar 2016 09:58:49 +0100 Subject: [PATCH 2/4] Return something before rebooting after an update --- update.go | 1 + 1 file changed, 1 insertion(+) diff --git a/update.go b/update.go index 0ee6b605b..ffaa3ce7b 100644 --- a/update.go +++ b/update.go @@ -52,6 +52,7 @@ func updateHandler(c *gin.Context) { return } + c.JSON(200, gin.H{"success": "Please wait a moment while the agent reboots itself"}) path, _ := osext.Executable() restart(path) } From 781d8aebd99f1c69a1dc925e1e500f92b8a73f00 Mon Sep 17 00:00:00 2001 From: Matteo Suppo Date: Tue, 1 Mar 2016 14:29:50 +0100 Subject: [PATCH 3/4] Handle error when looking for path --- update.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/update.go b/update.go index ffaa3ce7b..b662e7cc5 100644 --- a/update.go +++ b/update.go @@ -52,7 +52,13 @@ func updateHandler(c *gin.Context) { return } + path, err := osext.Executable() + + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"success": "Please wait a moment while the agent reboots itself"}) - path, _ := osext.Executable() restart(path) } From 8d60ac4267082c7f8edb2bbd2582773d7c4ce6a9 Mon Sep 17 00:00:00 2001 From: Matteo Suppo Date: Thu, 3 Mar 2016 14:44:21 +0100 Subject: [PATCH 4/4] Add autoupdater to vendor folder --- .../inconshreveable/go-update/LICENSE | 13 + .../inconshreveable/go-update/README.md | 37 ++ .../go-update/download/download.go | 235 +++++++++ .../inconshreveable/go-update/hide_noop.go | 7 + .../inconshreveable/go-update/hide_windows.go | 19 + .../inconshreveable/go-update/update.go | 491 ++++++++++++++++++ vendor/vendor.json | 10 + 7 files changed, 812 insertions(+) create mode 100644 vendor/github.com/inconshreveable/go-update/LICENSE create mode 100644 vendor/github.com/inconshreveable/go-update/README.md create mode 100644 vendor/github.com/inconshreveable/go-update/download/download.go create mode 100644 vendor/github.com/inconshreveable/go-update/hide_noop.go create mode 100644 vendor/github.com/inconshreveable/go-update/hide_windows.go create mode 100644 vendor/github.com/inconshreveable/go-update/update.go diff --git a/vendor/github.com/inconshreveable/go-update/LICENSE b/vendor/github.com/inconshreveable/go-update/LICENSE new file mode 100644 index 000000000..5f0d1fb6a --- /dev/null +++ b/vendor/github.com/inconshreveable/go-update/LICENSE @@ -0,0 +1,13 @@ +Copyright 2014 Alan Shreve + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendor/github.com/inconshreveable/go-update/README.md b/vendor/github.com/inconshreveable/go-update/README.md new file mode 100644 index 000000000..f070062c2 --- /dev/null +++ b/vendor/github.com/inconshreveable/go-update/README.md @@ -0,0 +1,37 @@ +# go-update: Automatically update Go programs from the internet + +go-update allows a program to update itself by replacing its executable file +with a new version. It provides the flexibility to implement different updating user experiences +like auto-updating, or manual user-initiated updates. It also boasts +advanced features like binary patching and code signing verification. + +Updating your program to a new version is as easy as: + + err, errRecover := update.New().FromUrl("http://release.example.com/2.0/myprogram") + if err != nil { + fmt.Printf("Update failed: %v\n", err) + } + +## Documentation and API Reference + +Comprehensive API documentation and code examples are available in the code documentation available on godoc.org: + +[![GoDoc](https://godoc.org/github.com/inconshreveable/go-update?status.svg)](https://godoc.org/github.com/inconshreveable/go-update) + +## Features + +- Cross platform support (Windows too!) +- Binary patch application +- Checksum verification +- Code signing verification +- Support for updating arbitrary files + +## [equinox.io](https://equinox.io) +go-update provides the primitives for building self-updating applications, but there a number of other challenges +involved in a complete updating solution such as hosting, code signing, update channels, gradual rollout, +dynamically computing binary patches, tracking update metrics like versions and failures, plus more. + +I provide this service, a complete solution, free for open source projects, at [equinox.io](https://equinox.io). + +## License +Apache diff --git a/vendor/github.com/inconshreveable/go-update/download/download.go b/vendor/github.com/inconshreveable/go-update/download/download.go new file mode 100644 index 000000000..4552c93dd --- /dev/null +++ b/vendor/github.com/inconshreveable/go-update/download/download.go @@ -0,0 +1,235 @@ +package download + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "net/http" + "os" + "runtime" +) + +type roundTripper struct { + RoundTripFn func(*http.Request) (*http.Response, error) +} + +func (rt *roundTripper) RoundTrip(r *http.Request) (*http.Response, error) { + return rt.RoundTripFn(r) +} + +// Download encapsulates the state and parameters to download content +// from a URL which: +// +// - Publishes the percentage of the download completed to a channel. +// - May resume a previous download that was partially completed. +// +// Create an instance with the New() factory function. +type Download struct { + // net/http.Client to use when downloading the update. + // If nil, a default http.Client is used + HttpClient *http.Client + + // As bytes are downloaded, they are written to Target. + // Download also uses the Target's Seek method to determine + // the size of partial-downloads so that it may properly + // request the remaining bytes to resume the download. + Target Target + + // Progress returns the percentage of the download + // completed as an integer between 0 and 100 + Progress chan (int) + + // HTTP Method to use in the download request. Default is "GET" + Method string + + // HTTP URL to issue the download request to + Url string +} + +// New initializes a new Download object which will download +// the content from url into target. +func New(url string, target Target, httpClient *http.Client) *Download { + return &Download{ + HttpClient: httpClient, + Progress: make(chan int), + Method: "GET", + Url: url, + Target: target, + } +} + +// Get() downloads the content of a url to a target destination. +// +// Only HTTP/1.1 servers that implement the Range header support resuming a +// partially completed download. +// +// On success, the server must return 200 and the content, or 206 when resuming a partial download. +// If the HTTP server returns a 3XX redirect, it will be followed according to d.HttpClient's redirect policy. +// +func (d *Download) Get() (err error) { + // Close the progress channel whenever this function completes + defer close(d.Progress) + + // determine the size of the download target to determine if we're resuming a partial download + offset, err := d.Target.Size() + if err != nil { + return + } + + // create the download request + req, err := http.NewRequest(d.Method, d.Url, nil) + if err != nil { + return + } + + // create an http client if one does not exist + if d.HttpClient == nil { + d.HttpClient = http.DefaultClient + } + + // we have to add headers like this so they get used across redirects + trans := d.HttpClient.Transport + if trans == nil { + trans = http.DefaultTransport + } + + d.HttpClient.Transport = &roundTripper{ + RoundTripFn: func(r *http.Request) (*http.Response, error) { + // add header for download continuation + if offset > 0 { + r.Header.Add("Range", fmt.Sprintf("%d-", offset)) + } + + // ask for gzipped content so that net/http won't unzip it for us + // and destroy the content length header we need for progress calculations + r.Header.Add("Accept-Encoding", "gzip") + + return trans.RoundTrip(r) + }, + } + + // issue the download request + resp, err := d.HttpClient.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + + switch resp.StatusCode { + // ok + case 200, 206: + + // server error + default: + err = fmt.Errorf("Non 2XX response when downloading update: %s", resp.Status) + return + } + + // Determine how much we have to download + // net/http sets this to -1 when it is unknown + clength := resp.ContentLength + + // Read the content from the response body + rd := resp.Body + + // meter the rate at which we download content for + // progress reporting if we know how much to expect + if clength > 0 { + rd = &meteredReader{rd: rd, totalSize: clength, progress: d.Progress} + } + + // Decompress the content if necessary + if resp.Header.Get("Content-Encoding") == "gzip" { + rd, err = gzip.NewReader(rd) + if err != nil { + return + } + } + + // Download the update + _, err = io.Copy(d.Target, rd) + if err != nil { + return + } + + return +} + +// meteredReader wraps a ReadCloser. Calls to a meteredReader's Read() method +// publish updates to a progress channel with the percentage read so far. +type meteredReader struct { + rd io.ReadCloser + totalSize int64 + progress chan int + totalRead int64 + ticks int64 +} + +func (m *meteredReader) Close() error { + return m.rd.Close() +} + +func (m *meteredReader) Read(b []byte) (n int, err error) { + chunkSize := (m.totalSize / 100) + 1 + lenB := int64(len(b)) + + var nChunk int + for start := int64(0); start < lenB; start += int64(nChunk) { + end := start + chunkSize + if end > lenB { + end = lenB + } + + nChunk, err = m.rd.Read(b[start:end]) + + n += nChunk + m.totalRead += int64(nChunk) + + if m.totalRead > (m.ticks * chunkSize) { + m.ticks += 1 + // try to send on channel, but don't block if it's full + select { + case m.progress <- int(m.ticks + 1): + default: + } + + // give the progress channel consumer a chance to run + runtime.Gosched() + } + + if err != nil { + return + } + } + + return +} + +// A Target is what you can supply to Download, +// it's just an io.Writer with a Size() method so that +// the a Download can "resume" an interrupted download +type Target interface { + io.Writer + Size() (int, error) +} + +type FileTarget struct { + *os.File +} + +func (t *FileTarget) Size() (int, error) { + if fi, err := t.File.Stat(); err != nil { + return 0, err + } else { + return int(fi.Size()), nil + } +} + +type MemoryTarget struct { + bytes.Buffer +} + +func (t *MemoryTarget) Size() (int, error) { + return t.Buffer.Len(), nil +} diff --git a/vendor/github.com/inconshreveable/go-update/hide_noop.go b/vendor/github.com/inconshreveable/go-update/hide_noop.go new file mode 100644 index 000000000..370775608 --- /dev/null +++ b/vendor/github.com/inconshreveable/go-update/hide_noop.go @@ -0,0 +1,7 @@ +// +build !windows + +package update + +func hideFile(path string) error { + return nil +} diff --git a/vendor/github.com/inconshreveable/go-update/hide_windows.go b/vendor/github.com/inconshreveable/go-update/hide_windows.go new file mode 100644 index 000000000..c368b9cc4 --- /dev/null +++ b/vendor/github.com/inconshreveable/go-update/hide_windows.go @@ -0,0 +1,19 @@ +package update + +import ( + "syscall" + "unsafe" +) + +func hideFile(path string) error { + kernel32 := syscall.NewLazyDLL("kernel32.dll") + setFileAttributes := kernel32.NewProc("SetFileAttributesW") + + r1, _, err := setFileAttributes.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(path))), 2) + + if r1 == 0 { + return err + } else { + return nil + } +} diff --git a/vendor/github.com/inconshreveable/go-update/update.go b/vendor/github.com/inconshreveable/go-update/update.go new file mode 100644 index 000000000..5f692f23b --- /dev/null +++ b/vendor/github.com/inconshreveable/go-update/update.go @@ -0,0 +1,491 @@ +/* +go-update allows a program to update itself by replacing its executable file +with a new version. It provides the flexibility to implement different updating user experiences +like auto-updating, or manual user-initiated updates. It also boasts +advanced features like binary patching and code signing verification. + +Updating your program to a new version is as easy as: + + err, errRecover := update.New().FromUrl("http://release.example.com/2.0/myprogram") + if err != nil { + fmt.Printf("Update failed: %v\n", err) + } + +You may also choose to update from other data sources such as a file or an io.Reader: + + err, errRecover := update.New().FromFile("/path/to/update") + +Binary Diff Patching + +Binary diff updates are supported and easy to use: + + up := update.New().ApplyPatch(update.PATCHTYPE_BSDIFF) + err, errRecover := up.FromUrl("http://release.example.com/2.0/mypatch") + +Checksum Verification + +You should also verify the checksum of new updates as well as verify +the digital signature of an update. Note that even when you choose to apply +a patch, the checksum is verified against the complete update after that patch +has been applied. + + up := update.New().ApplyPatch(update.PATCHTYPE_BSDIFF).VerifyChecksum(checksum) + err, errRecover := up.FromUrl("http://release.example.com/2.0/mypatch") + +Updating other files + +Updating arbitrary files is also supported. You may update files which are +not the currently running program: + + up := update.New().Target("/usr/local/bin/some-program") + err, errRecover := up.FromUrl("http://release.example.com/2.0/some-program") + +Code Signing + +Truly secure updates use code signing to verify that the update was issued by a trusted party. +To do this, you'll need to generate a public/private key pair. You can do this with openssl, +or the equinox.io client (https://equinox.io/client) can easily generate one for you: + + # with equinox client + equinox genkey --private-key=private.pem --public-key=public.pem + + # with openssl + openssl genrsa -out private.pem 2048 + openssl rsa -in private.pem -out public.pem -pubout + +Once you have your key pair, you can instruct your program to validate its updates +with the public key: + + const publicKey = `-----BEGIN PUBLIC KEY----- + ... + -----END PUBLIC KEY-----` + + up, err := update.New().VerifySignatureWithPEM(publicKey) + if err != nil { + return fmt.Errorf("Bad public key: '%v': %v", publicKey, err) + } + +Once you've configured your program this way, it will disallow all updates unless they +are properly signed. You must now pass in the signature to verify with: + + up.VerifySignature(signature).FromUrl("http://dl.example.com/update") + +Error Handling and Recovery + +To perform an update, the process must be able to read its executable file and to write +to the directory that contains its executable file. It can be useful to check whether the process +has the necessary permissions to perform an update before trying to apply one. Use the +CanUpdate call to provide a useful message to the user if the update can't proceed without +elevated permissions: + + up := update.New().Target("/etc/hosts") + err := up.CanUpdate() + if err != nil { + fmt.Printf("Can't update because: '%v'. Try as root or Administrator\n", err) + return + } + err, errRecover := up.FromUrl("https://example.com/new/hosts") + +Although exceedingly unlikely, the update operation itself is not atomic and can fail +in such a way that a user's computer is left in an inconsistent state. If that happens, +go-update attempts to recover to leave the system in a good state. If the recovery step +fails (even more unlikely), a second error, referred to as "errRecover" will be non-nil +so that you may inform your users of the bad news. You should handle this case as shown +here: + + err, errRecover := up.FromUrl("https://example.com/update") + if err != nil { + fmt.Printf("Update failed: %v\n", err) + if errRecover != nil { + fmt.Printf("Failed to recover bad update: %v!\n", errRecover) + fmt.Printf("Program exectuable may be missing!\n") + } + } + +Subpackages + +Sub-package check contains the client functionality for a simple protocol for negotiating +whether a new update is available, where it is, and the metadata needed for verifying it. + +Sub-package download contains functionality for downloading from an HTTP endpoint +while outputting a progress meter and supports resuming partial downloads. +*/ +package update + +import ( + "bytes" + "crypto" + "crypto/rsa" + "crypto/sha256" + _ "crypto/sha512" // for tls cipher support + "crypto/x509" + "encoding/pem" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + + "github.com/inconshreveable/go-update/download" + "github.com/kardianos/osext" + "github.com/kr/binarydist" +) + +// The type of a binary patch, if any. Only bsdiff is supported +type PatchType string + +const ( + PATCHTYPE_BSDIFF PatchType = "bsdiff" + PATCHTYPE_NONE = "" +) + +type Update struct { + // empty string means "path of the current executable" + TargetPath string + + // type of patch to apply. PATCHTYPE_NONE means "not a patch" + PatchType + + // sha256 checksum of the new binary to verify against + Checksum []byte + + // public key to use for signature verification + PublicKey *rsa.PublicKey + + // signature to use for signature verification + Signature []byte + + // configurable http client can be passed to download + HTTPClient *http.Client +} + +func (u *Update) getPath() (string, error) { + if u.TargetPath == "" { + return osext.Executable() + } else { + return u.TargetPath, nil + } +} + +// New creates a new Update object. +// A default update object assumes the complete binary +// content will be used for update (not a patch) and that +// the intended target is the running executable. +// +// Use this as the start of a chain of calls on the Update +// object to build up your configuration. Example: +// +// up := update.New().ApplyPatch(update.PATCHTYPE_BSDIFF).VerifyChecksum(checksum) +// +func New() *Update { + return &Update{ + TargetPath: "", + PatchType: PATCHTYPE_NONE, + } +} + +// Target configures the update to update the file at the given path. +// The emptry string means 'the executable file of the running program'. +func (u *Update) Target(path string) *Update { + u.TargetPath = path + return u +} + +// ApplyPatch configures the update to treat the contents of the update +// as a patch to apply to the existing to target. You must specify the +// format of the patch. Only PATCHTYPE_BSDIFF is supported at the moment. +func (u *Update) ApplyPatch(patchType PatchType) *Update { + u.PatchType = patchType + return u +} + +// VerifyChecksum configures the update to verify that the +// the update has the given sha256 checksum. +func (u *Update) VerifyChecksum(checksum []byte) *Update { + u.Checksum = checksum + return u +} + +// VerifySignature configures the update to verify the given +// signature of the update. You must also call one of the +// VerifySignatureWith* functions to specify a public key +// to use for verification. +func (u *Update) VerifySignature(signature []byte) *Update { + u.Signature = signature + return u +} + +// VerifySignatureWith configures the update to use the given RSA +// public key to verify the update's signature. You must also call +// VerifySignature() with a signature to check. +// +// You'll probably want to use VerifySignatureWithPEM instead of +// parsing the public key yourself. +func (u *Update) VerifySignatureWith(publicKey *rsa.PublicKey) *Update { + u.PublicKey = publicKey + return u +} + +// VerifySignatureWithPEM configures the update to use the given PEM-formatted +// RSA public key to verify the update's signature. You must also call +// VerifySignature() with a signature to check. +// +// A PEM formatted public key typically begins with +// -----BEGIN PUBLIC KEY----- +func (u *Update) VerifySignatureWithPEM(publicKeyPEM []byte) (*Update, error) { + block, _ := pem.Decode(publicKeyPEM) + if block == nil { + return u, fmt.Errorf("Couldn't parse PEM data") + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return u, err + } + + var ok bool + u.PublicKey, ok = pub.(*rsa.PublicKey) + if !ok { + return u, fmt.Errorf("Public key isn't an RSA public key") + } + + return u, nil +} + +// FromUrl updates the target with the contents of the given URL. +func (u *Update) FromUrl(url string) (err error, errRecover error) { + target := new(download.MemoryTarget) + err = download.New(url, target, u.HTTPClient).Get() + if err != nil { + return + } + + return u.FromStream(target) +} + +// FromFile updates the target the contents of the given file. +func (u *Update) FromFile(path string) (err error, errRecover error) { + // open the new updated contents + fp, err := os.Open(path) + if err != nil { + return + } + defer fp.Close() + + // do the update + return u.FromStream(fp) +} + +// FromStream updates the target file with the contents of the supplied io.Reader. +// +// FromStream performs the following actions to ensure a safe cross-platform update: +// +// 1. If configured, applies the contents of the io.Reader as a binary patch. +// +// 2. If configured, computes the sha256 checksum and verifies it matches. +// +// 3. If configured, verifies the RSA signature with a public key. +// +// 4. Creates a new file, /path/to/.target.new with mode 0755 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 rename is successful, deletes /path/to/.target.old, returns no error +// +// 8. If the rename fails, attempts to rename /path/to/.target.old back to /path/to/target +// If this operation fails, it is reported in the errRecover return value so as not to +// mask the original error that caused the recovery attempt. +// +// On Windows, the removal of /path/to/.target.old always fails, so instead, +// we just make the old file hidden instead. +func (u *Update) FromStream(updateWith io.Reader) (err error, errRecover error) { + updatePath, err := u.getPath() + if err != nil { + return + } + + var newBytes []byte + // apply a patch if requested + switch u.PatchType { + case PATCHTYPE_BSDIFF: + newBytes, err = applyPatch(updateWith, updatePath) + if err != nil { + return + } + case PATCHTYPE_NONE: + // no patch to apply, go on through + newBytes, err = ioutil.ReadAll(updateWith) + if err != nil { + return + } + default: + err = fmt.Errorf("Unrecognized patch type: %s", u.PatchType) + return + } + + // verify checksum if requested + if u.Checksum != nil { + if err = verifyChecksum(newBytes, u.Checksum); err != nil { + return + } + } + + // verify signature if requested + if u.Signature != nil || u.PublicKey != nil { + if u.Signature == nil { + err = fmt.Errorf("No public key specified to verify signature") + return + } + + if u.PublicKey == nil { + err = fmt.Errorf("No signature to verify!") + return + } + + if err = verifySignature(newBytes, u.Signature, u.PublicKey); err != nil { + return + } + } + + // get the directory the executable exists in + updateDir := filepath.Dir(updatePath) + filename := filepath.Base(updatePath) + + // Copy the contents of of newbinary to a the 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, 0755) + if err != nil { + return + } + defer fp.Close() + _, err = io.Copy(fp, bytes.NewReader(newBytes)) + + // 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 := 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(updatePath, oldPath) + if err != nil { + return + } + + // move the new exectuable in to become the new program + err = os.Rename(newPath, updatePath) + + if err != nil { + // copy unsuccessful + errRecover = os.Rename(oldPath, updatePath) + } else { + // copy successful, remove the old binary + errRemove := os.Remove(oldPath) + + // windows has trouble with removing old binaries, so hide it instead + if errRemove != nil { + _ = hideFile(oldPath) + } + } + + return +} + +// CanUpdate() 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 (u *Update) CanUpdate() (err error) { + // get the directory the file exists in + path, err := u.getPath() + if err != nil { + return + } + + 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 := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) + if err != nil { + return + } + fp.Close() + + _ = os.Remove(newPath) + return +} + +func applyPatch(patch io.Reader, updatePath string) ([]byte, error) { + // open the file to update + old, err := os.Open(updatePath) + if err != nil { + return nil, err + } + defer old.Close() + + // apply the patch + applied := new(bytes.Buffer) + if err = binarydist.Patch(old, applied, patch); err != nil { + return nil, err + } + + return applied.Bytes(), nil +} + +func verifyChecksum(updated []byte, expectedChecksum []byte) error { + checksum, err := ChecksumForBytes(updated) + if err != nil { + return err + } + + if !bytes.Equal(expectedChecksum, checksum) { + return fmt.Errorf("Updated file has wrong checksum. Expected: %x, got: %x", expectedChecksum, checksum) + } + + return nil +} + +// ChecksumForFile returns the sha256 checksum for the given file +func ChecksumForFile(path string) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + return ChecksumForReader(f) +} + +// ChecksumForReader returns the sha256 checksum for the entire +// contents of the given reader. +func ChecksumForReader(rd io.Reader) ([]byte, error) { + h := sha256.New() + if _, err := io.Copy(h, rd); err != nil { + return nil, err + } + return h.Sum(nil), nil +} + +// ChecksumForBytes returns the sha256 checksum for the given bytes +func ChecksumForBytes(source []byte) ([]byte, error) { + return ChecksumForReader(bytes.NewReader(source)) +} + +func verifySignature(source, signature []byte, publicKey *rsa.PublicKey) error { + checksum, err := ChecksumForBytes(source) + if err != nil { + return err + } + + return rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, checksum, signature) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index ab1627103..32de85d36 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -92,6 +92,16 @@ "revision": "2dbddebb8266b93c5e6b119efb54e89043186f3f", "revisionTime": "2015-03-09T12:10:02-07:00" }, + { + "path": "github.com/inconshreveable/go-update", + "revision": "f848a144bdfc3d6dc7669d4b026ecc7a42ed4575", + "revisionTime": "2015-07-06T15:23:39-07:00" + }, + { + "path": "github.com/inconshreveable/go-update/download", + "revision": "f848a144bdfc3d6dc7669d4b026ecc7a42ed4575", + "revisionTime": "2015-07-06T15:23:39-07:00" + }, { "path": "github.com/itsjamie/gin-cors", "revision": "bd9551838cd52133960a2f44ab990be71744b663",