Launcher, app/base, sentry, update service

This commit is contained in:
James Houlahan
2020-11-23 11:56:57 +01:00
parent 6fffb460b8
commit dc3f61acee
164 changed files with 5368 additions and 4039 deletions

View File

@ -0,0 +1,22 @@
// Copyright (c) 2021 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/>.
// +build beta
package updater
const Channel = "beta"

View File

@ -0,0 +1,24 @@
// Copyright (c) 2021 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/>.
// +build !beta
package updater
// Channel is the channel of updates users are subscribed to.
// For now it is hardcoded in the build. In future, it might be selectable in settings.
const Channel = "live"

View File

@ -0,0 +1,22 @@
// 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/>.
// +build !pmapi_qa
package updater
const Host = "https://protonmail.com/download"

View File

@ -0,0 +1,22 @@
// 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/>.
// +build pmapi_qa
package updater
const Host = "https://bridgeteam.protontech.ch/bridgeteam/autoupdates/download"

View File

@ -0,0 +1,64 @@
// 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 updater
import (
"compress/gzip"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/internal/versioner"
"github.com/ProtonMail/proton-bridge/pkg/tar"
"github.com/pkg/errors"
)
type Installer struct{}
func NewInstaller(*versioner.Versioner) *Installer {
return &Installer{}
}
func (i *Installer) InstallUpdate(_ *semver.Version, r io.Reader) error {
gr, err := gzip.NewReader(r)
if err != nil {
return err
}
defer func() { _ = gr.Close() }()
tempDir, err := ioutil.TempDir("", "proton-update-source")
if err != nil {
return errors.Wrap(err, "failed to get temporary update directory")
}
if err := tar.UntarToDir(gr, tempDir); err != nil {
return errors.Wrap(err, "failed to unpack update package")
}
exePath, err := os.Executable()
if err != nil {
return errors.Wrap(err, "failed to determine current executable path")
}
oldBundle := filepath.Dir(filepath.Dir(filepath.Dir(exePath)))
newBundle := filepath.Join(tempDir, filepath.Base(oldBundle))
return syncFolders(oldBundle, newBundle)
}

View File

@ -0,0 +1,41 @@
// 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/>.
// +build !darwin
package updater
import (
"io"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/internal/versioner"
)
type Installer struct {
versioner *versioner.Versioner
}
func NewInstaller(versioner *versioner.Versioner) *Installer {
return &Installer{
versioner: versioner,
}
}
func (i *Installer) InstallUpdate(version *semver.Version, r io.Reader) error {
return i.versioner.InstallNewVersion(version, r)
}

View File

