You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
342 lines
11 KiB
342 lines
11 KiB
// Copyright (c) 2020 Proton Technologies AG |
|
// |
|
// This file is part of ProtonMail Bridge. |
|
// |
|
// ProtonMail Bridge 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. |
|
// |
|
// ProtonMail Bridge 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. |
|
|
|
package updates |
|
|
|
import ( |
|
"bytes" |
|
"encoding/json" |
|
"errors" |
|
"io/ioutil" |
|
"os/exec" |
|
"path/filepath" |
|
"runtime" |
|
"strings" |
|
|
|
"github.com/ProtonMail/proton-bridge/internal/bridge" |
|
"github.com/ProtonMail/proton-bridge/internal/importexport" |
|
"github.com/ProtonMail/proton-bridge/pkg/constants" |
|
"github.com/kardianos/osext" |
|
"github.com/sirupsen/logrus" |
|
) |
|
|
|
const ( |
|
sigExtension = ".sig" |
|
) |
|
|
|
var ( |
|
Host = "https://protonmail.com" //nolint[gochecknoglobals] |
|
DownloadPath = "download" //nolint[gochecknoglobals] |
|
|
|
// BuildType specifies type of build (e.g. QA or beta). |
|
BuildType = "" //nolint[gochecknoglobals] |
|
) |
|
|
|
var ( |
|
log = logrus.WithField("pkg", "bridgeUtils/updates") //nolint[gochecknoglobals] |
|
|
|
ErrDownloadFailed = errors.New("error happened during download") //nolint[gochecknoglobals] |
|
ErrUpdateVerifyFailed = errors.New("cannot verify signature") //nolint[gochecknoglobals] |
|
) |
|
|
|
type Updates struct { |
|
version string |
|
revision string |
|
buildTime string |
|
releaseNotes string |
|
releaseFixedBugs string |
|
updateTempDir string |
|
landingPagePath string // Based on Host/; default landing page for download. |
|
winInstallerFile string // File for initial install or manual reinstall for windows |
|
macInstallerFile string // File for initial install or manual reinstall for mac |
|
linInstallerFile string // File for initial install or manual reinstall for linux |
|
versionFileBaseName string // Text file containing information about current file. per goos [_linux,_darwin,_windows].json (have .sig file). |
|
updateFileBaseName string // File for automatic update. per goos [_linux,_darwin,_windows].tgz (have .sig file). |
|
linuxFileBaseName string // Prefix of linux package names. |
|
macAppBundleName string // Name of Mac app file in the bundle for update procedure. |
|
cachedNewerVersion *VersionInfo // To have info about latest version even when the internet connection drops. |
|
} |
|
|
|
// NewBridge inits Updates struct for bridge. |
|
func NewBridge(updateTempDir string) *Updates { |
|
return &Updates{ |
|
version: constants.Version, |
|
revision: constants.Revision, |
|
buildTime: constants.BuildTime, |
|
releaseNotes: bridge.ReleaseNotes, |
|
releaseFixedBugs: bridge.ReleaseFixedBugs, |
|
updateTempDir: updateTempDir, |
|
landingPagePath: "bridge/download", |
|
winInstallerFile: "Bridge-Installer.exe", |
|
macInstallerFile: "Bridge-Installer.dmg", |
|
linInstallerFile: "Bridge-Installer.sh", |
|
versionFileBaseName: "current_version", |
|
updateFileBaseName: "bridge_upgrade", |
|
linuxFileBaseName: "protonmail-bridge", |
|
macAppBundleName: "ProtonMail Bridge.app", |
|
} |
|
} |
|
|
|
// NewImportExport inits Updates struct for import-export. |
|
func NewImportExport(updateTempDir string) *Updates { |
|
return &Updates{ |
|
version: constants.Version, |
|
revision: constants.Revision, |
|
buildTime: constants.BuildTime, |
|
releaseNotes: importexport.ReleaseNotes, |
|
releaseFixedBugs: importexport.ReleaseFixedBugs, |
|
updateTempDir: updateTempDir, |
|
landingPagePath: "import-export", |
|
winInstallerFile: "ie/Import-Export-app-installer.exe", |
|
macInstallerFile: "ie/Import-Export-app.dmg", |
|
linInstallerFile: "ie/Import-Export-app-installer.sh", |
|
versionFileBaseName: "current_version_ie", |
|
updateFileBaseName: "ie/ie_upgrade", |
|
linuxFileBaseName: "ie/protonmail-import-export-app", |
|
macAppBundleName: "Import-Export app.app", |
|
} |
|
} |
|
|
|
func (u *Updates) CreateJSONAndSign(deployDir, goos string) error { |
|
versionInfo := u.getLocalVersion(goos) |
|
versionInfo.Version = sanitizeVersion(versionInfo.Version) |
|
|
|
versionFileName := filepath.Base(u.versionFileURL(goos)) |
|
versionFilePath := filepath.Join(deployDir, versionFileName) |
|
|
|
txt, err := json.Marshal(versionInfo) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if err = ioutil.WriteFile(versionFilePath, txt, 0600); err != nil { |
|
return err |
|
} |
|
|
|
if err := singAndVerify(versionFilePath); err != nil { |
|
return err |
|
} |
|
|
|
updateFileName := filepath.Base(versionInfo.UpdateFile) |
|
updateFilePath := filepath.Join(deployDir, updateFileName) |
|
if err := singAndVerify(updateFilePath); err != nil { |
|
return err |
|
} |
|
|
|
return nil |
|
} |
|
|
|
func (u *Updates) CheckIsUpToDate() (isUpToDate bool, latestVersion VersionInfo, err error) { |
|
localVersion := u.GetLocalVersion() |
|
latestVersion, err = u.getLatestVersion() |
|
if err != nil { |
|
return |
|
} |
|
|
|
localIsOld, err := isFirstVersionNewer(latestVersion.Version, localVersion.Version) |
|
return !localIsOld, latestVersion, err |
|
} |
|
|
|
func (u *Updates) GetDownloadLink() string { |
|
latestVersion, err := u.getLatestVersion() |
|
if err != nil || latestVersion.InstallerFile == "" { |
|
localVersion := u.GetLocalVersion() |
|
return localVersion.GetDownloadLink() |
|
} |
|
return latestVersion.GetDownloadLink() |
|
} |
|
|
|
func (u *Updates) GetLocalVersion() VersionInfo { |
|
return u.getLocalVersion(runtime.GOOS) |
|
} |
|
|
|
func (u *Updates) getLocalVersion(goos string) VersionInfo { |
|
version := u.version |
|
if BuildType != "" { |
|
version += " " + BuildType |
|
} |
|
|
|
versionInfo := VersionInfo{ |
|
Version: version, |
|
Revision: u.revision, |
|
ReleaseDate: u.buildTime, |
|
ReleaseNotes: u.releaseNotes, |
|
ReleaseFixedBugs: u.releaseFixedBugs, |
|
FixedBugs: strings.Split(u.releaseFixedBugs, "\n"), |
|
URL: u.installerFileURL(goos), |
|
|
|
LandingPage: u.landingPageURL(), |
|
UpdateFile: u.updateFileURL(goos), |
|
InstallerFile: u.installerFileURL(goos), |
|
} |
|
|
|
if goos == "linux" { |
|
pkgName := u.linuxFileBaseName |
|
pkgRel := "1" |
|
pkgBaseFile := strings.Join([]string{Host, DownloadPath, pkgName}, "/") |
|
|
|
pkgBasePath := DownloadPath + "/" + pkgName // add at least one dir |
|
pkgBasePath = filepath.Dir(pkgBasePath) // keep only last dir |
|
pkgBasePath = Host + "/" + pkgBasePath // add host in the end to not strip off double slash in URL |
|
|
|
versionInfo.DebFile = pkgBaseFile + "_" + u.version + "-" + pkgRel + "_amd64.deb" |
|
versionInfo.RpmFile = pkgBaseFile + "-" + u.version + "-" + pkgRel + ".x86_64.rpm" |
|
versionInfo.PkgFile = strings.Join([]string{pkgBasePath, "PKGBUILD"}, "/") |
|
} |
|
|
|
return versionInfo |
|
} |
|
|
|
func (u *Updates) getLatestVersion() (latestVersion VersionInfo, err error) { |
|
version, err := downloadToBytes(u.versionFileURL(runtime.GOOS)) |
|
if err != nil { |
|
if u.cachedNewerVersion != nil { |
|
return *u.cachedNewerVersion, nil |
|
} |
|
return |
|
} |
|
|
|
signature, err := downloadToBytes(u.signatureFileURL(runtime.GOOS)) |
|
if err != nil { |
|
if u.cachedNewerVersion != nil { |
|
return *u.cachedNewerVersion, nil |
|
} |
|
return |
|
} |
|
|
|
if err = verifyBytes(bytes.NewReader(version), bytes.NewReader(signature)); err != nil { |
|
return |
|
} |
|
|
|
if err = json.NewDecoder(bytes.NewReader(version)).Decode(&latestVersion); err != nil { |
|
return |
|
} |
|
if localIsOld, _ := isFirstVersionNewer(latestVersion.Version, u.version); localIsOld { |
|
u.cachedNewerVersion = &latestVersion |
|
} |
|
return |
|
} |
|
|
|
func (u *Updates) landingPageURL() string { |
|
return strings.Join([]string{Host, u.landingPagePath}, "/") |
|
} |
|
|
|
func (u *Updates) signatureFileURL(goos string) string { |
|
return u.versionFileURL(goos) + sigExtension |
|
} |
|
|
|
func (u *Updates) versionFileURL(goos string) string { |
|
return strings.Join([]string{Host, DownloadPath, u.versionFileBaseName + "_" + goos + ".json"}, "/") |
|
} |
|
|
|
func (u *Updates) installerFileURL(goos string) string { |
|
installerFile := u.linInstallerFile |
|
switch goos { |
|
case "darwin": //nolint[goconst] |
|
installerFile = u.macInstallerFile |
|
case "windows": //nolint[goconst] |
|
installerFile = u.winInstallerFile |
|
} |
|
return strings.Join([]string{Host, DownloadPath, installerFile}, "/") |
|
} |
|
|
|
func (u *Updates) updateFileURL(goos string) string { |
|
return strings.Join([]string{Host, DownloadPath, u.updateFileBaseName + "_" + goos + ".tgz"}, "/") |
|
} |
|
|
|
func (u *Updates) StartUpgrade(currentStatus chan<- Progress) { // nolint[funlen] |
|
status := &Progress{channel: currentStatus} |
|
defer status.Update() |
|
|
|
// Get latest version. |
|
var verInfo VersionInfo |
|
status.UpdateDescription(InfoCurrentVersion) |
|
if verInfo, status.Err = u.getLatestVersion(); status.Err != nil { |
|
return |
|
} |
|
|
|
if verInfo.UpdateFile == "" { |
|
log.Warn("Empty update URL. Update manually.") |
|
status.Err = ErrDownloadFailed |
|
return |
|
} |
|
|
|
// Download. |
|
status.UpdateDescription(InfoDownloading) |
|
if status.Err = mkdirAllClear(u.updateTempDir); status.Err != nil { |
|
return |
|
} |
|
var updateTar string |
|
updateTar, status.Err = downloadWithSignature( |
|
status, |
|
verInfo.UpdateFile, |
|
u.updateTempDir, |
|
) |
|
if status.Err != nil { |
|
return |
|
} |
|
|
|
// Check signature. |
|
status.UpdateDescription(InfoVerifying) |
|
status.Err = verifyFile(updateTar) |
|
if status.Err != nil { |
|
log.Warnf("Cannot verify update file %s: %v", updateTar, status.Err) |
|
status.Err = ErrUpdateVerifyFailed |
|
return |
|
} |
|
|
|
// Untar. |
|
status.UpdateDescription(InfoUnpacking) |
|
status.Err = untarToDir(updateTar, u.updateTempDir, status) |
|
if status.Err != nil { |
|
return |
|
} |
|
|
|
// Run upgrade (OS specific). |
|
status.UpdateDescription(InfoUpgrading) |
|
switch runtime.GOOS { |
|
case "windows": //nolint[goconst] |
|
installerFile := strings.Split(u.winInstallerFile, "/")[1] |
|
cmd := exec.Command("./" + installerFile) // nolint[gosec] |
|
cmd.Dir = u.updateTempDir |
|
status.Err = cmd.Start() |
|
case "darwin": //nolint[goconst] |
|
// current path is better then appDir = filepath.Join("/Applications") |
|
var exePath string |
|
exePath, status.Err = osext.Executable() |
|
if status.Err != nil { |
|
return |
|
} |
|
localPath := filepath.Dir(exePath) // Macos |
|
localPath = filepath.Dir(localPath) // Contents |
|
localPath = filepath.Dir(localPath) // .app |
|
|
|
updatePath := filepath.Join(u.updateTempDir, u.macAppBundleName) |
|
log.Warn("localPath ", localPath) |
|
log.Warn("updatePath ", updatePath) |
|
status.Err = syncFolders(localPath, updatePath) |
|
if status.Err != nil { |
|
return |
|
} |
|
status.UpdateDescription(InfoRestartApp) |
|
return |
|
default: |
|
status.Err = errors.New("upgrade for " + runtime.GOOS + " not implemented") |
|
} |
|
|
|
status.UpdateDescription(InfoQuitApp) |
|
}
|
|
|