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

5
.gitignore vendored
View File

@ -27,7 +27,10 @@ internal/frontend/qml/ImportExportUI/images
frontend/qml/*.qmlc
# Build files
bridge_darwin_*.tgz
/launcher-*
/bridge_*_*.tgz
/ie_*_*.tgz
/versioner
cmd/Desktop-Bridge/deploy
cmd/Import-Export/deploy
internal/frontend/qt*/moc.cpp

View File

@ -63,6 +63,11 @@ make build-ie
* for `windows`, the binary will have the file extension `.exe` (e.g `proton-bridge.exe`)
* for `darwin`, the application will be created with name of the project directory (e.g `proton-bridge.app`)
### Launchers
Launchers are only included in official distributions and provide the public
key used to verify signed app binaries, allowing the automatic update feature.
See README for more information.
### Tags
Note that repository contains both Bridge and Import-Export apps and they are
not released together. Therefore, each app has own tag prefix. Bridge tags

View File

@ -62,7 +62,6 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-878 Tests for send packet creation logic.
### Changed
* GODT-180 Updated Sentry client.
* GODT-651 Build creates proper binary names.
* GODT-878 Fix an issue where the random session key is inadvertently sent to
the Proton server. The data payload is always encrypted within TLS, but this
@ -103,6 +102,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-763 Detect Gmail labels from All Mail mbox export (using X-Gmail-Label header).
* GODT-834 Info about tags in BUILDS.md and link to Import-Export page in README.md.
* GODT-777 Support Apple Mail MBOX export format.
* GODT-731 Re-open Import-Export app from the second instance.
### Fixed
* GODT-677 Windows IE: global import settings not fit in window.
@ -159,6 +159,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
### Fixed
* GODT-770 Better handling of extraneous end-of-mail indicator.
* GODT-776 Fix crash when IMAP client connects while account is logging in.
* GODT-744 User agent not being sent to sentry.
### Changed
* Bump crypto version to v0.0.0-20200818122824-ed5d25e28db8.
@ -192,6 +193,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-682 Persistent anonymous API cookies for Import-Export.
* GODT-357 Use go-message to make a better message parser.
* GODT-720 Time measurement of progress for Import-Export.
* GODT-693 Launcher
### Changed
* GODT-511 User agent format changed.

View File

@ -7,7 +7,7 @@ TARGET_CMD?=Desktop-Bridge
TARGET_OS?=${GOOS}
## Build
.PHONY: build build-ie build-nogui build-ie-nogui check-has-go
.PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner
# Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=1.5.5-git
@ -29,10 +29,9 @@ endif
REVISION:=$(shell git rev-parse --short=10 HEAD)
BUILD_TIME:=$(shell date +%FT%T%z)
BUILD_TAGS?=pmapi_prod
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
BUILD_FLAGS_NOGUI:=-tags='${BUILD_TAGS} nogui'
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/pkg/constants.,Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/internal/constants.,Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
ifneq "${BUILD_LDFLAGS}" ""
GO_LDFLAGS+= ${BUILD_LDFLAGS}
endif
@ -71,6 +70,7 @@ else
endif
build: ${TGZ_TARGET}
build-ie:
TARGET_CMD=Import-Export $(MAKE) build
@ -80,9 +80,18 @@ build-nogui:
build-ie-nogui:
TARGET_CMD=Import-Export $(MAKE) build-nogui
build-launcher:
go build -ldflags="-X 'main.ConfigName=bridge' -X 'main.ExeName=proton-bridge'" -o launcher-bridge cmd/launcher/main.go
build-launcher-ie:
go build -ldflags="-X 'main.ConfigName=importExport' -X 'main.ExeName=Import-Export'" -o launcher-ie cmd/launcher/main.go
versioner:
go build ${BUILD_FLAGS} ${GO_LDFLAGS} -o versioner cmd/versioner/main.go
${TGZ_TARGET}: ${DEPLOY_DIR}/${TARGET_OS}
rm -f $@
cd ${DEPLOY_DIR} && tar czf ../../../$@ ${TARGET_OS}
cd ${DEPLOY_DIR}/${TARGET_OS} && tar czf ../../../../$@ .
${DEPLOY_DIR}/linux: ${EXE_TARGET}
cp -pf ./internal/frontend/share/icons/${SRC_SVG} ${DEPLOY_DIR}/linux/logo.svg
@ -192,18 +201,24 @@ test: gofiles
go test -coverprofile=/tmp/coverage.out -run=${TESTRUN} \
./internal/api/... \
./internal/bridge/... \
./internal/config/... \
./internal/constants/... \
./internal/cookies/... \
./internal/crash/... \
./internal/events/... \
./internal/frontend/autoconfig/... \
./internal/frontend/cli/... \
./internal/imap/... \
./internal/metrics/... \
./internal/importexport/... \
./internal/preferences/... \
./internal/locations/... \
./internal/logging/... \
./internal/metrics/... \
./internal/smtp/... \
./internal/store/... \
./internal/transfer/... \
./internal/updates/... \
./internal/updater/... \
./internal/users/... \
./internal/versioner/... \
./pkg/...
bench:
@ -215,7 +230,7 @@ coverage: test
go tool cover -html=/tmp/coverage.out -o=coverage.html
mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Configer,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Locator,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,ClientManager,IMAPClientProvider > internal/transfer/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser,ChangeNotifier > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go
@ -258,7 +273,7 @@ gofiles: ./internal/bridge/credits.go ./internal/bridge/release_notes.go ./inter
## Run and debug
.PHONY: run run-qt run-qt-cli run-nogui run-nogui-cli run-debug run-qml-preview run-ie-qml-preview run-ie run-ie-qt run-ie-qt-cli run-ie-nogui run-ie-nogui-cli clean-vendor clean-frontend-qt clean-frontend-qt-ie clean-frontend-qt-common clean
VERBOSITY?=debug-client
VERBOSITY?=debug
RUN_FLAGS:=-m -l=${VERBOSITY}
run: run-nogui-cli

View File