@ -0,0 +1,75 @@
// 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/>.
// +build !pmapi_qa
package updater
// DefaultPublicKey is the public key used to sign builds.
const DefaultPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
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
qdwhrw==
=mu62
-----END PGP PUBLIC KEY BLOCK-----`

View File

@ -0,0 +1,52 @@
// 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/>.
// +build pmapi_qa
package updater
// DefaultPublicKey is the public key used to sign builds.
const DefaultPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBF9Q55wBCADiwBHGCyJiO2ZSDh9ZPecFKnf+JEryzqGYu3jImEoV2X5Bx/Kl
5n3hHvao9jekEDFr1AjvSKfG9Zz/1GdionUUEdw76mkc7y09GKdXENOyCQYs7CV7
WbWDSGSmp6DVBcRzRzMKm4zuB208a6Wwd2aYqIJ9Oo0l3ypQnox0BQCbbqewYSYN
Dmj+WJkO+e2ovJQWrQgtpnj/QBX18KBjP4FiLSPHAyy7aC2t6JlTIz8UVAw2VZFn
GBUUqnn0iy3W0nJNgv1ouo0rCa+eYBpz3n+GKTFWFDTIPQfZbh15nFJJgBSuiwyM
sHjWCNJYu5PQmwNlGJJjtKw/9xgTFLC9yaNPABEBAAG0BkJyaWRnZYkBTgQTAQgA
OBYhBH3hU445a9yHH+QknbtAQ7nyijPUBQJfUOecAhsDBQsJCAcCBhUKCQgLAgQW
AgMBAh4BAheAAAoJELtAQ7nyijPUpisH/iznWGoma1PXpaQlD2241k9zSzg3Nczn
yfm2mYtXlGVvjGLr29neErWpLy0Kb2ihKTTsgMkwSwcasBap8HYTtENNl1nUzQL7
UhaASTzZ2jYw4Dypps+DYpoLm9RUWKHuUOE5Ov8QPjTBC/BswA0Lv1Z9u9t5qsdp
UgB+YVYgRC+zSHMIzWSMx0dCSPgRilkPvIa5wB77J1+ZE7y1n/uQXOYrKitWrf+w
tXcRYoPqYQ4KXIQ/PMCTwSEDDbsPD7F09AzYQPv6D20d7dyEf0/hlfpj+cvGyBG0
GdGLjwjjKNA99ra1IXjgBUIEv/XpijfKK2D0FDiOdZi3JnVr8OYBCeW5AQ0EX1Dn
nAEIAMtD5sLJ3hXE/bKRQaINx+7hzYhFOxzdGdOTlzlzEjsWYLmy2cWb2fjazIhf
37g8HlSlMaHtHkdJIn1hS9+N76GxEChH31tF6Cuyz+k6TRqroNHsIxzOIjv3+qkM
7xWPRhq8msB8ulWKBQtWpwVVC3sa/qTh9k29wuEiwQY0IxLV0a6BkE1TqK5/7A6Q
o8SMCvQW6wAxPZMhPM/FwxMYxrKUT3UUDmRYS5RvSlMGUwK2HucQVU/qwsOPkJs4
wq6RI+5NDtyGxMxUKod/GYpPaICUI/VNgIZXX6NNzS7JYEYBjtI/JOEOc0yQSh1u
jEGl1k+4OLogUiV02mpGCrHutm0AEQEAAYkBNgQYAQgAIBYhBH3hU445a9yHH+Qk
nbtAQ7nyijPUBQJfUOecAhsMAAoJELtAQ7nyijPU/wUIAKibg4GFxHFSiEjtzdlO
2cIIr3yCsFmGFYVLF3JkOtVvQk7QDZTNsx5ZqC+Mtlf3Z04btG5M/FpHQ097orfl
IH+bZVXMrYtzd4J7ujKGEJU2hY6a9j50odsiwl6CSrXdppS7RGdkhui0RCke/y9Z
wJU5oyiWmcsQfhnET7DEpI7twqEwg43VBGOnaRxKFecyYsQVASlrWMENEpoaup8B
oIS2nDvMVSSK77tmkNcLt8911VqZPtOYmxzM5rc+gm7Pn9kSZUXoGy4p5sFDu/mj
zT1w+Qev2GlSVwFdKPasefLmb3lBEbNeZAkfFl48WEzwtK3VJM60Xl8RPFk0IKLe
tXw=
=aaxG
-----END PGP PUBLIC KEY BLOCK-----`

View File

@ -0,0 +1,50 @@
// 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 updater
import (
"sync/atomic"
"github.com/pkg/errors"
)
var ErrOperationOngoing = errors.New("the operation is already ongoing")
// locker is an easy way to ensure we only perform one update at a time.
type locker struct {
ongoing atomic.Value
}
func newLocker() *locker {
l := &locker{}
l.ongoing.Store(false)
return l
}
func (l *locker) doOnce(fn func() error) error {
if l.ongoing.Load().(bool) {
return ErrOperationOngoing
}
l.ongoing.Store(true)
defer func() { l.ongoing.Store(false) }()
return fn()
}

View File

@ -0,0 +1,67 @@
// 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 updater
import (
"sync"
"testing"
"time"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
func TestLocker(t *testing.T) {
l := newLocker()
assert.NoError(t, l.doOnce(func() error {
return nil
}))
}
func TestLockerForwardsErrors(t *testing.T) {
l := newLocker()
assert.Error(t, l.doOnce(func() error {
return errors.New("something went wrong")
}))
}
func TestLockerAllowsOnlyOneOperation(t *testing.T) {
l := newLocker()
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
assert.NoError(t, l.doOnce(func() error {
time.Sleep(2 * time.Second)
wg.Done()
return nil
}))
}()
time.Sleep(time.Second)
err := l.doOnce(func() error { return nil })
if assert.Error(t, err) {
assert.Equal(t, ErrOperationOngoing, err)
}
wg.Wait()
}

254
internal/updater/sync.go Normal file
View File

