From a0f32d1df11b1b932480597721704463d5b3ab1e Mon Sep 17 00:00:00 2001 From: Mark Sanborn Date: Tue, 12 Nov 2013 22:23:41 -0800 Subject: [PATCH 1/4] Initial commit --- .gitignore | 14 ++++++ main.go | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 .gitignore create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..492a759 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Use glob syntax. +syntax: glob + +*.DS_Store +*.swp +*.swo +*.pyc +*.php~ +*.orig +*~ +*.db +*.log +public/* +go-selfupdate diff --git a/main.go b/main.go new file mode 100644 index 0000000..be11f09 --- /dev/null +++ b/main.go @@ -0,0 +1,131 @@ +package main + +import ( + "fmt" + "os" + "encoding/json" + "io/ioutil" + "io" + "path/filepath" + "compress/gzip" + "bytes" + "crypto/sha256" + "github.com/kr/binarydist" + "runtime" + //"encoding/base64" +) + +const ( + plat = runtime.GOOS + "-" + runtime.GOARCH +) + +type current struct { + Version string + Sha256 []byte +} + +func generateSha256(path string) []byte { + h := sha256.New() + b, err := ioutil.ReadFile(path) + if err != nil { + fmt.Println(err) + } + h.Write(b) + sum := h.Sum(nil) + return sum + //return base64.URLEncoding.EncodeToString(sum) +} + +type gzReader struct { + z, r io.ReadCloser +} + +func (g *gzReader) Read(p []byte) (int, error) { + return g.z.Read(p) +} + +func (g *gzReader) Close() error { + g.z.Close() + return g.r.Close() +} + +func newGzReader(r io.ReadCloser) io.ReadCloser { + var err error + g := new(gzReader) + g.r = r + g.z, err = gzip.NewReader(r) + if err != nil { + panic(err) + } + return g +} + +func main() { + appPath := os.Args[1] + version := os.Args[2] + genDir := "public" + os.MkdirAll(genDir, 0755) + + c := current{Version: version, Sha256: generateSha256(appPath)} + + b, err := json.MarshalIndent(c, "", " ") + if err != nil { + fmt.Println("error:", err) + } + err = ioutil.WriteFile(filepath.Join(genDir, plat + ".json"), b, 0755) + if err != nil { + panic(err) + } + + os.MkdirAll(filepath.Join(genDir, version), 0755) + + var buf bytes.Buffer + w := gzip.NewWriter(&buf) + f, err := ioutil.ReadFile(appPath) + if err != nil { + panic(err) + } + w.Write(f) + w.Close() // You must close this first to flush the bytes to the buffer. + err = ioutil.WriteFile(filepath.Join(genDir, version, plat + ".gz"), buf.Bytes(), 0755) + + files, err := ioutil.ReadDir(genDir) + if err != nil { + fmt.Println(err) + } + + for _, file := range files { + if file.IsDir() == false { + continue + } + if file.Name() == version { + continue + } + os.Mkdir(filepath.Join(genDir, file.Name(), version), 0755) + + fName := filepath.Join(genDir, file.Name(), plat + ".gz") + old, err := os.Open(fName) + if err != nil { + fmt.Fprintf(os.Stderr, "Can't open %s: error: %s\n", fName, err) + os.Exit(1) + } + + fName = filepath.Join(genDir, version, plat + ".gz") + newF, err := os.Open(fName) + if err != nil { + fmt.Fprintf(os.Stderr, "Can't open %s: error: %s\n", fName, err) + os.Exit(1) + } + + ar := newGzReader(old) + defer ar.Close() + br := newGzReader(newF) + defer br.Close() + patch := new(bytes.Buffer) + if err := binarydist.Diff(ar, br, patch); err != nil { + panic(err) + } + ioutil.WriteFile(filepath.Join(genDir, file.Name(), version, plat), patch.Bytes(), 0755) + } + +} From 8a48766e0b5a8f096c3030e48994e423652b16c6 Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 12 Nov 2013 22:24:58 -0800 Subject: [PATCH 2/4] Create README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..a365b17 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +go-selfupdate +============= + +Enable your Golang applications to self update From f906cb7b7d0ade0b974c7ddc1c3ca06824f7d08b Mon Sep 17 00:00:00 2001 From: vagrant Date: Thu, 14 Nov 2013 17:36:10 -0800 Subject: [PATCH 3/4] initial --- main.go | 1 + selfupdate/selfupdate.go | 263 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 selfupdate/selfupdate.go diff --git a/main.go b/main.go index be11f09..fe1229a 100644 --- a/main.go +++ b/main.go @@ -101,6 +101,7 @@ func main() { if file.Name() == version { continue } + os.Mkdir(filepath.Join(genDir, file.Name(), version), 0755) fName := filepath.Join(genDir, file.Name(), plat + ".gz") diff --git a/selfupdate/selfupdate.go b/selfupdate/selfupdate.go new file mode 100644 index 0000000..b7020f7 --- /dev/null +++ b/selfupdate/selfupdate.go @@ -0,0 +1,263 @@ +package selfupdate + +import ( + "bitbucket.org/kardianos/osext" + "bytes" + "compress/gzip" + "crypto/sha256" + "encoding/json" + "errors" + "fmt" + "github.com/inconshreveable/go-update" + "github.com/kr/binarydist" + "io" + "io/ioutil" + "log" + "math/rand" + "net/http" + "os" + "runtime" + "time" + "path/filepath" +) + +const ( + upcktimePath = "cktime" + plat = runtime.GOOS + "-" + runtime.GOARCH +) + +const devValidTime = 7 * 24 * time.Hour + +var ErrHashMismatch = errors.New("new file hash mismatch after patch") + +// 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] +type Updater struct { + CurrentVersion string + ApiURL string + CmdName string + BinURL string + DiffURL string + Dir string + Info struct { + Version string + Sha256 []byte + } +} + +func (u *Updater) getExecRelativeDir(dir string) string { + filename, _ := osext.Executable() + path := filepath.Join(filepath.Dir(filename), dir) + fmt.Println(path) + return path +} + +func (u *Updater) BackgroundRun() { + os.MkdirAll(u.getExecRelativeDir(u.Dir), 0777) + if u.wantUpdate() { + if err := update.SanityCheck(); err != nil { + // fail + return + } + //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 { + log.Fatal(err) + } + } +} + +func (u *Updater) wantUpdate() bool { + path := u.getExecRelativeDir(u.Dir + upcktimePath) + if u.CurrentVersion == "dev" || readTime(path).After(time.Now()) { + return false + } + wait := 24*time.Hour + randDuration(24*time.Hour) + return writeTime(path, time.Now().Add(wait)) +} + +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 { + 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 { + 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 := update.FromStream(bytes.NewBuffer(bin)) + if errRecover != nil { + return fmt.Errorf("update and recovery errors: %q %q", err, errRecover) + } + if err != nil { + return err + } + return nil +} + +func (u *Updater) fetchInfo() error { + fmt.Println(u.ApiURL) + fmt.Println(plat) + 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 { + 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 +} From 0bed6e537a7649f4025945b3285cbe1527b674d1 Mon Sep 17 00:00:00 2001 From: Mark Sanborn Date: Fri, 15 Nov 2013 10:45:45 -0800 Subject: [PATCH 4/4] Removed log.Fatal as we dont want to stop the application if the update fails --- selfupdate/selfupdate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfupdate/selfupdate.go b/selfupdate/selfupdate.go index b7020f7..33d2b6a 100644 --- a/selfupdate/selfupdate.go +++ b/selfupdate/selfupdate.go @@ -87,7 +87,7 @@ func (u *Updater) BackgroundRun() { //} // TODO(bgentry): logger isn't on Windows. Replace w/ proper error reports. if err := u.update(); err != nil { - log.Fatal(err) + log.Println(err) } } }