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.
310 lines
9.2 KiB
310 lines
9.2 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/pkg/config" |
|
"github.com/kardianos/osext" |
|
) |
|
|
|
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 = config.GetLogEntry("bridgeUtils/updates") //nolint[gochecknoglobals] |
|
|
|
installFileSuffix = map[string]string{ //nolint[gochecknoglobals] |
|
"darwin": ".dmg", |
|
"windows": ".exe", |
|
"linux": ".sh", |
|
} |
|
|
|
ErrDownloadFailed = errors.New("error happened during download") //nolint[gochecknoglobals] |
|
ErrUpdateVerifyFailed = errors.New("cannot verify signature") //nolint[gochecknoglobals] |
|
) |
|
|
|
type Updates struct { |
|
appName string |
|
version string |
|
revision string |
|
buildTime string |
|
releaseNotes string |
|
releaseFixedBugs string |
|
updateTempDir string |
|
landingPagePath string // Based on Host/; default landing page for download. |
|
installerFileBaseName string // File for initial install or manual reinstall. per goos [exe, dmg, sh]. |
|
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). |
|
macAppBundleName string // For update procedure. |
|
cachedNewerVersion *VersionInfo // To have info about latest version even when the internet connection drops. |
|
} |
|
|
|
// New inits Updates struct. |
|
// `appName` should be in camelCase format for file names. For installer files is converted to CamelCase. |
|
func New(appName, version, revision, buildTime, releaseNotes, releaseFixedBugs, updateTempDir string) *Updates { |
|
return &Updates{ |
|
appName: appName, |
|
version: version, |
|
revision: revision, |
|
buildTime: buildTime, |
|
releaseNotes: releaseNotes, |
|
releaseFixedBugs: releaseFixedBugs, |
|
updateTempDir: updateTempDir, |
|
landingPagePath: appName + "/download", |
|
installerFileBaseName: strings.Title(appName) + "-Installer", |
|
versionFileBaseName: "current_version", |
|
updateFileBaseName: appName + "_upgrade", |
|
macAppBundleName: "ProtonMail " + strings.Title(appName) + ".app", // For update procedure. |
|
} |
|
} |
|
|
|
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, 0644); err != nil { //nolint[gosec] |
|
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) CheckIsBridgeUpToDate() (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 := "protonmail-" + u.appName |
|
pkgRel := "1" |
|
pkgBase := strings.Join([]string{Host, DownloadPath, pkgName}, "/") |
|
|
|
versionInfo.DebFile = pkgBase + "_" + u.version + "-" + pkgRel + "_amd64.deb" |
|
versionInfo.RpmFile = pkgBase + "-" + u.version + "-" + pkgRel + ".x86_64.rpm" |
|
versionInfo.PkgFile = strings.Join([]string{Host, DownloadPath, "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 { |
|
return strings.Join([]string{Host, DownloadPath, u.installerFileBaseName + installFileSuffix[goos]}, "/") |
|
} |
|
|
|
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] |
|
cmd := exec.Command("./" + u.installerFileBaseName) // nolint[gosec] |
|
cmd.Dir = u.updateTempDir |
|
status.Err = cmd.Start() |
|
case "darwin": |
|
// 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) |
|
}
|
|
|