@ -0,0 +1,254 @@
// Copyright (c) 2021 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 updater
import (
"crypto/sha256"
"errors"
"io"
"os"
"path/filepath"
"github.com/sirupsen/logrus"
)
func syncFolders(localPath, updatePath string) (err error) {
backupDir := filepath.Join(filepath.Dir(updatePath), "backup")
if err = createBackup(localPath, backupDir); err != nil {
return
}
if err = removeMissing(localPath, updatePath); err != nil {
restoreFromBackup(backupDir, localPath)
return
}
if err = copyRecursively(updatePath, localPath); err != nil {
restoreFromBackup(backupDir, localPath)
return
}
return nil
}
func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) {
logrus.WithField("from", folderToCleanPath).Debug("Remove missing")
// Create list of files.
existingRelPaths := map[string]bool{}
err = filepath.Walk(itemsToKeepPath, func(keepThis string, _ os.FileInfo, walkErr error) error {
if walkErr != nil {
return walkErr
}
relPath, walkErr := filepath.Rel(itemsToKeepPath, keepThis)
if walkErr != nil {
return walkErr
}
logrus.WithField("path", relPath).Trace("Keep the path")
existingRelPaths[relPath] = true
return nil
})
if err != nil {
return
}
delList := []string{}
err = filepath.Walk(folderToCleanPath, func(removeThis string, _ os.FileInfo, walkErr error) error {
if walkErr != nil {
return walkErr
}
relPath, walkErr := filepath.Rel(folderToCleanPath, removeThis)
if walkErr != nil {
return walkErr
}
logrus.Debug("check path ", relPath)
if !existingRelPaths[relPath] {
logrus.Debug("path not in list, removing ", removeThis)
delList = append(delList, removeThis)
}
return nil
})
if err != nil {
return
}
for _, removeThis := range delList {
if err = os.RemoveAll(removeThis); err != nil && !os.IsNotExist(err) {
logrus.Error("remove error ", err)
return
}
}
return nil
}
func restoreFromBackup(backupDir, localPath string) {
logrus.WithField("from", backupDir).
WithField("to", localPath).
Error("recovering")
if err := copyRecursively(backupDir, localPath); err != nil {
logrus.WithField("from", backupDir).
WithField("to", localPath).
Error("Not able to recover.")
}
}
func createBackup(srcFile, dstDir string) (err error) {
logrus.WithField("from", srcFile).WithField("to", dstDir).Debug("Create backup")
if err = mkdirAllClear(dstDir); err != nil {
return
}
return copyRecursively(srcFile, dstDir)
}
func mkdirAllClear(path string) error {
if err := os.RemoveAll(path); err != nil {
return err
}
return os.MkdirAll(path, 0750)
}
// checksum assumes the file is a regular file and that it exists.
func checksum(path string) (hash string) {
file, err := os.Open(path) //nolint[gosec]
if err != nil {
return
}
defer file.Close() //nolint[errcheck]
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
return
}
return string(hasher.Sum(nil))
}
// srcDir including app folder.
// dstDir including app folder.
func copyRecursively(srcDir, dstDir string) error { // nolint[funlen]
return filepath.Walk(srcDir, func(srcPath string, srcInfo os.FileInfo, err error) error {
if err != nil {
return err
}
srcIsLink := srcInfo.Mode()&os.ModeSymlink == os.ModeSymlink
srcIsDir := srcInfo.IsDir()
// Non regular source (e.g. named pipes, sockets, devices...).
if !srcIsLink && !srcIsDir && !srcInfo.Mode().IsRegular() {
logrus.Error("File ", srcPath, " with mode ", srcInfo.Mode())
return errors.New("irregular source file. Copy not implemented")
}
// Destination path.
srcRelPath, err := filepath.Rel(srcDir, srcPath)
if err != nil {
return err
}
dstPath := filepath.Join(dstDir, srcRelPath)
logrus.Debug("src: ", srcPath, " dst: ", dstPath)
// Destination exists.
dstInfo, err := os.Lstat(dstPath)
if err == nil {
dstIsLink := dstInfo.Mode()&os.ModeSymlink == os.ModeSymlink
dstIsDir := dstInfo.IsDir()
// Non regular destination (e.g. named pipes, sockets, devices...).
if !dstIsLink && !dstIsDir && !dstInfo.Mode().IsRegular() {
logrus.Error("File ", dstPath, " with mode ", dstInfo.Mode())
return errors.New("irregular target file. Copy not implemented")
}
if dstIsLink {
if err = os.Remove(dstPath); err != nil {
return err
}
}
if !dstIsLink && dstIsDir && !srcIsDir {
if err = os.RemoveAll(dstPath); err != nil {
return err
}
}
// NOTE: Do not return if !dstIsLink && dstIsDir && srcIsDir: the permissions might change.
if dstInfo.Mode().IsRegular() && !srcInfo.Mode().IsRegular() {
if err = os.Remove(dstPath); err != nil {
return err
}
}
} else if !os.IsNotExist(err) {
return err
}
// Create symbolic link and return.
if srcIsLink {
logrus.Debug("It is a symlink")
linkPath, err := os.Readlink(srcPath)
if err != nil {
return err
}
logrus.Debug("link to ", linkPath)
return os.Symlink(linkPath, dstPath)
}
// Create dir and return.
if srcIsDir {
logrus.Debug("It is a dir")
return os.MkdirAll(dstPath, srcInfo.Mode())
}
// Regular files only.
// If files are same return.
if os.SameFile(srcInfo, dstInfo) || checksum(srcPath) == checksum(dstPath) {
logrus.Debug("Same files, skip copy")
return nil
}
// Create/overwrite regular file.
srcReader, err := os.Open(srcPath) //nolint[gosec]
if err != nil {
return err
}
defer srcReader.Close() //nolint[errcheck]
return copyToTmpFileRename(srcReader, dstPath, srcInfo.Mode())
})
}
func copyToTmpFileRename(srcReader io.Reader, dstPath string, dstMode os.FileMode) error {
logrus.Debug("Tmp and rename ", dstPath)
tmpPath := dstPath + ".tmp"
if err := copyToFileTruncate(srcReader, tmpPath, dstMode); err != nil {
return err
}
return os.Rename(tmpPath, dstPath)
}
func copyToFileTruncate(srcReader io.Reader, dstPath string, dstMode os.FileMode) error {
logrus.Debug("Copy and truncate ", dstPath)
dstWriter, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, dstMode)
if err != nil {
return err
}
defer dstWriter.Close() //nolint[errcheck]
_, err = io.Copy(dstWriter, srcReader)
return err
}

