// Copyright (C) 2014 The Syncthing Authors. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at http://mozilla.org/MPL/2.0/. // +build !noupgrade package upgrade import ( "archive/tar" "archive/zip" "bytes" "compress/gzip" "crypto/tls" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "os" "path" "path/filepath" "runtime" "sort" "strings" "github.com/syncthing/syncthing/lib/dialer" "github.com/syncthing/syncthing/lib/signature" ) const DisabledByCompilation = false // This is an HTTP/HTTPS client that does *not* perform certificate // validation. We do this because some systems where Syncthing runs have // issues with old or missing CA roots. It doesn't actually matter that we // load the upgrade insecurely as we verify an ECDSA signature of the actual // binary contents before accepting the upgrade. var insecureHTTP = &http.Client{ Transport: &http.Transport{ Dial: dialer.Dial, Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, }, } // FetchLatestReleases returns the latest releases, including prereleases or // not depending on the argument func FetchLatestReleases(releasesURL, version string) ([]Release, error) { resp, err := insecureHTTP.Get(releasesURL) if err != nil { return nil, err } if resp.StatusCode > 299 { return nil, fmt.Errorf("API call returned HTTP error: %s", resp.Status) } var rels []Release json.NewDecoder(resp.Body).Decode(&rels) resp.Body.Close() return rels, nil } type SortByRelease []Release func (s SortByRelease) Len() int { return len(s) } func (s SortByRelease) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s SortByRelease) Less(i, j int) bool { return CompareVersions(s[i].Tag, s[j].Tag) > 0 } func LatestRelease(releasesURL, version string) (Release, error) { rels, _ := FetchLatestReleases(releasesURL, version) return SelectLatestRelease(version, rels) } func SelectLatestRelease(version string, rels []Release) (Release, error) { if len(rels) == 0 { return Release{}, ErrVersionUnknown } sort.Sort(SortByRelease(rels)) // Check for a beta build beta := strings.Contains(version, "-") for _, rel := range rels { if rel.Prerelease && !beta { continue } for _, asset := range rel.Assets { assetName := path.Base(asset.Name) // Check for the architecture expectedRelease := releaseName(rel.Tag) l.Debugf("expected release asset %q", expectedRelease) l.Debugln("considering release", assetName) if strings.HasPrefix(assetName, expectedRelease) { return rel, nil } } } return Release{}, ErrVersionUnknown } // Upgrade to the given release, saving the previous binary with a ".old" extension. func upgradeTo(binary string, rel Release) error { expectedRelease := releaseName(rel.Tag) l.Debugf("expected release asset %q", expectedRelease) for _, asset := range rel.Assets { assetName := path.Base(asset.Name) l.Debugln("considering release", assetName) if strings.HasPrefix(assetName, expectedRelease) { return upgradeToURL(binary, asset.URL) } } return ErrVersionUnknown } // Upgrade to the given release, saving the previous binary with a ".old" extension. func upgradeToURL(binary string, url string) error { fname, err := readRelease(filepath.Dir(binary), 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 } func readRelease(dir, url string) (string, error) { 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 := insecureHTTP.Do(req) if err != nil { return "", err } defer resp.Body.Close() 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 string var sig []byte // Iterate through the files in the archive. for { hdr, err := tr.Next() if err == io.EOF { // end of tar archive break } if err != nil { return "", err } err = archiveFileVisitor(dir, &tempName, &sig, hdr.Name, tr) if err != nil { return "", err } if tempName != "" && sig != nil { break } } if err := verifyUpgrade(tempName, sig); err != nil { return "", err } return tempName, nil } 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 string var sig []byte // Iterate through the files in the archive. for _, file := range archive.File { inFile, err := file.Open() if err != nil { return "", err } err = archiveFileVisitor(dir, &tempName, &sig, file.Name, inFile) inFile.Close() if err != nil { return "", err } if tempName != "" && sig != nil { break } } if err := verifyUpgrade(tempName, sig); err != nil { return "", err } return tempName, nil } // archiveFileVisitor is called for each file in an archive. It may set // tempFile and signature. func archiveFileVisitor(dir string, tempFile *string, signature *[]byte, archivePath string, filedata io.Reader) error { var err error filename := path.Base(archivePath) archiveDir := path.Dir(archivePath) archiveDirs := strings.Split(archiveDir, "/") if len(archiveDirs) > 1 { //don't consider files in subfolders return nil } l.Debugf("considering file %s", archivePath) switch filename { case "syncthing", "syncthing.exe": l.Debugf("found upgrade binary %s", archivePath) *tempFile, err = writeBinary(dir, filedata) if err != nil { return err } case "syncthing.sig", "syncthing.exe.sig": l.Debugf("found signature %s", archivePath) *signature, err = ioutil.ReadAll(filedata) if err != nil { return err } } return nil } func verifyUpgrade(tempName string, sig []byte) error { if tempName == "" { return fmt.Errorf("no upgrade found") } if sig == nil { return fmt.Errorf("no signature found") } l.Debugf("checking signature\n%s", sig) fd, err := os.Open(tempName) if err != nil { return err } err = signature.Verify(SigningKey, sig, fd) fd.Close() if err != nil { os.Remove(tempName) return err } return nil } func writeBinary(dir string, inFile io.Reader) (filename string, err error) { // Write the binary to a temporary file. outFile, err := ioutil.TempFile(dir, "syncthing") if err != nil { return "", err } _, err = io.Copy(outFile, 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 } return outFile.Name(), nil }