From fbb1e168f72d31c22f24933808ef9dd1696c2f07 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Mon, 22 Dec 2014 10:52:09 +0100 Subject: [PATCH 1/3] Include MD5 sums in archives --- .gitignore | 2 ++ build.go | 43 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1c288100..c24a07bd 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ perfstats*.csv coverage.xml !gui/scripts/syncthing .DS_Store +syncthing.md5 +syncthing.exe.md5 diff --git a/build.go b/build.go index 8b67cfce..b1594c0f 100644 --- a/build.go +++ b/build.go @@ -22,6 +22,7 @@ import ( "archive/zip" "bytes" "compress/gzip" + "crypto/md5" "flag" "fmt" "io" @@ -190,7 +191,12 @@ func install(pkg string, tags []string) { } func build(pkg string, tags []string) { - rmr("syncthing", "syncthing.exe") + binary := "syncthing" + if goos == "windows" { + binary += ".exe" + } + + rmr(binary, binary+".md5") args := []string{"build", "-ldflags", ldflags()} if len(tags) > 0 { args = append(args, "-tags", strings.Join(tags, ",")) @@ -201,6 +207,13 @@ func build(pkg string, tags []string) { args = append(args, pkg) setBuildEnv() runPrint("go", args...) + + // Create an md5 checksum of the binary, to be included in the archive for + // automatic upgrades. + err := md5File(binary) + if err != nil { + log.Fatal(err) + } } func buildTar() { @@ -217,6 +230,7 @@ func buildTar() { {"LICENSE", name + "/LICENSE.txt"}, {"AUTHORS", name + "/AUTHORS.txt"}, {"syncthing", name + "/syncthing"}, + {"syncthing.md5", name + "/syncthing.md5"}, } for _, file := range listFiles("etc") { files = append(files, archiveFile{file, name + "/" + file}) @@ -239,6 +253,7 @@ func buildZip() { {"LICENSE", name + "/LICENSE.txt"}, {"AUTHORS", name + "/AUTHORS.txt"}, {"syncthing.exe", name + "/syncthing.exe"}, + {"syncthing.exe.md5", name + "/syncthing.exe.md5"}, } zipFile(filename, files) log.Println(filename) @@ -554,3 +569,29 @@ func zipFile(out string, files []archiveFile) { log.Fatal(err) } } + +func md5File(file string) error { + fd, err := os.Open(file) + if err != nil { + return err + } + defer fd.Close() + + h := md5.New() + _, err = io.Copy(h, fd) + if err != nil { + return err + } + + out, err := os.Create(file + ".md5") + if err != nil { + return err + } + + _, err = fmt.Fprintf(out, "%x\n", h.Sum(nil)) + if err != nil { + return err + } + + return out.Close() +} From 110816c7aa29e3d4ad8aea540c0467c35208504f Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Mon, 22 Dec 2014 11:03:17 +0100 Subject: [PATCH 2/3] Consolidate Windows/Unix upgrading and check MD5 (fixes #1138) --- internal/upgrade/upgrade_supported.go | 261 +++++++++++++++++++++----- internal/upgrade/upgrade_windows.go | 169 ----------------- 2 files changed, 209 insertions(+), 221 deletions(-) delete mode 100644 internal/upgrade/upgrade_windows.go diff --git a/internal/upgrade/upgrade_supported.go b/internal/upgrade/upgrade_supported.go index fe44a734..b80c3fe8 100644 --- a/internal/upgrade/upgrade_supported.go +++ b/internal/upgrade/upgrade_supported.go @@ -13,13 +13,16 @@ // You should have received a copy of the GNU General Public License along // with this program. If not, see . -// +build !windows,!noupgrade +// +build !noupgrade package upgrade import ( "archive/tar" + "archive/zip" + "bytes" "compress/gzip" + "crypto/md5" "encoding/json" "fmt" "io" @@ -28,43 +31,10 @@ import ( "os" "path" "path/filepath" + "runtime" "strings" ) -// Upgrade to the given release, saving the previous binary with a ".old" extension. -func upgradeTo(path string, rel Release) error { - expectedRelease := releaseName(rel.Tag) - if debug { - l.Debugf("expected release asset %q", expectedRelease) - } - for _, asset := range rel.Assets { - if debug { - l.Debugln("considering release", asset) - } - if strings.HasPrefix(asset.Name, expectedRelease) { - if strings.HasSuffix(asset.Name, ".tar.gz") { - fname, err := readTarGZ(asset.URL, filepath.Dir(path)) - if err != nil { - return err - } - - old := path + ".old" - err = os.Rename(path, old) - if err != nil { - return err - } - err = os.Rename(fname, path) - if err != nil { - return err - } - return nil - } - } - } - - return ErrVersionUnknown -} - // Returns the latest release, including prereleases or not depending on the argument func LatestRelease(prerelease bool) (Release, error) { resp, err := http.Get("https://api.github.com/repos/syncthing/syncthing/releases?per_page=10") @@ -97,7 +67,42 @@ func LatestRelease(prerelease bool) (Release, error) { return Release{}, ErrVersionUnknown } -func readTarGZ(url string, dir string) (string, error) { +// Upgrade to the given release, saving the previous binary with a ".old" extension. +func upgradeTo(binary string, rel Release) error { + expectedRelease := releaseName(rel.Tag) + if debug { + l.Debugf("expected release asset %q", expectedRelease) + } + for _, asset := range rel.Assets { + assetName := path.Base(asset.Name) + if debug { + l.Debugln("considering release", assetName) + } + + if strings.HasPrefix(assetName, expectedRelease) { + fname, err := readRelease(filepath.Dir(binary), asset.URL) + if err != nil { + return err + } + + old := binary + ".old" + _ = os.Remove(old) + err = os.Rename(binary, old) + if err != nil { + return err + } + err = os.Rename(fname, binary) + if err != nil { + return err + } + return nil + } + } + + return ErrVersionUnknown +} + +func readRelease(dir, url string) (string, error) { if debug { l.Debugf("loading %q", url) } @@ -114,14 +119,26 @@ func readTarGZ(url string, dir string) (string, error) { } defer resp.Body.Close() - gr, err := gzip.NewReader(resp.Body) + switch runtime.GOOS { + case "windows": + return readZip(dir, resp.Body) + default: + return readTarGz(dir, resp.Body) + } +} + +func readTarGz(dir string, r io.Reader) (string, error) { + gr, err := gzip.NewReader(r) if err != nil { return "", err } tr := tar.NewReader(gr) + var tempName, actualMD5, expectedMD5 string + // Iterate through the files in the archive. +fileLoop: for { hdr, err := tr.Next() if err == io.EOF { @@ -131,37 +148,177 @@ func readTarGZ(url string, dir string) (string, error) { if err != nil { return "", err } + + shortName := path.Base(hdr.Name) + if debug { - l.Debugf("considering file %q", hdr.Name) + l.Debugf("considering file %q", shortName) } - if path.Base(hdr.Name) == "syncthing" { - of, err := ioutil.TempFile(dir, "syncthing") + switch shortName { + case "syncthing": + if debug { + l.Debugln("writing and hashing binary") + } + tempName, actualMD5, err = writeBinary(dir, tr) if err != nil { return "", err } - _, err = io.Copy(of, tr) + if expectedMD5 != "" { + // We're done + break fileLoop + } + + case "syncthing.md5": + bs, err := ioutil.ReadAll(tr) if err != nil { - os.Remove(of.Name()) return "", err } - err = of.Close() - if err != nil { - os.Remove(of.Name()) - return "", err + expectedMD5 = strings.TrimSpace(string(bs)) + if debug { + l.Debugln("expected md5 is", actualMD5) } - err = os.Chmod(of.Name(), os.FileMode(hdr.Mode)) - if err != nil { - os.Remove(of.Name()) - return "", err + if actualMD5 != "" { + // We're done + break fileLoop } - - return of.Name(), nil } } + if tempName != "" && actualMD5 != "" { + // We found and saved something to disk. + if expectedMD5 == "" { + if debug { + l.Debugln("there is no md5 to compare with") + } + } else if actualMD5 != expectedMD5 { + // There was an md5 file included in the archive, and it doesn't + // match what we just wrote to disk. + return "", fmt.Errorf("incorrect MD5 checksum") + } + return tempName, nil + } + + return "", fmt.Errorf("no upgrade found") +} + +func readZip(dir string, r io.Reader) (string, error) { + body, err := ioutil.ReadAll(r) + if err != nil { + return "", err + } + + archive, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) + if err != nil { + return "", err + } + + var tempName, actualMD5, expectedMD5 string + + // Iterate through the files in the archive. +fileLoop: + for _, file := range archive.File { + shortName := path.Base(file.Name) + + if debug { + l.Debugf("considering file %q", shortName) + } + + switch shortName { + case "syncthing.exe": + if debug { + l.Debugln("writing and hashing binary") + } + + inFile, err := file.Open() + if err != nil { + return "", err + } + tempName, actualMD5, err = writeBinary(dir, inFile) + if err != nil { + return "", err + } + + if expectedMD5 != "" { + // We're done + break fileLoop + } + + case "syncthing.exe.md5": + inFile, err := file.Open() + if err != nil { + return "", err + } + bs, err := ioutil.ReadAll(inFile) + if err != nil { + return "", err + } + + expectedMD5 = strings.TrimSpace(string(bs)) + if debug { + l.Debugln("expected md5 is", actualMD5) + } + + if actualMD5 != "" { + // We're done + break fileLoop + } + } + } + + if tempName != "" && actualMD5 != "" { + // We found and saved something to disk. + if expectedMD5 == "" { + if debug { + l.Debugln("there is no md5 to compare with") + } + } else if actualMD5 != expectedMD5 { + // There was an md5 file included in the archive, and it doesn't + // match what we just wrote to disk. + return "", fmt.Errorf("incorrect MD5 checksum") + } + return tempName, nil + } + return "", fmt.Errorf("No upgrade found") } + +func writeBinary(dir string, inFile io.Reader) (filename, md5sum string, err error) { + outFile, err := ioutil.TempFile(dir, "syncthing") + if err != nil { + return "", "", err + } + + // Write the binary both a temporary file and to the MD5 hasher. + + h := md5.New() + mw := io.MultiWriter(h, outFile) + + _, err = io.Copy(mw, inFile) + if err != nil { + os.Remove(outFile.Name()) + return "", "", err + } + + err = outFile.Close() + if err != nil { + os.Remove(outFile.Name()) + return "", "", err + } + + err = os.Chmod(outFile.Name(), os.FileMode(0755)) + if err != nil { + os.Remove(outFile.Name()) + return "", "", err + } + + actualMD5 := fmt.Sprintf("%x", h.Sum(nil)) + if debug { + l.Debugln("actual md5 is", actualMD5) + } + + return outFile.Name(), actualMD5, nil +} diff --git a/internal/upgrade/upgrade_windows.go b/internal/upgrade/upgrade_windows.go deleted file mode 100644 index 9627f7cf..00000000 --- a/internal/upgrade/upgrade_windows.go +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright (C) 2014 The Syncthing Authors. -// -// This program is free software: you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation, either version 3 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program. If not, see . - -// +build windows,!noupgrade - -package upgrade - -import ( - "archive/zip" - "bytes" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/http" - "os" - "path" - "path/filepath" - "strings" -) - -// Upgrade to the given release, saving the previous binary with a ".old" extension. -func upgradeTo(path string, rel Release) error { - expectedRelease := releaseName(rel.Tag) - if debug { - l.Debugf("expected release asset %q", expectedRelease) - } - for _, asset := range rel.Assets { - if debug { - l.Debugln("considering release", asset) - } - if strings.HasPrefix(asset.Name, expectedRelease) { - if strings.HasSuffix(asset.Name, ".zip") { - fname, err := readZip(asset.URL, filepath.Dir(path)) - if err != nil { - return err - } - - old := path + ".old" - - os.Remove(old) - err = os.Rename(path, old) - if err != nil { - return err - } - err = os.Rename(fname, path) - if err != nil { - return err - } - return nil - } - } - } - - return ErrVersionUnknown -} - -// Returns the latest release, including prereleases or not depending on the argument -func LatestRelease(prerelease bool) (Release, error) { - resp, err := http.Get("https://api.github.com/repos/syncthing/syncthing/releases?per_page=10") - if err != nil { - return Release{}, err - } - if resp.StatusCode > 299 { - return Release{}, fmt.Errorf("API call returned HTTP error: %s", resp.Status) - } - - var rels []Release - json.NewDecoder(resp.Body).Decode(&rels) - resp.Body.Close() - - if len(rels) == 0 { - return Release{}, ErrVersionUnknown - } - - if prerelease { - // We are a beta version. Use the latest. - return rels[0], nil - } else { - // We are a regular release. Only consider non-prerelease versions for upgrade. - for _, rel := range rels { - if !rel.Prerelease { - return rel, nil - } - } - return Release{}, ErrVersionUnknown - } -} - -func readZip(url, dir string) (string, error) { - if debug { - l.Debugf("loading %q", url) - } - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return "", err - } - - req.Header.Add("Accept", "application/octet-stream") - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", err - } - - archive, err := zip.NewReader(bytes.NewReader(body), resp.ContentLength) - if err != nil { - return "", err - } - - // Iterate through the files in the archive. - for _, file := range archive.File { - - if debug { - l.Debugf("considering file %q", file.Name) - } - - if path.Base(file.Name) == "syncthing.exe" { - infile, err := file.Open() - if err != nil { - return "", err - } - - outfile, err := ioutil.TempFile(dir, "syncthing") - if err != nil { - return "", err - } - - _, err = io.Copy(outfile, infile) - if err != nil { - return "", err - } - - err = infile.Close() - if err != nil { - return "", err - } - - err = outfile.Close() - if err != nil { - os.Remove(outfile.Name()) - return "", err - } - - os.Chmod(outfile.Name(), file.Mode()) - return outfile.Name(), nil - } - } - - return "", fmt.Errorf("No upgrade found") -} From cde8ef56e561153cbb56f8d9454d740577c94d69 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Mon, 22 Dec 2014 12:07:04 +0100 Subject: [PATCH 3/3] Implement manual -upgrade-to option --- cmd/syncthing/main.go | 11 ++++++++ internal/upgrade/upgrade_common.go | 20 +++++++++++++++ internal/upgrade/upgrade_supported.go | 37 +++++++++++++++------------ internal/upgrade/upgrade_unsupp.go | 6 ++++- 4 files changed, 57 insertions(+), 17 deletions(-) diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index b81d83cd..8dace043 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -182,6 +182,7 @@ var ( showVersion bool doUpgrade bool doUpgradeCheck bool + upgradeTo string noBrowser bool noConsole bool generateDir string @@ -227,6 +228,7 @@ func main() { flag.BoolVar(&doUpgrade, "upgrade", false, "Perform upgrade") flag.BoolVar(&doUpgradeCheck, "upgrade-check", false, "Check for available upgrade") flag.BoolVar(&showVersion, "version", false, "Show version") + flag.StringVar(&upgradeTo, "upgrade-to", upgradeTo, "Force upgrade directly from specified URL") flag.Usage = usageFor(flag.CommandLine, usage, fmt.Sprintf(extraUsage, defConfDir)) flag.Parse() @@ -315,6 +317,15 @@ func main() { // Ensure that our home directory exists. ensureDir(confDir, 0700) + if upgradeTo != "" { + err := upgrade.ToURL(upgradeTo) + if err != nil { + l.Fatalln("Upgrade:", err) // exits 1 + } + l.Okln("Upgraded from", upgradeTo) + return + } + if doUpgrade || doUpgradeCheck { rel, err := upgrade.LatestRelease(IsBeta) if err != nil { diff --git a/internal/upgrade/upgrade_common.go b/internal/upgrade/upgrade_common.go index 5ad69754..d447eef1 100644 --- a/internal/upgrade/upgrade_common.go +++ b/internal/upgrade/upgrade_common.go @@ -67,6 +67,26 @@ func To(rel Release) error { } } +// A wrapper around actual implementations +func ToURL(url string) error { + select { + case <-upgradeUnlocked: + path, err := osext.Executable() + if err != nil { + upgradeUnlocked <- true + return err + } + err = upgradeToURL(path, url) + // If we've failed to upgrade, unlock so that another attempt could be made + if err != nil { + upgradeUnlocked <- true + } + return err + default: + return ErrUpgradeInProgress + } +} + // Returns 1 if a>b, -1 if a