View File

@ -0,0 +1,157 @@
// Copyright (c) 2021 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 updater
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
const (
FileType = "File"
SymlinkType = "Symlink"
DirType = "Dir"
EmptyType = "Empty"
NewType = "New"
)
func TestSyncFolder(t *testing.T) {
for _, srcType := range []string{EmptyType, FileType, SymlinkType, DirType} {
for _, dstType := range []string{EmptyType, FileType, SymlinkType, DirType} {
require.NoError(t, checkCopyWorks(srcType, dstType))
logrus.Warn("OK: from ", srcType, " to ", dstType)
}
}
}
func checkCopyWorks(srcType, dstType string) error {
dirName := "from_" + srcType + "_to_" + dstType
AppCacheDir := "/tmp"
srcDir := filepath.Join(AppCacheDir, "sync_src", dirName)
destDir := filepath.Join(AppCacheDir, "sync_dst", dirName)
// clear before
logrus.Info("remove all ", srcDir)
err := os.RemoveAll(srcDir)
if err != nil {
return err
}
logrus.Info("remove all ", destDir)
err = os.RemoveAll(destDir)
if err != nil {
return err
}
// create
err = createTestFolder(srcDir, srcType)
if err != nil {
return err
}
err = createTestFolder(destDir, dstType)
if err != nil {
return err
}
// copy
logrus.Info("Sync from ", srcDir, " to ", destDir)
err = syncFolders(destDir, srcDir)
if err != nil {
return err
}
// Check
logrus.Info("check ", srcDir, " and ", destDir)
err = checkThatFilesAreSame(srcDir, destDir)
if err != nil {
return err
}
// clear after
logrus.Info("remove all ", srcDir)
err = os.RemoveAll(srcDir)
if err != nil {
return err
}
logrus.Info("remove all ", destDir)
err = os.RemoveAll(destDir)
if err != nil {
return err
}
return err
}
func checkThatFilesAreSame(src, dst string) error {
cmd := exec.Command("diff", "-qr", src, dst) //nolint[gosec]
cmd.Stderr = logrus.StandardLogger().WriterLevel(logrus.ErrorLevel)
cmd.Stdout = logrus.StandardLogger().WriterLevel(logrus.InfoLevel)
return cmd.Run()
}
func createTestFolder(dirPath, dirType string) error {
logrus.Info("creating folder ", dirPath, " type ", dirType)
if dirType == NewType {
return nil
}
err := mkdirAllClear(dirPath)
if err != nil {
return err
}
if dirType == EmptyType {
return nil
}
path := filepath.Join(dirPath, "testpath")
switch dirType {
case FileType:
err = ioutil.WriteFile(path, []byte("This is a test"), 0640)
if err != nil {
return err
}
case SymlinkType:
err = os.Symlink("../../", path)
if err != nil {
return err
}
case DirType:
err = os.MkdirAll(path, 0750)
if err != nil {
return err
}
err = ioutil.WriteFile(filepath.Join(path, "another_file"), []byte("This is a test"), 0640)
if err != nil {
return err
}
}
return nil
}

