forked from Silverfish/proton-bridge
Launcher, app/base, sentry, update service
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
33
Makefile
33
Makefile
@ -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
|
||||
|
||||
12
README.md
12
README.md
@ -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
|
||||
|
||||
@ -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.
|
||||
cacheVersion = "c11"
|
||||
|
||||
appName = "bridge"
|
||||
)
|
||||
|
||||
var (
|
||||
log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals]
|
||||
appName = "ProtonMail Bridge"
|
||||
appUsage = "ProtonMail IMAP and SMTP Bridge"
|
||||
configName = "bridge"
|
||||
updateURLName = "bridge"
|
||||
keychainName = "bridge"
|
||||
cacheVersion = "c11"
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
pref11Path := cfg.GetPreferencesPath()
|
||||
if _, err := os.Stat(pref11Path); err == nil {
|
||||
log.WithField("path", pref11Path).Trace("New preferences already exists, migration skipped")
|
||||
return
|
||||
}
|
||||
|
||||
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")
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to create app base")
|
||||
}
|
||||
// Other instance already running.
|
||||
if base == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := bridge.New(base).Run(os.Args); err != nil {
|
||||
logrus.WithError(err).Fatal("Bridge exited with error")
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
cacheVersion = "c11"
|
||||
|
||||
appName = "importExport"
|
||||
appNameDash = "import-export-app"
|
||||
)
|
||||
|
||||
var (
|
||||
log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals]
|
||||
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"
|
||||
)
|
||||
|
||||
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
177
cmd/launcher/main.go
Normal 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
179
cmd/versioner/main.go
Normal 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
4
go.mod
@ -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
13
go.sum
@ -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=
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -67,14 +59,12 @@ func (api *apiServer) ListenAndServe() {
|
||||
|
||||
addr := api.getAddress()
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
TLSConfig: api.tls,
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
307
internal/app/base/base.go
Normal 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
|
||||
}
|
||||
}
|
||||
81
internal/app/base/migration.go
Normal file
81
internal/app/base/migration.go
Normal 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)
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
88
internal/app/base/restart.go
Normal file
88
internal/app/base/restart.go
Normal 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
|
||||
}
|
||||
49
internal/app/base/restart_test.go
Normal file
49
internal/app/base/restart_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
137
internal/app/bridge/bridge.go
Normal file
137
internal/app/bridge/bridge.go
Normal 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
83
internal/app/ie/ie.go
Normal 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()
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;"
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
65
internal/config/cache/cache.go
vendored
Normal 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
70
internal/config/cache/cache_test.go
vendored
Normal 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())
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
@ -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))
|
||||
@ -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]
|
||||
func New(settingsPath string) *Settings {
|
||||
s := &Settings{
|
||||
keyValueStore: newKeyValueStore(filepath.Join(settingsPath, "prefs.json")),
|
||||
settingsPath: settingsPath,
|
||||
}
|
||||
|
||||
// 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)
|
||||
s.setDefaultValues()
|
||||
|
||||
log.WithField("path", path).Trace("Opened preferences")
|
||||
|
||||
return
|
||||
return s
|
||||
}
|
||||
|
||||
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, "")
|
||||
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")
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
)
|
||||
@ -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
42
internal/crash/actions.go
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
return
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
f.Println("Your version is up to date.")
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
f.Println("Your version is up to date.")
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,7 +226,7 @@ Dialog {
|
||||
target: timer
|
||||
onTriggered: {
|
||||
go.setPortsAndSecurity(imapPort.text, smtpPort.text, securitySMTPSTARTTLS.checked)
|
||||
go.isRestarting = true
|
||||
go.setToRestart()
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,20 +69,9 @@ Item {
|
||||
Connections {
|
||||
target: go
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
onShowWindow : {
|
||||
winMain.showAndRise()
|
||||
}
|
||||
|
||||
onProcessFinished : {
|
||||
winMain.dialogAddUser.hide()
|
||||
|
||||
@ -566,7 +566,6 @@ Window {
|
||||
return 0
|
||||
}
|
||||
|
||||
property bool isRestarting: false
|
||||
function setPortsAndSecurity(portIMAP, portSMTP, secSMTP) {
|
||||
console.log("Test: ports changed", portIMAP, portSMTP, secSMTP)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
@ -50,23 +48,25 @@ type QMLer interface {
|
||||
|
||||
// Accounts holds functionality of users
|
||||
type Accounts struct {
|
||||
Model *AccountsModel
|
||||
qml QMLer
|
||||
um types.UserManager
|
||||
prefs *config.Preferences
|
||||
Model *AccountsModel
|
||||
qml QMLer
|
||||
um types.UserManager
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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.SetUpdateState(StatusUpToDate)
|
||||
if showMessage {
|
||||
f.Qml.NotifyVersionIsTheLatest()
|
||||
}
|
||||
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.
|
||||
|
||||
@ -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{}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
@ -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.SetUpdateState("upToDate")
|
||||
if showMessage {
|
||||
s.Qml.NotifyVersionIsTheLatest()
|
||||
}
|
||||
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.
|
||||
}
|
||||
|
||||
@ -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) InstanceExistAlert() {}
|
||||
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 (s *FrontendHeadless) InstanceExistAlert() {}
|
||||
|
||||
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{}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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{},
|
||||
|
||||
@ -23,8 +23,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
type configProvider interface {
|
||||
GetEventsPath() string
|
||||
type cacheProvider interface {
|
||||
GetDBDir() string
|
||||
GetIMAPCachePath() string
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;"
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
191
internal/locations/locations.go
Normal file
191
internal/locations/locations.go
Normal 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()
|
||||
}
|
||||
152
internal/locations/locations_test.go
Normal file
152
internal/locations/locations_test.go
Normal 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
85
internal/logging/clear.go
Normal 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
62
internal/logging/crash.go
Normal 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)
|
||||
}
|
||||
90
internal/logging/logging.go
Normal file
90
internal/logging/logging.go
Normal 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)
|
||||
}
|
||||
69
internal/logging/logging_test.go
Normal file
69
internal/logging/logging_test.go
Normal 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
|
||||
}
|
||||
75
internal/logging/rotator.go
Normal file
75
internal/logging/rotator.go
Normal 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
|
||||
}
|
||||
131
internal/logging/rotator_test.go
Normal file
131
internal/logging/rotator_test.go
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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").
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
@ -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"
|
||||
22
internal/updater/host_default.go
Normal file
22
internal/updater/host_default.go
Normal 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"
|
||||
22
internal/updater/host_qa.go
Normal file
22
internal/updater/host_qa.go
Normal 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"
|
||||
64
internal/updater/install_darwin.go
Normal file
64
internal/updater/install_darwin.go
Normal 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)
|
||||
}
|
||||
41
internal/updater/install_default.go
Normal file
41
internal/updater/install_default.go
Normal 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)
|
||||
}
|
||||
@ -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-----`
|
||||
52
internal/updater/key_qa.go
Normal file
52
internal/updater/key_qa.go
Normal 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-----`
|
||||
@ -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()
|
||||
}
|
||||
67
internal/updater/locker_test.go
Normal file
67
internal/updater/locker_test.go
Normal 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()
|
||||
}
|
||||
@ -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
|
||||
@ -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
167
internal/updater/updater.go
Normal 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
|
||||
}
|
||||
336
internal/updater/updater_test.go
Normal file
336
internal/updater/updater_test.go
Normal 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
|
||||
}
|
||||
85
internal/updater/version.go
Normal file
85
internal/updater/version.go
Normal 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)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
Reference in New Issue
Block a user