proton-bridge/internal/updates/updates.go

331 lines
9.9 KiB
Go

// 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]
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 {
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).
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",
installerFileBaseName: "Bridge-Installer",
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",
installerFileBaseName: "Import-Export-Installer",
versionFileBaseName: "current_version_ie",
updateFileBaseName: "ie_upgrade",
linuxFileBaseName: "protonmail-import-export",
macAppBundleName: "ProtonMail Import-Export.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"
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)
}