167
internal/updater/updater.go Normal file
View File

@ -0,0 +1,167 @@
// 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 updater
import (
"encoding/json"
"io"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type clientProvider interface {
GetAnonymousClient() pmapi.Client
}
type installer interface {
InstallUpdate(*semver.Version, io.Reader) error
}
type Updater struct {
cm clientProvider
installer installer
kr *crypto.KeyRing
curVer *semver.Version
updateURLName string
platform string
rollout float64
locker *locker
}
func New(
cm clientProvider,
installer installer,
kr *crypto.KeyRing,
curVer *semver.Version,
updateURLName, platform string,
rollout float64,
) *Updater {
return &Updater{
cm: cm,
installer: installer,
kr: kr,
curVer: curVer,
updateURLName: updateURLName,
platform: platform,
rollout: rollout,
locker: newLocker(),
}
}
func (u *Updater) Watch(
period time.Duration,
handleUpdate func(VersionInfo) error,
handleError func(error),
) func() {
logrus.WithField("period", period).Info("Watching for updates")
ticker := time.NewTicker(period)
go func() {
for {
u.watch(handleUpdate, handleError)
<-ticker.C
}
}()
return ticker.Stop
}
func (u *Updater) watch(
handleUpdate func(VersionInfo) error,
handleError func(error),
) {
logrus.Info("Checking for updates")
latest, err := u.fetchVersionInfo()
if err != nil {
handleError(errors.Wrap(err, "failed to fetch version info"))
return
}
if !latest.Version.GreaterThan(u.curVer) || u.rollout > latest.Rollout {
logrus.WithError(err).Debug("No need to update")
return
}
if u.curVer.LessThan(latest.MinAuto) {
logrus.Debug("A manual update is required")
// NOTE: Need to notify user that they must update manually.
return
}
logrus.
WithField("latest", latest.Version).
WithField("current", u.curVer).
Info("An update is available")
if err := handleUpdate(latest); err != nil {
handleError(errors.Wrap(err, "failed to handle update"))
}
}
func (u *Updater) InstallUpdate(update VersionInfo) error {
return u.locker.doOnce(func() error {
logrus.WithField("package", update.Package).Info("Installing update package")
client := u.cm.GetAnonymousClient()
defer client.Logout()
r, err := client.DownloadAndVerify(update.Package, update.Package+".sig", u.kr)
if err != nil {
return errors.Wrap(err, "failed to download and verify update package")
}
if err := u.installer.InstallUpdate(update.Version, r); err != nil {
return errors.Wrap(err, "failed to install update package")
}
u.curVer = update.Version
return nil
})
}
func (u *Updater) fetchVersionInfo() (VersionInfo, error) {
client := u.cm.GetAnonymousClient()
defer client.Logout()
r, err := client.DownloadAndVerify(
u.getVersionFileURL(),
u.getVersionFileURL()+".sig",
u.kr,
)
if err != nil {
return VersionInfo{}, err
}
var versionMap VersionMap
if err := json.NewDecoder(r).Decode(&versionMap); err != nil {
return VersionInfo{}, err
}
return versionMap[Channel], nil
}

View File

@ -0,0 +1,336 @@
// 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 updater
import (
"bytes"
"encoding/json"
"errors"
"io"
"sync"
"testing"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWatch(t *testing.T) {
c := gomock.NewController(t)
defer c.Finish()
client := mocks.NewMockClient(c)
updater := newTestUpdater(client, "1.4.0")
versionMap := VersionMap{
"live": VersionInfo{
Version: semver.MustParse("1.5.0"),
MinAuto: semver.MustParse("1.4.0"),
Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz",
Rollout: 1.0,
},
}
client.EXPECT().DownloadAndVerify(
updater.getVersionFileURL(),
updater.getVersionFileURL()+".sig",
gomock.Any(),
).Return(bytes.NewReader(mustMarshal(t, versionMap)), nil)
client.EXPECT().Logout()
updateCh := make(chan VersionInfo)
defer updater.Watch(
time.Minute,
func(update VersionInfo) error {
updateCh <- update
return nil
},
func(err error) {
t.Fatal(err)
},
)()
assert.Equal(t, semver.MustParse("1.5.0"), (<-updateCh).Version)
}
func TestWatchIgnoresCurrentVersion(t *testing.T) {
c := gomock.NewController(t)
defer c.Finish()
client := mocks.NewMockClient(c)
updater := newTestUpdater(client, "1.5.0")
versionMap := VersionMap{
"live": VersionInfo{
Version: semver.MustParse("1.5.0"),
MinAuto: semver.MustParse("1.4.0"),
Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz",
Rollout: 1.0,
},
}
client.EXPECT().DownloadAndVerify(
updater.getVersionFileURL(),
updater.getVersionFileURL()+".sig",
gomock.Any(),
).Return(bytes.NewReader(mustMarshal(t, versionMap)), nil)
client.EXPECT().Logout()
updateCh := make(chan VersionInfo)
defer updater.Watch(
time.Minute,
func(update VersionInfo) error {
updateCh <- update
return nil
},
func(err error) {
t.Fatal(err)
},
)()
select {
case <-updateCh:
t.Fatal("We shouldn't update because we are already up to date")
case <-time.After(1500 * time.Millisecond):
}
}
func TestWatchIgnoresVerionsThatRequireManualUpdate(t *testing.T) {
c := gomock.NewController(t)
defer c.Finish()
client := mocks.NewMockClient(c)
updater := newTestUpdater(client, "1.4.0")
versionMap := VersionMap{
"live": VersionInfo{
Version: semver.MustParse("1.5.0"),
MinAuto: semver.MustParse("1.5.0"),
Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz",
Rollout: 1.0,
},
}
client.EXPECT().DownloadAndVerify(
updater.getVersionFileURL(),
updater.getVersionFileURL()+".sig",
gomock.Any(),
).Return(bytes.NewReader(mustMarshal(t, versionMap)), nil)
client.EXPECT().Logout()
updateCh := make(chan VersionInfo)
defer updater.Watch(
time.Minute,
func(update VersionInfo) error {
updateCh <- update
return nil
},
func(err error) {
t.Fatal(err)
},
)()
select {
case <-updateCh:
t.Fatal("We shouldn't update because this version requires a manual update")
case <-time.After(1500 * time.Millisecond):
}
}
func TestWatchBadSignature(t *testing.T) {
c := gomock.NewController(t)
defer c.Finish()
client := mocks.NewMockClient(c)
updater := newTestUpdater(client, "1.4.0")
client.EXPECT().DownloadAndVerify(
updater.getVersionFileURL(),
updater.getVersionFileURL()+".sig",
gomock.Any(),
).Return(nil, errors.New("bad signature"))
client.EXPECT().Logout()
updateCh := make(chan VersionInfo)
errorsCh := make(chan error)
defer updater.Watch(
time.Minute,
func(update VersionInfo) error {
updateCh <- update
return nil
},
func(err error) {
errorsCh <- err
},
)()
assert.Error(t, <-errorsCh)
}
func TestInstallUpdate(t *testing.T) {
c := gomock.NewController(t)
defer c.Finish()
client := mocks.NewMockClient(c)
updater := newTestUpdater(client, "1.4.0")
latestVersion := VersionInfo{
Version: semver.MustParse("1.5.0"),
MinAuto: semver.MustParse("1.4.0"),
Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz",
Rollout: 1.0,
}
client.EXPECT().DownloadAndVerify(
latestVersion.Package,
latestVersion.Package+".sig",
gomock.Any(),
).Return(bytes.NewReader([]byte("tgz_data_here")), nil)
client.EXPECT().Logout()
assert.NoError(t, updater.InstallUpdate(latestVersion))
}
func TestInstallUpdateBadSignature(t *testing.T) {
c := gomock.NewController(t)
defer c.Finish()
client := mocks.NewMockClient(c)
updater := newTestUpdater(client, "1.4.0")
latestVersion := VersionInfo{
Version: semver.MustParse("1.5.0"),
MinAuto: semver.MustParse("1.4.0"),
Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz",
Rollout: 1.0,
}
client.EXPECT().DownloadAndVerify(
latestVersion.Package,
latestVersion.Package+".sig",
gomock.Any(),
).Return(nil, errors.New("bad signature"))
client.EXPECT().Logout()
assert.Error(t, updater.InstallUpdate(latestVersion))
}
func TestInstallUpdateAlreadyOngoing(t *testing.T) {
c := gomock.NewController(t)
defer c.Finish()
client := mocks.NewMockClient(c)
updater := newTestUpdater(client, "1.4.0")
updater.installer = &fakeInstaller{delay: 2 * time.Second}
latestVersion := VersionInfo{
Version: semver.MustParse("1.5.0"),
MinAuto: semver.MustParse("1.4.0"),
Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz",
Rollout: 1.0,
}
client.EXPECT().DownloadAndVerify(
latestVersion.Package,
latestVersion.Package+".sig",
gomock.Any(),
).Return(bytes.NewReader([]byte("tgz_data_here")), nil)
client.EXPECT().Logout()
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
assert.NoError(t, updater.InstallUpdate(latestVersion))
wg.Done()
}()
// Wait for the installation to begin.
time.Sleep(time.Second)
err := updater.InstallUpdate(latestVersion)
if assert.Error(t, err) {
assert.Equal(t, ErrOperationOngoing, err)
}
wg.Wait()
}
func newTestUpdater(client *mocks.MockClient, curVer string) *Updater {
return New(
&fakeClientProvider{client: client},
&fakeInstaller{},
nil,
semver.MustParse(curVer),
"bridge", "linux",
0.5,
)
}
type fakeClientProvider struct {
client *mocks.MockClient
}
func (p *fakeClientProvider) GetAnonymousClient() pmapi.Client {
return p.client
}
type fakeInstaller struct {
bad bool
delay time.Duration
}
func (i *fakeInstaller) InstallUpdate(version *semver.Version, r io.Reader) error {
if i.bad {
return errors.New("bad install")
}
time.Sleep(i.delay)
return nil
}
func mustMarshal(t *testing.T, v interface{}) []byte {
b, err := json.Marshal(v)
require.NoError(t, err)
return b
}

View File

@ -0,0 +1,85 @@
// 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 updater
import (
"fmt"
"github.com/Masterminds/semver/v3"
)
// VersionInfo is information about one version of the app.
type VersionInfo struct {
// Version is the semantic version of the release.
Version *semver.Version
// MinAuto is the earliest version that is able to autoupdate to this version.
// Apps older than this version must run the manual installer and cannot autoupdate.
MinAuto *semver.Version
// Package is the location of the update package.
Package string
// Installers are the locations of installer files (for manual installation).
Installers []string
// Landing is the address of the app landing page on protonmail.com.
Landing string
// Rollout is the current progress of the rollout of this release.
Rollout float64
}
// VersionMap represents the structure of the version.json file.
// It looks like this:
// {
// "live": {
// "Version": "2.3.4",
// "Package": "https://protonmail.com/.../bridge_2.3.4_linux.tgz",
// "Installers": [
// "https://protonmail.com/.../something.deb",
// "https://protonmail.com/.../something.rpm",
// "https://protonmail.com/.../PKGBUILD"
// ],
// "Landing "https://protonmail.com/bridge",
// "Rollout": 0.5
// },
// "beta": {
// "Version": "2.4.0-beta",
// "Package": "https://protonmail.com/.../bridge_2.4.0-beta_linux.tgz",
// "Installers": [
// "https://protonmail.com/.../something.deb",
// "https://protonmail.com/.../something.rpm",
// "https://protonmail.com/.../PKGBUILD"
// ],
// "Landing "https://protonmail.com/bridge",
// "Rollout": 0.5
// },
// "...": {
// ...
// }
// }
type VersionMap map[string]VersionInfo
// getVersionFileURL returns the URL of the version file.
// For example:
// - https://protonmail.com/download/bridge/version_linux.json
// - https://protonmail.com/download/ie/version_linux.json
func (u *Updater) getVersionFileURL() string {
return fmt.Sprintf("%v/%v/version_%v.json", Host, u.updateURLName, u.platform)
}