@ -37,6 +37,18 @@ check the results.
More details [on the public website](https://protonmail.com/import-export).
## Launchers
Launchers are binaries used to run the ProtonMail Bridge or Import-Export apps.
Official distributions of the ProtonMail Bridge and Import-Export apps contain
both a launcher and the app itself. The launcher is installed in a protected
area of the system (i.e. an area accessible only with admin privileges) and is
used to run the app. The launcher ensures that nobody tampered with the app's
files by verifying their signature using a hardcoded public key. App files are
placed in regular userspace and are signed by Proton's private key. This
feature enables the app to securely update itself automatically without asking
the user for a password.
## Keychain
You need to have a keychain in order to run the ProtonMail Bridge. On Mac or
Windows, Bridge uses native credential managers. On Linux, use

View File

@ -35,261 +35,50 @@ package main
*/
import (
"io/ioutil"
"math/rand"
"os"
"runtime/pprof"
"time"
"github.com/ProtonMail/proton-bridge/internal/api"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/cmd"
"github.com/ProtonMail/proton-bridge/internal/cookies"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/internal/imap"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/smtp"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/constants"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/allan-simon/go-singleinstance"
"github.com/ProtonMail/proton-bridge/internal/app/base"
"github.com/ProtonMail/proton-bridge/internal/app/bridge"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
const (
// cacheVersion is used for cache files such as lock, events, preferences, user_info, db files.
// Different number will drop old files and create new ones.
appName = "ProtonMail Bridge"
appUsage = "ProtonMail IMAP and SMTP Bridge"
configName = "bridge"
updateURLName = "bridge"
keychainName = "bridge"
cacheVersion = "c11"
appName = "bridge"
)
var (
log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals]
)
func main() {
cmd.Main(
"ProtonMail Bridge",
"ProtonMail IMAP and SMTP Bridge",
[]cli.Flag{
cli.BoolFlag{
Name: "no-window",
Usage: "Don't show window after start"},
cli.BoolFlag{
Name: "noninteractive",
Usage: "Start Bridge entirely noninteractively"},
},
run,
rand.Seed(time.Now().UnixNano())
if err := base.MigrateFiles(configName); err != nil {
logrus.WithError(err).Warn("Old config files could not be migrated")
}
os.Args = base.StripProcessSerialNumber(os.Args)
base, err := base.New(
appName,
appUsage,
configName,
updateURLName,
keychainName,
cacheVersion,
)
}
// run initializes and starts everything in a precise order.
//
// IMPORTANT: ***Read the comments before CHANGING the order ***
func run(context *cli.Context) (contextError error) { // nolint[funlen]
// We need to have config instance to setup a logs, panic handler, etc ...
cfg := config.New(appName, constants.Version, constants.Revision, cacheVersion)
// We want to know about any problem. Our PanicHandler calls sentry which is
// not dependent on anything else. If that fails, it tries to create crash
// report which will not be possible if no folder can be created. That's the
// only problem we will not be notified about in any way.
panicHandler := &cmd.PanicHandler{
AppName: "ProtonMail Bridge",
Config: cfg,
Err: &contextError,
}
defer panicHandler.HandlePanic()
// First we need config and create necessary folder; it's dependency for everything.
if err := cfg.CreateDirs(); err != nil {
log.Fatal("Cannot create necessary folders: ", err)
}
// Setup of logs should be as soon as possible to ensure we record every wanted report in the log.
logLevel := context.GlobalString("log-level")
debugClient, debugServer := config.SetupLog(cfg, logLevel)
// Doesn't make sense to continue when Bridge was invoked with wrong arguments.
// We should tell that to the user before we do anything else.
if context.Args().First() != "" {
_ = cli.ShowAppHelp(context)
return cli.NewExitError("Unknown argument", 4)
}
// It's safe to get version JSON file even when other instance is running.
// (thus we put it before check of presence of other Bridge instance).
updates := updates.NewBridge(cfg.GetUpdateDir())
if dir := context.GlobalString("version-json"); dir != "" {
cmd.GenerateVersionFiles(updates, dir)
return nil
}
// Should be called after logs are configured but before preferences are created.
migratePreferencesFromC10(cfg)
// ClearOldData before starting new bridge to do a proper setup.
//
// IMPORTANT: If you the change position of this you will need to wait
// until force-update to be applied on all currently used bridge
// versions
if err := cfg.ClearOldData(); err != nil {
log.Error("Cannot clear old data: ", err)
}
// GetTLSConfig is needed for IMAP, SMTL and local bridge API (to check second instance).
//
// This should be called after ClearOldData, in order to re-create the
// certificates if clean data will remove them (accidentally or on purpose).
tls, err := config.GetTLSConfig(cfg)
if err != nil {
log.WithError(err).Fatal("Cannot get TLS certificate")
logrus.WithError(err).Fatal("Failed to create app base")
}
pref := preferences.New(cfg)
// Now we can try to proceed with starting the bridge. First we need to ensure
// this is the only instance. If not, we will end and focus the existing one.
lock, err := singleinstance.CreateLockFile(cfg.GetLockPath())
if err != nil {
log.Warn("Bridge is already running")
if err := api.CheckOtherInstanceAndFocus(pref.GetInt(preferences.APIPortKey), tls); err != nil {
cmd.DisableRestart()
log.Error("Second instance: ", err)
}
return cli.NewExitError("Bridge is already running.", 3)
}
defer lock.Close() //nolint[errcheck]
// In case user wants to do CPU or memory profiles...
if doCPUProfile := context.GlobalBool("cpu-prof"); doCPUProfile {
cmd.StartCPUProfile()
defer pprof.StopCPUProfile()
}
if doMemoryProfile := context.GlobalBool("mem-prof"); doMemoryProfile {
defer cmd.MakeMemoryProfile()
}
// Now we initialize all Bridge parts.
log.Debug("Initializing bridge...")
eventListener := listener.New()
events.SetupEvents(eventListener)
credentialsStore, credentialsError := credentials.NewStore(appName)
if credentialsError != nil {
log.Error("Could not get credentials store: ", credentialsError)
}
cm := pmapi.NewClientManager(cfg.GetAPIConfig())
// Different build types have different roundtrippers (e.g. we want to enable
// TLS fingerprint checks in production builds). GetRoundTripper has a different
// implementation depending on whether build flag pmapi_prod is used or not.
cm.SetRoundTripper(cfg.GetRoundTripper(cm, eventListener))
// Cookies must be persisted across restarts.
jar, err := cookies.NewCookieJar(pref)
if err != nil {
logrus.WithError(err).Warn("Could not create cookie jar")
} else {
cm.SetCookieJar(jar)
}
bridgeInstance := bridge.New(cfg, pref, panicHandler, eventListener, cm, credentialsStore)
imapBackend := imap.NewIMAPBackend(panicHandler, eventListener, cfg, bridgeInstance)
smtpBackend := smtp.NewSMTPBackend(panicHandler, eventListener, pref, bridgeInstance)
go func() {
defer panicHandler.HandlePanic()
apiServer := api.NewAPIServer(pref, tls, cfg.GetTLSCertPath(), cfg.GetTLSKeyPath(), eventListener)
apiServer.ListenAndServe()
}()
go func() {
defer panicHandler.HandlePanic()
imapPort := pref.GetInt(preferences.IMAPPortKey)
imapServer := imap.NewIMAPServer(debugClient, debugServer, imapPort, tls, imapBackend, eventListener)
imapServer.ListenAndServe()
}()
go func() {
defer panicHandler.HandlePanic()
smtpPort := pref.GetInt(preferences.SMTPPortKey)
useSSL := pref.GetBool(preferences.SMTPSSLKey)
smtpServer := smtp.NewSMTPServer(debugClient || debugServer, smtpPort, useSSL, tls, smtpBackend, eventListener)
smtpServer.ListenAndServe()
}()
// Decide about frontend mode before initializing rest of bridge.
var frontendMode string
switch {
case context.GlobalBool("cli"):
frontendMode = "cli"
case context.GlobalBool("noninteractive"):
frontendMode = "noninteractive"
default:
frontendMode = "qt"
}
log.WithField("mode", frontendMode).Debug("Determined frontend mode to use")
// If we are starting bridge in noninteractive mode, simply block instead of starting a frontend.
if frontendMode == "noninteractive" {
<-(make(chan struct{}))
return nil
}
showWindowOnStart := !context.GlobalBool("no-window")
frontend := frontend.New(constants.Version, constants.BuildVersion, frontendMode, showWindowOnStart, panicHandler, cfg, pref, eventListener, updates, bridgeInstance, smtpBackend)
// Last part is to start everything.
log.Debug("Starting frontend...")
if err := frontend.Loop(credentialsError); err != nil {
log.Error("Frontend failed with error: ", err)
return cli.NewExitError("Frontend error", 2)
}
if frontend.IsAppRestarting() {
cmd.RestartApp()
}
return nil
}
// migratePreferencesFromC10 will copy preferences from c10 folder to c11.
// It will happen only when c10/prefs.json exists and c11/prefs.json not.
// No configuration changed between c10 and c11 versions.
func migratePreferencesFromC10(cfg *config.Config) {
pref10Path := config.New(appName, constants.Version, constants.Revision, "c10").GetPreferencesPath()
if _, err := os.Stat(pref10Path); os.IsNotExist(err) {
log.WithField("path", pref10Path).Trace("Old preferences does not exist, migration skipped")
// Other instance already running.
if base == nil {
return
}
pref11Path := cfg.GetPreferencesPath()
if _, err := os.Stat(pref11Path); err == nil {
log.WithField("path", pref11Path).Trace("New preferences already exists, migration skipped")
return
if err := bridge.New(base).Run(os.Args); err != nil {
logrus.WithError(err).Fatal("Bridge exited with error")
}
data, err := ioutil.ReadFile(pref10Path) //nolint[gosec]
if err != nil {
log.WithError(err).Error("Problem to load old preferences")
return
}
err = ioutil.WriteFile(pref11Path, data, 0600)
if err != nil {
log.WithError(err).Error("Problem to migrate preferences")
return
}
log.Info("Preferences migrated")
}

View File

@ -18,160 +18,50 @@
package main
import (
"runtime/pprof"
"math/rand"
"os"
"time"
"github.com/ProtonMail/proton-bridge/internal/cmd"
"github.com/ProtonMail/proton-bridge/internal/cookies"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/constants"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/allan-simon/go-singleinstance"
"github.com/ProtonMail/proton-bridge/internal/app/base"
"github.com/ProtonMail/proton-bridge/internal/app/ie"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
const (
// cacheVersion is used for cache files such as lock, or preferences.
// Different number will drop old files and create new ones.
appName = "ProtonMail Import-Export app"
appUsage = "Import and export messages to/from your ProtonMail account"
configName = "importExport"
updateURLName = "ie"
keychainName = "import-export-app"
cacheVersion = "c11"
appName = "importExport"
appNameDash = "import-export-app"
)
var (
log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals]
)
func main() {
cmd.Main(
"ProtonMail Import-Export",
"ProtonMail Import-Export app",
nil,
run,
rand.Seed(time.Now().UnixNano())
if err := base.MigrateFiles(configName); err != nil {
logrus.WithError(err).Warn("Old config files could not be migrated")
}
os.Args = base.StripProcessSerialNumber(os.Args)
base, err := base.New(
appName,
appUsage,
configName,
updateURLName,
keychainName,
cacheVersion,
)
}
// run initializes and starts everything in a precise order.
//
// IMPORTANT: ***Read the comments before CHANGING the order ***
func run(context *cli.Context) (contextError error) { // nolint[funlen]
// We need to have config instance to setup a logs, panic handler, etc ...
cfg := config.New(appName, constants.Version, constants.Revision, cacheVersion)
// We want to know about any problem. Our PanicHandler calls sentry which is
// not dependent on anything else. If that fails, it tries to create crash
// report which will not be possible if no folder can be created. That's the
// only problem we will not be notified about in any way.
panicHandler := &cmd.PanicHandler{
AppName: "ProtonMail Import-Export app",
Config: cfg,
Err: &contextError,
}
defer panicHandler.HandlePanic()
// First we need config and create necessary folder; it's dependency for everything.
if err := cfg.CreateDirs(); err != nil {
log.Fatal("Cannot create necessary folders: ", err)
}
// Setup of logs should be as soon as possible to ensure we record every wanted report in the log.
logLevel := context.GlobalString("log-level")
_, _ = config.SetupLog(cfg, logLevel)
// Doesn't make sense to continue when Import-Export was invoked with wrong arguments.
// We should tell that to the user before we do anything else.
if context.Args().First() != "" {
_ = cli.ShowAppHelp(context)
return cli.NewExitError("Unknown argument", 4)
}
// It's safe to get version JSON file even when other instance is running.
// (thus we put it before check of presence of other Import-Export instance).
updates := updates.NewImportExport(cfg.GetUpdateDir())
if dir := context.GlobalString("version-json"); dir != "" {
cmd.GenerateVersionFiles(updates, dir)
return nil
}
// Now we can try to proceed with starting the Import-Export. First we need to ensure
// this is the only instance. If not, we will end and focus the existing one.
lock, err := singleinstance.CreateLockFile(cfg.GetLockPath())
if err != nil {
log.Warn("Import-Export app is already running")
return cli.NewExitError("Import-Export app is already running.", 3)
logrus.WithError(err).Fatal("Failed to create app base")
}
defer lock.Close() //nolint[errcheck]
// In case user wants to do CPU or memory profiles...
if doCPUProfile := context.GlobalBool("cpu-prof"); doCPUProfile {
cmd.StartCPUProfile()
defer pprof.StopCPUProfile()
// Other instance already running.
if base == nil {
return
}
if doMemoryProfile := context.GlobalBool("mem-prof"); doMemoryProfile {
defer cmd.MakeMemoryProfile()
if err := ie.New(base).Run(os.Args); err != nil {
logrus.WithError(err).Fatal("IE exited with error")
}
// Now we initialize all Import-Export parts.
log.Debug("Initializing import-export...")
eventListener := listener.New()
events.SetupEvents(eventListener)
credentialsStore, credentialsError := credentials.NewStore(appNameDash)
if credentialsError != nil {
log.Error("Could not get credentials store: ", credentialsError)
}
cm := pmapi.NewClientManager(cfg.GetAPIConfig())
// Different build types have different roundtrippers (e.g. we want to enable
// TLS fingerprint checks in production builds). GetRoundTripper has a different
// implementation depending on whether build flag pmapi_prod is used or not.
cm.SetRoundTripper(cfg.GetRoundTripper(cm, eventListener))
pref := preferences.New(cfg)
// Cookies must be persisted across restarts.
jar, err := cookies.NewCookieJar(pref)
if err != nil {
logrus.WithError(err).Warn("Could not create cookie jar")
} else {
cm.SetCookieJar(jar)
}
importexportInstance := importexport.New(cfg, panicHandler, eventListener, cm, credentialsStore)
// Decide about frontend mode before initializing rest of import-export.
var frontendMode string
switch {
case context.GlobalBool("cli"):
frontendMode = "cli"
default:
frontendMode = "qt"
}
log.WithField("mode", frontendMode).Debug("Determined frontend mode to use")
frontend := frontend.NewImportExport(constants.Version, constants.BuildVersion, frontendMode, panicHandler, cfg, eventListener, updates, importexportInstance)
// Last part is to start everything.
log.Debug("Starting frontend...")
if err := frontend.Loop(credentialsError); err != nil {
log.Error("Frontend failed with error: ", err)
return cli.NewExitError("Frontend error", 2)
}
if frontend.IsAppRestarting() {
cmd.RestartApp()
}
return nil
}

177
cmd/launcher/main.go Normal file
View File

@ -0,0 +1,177 @@
// 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 main
import (
"os"
"os/exec"
"path/filepath"
"runtime"
"github.com/ProtonMail/go-appdir"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/internal/crash"
"github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/logging"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/internal/versioner"
"github.com/ProtonMail/proton-bridge/pkg/sentry"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const appName = "ProtonMail Launcher"
var (
ConfigName = "" // nolint[gochecknoglobals]
ExeName = "" // nolint[gochecknoglobals]
)
func main() { // nolint[funlen]
sentryReporter := sentry.NewReporter(appName, constants.Version)
crashHandler := crash.NewHandler(sentryReporter.Report)
defer crashHandler.HandlePanic()
locations := locations.New(
appdir.New(filepath.Join(constants.VendorName, ConfigName)),
ConfigName,
)
logsPath, err := locations.ProvideLogsPath()
if err != nil {
logrus.WithError(err).Fatal("Failed to get logs path")
}
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
if err := logging.Init(logsPath); err != nil {
logrus.WithError(err).Fatal("Failed to setup logging")
}
logging.SetLevel(os.Getenv("VERBOSITY"))
updatesPath, err := locations.ProvideUpdatesPath()
if err != nil {
logrus.WithError(err).Fatal("Failed to get updates path")
}
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
if err != nil {
logrus.WithError(err).Fatal("Failed to create new verification key")
}
kr, err := crypto.NewKeyRing(key)
if err != nil {
logrus.WithError(err).Fatal("Failed to create new verification keyring")
}
versioner := versioner.New(updatesPath)
exe, err := getPathToExecutable(ExeName, versioner, kr)
if err != nil {
if exe, err = getFallbackExecutable(ExeName, versioner); err != nil {
logrus.WithError(err).Fatal("Failed to find any launchable executable")
}
}
launcher, err := os.Executable()
if err != nil {
logrus.WithError(err).Fatal("Failed to determine path to launcher")
}
cmd := exec.Command(exe, appendLauncherPath(launcher, os.Args[1:])...) // nolint[gosec]
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// On windows, if you use Run(), a terminal stays open; we don't want that.
if runtime.GOOS == "windows" {
err = cmd.Start()
} else {
err = cmd.Run()
}
if err != nil {
logrus.WithError(err).Fatal("Failed to launch")
}
}
func appendLauncherPath(path string, args []string) []string {
res := append([]string{}, args...)
hasFlag := false
for k, v := range res {
if v != "--launcher" {
continue
}
hasFlag = true
if k+1 >= len(res) {
continue
}
res[k+1] = path
}
if !hasFlag {
res = append(res, "--launcher", path)
}
return res
}
func getPathToExecutable(name string, versioner *versioner.Versioner, kr *crypto.KeyRing) (string, error) {
versions, err := versioner.ListVersions()
if err != nil {
return "", errors.Wrap(err, "failed to list available versions")
}
for _, version := range versions {
vlog := logrus.WithField("version", version)
if err := version.VerifyFiles(kr); err != nil {
vlog.WithError(err).Error("Failed to verify files")
continue
}
exe, err := version.GetExecutable(name)
if err != nil {
vlog.WithError(err).Error("Failed to get executable")
continue
}
return exe, nil
}
return "", errors.New("no available versions")
}
func getFallbackExecutable(name string, versioner *versioner.Versioner) (string, error) {
logrus.Info("Searching for fallback executable")
launcher, err := os.Executable()
if err != nil {
return "", errors.Wrap(err, "failed to determine path to launcher")
}
return versioner.GetExecutableInDirectory(name, filepath.Dir(launcher))
}

179
cmd/versioner/main.go Normal file
View File

@ -0,0 +1,179 @@
// 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 main
import (
"encoding/json"
"fmt"
"io"
"os"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
type versionInfo struct {
updater.VersionInfo
Commit string
}
func main() {
if err := createApp().Run(os.Args); err != nil {
logrus.Fatal(err)
}
}
func createApp() *cli.App { // nolint[funlen]
app := cli.NewApp()
app.Name = "versioner"
app.Usage = "Create and update version files"
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "app",
Usage: "The app (bridge, importExport)",
Required: true,
},
&cli.StringFlag{
Name: "platform",
Usage: "The platform (windows, darwin, linux)",
Required: true,
},
}
app.Commands = []*cli.Command{{
Name: "update",
Action: update,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "channel",
Usage: "The update channel (live/beta/...)",
Required: true,
},
&cli.StringFlag{
Name: "version",
Usage: "The version of the app",
},
&cli.StringFlag{
Name: "min-auto",
Usage: "The minimum version of the app that can autoupdate to this version",
},
&cli.StringFlag{
Name: "package",
Usage: "The package file",
},
&cli.StringSliceFlag{
Name: "installer",
Usage: "An installer that can be used to manually install the app (can be specified multiple times)",
},
&cli.StringFlag{
Name: "landing",
Usage: "The landing page",
},
&cli.Float64Flag{
Name: "rollout",
Usage: "What proportion of users should receive this update",
},
&cli.StringFlag{
Name: "commit",
Usage: "What commit produced this update",
},
},
}, {
Name: "dump",
Action: dump,
}}
return app
}
func update(c *cli.Context) error {
versions := fetch(c.String("app"), c.String("platform"))
version := versions[c.String("channel")]
if c.IsSet("version") {
version.Version = semver.MustParse(c.String("version"))
}
if c.IsSet("min-auto") {
version.MinAuto = semver.MustParse(c.String("min-auto"))
}
if c.IsSet("package") {
version.Package = c.String("package")
}
if c.IsSet("installer") {
version.Installers = c.StringSlice("installer")
}
if c.IsSet("landing") {
version.Landing = c.String("landing")
}
if c.IsSet("rollout") {
version.Rollout = c.Float64("rollout")
}
if c.IsSet("commit") {
version.Commit = c.String("commit")
}
versions[c.String("channel")] = version
return write(c.App.Writer, versions)
}
func dump(c *cli.Context) error {
return write(c.App.Writer, fetch(c.String("app"), c.String("platform")))
}
func fetch(app, platform string) map[string]versionInfo {
url := fmt.Sprintf(
"%v/%v/version_%v.json",
updater.Host, app, platform,
)
res, err := resty.New().R().Get(url)
if err != nil {
return make(map[string]versionInfo)
}
var versionMap map[string]versionInfo
if err := json.Unmarshal(res.Body(), &versionMap); err != nil {
return make(map[string]versionInfo)
}
return versionMap
}
func write(w io.Writer, versions map[string]versionInfo) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(versions)
}

4
go.mod
View File

@ -63,8 +63,10 @@ require (
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/testify v1.6.1
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d // indirect
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d // indirect
github.com/twinj/uuid v1.0.0 // indirect
github.com/urfave/cli v1.22.4
github.com/urfave/cli/v2 v2.2.0
go.etcd.io/bbolt v1.3.5
golang.org/x/net v0.0.0-20200707034311-ab3426394381
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec

13
go.sum
View File

@ -25,8 +25,6 @@ github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDE
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-rfc5322 v0.4.0 h1:H6RJNNu+xdkG7A3xKU+dV9sP8/w2K4e7pz1R2FM8kd8=
github.com/ProtonMail/go-rfc5322 v0.4.0/go.mod h1:mzZWlMWnQJuYLL7JpzuPF5+FimV2lZ9f0jeq24kJjpU=
github.com/ProtonMail/go-rfc5322 v0.5.0 h1:LbKWjgfvumYZCr8BgGyTUk3ETGkFLAjQdkuSUpZ5CcE=
github.com/ProtonMail/go-rfc5322 v0.5.0/go.mod h1:mzZWlMWnQJuYLL7JpzuPF5+FimV2lZ9f0jeq24kJjpU=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
@ -272,14 +270,20 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d h1:T+d8FnaLSvM/1BdlDXhW4d5dr2F07bAbB+LpgzMxx+o=
github.com/therecipe/qt/internal/binding/files/docs v0.0.0-20191019224306-1097424d656c h1:/VhcwU7WuFEVgDHZ9V8PIYAyYqQ6KNxFUjBMOf2aFZM=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d h1:hAZyEG2swPRWjF0kqqdGERXUazYnRJdAk4a58f14z7Y=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d h1:AJRoBel/g9cDS+yE8BcN3E+TDD/xNAguG21aoR8DAIE=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4=
github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
@ -313,7 +317,6 @@ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@ -22,14 +22,12 @@
package api
import (
"crypto/tls"
"fmt"
"net/http"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/sirupsen/logrus"
@ -41,21 +39,15 @@ var (
type apiServer struct {
host string
pref *config.Preferences
tls *tls.Config
certPath string
keyPath string
settings *settings.Settings
eventListener listener.Listener
}
// NewAPIServer returns prepared API server struct.
func NewAPIServer(pref *config.Preferences, tls *tls.Config, certPath, keyPath string, eventListener listener.Listener) *apiServer { //nolint[golint]
func NewAPIServer(settings *settings.Settings, eventListener listener.Listener) *apiServer { //nolint[golint]
return &apiServer{
host: bridge.Host,
pref: pref,
tls: tls,
certPath: certPath,
keyPath: keyPath,
settings: settings,
eventListener: eventListener,
}
}
@ -69,12 +61,10 @@ func (api *apiServer) ListenAndServe() {
server := &http.Server{
Addr: addr,
Handler: mux,
TLSConfig: api.tls,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
}
log.Info("API listening at ", addr)
if err := server.ListenAndServeTLS(api.certPath, api.keyPath); err != nil {
if err := server.ListenAndServe(); err != nil {
api.eventListener.Emit(events.ErrorEvent, "API failed: "+err.Error())
log.Error("API failed: ", err)
}
@ -82,10 +72,10 @@ func (api *apiServer) ListenAndServe() {
}
func (api *apiServer) getAddress() string {
port := api.pref.GetInt(preferences.APIPortKey)
port := api.settings.GetInt(settings.APIPortKey)
newPort := ports.FindFreePortFrom(port)
if newPort != port {
api.pref.SetInt(preferences.APIPortKey, newPort)
api.settings.SetInt(settings.APIPortKey, newPort)
}
return getAPIAddress(api.host, newPort)
}

View File

@ -18,7 +18,6 @@
package api
import (
"crypto/tls"
"fmt"
"net/http"
@ -37,12 +36,9 @@ func focusHandler(ctx handlerContext) error {
// CheckOtherInstanceAndFocus is helper for new instances to check if there is
// already a running instance and get it's focus.
func CheckOtherInstanceAndFocus(port int, tls *tls.Config) error {
transport := &http.Transport{TLSClientConfig: tls}
client := &http.Client{Transport: transport}
func CheckOtherInstanceAndFocus(port int) error {
addr := getAPIAddress(bridge.Host, port)
resp, err := client.Get("https://" + addr + "/focus")
resp, err := (&http.Client{}).Get("http://" + addr + "/focus")
if err != nil {
return err
}

View File

@ -15,21 +15,21 @@
// 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 cmd
package base
import (
"os"
"strings"
)
import "strings"
// filterProcessSerialNumberFromArgs removes additional flag from MacOS. More info ProcessSerialNumber
// StripProcessSerialNumber removes additional flag from macOS.
// More info:
// http://mirror.informatimago.com/next/developer.apple.com/documentation/Carbon/Reference/Process_Manager/prmref_main/data_type_5.html#//apple_ref/doc/uid/TP30000208/C001951
func filterProcessSerialNumberFromArgs() {
tmp := os.Args[:0]
for _, arg := range os.Args {
func StripProcessSerialNumber(args []string) []string {
res := args[:0]
for _, arg := range args {
if !strings.Contains(arg, "-psn_") {
tmp = append(tmp, arg)
res = append(res, arg)
}
}
os.Args = tmp
return res
}

307
internal/app/base/base.go Normal file
View File

@ -0,0 +1,307 @@
// 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 base implements a common application base currently shared by bridge and IE.
// The base includes the following:
// - access to standard filesystem locations like config, cache, logging dirs
// - an extensible crash handler
// - versioned cache directory
// - persistent settings
// - event listener
// - credentials store
// - pmapi ClientManager
// In addition, the base initialises logging and reacts to command line arguments
// which control the log verbosity and enable cpu/memory profiling.
package base
import (
"os"
"path/filepath"
"runtime"
"runtime/pprof"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/go-appdir"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/internal/api"
"github.com/ProtonMail/proton-bridge/internal/config/cache"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/config/tls"
"github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/internal/cookies"
"github.com/ProtonMail/proton-bridge/internal/crash"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/logging"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/internal/versioner"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/sentry"
"github.com/allan-simon/go-singleinstance"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
type Base struct {
CrashHandler *crash.Handler
Locations *locations.Locations
Settings *settings.Settings
Lock *os.File
Cache *cache.Cache
Listener listener.Listener
Creds *credentials.Store
CM *pmapi.ClientManager
Updater *updater.Updater
Versioner *versioner.Versioner
TLS *tls.TLS
name string
usage string
restart bool
}
func New( // nolint[funlen]
appName,
appUsage,
configName,
updateURLName,
keychainName,
cacheVersion string,
) (*Base, error) {
sentryReporter := sentry.NewReporter(appName, constants.Version)
crashHandler := crash.NewHandler(
sentryReporter.Report,
crash.ShowErrorNotification(appName),
)
defer crashHandler.HandlePanic()
locations := locations.New(
appdir.New(filepath.Join(constants.VendorName, configName)),
configName,
)
if err := locations.Clean(); err != nil {
return nil, err
}
logsPath, err := locations.ProvideLogsPath()
if err != nil {
return nil, err
}
if err := logging.Init(logsPath); err != nil {
return nil, err
}
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
settingsPath, err := locations.ProvideSettingsPath()
if err != nil {
return nil, err
}
settingsObj := settings.New(settingsPath)
lock, err := singleinstance.CreateLockFile(locations.GetLockFile())
if err != nil {
logrus.Warnf("%v is already running", appName)
return nil, api.CheckOtherInstanceAndFocus(settingsObj.GetInt(settings.APIPortKey))
}
cachePath, err := locations.ProvideCachePath()
if err != nil {
return nil, err
}
cache, err := cache.New(cachePath, cacheVersion)
if err != nil {
return nil, err
}
if err := cache.RemoveOldVersions(); err != nil {
return nil, err
}
listener := listener.New()
events.SetupEvents(listener)
// NOTE: If we can't load the credentials for whatever reason,
// do we really want to error out? Need to signal to frontend.
creds, err := credentials.NewStore(keychainName)
if err != nil {
logrus.WithError(err).Error("Could not get credentials store")
listener.Emit(events.CredentialsErrorEvent, err.Error())
}
jar, err := cookies.NewCookieJar(settingsObj)
if err != nil {
return nil, err
}
cm := pmapi.NewClientManager(pmapi.GetAPIConfig(configName, constants.Version))
cm.SetRoundTripper(pmapi.GetRoundTripper(cm, listener))
cm.SetCookieJar(jar)
sentryReporter.SetUserAgentProvider(cm)
tls := tls.New(settingsPath)
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
if err != nil {
return nil, err
}
kr, err := crypto.NewKeyRing(key)
if err != nil {
return nil, err
}
updatesDir, err := locations.ProvideUpdatesPath()
if err != nil {
return nil, err
}
versioner := versioner.New(updatesDir)
installer := updater.NewInstaller(versioner)
updater := updater.New(
cm,
installer,
kr,
semver.MustParse(constants.Version),
updateURLName,
runtime.GOOS,
settingsObj.GetFloat64(settings.RolloutKey),
)
return &Base{
CrashHandler: crashHandler,
Locations: locations,
Settings: settingsObj,
Lock: lock,
Cache: cache,
Listener: listener,
Creds: creds,
CM: cm,
Updater: updater,
Versioner: versioner,
TLS: tls,
name: appName,
usage: appUsage,
}, nil
}
func (b *Base) NewApp(action func(*Base, *cli.Context) error) *cli.App {
app := cli.NewApp()
app.Name = b.name
app.Usage = b.usage
app.Version = constants.Version
app.Action = b.run(action)
app.Flags = []cli.Flag{
&cli.BoolFlag{
Name: "cpu-prof",
Aliases: []string{"p"},
Usage: "Generate CPU profile",
},
&cli.BoolFlag{
Name: "mem-prof",
Aliases: []string{"m"},
Usage: "Generate memory profile",
},
&cli.StringFlag{
Name: "log-level",
Aliases: []string{"l"},
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
},
&cli.BoolFlag{
Name: "cli",
Aliases: []string{"c"},
Usage: "Use command line interface",
},
&cli.StringFlag{
Name: "restart",
Usage: "The number of times the application has already restarted",
Hidden: true,
},
&cli.StringFlag{
Name: "launcher",
Usage: "The launcher to use to restart the application",
Hidden: true,
},
}
return app
}
// SetToRestart sets the app to restart the next time it is closed.
func (b *Base) SetToRestart() {
b.restart = true
}
func (b *Base) run(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc { // nolint[funlen]
return func(c *cli.Context) error {
defer b.CrashHandler.HandlePanic()
defer func() { _ = b.Lock.Close() }()
if doCPUProfile := c.Bool("cpu-prof"); doCPUProfile {
startCPUProfile()
defer pprof.StopCPUProfile()
}
if doMemoryProfile := c.Bool("mem-prof"); doMemoryProfile {
defer makeMemoryProfile()
}
logging.SetLevel(c.String("log-level"))
logrus.
WithField("appName", b.name).
WithField("version", constants.Version).
WithField("revision", constants.Revision).
WithField("build", constants.BuildTime).
WithField("runtime", runtime.GOOS).
WithField("args", os.Args).
Info("Run app")
b.CrashHandler.AddRecoveryAction(func(interface{}) error {
if c.Int("restart") > maxAllowedRestarts {
logrus.
WithField("restart", c.Int("restart")).
Warn("Not restarting, already restarted too many times")
return nil
}
return restartApp(c.String("launcher"), true)
})
if err := appMainLoop(b, c); err != nil {
return err
}
if b.restart {
return restartApp(c.String("launcher"), false)
}
if err := b.Versioner.RemoveOldVersions(); err != nil {
return err
}
return nil
}
}

View File

@ -0,0 +1,81 @@
// 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 base
import (
"os"
"path/filepath"
"github.com/ProtonMail/go-appdir"
"github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/sirupsen/logrus"
)
// MigrateFiles migrates files from their old (pre-refactor) locations to their new locations.
// We can remove this eventually.
//
// | entity | old location | new location |
// |--------|-------------------------------------------|----------------------------------------|
// | prefs | ~/.cache/protonmail/<app>/c11/prefs.json | ~/.config/protonmail/<app>/prefs.json |
// | c11 | ~/.cache/protonmail/<app>/c11 | ~/.cache/protonmail/<app>/cache/c11 |
func MigrateFiles(configName string) error {
appDirs := appdir.New(filepath.Join(constants.VendorName, configName))
locations := locations.New(appDirs, configName)
userCacheDir := appDirs.UserCache()
newSettingsDir, err := locations.ProvideSettingsPath()
if err != nil {
return err
}
if err := moveIfExists(
filepath.Join(userCacheDir, "c11", "prefs.json"),
filepath.Join(newSettingsDir, "prefs.json"),
); err != nil {
return err
}
newCacheDir, err := locations.ProvideCachePath()
if err != nil {
return err
}
if err := moveIfExists(
filepath.Join(userCacheDir, "c11"),
filepath.Join(newCacheDir, "c11"),
); err != nil {
return err
}
return nil
}
func moveIfExists(source, destination string) error {
if _, err := os.Stat(source); os.IsNotExist(err) {
logrus.WithField("source", source).WithField("destination", destination).Debug("No need to migrate file")
return nil
}
if _, err := os.Stat(destination); !os.IsNotExist(err) {
logrus.WithField("source", source).WithField("destination", destination).Debug("No need to migrate file")
return nil
}
return os.Rename(source, destination)
}

View File

@ -15,40 +15,42 @@
// 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 cmd
package base
import (
"os"
"path/filepath"
"runtime"
"runtime/pprof"
"github.com/sirupsen/logrus"
)
// StartCPUProfile starts CPU pprof.
func StartCPUProfile() {
// startCPUProfile starts CPU pprof.
func startCPUProfile() {
f, err := os.Create("./cpu.pprof")
if err != nil {
log.Fatal("Could not create CPU profile: ", err)
logrus.Fatal("Could not create CPU profile: ", err)
}
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal("Could not start CPU profile: ", err)
logrus.Fatal("Could not start CPU profile: ", err)
}
}
// MakeMemoryProfile generates memory pprof.
func MakeMemoryProfile() {
// makeMemoryProfile generates memory pprof.
func makeMemoryProfile() {
name := "./mem.pprof"
f, err := os.Create(name)
if err != nil {
log.Fatal("Could not create memory profile: ", err)
logrus.Fatal("Could not create memory profile: ", err)
}
if abs, err := filepath.Abs(name); err == nil {
name = abs
}
log.Info("Writing memory profile to ", name)
logrus.Info("Writing memory profile to ", name)
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
log.Fatal("Could not write memory profile: ", err)
logrus.Fatal("Could not write memory profile: ", err)
}
_ = f.Close()
}

View File

@ -0,0 +1,88 @@
// 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 base
import (
"os"
"os/exec"
"strconv"
"github.com/sirupsen/logrus"
)
// maxAllowedRestarts controls after how many crashes the app will give up restarting.
const maxAllowedRestarts = 10
func restartApp(path string, crash bool) error {
if path == "" {
exe, err := os.Executable()
if err != nil {
return err
}
path = exe
}
var args []string
if crash {
args = incrementRestartFlag(os.Args)[1:]
} else {
args = os.Args[1:]
}
logrus.
WithField("path", path).
WithField("args", args).
Warn("Restarting")
return exec.Command(path, args...).Start() // nolint[gosec]
}
// incrementRestartFlag increments the value of the restart flag.
// If no such flag is present, it is added with initial value 1.
func incrementRestartFlag(args []string) []string {
res := append([]string{}, args...)
hasFlag := false
for k, v := range res {
if v != "--restart" {
continue
}
hasFlag = true
if k+1 >= len(res) {
continue
}
n, err := strconv.Atoi(res[k+1])
if err != nil {
res[k+1] = "1"
} else {
res[k+1] = strconv.Itoa(n + 1)
}
}
if !hasFlag {
res = append(res, "--restart", "1")
}
return res
}

View File

@ -0,0 +1,49 @@
// 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 base
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIncrementRestartFlag(t *testing.T) {
var tests = []struct {
in []string
out []string
}{
{[]string{"./bridge", "--restart", "1"}, []string{"./bridge", "--restart", "2"}},
{[]string{"./bridge", "--restart", "2"}, []string{"./bridge", "--restart", "3"}},
{[]string{"./bridge", "--other", "--restart", "2"}, []string{"./bridge", "--other", "--restart", "3"}},
{[]string{"./bridge", "--restart", "2", "--other"}, []string{"./bridge", "--restart", "3", "--other"}},
{[]string{"./bridge", "--restart", "2", "--other", "2"}, []string{"./bridge", "--restart", "3", "--other", "2"}},
{[]string{"./bridge"}, []string{"./bridge", "--restart", "1"}},
{[]string{"./bridge", "--something"}, []string{"./bridge", "--something", "--restart", "1"}},
{[]string{"./bridge", "--something", "--else"}, []string{"./bridge", "--something", "--else", "--restart", "1"}},
{[]string{"./bridge", "--restart", "bad"}, []string{"./bridge", "--restart", "1"}},
{[]string{"./bridge", "--restart", "bad", "--other"}, []string{"./bridge", "--restart", "1", "--other"}},
}
for _, tt := range tests {
t.Run(strings.Join(tt.in, " "), func(t *testing.T) {
assert.Equal(t, tt.out, incrementRestartFlag(tt.in))
})
}
}

View File

@ -0,0 +1,137 @@
// 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 bridge implements the bridge CLI application.
package bridge
import (
"time"
"github.com/ProtonMail/proton-bridge/internal/api"
"github.com/ProtonMail/proton-bridge/internal/app/base"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/internal/imap"
"github.com/ProtonMail/proton-bridge/internal/smtp"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
func New(base *base.Base) *cli.App {
app := base.NewApp(run)
app.Flags = append(app.Flags, []cli.Flag{
&cli.StringFlag{
Name: "log-imap",
Usage: "Enable logging of IMAP communications (all|client|server) (may contain decrypted data!)"},
&cli.BoolFlag{
Name: "log-smtp",
Usage: "Enable logging of SMTP communications (may contain decrypted data!)"},
&cli.BoolFlag{
Name: "no-window",
Usage: "Don't show window after start"},
&cli.BoolFlag{
Name: "noninteractive",
Usage: "Start Bridge entirely noninteractively"},
}...)
return app
}
func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
tls, err := b.TLS.GetConfig()
if err != nil {
logrus.WithError(err).Fatal("Failed to create TLS config")
}
bridge := bridge.New(b.Locations, b.Cache, b.Settings, b.CrashHandler, b.Listener, b.CM, b.Creds)
imapBackend := imap.NewIMAPBackend(b.CrashHandler, b.Listener, b.Cache, bridge)
smtpBackend := smtp.NewSMTPBackend(b.CrashHandler, b.Listener, b.Settings, bridge)
go func() {
defer b.CrashHandler.HandlePanic()
api.NewAPIServer(b.Settings, b.Listener).ListenAndServe()
}()
go func() {
defer b.CrashHandler.HandlePanic()
imapPort := b.Settings.GetInt(settings.IMAPPortKey)
imap.NewIMAPServer(
c.String("log-imap") == "client" || c.String("log-imap") == "all",
c.String("log-imap") == "server" || c.String("log-imap") == "all",
imapPort, tls, imapBackend, b.Listener).ListenAndServe()
}()
go func() {
defer b.CrashHandler.HandlePanic()
smtpPort := b.Settings.GetInt(settings.SMTPPortKey)
useSSL := b.Settings.GetBool(settings.SMTPSSLKey)
smtp.NewSMTPServer(
c.Bool("log-smtp"),
smtpPort, useSSL, tls, smtpBackend, b.Listener).ListenAndServe()
}()
var frontendMode string
switch {
case c.Bool("cli"):
frontendMode = "cli"
case c.Bool("noninteractive"):
frontendMode = "noninteractive"
default:
frontendMode = "qt"
}
if frontendMode == "noninteractive" {
<-(make(chan struct{}))
return nil
}
f := frontend.New(
constants.Version,
constants.BuildVersion,
frontendMode,
!c.Bool("no-window"),
b.CrashHandler,
b.Locations,
b.Settings,
b.Listener,
b.Updater,
bridge,
smtpBackend,
b,
)
b.Updater.Watch(
time.Hour,
func(update updater.VersionInfo) error {
if !b.Settings.GetBool(settings.AutoUpdateKey) {
return f.NotifyManualUpdate(update)
}
return b.Updater.InstallUpdate(update)
},
func(err error) {
logrus.WithError(err).Error("An error occurred while watching for updates")
},
)
return f.Loop()
}

83
internal/app/ie/ie.go Normal file
View File

@ -0,0 +1,83 @@
// 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 ie implements the ie CLI application.
package ie
import (
"time"
"github.com/ProtonMail/proton-bridge/internal/api"
"github.com/ProtonMail/proton-bridge/internal/app/base"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
func New(b *base.Base) *cli.App {
return b.NewApp(run)
}
func run(b *base.Base, c *cli.Context) error {
ie := importexport.New(b.Locations, b.Cache, b.CrashHandler, b.Listener, b.CM, b.Creds)
go func() {
defer b.CrashHandler.HandlePanic()
api.NewAPIServer(b.Settings, b.Listener).ListenAndServe()
}()
var frontendMode string
switch {
case c.Bool("cli"):
frontendMode = "cli"
default:
frontendMode = "qt"
}
f := frontend.NewImportExport(
constants.Version,
constants.BuildVersion,
frontendMode,
b.CrashHandler,
b.Locations,
b.Listener,
b.Updater,
ie,
b,
)
b.Updater.Watch(
time.Hour,
func(update updater.VersionInfo) error {
if !b.Settings.GetBool(settings.AutoUpdateKey) {
return f.NotifyManualUpdate(update)
}
return b.Updater.InstallUpdate(update)
},
func(err error) {
logrus.WithError(err).Error("An error occurred while watching for updates")
},
)
return f.Loop()
}

View File

@ -22,8 +22,9 @@ import (
"strconv"
"time"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -38,7 +39,7 @@ var (
type Bridge struct {
*users.Users
pref PreferenceProvider
settings SettingsProvider
clientManager users.ClientManager
userAgentClientName string
@ -47,8 +48,9 @@ type Bridge struct {
}
func New(
config Configer,
pref PreferenceProvider,
locations Locator,
cache Cacher,
s SettingsProvider,
panicHandler users.PanicHandler,
eventListener listener.Listener,
clientManager users.ClientManager,
@ -56,22 +58,22 @@ func New(
) *Bridge {
// Allow DoH before starting the app if the user has previously set this setting.
// This allows us to start even if protonmail is blocked.
if pref.GetBool(preferences.AllowProxyKey) {
if s.GetBool(settings.AllowProxyKey) {
clientManager.AllowProxy()
}
storeFactory := newStoreFactory(config, panicHandler, clientManager, eventListener)
u := users.New(config, panicHandler, eventListener, clientManager, credStorer, storeFactory, true)
storeFactory := newStoreFactory(cache, panicHandler, clientManager, eventListener)
u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, storeFactory, true)
b := &Bridge{
Users: u,
pref: pref,
settings: s,
clientManager: clientManager,
}
if pref.GetBool(preferences.FirstStartKey) {
b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(config.GetVersion())))
pref.SetBool(preferences.FirstStartKey, false)
if s.GetBool(settings.FirstStartKey) {
b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(constants.Version)))
s.SetBool(settings.FirstStartKey, false)
}
go b.heartbeat()
@ -84,7 +86,7 @@ func (b *Bridge) heartbeat() {
ticker := time.NewTicker(1 * time.Minute)
for range ticker.C {
next, err := strconv.ParseInt(b.pref.Get(preferences.NextHeartbeatKey), 10, 64)
next, err := strconv.ParseInt(b.settings.Get(settings.NextHeartbeatKey), 10, 64)
if err != nil {
continue
}
@ -92,7 +94,7 @@ func (b *Bridge) heartbeat() {
if time.Now().After(nextTime) {
b.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel))
nextTime = nextTime.Add(24 * time.Hour)
b.pref.Set(preferences.NextHeartbeatKey, strconv.FormatInt(nextTime.Unix(), 10))
b.settings.Set(settings.NextHeartbeatKey, strconv.FormatInt(nextTime.Unix(), 10))
}
}
}

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./credits.sh at Mon Dec 28 02:39:43 PM CET 2020. DO NOT EDIT.
// Code generated by ./credits.sh at Mon Jan 4 03:19:07 PM CET 2021. DO NOT EDIT.
package bridge
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/Masterminds/semver/v3;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/PuerkitoBio/goquery;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/Masterminds/semver/v3;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/PuerkitoBio/goquery;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli/v2;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

@ -28,7 +28,7 @@ import (
)
type storeFactory struct {
config StoreFactoryConfiger
cache Cacher
panicHandler users.PanicHandler
clientManager users.ClientManager
eventListener listener.Listener
@ -36,29 +36,29 @@ type storeFactory struct {
}
func newStoreFactory(
config StoreFactoryConfiger,
cache Cacher,
panicHandler users.PanicHandler,
clientManager users.ClientManager,
eventListener listener.Listener,
) *storeFactory {
return &storeFactory{
config: config,
cache: cache,
panicHandler: panicHandler,
clientManager: clientManager,
eventListener: eventListener,
storeCache: store.NewCache(config.GetIMAPCachePath()),
storeCache: store.NewCache(cache.GetIMAPCachePath()),
}
}
// New creates new store for given user.
func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) {
storePath := getUserStorePath(f.config.GetDBDir(), user.ID())
storePath := getUserStorePath(f.cache.GetDBDir(), user.ID())
return store.New(f.panicHandler, user, f.clientManager, f.eventListener, storePath, f.storeCache)
}
// Remove removes all store files for given user.
func (f *storeFactory) Remove(userID string) error {
storePath := getUserStorePath(f.config.GetDBDir(), userID)
storePath := getUserStorePath(f.cache.GetDBDir(), userID)
return store.RemoveStore(f.storeCache, storePath, userID)
}

View File

@ -17,22 +17,18 @@
package bridge
import "github.com/ProtonMail/proton-bridge/internal/users"
type Configer interface {
users.Configer
StoreFactoryConfiger
type Locator interface {
Clear() error
}
type StoreFactoryConfiger interface {
GetDBDir() string
type Cacher interface {
GetIMAPCachePath() string
GetDBDir() string
}
type PreferenceProvider interface {
type SettingsProvider interface {
Get(key string) string
Set(key string, value string)
GetBool(key string) bool
SetBool(key string, val bool)
GetInt(key string) int
Set(key string, value string)
}

View File

@ -1,96 +0,0 @@
// 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 cmd
import (
"os"
"runtime"
"github.com/ProtonMail/proton-bridge/pkg/constants"
pkgSentry "github.com/ProtonMail/proton-bridge/pkg/sentry"
"github.com/getsentry/sentry-go"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var (
log = logrus.WithField("pkg", "cmd") //nolint[gochecknoglobals]
baseFlags = []cli.Flag{ //nolint[gochecknoglobals]
cli.StringFlag{
Name: "log-level, l",
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug, debug-client, debug-server)"},
cli.BoolFlag{
Name: "cli, c",
Usage: "Use command line interface"},
cli.StringFlag{
Name: "version-json, g",
Usage: "Generate json version file"},
cli.BoolFlag{
Name: "mem-prof, m",
Usage: "Generate memory profile"},
cli.BoolFlag{
Name: "cpu-prof, p",
Usage: "Generate CPU profile"},
}
)
// Main sets up Sentry, filters out unwanted args, creates app and runs it.
func Main(appName, usage string, extraFlags []cli.Flag, run func(*cli.Context) error) {
err := sentry.Init(sentry.ClientOptions{
Dsn: constants.DSNSentry,
Release: constants.Revision,
BeforeSend: pkgSentry.EnhanceSentryEvent,
})
sentry.ConfigureScope(func(scope *sentry.Scope) {
scope.SetFingerprint([]string{"{{ default }}"})
})
if err != nil {
log.WithError(err).Errorln("Can not setup sentry DSN")
}
filterProcessSerialNumberFromArgs()
filterRestartNumberFromArgs()
app := newApp(appName, usage, extraFlags, run)
logrus.SetLevel(logrus.InfoLevel)
log.WithField("version", constants.Version).
WithField("revision", constants.Revision).
WithField("build", constants.BuildTime).
WithField("runtime", runtime.GOOS).
WithField("args", os.Args).
WithField("appName", app.Name).
Info("Run app")
if err := app.Run(os.Args); err != nil {
log.Error("Program exited with error: ", err)
}
}
func newApp(appName, usage string, extraFlags []cli.Flag, run func(*cli.Context) error) *cli.App {
app := cli.NewApp()
app.Name = appName
app.Usage = usage
app.Version = constants.BuildVersion
app.Flags = append(baseFlags, extraFlags...) //nolint[gocritic]
app.Action = run
return app
}

View File

@ -1,111 +0,0 @@
// 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 cmd
import (
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/sentry"
"github.com/urfave/cli"
)
const (
// After how many crashes app gives up starting.
maxAllowedCrashes = 10
)
var (
// How many crashes happened so far in a row.
// It will be filled from args by `filterRestartNumberFromArgs`.
// Every call of `HandlePanic` will increase this number.
// Then it will be passed as argument to the next try by `RestartApp`.
numberOfCrashes = 0 //nolint[gochecknoglobals]
)
// filterRestartNumberFromArgs removes flag with a number how many restart we already did.
// See restartApp how that number is used.
func filterRestartNumberFromArgs() {
tmp := os.Args[:0]
for i, arg := range os.Args {
if !strings.HasPrefix(arg, "--restart_") {
tmp = append(tmp, arg)
continue
}
var err error
numberOfCrashes, err = strconv.Atoi(os.Args[i][10:])
if err != nil {
numberOfCrashes = maxAllowedCrashes
}
}
os.Args = tmp
}
// DisableRestart disables restart once `RestartApp` is called.
func DisableRestart() {
numberOfCrashes = maxAllowedCrashes
}
// RestartApp starts a new instance in background.
func RestartApp() {
if numberOfCrashes >= maxAllowedCrashes {
log.Error("Too many crashes")
return
}
if exeFile, err := os.Executable(); err == nil {
arguments := append(os.Args[1:], fmt.Sprintf("--restart_%d", numberOfCrashes))
cmd := exec.Command(exeFile, arguments...) //nolint[gosec]
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
if err := cmd.Start(); err != nil {
log.Error("Restart failed: ", err)
}
}
}
// PanicHandler defines HandlePanic which can be used anywhere in defer.
type PanicHandler struct {
AppName string
Config *config.Config
Err *error // Pointer to error of cli action.
}
// HandlePanic should be called in defer to ensure restart of app after error.
func (ph *PanicHandler) HandlePanic() {
sentry.SkipDuringUnwind()
r := recover()
if r == nil {
return
}
config.HandlePanic(ph.Config, fmt.Sprintf("Recover: %v", r))
frontend.HandlePanic(ph.AppName)
*ph.Err = cli.NewExitError("Panic and restart", 255)
numberOfCrashes++
log.Error("Restarting after panic")
RestartApp()
os.Exit(255)
}

65
internal/config/cache/cache.go vendored Normal file
View File

@ -0,0 +1,65 @@
// 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 cache provides access to contents inside a cache directory.
package cache
import (
"os"
"path/filepath"
"github.com/ProtonMail/proton-bridge/pkg/files"
)
type Cache struct {
dir, version string
}
func New(dir, version string) (*Cache, error) {
if err := os.MkdirAll(filepath.Join(dir, version), 0700); err != nil {
return nil, err
}
return &Cache{
dir: dir,
version: version,
}, nil
}
// GetDBDir returns folder for db files.
func (c *Cache) GetDBDir() string {
return c.getCurrentCacheDir()
}
// GetIMAPCachePath returns path to file with IMAP status.
func (c *Cache) GetIMAPCachePath() string {
return filepath.Join(c.getCurrentCacheDir(), "user_info.json")
}
// GetTransferDir returns folder for import-export rules files.
func (c *Cache) GetTransferDir() string {
return c.getCurrentCacheDir()
}
// RemoveOldVersions removes any cache dirs that are not the current version.
func (c *Cache) RemoveOldVersions() error {
return files.Remove(c.dir).Except(c.getCurrentCacheDir()).Do()
}
func (c *Cache) getCurrentCacheDir() string {
return filepath.Join(c.dir, c.version)
}

70
internal/config/cache/cache_test.go vendored Normal file
View File

@ -0,0 +1,70 @@
// 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 cache
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRemoveOldVersions(t *testing.T) {
dir, err := ioutil.TempDir("", "test-cache")
require.NoError(t, err)
cache, err := New(dir, "c4")
require.NoError(t, err)
createFilesInDir(t, dir,
"unexpected1.txt",
"c1/unexpected1.txt",
"c2/unexpected2.txt",
"c3/unexpected3.txt",
"something.txt",
)
require.DirExists(t, filepath.Join(dir, "c4"))
require.FileExists(t, filepath.Join(dir, "unexpected1.txt"))
require.FileExists(t, filepath.Join(dir, "c1", "unexpected1.txt"))
require.FileExists(t, filepath.Join(dir, "c2", "unexpected2.txt"))
require.FileExists(t, filepath.Join(dir, "c3", "unexpected3.txt"))
require.FileExists(t, filepath.Join(dir, "something.txt"))
assert.NoError(t, cache.RemoveOldVersions())
assert.DirExists(t, filepath.Join(dir, "c4"))
assert.NoFileExists(t, filepath.Join(dir, "unexpected1.txt"))
assert.NoFileExists(t, filepath.Join(dir, "c1", "unexpected1.txt"))
assert.NoFileExists(t, filepath.Join(dir, "c2", "unexpected2.txt"))
assert.NoFileExists(t, filepath.Join(dir, "c3", "unexpected3.txt"))
assert.NoFileExists(t, filepath.Join(dir, "something.txt"))
}
func createFilesInDir(t *testing.T, dir string, files ...string) {
for _, target := range files {
require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(dir, target)), 0700))
f, err := os.Create(filepath.Join(dir, target))
require.NoError(t, err)
require.NoError(t, f.Close())
}
}

View File

@ -15,7 +15,7 @@
// 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 config
package settings
import (
"encoding/json"
@ -23,27 +23,29 @@ import (
"os"
"strconv"
"sync"
"github.com/sirupsen/logrus"
)
type Preferences struct {
type keyValueStore struct {
cache map[string]string
path string
lock *sync.RWMutex
}
// NewPreferences returns loaded preferences.
func NewPreferences(preferencesPath string) *Preferences {
p := &Preferences{
path: preferencesPath,
// newKeyValueStore returns loaded preferences.
func newKeyValueStore(path string) *keyValueStore {
p := &keyValueStore{
path: path,
lock: &sync.RWMutex{},
}
if err := p.load(); err != nil {
log.Warn("Cannot load preferences: ", err)
logrus.WithError(err).Warn("Cannot load preferences file, creating new one")
}
return p
}
func (p *Preferences) load() error {
func (p *keyValueStore) load() error {
if p.cache != nil {
return nil
}
@ -62,7 +64,7 @@ func (p *Preferences) load() error {
return json.NewDecoder(f).Decode(&p.cache)
}
func (p *Preferences) save() error {
func (p *keyValueStore) save() error {
if p.cache == nil {
return errors.New("cannot save preferences: cache is nil")
}
@ -79,42 +81,50 @@ func (p *Preferences) save() error {
return json.NewEncoder(f).Encode(p.cache)
}
func (p *Preferences) SetDefault(key, value string) {
func (p *keyValueStore) setDefault(key, value string) {
if p.Get(key) == "" {
p.Set(key, value)
}
}
func (p *Preferences) Get(key string) string {
func (p *keyValueStore) Get(key string) string {
p.lock.RLock()
defer p.lock.RUnlock()
return p.cache[key]
}
func (p *Preferences) GetBool(key string) bool {
func (p *keyValueStore) GetBool(key string) bool {
return p.Get(key) == "true"
}
func (p *Preferences) GetInt(key string) int {
func (p *keyValueStore) GetInt(key string) int {
value, err := strconv.Atoi(p.Get(key))
if err != nil {
log.Error("Cannot parse int: ", err)
logrus.WithError(err).Error("Cannot parse int")
}
return value
}
func (p *Preferences) Set(key, value string) {
func (p *keyValueStore) GetFloat64(key string) float64 {
value, err := strconv.ParseFloat(p.Get(key), 64)
if err != nil {
logrus.WithError(err).Error("Cannot parse float64")
}
return value
}
func (p *keyValueStore) Set(key, value string) {
p.lock.Lock()
p.cache[key] = value
p.lock.Unlock()
if err := p.save(); err != nil {
log.Warn("Cannot save preferences: ", err)
logrus.WithError(err).Warn("Cannot save preferences")
}
}
func (p *Preferences) SetBool(key string, value bool) {
func (p *keyValueStore) SetBool(key string, value bool) {
if value {
p.Set(key, "true")
} else {
@ -122,6 +132,6 @@ func (p *Preferences) SetBool(key string, value bool) {
}
}
func (p *Preferences) SetInt(key string, value int) {
func (p *keyValueStore) SetInt(key string, value int) {
p.Set(key, strconv.Itoa(value))
}

View File

@ -15,7 +15,7 @@
// 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 config
package settings
import (
"io/ioutil"
@ -27,82 +27,78 @@ import (
const testPrefFilePath = "/tmp/pref.json"
func shutdownTestPreferences() {
_ = os.RemoveAll(testPrefFilePath)
}
func TestLoadNoPreferences(t *testing.T) {
pref := newTestEmptyPreferences(t)
func TestLoadNoKeyValueStore(t *testing.T) {
pref := newTestEmptyKeyValueStore(t)
require.Equal(t, "", pref.Get("key"))
}
func TestLoadBadPreferences(t *testing.T) {
func TestLoadBadKeyValueStore(t *testing.T) {
require.NoError(t, ioutil.WriteFile(testPrefFilePath, []byte("{\"key\":\"value"), 0700))
pref := NewPreferences(testPrefFilePath)
pref := newKeyValueStore(testPrefFilePath)
require.Equal(t, "", pref.Get("key"))
}
func TestPreferencesGet(t *testing.T) {
pref := newTestPreferences(t)
func TestKeyValueStoreGet(t *testing.T) {
pref := newTestKeyValueStore(t)
require.Equal(t, "value", pref.Get("str"))
require.Equal(t, "42", pref.Get("int"))
require.Equal(t, "true", pref.Get("bool"))
require.Equal(t, "t", pref.Get("falseBool"))
}
func TestPreferencesGetInt(t *testing.T) {
pref := newTestPreferences(t)
func TestKeyValueStoreGetInt(t *testing.T) {
pref := newTestKeyValueStore(t)
require.Equal(t, 0, pref.GetInt("str"))
require.Equal(t, 42, pref.GetInt("int"))
require.Equal(t, 0, pref.GetInt("bool"))
require.Equal(t, 0, pref.GetInt("falseBool"))
}
func TestPreferencesGetBool(t *testing.T) {
pref := newTestPreferences(t)
func TestKeyValueStoreGetBool(t *testing.T) {
pref := newTestKeyValueStore(t)
require.Equal(t, false, pref.GetBool("str"))
require.Equal(t, false, pref.GetBool("int"))
require.Equal(t, true, pref.GetBool("bool"))
require.Equal(t, false, pref.GetBool("falseBool"))
}
func TestPreferencesSetDefault(t *testing.T) {
pref := newTestEmptyPreferences(t)
pref.SetDefault("key", "value")
pref.SetDefault("key", "othervalue")
func TestKeyValueStoreSetDefault(t *testing.T) {
pref := newTestEmptyKeyValueStore(t)
pref.setDefault("key", "value")
pref.setDefault("key", "othervalue")
require.Equal(t, "value", pref.Get("key"))
}
func TestPreferencesSet(t *testing.T) {
pref := newTestEmptyPreferences(t)
func TestKeyValueStoreSet(t *testing.T) {
pref := newTestEmptyKeyValueStore(t)
pref.Set("str", "value")
checkSavedPreferences(t, "{\"str\":\"value\"}")
checkSavedKeyValueStore(t, "{\"str\":\"value\"}")
}
func TestPreferencesSetInt(t *testing.T) {
pref := newTestEmptyPreferences(t)
func TestKeyValueStoreSetInt(t *testing.T) {
pref := newTestEmptyKeyValueStore(t)
pref.SetInt("int", 42)
checkSavedPreferences(t, "{\"int\":\"42\"}")
checkSavedKeyValueStore(t, "{\"int\":\"42\"}")
}
func TestPreferencesSetBool(t *testing.T) {
pref := newTestEmptyPreferences(t)
func TestKeyValueStoreSetBool(t *testing.T) {
pref := newTestEmptyKeyValueStore(t)
pref.SetBool("trueBool", true)
pref.SetBool("falseBool", false)
checkSavedPreferences(t, "{\"falseBool\":\"false\",\"trueBool\":\"true\"}")
checkSavedKeyValueStore(t, "{\"falseBool\":\"false\",\"trueBool\":\"true\"}")
}
func newTestEmptyPreferences(t *testing.T) *Preferences {
func newTestEmptyKeyValueStore(t *testing.T) *keyValueStore {
require.NoError(t, os.RemoveAll(testPrefFilePath))
return NewPreferences(testPrefFilePath)
return newKeyValueStore(testPrefFilePath)
}
func newTestPreferences(t *testing.T) *Preferences {
func newTestKeyValueStore(t *testing.T) *keyValueStore {
require.NoError(t, ioutil.WriteFile(testPrefFilePath, []byte("{\"str\":\"value\",\"int\":\"42\",\"bool\":\"true\",\"falseBool\":\"t\"}"), 0700))
return NewPreferences(testPrefFilePath)
return newKeyValueStore(testPrefFilePath)
}
func checkSavedPreferences(t *testing.T, expected string) {
func checkSavedKeyValueStore(t *testing.T, expected string) {
data, err := ioutil.ReadFile(testPrefFilePath)
require.NoError(t, err)
require.Equal(t, expected+"\n", string(data))

View File

@ -15,15 +15,14 @@
// 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 preferences provides key names and defaults for preferences used in Bridge.
package preferences
// Package settings provides access to persistent user settings.
package settings
import (
"strconv"
"fmt"
"math/rand"
"path/filepath"
"time"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/sirupsen/logrus"
)
// Keys of preferences in JSON file.
@ -37,43 +36,51 @@ const (
SMTPSSLKey = "user_ssl_smtp"
AllowProxyKey = "allow_proxy"
AutostartKey = "autostart"
AutoUpdateKey = "autoupdate"
CookiesKey = "cookies"
ReportOutgoingNoEncKey = "report_outgoing_email_without_encryption"
LastVersionKey = "last_used_version"
RolloutKey = "rollout"
)
type configProvider interface {
GetPreferencesPath() string
GetDefaultAPIPort() int
GetDefaultIMAPPort() int
GetDefaultSMTPPort() int
type Settings struct {
*keyValueStore
settingsPath string
}
var log = logrus.WithField("pkg", "store") //nolint[gochecknoglobals]
// New returns loaded preferences with Bridge defaults when values are not set yet.
func New(cfg configProvider) (pref *config.Preferences) {
path := cfg.GetPreferencesPath()
pref = config.NewPreferences(path)
setDefaults(pref, cfg)
log.WithField("path", path).Trace("Opened preferences")
return
func New(settingsPath string) *Settings {
s := &Settings{
keyValueStore: newKeyValueStore(filepath.Join(settingsPath, "prefs.json")),
settingsPath: settingsPath,
}
func setDefaults(preferences *config.Preferences, cfg configProvider) {
preferences.SetDefault(FirstStartKey, "true")
preferences.SetDefault(FirstStartGUIKey, "true")
preferences.SetDefault(NextHeartbeatKey, strconv.FormatInt(time.Now().Unix(), 10))
preferences.SetDefault(APIPortKey, strconv.Itoa(cfg.GetDefaultAPIPort()))
preferences.SetDefault(IMAPPortKey, strconv.Itoa(cfg.GetDefaultIMAPPort()))
preferences.SetDefault(SMTPPortKey, strconv.Itoa(cfg.GetDefaultSMTPPort()))
preferences.SetDefault(AllowProxyKey, "true")
preferences.SetDefault(AutostartKey, "true")
preferences.SetDefault(ReportOutgoingNoEncKey, "false")
preferences.SetDefault(LastVersionKey, "")
s.setDefaultValues()
return s
}
const (
DefaultIMAPPort = "1143"
DefaultSMTPPort = "1025"
DefaultAPIPort = "1042"
)
func (s *Settings) setDefaultValues() {
s.setDefault(FirstStartKey, "true")
s.setDefault(FirstStartGUIKey, "true")
s.setDefault(NextHeartbeatKey, fmt.Sprintf("%v", time.Now().Unix()))
s.setDefault(AllowProxyKey, "true")
s.setDefault(AutostartKey, "true")
s.setDefault(AutoUpdateKey, "false")
s.setDefault(ReportOutgoingNoEncKey, "false")
s.setDefault(LastVersionKey, "")
s.setDefault(RolloutKey, fmt.Sprintf("%v", rand.Float64()))
s.setDefault(APIPortKey, DefaultAPIPort)
s.setDefault(IMAPPortKey, DefaultIMAPPort)
s.setDefault(SMTPPortKey, DefaultSMTPPort)
// By default, stick to STARTTLS. If the user uses catalina+applemail they'll have to change to SSL.
preferences.SetDefault(SMTPSSLKey, "false")
s.setDefault(SMTPSSLKey, "false")
}

View File

@ -15,7 +15,7 @@
// 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 config
package tls
import (
"crypto/rand"
@ -29,13 +29,21 @@ import (
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"time"
"github.com/sirupsen/logrus"
)
type tlsConfiger interface {
GetTLSCertPath() string
GetTLSKeyPath() string
type TLS struct {
settingsPath string
}
func New(settingsPath string) *TLS {
return &TLS{
settingsPath: settingsPath,
}
}
var tlsTemplate = x509.Certificate{ //nolint[gochecknoglobals]
@ -57,14 +65,70 @@ var tlsTemplate = x509.Certificate{ //nolint[gochecknoglobals]
var ErrTLSCertExpireSoon = fmt.Errorf("TLS certificate will expire soon")
// GetTLSConfig tries to load TLS config or generate new one which is then returned.
func GetTLSConfig(cfg tlsConfiger) (tlsConfig *tls.Config, err error) {
certPath := cfg.GetTLSCertPath()
keyPath := cfg.GetTLSKeyPath()
// getTLSCertPath returns path to certificate; used for TLS servers (IMAP, SMTP).
func (t *TLS) getTLSCertPath() string {
return filepath.Join(t.settingsPath, "cert.pem")
}
// getTLSKeyPath returns path to private key; used for TLS servers (IMAP, SMTP).
func (t *TLS) getTLSKeyPath() string {
return filepath.Join(t.settingsPath, "key.pem")
}
// GenerateConfig generates certs and keys at the given filepaths and returns a TLS Config which holds them.
// See https://golang.org/src/crypto/tls/generate_cert.go
func (t *TLS) GenerateConfig() (tlsConfig *tls.Config, err error) {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
err = fmt.Errorf("failed to generate private key: %s", err)
return
}
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
err = fmt.Errorf("failed to generate serial number: %s", err)
return
}
tlsTemplate.SerialNumber = serialNumber
derBytes, err := x509.CreateCertificate(rand.Reader, &tlsTemplate, &tlsTemplate, &priv.PublicKey, priv)
if err != nil {
err = fmt.Errorf("failed to create certificate: %s", err)
return
}
certOut, err := os.Create(t.getTLSCertPath())
if err != nil {
return
}
defer certOut.Close() //nolint[errcheck]
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
if err != nil {
return
}
keyOut, err := os.OpenFile(t.getTLSKeyPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return
}
defer keyOut.Close() //nolint[errcheck]
err = pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
if err != nil {
return
}
return loadTLSConfig(t.getTLSCertPath(), t.getTLSKeyPath())
}
// GetConfig tries to load TLS config or generate new one which is then returned.
func (t *TLS) GetConfig() (tlsConfig *tls.Config, err error) {
certPath := t.getTLSCertPath()
keyPath := t.getTLSKeyPath()
tlsConfig, err = loadTLSConfig(certPath, keyPath)
if err != nil {
log.WithError(err).Warn("Cannot load cert, generating a new one")
tlsConfig, err = GenerateTLSConfig(certPath, keyPath)
logrus.WithError(err).Warn("Cannot load cert, generating a new one")
tlsConfig, err = t.GenerateConfig()
if err != nil {
return
}
@ -81,7 +145,7 @@ func GetTLSConfig(cfg tlsConfiger) (tlsConfig *tls.Config, err error) {
"-k", "/Library/Keychains/System.keychain",
certPath,
).Run(); err != nil {
log.WithError(err).Error("Failed to add cert to system keychain")
logrus.WithError(err).Error("Failed to add cert to system keychain")
}
}
}
@ -125,49 +189,3 @@ func loadTLSConfig(certPath, keyPath string) (tlsConfig *tls.Config, err error)
}
return
}
// GenerateTLSConfig generates certs and keys at the given filepaths and returns a TLS Config which holds them.
// See https://golang.org/src/crypto/tls/generate_cert.go
func GenerateTLSConfig(certPath, keyPath string) (tlsConfig *tls.Config, err error) {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
err = fmt.Errorf("failed to generate private key: %s", err)
return
}
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
err = fmt.Errorf("failed to generate serial number: %s", err)
return
}
tlsTemplate.SerialNumber = serialNumber
derBytes, err := x509.CreateCertificate(rand.Reader, &tlsTemplate, &tlsTemplate, &priv.PublicKey, priv)
if err != nil {
err = fmt.Errorf("failed to create certificate: %s", err)
return
}
certOut, err := os.Create(certPath)
if err != nil {
return
}
defer certOut.Close() //nolint[errcheck]
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
if err != nil {
return
}
keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return
}
defer keyOut.Close() //nolint[errcheck]
err = pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
if err != nil {
return
}
return loadTLSConfig(certPath, keyPath)
}

View File

@ -15,9 +15,10 @@
// 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 config
package tls
import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
@ -27,11 +28,6 @@ import (
"github.com/stretchr/testify/require"
)
type testTLSConfig struct{ certPath, keyPath string }
func (c *testTLSConfig) GetTLSCertPath() string { return c.certPath }
func (c *testTLSConfig) GetTLSKeyPath() string { return c.keyPath }
func TestTLSKeyRenewal(t *testing.T) {
// Remove keys.
configPath := "/tmp"
@ -40,10 +36,15 @@ func TestTLSKeyRenewal(t *testing.T) {
_ = os.Remove(certPath)
_ = os.Remove(keyPath)
dir, err := ioutil.TempDir("", "test-tls")
require.NoError(t, err)
tls := New(dir)
// Put old key there.
tlsTemplate.NotBefore = time.Now().Add(-365 * 24 * time.Hour)
tlsTemplate.NotAfter = time.Now()
cert, err := GenerateTLSConfig(certPath, keyPath)
cert, err := tls.GenerateConfig()
require.Equal(t, err, ErrTLSCertExpireSoon)
require.Equal(t, len(cert.Certificates), 1)
time.Sleep(time.Second)
@ -53,7 +54,7 @@ func TestTLSKeyRenewal(t *testing.T) {
// Renew key.
tlsTemplate.NotBefore = time.Now()
tlsTemplate.NotAfter = time.Now().Add(2 * 365 * 24 * time.Hour)
cert, err = GetTLSConfig(&testTLSConfig{certPath, keyPath})
cert, err = tls.GetConfig()
if runtime.GOOS != "darwin" { // Darwin is not supported.
require.NoError(t, err)
}

View File

@ -18,6 +18,10 @@
// Package constants contains variables that are set via ldflags during build.
package constants
import "fmt"
const VendorName = "protonmail"
// nolint[gochecknoglobals]
var (
// Version of the build.
@ -32,9 +36,6 @@ var (
// DSNSentry client keys to be able to report crashes to Sentry.
DSNSentry = ""
// LongVersion is derived from Version and Revision.
LongVersion = Version + " (" + Revision + ")"
// BuildVersion is derived from LongVersion and BuildTime.
BuildVersion = LongVersion + " " + BuildTime
BuildVersion = fmt.Sprintf("%v (%v) %v", Version, Revision, BuildTime)
)

View File

@ -22,7 +22,7 @@ import (
"net/http"
"time"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
)
// pantry persists and loads cookies to some persistent storage location.
@ -63,7 +63,7 @@ func (p *pantry) discardExpiredCookies() error {
type cookiesByHost map[string][]*http.Cookie
func (p *pantry) loadFromJSON() (cookiesByHost, error) {
b := p.gs.Get(preferences.CookiesKey)
b := p.gs.Get(settings.CookiesKey)
if b == "" {
return make(cookiesByHost), nil
@ -84,7 +84,7 @@ func (p *pantry) saveToJSON(cookies cookiesByHost) error {
return err
}
p.gs.Set(preferences.CookiesKey, string(b))
p.gs.Set(settings.CookiesKey, string(b))
return nil
}

42
internal/crash/actions.go Normal file
View File

@ -0,0 +1,42 @@
// 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 crash
import (
"fmt"
"github.com/0xAX/notificator"
)
// ShowErrorNotification shows a system notification that the app with the given appName has crashed.
// NOTE: Icons shouldn't be hardcoded.
func ShowErrorNotification(appName string) RecoveryAction {
return func(r interface{}) error {
notify := notificator.New(notificator.Options{
DefaultIcon: "../frontend/ui/icon/icon.png",
AppName: appName,
})
return notify.Push(
"Fatal Error",
fmt.Sprintf("%v has encountered a fatal error.", appName),
"/frontend/icon/icon.png",
notificator.UR_CRITICAL,
)
}
}

View File

@ -15,36 +15,40 @@
// 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 build_qa
package config
// Package crash implements a crash handler with configurable recovery actions.
package crash
import (
"github.com/ProtonMail/proton-bridge/pkg/sentry"
"github.com/sirupsen/logrus"
)
// getLogLevelAndFile for QA build is altered in a way even decrypted data are stored
// in the log file when forced with `debug-client-json` or `debug-server-json`.
func getLogLevelAndFile(levelFlag string) (level logrus.Level, useFile bool) {
useFile = true
switch levelFlag {
case "panic":
level = logrus.PanicLevel
case "fatal":
level = logrus.FatalLevel
case "error":
level = logrus.ErrorLevel
case "warn":
level = logrus.WarnLevel
case "info":
level = logrus.InfoLevel
case "debug-client-json", "debug-server-json":
level = logrus.DebugLevel
case "debug", "debug-client", "debug-server":
level = logrus.DebugLevel
useFile = false
default:
level = logrus.InfoLevel
type RecoveryAction func(interface{}) error
type Handler struct {
actions []RecoveryAction
}
func NewHandler(actions ...RecoveryAction) *Handler {
return &Handler{actions: actions}
}
func (h *Handler) AddRecoveryAction(action RecoveryAction) *Handler {
h.actions = append(h.actions, action)
return h
}
func (h *Handler) HandlePanic() {
sentry.SkipDuringUnwind()
r := recover()
if r == nil {
return
}
for _, action := range h.actions {
if err := action(r); err != nil {
logrus.WithError(err).Error("Failed to execute recovery action")
}
}
}

View File

@ -15,29 +15,44 @@
// 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_prod
package config
package crash
import (
"net/http"
"strings"
"fmt"
"testing"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/stretchr/testify/assert"
)
func (c *Config) GetAPIConfig() *pmapi.ClientConfig {
return &pmapi.ClientConfig{
AppVersion: c.getAPIOS() + strings.Title(c.appName) + "_" + c.version,
ClientID: c.appName,
}
}
func TestHandler(t *testing.T) {
var s string
func SetClientRoundTripper(_ *pmapi.ClientManager, _ *pmapi.ClientConfig, _ listener.Listener) {
// Use the default roundtripper; do nothing.
}
h := NewHandler(
func(r interface{}) error {
s += fmt.Sprintf("1: %v\n", r)
return nil
},
func(r interface{}) error {
s += fmt.Sprintf("2: %v\n", r)
return nil
},
)
func (c *Config) GetRoundTripper(_ *pmapi.ClientManager, _ listener.Listener) http.RoundTripper {
return http.DefaultTransport
h.
AddRecoveryAction(func(r interface{}) error {
s += fmt.Sprintf("3: %v\n", r)
return nil
}).
AddRecoveryAction(func(r interface{}) error {
s += fmt.Sprintf("4: %v\n", r)
return nil
})
defer func() {
assert.Equal(t, "1: thing\n2: thing\n3: thing\n4: thing\n", s)
}()
defer h.HandlePanic()
panic("thing")
}

View File

@ -27,6 +27,7 @@ import (
// Constants of events used by the event listener in bridge.
const (
ErrorEvent = "error"
CredentialsErrorEvent = "credentialsError"
CloseConnectionEvent = "closeConnection"
LogoutEvent = "logout"
AddressChangedEvent = "addressChanged"
@ -50,4 +51,5 @@ func SetupEvents(listener listener.Listener) {
listener.SetLimit(LogoutEvent, LogoutEventTimeout)
listener.SetBuffer(TLSCertIssue)
listener.SetBuffer(ErrorEvent)
listener.SetBuffer(CredentialsErrorEvent)
}

View File

@ -21,7 +21,8 @@ package cliie
import (
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/abiosoft/ishell"
@ -35,31 +36,33 @@ var (
type frontendCLI struct {
*ishell.Shell
config *config.Config
locations *locations.Locations
eventListener listener.Listener
updates types.Updater
updater types.Updater
ie types.ImportExporter
appRestart bool
restarter types.Restarter
}
// New returns a new CLI frontend configured with the given options.
func New( //nolint[funlen]
panicHandler types.PanicHandler,
config *config.Config,
locations *locations.Locations,
eventListener listener.Listener,
updates types.Updater,
updater types.Updater,
ie types.ImportExporter,
restarter types.Restarter,
) *frontendCLI { //nolint[golint]
fe := &frontendCLI{
Shell: ishell.New(),
config: config,
locations: locations,
eventListener: eventListener,
updates: updates,
updater: updater,
ie: ie,
appRestart: false,
restarter: restarter,
}
// Clear commands.
@ -175,13 +178,12 @@ func New( //nolint[funlen]
defer panicHandler.HandlePanic()
fe.watchEvents()
}()
fe.eventListener.RetryEmit(events.TLSCertIssue)
fe.eventListener.RetryEmit(events.ErrorEvent)
return fe
}
func (f *frontendCLI) watchEvents() {
errorCh := f.getEventChannel(events.ErrorEvent)
credentialsErrorCh := f.getEventChannel(events.CredentialsErrorEvent)
internetOffCh := f.getEventChannel(events.InternetOffEvent)
internetOnCh := f.getEventChannel(events.InternetOnEvent)
addressChangedLogoutCh := f.getEventChannel(events.AddressChangedLogoutEvent)
@ -191,6 +193,8 @@ func (f *frontendCLI) watchEvents() {
select {
case errorDetails := <-errorCh:
f.Println("Import-Export failed:", errorDetails)
case <-credentialsErrorCh:
f.notifyCredentialsError()
case <-internetOffCh:
f.notifyInternetOff()
case <-internetOnCh:
@ -212,21 +216,12 @@ func (f *frontendCLI) watchEvents() {
func (f *frontendCLI) getEventChannel(event string) <-chan string {
ch := make(chan string)
f.eventListener.Add(event, ch)
f.eventListener.RetryEmit(event)
return ch
}
// IsAppRestarting returns whether the app is currently set to restart.
func (f *frontendCLI) IsAppRestarting() bool {
return f.appRestart
}
// Loop starts the frontend loop with an interactive shell.
func (f *frontendCLI) Loop(credentialsError error) error {
if credentialsError != nil {
f.notifyCredentialsError()
return credentialsError
}
func (f *frontendCLI) Loop() error {
f.Print(`
Welcome to ProtonMail Import-Export app interactive shell
@ -235,3 +230,8 @@ WARNING: The CLI is an experimental feature and does not yet cover all functiona
f.Run()
return nil
}
func (f *frontendCLI) NotifyManualUpdate(update updater.VersionInfo) error {
// NOTE: Save the update somewhere so that it can be installed when user chooses "install now".
return nil
}

View File

@ -24,7 +24,7 @@ import (
func (f *frontendCLI) restart(c *ishell.Context) {
if f.yesNoQuestion("Are you sure you want to restart the Import-Export app") {
f.Println("Restarting the Import-Export app...")
f.appRestart = true
f.restarter.SetToRestart()
f.Stop()
}
}
@ -38,7 +38,11 @@ func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
}
func (f *frontendCLI) printLogDir(c *ishell.Context) {
f.Println("Log files are stored in\n\n ", f.config.GetLogDir())
if path, err := f.locations.ProvideLogsPath(); err != nil {
f.Println("Failed to determine location of log files")
} else {
f.Println("Log files are stored in\n\n ", path)
}
}
func (f *frontendCLI) printManual(c *ishell.Context) {

View File

@ -21,41 +21,15 @@ import (
"strings"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) checkUpdates(c *ishell.Context) {
isUpToDate, latestVersionInfo, err := f.updates.CheckIsUpToDate()
if err != nil {
f.printAndLogError("Cannot retrieve version info: ", err)
f.checkInternetConnection(c)
return
}
if isUpToDate {
f.Println("Your version is up to date.")
} else {
f.notifyNeedUpgrade()
f.Println("")
f.printReleaseNotes(latestVersionInfo)
}
}
func (f *frontendCLI) printLocalReleaseNotes(c *ishell.Context) {
localVersion := f.updates.GetLocalVersion()
f.printReleaseNotes(localVersion)
}
func (f *frontendCLI) printReleaseNotes(versionInfo updates.VersionInfo) {
f.Println(bold("ProtonMail Import-Export app "+versionInfo.Version), "\n")
if versionInfo.ReleaseNotes != "" {
f.Println(bold("Release Notes"))
f.Println(versionInfo.ReleaseNotes)
}
if versionInfo.ReleaseFixedBugs != "" {
f.Println(bold("Fixed bugs"))
f.Println(versionInfo.ReleaseFixedBugs)
}
f.Println("TODO")
}
func (f *frontendCLI) printCredits(c *ishell.Context) {

View File

@ -93,10 +93,10 @@ func (f *frontendCLI) notifyLogout(address string) {
}
func (f *frontendCLI) notifyNeedUpgrade() {
f.Println("Please download and install the newest version of application from", f.updates.GetDownloadLink())
f.Println("TODO")
}
func (f *frontendCLI) notifyCredentialsError() {
func (f *frontendCLI) notifyCredentialsError() { // nolint[unused]
// Print in 80-column width.
f.Println("ProtonMail Import-Export app is not able to detect a supported password manager")
f.Println("(pass, gnome-keyring). Please install and set up a supported password manager")

View File

@ -21,8 +21,8 @@ import (
"strings"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/abiosoft/ishell"
)
@ -65,13 +65,13 @@ func (f *frontendCLI) showAccountInfo(c *ishell.Context) {
func (f *frontendCLI) showAccountAddressInfo(user types.User, address string) {
smtpSecurity := "STARTTLS"
if f.preferences.GetBool(preferences.SMTPSSLKey) {
if f.settings.GetBool(settings.SMTPSSLKey) {
smtpSecurity = "SSL"
}
f.Println(bold("Configuration for " + address))
f.Printf("IMAP Settings\nAddress: %s\nIMAP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n",
bridge.Host,
f.preferences.GetInt(preferences.IMAPPortKey),
f.settings.GetInt(settings.IMAPPortKey),
address,
user.GetBridgePassword(),
"STARTTLS",
@ -79,7 +79,7 @@ func (f *frontendCLI) showAccountAddressInfo(user types.User, address string) {
f.Println("")
f.Printf("SMTP Settings\nAddress: %s\nSMTP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n",
bridge.Host,
f.preferences.GetInt(preferences.SMTPPortKey),
f.settings.GetInt(settings.SMTPPortKey),
address,
user.GetBridgePassword(),
smtpSecurity,

View File

@ -19,9 +19,11 @@
package cli
import (
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/abiosoft/ishell"
@ -35,34 +37,36 @@ var (
type frontendCLI struct {
*ishell.Shell
config *config.Config
preferences *config.Preferences
locations *locations.Locations
settings *settings.Settings
eventListener listener.Listener
updates types.Updater
updater types.Updater
bridge types.Bridger
appRestart bool
restarter types.Restarter
}
// New returns a new CLI frontend configured with the given options.
func New( //nolint[funlen]
panicHandler types.PanicHandler,
config *config.Config,
preferences *config.Preferences,
locations *locations.Locations,
settings *settings.Settings,
eventListener listener.Listener,
updates types.Updater,
updater types.Updater,
bridge types.Bridger,
restarter types.Restarter,
) *frontendCLI { //nolint[golint]
fe := &frontendCLI{
Shell: ishell.New(),
config: config,
preferences: preferences,
locations: locations,
settings: settings,
eventListener: eventListener,
updates: updates,
updater: updater,
bridge: bridge,
appRestart: false,
restarter: restarter,
}
// Clear commands.
@ -185,13 +189,12 @@ func New( //nolint[funlen]
defer panicHandler.HandlePanic()
fe.watchEvents()
}()
fe.eventListener.RetryEmit(events.TLSCertIssue)
fe.eventListener.RetryEmit(events.ErrorEvent)
return fe
}
func (f *frontendCLI) watchEvents() {
errorCh := f.getEventChannel(events.ErrorEvent)
credentialsErrorCh := f.getEventChannel(events.CredentialsErrorEvent)
internetOffCh := f.getEventChannel(events.InternetOffEvent)
internetOnCh := f.getEventChannel(events.InternetOnEvent)
addressChangedCh := f.getEventChannel(events.AddressChangedEvent)
@ -202,6 +205,8 @@ func (f *frontendCLI) watchEvents() {
select {
case errorDetails := <-errorCh:
f.Println("Bridge failed:", errorDetails)
case <-credentialsErrorCh:
f.notifyCredentialsError()
case <-internetOffCh:
f.notifyInternetOff()
case <-internetOnCh:
@ -225,21 +230,12 @@ func (f *frontendCLI) watchEvents() {
func (f *frontendCLI) getEventChannel(event string) <-chan string {
ch := make(chan string)
f.eventListener.Add(event, ch)
f.eventListener.RetryEmit(event)
return ch
}
// IsAppRestarting returns whether the app is currently set to restart.
func (f *frontendCLI) IsAppRestarting() bool {
return f.appRestart
}
// Loop starts the frontend loop with an interactive shell.
func (f *frontendCLI) Loop(credentialsError error) error {
if credentialsError != nil {
f.notifyCredentialsError()
return credentialsError
}
func (f *frontendCLI) Loop() error {
f.Print(`
Welcome to ProtonMail Bridge interactive shell
___....___
@ -260,3 +256,8 @@ func (f *frontendCLI) Loop(credentialsError error) error {
f.Run()
return nil
}
func (f *frontendCLI) NotifyManualUpdate(update updater.VersionInfo) error {
// NOTE: Save the update somewhere so that it can be installed when user chooses "install now".
return nil
}

View File

@ -22,7 +22,7 @@ import (
"strconv"
"strings"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/abiosoft/ishell"
)
@ -34,7 +34,7 @@ var (
func (f *frontendCLI) restart(c *ishell.Context) {
if f.yesNoQuestion("Are you sure you want to restart the Bridge") {
f.Println("Restarting Bridge...")
f.appRestart = true
f.restarter.SetToRestart()
f.Stop()
}
}
@ -48,7 +48,11 @@ func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
}
func (f *frontendCLI) printLogDir(c *ishell.Context) {
f.Println("Log files are stored in\n\n ", f.config.GetLogDir())
if path, err := f.locations.ProvideLogsPath(); err != nil {
f.Println("Failed to determine location of log files")
} else {
f.Println("Log files are stored in\n\n ", path)
}
}
func (f *frontendCLI) printManual(c *ishell.Context) {
@ -69,7 +73,7 @@ func (f *frontendCLI) deleteCache(c *ishell.Context) {
f.Println("Cached cleared, restarting bridge")
// Clearing data removes everything (db, preferences, ...)
// so everything has to be stopped and started again.
f.appRestart = true
f.restarter.SetToRestart()
f.Stop()
}
@ -77,7 +81,7 @@ func (f *frontendCLI) changeSMTPSecurity(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
isSSL := f.preferences.GetBool(preferences.SMTPSSLKey)
isSSL := f.settings.GetBool(settings.SMTPSSLKey)
newSecurity := "SSL"
if isSSL {
newSecurity = "STARTTLS"
@ -86,9 +90,9 @@ func (f *frontendCLI) changeSMTPSecurity(c *ishell.Context) {
msg := fmt.Sprintf("Are you sure you want to change SMTP setting to %q and restart the Bridge", newSecurity)
if f.yesNoQuestion(msg) {
f.preferences.SetBool(preferences.SMTPSSLKey, !isSSL)
f.settings.SetBool(settings.SMTPSSLKey, !isSSL)
f.Println("Restarting Bridge...")
f.appRestart = true
f.restarter.SetToRestart()
f.Stop()
}
}
@ -97,14 +101,14 @@ func (f *frontendCLI) changePort(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
currentPort = f.preferences.Get(preferences.IMAPPortKey)
currentPort = f.settings.Get(settings.IMAPPortKey)
newIMAPPort := f.readStringInAttempts("Set IMAP port (current "+currentPort+")", c.ReadLine, f.isPortFree)
if newIMAPPort == "" {
newIMAPPort = currentPort
}
imapPortChanged := newIMAPPort != currentPort
currentPort = f.preferences.Get(preferences.SMTPPortKey)
currentPort = f.settings.Get(settings.SMTPPortKey)
newSMTPPort := f.readStringInAttempts("Set SMTP port (current "+currentPort+")", c.ReadLine, f.isPortFree)
if newSMTPPort == "" {
newSMTPPort = currentPort
@ -118,10 +122,10 @@ func (f *frontendCLI) changePort(c *ishell.Context) {
if imapPortChanged || smtpPortChanged {
f.Println("Saving values IMAP:", newIMAPPort, "SMTP:", newSMTPPort)
f.preferences.Set(preferences.IMAPPortKey, newIMAPPort)
f.preferences.Set(preferences.SMTPPortKey, newSMTPPort)
f.settings.Set(settings.IMAPPortKey, newIMAPPort)
f.settings.Set(settings.SMTPPortKey, newSMTPPort)
f.Println("Restarting Bridge...")
f.appRestart = true
f.restarter.SetToRestart()
f.Stop()
} else {
f.Println("Nothing changed")
@ -129,16 +133,16 @@ func (f *frontendCLI) changePort(c *ishell.Context) {
}
func (f *frontendCLI) toggleAllowProxy(c *ishell.Context) {
if f.preferences.GetBool(preferences.AllowProxyKey) {
if f.settings.GetBool(settings.AllowProxyKey) {
f.Println("Bridge is currently set to use alternative routing to connect to Proton if it is being blocked.")
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") {
f.preferences.SetBool(preferences.AllowProxyKey, false)
f.settings.SetBool(settings.AllowProxyKey, false)
f.bridge.DisallowProxy()
}
} else {
f.Println("Bridge is currently set to NOT use alternative routing to connect to Proton if it is being blocked.")
if f.yesNoQuestion("Are you sure you want to allow bridge to do this") {
f.preferences.SetBool(preferences.AllowProxyKey, true)
f.settings.SetBool(settings.AllowProxyKey, true)
f.bridge.AllowProxy()
}
}

View File

@ -21,41 +21,15 @@ import (
"strings"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) checkUpdates(c *ishell.Context) {
isUpToDate, latestVersionInfo, err := f.updates.CheckIsUpToDate()
if err != nil {
f.printAndLogError("Cannot retrieve version info: ", err)
f.checkInternetConnection(c)
return
}
if isUpToDate {
f.Println("Your version is up to date.")
} else {
f.notifyNeedUpgrade()
f.Println("")
f.printReleaseNotes(latestVersionInfo)
}
}
func (f *frontendCLI) printLocalReleaseNotes(c *ishell.Context) {
localVersion := f.updates.GetLocalVersion()
f.printReleaseNotes(localVersion)
}
func (f *frontendCLI) printReleaseNotes(versionInfo updates.VersionInfo) {
f.Println(bold("ProtonMail Bridge "+versionInfo.Version), "\n")
if versionInfo.ReleaseNotes != "" {
f.Println(bold("Release Notes"))
f.Println(versionInfo.ReleaseNotes)
}
if versionInfo.ReleaseFixedBugs != "" {
f.Println(bold("Fixed bugs"))
f.Println(versionInfo.ReleaseFixedBugs)
}
f.Println("TODO")
}
func (f *frontendCLI) printCredits(c *ishell.Context) {

View File

@ -93,10 +93,10 @@ func (f *frontendCLI) notifyLogout(address string) {
}
func (f *frontendCLI) notifyNeedUpgrade() {
f.Println("Please download and install the newest version of application from", f.updates.GetDownloadLink())
f.Println("TODO")
}
func (f *frontendCLI) notifyCredentialsError() {
func (f *frontendCLI) notifyCredentialsError() { // nolint[unused]
// Print in 80-column width.
f.Println("ProtonMail Bridge is not able to detect a supported password manager")
f.Println("(pass, gnome-keyring). Please install and set up a supported password manager")

View File

@ -19,15 +19,16 @@
package frontend
import (
"github.com/0xAX/notificator"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/frontend/cli"
cliie "github.com/ProtonMail/proton-bridge/internal/frontend/cli-ie"
"github.com/ProtonMail/proton-bridge/internal/frontend/qt"
qtie "github.com/ProtonMail/proton-bridge/internal/frontend/qt-ie"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/sirupsen/logrus"
)
@ -38,17 +39,8 @@ var (
// Frontend is an interface to be implemented by each frontend type (cli, gui, html).
type Frontend interface {
Loop(credentialsError error) error
IsAppRestarting() bool
}
// HandlePanic handles panics which occur for users with GUI.
func HandlePanic(appName string) {
notify := notificator.New(notificator.Options{
DefaultIcon: "../frontend/ui/icon/icon.png",
AppName: appName,
})
_ = notify.Push("Fatal Error", "The "+appName+" has encountered a fatal error. ", "/frontend/icon/icon.png", notificator.UR_CRITICAL)
Loop() error
NotifyManualUpdate(update updater.VersionInfo) error
}
// New returns initialized frontend based on `frontendType`, which can be `cli` or `qt`.
@ -58,35 +50,70 @@ func New(
frontendType string,
showWindowOnStart bool,
panicHandler types.PanicHandler,
config *config.Config,
preferences *config.Preferences,
locations *locations.Locations,
settings *settings.Settings,
eventListener listener.Listener,
updates types.Updater,
updater types.Updater,
bridge *bridge.Bridge,
noEncConfirmator types.NoEncConfirmator,
restarter types.Restarter,
) Frontend {
bridgeWrap := types.NewBridgeWrap(bridge)
return new(version, buildVersion, frontendType, showWindowOnStart, panicHandler, config, preferences, eventListener, updates, bridgeWrap, noEncConfirmator)
return newBridgeFrontend(
version,
buildVersion,
frontendType,
showWindowOnStart,
panicHandler,
locations,
settings,
eventListener,
updater,
bridgeWrap,
noEncConfirmator,
restarter,
)
}
func new(
func newBridgeFrontend(
version,
buildVersion,
frontendType string,
showWindowOnStart bool,
panicHandler types.PanicHandler,
config *config.Config,
preferences *config.Preferences,
locations *locations.Locations,
settings *settings.Settings,
eventListener listener.Listener,
updates types.Updater,
updater types.Updater,
bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator,
restarter types.Restarter,
) Frontend {
switch frontendType {
case "cli":
return cli.New(panicHandler, config, preferences, eventListener, updates, bridge)
return cli.New(
panicHandler,
locations,
settings,
eventListener,
updater,
bridge,
restarter,
)
default:
return qt.New(version, buildVersion, showWindowOnStart, panicHandler, config, preferences, eventListener, updates, bridge, noEncConfirmator)
return qt.New(
version,
buildVersion,
showWindowOnStart,
panicHandler,
locations,
settings,
eventListener,
updater,
bridge,
noEncConfirmator,
restarter,
)
}
}
@ -96,29 +123,43 @@ func NewImportExport(
buildVersion,
frontendType string,
panicHandler types.PanicHandler,
config *config.Config,
locations *locations.Locations,
eventListener listener.Listener,
updates types.Updater,
updater types.Updater,
ie *importexport.ImportExport,
restarter types.Restarter,
) Frontend {
ieWrap := types.NewImportExportWrap(ie)
return newImportExport(version, buildVersion, frontendType, panicHandler, config, eventListener, updates, ieWrap)
return newIEFrontend(
version,
buildVersion,
frontendType,
panicHandler,
locations,
eventListener,
updater,
ieWrap,
restarter,
)
}
func newImportExport(
func newIEFrontend(
version,
buildVersion,
frontendType string,
panicHandler types.PanicHandler,
config *config.Config,
locations *locations.Locations,
eventListener listener.Listener,
updates types.Updater,
updater types.Updater,
ie types.ImportExporter,
restarter types.Restarter,
) Frontend {
switch frontendType {
case "cli":
return cliie.New(panicHandler, config, eventListener, updates, ie)
return cliie.New(panicHandler, locations, eventListener, updater, ie, restarter)
default:
return qtie.New(version, buildVersion, panicHandler, config, eventListener, updates, ie)
return qtie.New(version, buildVersion, panicHandler, locations, eventListener, updater, ie, restarter)
}
}

View File

@ -226,7 +226,7 @@ Dialog {
target: timer
onTriggered: {
go.setPortsAndSecurity(imapPort.text, smtpPort.text, securitySMTPSTARTTLS.checked)
go.isRestarting = true
go.setToRestart()
Qt.quit()
}
}

View File

@ -69,20 +69,9 @@ Item {
Connections {
target: go
onShowWindow : {
winMain.showAndRise()
}
onProcessFinished : {
winMain.dialogAddUser.hide()

View File

@ -566,7 +566,6 @@ Window {
return 0
}
property bool isRestarting: false
function setPortsAndSecurity(portIMAP, portSMTP, secSMTP) {
console.log("Test: ports changed", portIMAP, portSMTP, secSMTP)
}

View File

@ -25,10 +25,9 @@ import (
"sync"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/keychain"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
@ -38,7 +37,6 @@ type QMLer interface {
ProcessFinished()
NotifyHasNoKeychain()
SetConnectionStatus(bool)
SetIsRestarting(bool)
SetAddAccountWarning(string, int)
NotifyBubble(int, string)
EmitEvent(string, string)
@ -53,20 +51,22 @@ type Accounts struct {
Model *AccountsModel
qml QMLer
um types.UserManager
prefs *config.Preferences
settings *settings.Settings
authClient pmapi.Client
auth *pmapi.Auth
LatestUserID string
accountMutex sync.Mutex
restarter types.Restarter
}
// SetupAccounts will create Model and set QMLer and UserManager
func (a *Accounts) SetupAccounts(qml QMLer, um types.UserManager) {
func (a *Accounts) SetupAccounts(qml QMLer, um types.UserManager, restarter types.Restarter) {
a.Model = NewAccountsModel(nil)
a.qml = qml
a.um = um
a.restarter = restarter
}
// LoadAccounts refreshes the current account list in GUI
@ -102,9 +102,9 @@ func (a *Accounts) LoadAccounts() {
accInfo.SetUserID(user.ID())
accInfo.SetHostname(bridge.Host)
accInfo.SetPassword(user.GetBridgePassword())
if a.prefs != nil {
accInfo.SetPortIMAP(a.prefs.GetInt(preferences.IMAPPortKey))
accInfo.SetPortSMTP(a.prefs.GetInt(preferences.SMTPPortKey))
if a.settings != nil {
accInfo.SetPortIMAP(a.settings.GetInt(settings.IMAPPortKey))
accInfo.SetPortSMTP(a.settings.GetInt(settings.SMTPPortKey))
}
// Set aliases.
@ -127,7 +127,7 @@ func (a *Accounts) ClearCache() {
}
// Clearing data removes everything (db, preferences, ...)
// so everything has to be stopped and started again.
a.qml.SetIsRestarting(true)
a.restarter.SetToRestart()
a.qml.Quit()
}

View File

@ -111,10 +111,12 @@ func WaitForEnter() {
type Listener interface {
Add(string, chan<- string)
RetryEmit(string)
}
func MakeAndRegisterEvent(eventListener Listener, event string) <-chan string {
ch := make(chan string)
eventListener.Add(event, ch)
eventListener.RetryEmit(event)
return ch
}

View File

@ -22,16 +22,14 @@ package qtie
import (
"errors"
"os"
"strconv"
"time"
"github.com/ProtonMail/proton-bridge/internal/events"
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/therecipe/qt/core"
@ -51,9 +49,9 @@ var log = logrus.WithField("pkg", "frontend-qt-ie")
// Qt and QML objects. QML signals and slots are connected via methods of GoQMLInterface.
type FrontendQt struct {
panicHandler types.PanicHandler
config *config.Config
locations *locations.Locations
eventListener listener.Listener
updates types.Updater
updater types.Updater
ie types.ImportExporter
App *widgets.QApplication // Main Application pointer
@ -72,44 +70,39 @@ type FrontendQt struct {
transfer *transfer.Transfer
progress *transfer.Progress
notifyHasNoKeychain bool
restarter types.Restarter
}
// New is constructor for Import-Export Qt-Go interface
func New(
version, buildVersion string,
panicHandler types.PanicHandler,
config *config.Config,
locations *locations.Locations,
eventListener listener.Listener,
updates types.Updater,
updater types.Updater,
ie types.ImportExporter,
restarter types.Restarter,
) *FrontendQt {
f := &FrontendQt{
panicHandler: panicHandler,
config: config,
locations: locations,
programName: "ProtonMail Import-Export",
programVersion: "v" + version,
eventListener: eventListener,
updater: updater,
buildVersion: buildVersion,
updates: updates,
ie: ie,
restarter: restarter,
}
log.Debugf("New Qt frontend: %p", f)
return f
}
// IsAppRestarting for Import-Export is always false i.e never restarts
func (f *FrontendQt) IsAppRestarting() bool {
return false
}
// Loop function for Import-Export interface. It runs QtExecute in main thread
// with no additional function.
func (f *FrontendQt) Loop(setupError error) (err error) {
if setupError != nil {
f.notifyHasNoKeychain = true
}
func (f *FrontendQt) Loop() (err error) {
go func() {
defer f.panicHandler.HandlePanic()
f.watchEvents()
@ -118,9 +111,16 @@ func (f *FrontendQt) Loop(setupError error) (err error) {
return err
}
func (f *FrontendQt) NotifyManualUpdate(update updater.VersionInfo) error {
// NOTE: Save the update somewhere so that it can be installed when user chooses "install now".
return nil
}
func (f *FrontendQt) watchEvents() {
credentialsErrorCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.CredentialsErrorEvent)
internetOffCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.InternetOffEvent)
internetOnCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.InternetOnEvent)
secondInstanceCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.SecondInstanceEvent)
restartBridgeCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.RestartBridgeEvent)
addressChangedCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.AddressChangedEvent)
addressChangedLogoutCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.AddressChangedLogoutEvent)
@ -129,12 +129,16 @@ func (f *FrontendQt) watchEvents() {
newUserCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.UserRefreshEvent)
for {
select {
case <-credentialsErrorCh:
f.Qml.NotifyHasNoKeychain()
case <-internetOffCh:
f.Qml.SetConnectionStatus(false)
case <-internetOnCh:
f.Qml.SetConnectionStatus(true)
case <-secondInstanceCh:
f.Qml.ShowWindow()
case <-restartBridgeCh:
f.Qml.SetIsRestarting(true)
f.restarter.SetToRestart()
f.App.Quit()
case address := <-addressChangedCh:
f.Qml.NotifyAddressChanged(address)
@ -165,7 +169,7 @@ func (f *FrontendQt) qtSetupQmlAndStructures() {
f.View.RootContext().SetContextProperty("go", f.Qml)
// Add AccountsModel
f.Accounts.SetupAccounts(f.Qml, f.ie)
f.Accounts.SetupAccounts(f.Qml, f.ie, f.restarter)
f.View.RootContext().SetContextProperty("accountsModel", f.Accounts.Model)
// Add TransferRules structure
@ -189,11 +193,6 @@ func (f *FrontendQt) qtSetupQmlAndStructures() {
} else {
f.Qml.SetIsFirstStart(false)
}
// Notify user about error during initialization.
if f.notifyHasNoKeychain {
f.Qml.NotifyHasNoKeychain()
}
}
// QtExecute in main for starting Qt application
@ -233,7 +232,12 @@ func (f *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error {
}
func (f *FrontendQt) openLogs() {
go open.Run(f.config.GetLogDir())
logsPath, err := f.locations.ProvideLogsPath()
if err != nil {
return
}
go open.Run(logsPath)
}
func (f *FrontendQt) openReport() {
@ -241,7 +245,7 @@ func (f *FrontendQt) openReport() {
}
func (f *FrontendQt) openDownloadLink() {
go open.Run(f.updates.GetDownloadLink())
// NOTE: Fix this.
}
// sendImportReport sends an anonymized import or export report file to our customer support
@ -365,34 +369,8 @@ func (f *FrontendQt) setProgressManager(progress *transfer.Progress) {
}()
}
// StartUpdate is identical to bridge
func (f *FrontendQt) StartUpdate() {
progress := make(chan updates.Progress)
go func() { // Update progress in QML.
defer f.panicHandler.HandlePanic()
for current := range progress {
f.Qml.SetProgress(current.Processed)
f.Qml.SetProgressDescription(strconv.Itoa(current.Description))
// Error happend
if current.Err != nil {
log.Error("update progress: ", current.Err)
f.Qml.UpdateFinished(true)
return
}
// Finished everything OK.
if current.Description >= updates.InfoQuitApp {
f.Qml.UpdateFinished(false)
time.Sleep(3 * time.Second) // Just notify.
f.Qml.SetIsRestarting(current.Description == updates.InfoRestartApp)
f.App.Quit()
return
}
}
}()
go func() {
defer f.panicHandler.HandlePanic()
f.updates.StartUpgrade(progress)
}()
// NOTE: Fix this.
}
// isNewVersionAvailable is identical to bridge
@ -401,26 +379,11 @@ func (f *FrontendQt) StartUpdate() {
func (f *FrontendQt) isNewVersionAvailable(showMessage bool) {
go func() {
defer f.Qml.ProcessFinished()
isUpToDate, latestVersionInfo, err := f.updates.CheckIsUpToDate()
if err != nil {
log.Warnln("Cannot retrieve version info: ", err)
f.checkInternet()
return
}
f.Qml.SetConnectionStatus(true) // if we are here connection is ok
if isUpToDate {
f.Qml.SetUpdateState(StatusUpToDate)
if showMessage {
f.Qml.NotifyVersionIsTheLatest()
}
return
}
f.Qml.SetNewversion(latestVersionInfo.Version)
f.Qml.SetChangelog(latestVersionInfo.ReleaseNotes)
f.Qml.SetBugfixes(latestVersionInfo.ReleaseFixedBugs)
f.Qml.SetLandingPage(latestVersionInfo.LandingPage)
f.Qml.SetDownloadLink(latestVersionInfo.GetDownloadLink())
f.Qml.SetUpdateState(StatusNewVersionAvailable)
}()
}
@ -434,16 +397,12 @@ func (f *FrontendQt) resetSource() {
}
func (f *FrontendQt) openLicenseFile() {
go open.Run(f.config.GetLicenseFilePath())
go open.Run(f.locations.GetLicenseFilePath())
}
// getLocalVersionInfo is identical to bridge.
func (f *FrontendQt) getLocalVersionInfo() {
defer f.Qml.ProcessFinished()
localVersion := f.updates.GetLocalVersion()
f.Qml.SetNewversion(localVersion.Version)
f.Qml.SetChangelog(localVersion.ReleaseNotes)
f.Qml.SetBugfixes(localVersion.ReleaseFixedBugs)
// NOTE: Fix this.
}
// LeastUsedColor is intended to return color for creating a new inbox or label.

View File

@ -24,7 +24,8 @@ import (
"net/http"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/sirupsen/logrus"
)
@ -33,23 +34,27 @@ var log = logrus.WithField("pkg", "frontend-nogui") //nolint[gochecknoglobals]
type FrontendHeadless struct{}
func (s *FrontendHeadless) Loop(credentialsError error) error {
log.Info("Check status on localhost:8081")
func (s *FrontendHeadless) Loop() error {
log.Info("Check status on localhost:8082")
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "IE is running")
})
return http.ListenAndServe(":8081", nil)
return http.ListenAndServe(":8082", nil)
}
func (s *FrontendHeadless) IsAppRestarting() bool { return false }
func (s *FrontendHeadless) NotifyManualUpdate(update updater.VersionInfo) error {
// NOTE: Save the update somewhere so that it can be installed when user chooses "install now".
return nil
}
func New(
version, buildVersion string,
panicHandler types.PanicHandler,
config *config.Config,
locations *locations.Locations,
eventListener listener.Listener,
updates types.Updater,
updater types.Updater,
ie types.ImportExporter,
restarter types.Restarter,
) *FrontendHeadless {
return &FrontendHeadless{}
}

View File

@ -37,7 +37,6 @@ type GoQMLInterface struct {
_ string `property:"goos"`
_ string `property:"credits"`
_ bool `property:"isFirstStart"`
_ bool `property:"isRestarting"`
_ bool `property:"isConnectionOK"`
_ string `property:lastError`
@ -68,6 +67,8 @@ type GoQMLInterface struct {
_ func(updateState string) `signal:"setUpdateState"`
_ func() `slot:"checkInternet"`
_ func() `slot:"setToRestart"`
_ func() `signal:"processFinished"`
_ func(okay bool) `signal:"exportStructureLoadFinished"`
_ func(okay bool) `signal:"importStructuresLoadFinished"`
@ -77,6 +78,8 @@ type GoQMLInterface struct {
_ func() `slot:"getLocalVersionInfo"`
_ func() `slot:"loadImportReports"`
_ func() `signal:"showWindow"`
_ func() `slot:"quit"`
_ func() `slot:"loadAccounts"`
_ func() `slot:"openLogs"`
@ -165,7 +168,6 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
s.ConnectAddAccount(f.Accounts.AddAccount)
s.SetGoos(runtime.GOOS)
s.SetIsRestarting(false)
s.SetProgramTitle(f.programName)
s.ConnectOpenLicenseFile(f.openLicenseFile)
@ -177,6 +179,8 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
s.ConnectCheckInternet(f.checkInternet)
s.ConnectSetToRestart(f.restarter.SetToRestart)
s.ConnectLoadStructureForExport(f.LoadStructureForExport)
s.ConnectSetupAndLoadForImport(f.setupAndLoadForImport)
s.ConnectResetSource(f.resetSource)

View File

@ -24,8 +24,8 @@ import (
"strings"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/keychain"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
@ -63,8 +63,8 @@ func (s *FrontendQt) loadAccounts() {
acc_info.SetUserID(user.ID())
acc_info.SetHostname(bridge.Host)
acc_info.SetPassword(user.GetBridgePassword())
acc_info.SetPortIMAP(s.preferences.GetInt(preferences.IMAPPortKey))
acc_info.SetPortSMTP(s.preferences.GetInt(preferences.SMTPPortKey))
acc_info.SetPortIMAP(s.settings.GetInt(settings.IMAPPortKey))
acc_info.SetPortSMTP(s.settings.GetInt(settings.SMTPPortKey))
// Set aliases.
acc_info.SetAliases(strings.Join(user.GetAddresses(), ";"))
@ -85,7 +85,7 @@ func (s *FrontendQt) clearCache() {
}
// Clearing data removes everything (db, preferences, ...)
// so everything has to be stopped and started again.
s.Qml.SetIsRestarting(true)
s.restarter.SetToRestart()
s.App.Quit()
}

View File

@ -38,13 +38,13 @@ import (
"github.com/ProtonMail/go-autostart"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/autoconfig"
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/ports"
@ -70,10 +70,10 @@ type FrontendQt struct {
buildVersion string
showWindowOnStart bool
panicHandler types.PanicHandler
config *config.Config
preferences *config.Preferences
locations *locations.Locations
settings *settings.Settings
eventListener listener.Listener
updates types.Updater
updater types.Updater
bridge types.Bridger
noEncConfirmator types.NoEncConfirmator
@ -94,21 +94,22 @@ type FrontendQt struct {
// expand userID when added
userIDAdded string
notifyHasNoKeychain bool
restarter types.Restarter
}
// New returns a new Qt frontendend for the bridge.
// New returns a new Qt frontend for the bridge.
func New(
version,
buildVersion string,
showWindowOnStart bool,
panicHandler types.PanicHandler,
config *config.Config,
preferences *config.Preferences,
locations *locations.Locations,
settings *settings.Settings,
eventListener listener.Listener,
updates types.Updater,
updater types.Updater,
bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator,
restarter types.Restarter,
) *FrontendQt {
prgName := "ProtonMail Bridge"
tmp := &FrontendQt{
@ -116,10 +117,10 @@ func New(
buildVersion: buildVersion,
showWindowOnStart: showWindowOnStart,
panicHandler: panicHandler,
config: config,
preferences: preferences,
locations: locations,
settings: settings,
eventListener: eventListener,
updates: updates,
updater: updater,
bridge: bridge,
noEncConfirmator: noEncConfirmator,
@ -130,6 +131,8 @@ func New(
DisplayName: prgName,
Exec: []string{"", "--no-window"},
},
restarter: restarter,
}
// Handle autostart if wanted.
@ -161,10 +164,7 @@ func (s *FrontendQt) InstanceExistAlert() {
// Loop function for Bridge interface.
//
// It runs QtExecute in main thread with no additional function.
func (s *FrontendQt) Loop(credentialsError error) (err error) {
if credentialsError != nil {
s.notifyHasNoKeychain = true
}
func (s *FrontendQt) Loop() (err error) {
go func() {
defer s.panicHandler.HandlePanic()
s.watchEvents()
@ -173,8 +173,14 @@ func (s *FrontendQt) Loop(credentialsError error) (err error) {
return err
}
func (s *FrontendQt) NotifyManualUpdate(update updater.VersionInfo) error {
// NOTE: Save the update somewhere so that it can be installed when user chooses "install now".
return nil
}
func (s *FrontendQt) watchEvents() {
errorCh := s.getEventChannel(events.ErrorEvent)
credentialsErrorCh := s.getEventChannel(events.CredentialsErrorEvent)
outgoingNoEncCh := s.getEventChannel(events.OutgoingNoEncEvent)
noActiveKeyForRecipientCh := s.getEventChannel(events.NoActiveKeyForRecipientEvent)
internetOffCh := s.getEventChannel(events.InternetOffEvent)
@ -193,6 +199,8 @@ func (s *FrontendQt) watchEvents() {
imapIssue := strings.Contains(errorDetails, "IMAP failed")
smtpIssue := strings.Contains(errorDetails, "SMTP failed")
s.Qml.NotifyPortIssue(imapIssue, smtpIssue)
case <-credentialsErrorCh:
s.Qml.NotifyHasNoKeychain()
case idAndSubject := <-outgoingNoEncCh:
idAndSubjectSlice := strings.SplitN(idAndSubject, ":", 2)
messageID := idAndSubjectSlice[0]
@ -207,7 +215,7 @@ func (s *FrontendQt) watchEvents() {
case <-secondInstanceCh:
s.Qml.ShowWindow()
case <-restartBridgeCh:
s.Qml.SetIsRestarting(true)
s.restarter.SetToRestart()
// watchEvents is started in parallel with the Qt app.
// If the event comes too early, app might not be ready yet.
if s.App != nil {
@ -267,10 +275,6 @@ func (s *FrontendQt) Start() (err error) {
return nil
}
func (s *FrontendQt) IsAppRestarting() bool {
return s.Qml.IsRestarting()
}
// InvMethod runs the function with name `method` defined in RootObject of the QML.
// Used for tests.
func (s *FrontendQt) InvMethod(method string) error {
@ -304,13 +308,13 @@ func (s *FrontendQt) qtExecute(Procedure func(*FrontendQt) error) error {
s.View.RootContext().SetContextProperty("go", s.Qml)
// Set first start flag.
s.Qml.SetIsFirstStart(s.preferences.GetBool(preferences.FirstStartGUIKey))
s.preferences.SetBool(preferences.FirstStartGUIKey, false)
s.Qml.SetIsFirstStart(s.settings.GetBool(settings.FirstStartGUIKey))
s.settings.SetBool(settings.FirstStartGUIKey, false)
// Check if it is first start after update (fresh version).
lastVersion := s.preferences.Get(preferences.LastVersionKey)
lastVersion := s.settings.Get(settings.LastVersionKey)
s.Qml.SetIsFreshVersion(lastVersion != "" && s.version != lastVersion)
s.preferences.Set(preferences.LastVersionKey, s.version)
s.settings.Set(settings.LastVersionKey, s.version)
// Add AccountsModel.
s.Accounts = NewAccountsModel(nil)
@ -339,27 +343,25 @@ func (s *FrontendQt) qtExecute(Procedure func(*FrontendQt) error) error {
s.Qml.SetIsAutoStart(false)
}
if s.preferences.GetBool(preferences.AllowProxyKey) {
if s.settings.GetBool(settings.AllowProxyKey) {
s.Qml.SetIsProxyAllowed(true)
} else {
s.Qml.SetIsProxyAllowed(false)
}
// Notify user about error during initialization.
if s.notifyHasNoKeychain {
s.Qml.NotifyHasNoKeychain()
}
s.eventListener.RetryEmit(events.TLSCertIssue)
s.eventListener.RetryEmit(events.ErrorEvent)
// Set reporting of outgoing email without encryption.
s.Qml.SetIsReportingOutgoingNoEnc(s.preferences.GetBool(preferences.ReportOutgoingNoEncKey))
s.Qml.SetIsReportingOutgoingNoEnc(s.settings.GetBool(settings.ReportOutgoingNoEncKey))
defaultIMAPPort, _ := strconv.Atoi(settings.DefaultIMAPPort)
defaultSMTPPort, _ := strconv.Atoi(settings.DefaultSMTPPort)
// IMAP/SMTP ports.
s.Qml.SetIsDefaultPort(
s.config.GetDefaultIMAPPort() == s.preferences.GetInt(preferences.IMAPPortKey) &&
s.config.GetDefaultSMTPPort() == s.preferences.GetInt(preferences.SMTPPortKey),
defaultIMAPPort == s.settings.GetInt(settings.IMAPPortKey) &&
defaultSMTPPort == s.settings.GetInt(settings.SMTPPortKey),
)
// Check QML is loaded properly.
@ -387,7 +389,12 @@ func (s *FrontendQt) qtExecute(Procedure func(*FrontendQt) error) error {
}
func (s *FrontendQt) openLogs() {
go open.Run(s.config.GetLogDir())
logsPath, err := s.locations.ProvideLogsPath()
if err != nil {
return
}
go open.Run(logsPath)
}
// Check version in separate goroutine to not block the GUI (avoid program not responding message).
@ -395,40 +402,20 @@ func (s *FrontendQt) isNewVersionAvailable(showMessage bool) {
go func() {
defer s.panicHandler.HandlePanic()
defer s.Qml.ProcessFinished()
isUpToDate, latestVersionInfo, err := s.updates.CheckIsUpToDate()
if err != nil {
log.Warn("Can not retrieve version info: ", err)
s.checkInternet()
return
}
s.Qml.SetConnectionStatus(true) // If we are here connection is ok.
if isUpToDate {
s.Qml.SetUpdateState("upToDate")
if showMessage {
s.Qml.NotifyVersionIsTheLatest()
}
return
}
s.Qml.SetNewversion(latestVersionInfo.Version)
s.Qml.SetChangelog(latestVersionInfo.ReleaseNotes)
s.Qml.SetBugfixes(latestVersionInfo.ReleaseFixedBugs)
s.Qml.SetLandingPage(latestVersionInfo.LandingPage)
s.Qml.SetDownloadLink(latestVersionInfo.GetDownloadLink())
s.Qml.ShowWindow()
s.Qml.SetUpdateState("oldVersion")
}()
}
func (s *FrontendQt) openLicenseFile() {
go open.Run(s.config.GetLicenseFilePath())
go open.Run(s.locations.GetLicenseFilePath())
}
func (s *FrontendQt) getLocalVersionInfo() {
defer s.Qml.ProcessFinished()
localVersion := s.updates.GetLocalVersion()
s.Qml.SetNewversion(localVersion.Version)
s.Qml.SetChangelog(localVersion.ReleaseNotes)
s.Qml.SetBugfixes(localVersion.ReleaseFixedBugs)
// NOTE: Fix this.
}
func (s *FrontendQt) sendBug(description, client, address string) (isOK bool) {
@ -465,16 +452,16 @@ func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) {
return
}
imapPort := s.preferences.GetInt(preferences.IMAPPortKey)
imapPort := s.settings.GetInt(settings.IMAPPortKey)
imapSSL := false
smtpPort := s.preferences.GetInt(preferences.SMTPPortKey)
smtpSSL := s.preferences.GetBool(preferences.SMTPSSLKey)
smtpPort := s.settings.GetInt(settings.SMTPPortKey)
smtpSSL := s.settings.GetBool(settings.SMTPSSLKey)
// If configuring apple mail for Catalina or newer, users should use SSL.
doRestart := false
if !smtpSSL && useragent.IsCatalinaOrNewer() {
smtpSSL = true
s.preferences.SetBool(preferences.SMTPSSLKey, true)
s.settings.SetBool(settings.SMTPSSLKey, true)
log.Warn("Detected Catalina or newer with bad SMTP SSL settings, now using SSL, bridge needs to restart")
doRestart = true
}
@ -489,7 +476,7 @@ func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) {
if doRestart {
time.Sleep(2 * time.Second)
s.Qml.SetIsRestarting(true)
s.restarter.SetToRestart()
s.App.Quit()
}
return
@ -517,23 +504,23 @@ func (s *FrontendQt) toggleAutoStart() {
func (s *FrontendQt) toggleAllowProxy() {
defer s.Qml.ProcessFinished()
if s.preferences.GetBool(preferences.AllowProxyKey) {
s.preferences.SetBool(preferences.AllowProxyKey, false)
if s.settings.GetBool(settings.AllowProxyKey) {
s.settings.SetBool(settings.AllowProxyKey, false)
s.bridge.DisallowProxy()
s.Qml.SetIsProxyAllowed(false)
} else {
s.preferences.SetBool(preferences.AllowProxyKey, true)
s.settings.SetBool(settings.AllowProxyKey, true)
s.bridge.AllowProxy()
s.Qml.SetIsProxyAllowed(true)
}
}
func (s *FrontendQt) getIMAPPort() string {
return s.preferences.Get(preferences.IMAPPortKey)
return s.settings.Get(settings.IMAPPortKey)
}
func (s *FrontendQt) getSMTPPort() string {
return s.preferences.Get(preferences.SMTPPortKey)
return s.settings.Get(settings.SMTPPortKey)
}
// Return 0 -- port is free to use for server.
@ -550,13 +537,13 @@ func (s *FrontendQt) isPortOpen(portStr string) int {
}
func (s *FrontendQt) setPortsAndSecurity(imapPort, smtpPort string, useSTARTTLSforSMTP bool) {
s.preferences.Set(preferences.IMAPPortKey, imapPort)
s.preferences.Set(preferences.SMTPPortKey, smtpPort)
s.preferences.SetBool(preferences.SMTPSSLKey, !useSTARTTLSforSMTP)
s.settings.Set(settings.IMAPPortKey, imapPort)
s.settings.Set(settings.SMTPPortKey, smtpPort)
s.settings.SetBool(settings.SMTPSSLKey, !useSTARTTLSforSMTP)
}
func (s *FrontendQt) isSMTPSTARTTLS() bool {
return !s.preferences.GetBool(preferences.SMTPSSLKey)
return !s.settings.GetBool(settings.SMTPSSLKey)
}
func (s *FrontendQt) checkInternet() {
@ -594,7 +581,7 @@ func (s *FrontendQt) autostartError(err error) {
func (s *FrontendQt) toggleIsReportingOutgoingNoEnc() {
shouldReport := !s.Qml.IsReportingOutgoingNoEnc()
s.preferences.SetBool(preferences.ReportOutgoingNoEncKey, shouldReport)
s.settings.SetBool(settings.ReportOutgoingNoEncKey, shouldReport)
s.Qml.SetIsReportingOutgoingNoEnc(shouldReport)
}
@ -608,30 +595,5 @@ func (s *FrontendQt) saveOutgoingNoEncPopupCoord(x, y float32) {
}
func (s *FrontendQt) StartUpdate() {
progress := make(chan updates.Progress)
go func() { // Update progress in QML.
defer s.panicHandler.HandlePanic()
for current := range progress {
s.Qml.SetProgress(current.Processed)
s.Qml.SetProgressDescription(strconv.Itoa(current.Description))
// Error happend
if current.Err != nil {
log.Error("update progress: ", current.Err)
s.Qml.UpdateFinished(true)
return
}
// Finished everything OK.
if current.Description >= updates.InfoQuitApp {
s.Qml.UpdateFinished(false)
time.Sleep(3 * time.Second) // Just notify.
s.Qml.SetIsRestarting(current.Description == updates.InfoRestartApp)
s.App.Quit()
return
}
}
}()
go func() {
defer s.panicHandler.HandlePanic()
s.updates.StartUpgrade(progress)
}()
// NOTE: Fix this.
}

View File

@ -23,8 +23,10 @@ import (
"fmt"
"net/http"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/sirupsen/logrus"
)
@ -33,7 +35,7 @@ var log = logrus.WithField("pkg", "frontend-nogui") //nolint[gochecknoglobals]
type FrontendHeadless struct{}
func (s *FrontendHeadless) Loop(credentialsError error) error {
func (s *FrontendHeadless) Loop() error {
log.Info("Check status on localhost:8081")
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Bridge is running")
@ -41,20 +43,25 @@ func (s *FrontendHeadless) Loop(credentialsError error) error {
return http.ListenAndServe(":8081", nil)
}
func (s *FrontendHeadless) NotifyManualUpdate(update updater.VersionInfo) error {
// NOTE: Save the update somewhere so that it can be installed when user chooses "install now".
return nil
}
func (s *FrontendHeadless) InstanceExistAlert() {}
func (s *FrontendHeadless) IsAppRestarting() bool { return false }
func New(
version,
buildVersion string,
showWindowOnStart bool,
panicHandler types.PanicHandler,
config *config.Config,
preferences *config.Preferences,
locations *locations.Locations,
settings *settings.Settings,
eventListener listener.Listener,
updates types.Updater,
updater types.Updater,
bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator,
restarter types.Restarter,
) *FrontendHeadless {
return &FrontendHeadless{}
}

View File

@ -41,7 +41,6 @@ type GoQMLInterface struct {
_ bool `property:"isShownOnStart"`
_ bool `property:"isFirstStart"`
_ bool `property:"isFreshVersion"`
_ bool `property:"isRestarting"`
_ bool `property:"isConnectionOK"`
_ bool `property:"isDefaultPort"`
@ -70,6 +69,8 @@ type GoQMLInterface struct {
_ func(updateState string) `signal:"setUpdateState"`
_ func() `slot:"checkInternet"`
_ func() `slot:"setToRestart"`
_ func(systX, systY, systW, systH int) `signal:"toggleMainWin"`
_ func() `signal:"processFinished"`
@ -178,7 +179,6 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
s.ConnectSwitchAddressMode(f.switchAddressModeUser)
s.SetGoos(runtime.GOOS)
s.SetIsRestarting(false)
s.SetProgramTitle(f.programName)
s.ConnectGetBackendVersion(func() string {
@ -187,6 +187,8 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
s.ConnectCheckInternet(f.checkInternet)
s.ConnectSetToRestart(f.restarter.SetToRestart)
s.ConnectToggleIsReportingOutgoingNoEnc(f.toggleIsReportingOutgoingNoEnc)
s.ConnectShouldSendAnswer(f.shouldSendAnswer)
s.ConnectSaveOutgoingNoEncPopupCoord(f.saveOutgoingNoEncPopupCoord)

View File

@ -22,7 +22,7 @@ import (
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
@ -31,18 +31,19 @@ type PanicHandler interface {
HandlePanic()
}
// Updater is an interface for handling Bridge upgrades.
type Updater interface {
CheckIsUpToDate() (isUpToDate bool, latestVersion updates.VersionInfo, err error)
GetDownloadLink() string
GetLocalVersion() updates.VersionInfo
StartUpgrade(currentStatus chan<- updates.Progress)
// Restarter allows the app to set itself to restart next time it is closed.
type Restarter interface {
SetToRestart()
}
type NoEncConfirmator interface {
ConfirmNoEncryption(string, bool)
}
type Updater interface {
InstallUpdate(updater.VersionInfo) error
}
// UserManager is an interface of users needed by frontend.
type UserManager interface {
Login(username, password string) (pmapi.Client, *pmapi.Auth, error)

View File

@ -55,11 +55,11 @@ type imapBackend struct {
func NewIMAPBackend(
panicHandler panicHandler,
eventListener listener.Listener,
cfg configProvider,
cache cacheProvider,
bridge *bridge.Bridge,
) *imapBackend { //nolint[golint]
bridgeWrap := newBridgeWrap(bridge)
backend := newIMAPBackend(panicHandler, cfg, bridgeWrap, eventListener)
backend := newIMAPBackend(panicHandler, cache, bridgeWrap, eventListener)
go backend.monitorDisconnectedUsers()
@ -68,7 +68,7 @@ func NewIMAPBackend(
func newIMAPBackend(
panicHandler panicHandler,
cfg configProvider,
cache cacheProvider,
bridge bridger,
eventListener listener.Listener,
) *imapBackend {
@ -81,7 +81,7 @@ func newIMAPBackend(
users: map[string]*imapUser{},
usersLocker: &sync.Mutex{},
imapCachePath: cfg.GetIMAPCachePath(),
imapCachePath: cache.GetIMAPCachePath(),
imapCacheLock: &sync.RWMutex{},
updatesBlocking: map[string]bool{},

View File

@ -23,8 +23,7 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
type configProvider interface {
GetEventsPath() string
type cacheProvider interface {
GetDBDir() string
GetIMAPCachePath() string
}

View File

@ -58,6 +58,13 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
s.ErrorLog = newServerErrorLogger("server-imap")
s.AutoLogout = 30 * time.Minute
if debugServer {
fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
}
serverID := imapid.ID{
imapid.FieldName: "ProtonMail Bridge",
imapid.FieldVendor: "Proton Technologies AG",

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./credits.sh at Mon Dec 28 02:39:43 PM CET 2020. DO NOT EDIT.
// Code generated by ./credits.sh at Mon Jan 4 03:19:07 PM CET 2021. DO NOT EDIT.
package importexport
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/Masterminds/semver/v3;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/PuerkitoBio/goquery;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/Masterminds/semver/v3;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/PuerkitoBio/goquery;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli/v2;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

@ -36,23 +36,27 @@ var (
type ImportExport struct {
*users.Users
config Configer
locations Locator
cache Cacher
panicHandler users.PanicHandler
clientManager users.ClientManager
}
func New(
config Configer,
locations Locator,
cache Cacher,
panicHandler users.PanicHandler,
eventListener listener.Listener,
clientManager users.ClientManager,
credStorer users.CredentialsStorer,
) *ImportExport {
u := users.New(config, panicHandler, eventListener, clientManager, credStorer, &storeFactory{}, false)
u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, &storeFactory{}, false)
return &ImportExport{
Users: u,
config: config,
locations: locations,
cache: cache,
panicHandler: panicHandler,
clientManager: clientManager,
}
@ -120,7 +124,11 @@ func (ie *ImportExport) GetLocalImporter(address, path string) (*transfer.Transf
if err != nil {
return nil, err
}
return transfer.New(ie.panicHandler, newImportMetricsManager(ie), ie.config.GetLogDir(), ie.config.GetTransferDir(), source, target)
logsPath, err := ie.locations.ProvideLogsPath()
if err != nil {
return nil, err
}
return transfer.New(ie.panicHandler, newImportMetricsManager(ie), logsPath, ie.cache.GetTransferDir(), source, target)
}
// GetRemoteImporter returns transferrer from remote IMAP to ProtonMail account.
@ -133,7 +141,11 @@ func (ie *ImportExport) GetRemoteImporter(address, username, password, host, por
if err != nil {
return nil, err
}
return transfer.New(ie.panicHandler, newImportMetricsManager(ie), ie.config.GetLogDir(), ie.config.GetTransferDir(), source, target)
logsPath, err := ie.locations.ProvideLogsPath()
if err != nil {
return nil, err
}
return transfer.New(ie.panicHandler, newImportMetricsManager(ie), logsPath, ie.cache.GetTransferDir(), source, target)
}
// GetEMLExporter returns transferrer from ProtonMail account to local EML structure.
@ -143,7 +155,11 @@ func (ie *ImportExport) GetEMLExporter(address, path string) (*transfer.Transfer
return nil, err
}
target := transfer.NewEMLProvider(path)
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), ie.config.GetLogDir(), ie.config.GetTransferDir(), source, target)
logsPath, err := ie.locations.ProvideLogsPath()
if err != nil {
return nil, err
}
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), logsPath, ie.cache.GetTransferDir(), source, target)
}
// GetMBOXExporter returns transferrer from ProtonMail account to local MBOX structure.
@ -153,7 +169,11 @@ func (ie *ImportExport) GetMBOXExporter(address, path string) (*transfer.Transfe
return nil, err
}
target := transfer.NewMBOXProvider(path)
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), ie.config.GetLogDir(), ie.config.GetTransferDir(), source, target)
logsPath, err := ie.locations.ProvideLogsPath()
if err != nil {
return nil, err
}
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), logsPath, ie.cache.GetTransferDir(), source, target)
}
func (ie *ImportExport) getPMAPIProvider(address string) (*transfer.PMAPIProvider, error) {
@ -167,5 +187,5 @@ func (ie *ImportExport) getPMAPIProvider(address string) (*transfer.PMAPIProvide
log.WithError(err).Info("Address does not exist, using all addresses")
}
return transfer.NewPMAPIProvider(ie.config.GetAPIConfig(), ie.clientManager, user.ID(), addressID)
return transfer.NewPMAPIProvider(ie.clientManager, user.ID(), addressID)
}

View File

@ -17,11 +17,11 @@
package importexport
import "github.com/ProtonMail/proton-bridge/internal/users"
type Locator interface {
ProvideLogsPath() (string, error)
Clear() error
}
type Configer interface {
users.Configer
GetLogDir() string
type Cacher interface {
GetTransferDir() string
}

View File

@ -0,0 +1,191 @@
// 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 locations implements a type that provides cross-platform access to
// standard filesystem locations, including config, cache and log directories.
package locations
import (
"os"
"path/filepath"
"runtime"
"github.com/ProtonMail/proton-bridge/pkg/files"
"github.com/sirupsen/logrus"
)
// Locations provides cross-platform access to standard locations.
// On linux:
// - settings: ~/.config/protonmail/<app>
// - logs: ~/.cache/protonmail/<app>/logs
// - cache: ~/.cache/protonmail/<app>/cache
// - updates: ~/.cache/protonmail/<app>/updates
// - lockfile: ~/.cache/protonmail/<app>/<app>.lock
type Locations struct {
userConfig, userCache string
configName string
}
type appDirsProvider interface {
UserConfig() string
UserCache() string
}
func New(appDirs appDirsProvider, configName string) *Locations {
return &Locations{
userConfig: appDirs.UserConfig(),
userCache: appDirs.UserCache(),
configName: configName,
}
}
// GetLockFile returns the path to the lock file (e.g. ~/.cache/<company>/<app>/<app>.lock).
func (l *Locations) GetLockFile() string {
return filepath.Join(l.userCache, l.configName+".lock")
}
// GetLicenseFilePath returns path to liense file.
func (l *Locations) GetLicenseFilePath() string {
path := l.getLicenseFilePath()
logrus.WithField("path", path).Info("License file path")
return path
}
func (l *Locations) getLicenseFilePath() string {
// User can install app to different location, or user can run it
// directly from the package without installation, or it could be
// automatically updated (app started from differenet location).
// For all those cases, first let's check LICENSE next to the binary.
path := filepath.Join(filepath.Dir(os.Args[0]), "LICENSE")
if _, err := os.Stat(path); err == nil {
return path
}
switch runtime.GOOS {
case "linux":
appName := l.configName
if l.configName == "importExport" {
appName = "import-export"
}
// Most Linux distributions.
path := "/usr/share/doc/protonmail/" + appName + "/LICENSE"
if _, err := os.Stat(path); err == nil {
return path
}
// Arch distributions.
return "/usr/share/licenses/protonmail-" + appName + "/LICENSE"
case "darwin": //nolint[goconst]
path := filepath.Join(filepath.Dir(os.Args[0]), "..", "Resources", "LICENSE")
if _, err := os.Stat(path); err == nil {
return path
}
appName := "ProtonMail Bridge.app"
if l.configName == "importExport" {
appName = "ProtonMail Import-Export.app"
}
return "/Applications/" + appName + "/Contents/Resources/LICENSE"
case "windows":
path := filepath.Join(filepath.Dir(os.Args[0]), "LICENSE.txt")
if _, err := os.Stat(path); err == nil {
return path
}
// This should not happen, Windows should be handled by relative
// location to the binary above. This is just fallback which may
// or may not work, depends where user installed the app and how
// user started the app.
return filepath.FromSlash("C:/Program Files/Proton Technologies AG/ProtonMail Bridge/LICENSE.txt")
}
return ""
}
// ProvideSettingsPath returns a location for user settings (e.g. ~/.config/<company>/<app>).
// It creates it if it doesn't already exist.
func (l *Locations) ProvideSettingsPath() (string, error) {
if err := os.MkdirAll(l.getSettingsPath(), 0700); err != nil {
return "", err
}
return l.getSettingsPath(), nil
}
// ProvideLogsPath returns a location for user logs (e.g. ~/.cache/<company>/<app>/logs).
// It creates it if it doesn't already exist.
func (l *Locations) ProvideLogsPath() (string, error) {
if err := os.MkdirAll(l.getLogsPath(), 0700); err != nil {
return "", err
}
return l.getLogsPath(), nil
}
// ProvideCachePath returns a location for user cache dirs (e.g. ~/.cache/<company>/<app>/cache).
// It creates it if it doesn't already exist.
func (l *Locations) ProvideCachePath() (string, error) {
if err := os.MkdirAll(l.getCachePath(), 0700); err != nil {
return "", err
}
return l.getCachePath(), nil
}
// ProvideUpdatesPath returns a location for update files (e.g. ~/.cache/<company>/<app>/updates).
// It creates it if it doesn't already exist.
func (l *Locations) ProvideUpdatesPath() (string, error) {
if err := os.MkdirAll(l.getUpdatesPath(), 0700); err != nil {
return "", err
}
return l.getUpdatesPath(), nil
}
func (l *Locations) getSettingsPath() string {
return l.userConfig
}
func (l *Locations) getLogsPath() string {
return filepath.Join(l.userCache, "logs")
}
func (l *Locations) getCachePath() string {
return filepath.Join(l.userCache, "cache")
}
func (l *Locations) getUpdatesPath() string {
return filepath.Join(l.userCache, "updates")
}
// Clear removes everything except the lock file.
func (l *Locations) Clear() error {
return files.Remove(
l.getSettingsPath(),
l.getLogsPath(),
l.getCachePath(),
l.getUpdatesPath(),
).Do()
}
// Clean removes any unexpected files from the app cache folder
// while leaving files in the standard locations untouched.
func (l *Locations) Clean() error {
return files.Remove(l.userCache).Except(
l.GetLockFile(),
l.getLogsPath(),
l.getCachePath(),
l.getUpdatesPath(),
).Do()
}

View File

@ -0,0 +1,152 @@
// 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 locations
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type fakeAppDirs struct {
configDir, cacheDir string
}
func (dirs *fakeAppDirs) UserConfig() string {
return dirs.configDir
}
func (dirs *fakeAppDirs) UserCache() string {
return dirs.cacheDir
}
func TestClearRemovesEverythingExceptLockFile(t *testing.T) {
l := newTestLocations(t)
assert.NoError(t, l.Clear())
assert.FileExists(t, l.GetLockFile())
assert.NoDirExists(t, l.getSettingsPath())
assert.NoDirExists(t, l.getLogsPath())
assert.NoDirExists(t, l.getCachePath())
assert.NoDirExists(t, l.getUpdatesPath())
}
func TestCleanLeavesStandardLocationsUntouched(t *testing.T) {
l := newTestLocations(t)
createFilesInDir(t, l.getLogsPath(),
"log1.txt",
"log2.txt",
)
assert.NoError(t, l.Clean())
assert.FileExists(t, l.GetLockFile())
assert.DirExists(t, l.getSettingsPath())
assert.DirExists(t, l.getLogsPath())
assert.FileExists(t, filepath.Join(l.getLogsPath(), "log1.txt"))
assert.FileExists(t, filepath.Join(l.getLogsPath(), "log2.txt"))
assert.DirExists(t, l.getCachePath())
assert.DirExists(t, l.getUpdatesPath())
}
func TestCleanRemovesUnexpectedFilesAndFolders(t *testing.T) {
l := newTestLocations(t)
createFilesInDir(t, l.userCache,
"unexpected1.txt",
"dir1/unexpected2.txt",
"dir1/unexpected3.txt",
"dir2/unexpected4.txt",
"dir3/dir4/unexpected5.txt",
)
require.FileExists(t, filepath.Join(l.userCache, "unexpected1.txt"))
require.FileExists(t, filepath.Join(l.userCache, "dir1", "unexpected2.txt"))
require.FileExists(t, filepath.Join(l.userCache, "dir1", "unexpected3.txt"))
require.FileExists(t, filepath.Join(l.userCache, "dir2", "unexpected4.txt"))
require.FileExists(t, filepath.Join(l.userCache, "dir3", "dir4", "unexpected5.txt"))
assert.NoError(t, l.Clean())
assert.FileExists(t, l.GetLockFile())
assert.DirExists(t, l.getSettingsPath())
assert.DirExists(t, l.getLogsPath())
assert.DirExists(t, l.getCachePath())
assert.DirExists(t, l.getUpdatesPath())
assert.NoFileExists(t, filepath.Join(l.userCache, "unexpected1.txt"))
assert.NoFileExists(t, filepath.Join(l.userCache, "dir1", "unexpected2.txt"))
assert.NoFileExists(t, filepath.Join(l.userCache, "dir1", "unexpected3.txt"))
assert.NoFileExists(t, filepath.Join(l.userCache, "dir2", "unexpected4.txt"))
assert.NoFileExists(t, filepath.Join(l.userCache, "dir3", "dir4", "unexpected5.txt"))
}
func newFakeAppDirs(t *testing.T) *fakeAppDirs {
configDir, err := ioutil.TempDir("", "test-locations-config")
require.NoError(t, err)
cacheDir, err := ioutil.TempDir("", "test-locations-cache")
require.NoError(t, err)
return &fakeAppDirs{
configDir: configDir,
cacheDir: cacheDir,
}
}
func newTestLocations(t *testing.T) *Locations {
l := New(newFakeAppDirs(t), "configName")
lock := l.GetLockFile()
createFilesInDir(t, "", lock)
require.FileExists(t, lock)
settings, err := l.ProvideSettingsPath()
require.NoError(t, err)
require.DirExists(t, settings)
logs, err := l.ProvideLogsPath()
require.NoError(t, err)
require.DirExists(t, logs)
cache, err := l.ProvideCachePath()
require.NoError(t, err)
require.DirExists(t, cache)
updates, err := l.ProvideUpdatesPath()
require.NoError(t, err)
require.DirExists(t, updates)
return l
}
func createFilesInDir(t *testing.T, dir string, files ...string) {
for _, target := range files {
require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(dir, target)), 0700))
f, err := os.Create(filepath.Join(dir, target))
require.NoError(t, err)
require.NoError(t, f.Close())
}
}

85
internal/logging/clear.go Normal file
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 logging
import (
"io/ioutil"
"os"
"path/filepath"
"sort"
"github.com/sirupsen/logrus"
)
func clearLogs(logDir string, maxLogs int) error {
files, err := ioutil.ReadDir(logDir)
if err != nil {
return err
}
var logsWithPrefix []string
var crashesWithPrefix []string
for _, file := range files {
if matchLogName(file.Name()) {
if matchStackTraceName(file.Name()) {
crashesWithPrefix = append(crashesWithPrefix, file.Name())
} else {
logsWithPrefix = append(logsWithPrefix, file.Name())
}
} else {
// Older versions of Bridge stored logs in subfolders for each version.
// That also has to be cleared and the functionality can be removed after some time.
if file.IsDir() {
if err := clearLogs(filepath.Join(logDir, file.Name()), maxLogs); err != nil {
return err
}
} else {
removeLog(logDir, file.Name())
}
}
}
removeOldLogs(logDir, logsWithPrefix, maxLogs)
removeOldLogs(logDir, crashesWithPrefix, maxLogs)
return nil
}
func removeOldLogs(logDir string, filenames []string, maxLogs int) {
count := len(filenames)
if count <= maxLogs {
return
}
sort.Strings(filenames) // Sorted by timestamp: oldest first.
for _, filename := range filenames[:count-maxLogs] {
removeLog(logDir, filename)
}
}
func removeLog(logDir, filename string) {
// We need to be sure to delete only log files.
// Directory with logs can also contain other files.
if !matchLogName(filename) {
return
}
if err := os.RemoveAll(filepath.Join(logDir, filename)); err != nil {
logrus.WithError(err).Error("Failed to remove old logs")
}
}

62
internal/logging/crash.go Normal file
View File

@ -0,0 +1,62 @@
// 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 logging
import (
"fmt"
"os"
"path/filepath"
"regexp"
"runtime/pprof"
"time"
"github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/internal/crash"
"github.com/sirupsen/logrus"
)
func DumpStackTrace(logsPath string) crash.RecoveryAction {
return func(r interface{}) error {
file := filepath.Join(logsPath, getStackTraceName(constants.Version, constants.Revision))
f, err := os.OpenFile(file, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
if err != nil {
return err
}
if _, err := f.WriteString(fmt.Sprintf("Recover: %v", r)); err != nil {
return err
}
if err := pprof.Lookup("goroutine").WriteTo(f, 2); err != nil {
return err
}
logrus.WithField("file", file).Warn("Saved crash report")
return nil
}
}
func getStackTraceName(version, revision string) string {
return fmt.Sprintf("v%v_%v_crash_%v.log", version, revision, time.Now().Unix())
}
func matchStackTraceName(name string) bool {
return regexp.MustCompile(`^v.*_crash_.*\.log$`).MatchString(name)
}

View File

@ -0,0 +1,90 @@
// 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 logging
import (
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"time"
"github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/sirupsen/logrus"
"github.com/sirupsen/logrus/hooks/writer"
)
const (
// MaxLogSize defines the maximum log size we should permit.
// Zendesk has a file size limit of 20MB. When the last N log files are zipped,
// it should fit under 20MB. So here we permit up to 10MB (most files are a few hundred kB).
MaxLogSize = 10 * 2 << 20
// MaxLogs defines how many old log files should be kept.
MaxLogs = 3
)
func Init(logsPath string) error {
logrus.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: time.StampMilli,
})
rotator, err := NewRotator(MaxLogSize, func() (io.WriteCloser, error) {
if err := clearLogs(logsPath, MaxLogs); err != nil {
return nil, err
}
return os.Create(filepath.Join(logsPath, getLogName(constants.Version, constants.Revision)))
})
if err != nil {
return err
}
logrus.SetOutput(rotator)
logrus.AddHook(&writer.Hook{
Writer: os.Stderr,
LogLevels: []logrus.Level{
logrus.PanicLevel,
logrus.FatalLevel,
logrus.ErrorLevel,
},
})
return nil
}
func SetLevel(level string) {
if lvl, err := logrus.ParseLevel(level); err == nil {
logrus.SetLevel(lvl)
}
if logrus.GetLevel() == logrus.DebugLevel || logrus.GetLevel() == logrus.TraceLevel {
logrus.SetOutput(os.Stderr)
}
}
func getLogName(version, revision string) string {
return fmt.Sprintf("v%v_%v_%v.log", version, revision, time.Now().Unix())
}
func matchLogName(name string) bool {
return regexp.MustCompile(`^v.*\.log$`).MatchString(name)
}

View File

@ -0,0 +1,69 @@
// 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 logging
import (
"io/ioutil"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
// TestClearLogs tests that cearLogs removes only bridge old log files keeping last three of them.
func TestClearLogs(t *testing.T) {
dir, err := ioutil.TempDir("", "clear-logs-test")
require.NoError(t, err)
require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "other.log"), []byte("Hello"), 0755))
require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "v1_10.log"), []byte("Hello"), 0755))
require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "v1_11.log"), []byte("Hello"), 0755))
require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "v2_12.log"), []byte("Hello"), 0755))
require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "v2_13.log"), []byte("Hello"), 0755))
require.NoError(t, clearLogs(dir, 3))
checkFileNames(t, dir, []string{
"other.log",
"v1_11.log",
"v2_12.log",
"v2_13.log",
})
}
func checkFileNames(t *testing.T, dir string, expectedFileNames []string) {
fileNames := getFileNames(t, dir)
require.Equal(t, expectedFileNames, fileNames)
}
func getFileNames(t *testing.T, dir string) []string {
files, err := ioutil.ReadDir(dir)
require.NoError(t, err)
fileNames := []string{}
for _, file := range files {
fileNames = append(fileNames, file.Name())
if file.IsDir() {
subDir := filepath.Join(dir, file.Name())
subFileNames := getFileNames(t, subDir)
for _, subFileName := range subFileNames {
fileNames = append(fileNames, file.Name()+"/"+subFileName)
}
}
}
return fileNames
}

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/>.
package logging
import "io"
type Rotator struct {
getFile FileProvider
wc io.WriteCloser
size int
maxSize int
}
type FileProvider func() (io.WriteCloser, error)
func NewRotator(maxSize int, getFile FileProvider) (*Rotator, error) {
r := &Rotator{
getFile: getFile,
maxSize: maxSize,
}
if err := r.rotate(); err != nil {
return nil, err
}
return r, nil
}
func (r *Rotator) Write(p []byte) (int, error) {
if r.size+len(p) > r.maxSize {
if err := r.rotate(); err != nil {
return 0, err
}
}
n, err := r.wc.Write(p)
if err != nil {
return n, err
}
r.size += n
return n, nil
}
func (r *Rotator) rotate() error {
if r.wc != nil {
_ = r.wc.Close()
}
wc, err := r.getFile()
if err != nil {
return err
}
r.wc = wc
r.size = 0
return nil
}

View File

@ -0,0 +1,131 @@
// 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 logging
import (
"bytes"
"io"
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type WriteCloser struct {
bytes.Buffer
}
func (c *WriteCloser) Close() error {
return nil
}
func TestRotator(t *testing.T) {
n := 0
getFile := func() (io.WriteCloser, error) {
n++
return &WriteCloser{}, nil
}
r, err := NewRotator(10, getFile)
require.NoError(t, err)
_, err = r.Write([]byte("12345"))
require.NoError(t, err)
assert.Equal(t, 1, n)
_, err = r.Write([]byte("12345"))
require.NoError(t, err)
assert.Equal(t, 1, n)
_, err = r.Write([]byte("01234"))
require.NoError(t, err)
assert.Equal(t, 2, n)
_, err = r.Write([]byte("01234"))
require.NoError(t, err)
assert.Equal(t, 2, n)
_, err = r.Write([]byte("01234"))
require.NoError(t, err)
assert.Equal(t, 3, n)
_, err = r.Write([]byte("01234"))
require.NoError(t, err)
assert.Equal(t, 3, n)
_, err = r.Write([]byte("01234"))
require.NoError(t, err)
assert.Equal(t, 4, n)
}
func BenchmarkRotateRAMFile(b *testing.B) {
dir, err := ioutil.TempDir("", "rotate-benchmark")
require.NoError(b, err)
defer os.RemoveAll(dir) // nolint[errcheck]
benchRotate(b, MaxLogSize, getTestFile(b, dir, MaxLogSize-1))
}
func BenchmarkRotateDiskFile(b *testing.B) {
cache, err := os.UserCacheDir()
require.NoError(b, err)
dir, err := ioutil.TempDir(cache, "rotate-benchmark")
require.NoError(b, err)
defer os.RemoveAll(dir) // nolint[errcheck]
benchRotate(b, MaxLogSize, getTestFile(b, dir, MaxLogSize-1))
}
func benchRotate(b *testing.B, logSize int, getFile func() (io.WriteCloser, error)) {
r, err := NewRotator(logSize, getFile)
require.NoError(b, err)
for n := 0; n < b.N; n++ {
require.NoError(b, r.rotate())
f, ok := r.wc.(*os.File)
require.True(b, ok)
require.NoError(b, os.Remove(f.Name()))
}
}
func getTestFile(b *testing.B, dir string, length int) func() (io.WriteCloser, error) {
return func() (io.WriteCloser, error) {
b.StopTimer()
defer b.StartTimer()
f, err := ioutil.TempFile(dir, "log")
if err != nil {
return nil, err
}
if _, err := f.Write(make([]byte, length)); err != nil {
return nil, err
}
if err := f.Sync(); err != nil {
return nil, err
}
return f, nil
}
}

View File

@ -22,8 +22,7 @@ import (
"time"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/pkg/confirmer"
"github.com/ProtonMail/proton-bridge/pkg/listener"
goSMTPBackend "github.com/emersion/go-smtp"
@ -35,10 +34,14 @@ type panicHandler interface {
HandlePanic()
}
type settingsProvider interface {
GetBool(string) bool
}
type smtpBackend struct {
panicHandler panicHandler
eventListener listener.Listener
preferences *config.Preferences
settings settingsProvider
bridge bridger
confirmer *confirmer.Confirmer
sendRecorder *sendRecorder
@ -48,22 +51,22 @@ type smtpBackend struct {
func NewSMTPBackend(
panicHandler panicHandler,
eventListener listener.Listener,
preferences *config.Preferences,
settings settingsProvider,
bridge *bridge.Bridge,
) *smtpBackend { //nolint[golint]
return newSMTPBackend(panicHandler, eventListener, preferences, newBridgeWrap(bridge))
return newSMTPBackend(panicHandler, eventListener, settings, newBridgeWrap(bridge))
}
func newSMTPBackend(
panicHandler panicHandler,
eventListener listener.Listener,
preferences *config.Preferences,
settings settingsProvider,
bridge bridger,
) *smtpBackend {
return &smtpBackend{
panicHandler: panicHandler,
eventListener: eventListener,
preferences: preferences,
settings: settings,
bridge: bridge,
confirmer: confirmer.New(),
sendRecorder: newSendRecorder(),
@ -109,7 +112,7 @@ func (sb *smtpBackend) AnonymousLogin(_ *goSMTPBackend.ConnectionState) (goSMTPB
}
func (sb *smtpBackend) shouldReportOutgoingNoEnc() bool {
return sb.preferences.GetBool(preferences.ReportOutgoingNoEncKey)
return sb.settings.GetBool(settings.ReportOutgoingNoEncKey)
}
func (sb *smtpBackend) ConfirmNoEncryption(messageID string, shouldSend bool) {

View File

@ -43,6 +43,13 @@ func NewSMTPServer(debug bool, port int, useSSL bool, tls *tls.Config, smtpBacke
s.Domain = bridge.Host
s.AllowInsecureAuth = true
if debug {
fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
}
if debug {
s.Debug = logrus.
WithField("pkg", "smtp/server").

View File

@ -24,7 +24,7 @@ import (
"time"
imapID "github.com/ProtonMail/go-imap-id"
"github.com/ProtonMail/proton-bridge/pkg/constants"
"github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap"
imapClient "github.com/emersion/go-imap/client"

View File

@ -27,7 +27,6 @@ import (
// PMAPIProvider implements import and export to/from ProtonMail server.
type PMAPIProvider struct {
clientConfig *pmapi.ClientConfig
clientManager ClientManager
userID string
addressID string
@ -40,9 +39,8 @@ type PMAPIProvider struct {
}
// NewPMAPIProvider returns new PMAPIProvider.
func NewPMAPIProvider(config *pmapi.ClientConfig, clientManager ClientManager, userID, addressID string) (*PMAPIProvider, error) {
func NewPMAPIProvider(clientManager ClientManager, userID, addressID string) (*PMAPIProvider, error) {
provider := &PMAPIProvider{
clientConfig: config,
clientManager: clientManager,
userID: userID,
addressID: addressID,

View File

@ -26,7 +26,6 @@ import (
pkgMessage "github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/sentry"
"github.com/pkg/errors"
)
@ -247,22 +246,6 @@ func (p *PMAPIProvider) generateImportMsgReq(rules transferRules, progress *Prog
func (p *PMAPIProvider) parseMessage(msg Message) (m *pmapi.Message, r []io.Reader, err error) {
p.timeIt.start("parse", msg.ID)
defer p.timeIt.stop("parse", msg.ID)
// Old message parser is panicking in some cases.
// Instead of crashing we try to convert to regular error.
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic while parse: %v", r)
if sentryErr := sentry.ReportSentryCrash(
p.clientConfig.ClientID,
p.clientConfig.AppVersion,
p.clientConfig.UserAgent,
err,
); sentryErr != nil {
log.Error("Sentry crash report failed: ", sentryErr)
}
}
}()
message, _, _, attachmentReaders, err := pkgMessage.Parse(bytes.NewBuffer(msg.Body))
return message, attachmentReaders, err
}

View File

@ -33,7 +33,7 @@ func TestPMAPIProviderMailboxes(t *testing.T) {
defer m.ctrl.Finish()
setupPMAPIClientExpectationForExport(&m)
provider, err := NewPMAPIProvider(m.pmapiConfig, m.clientManager, "user", "addressID")
provider, err := NewPMAPIProvider(m.clientManager, "user", "addressID")
r.NoError(t, err)
tests := []struct {
@ -78,7 +78,7 @@ func TestPMAPIProviderTransferTo(t *testing.T) {
defer m.ctrl.Finish()
setupPMAPIClientExpectationForExport(&m)
provider, err := NewPMAPIProvider(m.pmapiConfig, m.clientManager, "user", "addressID")
provider, err := NewPMAPIProvider(m.clientManager, "user", "addressID")
r.NoError(t, err)
rules, rulesClose := newTestRules(t)
@ -96,7 +96,7 @@ func TestPMAPIProviderTransferFrom(t *testing.T) {
defer m.ctrl.Finish()
setupPMAPIClientExpectationForImport(&m)
provider, err := NewPMAPIProvider(m.pmapiConfig, m.clientManager, "user", "addressID")
provider, err := NewPMAPIProvider(m.clientManager, "user", "addressID")
r.NoError(t, err)
rules, rulesClose := newTestRules(t)
@ -114,7 +114,7 @@ func TestPMAPIProviderTransferFromDraft(t *testing.T) {
defer m.ctrl.Finish()
setupPMAPIClientExpectationForImportDraft(&m)
provider, err := NewPMAPIProvider(m.pmapiConfig, m.clientManager, "user", "addressID")
provider, err := NewPMAPIProvider(m.clientManager, "user", "addressID")
r.NoError(t, err)
rules, rulesClose := newTestRules(t)
@ -133,9 +133,9 @@ func TestPMAPIProviderTransferFromTo(t *testing.T) {
setupPMAPIClientExpectationForExport(&m)
setupPMAPIClientExpectationForImport(&m)
source, err := NewPMAPIProvider(m.pmapiConfig, m.clientManager, "user", "addressID")
source, err := NewPMAPIProvider(m.clientManager, "user", "addressID")
r.NoError(t, err)
target, err := NewPMAPIProvider(m.pmapiConfig, m.clientManager, "user", "addressID")
target, err := NewPMAPIProvider(m.clientManager, "user", "addressID")
r.NoError(t, err)
rules, rulesClose := newTestRules(t)

View File

@ -15,12 +15,8 @@
// 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 build_qa
// +build beta
package updates
package updater
func init() {
Host = "https://bridgeteam.protontech.ch"
DownloadPath = "download/qa"
BuildType = "QA"
}
const Channel = "beta"

View File

@ -15,14 +15,10 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./release-notes.sh at 'Mon Dec 28 02:39:43 PM CET 2020'. DO NOT EDIT.
// +build !beta
package importexport
package updater
const ReleaseNotes = ` Allow an import of already encrypted messages (as cypher text)
Cosmetic GUI changes
Better error handling
`
const ReleaseFixedBugs = ` Installation issues on linux
`
// 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

@ -1,6 +1,28 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
// 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/>.
mQINBFo9OeEBEAC+fPrLcUBY+YUc5YiMrYJQ6ogrJWMGC00h9fAv3PsrHkBz0z7c
// +build !pmapi_qa
package updater
// DefaultPublicKey is the public key used to sign builds.
const DefaultPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
xsFNBFo9OeEBEAC+fPrLcUBY+YUc5YiMrYJQ6ogrJWMGC00h9fAv3PsrHkBz0z7c
QFDyNdNatokFDtZDX115M0vzDwk5NkcjmO7CWbf6nCZcwYqOSrBoH8wNT9uTS/6p
R3AHk1r3C/36QG3iWx6Wg4ycRkXWYToT3/yh5waE5BbLi/9TSBAdfJzTyxt4IpZG
3OTMnOwuz6eNRWVHkA48CJydWS6M8z+jIsBwFq4nOIChvLjIF42PuAT1VaiCYSmy
@ -11,8 +33,8 @@ d1UzLPCSUNUO+/7fslZCax26d1r1kbHzJLAN1Jer6rxoEDaEiVSCUTnHgykCq5rO
C3PScGEdOaIi4H5c6YFZrLmdz409YmJEWLKIPV/u5DpI+YGmAfAevrjkMBgQBOmZ
D8Gp19LnRtmqjVh2rVdr8yc5nAjoNOZwanMwD5vCWPUVELWXubNFBv8hqZMxHZqW
GrB8x8hkdgiNmuyqsxzBmOEJHWLlvbFhvHhIedT8paU/spL/qJmWp3EB4QARAQAB
tExQcm90b24gVGVjaG5vbG9naWVzIEFHIChQcm90b25NYWlsIEJyaWRnZSBkZXZl
bG9wZXJzKSA8YnJpZGdlQHByb3Rvbm1haWwuY2g+iQJUBBMBCAA+AhsDBQsJCAcC
zUxQcm90b24gVGVjaG5vbG9naWVzIEFHIChQcm90b25NYWlsIEJyaWRnZSBkZXZl
bG9wZXJzKSA8YnJpZGdlQHByb3Rvbm1haWwuY2g+wsGUBBMBCAA+AhsDBQsJCAcC
BhUICQoLAgQWAgMBAh4BAheAFiEE1R5k0+Y+3D7veGTO4sddaOYjSwcFAlv377wF
CQO83tsACgkQ4sddaOYjSwfhng//WNhZqr0StuN4KbYdQG+FY+aLijLhiVI3i4j6
wUis+7UWFNMUGePsBUrF7zOrzo4Vp16FSRhhpveIbDMVJg4yGlzwN+jZr9FBvF8z
@ -25,7 +47,7 @@ O1GihEpoXpOezs46+ER/YGx4ZF2ne2bmYnzoOOZBbGXwsMZTNaa9QJHbc1bz9jjj
IFBc1zmrdi0nsbjlvLugEYIbSb/WP0wKwG66zTatslRIQ2unlUJNnWb0E4VLgz9y
q57QpvxS7D312dZV0NnAwhyDI+54XAivXTQb0fAGfcgbtKdKpJb1dcAMb9WOBnpr
BK7XLsWbJj5v5nB3AuWer7NhUyJB/ogWQtqRUY1bAcI4cB1zFwYq/PL0sbfAHDxx
ZEF6Xhi5Ag0EWj054QEQALdPQOlRT1omHljxnN64jFuDXXSIb6zqaBvUwdYoDpV2
ZEF6XhjOwU0EWj054QEQALdPQOlRT1omHljxnN64jFuDXXSIb6zqaBvUwdYoDpV2
dfRmzGklsCVA7WHXBmDWbUe9avgO3OO7ANw6/JzzYjP+jwImpJg7cSqTqW8A1U6T
YfGXVUV3a/obIEttl7bI9BsUNgmLsBYIwHov+gl/ajKQdALYHCmq3Bj6o7BBeWPp
Vpk9dzjcsLVbmNszNGP1Ik5dKE0jZUi6h+YoVuJE9o/+T+jxoqFRpXNsZqWOEKmC
@ -36,7 +58,7 @@ nnnUqvCcoekFMURDtP3z09KZXuOMnt834utd7WLe+LZD6dxs+rPhyDiW80E8Bdlz
4Aip2hhFqWJAbUQXCyMaeU2WTWIzy0FQ6SEFFy/RM8O5O1HHsDYjtIic9QJ/PqSD
0qN7LMlkjR8AdWvAxm95i5GpxDZODldsOneeummvsn3I1jCoULTik7iJVdRuY1V3
vfsYAkefGN/n2ga3MvatCJipwoCGsMgUXGTdokXOqKBgMBuBLCkxj2wlol2R9p8R
ABEBAAGJAjwEGAEIACYCGwwWIQTVHmTT5j7cPu94ZM7ix11o5iNLBwUCW/fygQUJ
ABEBAAHCwXwEGAEIACYCGwwWIQTVHmTT5j7cPu94ZM7ix11o5iNLBwUCW/fygQUJ
A7zhoAAKCRDix11o5iNLB7eTD/4x8I7I7MQV63Z8hDShJixSi49bfXeykzlrZyrA
bqNr7JrIKzgX5F1HTU0JF3m+VGkhlpMIlTF/jLq9f1vzmRuiPvux/jItXYbnHFhh
lFekwZkXx4nS5iwjpMDt6C1ERftv+Z5yHK91mZsr6eNcfA6VeIdKBQenltZvDVsq
@ -49,5 +71,5 @@ b3mx3wudw+aI8MXXPzMBCAn57S7/xuQ4fODx62NOeme/BOnjASbeE3mZ5/3qBbnu
YIgVTYNp5frIG3wK8W1r6NY2vYQ0iBIzOCIxnNDjYqsGlpAytX+SM+YY7J9n1dZa
UsUfX5Qs+D9VIr/j3jurObPehn9fahCOC2YXicKgSbmQyBLysbFyLT5AMpn5aes0
qdwhrw==
=B6/F
-----END PGP PUBLIC KEY BLOCK-----
=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

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
@ -15,18 +15,36 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./release-notes.sh at 'Wed Jan 13 03:17:24 PM CET 2021'. DO NOT EDIT.
package updater
package bridge
import (
"sync/atomic"
const ReleaseNotes = ` Improvements to message parsing
Better error handling
`
"github.com/pkg/errors"
)
const ReleaseFixedBugs = ` Message corruption - rare cases of overly long headers
AppleMail crashes (related to timestamps)
Sending messages from aliases in combined inbox mode
Fedora font issues
var ErrOperationOngoing = errors.New("the operation is already ongoing")
For more detailed summary of the changes see https://github.com/ProtonMail/proton-bridge/blob/master/Changelog.md
`
// 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()
}

View File

@ -15,7 +15,7 @@
// 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
package updater
import (
"crypto/sha256"
@ -23,6 +23,8 @@ import (
"io"
"os"
"path/filepath"
"github.com/sirupsen/logrus"
)
func syncFolders(localPath, updatePath string) (err error) {
@ -45,7 +47,7 @@ func syncFolders(localPath, updatePath string) (err error) {
}
func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) {
log.WithField("from", folderToCleanPath).Debug("Remove missing.")
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 {
@ -56,7 +58,7 @@ func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) {
if walkErr != nil {
return walkErr
}
log.WithField("path", relPath).Trace("Keep the path.")
logrus.WithField("path", relPath).Trace("Keep the path")
existingRelPaths[relPath] = true
return nil
})
@ -73,9 +75,9 @@ func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) {
if walkErr != nil {
return walkErr
}
log.Debug("check path ", relPath)
logrus.Debug("check path ", relPath)
if !existingRelPaths[relPath] {
log.Debug("path not in list, removing ", removeThis)
logrus.Debug("path not in list, removing ", removeThis)
delList = append(delList, removeThis)
}
return nil
@ -86,7 +88,7 @@ func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) {
for _, removeThis := range delList {
if err = os.RemoveAll(removeThis); err != nil && !os.IsNotExist(err) {
log.Error("remove error ", err)
logrus.Error("remove error ", err)
return
}
}
@ -95,18 +97,18 @@ func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) {
}
func restoreFromBackup(backupDir, localPath string) {
log.WithField("from", backupDir).
logrus.WithField("from", backupDir).
WithField("to", localPath).
Error("recovering")
if err := copyRecursively(backupDir, localPath); err != nil {
log.WithField("from", backupDir).
logrus.WithField("from", backupDir).
WithField("to", localPath).
Error("Not able to recover.")
}
}
func createBackup(srcFile, dstDir string) (err error) {
log.WithField("from", srcFile).WithField("to", dstDir).Debug("Create backup")
logrus.WithField("from", srcFile).WithField("to", dstDir).Debug("Create backup")
if err = mkdirAllClear(dstDir); err != nil {
return
}
@ -114,6 +116,13 @@ func createBackup(srcFile, dstDir string) (err error) {
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]
@ -143,7 +152,7 @@ func copyRecursively(srcDir, dstDir string) error { // nolint[funlen]
// Non regular source (e.g. named pipes, sockets, devices...).
if !srcIsLink && !srcIsDir && !srcInfo.Mode().IsRegular() {
log.Error("File ", srcPath, " with mode ", srcInfo.Mode())
logrus.Error("File ", srcPath, " with mode ", srcInfo.Mode())
return errors.New("irregular source file. Copy not implemented")
}
@ -153,7 +162,7 @@ func copyRecursively(srcDir, dstDir string) error { // nolint[funlen]
return err
}
dstPath := filepath.Join(dstDir, srcRelPath)
log.Debug("src: ", srcPath, " dst: ", dstPath)
logrus.Debug("src: ", srcPath, " dst: ", dstPath)
// Destination exists.
dstInfo, err := os.Lstat(dstPath)
@ -163,7 +172,7 @@ func copyRecursively(srcDir, dstDir string) error { // nolint[funlen]
// Non regular destination (e.g. named pipes, sockets, devices...).
if !dstIsLink && !dstIsDir && !dstInfo.Mode().IsRegular() {
log.Error("File ", dstPath, " with mode ", dstInfo.Mode())
logrus.Error("File ", dstPath, " with mode ", dstInfo.Mode())
return errors.New("irregular target file. Copy not implemented")
}
@ -192,25 +201,25 @@ func copyRecursively(srcDir, dstDir string) error { // nolint[funlen]
// Create symbolic link and return.
if srcIsLink {
log.Debug("It is a symlink")
logrus.Debug("It is a symlink")
linkPath, err := os.Readlink(srcPath)
if err != nil {
return err
}
log.Debug("link to ", linkPath)
logrus.Debug("link to ", linkPath)
return os.Symlink(linkPath, dstPath)
}
// Create dir and return.
if srcIsDir {
log.Debug("It is a dir")
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) {
log.Debug("Same files, skip copy")
logrus.Debug("Same files, skip copy")
return nil
}
@ -225,7 +234,7 @@ func copyRecursively(srcDir, dstDir string) error { // nolint[funlen]
}
func copyToTmpFileRename(srcReader io.Reader, dstPath string, dstMode os.FileMode) error {
log.Debug("Tmp and rename ", dstPath)
logrus.Debug("Tmp and rename ", dstPath)
tmpPath := dstPath + ".tmp"
if err := copyToFileTruncate(srcReader, tmpPath, dstMode); err != nil {
return err
@ -234,7 +243,7 @@ func copyToTmpFileRename(srcReader io.Reader, dstPath string, dstMode os.FileMod
}
func copyToFileTruncate(srcReader io.Reader, dstPath string, dstMode os.FileMode) error {
log.Debug("Copy and truncate ", dstPath)
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

View File

@ -15,7 +15,7 @@
// 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
package updater
import (
"io/ioutil"
@ -40,7 +40,7 @@ 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))
log.Warn("OK: from ", srcType, " to ", dstType)
logrus.Warn("OK: from ", srcType, " to ", dstType)
}
}
}
@ -52,13 +52,13 @@ func checkCopyWorks(srcType, dstType string) error {
destDir := filepath.Join(AppCacheDir, "sync_dst", dirName)
// clear before
log.Info("remove all ", srcDir)
logrus.Info("remove all ", srcDir)
err := os.RemoveAll(srcDir)
if err != nil {
return err
}
log.Info("remove all ", destDir)
logrus.Info("remove all ", destDir)
err = os.RemoveAll(destDir)
if err != nil {
return err
@ -76,27 +76,27 @@ func checkCopyWorks(srcType, dstType string) error {
}
// copy
log.Info("Sync from ", srcDir, " to ", destDir)
logrus.Info("Sync from ", srcDir, " to ", destDir)
err = syncFolders(destDir, srcDir)
if err != nil {
return err
}
// Check
log.Info("check ", srcDir, " and ", destDir)
logrus.Info("check ", srcDir, " and ", destDir)
err = checkThatFilesAreSame(srcDir, destDir)
if err != nil {
return err
}
// clear after
log.Info("remove all ", srcDir)
logrus.Info("remove all ", srcDir)
err = os.RemoveAll(srcDir)
if err != nil {
return err
}
log.Info("remove all ", destDir)
logrus.Info("remove all ", destDir)
err = os.RemoveAll(destDir)
if err != nil {
return err
@ -107,13 +107,13 @@ func checkCopyWorks(srcType, dstType string) error {
func checkThatFilesAreSame(src, dst string) error {
cmd := exec.Command("diff", "-qr", src, dst) //nolint[gosec]
cmd.Stderr = log.WriterLevel(logrus.ErrorLevel)
cmd.Stdout = log.WriterLevel(logrus.InfoLevel)
cmd.Stderr = logrus.StandardLogger().WriterLevel(logrus.ErrorLevel)
cmd.Stdout = logrus.StandardLogger().WriterLevel(logrus.InfoLevel)
return cmd.Run()
}
func createTestFolder(dirPath, dirType string) error {
log.Info("creating folder ", dirPath, " type ", dirType)
logrus.Info("creating folder ", dirPath, " type ", dirType)
if dirType == NewType {
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)
}

View File

@ -1,102 +0,0 @@
// 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 updates
import (
"regexp"
"strconv"
"strings"
)
var nonVersionChars = regexp.MustCompile(`([^0-9.]+)`) //nolint[gochecknoglobals]
// sanitizeVersion returns only numbers and periods.
func sanitizeVersion(version string) string {
return nonVersionChars.ReplaceAllString(version, "")
}
// Result can be false positive, but must not be false negative.
// Assuming
// * dot separated integers format e.g. "A.B.C.…" where A,B,C,… are integers
// * `1.1` == `1.1.0` (i.e. first is not newer)
// * `1.1.1` > `1.1` (i.e. first is newer)
func isFirstVersionNewer(first, second string) (firstIsNewer bool, err error) {
first = sanitizeVersion(first)
second = sanitizeVersion(second)
firstIsNewer, err = false, nil
if first == second {
return
}
firstIsNewer = true
var firstArr, secondArr []int
if firstArr, err = versionStrToInts(first); err != nil {
return
}
if secondArr, err = versionStrToInts(second); err != nil {
return
}
verLength := max(len(firstArr), len(secondArr))
firstArr = appendZeros(firstArr, verLength)
secondArr = appendZeros(secondArr, verLength)
for i := 0; i < verLength; i++ {
if firstArr[i] == secondArr[i] {
continue
}
return firstArr[i] > secondArr[i], nil
}
return false, nil
}
func versionStrToInts(version string) (intArr []int, err error) {
strArr := strings.Split(version, ".")
intArr = make([]int, len(strArr))
for index, item := range strArr {
if item == "" {
intArr[index] = 0
continue
}
intArr[index], err = strconv.Atoi(item)
if err != nil {
return
}
}
return
}
func appendZeros(ints []int, newsize int) []int {
size := len(ints)
if size >= newsize {
return ints
}
zeros := make([]int, newsize-size)
return append(ints, zeros...)
}
func max(ints ...int) (max int) {
max = ints[0]
for _, a := range ints {
if max < a {
max = a
}
}
return
}

View File

@ -1,78 +0,0 @@
// 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 updates
import (
"testing"
"github.com/stretchr/testify/require"
)
type testDataValues struct {
expectErr, expectedNewer bool
first, second string
}
type testDataList []testDataValues
func (tdl *testDataList) add(err, newer bool, first, second string) { //nolint[unparam]
*tdl = append(*tdl, testDataValues{err, newer, first, second})
}
func (tdl *testDataList) addFirstIsNewer(first, second string) {
tdl.add(false, true, first, second)
tdl.add(false, false, second, first)
}
func TestCompareVersion(t *testing.T) {
testData := testDataList{}
// same is never newer
testData.add(false, false, "1.1.1", "1.1.1")
testData.add(false, false, "1.1.0", "1.1")
testData.add(false, false, "1.0.0", "1")
testData.add(false, false, ".1.1", "0.1.1")
testData.add(false, false, "0.1.1", ".1.1")
testData.addFirstIsNewer("1.1.10", "1.1.1")
testData.addFirstIsNewer("1.10.1", "1.1.1")
testData.addFirstIsNewer("10.1.1", "1.1.1")
testData.addFirstIsNewer("1.1.1", "0.1.1")
testData.addFirstIsNewer("1.1.1", "1.0.1")
testData.addFirstIsNewer("1.1.1", "1.1.0")
testData.addFirstIsNewer("1.1.1", "1")
testData.addFirstIsNewer("1.1.1", "1.1")
testData.addFirstIsNewer("1.1.1.1", "1.1.1")
testData.addFirstIsNewer("1.1.1 beta", "1.1.0")
testData.addFirstIsNewer("1z.1z.1z", "1.1.0")
testData.addFirstIsNewer("1a.1b.1c", "1.1.0")
for _, td := range testData {
t.Log(td)
isNewer, err := isFirstVersionNewer(td.first, td.second)
if td.expectErr {
require.True(t, err != nil, "expected error but got nil for %#v", td)
require.True(t, true == isNewer, "error expected but first is not newer for %#v", td)
continue
}
require.True(t, err == nil, "expected no error but have %v for %#v", err, td)
require.True(t, isNewer == td.expectedNewer, "expected %v but have %v for %#v", td.expectedNewer, isNewer, err, td)
}
}

Some files were not shown because too many files have changed in this diff Show More