From dc3f61acee9e339302dc3f2567c63ac32a43dc05 Mon Sep 17 00:00:00 2001 From: James Houlahan Date: Mon, 23 Nov 2020 11:56:57 +0100 Subject: [PATCH] Launcher, app/base, sentry, update service --- .gitignore | 5 +- BUILDS.md | 5 + Changelog.md | 4 +- Makefile | 33 +- README.md | 12 + cmd/Desktop-Bridge/main.go | 283 ++------------ cmd/Import-Export/main.go | 174 ++------- cmd/launcher/main.go | 177 +++++++++ cmd/versioner/main.go | 179 +++++++++ go.mod | 4 +- go.sum | 13 +- internal/api/api.go | 28 +- internal/api/focus.go | 8 +- internal/{cmd => app/base}/args.go | 22 +- internal/app/base/base.go | 307 +++++++++++++++ internal/app/base/migration.go | 81 ++++ .../profiles.go => app/base/profiling.go} | 22 +- internal/app/base/restart.go | 88 +++++ internal/app/base/restart_test.go | 49 +++ internal/app/bridge/bridge.go | 137 +++++++ internal/app/ie/ie.go | 83 +++++ internal/bridge/bridge.go | 28 +- internal/bridge/credits.go | 4 +- internal/bridge/store_factory.go | 12 +- internal/bridge/types.go | 16 +- internal/cmd/main.go | 96 ----- internal/cmd/restart.go | 111 ------ internal/config/cache/cache.go | 65 ++++ internal/config/cache/cache_test.go | 70 ++++ .../config/settings/kvs.go | 46 ++- .../config/settings/kvs_test.go | 62 ++-- .../settings/settings.go} | 71 ++-- {pkg/config => internal/config/tls}/tls.go | 132 ++++--- .../config/tls}/tls_test.go | 17 +- {pkg => internal}/constants/constants.go | 9 +- internal/cookies/pantry.go | 6 +- internal/crash/actions.go | 42 +++ .../logs_qa.go => internal/crash/handler.go | 58 +-- .../crash/handler_test.go | 51 ++- internal/events/events.go | 2 + internal/frontend/cli-ie/frontend.go | 44 +-- internal/frontend/cli-ie/system.go | 8 +- internal/frontend/cli-ie/updates.go | 30 +- internal/frontend/cli-ie/utils.go | 4 +- internal/frontend/cli/accounts.go | 8 +- internal/frontend/cli/frontend.go | 51 +-- internal/frontend/cli/system.go | 34 +- internal/frontend/cli/updates.go | 30 +- internal/frontend/cli/utils.go | 4 +- internal/frontend/frontend.go | 103 ++++-- .../qml/BridgeUI/DialogPortChange.qml | 2 +- internal/frontend/qml/GuiIE.qml | 17 +- internal/frontend/qml/tst_Gui.qml | 1 - internal/frontend/qt-common/accounts.go | 24 +- internal/frontend/qt-common/common.go | 2 + internal/frontend/qt-ie/frontend.go | 119 ++---- internal/frontend/qt-ie/frontend_nogui.go | 19 +- internal/frontend/qt-ie/ui.go | 8 +- internal/frontend/qt/accounts.go | 8 +- internal/frontend/qt/frontend.go | 168 ++++----- internal/frontend/qt/frontend_nogui.go | 21 +- internal/frontend/qt/ui.go | 6 +- internal/frontend/types/types.go | 15 +- internal/imap/backend.go | 8 +- internal/imap/bridge.go | 3 +- internal/imap/server.go | 7 + internal/importexport/credits.go | 4 +- internal/importexport/importexport.go | 38 +- internal/importexport/types.go | 10 +- internal/locations/locations.go | 191 ++++++++++ internal/locations/locations_test.go | 152 ++++++++ internal/logging/clear.go | 85 +++++ internal/logging/crash.go | 62 ++++ internal/logging/logging.go | 90 +++++ internal/logging/logging_test.go | 69 ++++ internal/logging/rotator.go | 75 ++++ internal/logging/rotator_test.go | 131 +++++++ internal/smtp/backend.go | 19 +- internal/smtp/server.go | 7 + internal/transfer/provider_imap_utils.go | 2 +- internal/transfer/provider_pmapi.go | 4 +- internal/transfer/provider_pmapi_target.go | 17 - internal/transfer/provider_pmapi_test.go | 12 +- .../updates_qa.go => updater/channel_beta.go} | 10 +- .../channel_default.go} | 14 +- internal/updater/host_default.go | 22 ++ internal/updater/host_qa.go | 22 ++ internal/updater/install_darwin.go | 64 ++++ internal/updater/install_default.go | 41 ++ .../key_default.go} | 38 +- internal/updater/key_qa.go | 52 +++ .../release_notes.go => updater/locker.go} | 42 ++- internal/updater/locker_test.go | 67 ++++ internal/{updates => updater}/sync.go | 45 ++- internal/{updates => updater}/sync_test.go | 22 +- internal/updater/updater.go | 167 +++++++++ internal/updater/updater_test.go | 336 +++++++++++++++++ internal/updater/version.go | 85 +++++ internal/updates/compare_versions.go | 102 ----- internal/updates/compare_versions_test.go | 78 ---- internal/updates/downloader.go | 131 ------- internal/updates/progress.go | 50 --- internal/updates/signature.go | 108 ------ internal/updates/tar.go | 126 ------- .../testdata/current_version_linux.json | 1 - .../testdata/current_version_linux.json.sig | Bin 566 -> 0 bytes internal/updates/updates.go | 349 ------------------ internal/updates/updates_test.go | 184 --------- internal/updates/version_info.go | 49 --- internal/users/mocks/mocks.go | 64 +--- internal/users/types.go | 7 +- internal/users/users.go | 15 +- internal/users/users_test.go | 9 +- internal/versioner/install.go | 38 ++ .../types.go => versioner/name_default.go} | 9 +- .../name_windows.go} | 9 +- internal/versioner/remove_darwin.go | 24 ++ .../remove_default.go} | 35 +- internal/versioner/util.go | 43 +++ internal/versioner/version.go | 87 +++++ internal/versioner/version_test.go | 142 +++++++ internal/versioner/versioner.go | 81 ++++ internal/versioner/versioner_test.go | 95 +++++ pkg/config/config.go | 319 ---------------- pkg/config/config_test.go | 238 ------------ pkg/config/logs.go | 251 ------------- pkg/config/logs_test.go | 225 ----------- pkg/config/mock_config.go | 76 ---- pkg/files/removal.go | 82 ++++ pkg/files/removal_test.go | 116 ++++++ pkg/message/parser.go | 9 + pkg/message/parser_test.go | 14 + pkg/pmapi/client_types.go | 2 + pkg/pmapi/clientmanager.go | 10 +- pkg/pmapi/config.go | 26 +- .../pmapi_prod.go => pmapi/config_default.go} | 29 +- pkg/pmapi/config_qa.go | 14 +- pkg/pmapi/dialer_pinning.go | 38 +- pkg/pmapi/download.go | 74 ++++ pkg/pmapi/mocks/mocks.go | 15 + pkg/pmapi/pin_checker.go | 46 ++- pkg/pmapi/pin_checker_test.go | 14 +- pkg/pmapi/users.go | 4 +- pkg/ports/ports.go | 11 +- pkg/sentry/{report.go => reporter.go} | 73 +++- .../{report_test.go => reporter_test.go} | 0 .../logs_all.go => signature/signature.go} | 46 +-- pkg/tar/tar.go | 76 ++++ test/api_checks_test.go | 15 + test/context/bridge.go | 10 +- test/context/cache.go | 55 +++ test/context/config.go | 103 ------ test/context/context.go | 19 +- test/context/imap.go | 12 +- test/context/importexport.go | 7 +- test/context/locations.go | 50 +++ test/context/settings.go | 50 +++ test/context/smtp.go | 14 +- test/context/users.go | 2 +- test/fakeapi/download.go | 28 ++ test/features/bridge/imap/user_agent.feature | 24 ++ test/imap_actions_messages_test.go | 12 + test/main_test.go | 2 +- test/mocks/imap_client.go | 9 +- 164 files changed, 5368 insertions(+), 4039 deletions(-) create mode 100644 cmd/launcher/main.go create mode 100644 cmd/versioner/main.go rename internal/{cmd => app/base}/args.go (77%) create mode 100644 internal/app/base/base.go create mode 100644 internal/app/base/migration.go rename internal/{cmd/profiles.go => app/base/profiling.go} (72%) create mode 100644 internal/app/base/restart.go create mode 100644 internal/app/base/restart_test.go create mode 100644 internal/app/bridge/bridge.go create mode 100644 internal/app/ie/ie.go delete mode 100644 internal/cmd/main.go delete mode 100644 internal/cmd/restart.go create mode 100644 internal/config/cache/cache.go create mode 100644 internal/config/cache/cache_test.go rename pkg/config/preferences.go => internal/config/settings/kvs.go (64%) rename pkg/config/preferences_test.go => internal/config/settings/kvs_test.go (62%) rename internal/{preferences/preferences.go => config/settings/settings.go} (50%) rename {pkg/config => internal/config/tls}/tls.go (76%) rename {pkg/config => internal/config/tls}/tls_test.go (86%) rename {pkg => internal}/constants/constants.go (89%) create mode 100644 internal/crash/actions.go rename pkg/config/logs_qa.go => internal/crash/handler.go (52%) rename pkg/config/pmapi_noprod.go => internal/crash/handler_test.go (54%) create mode 100644 internal/locations/locations.go create mode 100644 internal/locations/locations_test.go create mode 100644 internal/logging/clear.go create mode 100644 internal/logging/crash.go create mode 100644 internal/logging/logging.go create mode 100644 internal/logging/logging_test.go create mode 100644 internal/logging/rotator.go create mode 100644 internal/logging/rotator_test.go rename internal/{updates/updates_qa.go => updater/channel_beta.go} (84%) rename internal/{importexport/release_notes.go => updater/channel_default.go} (70%) create mode 100644 internal/updater/host_default.go create mode 100644 internal/updater/host_qa.go create mode 100644 internal/updater/install_darwin.go create mode 100644 internal/updater/install_default.go rename internal/{updates/bridge_pubkey.gpg => updater/key_default.go} (68%) create mode 100644 internal/updater/key_qa.go rename internal/{bridge/release_notes.go => updater/locker.go} (55%) create mode 100644 internal/updater/locker_test.go rename internal/{updates => updater}/sync.go (85%) rename internal/{updates => updater}/sync_test.go (85%) create mode 100644 internal/updater/updater.go create mode 100644 internal/updater/updater_test.go create mode 100644 internal/updater/version.go delete mode 100644 internal/updates/compare_versions.go delete mode 100644 internal/updates/compare_versions_test.go delete mode 100644 internal/updates/downloader.go delete mode 100644 internal/updates/progress.go delete mode 100644 internal/updates/signature.go delete mode 100644 internal/updates/tar.go delete mode 100644 internal/updates/testdata/current_version_linux.json delete mode 100644 internal/updates/testdata/current_version_linux.json.sig delete mode 100644 internal/updates/updates.go delete mode 100644 internal/updates/updates_test.go delete mode 100644 internal/updates/version_info.go create mode 100644 internal/versioner/install.go rename internal/{frontend/qt-ie/types.go => versioner/name_default.go} (88%) rename internal/{updates/updates_beta.go => versioner/name_windows.go} (87%) create mode 100644 internal/versioner/remove_darwin.go rename internal/{cmd/version_file.go => versioner/remove_default.go} (59%) create mode 100644 internal/versioner/util.go create mode 100644 internal/versioner/version.go create mode 100644 internal/versioner/version_test.go create mode 100644 internal/versioner/versioner.go create mode 100644 internal/versioner/versioner_test.go delete mode 100644 pkg/config/config.go delete mode 100644 pkg/config/config_test.go delete mode 100644 pkg/config/logs.go delete mode 100644 pkg/config/logs_test.go delete mode 100644 pkg/config/mock_config.go create mode 100644 pkg/files/removal.go create mode 100644 pkg/files/removal_test.go rename pkg/{config/pmapi_prod.go => pmapi/config_default.go} (55%) create mode 100644 pkg/pmapi/download.go rename pkg/sentry/{report.go => reporter.go} (61%) rename pkg/sentry/{report_test.go => reporter_test.go} (100%) rename pkg/{config/logs_all.go => signature/signature.go} (53%) create mode 100644 pkg/tar/tar.go create mode 100644 test/context/cache.go delete mode 100644 test/context/config.go create mode 100644 test/context/locations.go create mode 100644 test/context/settings.go create mode 100644 test/fakeapi/download.go create mode 100644 test/features/bridge/imap/user_agent.feature diff --git a/.gitignore b/.gitignore index 8cd13cec..80079e7e 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/BUILDS.md b/BUILDS.md index 6e51e1d6..0aad8f53 100644 --- a/BUILDS.md +++ b/BUILDS.md @@ -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 diff --git a/Changelog.md b/Changelog.md index 059454db..3fc5cdf0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -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. diff --git a/Makefile b/Makefile index 4f9ee926..062c6293 100644 --- a/Makefile +++ b/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 diff --git a/README.md b/README.md index 6802415d..ae9bc39f 100644 --- a/README.md +++ b/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 diff --git a/cmd/Desktop-Bridge/main.go b/cmd/Desktop-Bridge/main.go index 89e54e3d..04decf45 100644 --- a/cmd/Desktop-Bridge/main.go +++ b/cmd/Desktop-Bridge/main.go @@ -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") + } } diff --git a/cmd/Import-Export/main.go b/cmd/Import-Export/main.go index b51664d0..324ed53c 100644 --- a/cmd/Import-Export/main.go +++ b/cmd/Import-Export/main.go @@ -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 } diff --git a/cmd/launcher/main.go b/cmd/launcher/main.go new file mode 100644 index 00000000..c0ca82ca --- /dev/null +++ b/cmd/launcher/main.go @@ -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 . + +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)) +} diff --git a/cmd/versioner/main.go b/cmd/versioner/main.go new file mode 100644 index 00000000..88b9c4b9 --- /dev/null +++ b/cmd/versioner/main.go @@ -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 . + +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) +} diff --git a/go.mod b/go.mod index 5d669b99..97f6feeb 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 0bc6a12b..df53cf03 100644 --- a/go.sum +++ b/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= diff --git a/internal/api/api.go b/internal/api/api.go index ff5837f5..12f1b92c 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -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) } diff --git a/internal/api/focus.go b/internal/api/focus.go index fc694ceb..ab5e63ae 100644 --- a/internal/api/focus.go +++ b/internal/api/focus.go @@ -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 } diff --git a/internal/cmd/args.go b/internal/app/base/args.go similarity index 77% rename from internal/cmd/args.go rename to internal/app/base/args.go index 17d9c158..af00f7c9 100644 --- a/internal/cmd/args.go +++ b/internal/app/base/args.go @@ -15,21 +15,21 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -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 } diff --git a/internal/app/base/base.go b/internal/app/base/base.go new file mode 100644 index 00000000..5f400efe --- /dev/null +++ b/internal/app/base/base.go @@ -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 . + +// 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 + } +} diff --git a/internal/app/base/migration.go b/internal/app/base/migration.go new file mode 100644 index 00000000..87451a04 --- /dev/null +++ b/internal/app/base/migration.go @@ -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 . + +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//c11/prefs.json | ~/.config/protonmail//prefs.json | +// | c11 | ~/.cache/protonmail//c11 | ~/.cache/protonmail//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) +} diff --git a/internal/cmd/profiles.go b/internal/app/base/profiling.go similarity index 72% rename from internal/cmd/profiles.go rename to internal/app/base/profiling.go index f359df8f..02a0c275 100644 --- a/internal/cmd/profiles.go +++ b/internal/app/base/profiling.go @@ -15,40 +15,42 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -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() } diff --git a/internal/app/base/restart.go b/internal/app/base/restart.go new file mode 100644 index 00000000..b6adb683 --- /dev/null +++ b/internal/app/base/restart.go @@ -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 . + +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 +} diff --git a/internal/app/base/restart_test.go b/internal/app/base/restart_test.go new file mode 100644 index 00000000..e5aa99e8 --- /dev/null +++ b/internal/app/base/restart_test.go @@ -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 . + +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)) + }) + } +} diff --git a/internal/app/bridge/bridge.go b/internal/app/bridge/bridge.go new file mode 100644 index 00000000..a9aa22fc --- /dev/null +++ b/internal/app/bridge/bridge.go @@ -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 . + +// 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() +} diff --git a/internal/app/ie/ie.go b/internal/app/ie/ie.go new file mode 100644 index 00000000..83d733f7 --- /dev/null +++ b/internal/app/ie/ie.go @@ -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 . + +// 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() +} diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go index feab5b38..5c28b368 100644 --- a/internal/bridge/bridge.go +++ b/internal/bridge/bridge.go @@ -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)) } } } diff --git a/internal/bridge/credits.go b/internal/bridge/credits.go index 6a97c783..30e6ef9d 100644 --- a/internal/bridge/credits.go +++ b/internal/bridge/credits.go @@ -15,8 +15,8 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// 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;" diff --git a/internal/bridge/store_factory.go b/internal/bridge/store_factory.go index 04712829..febe3af1 100644 --- a/internal/bridge/store_factory.go +++ b/internal/bridge/store_factory.go @@ -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) } diff --git a/internal/bridge/types.go b/internal/bridge/types.go index 85d34c46..51e7d231 100644 --- a/internal/bridge/types.go +++ b/internal/bridge/types.go @@ -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) } diff --git a/internal/cmd/main.go b/internal/cmd/main.go deleted file mode 100644 index 969171ea..00000000 --- a/internal/cmd/main.go +++ /dev/null @@ -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 . - -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 -} diff --git a/internal/cmd/restart.go b/internal/cmd/restart.go deleted file mode 100644 index af91fded..00000000 --- a/internal/cmd/restart.go +++ /dev/null @@ -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 . - -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) -} diff --git a/internal/config/cache/cache.go b/internal/config/cache/cache.go new file mode 100644 index 00000000..af9fe444 --- /dev/null +++ b/internal/config/cache/cache.go @@ -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 . + +// 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) +} diff --git a/internal/config/cache/cache_test.go b/internal/config/cache/cache_test.go new file mode 100644 index 00000000..f0c1eb7c --- /dev/null +++ b/internal/config/cache/cache_test.go @@ -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 . + +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()) + } +} diff --git a/pkg/config/preferences.go b/internal/config/settings/kvs.go similarity index 64% rename from pkg/config/preferences.go rename to internal/config/settings/kvs.go index 94a119bb..bd2ecb9b 100644 --- a/pkg/config/preferences.go +++ b/internal/config/settings/kvs.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -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)) } diff --git a/pkg/config/preferences_test.go b/internal/config/settings/kvs_test.go similarity index 62% rename from pkg/config/preferences_test.go rename to internal/config/settings/kvs_test.go index 6930a671..cca938b3 100644 --- a/pkg/config/preferences_test.go +++ b/internal/config/settings/kvs_test.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -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)) diff --git a/internal/preferences/preferences.go b/internal/config/settings/settings.go similarity index 50% rename from internal/preferences/preferences.go rename to internal/config/settings/settings.go index 0cdadf4c..c46e3dcc 100644 --- a/internal/preferences/preferences.go +++ b/internal/config/settings/settings.go @@ -15,15 +15,14 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// 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") } diff --git a/pkg/config/tls.go b/internal/config/tls/tls.go similarity index 76% rename from pkg/config/tls.go rename to internal/config/tls/tls.go index f346ddf7..d4470bee 100644 --- a/pkg/config/tls.go +++ b/internal/config/tls/tls.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -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) -} diff --git a/pkg/config/tls_test.go b/internal/config/tls/tls_test.go similarity index 86% rename from pkg/config/tls_test.go rename to internal/config/tls/tls_test.go index 73eab47e..292682d0 100644 --- a/pkg/config/tls_test.go +++ b/internal/config/tls/tls_test.go @@ -15,9 +15,10 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -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) } diff --git a/pkg/constants/constants.go b/internal/constants/constants.go similarity index 89% rename from pkg/constants/constants.go rename to internal/constants/constants.go index 900e58c8..d7bbdc17 100644 --- a/pkg/constants/constants.go +++ b/internal/constants/constants.go @@ -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) ) diff --git a/internal/cookies/pantry.go b/internal/cookies/pantry.go index b52a5b8d..9ede968e 100644 --- a/internal/cookies/pantry.go +++ b/internal/cookies/pantry.go @@ -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 } diff --git a/internal/crash/actions.go b/internal/crash/actions.go new file mode 100644 index 00000000..67e7d3a3 --- /dev/null +++ b/internal/crash/actions.go @@ -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 . + +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, + ) + } +} diff --git a/pkg/config/logs_qa.go b/internal/crash/handler.go similarity index 52% rename from pkg/config/logs_qa.go rename to internal/crash/handler.go index ec5c90ff..115f0773 100644 --- a/pkg/config/logs_qa.go +++ b/internal/crash/handler.go @@ -15,36 +15,40 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// +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") + } + } } diff --git a/pkg/config/pmapi_noprod.go b/internal/crash/handler_test.go similarity index 54% rename from pkg/config/pmapi_noprod.go rename to internal/crash/handler_test.go index 50dffd55..d93e49b9 100644 --- a/pkg/config/pmapi_noprod.go +++ b/internal/crash/handler_test.go @@ -15,29 +15,44 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// +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") } diff --git a/internal/events/events.go b/internal/events/events.go index bcdecd4e..696c368e 100644 --- a/internal/events/events.go +++ b/internal/events/events.go @@ -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) } diff --git a/internal/frontend/cli-ie/frontend.go b/internal/frontend/cli-ie/frontend.go index e69c05fe..8123c04d 100644 --- a/internal/frontend/cli-ie/frontend.go +++ b/internal/frontend/cli-ie/frontend.go @@ -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 +} diff --git a/internal/frontend/cli-ie/system.go b/internal/frontend/cli-ie/system.go index 6965dc6e..45f04efe 100644 --- a/internal/frontend/cli-ie/system.go +++ b/internal/frontend/cli-ie/system.go @@ -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) { diff --git a/internal/frontend/cli-ie/updates.go b/internal/frontend/cli-ie/updates.go index 2a63fc4e..e4e8dc19 100644 --- a/internal/frontend/cli-ie/updates.go +++ b/internal/frontend/cli-ie/updates.go @@ -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) { diff --git a/internal/frontend/cli-ie/utils.go b/internal/frontend/cli-ie/utils.go index 0fe1d184..5057a223 100644 --- a/internal/frontend/cli-ie/utils.go +++ b/internal/frontend/cli-ie/utils.go @@ -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") diff --git a/internal/frontend/cli/accounts.go b/internal/frontend/cli/accounts.go index 768ee2a8..8c3d1e51 100644 --- a/internal/frontend/cli/accounts.go +++ b/internal/frontend/cli/accounts.go @@ -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, diff --git a/internal/frontend/cli/frontend.go b/internal/frontend/cli/frontend.go index 7b925596..b67fa723 100644 --- a/internal/frontend/cli/frontend.go +++ b/internal/frontend/cli/frontend.go @@ -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 +} diff --git a/internal/frontend/cli/system.go b/internal/frontend/cli/system.go index 297c4dca..ebe582d6 100644 --- a/internal/frontend/cli/system.go +++ b/internal/frontend/cli/system.go @@ -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() } } diff --git a/internal/frontend/cli/updates.go b/internal/frontend/cli/updates.go index a99d01b8..45969516 100644 --- a/internal/frontend/cli/updates.go +++ b/internal/frontend/cli/updates.go @@ -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) { diff --git a/internal/frontend/cli/utils.go b/internal/frontend/cli/utils.go index 1d94edd0..b7ce01c3 100644 --- a/internal/frontend/cli/utils.go +++ b/internal/frontend/cli/utils.go @@ -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") diff --git a/internal/frontend/frontend.go b/internal/frontend/frontend.go index a82d6186..cca77eaa 100644 --- a/internal/frontend/frontend.go +++ b/internal/frontend/frontend.go @@ -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) } } diff --git a/internal/frontend/qml/BridgeUI/DialogPortChange.qml b/internal/frontend/qml/BridgeUI/DialogPortChange.qml index d5c176e0..078be9f8 100644 --- a/internal/frontend/qml/BridgeUI/DialogPortChange.qml +++ b/internal/frontend/qml/BridgeUI/DialogPortChange.qml @@ -226,7 +226,7 @@ Dialog { target: timer onTriggered: { go.setPortsAndSecurity(imapPort.text, smtpPort.text, securitySMTPSTARTTLS.checked) - go.isRestarting = true + go.setToRestart() Qt.quit() } } diff --git a/internal/frontend/qml/GuiIE.qml b/internal/frontend/qml/GuiIE.qml index 39d2af7a..1cc23ac1 100644 --- a/internal/frontend/qml/GuiIE.qml +++ b/internal/frontend/qml/GuiIE.qml @@ -69,20 +69,9 @@ Item { Connections { target: go - - - - - - - - - - - - - - + onShowWindow : { + winMain.showAndRise() + } onProcessFinished : { winMain.dialogAddUser.hide() diff --git a/internal/frontend/qml/tst_Gui.qml b/internal/frontend/qml/tst_Gui.qml index 4377f47b..4acbb395 100644 --- a/internal/frontend/qml/tst_Gui.qml +++ b/internal/frontend/qml/tst_Gui.qml @@ -566,7 +566,6 @@ Window { return 0 } - property bool isRestarting: false function setPortsAndSecurity(portIMAP, portSMTP, secSMTP) { console.log("Test: ports changed", portIMAP, portSMTP, secSMTP) } diff --git a/internal/frontend/qt-common/accounts.go b/internal/frontend/qt-common/accounts.go index 653f1cee..aa66d2f2 100644 --- a/internal/frontend/qt-common/accounts.go +++ b/internal/frontend/qt-common/accounts.go @@ -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() } diff --git a/internal/frontend/qt-common/common.go b/internal/frontend/qt-common/common.go index f8b19cd8..4599ed1e 100644 --- a/internal/frontend/qt-common/common.go +++ b/internal/frontend/qt-common/common.go @@ -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 } diff --git a/internal/frontend/qt-ie/frontend.go b/internal/frontend/qt-ie/frontend.go index 82632c2c..58c5c7ef 100644 --- a/internal/frontend/qt-ie/frontend.go +++ b/internal/frontend/qt-ie/frontend.go @@ -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. diff --git a/internal/frontend/qt-ie/frontend_nogui.go b/internal/frontend/qt-ie/frontend_nogui.go index c9bd1268..aef4ba69 100644 --- a/internal/frontend/qt-ie/frontend_nogui.go +++ b/internal/frontend/qt-ie/frontend_nogui.go @@ -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{} } diff --git a/internal/frontend/qt-ie/ui.go b/internal/frontend/qt-ie/ui.go index 10bbfadb..f8b59a38 100644 --- a/internal/frontend/qt-ie/ui.go +++ b/internal/frontend/qt-ie/ui.go @@ -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) diff --git a/internal/frontend/qt/accounts.go b/internal/frontend/qt/accounts.go index ca0563b6..3d9c44a4 100644 --- a/internal/frontend/qt/accounts.go +++ b/internal/frontend/qt/accounts.go @@ -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() } diff --git a/internal/frontend/qt/frontend.go b/internal/frontend/qt/frontend.go index 80741789..7889b8d9 100644 --- a/internal/frontend/qt/frontend.go +++ b/internal/frontend/qt/frontend.go @@ -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. } diff --git a/internal/frontend/qt/frontend_nogui.go b/internal/frontend/qt/frontend_nogui.go index 1e1c9863..127553a4 100644 --- a/internal/frontend/qt/frontend_nogui.go +++ b/internal/frontend/qt/frontend_nogui.go @@ -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{} } diff --git a/internal/frontend/qt/ui.go b/internal/frontend/qt/ui.go index 954074b3..dbffa3db 100644 --- a/internal/frontend/qt/ui.go +++ b/internal/frontend/qt/ui.go @@ -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) diff --git a/internal/frontend/types/types.go b/internal/frontend/types/types.go index 2bab2c2f..5da0522f 100644 --- a/internal/frontend/types/types.go +++ b/internal/frontend/types/types.go @@ -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) diff --git a/internal/imap/backend.go b/internal/imap/backend.go index aa6cb554..545aceb1 100644 --- a/internal/imap/backend.go +++ b/internal/imap/backend.go @@ -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{}, diff --git a/internal/imap/bridge.go b/internal/imap/bridge.go index 825482df..e950bd59 100644 --- a/internal/imap/bridge.go +++ b/internal/imap/bridge.go @@ -23,8 +23,7 @@ import ( "github.com/ProtonMail/proton-bridge/pkg/pmapi" ) -type configProvider interface { - GetEventsPath() string +type cacheProvider interface { GetDBDir() string GetIMAPCachePath() string } diff --git a/internal/imap/server.go b/internal/imap/server.go index b391990a..d338abf0 100644 --- a/internal/imap/server.go +++ b/internal/imap/server.go @@ -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", diff --git a/internal/importexport/credits.go b/internal/importexport/credits.go index b8973fbb..5be3cb3d 100644 --- a/internal/importexport/credits.go +++ b/internal/importexport/credits.go @@ -15,8 +15,8 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// 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;" diff --git a/internal/importexport/importexport.go b/internal/importexport/importexport.go index f5b95e74..a3799c73 100644 --- a/internal/importexport/importexport.go +++ b/internal/importexport/importexport.go @@ -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) } diff --git a/internal/importexport/types.go b/internal/importexport/types.go index 38eda195..763a23ca 100644 --- a/internal/importexport/types.go +++ b/internal/importexport/types.go @@ -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 } diff --git a/internal/locations/locations.go b/internal/locations/locations.go new file mode 100644 index 00000000..f3186f0a --- /dev/null +++ b/internal/locations/locations.go @@ -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 . + +// 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/ +// - logs: ~/.cache/protonmail//logs +// - cache: ~/.cache/protonmail//cache +// - updates: ~/.cache/protonmail//updates +// - lockfile: ~/.cache/protonmail//.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///.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//). +// 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///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///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///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() +} diff --git a/internal/locations/locations_test.go b/internal/locations/locations_test.go new file mode 100644 index 00000000..50961cff --- /dev/null +++ b/internal/locations/locations_test.go @@ -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 . + +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()) + } +} diff --git a/internal/logging/clear.go b/internal/logging/clear.go new file mode 100644 index 00000000..667a3ccf --- /dev/null +++ b/internal/logging/clear.go @@ -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 . + +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") + } +} diff --git a/internal/logging/crash.go b/internal/logging/crash.go new file mode 100644 index 00000000..b6cc0a6f --- /dev/null +++ b/internal/logging/crash.go @@ -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 . + +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) +} diff --git a/internal/logging/logging.go b/internal/logging/logging.go new file mode 100644 index 00000000..f2bd68a1 --- /dev/null +++ b/internal/logging/logging.go @@ -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 . + +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) +} diff --git a/internal/logging/logging_test.go b/internal/logging/logging_test.go new file mode 100644 index 00000000..1fba324c --- /dev/null +++ b/internal/logging/logging_test.go @@ -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 . + +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 +} diff --git a/internal/logging/rotator.go b/internal/logging/rotator.go new file mode 100644 index 00000000..9ed45065 --- /dev/null +++ b/internal/logging/rotator.go @@ -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 . + +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 +} diff --git a/internal/logging/rotator_test.go b/internal/logging/rotator_test.go new file mode 100644 index 00000000..a87364ef --- /dev/null +++ b/internal/logging/rotator_test.go @@ -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 . + +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 + } +} diff --git a/internal/smtp/backend.go b/internal/smtp/backend.go index 73c87e14..50f5a982 100644 --- a/internal/smtp/backend.go +++ b/internal/smtp/backend.go @@ -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) { diff --git a/internal/smtp/server.go b/internal/smtp/server.go index c3013ebb..796e6535 100644 --- a/internal/smtp/server.go +++ b/internal/smtp/server.go @@ -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"). diff --git a/internal/transfer/provider_imap_utils.go b/internal/transfer/provider_imap_utils.go index 8c622933..c8d50782 100644 --- a/internal/transfer/provider_imap_utils.go +++ b/internal/transfer/provider_imap_utils.go @@ -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" diff --git a/internal/transfer/provider_pmapi.go b/internal/transfer/provider_pmapi.go index fd16dc0f..202035ee 100644 --- a/internal/transfer/provider_pmapi.go +++ b/internal/transfer/provider_pmapi.go @@ -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, diff --git a/internal/transfer/provider_pmapi_target.go b/internal/transfer/provider_pmapi_target.go index 4ce7443c..445bcfee 100644 --- a/internal/transfer/provider_pmapi_target.go +++ b/internal/transfer/provider_pmapi_target.go @@ -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 } diff --git a/internal/transfer/provider_pmapi_test.go b/internal/transfer/provider_pmapi_test.go index f7a7587a..e1f67b40 100644 --- a/internal/transfer/provider_pmapi_test.go +++ b/internal/transfer/provider_pmapi_test.go @@ -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) diff --git a/internal/updates/updates_qa.go b/internal/updater/channel_beta.go similarity index 84% rename from internal/updates/updates_qa.go rename to internal/updater/channel_beta.go index 365e217c..6463ee4b 100644 --- a/internal/updates/updates_qa.go +++ b/internal/updater/channel_beta.go @@ -15,12 +15,8 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// +build build_qa +// +build beta -package updates +package updater -func init() { - Host = "https://bridgeteam.protontech.ch" - DownloadPath = "download/qa" - BuildType = "QA" -} +const Channel = "beta" diff --git a/internal/importexport/release_notes.go b/internal/updater/channel_default.go similarity index 70% rename from internal/importexport/release_notes.go rename to internal/updater/channel_default.go index 57be8f5f..b9878af6 100644 --- a/internal/importexport/release_notes.go +++ b/internal/updater/channel_default.go @@ -15,14 +15,10 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// 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" diff --git a/internal/updater/host_default.go b/internal/updater/host_default.go new file mode 100644 index 00000000..7d3edee3 --- /dev/null +++ b/internal/updater/host_default.go @@ -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 . + +// +build !pmapi_qa + +package updater + +const Host = "https://protonmail.com/download" diff --git a/internal/updater/host_qa.go b/internal/updater/host_qa.go new file mode 100644 index 00000000..e8fb8b5b --- /dev/null +++ b/internal/updater/host_qa.go @@ -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 . + +// +build pmapi_qa + +package updater + +const Host = "https://bridgeteam.protontech.ch/bridgeteam/autoupdates/download" diff --git a/internal/updater/install_darwin.go b/internal/updater/install_darwin.go new file mode 100644 index 00000000..daf0e9ec --- /dev/null +++ b/internal/updater/install_darwin.go @@ -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 . + +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) +} diff --git a/internal/updater/install_default.go b/internal/updater/install_default.go new file mode 100644 index 00000000..111a6294 --- /dev/null +++ b/internal/updater/install_default.go @@ -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 . + +// +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) +} diff --git a/internal/updates/bridge_pubkey.gpg b/internal/updater/key_default.go similarity index 68% rename from internal/updates/bridge_pubkey.gpg rename to internal/updater/key_default.go index dfa40e8c..4ae95c7b 100644 --- a/internal/updates/bridge_pubkey.gpg +++ b/internal/updater/key_default.go @@ -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 . -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-----` diff --git a/internal/updater/key_qa.go b/internal/updater/key_qa.go new file mode 100644 index 00000000..b5246843 --- /dev/null +++ b/internal/updater/key_qa.go @@ -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 . + +// +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-----` diff --git a/internal/bridge/release_notes.go b/internal/updater/locker.go similarity index 55% rename from internal/bridge/release_notes.go rename to internal/updater/locker.go index e3a9e2c6..77a8bd95 100644 --- a/internal/bridge/release_notes.go +++ b/internal/updater/locker.go @@ -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 . -// 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() +} diff --git a/internal/updater/locker_test.go b/internal/updater/locker_test.go new file mode 100644 index 00000000..4e3544f9 --- /dev/null +++ b/internal/updater/locker_test.go @@ -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 . + +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() +} diff --git a/internal/updates/sync.go b/internal/updater/sync.go similarity index 85% rename from internal/updates/sync.go rename to internal/updater/sync.go index e59e45a4..d316cc74 100644 --- a/internal/updates/sync.go +++ b/internal/updater/sync.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -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 diff --git a/internal/updates/sync_test.go b/internal/updater/sync_test.go similarity index 85% rename from internal/updates/sync_test.go rename to internal/updater/sync_test.go index 2a390ebb..2a427f39 100644 --- a/internal/updates/sync_test.go +++ b/internal/updater/sync_test.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -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 } diff --git a/internal/updater/updater.go b/internal/updater/updater.go new file mode 100644 index 00000000..045623ba --- /dev/null +++ b/internal/updater/updater.go @@ -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 . + +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 +} diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go new file mode 100644 index 00000000..ee2cec8d --- /dev/null +++ b/internal/updater/updater_test.go @@ -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 . + +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 +} diff --git a/internal/updater/version.go b/internal/updater/version.go new file mode 100644 index 00000000..0dc68cb2 --- /dev/null +++ b/internal/updater/version.go @@ -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 . + +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) +} diff --git a/internal/updates/compare_versions.go b/internal/updates/compare_versions.go deleted file mode 100644 index ff83b94c..00000000 --- a/internal/updates/compare_versions.go +++ /dev/null @@ -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 . - -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 -} diff --git a/internal/updates/compare_versions_test.go b/internal/updates/compare_versions_test.go deleted file mode 100644 index af3fe6c3..00000000 --- a/internal/updates/compare_versions_test.go +++ /dev/null @@ -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 . - -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) - } -} diff --git a/internal/updates/downloader.go b/internal/updates/downloader.go deleted file mode 100644 index 97748f00..00000000 --- a/internal/updates/downloader.go +++ /dev/null @@ -1,131 +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 . - -package updates - -import ( - "errors" - "io" - "io/ioutil" - "net/http" - "os" - "path/filepath" - "strconv" - - "github.com/ProtonMail/proton-bridge/pkg/dialer" -) - -func mkdirAllClear(path string) error { - if err := os.RemoveAll(path); err != nil { - return err - } - return os.MkdirAll(path, 0750) -} - -func downloadToBytes(path string) (out []byte, err error) { - var ( - client *http.Client - response *http.Response - ) - client = dialer.DialTimeoutClient() - log.WithField("path", path).Trace("Downloading") - - response, err = client.Get(path) - if err != nil { - return - } - out, err = ioutil.ReadAll(response.Body) - _ = response.Body.Close() - if response.StatusCode < http.StatusOK || http.StatusIMUsed < response.StatusCode { - err = errors.New(path + " " + response.Status) - } - return -} - -func downloadWithProgress(status *Progress, sourceURL, targetPath string) (err error) { - targetFile, err := os.Create(targetPath) - if err != nil { - log.Warnf("Cannot create update file %s: %v", targetPath, err) - return - } - defer targetFile.Close() //nolint[errcheck] - - var ( - client *http.Client - response *http.Response - ) - client = dialer.DialTimeoutClient() - response, err = client.Get(sourceURL) - if err != nil { - return - } - defer response.Body.Close() //nolint[errcheck] - - contentLength, _ := strconv.ParseUint(response.Header.Get("Content-Length"), 10, 64) - - wc := WriteCounter{ - Status: status, - Target: targetFile, - Size: contentLength, - } - - err = wc.ReadAll(response.Body) - return -} - -func downloadWithSignature(status *Progress, sourceURL, targetDir string) (localPath string, err error) { - localPath = filepath.Join(targetDir, filepath.Base(sourceURL)) - - if err = downloadWithProgress(nil, sourceURL+sigExtension, localPath+sigExtension); err != nil { - return - } - - if err = downloadWithProgress(status, sourceURL, localPath); err != nil { - return - } - return -} - -type WriteCounter struct { - Status *Progress - Target io.Writer - processed, Size, counter uint64 -} - -func (s *WriteCounter) ReadAll(source io.Reader) (err error) { - s.counter = uint64(0) - if s.Target == nil { - return errors.New("can not read all, target unset") - } - if source == nil { - return errors.New("can not read all, source unset") - } - _, err = io.Copy(s.Target, io.TeeReader(source, s)) - return -} - -func (s *WriteCounter) Write(p []byte) (int, error) { - if s.Status != nil && s.Size != 0 { - s.processed += uint64(len(p)) - fraction := float32(s.processed) / float32(s.Size) - if s.counter%uint64(100) == 0 || fraction == 1. { - s.Status.UpdateProcessed(fraction) - } - } - s.counter++ - return len(p), nil -} diff --git a/internal/updates/progress.go b/internal/updates/progress.go deleted file mode 100644 index 462643da..00000000 --- a/internal/updates/progress.go +++ /dev/null @@ -1,50 +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 . - -package updates - -const ( - InfoCurrentVersion = 1 + iota - InfoDownloading - InfoVerifying - InfoUnpacking - InfoUpgrading - InfoQuitApp - InfoRestartApp -) - -type Progress struct { - Processed float32 // fraction of finished procedure [0.0-1.0] - Description int // description by code (needs to be translated anyway) - Err error // occurred error - channel chan<- Progress -} - -func (s *Progress) Update() { - s.channel <- *s -} - -func (s *Progress) UpdateDescription(description int) { - s.Description = description - s.Processed = 0 - s.Update() -} - -func (s *Progress) UpdateProcessed(processed float32) { - s.Processed = processed - s.Update() -} diff --git a/internal/updates/signature.go b/internal/updates/signature.go deleted file mode 100644 index dbf6d37f..00000000 --- a/internal/updates/signature.go +++ /dev/null @@ -1,108 +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 . - -package updates - -import ( - "bytes" - "encoding/hex" - "errors" - "io" - "os" - "os/exec" - "runtime" - - "golang.org/x/crypto/openpgp" -) - -// gpg --export D51E64D3E63EDC3EEF7864CEE2C75D68E6234B07 | xxd -p | tr -d '\n' | xclip -const ( - keyID = "D51E64D3E63EDC3EEF7864CEE2C75D68E6234B07" - pubkeyHex = "99020d045a3d39e1011000be7cfacb714058f9851ce5888cad8250ea882b2563060b4d21f5f02fdcfb2b1e4073d33edc4050f235d35ab689050ed6435f5d79334bf30f093936472398eec259b7fa9c265cc18a8e4ab0681fcc0d4fdb934bfea9477007935af70bfdfa406de25b1e96838c9c4645d6613a13dffca1e70684e416cb8bff5348101d7c9cd3cb1b78229646dce4cc9cec2ecfa78d456547900e3c089c9d592e8cf33fa322c07016ae273880a1bcb8c8178d8fb804f555a8826129b2e2c535631c56a1fe73f476345e0851a5deda508833008b1751b6845e1ff788264350c3792f0932027fabe63dd230dce4da1b45f15eea584f25758355ae9784c32a2bd31d70333a5b6ff0b863cc177bcacfd35774029887551113cec424d9eb1f5ee4ab042b69c8b73a113d6596e88bdac55451e9403ee7944253b26177cbd97f79f22d138010a2e9044f5f16cf8c23ec7755332cf09250d50efbfedfb256426b1dba775af591b1f324b00dd497abeabc681036848954825139c7832902ab9ace0b73d270611d39a222e07e5ce98159acb99dcf8d3d62624458b2883d5feee43a48f981a601f01ebeb8e430181004e9990fc1a9d7d2e746d9aa8d5876ad576bf327399c08e834e6706a73300f9bc258f51510b597b9b34506ff21a993311d9a961ab07cc7c86476088d9aecaab31cc198e1091d62e5bdb161bc784879d4fca5a53fb292ffa89996a77101e10011010001b44c50726f746f6e20546563686e6f6c6f67696573204147202850726f746f6e4d61696c2042726964676520646576656c6f7065727329203c6272696467654070726f746f6e6d61696c2e63683e89025404130108003e021b03050b09080702061508090a0b020416020301021e01021780162104d51e64d3e63edc3eef7864cee2c75d68e6234b0705025bf7efbc050903bcdedb000a0910e2c75d68e6234b07e19e0fff58d859aabd12b6e37829b61d406f8563e68b8a32e18952378b88fac148acfbb51614d31419e3ec054ac5ef33abce8e15a75e85491861a6f7886c3315260e321a5cf037e8d9afd141bc5f3391b3aa8da8e44c5deb3b2a92402a5956079a991b7a7388064133b07f0c41e0ae66560e2bbfae484882a32d4b42676ee0a43e9e26cdb92e1c9942723ae491e91c39156abee84369db5afd2d9f6c2bad428113d851339267d7119a1d892b6313de1cd0c7aeac495036cf7c985c6b9e0cf7d6ecf50ba6dca913d56594e7fd2dac0d15f1e5196bddf3c9d2cdca724972a6ea294601bce9e9ccbff497785d7df9a8969517585ae514f94e2d54252b829cea842a4b62960724901e0f64953b68d69f5ca4d644dec3b9f947e57b25d6d1d37a0dc53fc62c2860a27ffb1e09658b6fa87fe43fb82f7b6a339c4b0b330ea4d906683b51a2844a685e939ecece3af8447f606c78645da77b66e6627ce838e6416c65f0b0c65335a6bd4091db7356f3f638e320505cd739ab762d27b1b8e5bcbba011821b49bfd63f4c0ac06ebacd36adb25448436ba795424d9d66f413854b833f72ab9ed0a6fc52ec3df5d9d655d0d9c0c21c8323ee785c08af5d341bd1f0067dc81bb4a74aa496f575c00c6fd58e067a6b04aed72ec59b263e6fe6707702e59eafb361532241fe881642da91518d5b01c238701d7317062afcf2f4b1b7c01c3c7164417a5e18b9020d045a3d39e1011000b74f40e9514f5a261e58f19cdeb88c5b835d74886facea681bd4c1d6280e957675f466cc6925b02540ed61d70660d66d47bd6af80edce3bb00dc3afc9cf36233fe8f0226a4983b712a93a96f00d54e9361f1975545776bfa1b204b6d97b6c8f41b1436098bb01608c07a2ffa097f6a32907402d81c29aadc18faa3b0417963e956993d7738dcb0b55b98db333463f5224e5d284d236548ba87e62856e244f68ffe4fe8f1a2a151a5736c66a58e10a9821c3cfa4d3b35893a7e0a867fe60d1e2a987a5c9f93b8da2a17de3dd48604167f68c73b2820191ec1363f7c95a65dffbe727a41383ad92c5fc6a1115cecc5bd29a7a5f6f990d09ee68ccd9b16cdf4f251f4ad1adf99d94b014e622a03efd1a9319e79d4aaf09ca1e905314443b4fdf3d3d2995ee38c9edf37e2eb5ded62def8b643e9dc6cfab3e1c83896f3413c05d973d49a3e73683ab6820dfae0fbfdfe60c1a65a5e1241d283bb7d6495568f87269ad606799ff5fae02aad4e712d0136f03ee008a9da1845a962406d44170b231a794d964d6233cb4150e92105172fd133c3b93b51c7b03623b4889cf5027f3ea483d2a37b2cc9648d1f00756bc0c66f798b91a9c4364e0e576c3a779eba69afb27dc8d630a850b4e293b88955d46e635577bdfb1802479f18dfe7da06b732f6ad0898a9c28086b0c8145c64dda245cea8a060301b812c29318f6c25a25d91f69f11001101000189023c041801080026021b0c162104d51e64d3e63edc3eef7864cee2c75d68e6234b0705025bf7f281050903bce1a0000a0910e2c75d68e6234b07b7930ffe31f08ec8ecc415eb767c8434a1262c528b8f5b7d77b293396b672ac06ea36bec9ac82b3817e45d474d4d091779be54692196930895317f8cbabd7f5bf3991ba23efbb1fe322d5d86e71c58619457a4c19917c789d2e62c23a4c0ede82d4445fb6ff99e721caf75999b2be9e35c7c0e9578874a0507a796d66f0d5b2a1d2544b03861b0a278ef7b5abb0b8f5e4b3b72aabcb6c4a05731f344ef8257a1d5d5bdccba2cb9640ef7442d68206613979cc8935334876148c827df7d3044859fdd00c12bcf072881303e100abe3a5ca94e2c36497643471e6c43c4fed35f24777cee259e0f873243a7343adbbce1cbed4ce073d838733f9e2a4e9281a5f43f2aac56ff0c472843a07814a5515ae809ed976d0699ebce1f5e5661fd6752f22af8521cc485ea2925bc8c650865dab398fbd64460fd873f687fd2b7db55d1920fd5787010063eba5d4b08fd9882e9c2244270886f8c6411194d4e55d207e374d6bf9ea3463ce4db2f2e6818f57ac964f76f79b1df0b9dc3e688f0c5d73f33010809f9ed2effc6e4387ce0f1eb634e7a67bf04e9e30126de137999e7fdea05b9ee6088154d8369e5fac81b7c0af16d6be8d636bd84348812333822319cd0e362ab06969032b57f9233e618ec9f67d5d65a52c51f5f942cf83f5522bfe3de3bab39b3de867f5f6a108e0b661789c2a049b990c812f2b1b1722d3e403299f969eb34a9dc21af" -) - -var ( - pubkeyRing = openpgp.EntityList{} //nolint[gochecknoglobals] -) - -func singAndVerify(pathToFile string) (err error) { - err = signFile(pathToFile) - if err != nil { - err = verifyFile(pathToFile) - } - return -} - -func signFile(pathToFile string) (err error) { - if runtime.GOOS != "linux" { //nolint[goconst] - return errors.New("tar not implemented only for linux") - } - // assuming gpg detach-sign creates file with suffix .sig by default. - // Lstat does not follow the link i.e. only link is deleted (not link target). - if _, err := os.Lstat(pathToFile + sigExtension); !os.IsNotExist(err) { - _ = os.Remove(pathToFile + sigExtension) - } - cmd := exec.Command("gpg", "--local-user", keyID, "--detach-sign", pathToFile) //nolint[gosec] - return cmd.Run() -} - -func verifyFile(pathToFile string) error { - fileReader, err := os.Open(pathToFile) //nolint[gosec] - if err != nil { - return err - } - defer fileReader.Close() //nolint[errcheck] - - signatureReader, err := os.Open(pathToFile + sigExtension) //nolint[gosec] - if err != nil { - return err - } - defer signatureReader.Close() //nolint[errcheck] - - return verifyBytes(fileReader, signatureReader) -} - -func verifyBytes(fileReader, signatureReader io.Reader) (err error) { - if _, err = getPubKey(); err != nil { - return err - } - - _, err = openpgp.CheckDetachedSignature(pubkeyRing, fileReader, signatureReader, nil) - /* - if err != nil { - return err - } - - if signer == nil || signer.PrimaryKey.KeyId != keyID { - return errors.New("Signer with wrong key ID") - } - */ - return -} - -// from opengpg/read_test.go -func getPubKey() (el openpgp.EntityList, err error) { - if pubkeyRing != nil && len(pubkeyRing) != 0 { - return pubkeyRing, nil - } - data, err := hex.DecodeString(pubkeyHex) - if err != nil { - return - } - pubkeyRing, err = openpgp.ReadKeyRing(bytes.NewBuffer(data)) - return pubkeyRing, err -} diff --git a/internal/updates/tar.go b/internal/updates/tar.go deleted file mode 100644 index c34ec24a..00000000 --- a/internal/updates/tar.go +++ /dev/null @@ -1,126 +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 . - -package updates - -import ( - "archive/tar" - "compress/gzip" - "errors" - "io" - "os" - "os/exec" - "path/filepath" - "runtime" - - "github.com/sirupsen/logrus" -) - -func createTar(tarPath, sourcePath string) error { //nolint[unused] - if runtime.GOOS != "linux" { - return errors.New("tar not implemented only for linux") - } - // Check whether it exists and is a directory. - if _, err := os.Lstat(sourcePath); err != nil { - return err - } - - absPath, err := filepath.Abs(tarPath) - if err != nil { - return err - } - - cmd := exec.Command("tar", "-zvcf", absPath, filepath.Base(sourcePath)) //nolint[gosec] - cmd.Dir = filepath.Dir(sourcePath) - cmd.Stderr = log.WriterLevel(logrus.ErrorLevel) - cmd.Stdout = log.WriterLevel(logrus.InfoLevel) - return cmd.Run() -} - -func untarToDir(tarPath, targetDir string, status *Progress) error { //nolint[funlen] - // Check whether it exists and is a directory. - if ls, err := os.Lstat(targetDir); err == nil { - if !ls.IsDir() { - return errors.New("not a dir") - } - } else { - return err - } - - tgzReader, err := os.Open(tarPath) //nolint[gosec] - if err != nil { - return err - } - defer tgzReader.Close() //nolint[errcheck] - - size := uint64(0) - if info, err := tgzReader.Stat(); err == nil { - size = uint64(info.Size()) - } - - wc := &WriteCounter{ - Status: status, - Size: size, - } - - tarReader, err := gzip.NewReader(io.TeeReader(tgzReader, wc)) - if err != nil { - return err - } - - fileReader := tar.NewReader(tarReader) - for { - header, err := fileReader.Next() - if err == io.EOF { - break - } - if err != nil { - return err - } - if header == nil { - continue - } - - targetFile := filepath.Join(targetDir, header.Name) - info := header.FileInfo() - - // Create symlink. - if header.Typeflag == tar.TypeSymlink { - if header.Linkname == "" { - return errors.New("missing linkname") - } - if err := os.Symlink(header.Linkname, targetFile); err != nil { - return err - } - continue - } - - // Handle case that it is a directory. - if info.IsDir() { - if err := os.MkdirAll(targetFile, info.Mode()); err != nil { - return err - } - continue - } - - // Handle case that it is a regular file. - if err := copyToFileTruncate(fileReader, targetFile, info.Mode()); err != nil { - return err - } - } - return nil -} diff --git a/internal/updates/testdata/current_version_linux.json b/internal/updates/testdata/current_version_linux.json deleted file mode 100644 index 9ed70664..00000000 --- a/internal/updates/testdata/current_version_linux.json +++ /dev/null @@ -1 +0,0 @@ -{"Version":"1.1.6","ReleaseDate":"10 Jul 19 11:02 +0200","ReleaseNotes":"• Necessary updates reflecting API changes\n• Report wrongly formated messages\n","ReleaseFixedBugs":"• Fixed verification for contacts signed by older or missing key\n• Outlook always shows attachment icon\n","FixedBugs":["• Fixed verification for contacts signed by older or missing key","• Outlook always shows attachment icon",""],"URL":"https://protonmail.com/download/Bridge-Installer.sh","LandingPage":"https://protonmail.com/bridge/download","UpdateFile":"https://protonmail.com/download/bridge_upgrade_linux.tgz","InstallerFile":"https://protonmail.com/download/Bridge-Installer.sh","DebFile":"https://protonmail.com/download/protonmail-bridge_1.1.6-1_amd64.deb","RpmFile":"https://protonmail.com/download/protonmail-bridge-1.1.6-1.x86_64.rpm","PkgFile":"https://protonmail.com/download/PKGBUILD"} \ No newline at end of file diff --git a/internal/updates/testdata/current_version_linux.json.sig b/internal/updates/testdata/current_version_linux.json.sig deleted file mode 100644 index 2f82c56a9d1537ff0c263d78197e099297166043..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 566 zcmV-60?GY}0y6{v0SEvc79j-H9%R$zKHNU2(ea{N%wY9=n!TavXDU2LKqdM>EgM6#0fW1xPoANVTFh zAK2b!UMeVSvD$PgA#V-F`Q!E8MnMO^yLu^87N?|3<1%zvrmBph!56Nb_>oreR^Jr& zrBrKUMwBHmzC?B2x~5_4Y2ysmk>Kge7bof9UphM8mY__AFNy>dtN1+TU2a%<&i4@c zE=2L{;mH{rI6;Q1Zt}ch_JNO@;Mw_X(CzH~ve%&3>DPsD>nh0PSn>XLzms*|gSfge zfyI++QX6}Zj<3y%2mflhg~p|0W{Lr-K1Iu2eSKG!QH0qSR~~~2m_w4)Cv!)AVoSO> z2M^_%CkParR*Jc>W{#t2e|=GW9=Htv<-;UDrX}&!*W|vz82r08)IhAX?WI)!-R)=9 z@T0sBJ@Q91x4YToPC#lJf%uun46xG|Ga-EoRB_c(v|<2k=oOpkUT|Bp!#THxw&}q0 z4u)gvF*}gT##DYKAgy*!ZF-gFL7zI2V-i`wn)XuJGPA;Rr&*YPD&n!r5zr8$!hUpK E@ZBmMQUCw| diff --git a/internal/updates/updates.go b/internal/updates/updates.go deleted file mode 100644 index 5666388a..00000000 --- a/internal/updates/updates.go +++ /dev/null @@ -1,349 +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 . - -package updates - -import ( - "bytes" - "encoding/json" - "errors" - "io/ioutil" - "os/exec" - "path/filepath" - "runtime" - "strings" - - "github.com/ProtonMail/proton-bridge/internal/bridge" - "github.com/ProtonMail/proton-bridge/internal/importexport" - "github.com/ProtonMail/proton-bridge/pkg/constants" - "github.com/kardianos/osext" - "github.com/sirupsen/logrus" -) - -const ( - sigExtension = ".sig" -) - -var ( - Host = "https://protonmail.com" //nolint[gochecknoglobals] - DownloadPath = "download" //nolint[gochecknoglobals] - - // BuildType specifies type of build (e.g. QA or beta). - BuildType = "" //nolint[gochecknoglobals] -) - -var ( - log = logrus.WithField("pkg", "bridgeUtils/updates") //nolint[gochecknoglobals] - - ErrDownloadFailed = errors.New("error happened during download") //nolint[gochecknoglobals] - ErrUpdateVerifyFailed = errors.New("cannot verify signature") //nolint[gochecknoglobals] -) - -type Updates struct { - version string - revision string - buildTime string - releaseNotes string - releaseFixedBugs string - updateTempDir string - landingPagePath string // Based on Host/; default landing page for download. - winInstallerFile string // File for initial install or manual reinstall for windows - macInstallerFile string // File for initial install or manual reinstall for mac - linInstallerFile string // File for initial install or manual reinstall for linux - versionFileBaseName string // Text file containing information about current file. per goos [_linux,_darwin,_windows].json (have .sig file). - updateFileBaseName string // File for automatic update. per goos [_linux,_darwin,_windows].tgz (have .sig file). - linuxFileBaseName string // Prefix of linux package names. - macAppBundleName string // Name of Mac app file in the bundle for update procedure. - cachedNewerVersion *VersionInfo // To have info about latest version even when the internet connection drops. -} - -// NewBridge inits Updates struct for bridge. -func NewBridge(updateTempDir string) *Updates { - return &Updates{ - version: constants.Version, - revision: constants.Revision, - buildTime: constants.BuildTime, - releaseNotes: bridge.ReleaseNotes, - releaseFixedBugs: bridge.ReleaseFixedBugs, - updateTempDir: updateTempDir, - landingPagePath: "bridge/download", - winInstallerFile: "Bridge-Installer.exe", - macInstallerFile: "Bridge-Installer.dmg", - linInstallerFile: "Bridge-Installer.sh", - versionFileBaseName: "current_version", - updateFileBaseName: "bridge_upgrade", - linuxFileBaseName: "protonmail-bridge", - macAppBundleName: "ProtonMail Bridge.app", - } -} - -// NewImportExport inits Updates struct for import-export. -func NewImportExport(updateTempDir string) *Updates { - return &Updates{ - version: constants.Version, - revision: constants.Revision, - buildTime: constants.BuildTime, - releaseNotes: importexport.ReleaseNotes, - releaseFixedBugs: importexport.ReleaseFixedBugs, - updateTempDir: updateTempDir, - landingPagePath: "import-export", - winInstallerFile: "ie/Import-Export-app-installer.exe", - macInstallerFile: "ie/Import-Export-app.dmg", - linInstallerFile: "ie/Import-Export-app-installer.sh", - versionFileBaseName: "current_version_ie", - updateFileBaseName: "ie/ie_upgrade", - linuxFileBaseName: "ie/protonmail-import-export-app", - macAppBundleName: "ProtonMail Import-Export app.app", - } -} - -func (u *Updates) CreateJSONAndSign(deployDir, goos string) error { - versionInfo := u.getLocalVersion(goos) - versionInfo.Version = sanitizeVersion(versionInfo.Version) - - versionFileName := filepath.Base(u.versionFileURL(goos)) - versionFilePath := filepath.Join(deployDir, versionFileName) - - txt, err := json.Marshal(versionInfo) - if err != nil { - return err - } - - if err = ioutil.WriteFile(versionFilePath, txt, 0600); err != nil { - return err - } - - if err := singAndVerify(versionFilePath); err != nil { - return err - } - - updateFileName := filepath.Base(versionInfo.UpdateFile) - updateFilePath := filepath.Join(deployDir, updateFileName) - if err := singAndVerify(updateFilePath); err != nil { - return err - } - - return nil -} - -func (u *Updates) CheckIsUpToDate() (isUpToDate bool, latestVersion VersionInfo, err error) { - localVersion := u.GetLocalVersion() - latestVersion, err = u.getLatestVersion() - if err != nil { - return - } - - localIsOld, err := isFirstVersionNewer(latestVersion.Version, localVersion.Version) - return !localIsOld, latestVersion, err -} - -func (u *Updates) GetDownloadLink() string { - latestVersion, err := u.getLatestVersion() - if err != nil || latestVersion.InstallerFile == "" { - localVersion := u.GetLocalVersion() - return localVersion.GetDownloadLink() - } - return latestVersion.GetDownloadLink() -} - -func (u *Updates) GetLocalVersion() VersionInfo { - return u.getLocalVersion(runtime.GOOS) -} - -func (u *Updates) getLocalVersion(goos string) VersionInfo { - version := u.version - if BuildType != "" { - version += " " + BuildType - } - - versionInfo := VersionInfo{ - Version: version, - Revision: u.revision, - ReleaseDate: u.buildTime, - ReleaseNotes: u.releaseNotes, - ReleaseFixedBugs: u.releaseFixedBugs, - FixedBugs: strings.Split(u.releaseFixedBugs, "\n"), - URL: u.installerFileURL(goos), - - LandingPage: u.landingPageURL(), - UpdateFile: u.updateFileURL(goos), - InstallerFile: u.installerFileURL(goos), - } - - if goos == "linux" { - pkgName := u.linuxFileBaseName - pkgRel := "1" - pkgBaseFile := strings.Join([]string{Host, DownloadPath, pkgName}, "/") - - pkgBasePath := DownloadPath + "/" + pkgName // add at least one dir - pkgBasePath = filepath.Dir(pkgBasePath) // keep only last dir - pkgBasePath = Host + "/" + pkgBasePath // add host in the end to not strip off double slash in URL - - versionInfo.DebFile = pkgBaseFile + "_" + u.version + "-" + pkgRel + "_amd64.deb" - versionInfo.RpmFile = pkgBaseFile + "-" + u.version + "-" + pkgRel + ".x86_64.rpm" - versionInfo.PkgFile = strings.Join([]string{pkgBasePath, "PKGBUILD"}, "/") - } - - return versionInfo -} - -func (u *Updates) getLatestVersion() (latestVersion VersionInfo, err error) { - version, err := downloadToBytes(u.versionFileURL(runtime.GOOS)) - if err != nil { - if u.cachedNewerVersion != nil { - return *u.cachedNewerVersion, nil - } - return - } - - signature, err := downloadToBytes(u.signatureFileURL(runtime.GOOS)) - if err != nil { - if u.cachedNewerVersion != nil { - return *u.cachedNewerVersion, nil - } - return - } - - if err = verifyBytes(bytes.NewReader(version), bytes.NewReader(signature)); err != nil { - return - } - - if err = json.NewDecoder(bytes.NewReader(version)).Decode(&latestVersion); err != nil { - return - } - if localIsOld, _ := isFirstVersionNewer(latestVersion.Version, u.version); localIsOld { - u.cachedNewerVersion = &latestVersion - } - return -} - -func (u *Updates) landingPageURL() string { - return strings.Join([]string{Host, u.landingPagePath}, "/") -} - -func (u *Updates) signatureFileURL(goos string) string { - return u.versionFileURL(goos) + sigExtension -} - -func (u *Updates) versionFileURL(goos string) string { - return strings.Join([]string{Host, DownloadPath, u.versionFileBaseName + "_" + goos + ".json"}, "/") -} - -func (u *Updates) installerFileURL(goos string) string { - installerFile := u.linInstallerFile - switch goos { - case "darwin": //nolint[goconst] - installerFile = u.macInstallerFile - case "windows": //nolint[goconst] - installerFile = u.winInstallerFile - } - return strings.Join([]string{Host, DownloadPath, installerFile}, "/") -} - -func (u *Updates) updateFileURL(goos string) string { - return strings.Join([]string{Host, DownloadPath, u.updateFileBaseName + "_" + goos + ".tgz"}, "/") -} - -func (u *Updates) StartUpgrade(currentStatus chan<- Progress) { // nolint[funlen] - status := &Progress{channel: currentStatus} - defer status.Update() - - // Get latest version. - var verInfo VersionInfo - status.UpdateDescription(InfoCurrentVersion) - if verInfo, status.Err = u.getLatestVersion(); status.Err != nil { - return - } - - if verInfo.UpdateFile == "" { - log.Warn("Empty update URL. Update manually.") - status.Err = ErrDownloadFailed - return - } - - // Download. - status.UpdateDescription(InfoDownloading) - if status.Err = mkdirAllClear(u.updateTempDir); status.Err != nil { - return - } - var updateTar string - updateTar, status.Err = downloadWithSignature( - status, - verInfo.UpdateFile, - u.updateTempDir, - ) - if status.Err != nil { - return - } - - // Check signature. - status.UpdateDescription(InfoVerifying) - status.Err = verifyFile(updateTar) - if status.Err != nil { - log.Warnf("Cannot verify update file %s: %v", updateTar, status.Err) - status.Err = ErrUpdateVerifyFailed - return - } - - // Untar. - status.UpdateDescription(InfoUnpacking) - status.Err = untarToDir(updateTar, u.updateTempDir, status) - if status.Err != nil { - return - } - - // Run upgrade (OS specific). - status.UpdateDescription(InfoUpgrading) - switch runtime.GOOS { - case "windows": //nolint[goconst] - // Cannot use filepath.Base on windows it has different delimiter - split := strings.Split(u.winInstallerFile, "/") - installerFile := split[len(split)-1] - cmd := exec.Command("./" + installerFile) // nolint[gosec] - cmd.Dir = u.updateTempDir - status.Err = cmd.Start() - case "darwin": //nolint[goconst] - // current path is better then appDir = filepath.Join("/Applications") - var exePath string - exePath, status.Err = osext.Executable() - if status.Err != nil { - return - } - localPath := filepath.Dir(exePath) // Macos - localPath = filepath.Dir(localPath) // Contents - localPath = filepath.Dir(localPath) // .app - - updatePath := filepath.Join(u.updateTempDir, u.macAppBundleName) - log.WithField("local", localPath). - WithField("update", updatePath). - Info("Syncing folders..") - status.Err = syncFolders(localPath, updatePath) - if status.Err != nil { - log.WithField("from", localPath). - WithField("to", updatePath). - WithError(status.Err). - Error("Sync failed.") - return - } - status.UpdateDescription(InfoRestartApp) - return - default: - status.Err = errors.New("upgrade for " + runtime.GOOS + " not implemented") - } - - status.UpdateDescription(InfoQuitApp) -} diff --git a/internal/updates/updates_test.go b/internal/updates/updates_test.go deleted file mode 100644 index 2bf86bf1..00000000 --- a/internal/updates/updates_test.go +++ /dev/null @@ -1,184 +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 . - -package updates - -import ( - "io/ioutil" - "net/http" - "os" - "runtime" - "testing" - - "github.com/stretchr/testify/require" -) - -const testServerPort = "8999" - -var testUpdateDir string //nolint[gochecknoglobals] - -func TestMain(m *testing.M) { - setup() - code := m.Run() - shutdown() - os.Exit(code) -} - -func setup() { - var err error - testUpdateDir, err = ioutil.TempDir("", "upgrade") - if err != nil { - panic(err) - } - - Host = "http://localhost:" + testServerPort - go startServer() -} - -func shutdown() { - _ = os.RemoveAll(testUpdateDir) -} - -func startServer() { - http.HandleFunc("/download/current_version_linux.json", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./testdata/current_version_linux.json") - }) - http.HandleFunc("/download/current_version_linux.json.sig", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./testdata/current_version_linux.json.sig") - }) - http.HandleFunc("/download/current_version_darwin.json", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./testdata/current_version_linux.json") - }) - http.HandleFunc("/download/current_version_darwin.json.sig", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./testdata/current_version_linux.json.sig") - }) - panic(http.ListenAndServe(":"+testServerPort, nil)) -} - -func TestCheckBridgeIsUpToDate(t *testing.T) { - updates := newTestUpdates("1.1.6") - isUpToDate, _, err := updates.CheckIsUpToDate() - require.NoError(t, err) - require.True(t, isUpToDate, "Bridge should be up to date") -} - -func TestCheckBridgeIsNotUpToDate(t *testing.T) { - updates := newTestUpdates("1.1.5") - isUpToDate, _, err := updates.CheckIsUpToDate() - require.NoError(t, err) - require.True(t, !isUpToDate, "Bridge should not be up to date") -} - -func TestGetLocalVersion(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("skipping test because local version for windows is currently not supported by tests.") - } - updates := newTestUpdates("1") - expectedVersion := VersionInfo{ - Version: "1", - Revision: "rev123", - ReleaseDate: "42", - ReleaseNotes: "• new feature", - ReleaseFixedBugs: "• fixed foo", - FixedBugs: []string{"• fixed foo"}, - URL: Host + "/" + DownloadPath + "/Bridge-Installer.sh", - - LandingPage: Host + "/bridge/download", - UpdateFile: Host + "/" + DownloadPath + "/bridge_upgrade_linux.tgz", - InstallerFile: Host + "/" + DownloadPath + "/Bridge-Installer.sh", - - DebFile: Host + "/" + DownloadPath + "/protonmail-bridge_1-1_amd64.deb", - RpmFile: Host + "/" + DownloadPath + "/protonmail-bridge-1-1.x86_64.rpm", - PkgFile: Host + "/" + DownloadPath + "/PKGBUILD", - } - if runtime.GOOS == "darwin" { - expectedVersion.URL = Host + "/" + DownloadPath + "/Bridge-Installer.dmg" - expectedVersion.UpdateFile = Host + "/" + DownloadPath + "/bridge_upgrade_darwin.tgz" - expectedVersion.InstallerFile = expectedVersion.URL - expectedVersion.DebFile = "" - expectedVersion.RpmFile = "" - expectedVersion.PkgFile = "" - } - version := updates.GetLocalVersion() - require.Equal(t, expectedVersion, version) -} - -func TestGetLatestVersion(t *testing.T) { - updates := newTestUpdates("1") - expectedVersion := VersionInfo{ - Version: "1.1.6", - Revision: "", - ReleaseDate: "10 Jul 19 11:02 +0200", - ReleaseNotes: "• Necessary updates reflecting API changes\n• Report wrongly formated messages\n", - ReleaseFixedBugs: "• Fixed verification for contacts signed by older or missing key\n• Outlook always shows attachment icon\n", - FixedBugs: []string{ - "• Fixed verification for contacts signed by older or missing key", - "• Outlook always shows attachment icon", - "", - }, - URL: "https://protonmail.com/download/Bridge-Installer.sh", - - LandingPage: "https://protonmail.com/bridge/download", - UpdateFile: "https://protonmail.com/download/bridge_upgrade_linux.tgz", - InstallerFile: "https://protonmail.com/download/Bridge-Installer.sh", - - DebFile: "https://protonmail.com/download/protonmail-bridge_1.1.6-1_amd64.deb", - RpmFile: "https://protonmail.com/download/protonmail-bridge-1.1.6-1.x86_64.rpm", - PkgFile: "https://protonmail.com/download/PKGBUILD", - } - version, err := updates.getLatestVersion() - require.NoError(t, err) - require.Equal(t, expectedVersion, version) -} - -func TestStartUpgrade(t *testing.T) { - if testing.Short() { - t.Skip("skipping test in short mode.") - } - if runtime.GOOS != "windows" { - t.Skip("skipping test because only upgrading on windows is currently supported by tests.") - } - - updates := newTestUpdates("1") - progress := make(chan Progress, 1) - done := make(chan error) - - go func() { - for current := range progress { - log.Infof("progress descr: %d processed %f err %v", current.Description, current.Processed, current.Err) - if current.Err != nil { - done <- current.Err - break - } - } - done <- nil - }() - - updates.StartUpgrade(progress) - close(progress) - require.NoError(t, <-done) -} - -func newTestUpdates(version string) *Updates { - u := NewBridge(testUpdateDir) - u.version = version - u.revision = "rev123" - u.buildTime = "42" - u.releaseNotes = "• new feature" - u.releaseFixedBugs = "• fixed foo" - return u -} diff --git a/internal/updates/version_info.go b/internal/updates/version_info.go deleted file mode 100644 index 6ab2afc7..00000000 --- a/internal/updates/version_info.go +++ /dev/null @@ -1,49 +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 . - -package updates - -import ( - "runtime" - "strings" -) - -type VersionInfo struct { - Version string - Revision string - ReleaseDate string // Timestamp generated automatically - ReleaseNotes string // List of features, new line separated with leading dot e.g. `• example\n` - ReleaseFixedBugs string // List of fixed bugs, same usage as release notes - FixedBugs []string // Deprecated list of fixed bugs keeping for backward compatibility (mandatory for working versions up to 1.1.5) - URL string // Open browser and download (obsolete replaced by InstallerFile) - - LandingPage string // landing page for manual download - UpdateFile string // automatic update file - InstallerFile string `json:",omitempty"` // manual update file - DebFile string `json:",omitempty"` // debian package file - RpmFile string `json:",omitempty"` // red hat package file - PkgFile string `json:",omitempty"` // arch PKGBUILD file -} - -func (info *VersionInfo) GetDownloadLink() string { - switch runtime.GOOS { - case "linux": - return strings.Join([]string{info.DebFile, info.RpmFile, info.PkgFile}, "\n") - default: - return info.InstallerFile - } -} diff --git a/internal/users/mocks/mocks.go b/internal/users/mocks/mocks.go index dd087706..78fcd248 100644 --- a/internal/users/mocks/mocks.go +++ b/internal/users/mocks/mocks.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ProtonMail/proton-bridge/internal/users (interfaces: Configer,PanicHandler,ClientManager,CredentialsStorer,StoreMaker) +// Source: github.com/ProtonMail/proton-bridge/internal/users (interfaces: Locator,PanicHandler,ClientManager,CredentialsStorer,StoreMaker) // Package mocks is a generated GoMock package. package mocks @@ -13,69 +13,41 @@ import ( gomock "github.com/golang/mock/gomock" ) -// MockConfiger is a mock of Configer interface -type MockConfiger struct { +// MockLocator is a mock of Locator interface +type MockLocator struct { ctrl *gomock.Controller - recorder *MockConfigerMockRecorder + recorder *MockLocatorMockRecorder } -// MockConfigerMockRecorder is the mock recorder for MockConfiger -type MockConfigerMockRecorder struct { - mock *MockConfiger +// MockLocatorMockRecorder is the mock recorder for MockLocator +type MockLocatorMockRecorder struct { + mock *MockLocator } -// NewMockConfiger creates a new mock instance -func NewMockConfiger(ctrl *gomock.Controller) *MockConfiger { - mock := &MockConfiger{ctrl: ctrl} - mock.recorder = &MockConfigerMockRecorder{mock} +// NewMockLocator creates a new mock instance +func NewMockLocator(ctrl *gomock.Controller) *MockLocator { + mock := &MockLocator{ctrl: ctrl} + mock.recorder = &MockLocatorMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use -func (m *MockConfiger) EXPECT() *MockConfigerMockRecorder { +func (m *MockLocator) EXPECT() *MockLocatorMockRecorder { return m.recorder } -// ClearData mocks base method -func (m *MockConfiger) ClearData() error { +// Clear mocks base method +func (m *MockLocator) Clear() error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ClearData") + ret := m.ctrl.Call(m, "Clear") ret0, _ := ret[0].(error) return ret0 } -// ClearData indicates an expected call of ClearData -func (mr *MockConfigerMockRecorder) ClearData() *gomock.Call { +// Clear indicates an expected call of Clear +func (mr *MockLocatorMockRecorder) Clear() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearData", reflect.TypeOf((*MockConfiger)(nil).ClearData)) -} - -// GetAPIConfig mocks base method -func (m *MockConfiger) GetAPIConfig() *pmapi.ClientConfig { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAPIConfig") - ret0, _ := ret[0].(*pmapi.ClientConfig) - return ret0 -} - -// GetAPIConfig indicates an expected call of GetAPIConfig -func (mr *MockConfigerMockRecorder) GetAPIConfig() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIConfig", reflect.TypeOf((*MockConfiger)(nil).GetAPIConfig)) -} - -// GetVersion mocks base method -func (m *MockConfiger) GetVersion() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetVersion") - ret0, _ := ret[0].(string) - return ret0 -} - -// GetVersion indicates an expected call of GetVersion -func (mr *MockConfigerMockRecorder) GetVersion() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVersion", reflect.TypeOf((*MockConfiger)(nil).GetVersion)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clear", reflect.TypeOf((*MockLocator)(nil).Clear)) } // MockPanicHandler is a mock of PanicHandler interface diff --git a/internal/users/types.go b/internal/users/types.go index f108d977..975d9325 100644 --- a/internal/users/types.go +++ b/internal/users/types.go @@ -24,11 +24,14 @@ import ( ) type Configer interface { - ClearData() error - GetVersion() string + GetAppVersion() string GetAPIConfig() *pmapi.ClientConfig } +type Locator interface { + Clear() error +} + type PanicHandler interface { HandlePanic() } diff --git a/internal/users/users.go b/internal/users/users.go index 01159663..295c234c 100644 --- a/internal/users/users.go +++ b/internal/users/users.go @@ -38,7 +38,7 @@ var ( // Users is a struct handling users. type Users struct { - config Configer + locations Locator panicHandler PanicHandler events listener.Listener clientManager ClientManager @@ -64,7 +64,7 @@ type Users struct { } func New( - config Configer, + locations Locator, panicHandler PanicHandler, eventListener listener.Listener, clientManager ClientManager, @@ -75,7 +75,7 @@ func New( log.Trace("Creating new users") u := &Users{ - config: config, + locations: locations, panicHandler: panicHandler, events: eventListener, clientManager: clientManager, @@ -387,7 +387,8 @@ func (u *Users) GetUser(query string) (*User, error) { // ClearData closes all connections (to release db files and so on) and clears all data. func (u *Users) ClearData() error { - var result *multierror.Error + var result error + for _, user := range u.users { if err := user.Logout(); err != nil { result = multierror.Append(result, err) @@ -396,10 +397,12 @@ func (u *Users) ClearData() error { result = multierror.Append(result, err) } } - if err := u.config.ClearData(); err != nil { + + if err := u.locations.Clear(); err != nil { result = multierror.Append(result, err) } - return result.ErrorOrNil() + + return result } // DeleteUser deletes user completely; it logs user out from the API, stops any diff --git a/internal/users/users_test.go b/internal/users/users_test.go index ef087e1e..baa92d05 100644 --- a/internal/users/users_test.go +++ b/internal/users/users_test.go @@ -127,7 +127,7 @@ type mocks struct { t *testing.T ctrl *gomock.Controller - config *usersmocks.MockConfiger + locator *usersmocks.MockLocator PanicHandler *usersmocks.MockPanicHandler clientManager *usersmocks.MockClientManager credentialsStore *usersmocks.MockCredentialsStorer @@ -168,7 +168,7 @@ func initMocks(t *testing.T) mocks { t: t, ctrl: mockCtrl, - config: usersmocks.NewMockConfiger(mockCtrl), + locator: usersmocks.NewMockLocator(mockCtrl), PanicHandler: usersmocks.NewMockPanicHandler(mockCtrl), clientManager: usersmocks.NewMockClientManager(mockCtrl), credentialsStore: usersmocks.NewMockCredentialsStorer(mockCtrl), @@ -234,11 +234,10 @@ func testNewUsersWithUsers(t *testing.T, m mocks) *Users { } func testNewUsers(t *testing.T, m mocks) *Users { //nolint[unparam] - m.config.EXPECT().GetVersion().Return("ver").AnyTimes() m.eventListener.EXPECT().Add(events.UpgradeApplicationEvent, gomock.Any()) m.clientManager.EXPECT().GetAuthUpdateChannel().Return(make(chan pmapi.ClientAuth)) - users := New(m.config, m.PanicHandler, m.eventListener, m.clientManager, m.credentialsStore, m.storeMaker, true) + users := New(m.locator, m.PanicHandler, m.eventListener, m.clientManager, m.credentialsStore, m.storeMaker, true) waitForEvents() @@ -274,7 +273,7 @@ func TestClearData(t *testing.T) { m.credentialsStore.EXPECT().Logout("users").Return(nil) m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil) - m.config.EXPECT().ClearData().Return(nil) + m.locator.EXPECT().Clear() require.NoError(t, users.ClearData()) diff --git a/internal/versioner/install.go b/internal/versioner/install.go new file mode 100644 index 00000000..4e9ed5e4 --- /dev/null +++ b/internal/versioner/install.go @@ -0,0 +1,38 @@ +// 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 . + +package versioner + +import ( + "compress/gzip" + "io" + "path/filepath" + + "github.com/Masterminds/semver/v3" + "github.com/ProtonMail/proton-bridge/pkg/tar" +) + +// InstallNewVersion installs a tgz update package of the given version. +func (v *Versioner) InstallNewVersion(version *semver.Version, r io.Reader) error { + gr, err := gzip.NewReader(r) + if err != nil { + return err + } + defer func() { _ = gr.Close() }() + + return tar.UntarToDir(gr, filepath.Join(v.root, version.Original())) +} diff --git a/internal/frontend/qt-ie/types.go b/internal/versioner/name_default.go similarity index 88% rename from internal/frontend/qt-ie/types.go rename to internal/versioner/name_default.go index f603b51c..bf8276e0 100644 --- a/internal/frontend/qt-ie/types.go +++ b/internal/versioner/name_default.go @@ -15,11 +15,10 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// +build !nogui +// +build !windows -package qtie +package versioner -type panicHandler interface { - HandlePanic() - SendReport(interface{}) +func getExeName(name string) string { + return name } diff --git a/internal/updates/updates_beta.go b/internal/versioner/name_windows.go similarity index 87% rename from internal/updates/updates_beta.go rename to internal/versioner/name_windows.go index 4f8a8741..b173b217 100644 --- a/internal/updates/updates_beta.go +++ b/internal/versioner/name_windows.go @@ -15,11 +15,8 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// +build build_beta +package versioner -package updates - -func init() { - DownloadPath = "download/beta" - BuildType = "beta" +func getExeName(name string) string { + return name + ".exe" } diff --git a/internal/versioner/remove_darwin.go b/internal/versioner/remove_darwin.go new file mode 100644 index 00000000..068bc3ea --- /dev/null +++ b/internal/versioner/remove_darwin.go @@ -0,0 +1,24 @@ +// 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 . + +package versioner + +// RemoveOldVersions removes all but the latest app version. +func (v *Versioner) RemoveOldVersions() error { + // darwin does not use the versioner; removal is a noop. + return nil +} diff --git a/internal/cmd/version_file.go b/internal/versioner/remove_default.go similarity index 59% rename from internal/cmd/version_file.go rename to internal/versioner/remove_default.go index 7f3caf20..a9ad802c 100644 --- a/internal/cmd/version_file.go +++ b/internal/versioner/remove_default.go @@ -15,18 +15,33 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -package cmd +// +build !darwin -import "github.com/ProtonMail/proton-bridge/internal/updates" +package versioner -// GenerateVersionFiles writes a JSON file with details about current build. -// Those files are used for upgrading the app. -func GenerateVersionFiles(updates *updates.Updates, dir string) { - log.Info("Generating version files") - for _, goos := range []string{"windows", "darwin", "linux"} { - log.Debug("Generating JSON for ", goos) - if err := updates.CreateJSONAndSign(dir, goos); err != nil { - log.Error(err) +import ( + "os" + + "github.com/sirupsen/logrus" +) + +// RemoveOldVersions removes all but the latest app version. +func (v *Versioner) RemoveOldVersions() error { + versions, err := v.ListVersions() + if err != nil { + return err + } + + // darwin does not currently use the versioner. + if len(versions) == 0 { + return nil + } + + for _, version := range versions[1:] { + if err := os.RemoveAll(version.path); err != nil { + logrus.WithError(err).Error("Failed to remove old app version") } } + + return nil } diff --git a/internal/versioner/util.go b/internal/versioner/util.go new file mode 100644 index 00000000..44c56452 --- /dev/null +++ b/internal/versioner/util.go @@ -0,0 +1,43 @@ +// 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 . + +package versioner + +import ( + "os" + "runtime" +) + +// fileExists returns whether the given file exists. +func fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +// fileIsExecutable returns the given filepath and true if it exists. +func fileIsExecutable(path string) bool { + if runtime.GOOS == "windows" { + return true + } + + info, err := os.Stat(path) + if err != nil { + return false + } + + return info.Mode()&0111 != 0 +} diff --git a/internal/versioner/version.go b/internal/versioner/version.go new file mode 100644 index 00000000..4e64863e --- /dev/null +++ b/internal/versioner/version.go @@ -0,0 +1,87 @@ +// 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 . + +package versioner + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/Masterminds/semver/v3" + "github.com/ProtonMail/gopenpgp/v2/crypto" +) + +type Version struct { + version *semver.Version + path string +} + +type Versions []*Version + +func (v Versions) Len() int { + return len(v) +} + +func (v Versions) Less(i, j int) bool { + return v[i].version.LessThan(v[j].version) +} + +func (v Versions) Swap(i, j int) { + v[i], v[j] = v[j], v[i] +} + +// VerifyFiles verifies all files in the version directory. +func (v *Version) VerifyFiles(kr *crypto.KeyRing) error { + return filepath.Walk(v.path, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if filepath.Ext(path) == ".sig" || info.IsDir() { + return nil + } + + fileBytes, err := ioutil.ReadFile(path) // nolint[gosec] + if err != nil { + return err + } + + sigBytes, err := ioutil.ReadFile(path + ".sig") // nolint[gosec] + if err != nil { + return err + } + + return kr.VerifyDetached( + crypto.NewPlainMessage(fileBytes), + crypto.NewPGPSignature(sigBytes), + crypto.GetUnixTime(), + ) + }) +} + +// GetExecutable returns the full path to the executable of the given version. +// It returns an error if the executable is missing or does not have executable permissions set. +func (v *Version) GetExecutable(name string) (string, error) { + exe := filepath.Join(v.path, getExeName(name)) + + if !fileExists(exe) || !fileIsExecutable(exe) { + return "", ErrNoExecutable + } + + return exe, nil +} diff --git a/internal/versioner/version_test.go b/internal/versioner/version_test.go new file mode 100644 index 00000000..0fe84b70 --- /dev/null +++ b/internal/versioner/version_test.go @@ -0,0 +1,142 @@ +// 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 . + +package versioner + +import ( + "crypto/rand" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestVerifyFiles(t *testing.T) { + tempDir, err := ioutil.TempDir("", "verify-test") + require.NoError(t, err) + + version := &Version{ + version: semver.MustParse("1.2.3"), + path: tempDir, + } + + kr := createSignedFiles(t, + filepath.Join(tempDir, "f1.txt"), + filepath.Join(tempDir, "f2.png"), + filepath.Join(tempDir, "f3.dat"), + filepath.Join(tempDir, "sub", "f4.tar"), + filepath.Join(tempDir, "sub", "f5.tgz"), + ) + + assert.NoError(t, version.VerifyFiles(kr)) +} + +func TestVerifyWithBadFile(t *testing.T) { + tempDir, err := ioutil.TempDir("", "verify-test") + require.NoError(t, err) + + version := &Version{ + version: semver.MustParse("1.2.3"), + path: tempDir, + } + + kr := createSignedFiles(t, + filepath.Join(tempDir, "f1.txt"), + filepath.Join(tempDir, "f2.png"), + filepath.Join(tempDir, "f3.bad"), + filepath.Join(tempDir, "sub", "f4.tar"), + filepath.Join(tempDir, "sub", "f5.tgz"), + ) + + badKeyRing := makeKeyRing(t) + signFile(t, filepath.Join(tempDir, "f3.bad"), badKeyRing) + + assert.Error(t, version.VerifyFiles(kr)) +} + +func TestVerifyWithBadSubFile(t *testing.T) { + tempDir, err := ioutil.TempDir("", "verify-test") + require.NoError(t, err) + + version := &Version{ + version: semver.MustParse("1.2.3"), + path: tempDir, + } + + kr := createSignedFiles(t, + filepath.Join(tempDir, "f1.txt"), + filepath.Join(tempDir, "f2.png"), + filepath.Join(tempDir, "f3.dat"), + filepath.Join(tempDir, "sub", "f4.tar"), + filepath.Join(tempDir, "sub", "f5.bad"), + ) + + badKeyRing := makeKeyRing(t) + signFile(t, filepath.Join(tempDir, "sub", "f5.bad"), badKeyRing) + + assert.Error(t, version.VerifyFiles(kr)) +} + +func createSignedFiles(t *testing.T, paths ...string) *crypto.KeyRing { + kr := makeKeyRing(t) + + for _, path := range paths { + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0700)) + makeFile(t, path) + signFile(t, path, kr) + } + + return kr +} + +func makeKeyRing(t *testing.T) *crypto.KeyRing { + key, err := crypto.GenerateKey("name", "email", "rsa", 2048) + require.NoError(t, err) + + kr, err := crypto.NewKeyRing(key) + require.NoError(t, err) + + return kr +} + +func makeFile(t *testing.T, path string) { + f, err := os.Create(path) + require.NoError(t, err) + + data := make([]byte, 64) + _, err = rand.Read(data) + require.NoError(t, err) + + _, err = f.Write(data) + require.NoError(t, err) + + require.NoError(t, f.Close()) +} + +func signFile(t *testing.T, path string, kr *crypto.KeyRing) { + file, err := ioutil.ReadFile(path) + require.NoError(t, err) + + sig, err := kr.SignDetached(crypto.NewPlainMessage(file)) + require.NoError(t, err) + require.NoError(t, ioutil.WriteFile(path+".sig", sig.GetBinary(), 0700)) +} diff --git a/internal/versioner/versioner.go b/internal/versioner/versioner.go new file mode 100644 index 00000000..a6208b96 --- /dev/null +++ b/internal/versioner/versioner.go @@ -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 . + +package versioner + +import ( + "errors" + "io/ioutil" + "path/filepath" + "sort" + + "github.com/Masterminds/semver/v3" +) + +var ( + ErrNoVersions = errors.New("no available versions") + ErrNoExecutable = errors.New("no executable found") +) + +// Versioner manages a directory of versioned app directories. +type Versioner struct { + root string +} + +func New(root string) *Versioner { + return &Versioner{root: root} +} + +// ListVersions returns a collection of all available version numbers, sorted from newest to oldest. +func (v *Versioner) ListVersions() (Versions, error) { + dirs, err := ioutil.ReadDir(v.root) + if err != nil { + return nil, err + } + + var versions Versions + + for _, dir := range dirs { + version, err := semver.StrictNewVersion(dir.Name()) + if err != nil { + continue + } + + // NOTE: If it's a bad directory, maybe delete it? + + versions = append(versions, &Version{ + version: version, + path: filepath.Join(v.root, dir.Name()), + }) + } + + sort.Sort(sort.Reverse(versions)) + + return versions, nil +} + +// GetExecutableInDirectory returns the full path to the executable in the given directory, if present. +// It returns an error if the executable is missing or does not have executable permissions set. +func (v *Versioner) GetExecutableInDirectory(name, directory string) (string, error) { + exe := filepath.Join(directory, getExeName(name)) + + if !fileExists(exe) || !fileIsExecutable(exe) { + return "", ErrNoExecutable + } + + return exe, nil +} diff --git a/internal/versioner/versioner_test.go b/internal/versioner/versioner_test.go new file mode 100644 index 00000000..c8d12eb8 --- /dev/null +++ b/internal/versioner/versioner_test.go @@ -0,0 +1,95 @@ +// 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 . + +package versioner + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestListVersions(t *testing.T) { + updates, err := ioutil.TempDir("", "updates") + require.NoError(t, err) + + v := newTestVersioner(t, "myCoolApp", updates, "2.3.4-beta", "2.3.4", "2.3.5", "2.4.0") + + versions, err := v.ListVersions() + require.NoError(t, err) + + assert.Equal(t, semver.MustParse("2.4.0"), versions[0].version) + assert.Equal(t, filepath.Join(updates, "2.4.0"), versions[0].path) + + assert.Equal(t, semver.MustParse("2.3.5"), versions[1].version) + assert.Equal(t, filepath.Join(updates, "2.3.5"), versions[1].path) + + assert.Equal(t, semver.MustParse("2.3.4"), versions[2].version) + assert.Equal(t, filepath.Join(updates, "2.3.4"), versions[2].path) + + assert.Equal(t, semver.MustParse("2.3.4-beta"), versions[3].version) + assert.Equal(t, filepath.Join(updates, "2.3.4-beta"), versions[3].path) +} + +func TestRemoveOldVersions(t *testing.T) { + updates, err := ioutil.TempDir("", "updates") + require.NoError(t, err) + + v := newTestVersioner(t, "myCoolApp", updates, "2.3.4-beta", "2.3.4", "2.3.5", "2.4.0") + + allVersions, err := v.ListVersions() + require.NoError(t, err) + require.Len(t, allVersions, 4) + + assert.NoError(t, v.RemoveOldVersions()) + + cleanedVersions, err := v.ListVersions() + assert.NoError(t, err) + assert.Len(t, cleanedVersions, 1) + + assert.Equal(t, semver.MustParse("2.4.0"), cleanedVersions[0].version) + assert.Equal(t, filepath.Join(updates, "2.4.0"), cleanedVersions[0].path) +} + +func newTestVersioner(t *testing.T, exeName, updates string, versions ...string) *Versioner { + for _, version := range versions { + makeDummyVersionDirectory(t, exeName, updates, version) + } + + return New(updates) +} + +func makeDummyVersionDirectory(t *testing.T, exeName, updates, version string) string { + target := filepath.Join(updates, version) + require.NoError(t, os.Mkdir(target, 0700)) + + exe, err := os.Create(filepath.Join(target, getExeName(exeName))) + require.NoError(t, err) + require.NotNil(t, exe) + require.NoError(t, os.Chmod(exe.Name(), 0700)) + + sig, err := os.Create(filepath.Join(target, getExeName(exeName)+".sig")) + require.NoError(t, err) + require.NotNil(t, sig) + + return target +} diff --git a/pkg/config/config.go b/pkg/config/config.go deleted file mode 100644 index 7208a743..00000000 --- a/pkg/config/config.go +++ /dev/null @@ -1,319 +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 . - -package config - -import ( - "io/ioutil" - "os" - "path/filepath" - "runtime" - - "github.com/ProtonMail/go-appdir" - "github.com/hashicorp/go-multierror" - "github.com/sirupsen/logrus" -) - -var ( - log = logrus.WithField("pkg", "config") //nolint[gochecknoglobals] -) - -type appDirProvider interface { - UserConfig() string - UserCache() string - UserLogs() string -} - -type Config struct { - appName string - version string - revision string - cacheVersion string - appDirs appDirProvider - appDirsVersion appDirProvider -} - -// New returns fully initialized config struct. -// `appName` should be in camelCase format for folder or file names. It's also used in API -// as `AppVersion` which is converted to CamelCase. -// `version` is the version of the app (e.g. v1.2.3). -// `cacheVersion` is the version of the cache files (setting a different number will remove the old ones). -func New(appName, version, revision, cacheVersion string) *Config { - appDirs := appdir.New(filepath.Join("protonmail", appName)) - appDirsVersion := appdir.New(filepath.Join("protonmail", appName, cacheVersion)) - return newConfig(appName, version, revision, cacheVersion, appDirs, appDirsVersion) -} - -func newConfig(appName, version, revision, cacheVersion string, appDirs, appDirsVersion appDirProvider) *Config { - return &Config{ - appName: appName, - version: version, - revision: revision, - cacheVersion: cacheVersion, - appDirs: appDirs, - appDirsVersion: appDirsVersion, - } -} - -// CreateDirs creates all folders that are necessary for bridge to properly function. -func (c *Config) CreateDirs() error { - // Log files. - if err := os.MkdirAll(c.appDirs.UserLogs(), 0700); err != nil { - return err - } - // TLS files. - if err := os.MkdirAll(c.appDirs.UserConfig(), 0750); err != nil { - return err - } - // Lock, events, preferences, user_info, db files. - if err := os.MkdirAll(c.appDirsVersion.UserCache(), 0750); err != nil { - return err - } - return nil -} - -// ClearData removes all files except the lock file. -// The lock file will be removed when the Bridge stops. -func (c *Config) ClearData() error { - dirs := []string{ - c.appDirs.UserLogs(), - c.appDirs.UserConfig(), - c.appDirs.UserCache(), - } - shouldRemove := func(filePath string) bool { - return filePath != c.GetLockPath() - } - return c.removeAllExcept(dirs, shouldRemove) -} - -// ClearOldData removes all old files, such as old log files or old versions of cache and so on. -func (c *Config) ClearOldData() error { - // `appDirs` is parent for `appDirsVersion`. - // `dir` then contains all subfolders and only `cacheVersion` should stay. - // But on Windows all files (dirs) are in the same one - we cannot remove log, lock or tls files. - dir := c.appDirs.UserCache() - - return c.removeExcept(dir, func(filePath string) bool { - fileName := filepath.Base(filePath) - return (fileName != c.cacheVersion && - !logFileRgx.MatchString(fileName) && - filePath != c.GetLogDir() && - filePath != c.GetTLSCertPath() && - filePath != c.GetTLSKeyPath() && - filePath != c.GetEventsPath() && - filePath != c.GetIMAPCachePath() && - filePath != c.GetLockPath() && - filePath != c.GetPreferencesPath()) - }) -} - -func (c *Config) removeAllExcept(dirs []string, shouldRemove func(string) bool) error { - var result *multierror.Error - for _, dir := range dirs { - if err := c.removeExcept(dir, shouldRemove); err != nil { - result = multierror.Append(result, err) - } - } - return result.ErrorOrNil() -} - -func (c *Config) removeExcept(dir string, shouldRemove func(string) bool) error { - files, err := ioutil.ReadDir(dir) - if err != nil { - return err - } - - var result *multierror.Error - for _, file := range files { - filePath := filepath.Join(dir, file.Name()) - if !shouldRemove(filePath) { - continue - } - - if !file.IsDir() { - if err := os.RemoveAll(filePath); err != nil { - result = multierror.Append(result, err) - } - continue - } - - subDir := filepath.Join(dir, file.Name()) - if err := c.removeExcept(subDir, shouldRemove); err != nil { - result = multierror.Append(result, err) - } else { - // Remove dir itself only if it's empty. - subFiles, err := ioutil.ReadDir(subDir) - if err != nil { - result = multierror.Append(result, err) - } else if len(subFiles) == 0 { - if err := os.RemoveAll(subDir); err != nil { - result = multierror.Append(result, err) - } - } - } - } - return result.ErrorOrNil() -} - -// IsDevMode should be used for development conditions such us whether to send sentry reports. -func (c *Config) IsDevMode() bool { - return os.Getenv("PROTONMAIL_ENV") == "dev" -} - -// GetVersion returns the version. -func (c *Config) GetVersion() string { - return c.version -} - -// GetLogDir returns folder for log files. -func (c *Config) GetLogDir() string { - return c.appDirs.UserLogs() -} - -// GetLogPrefix returns prefix for log files. Bridge uses format vVERSION. -func (c *Config) GetLogPrefix() string { - return "v" + c.version + "_" + c.revision -} - -// GetLicenseFilePath returns path to liense file. -func (c *Config) GetLicenseFilePath() string { - path := c.getLicenseFilePath() - log.WithField("path", path).Info("License file path") - return path -} - -func (c *Config) 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 := c.appName - if c.appName == "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 c.appName == "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 "" -} - -// GetTLSCertPath returns path to certificate; used for TLS servers (IMAP, SMTP and API). -func (c *Config) GetTLSCertPath() string { - return filepath.Join(c.appDirs.UserConfig(), "cert.pem") -} - -// GetTLSKeyPath returns path to private key; used for TLS servers (IMAP, SMTP and API). -func (c *Config) GetTLSKeyPath() string { - return filepath.Join(c.appDirs.UserConfig(), "key.pem") -} - -// GetDBDir returns folder for db files. -func (c *Config) GetDBDir() string { - return c.appDirsVersion.UserCache() -} - -// GetEventsPath returns path to events file containing the last processed event IDs. -func (c *Config) GetEventsPath() string { - return filepath.Join(c.appDirsVersion.UserCache(), "events.json") -} - -// GetIMAPCachePath returns path to file with IMAP status. -func (c *Config) GetIMAPCachePath() string { - return filepath.Join(c.appDirsVersion.UserCache(), "user_info.json") -} - -// GetLockPath returns path to lock file to check if bridge is already running. -func (c *Config) GetLockPath() string { - return filepath.Join(c.appDirsVersion.UserCache(), c.appName+".lock") -} - -// GetUpdateDir returns folder for update files; such as new binary. -func (c *Config) GetUpdateDir() string { - return filepath.Join(c.appDirsVersion.UserCache(), "updates") -} - -// GetPreferencesPath returns path to preference file. -func (c *Config) GetPreferencesPath() string { - return filepath.Join(c.appDirsVersion.UserCache(), "prefs.json") -} - -// GetTransferDir returns folder for import-export rules files. -func (c *Config) GetTransferDir() string { - return c.appDirsVersion.UserCache() -} - -// GetDefaultAPIPort returns default Bridge local API port. -func (c *Config) GetDefaultAPIPort() int { - return 1042 -} - -// GetDefaultIMAPPort returns default Bridge IMAP port. -func (c *Config) GetDefaultIMAPPort() int { - return 1143 -} - -// GetDefaultSMTPPort returns default Bridge SMTP port. -func (c *Config) GetDefaultSMTPPort() int { - return 1025 -} - -// getAPIOS returns actual operating system. -func (c *Config) getAPIOS() string { - switch os := runtime.GOOS; os { - case "darwin": // nolint: goconst - return "macOS" - case "linux": - return "Linux" - case "windows": - return "Windows" - } - - return "Linux" -} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go deleted file mode 100644 index 966a4f9c..00000000 --- a/pkg/config/config_test.go +++ /dev/null @@ -1,238 +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 . - -package config - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - gomock "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" -) - -const testAppName = "bridge-test" - -var testConfigDir string //nolint[gochecknoglobals] - -func TestMain(m *testing.M) { - setupTestConfig() - setupTestLogs() - code := m.Run() - shutdownTestConfig() - shutdownTestLogs() - shutdownTestPreferences() - os.Exit(code) -} - -func setupTestConfig() { - var err error - testConfigDir, err = ioutil.TempDir("", "config") - if err != nil { - panic(err) - } -} - -func shutdownTestConfig() { - _ = os.RemoveAll(testConfigDir) -} - -type mocks struct { - t *testing.T - - ctrl *gomock.Controller - appDir *MockappDirer - appDirVersion *MockappDirer -} - -func initMocks(t *testing.T) mocks { - mockCtrl := gomock.NewController(t) - return mocks{ - t: t, - - ctrl: mockCtrl, - appDir: NewMockappDirer(mockCtrl), - appDirVersion: NewMockappDirer(mockCtrl), - } -} - -func TestClearDataLinux(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - createTestStructureLinux(m, testConfigDir) - cfg := newConfig(testAppName, "v1", "rev123", "c2", m.appDir, m.appDirVersion) - require.NoError(t, cfg.ClearData()) - checkFileNames(t, testConfigDir, []string{ - "cache", - "cache/c2", - "cache/c2/bridge-test.lock", - "config", - "logs", - }) -} - -func TestClearDataWindows(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - createTestStructureWindows(m, testConfigDir) - cfg := newConfig(testAppName, "v1", "rev123", "c2", m.appDir, m.appDirVersion) - require.NoError(t, cfg.ClearData()) - checkFileNames(t, testConfigDir, []string{ - "cache", - "cache/c2", - "cache/c2/bridge-test.lock", - "config", - }) -} - -// OldData touches only cache folder. -// Removes only c1 folder as nothing else is part of cache folder on Linux/Mac. -func TestClearOldDataLinux(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - createTestStructureLinux(m, testConfigDir) - cfg := newConfig(testAppName, "v1", "rev123", "c2", m.appDir, m.appDirVersion) - require.NoError(t, cfg.ClearOldData()) - checkFileNames(t, testConfigDir, []string{ - "cache", - "cache/c2", - "cache/c2/bridge-test.lock", - "cache/c2/events.json", - "cache/c2/mailbox-user@pm.me.db", - "cache/c2/prefs.json", - "cache/c2/updates", - "cache/c2/user_info.json", - "config", - "config/cert.pem", - "config/key.pem", - "logs", - "logs/other.log", - "logs/v1_10.log", - "logs/v1_11.log", - "logs/v2_12.log", - "logs/v2_13.log", - }) -} - -// OldData touches only cache folder. Removes everything except c2 folder -// and bridge log files which are part of cache folder on Windows. -func TestClearOldDataWindows(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - createTestStructureWindows(m, testConfigDir) - cfg := newConfig(testAppName, "v1", "rev123", "c2", m.appDir, m.appDirVersion) - require.NoError(t, cfg.ClearOldData()) - checkFileNames(t, testConfigDir, []string{ - "cache", - "cache/c2", - "cache/c2/bridge-test.lock", - "cache/c2/events.json", - "cache/c2/mailbox-user@pm.me.db", - "cache/c2/prefs.json", - "cache/c2/updates", - "cache/c2/user_info.json", - "cache/v1_10.log", - "cache/v1_11.log", - "cache/v2_12.log", - "cache/v2_13.log", - "config", - "config/cert.pem", - "config/key.pem", - }) -} - -func createTestStructureLinux(m mocks, baseDir string) { - logsDir := filepath.Join(baseDir, "logs") - configDir := filepath.Join(baseDir, "config") - cacheDir := filepath.Join(baseDir, "cache") - versionedOldCacheDir := filepath.Join(baseDir, "cache", "c1") - versionedCacheDir := filepath.Join(baseDir, "cache", "c2") - createTestStructure(m, baseDir, logsDir, configDir, cacheDir, versionedOldCacheDir, versionedCacheDir) -} - -func createTestStructureWindows(m mocks, baseDir string) { - logsDir := filepath.Join(baseDir, "cache") - configDir := filepath.Join(baseDir, "config") - cacheDir := filepath.Join(baseDir, "cache") - versionedOldCacheDir := filepath.Join(baseDir, "cache", "c1") - versionedCacheDir := filepath.Join(baseDir, "cache", "c2") - createTestStructure(m, baseDir, logsDir, configDir, cacheDir, versionedOldCacheDir, versionedCacheDir) -} - -func createTestStructure(m mocks, baseDir, logsDir, configDir, cacheDir, versionedOldCacheDir, versionedCacheDir string) { - m.appDir.EXPECT().UserLogs().Return(logsDir).AnyTimes() - m.appDir.EXPECT().UserConfig().Return(configDir).AnyTimes() - m.appDir.EXPECT().UserCache().Return(cacheDir).AnyTimes() - m.appDirVersion.EXPECT().UserCache().Return(versionedCacheDir).AnyTimes() - - require.NoError(m.t, os.RemoveAll(baseDir)) - require.NoError(m.t, os.MkdirAll(baseDir, 0700)) - require.NoError(m.t, os.MkdirAll(logsDir, 0700)) - require.NoError(m.t, os.MkdirAll(configDir, 0700)) - require.NoError(m.t, os.MkdirAll(cacheDir, 0700)) - require.NoError(m.t, os.MkdirAll(versionedOldCacheDir, 0700)) - require.NoError(m.t, os.MkdirAll(versionedCacheDir, 0700)) - require.NoError(m.t, os.MkdirAll(filepath.Join(versionedCacheDir, "updates"), 0700)) - - require.NoError(m.t, ioutil.WriteFile(filepath.Join(logsDir, "other.log"), []byte("Hello"), 0755)) - require.NoError(m.t, ioutil.WriteFile(filepath.Join(logsDir, "v1_10.log"), []byte("Hello"), 0755)) - require.NoError(m.t, ioutil.WriteFile(filepath.Join(logsDir, "v1_11.log"), []byte("Hello"), 0755)) - require.NoError(m.t, ioutil.WriteFile(filepath.Join(logsDir, "v2_12.log"), []byte("Hello"), 0755)) - require.NoError(m.t, ioutil.WriteFile(filepath.Join(logsDir, "v2_13.log"), []byte("Hello"), 0755)) - - require.NoError(m.t, ioutil.WriteFile(filepath.Join(configDir, "cert.pem"), []byte("Hello"), 0755)) - require.NoError(m.t, ioutil.WriteFile(filepath.Join(configDir, "key.pem"), []byte("Hello"), 0755)) - - require.NoError(m.t, ioutil.WriteFile(filepath.Join(versionedOldCacheDir, "prefs.json"), []byte("Hello"), 0755)) - require.NoError(m.t, ioutil.WriteFile(filepath.Join(versionedOldCacheDir, "events.json"), []byte("Hello"), 0755)) - require.NoError(m.t, ioutil.WriteFile(filepath.Join(versionedOldCacheDir, "user_info.json"), []byte("Hello"), 0755)) - require.NoError(m.t, ioutil.WriteFile(filepath.Join(versionedOldCacheDir, "mailbox-user@pm.me.db"), []byte("Hello"), 0755)) - require.NoError(m.t, ioutil.WriteFile(filepath.Join(versionedCacheDir, "prefs.json"), []byte("Hello"), 0755)) - require.NoError(m.t, ioutil.WriteFile(filepath.Join(versionedCacheDir, "events.json"), []byte("Hello"), 0755)) - require.NoError(m.t, ioutil.WriteFile(filepath.Join(versionedCacheDir, "user_info.json"), []byte("Hello"), 0755)) - require.NoError(m.t, ioutil.WriteFile(filepath.Join(versionedCacheDir, testAppName+".lock"), []byte("Hello"), 0755)) - require.NoError(m.t, ioutil.WriteFile(filepath.Join(versionedCacheDir, "mailbox-user@pm.me.db"), []byte("Hello"), 0755)) -} - -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 -} diff --git a/pkg/config/logs.go b/pkg/config/logs.go deleted file mode 100644 index c3930d0e..00000000 --- a/pkg/config/logs.go +++ /dev/null @@ -1,251 +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 . - -package config - -import ( - "bytes" - "errors" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "regexp" - "runtime" - "runtime/pprof" - "sort" - "strconv" - "time" - - "github.com/ProtonMail/proton-bridge/pkg/sentry" - "github.com/sirupsen/logrus" -) - -type logConfiger interface { - GetLogDir() string - GetLogPrefix() string -} - -const ( - // Zendesk now has a file size limit of 20MB. When the last N log files - // are zipped, it should fit under 20MB. Value in MB (average file has - // few hundreds kB). - maxLogFileSize = 10 * 1024 * 1024 //nolint[gochecknoglobals] - // Including the current logfile. - maxNumberLogFiles = 3 //nolint[gochecknoglobals] -) - -// logFile is pointer to currently open file used by logrus. -var logFile *os.File //nolint[gochecknoglobals] - -var logFileRgx = regexp.MustCompile("^v.*\\.log$") //nolint[gochecknoglobals] -var logCrashRgx = regexp.MustCompile("^v.*_crash_.*\\.log$") //nolint[gochecknoglobals] - -// HandlePanic reports the crash to sentry or local file when sentry fails. -func HandlePanic(cfg *Config, output string) { - sentry.SkipDuringUnwind() - - if !cfg.IsDevMode() { - apiCfg := cfg.GetAPIConfig() - if err := sentry.ReportSentryCrash(apiCfg.ClientID, apiCfg.AppVersion, apiCfg.UserAgent, errors.New(output)); err != nil { - log.Error("Sentry crash report failed: ", err) - } - } - - filename := getLogFilename(cfg.GetLogPrefix() + "_crash_") - filepath := filepath.Join(cfg.GetLogDir(), filename) - f, err := os.OpenFile(filepath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) - if err != nil { - log.Error("Cannot open file to write crash report: ", err) - return - } - - _, _ = f.WriteString(output) - _ = pprof.Lookup("goroutine").WriteTo(f, 2) - - log.Warn("Crash report saved to ", filepath) -} - -// GetGID returns goroutine number which can be used to distiguish logs from -// the concurent processes. Keep in mind that it returns the number of routine -// which executes the function. -func GetGID() uint64 { - b := make([]byte, 64) - b = b[:runtime.Stack(b, false)] - b = bytes.TrimPrefix(b, []byte("goroutine ")) - b = b[:bytes.IndexByte(b, ' ')] - n, _ := strconv.ParseUint(string(b), 10, 64) - return n -} - -// SetupLog set up log level, formatter and output (file or stdout). -// Returns whether should be used debug for IMAP and SMTP servers. -func SetupLog(cfg logConfiger, levelFlag string) (debugClient, debugServer bool) { - level, useFile := getLogLevelAndFile(levelFlag) - - logrus.SetLevel(level) - - if useFile { - logrus.SetFormatter(&logrus.JSONFormatter{}) - setLogFile(cfg.GetLogDir(), cfg.GetLogPrefix()) - watchLogFileSize(cfg.GetLogDir(), cfg.GetLogPrefix()) - } else { - logrus.SetFormatter(&logrus.TextFormatter{ - ForceColors: true, - FullTimestamp: true, - TimestampFormat: time.StampMilli, - }) - logrus.SetOutput(os.Stdout) - } - - switch levelFlag { - case "debug-client", "debug-client-json": - debugClient = true - case "debug-server", "debug-server-json", "trace": - fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") - log.Warning("================================================") - log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") - log.Warning("================================================") - debugClient = true - debugServer = true - } - - return debugClient, debugServer -} - -func setLogFile(logDir, logPrefix string) { - if logFile != nil { - return - } - - filename := getLogFilename(logPrefix) - var err error - logFile, err = os.OpenFile(filepath.Join(logDir, filename), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) - if err != nil { - panic(err) - } - - logrus.SetOutput(logFile) - - // Users sometimes change the name of the log file. We want to always log - // information about bridge version (included in log prefix) and OS. - log.Warn("Bridge version: ", logPrefix, " ", runtime.GOOS) -} - -func getLogFilename(logPrefix string) string { - currentTime := strconv.Itoa(int(time.Now().Unix())) - return logPrefix + "_" + currentTime + ".log" -} - -func watchLogFileSize(logDir, logPrefix string) { - go func() { - for { - // Some rare bug can cause log file spamming a lot. Checking file - // size too often is not good, and at the same time postpone next - // check for too long is the same thing. 30 seconds seems as good - // compromise; average computer can generates ~500MB in 30 seconds. - time.Sleep(30 * time.Second) - checkLogFileSize(logDir, logPrefix) - } - }() -} - -func checkLogFileSize(logDir, logPrefix string) { - if logFile == nil { - return - } - - stat, err := logFile.Stat() - if err != nil { - log.Error("Log file size check failed: ", err) - return - } - - if stat.Size() >= maxLogFileSize { - log.Warn("Current log file ", logFile.Name(), " is too big, opening new file") - closeLogFile() - setLogFile(logDir, logPrefix) - } - - if err := clearLogs(logDir); err != nil { - log.Error("Cannot clear logs ", err) - } -} - -func closeLogFile() { - if logFile != nil { - _ = logFile.Close() - logFile = nil - } -} - -func clearLogs(logDir string) error { - files, err := ioutil.ReadDir(logDir) - if err != nil { - return err - } - - var logsWithPrefix []string - var crashesWithPrefix []string - - for _, file := range files { - if logFileRgx.MatchString(file.Name()) { - if logCrashRgx.MatchString(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())); err != nil { - return err - } - } else { - removeLog(logDir, file.Name()) - } - } - } - - removeOldLogs(logDir, logsWithPrefix) - removeOldLogs(logDir, crashesWithPrefix) - return nil -} - -func removeOldLogs(logDir string, filenames []string) { - count := len(filenames) - if count <= maxNumberLogFiles { - return - } - - sort.Strings(filenames) // Sorted by timestamp: oldest first. - for _, filename := range filenames[:count-maxNumberLogFiles] { - 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 !logFileRgx.MatchString(filename) { - return - } - if err := os.RemoveAll(filepath.Join(logDir, filename)); err != nil { - log.Error("Cannot remove old logs ", err) - } -} diff --git a/pkg/config/logs_test.go b/pkg/config/logs_test.go deleted file mode 100644 index cc5dd706..00000000 --- a/pkg/config/logs_test.go +++ /dev/null @@ -1,225 +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 . - -package config - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/require" -) - -type testLogConfig struct{ logDir, logPrefix string } - -func (c *testLogConfig) GetLogDir() string { return c.logDir } -func (c *testLogConfig) GetLogPrefix() string { return c.logPrefix } - -var testLogDir string //nolint[gochecknoglobals] - -func setupTestLogs() { - var err error - testLogDir, err = ioutil.TempDir("", "log") - if err != nil { - panic(err) - } -} - -func shutdownTestLogs() { - _ = os.RemoveAll(testLogDir) -} - -func TestLogNameLength(t *testing.T) { - cfg := New("bridge-test", "longVersion123", "longRevision1234567890", "c2") - name := getLogFilename(cfg.GetLogPrefix()) - if len(name) > 128 { - t.Fatal("Name of the log is too long - limit for encrypted linux is 128 characters") - } -} - -// Info and higher levels writes to the file. -func TestSetupLogInfo(t *testing.T) { - dir := beforeEachCreateTestDir(t, "setupInfo") - - SetupLog(&testLogConfig{dir, "v"}, "info") - require.Equal(t, "info", logrus.GetLevel().String()) - - logrus.Info("test message") - files := checkLogFiles(t, dir, 1) - checkLogContains(t, dir, files[0].Name(), "test message") -} - -// Debug levels writes to stdout. -func TestSetupLogDebug(t *testing.T) { - dir := beforeEachCreateTestDir(t, "setupDebug") - - SetupLog(&testLogConfig{dir, "v"}, "debug") - require.Equal(t, "debug", logrus.GetLevel().String()) - - logrus.Info("test message") - checkLogFiles(t, dir, 0) -} - -func TestReopenLogFile(t *testing.T) { - dir := beforeEachCreateTestDir(t, "reopenLogFile") - - setLogFile(dir, "v1") - - done := make(chan interface{}) - - log.Info("first message") - - go func() { - <-done // Wait for closing file and opening new one. - log.Info("second message") - done <- nil - }() - - closeLogFile() - setLogFile(dir, "v2") - - done <- nil - <-done // Wait for second log message. - - files := checkLogFiles(t, dir, 2) - checkLogContains(t, dir, files[0].Name(), "first message") - checkLogContains(t, dir, files[1].Name(), "second message") -} - -func TestCheckLogFileSizeSmall(t *testing.T) { - dir := beforeEachCreateTestDir(t, "logFileSizeSmall") - - setLogFile(dir, "v1") - originalFileName := logFile.Name() - - _, _ = logFile.WriteString("small file") - checkLogFileSize(dir, "v2") - - require.Equal(t, originalFileName, logFile.Name()) -} - -func TestCheckLogFileSizeBig(t *testing.T) { - if testing.Short() { - t.Skip("skipping test in short mode.") - } - - dir := beforeEachCreateTestDir(t, "logFileSizeBig") - - setLogFile(dir, "v1") - originalFileName := logFile.Name() - - // The limit for big file is 10*1024*1024 - keep the string 10 letters long. - for i := 0; i < 1024*1024; i++ { - _, _ = logFile.WriteString("big file!\n") - } - checkLogFileSize(dir, "v2") - - require.NotEqual(t, originalFileName, logFile.Name()) -} - -// ClearLogs removes only bridge old log files keeping last three of them. -func TestClearLogsLinux(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - dir := beforeEachCreateTestDir(t, "clearLogs") - - createTestStructureLinux(m, dir) - require.NoError(t, clearLogs(dir)) - checkFileNames(t, dir, []string{ - "cache", - "cache/c1", - "cache/c1/events.json", - "cache/c1/mailbox-user@pm.me.db", - "cache/c1/prefs.json", - "cache/c1/user_info.json", - "cache/c2", - "cache/c2/bridge-test.lock", - "cache/c2/events.json", - "cache/c2/mailbox-user@pm.me.db", - "cache/c2/prefs.json", - "cache/c2/updates", - "cache/c2/user_info.json", - "config", - "config/cert.pem", - "config/key.pem", - "logs", - "logs/other.log", - "logs/v1_11.log", - "logs/v2_12.log", - "logs/v2_13.log", - }) -} - -// ClearLogs removes only bridge old log files even when log folder -// is shared with other files on Windows. -func TestClearLogsWindows(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - dir := beforeEachCreateTestDir(t, "clearLogs") - - createTestStructureWindows(m, dir) - require.NoError(t, clearLogs(dir)) - checkFileNames(t, dir, []string{ - "cache", - "cache/c1", - "cache/c1/events.json", - "cache/c1/mailbox-user@pm.me.db", - "cache/c1/prefs.json", - "cache/c1/user_info.json", - "cache/c2", - "cache/c2/bridge-test.lock", - "cache/c2/events.json", - "cache/c2/mailbox-user@pm.me.db", - "cache/c2/prefs.json", - "cache/c2/updates", - "cache/c2/user_info.json", - "cache/other.log", - "cache/v1_11.log", - "cache/v2_12.log", - "cache/v2_13.log", - "config", - "config/cert.pem", - "config/key.pem", - }) -} - -func beforeEachCreateTestDir(t *testing.T, dir string) string { - // Make sure opened file (from the previous test) is cleared. - closeLogFile() - - dir = filepath.Join(testLogDir, dir) - require.NoError(t, os.MkdirAll(dir, 0700)) - return dir -} - -func checkLogFiles(t *testing.T, dir string, expectedCount int) []os.FileInfo { - files, err := ioutil.ReadDir(dir) - require.NoError(t, err) - require.Equal(t, expectedCount, len(files)) - return files -} - -func checkLogContains(t *testing.T, dir, fileName, expectedSubstr string) { - data, err := ioutil.ReadFile(filepath.Join(dir, fileName)) //nolint[gosec] - require.NoError(t, err) - require.Contains(t, string(data), expectedSubstr) -} diff --git a/pkg/config/mock_config.go b/pkg/config/mock_config.go deleted file mode 100644 index a2066ecc..00000000 --- a/pkg/config/mock_config.go +++ /dev/null @@ -1,76 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: config/config.go - -// Package config is a generated GoMock package. -package config - -import ( - reflect "reflect" - - gomock "github.com/golang/mock/gomock" -) - -// MockappDirer is a mock of appDirer interface -type MockappDirer struct { - ctrl *gomock.Controller - recorder *MockappDirerMockRecorder -} - -// MockappDirerMockRecorder is the mock recorder for MockappDirer -type MockappDirerMockRecorder struct { - mock *MockappDirer -} - -// NewMockappDirer creates a new mock instance -func NewMockappDirer(ctrl *gomock.Controller) *MockappDirer { - mock := &MockappDirer{ctrl: ctrl} - mock.recorder = &MockappDirerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockappDirer) EXPECT() *MockappDirerMockRecorder { - return m.recorder -} - -// UserConfig mocks base method -func (m *MockappDirer) UserConfig() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UserConfig") - ret0, _ := ret[0].(string) - return ret0 -} - -// UserConfig indicates an expected call of UserConfig -func (mr *MockappDirerMockRecorder) UserConfig() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserConfig", reflect.TypeOf((*MockappDirer)(nil).UserConfig)) -} - -// UserCache mocks base method -func (m *MockappDirer) UserCache() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UserCache") - ret0, _ := ret[0].(string) - return ret0 -} - -// UserCache indicates an expected call of UserCache -func (mr *MockappDirerMockRecorder) UserCache() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserCache", reflect.TypeOf((*MockappDirer)(nil).UserCache)) -} - -// UserLogs mocks base method -func (m *MockappDirer) UserLogs() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UserLogs") - ret0, _ := ret[0].(string) - return ret0 -} - -// UserLogs indicates an expected call of UserLogs -func (mr *MockappDirerMockRecorder) UserLogs() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserLogs", reflect.TypeOf((*MockappDirer)(nil).UserLogs)) -} diff --git a/pkg/files/removal.go b/pkg/files/removal.go new file mode 100644 index 00000000..dcb7e3bb --- /dev/null +++ b/pkg/files/removal.go @@ -0,0 +1,82 @@ +// 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 . + +// Package files provides standard filesystem operations. +package files + +import ( + "os" + "path/filepath" + "sort" + "strings" + + "github.com/hashicorp/go-multierror" +) + +type OpRemove struct { + targets []string + exceptions []string +} + +func Remove(targets ...string) *OpRemove { + return &OpRemove{targets: targets} +} + +func (op *OpRemove) Except(exceptions ...string) *OpRemove { + op.exceptions = exceptions + return op +} + +func (op *OpRemove) Do() error { + var multiErr error + + for _, target := range op.targets { + if err := remove(target, op.exceptions...); err != nil { + multiErr = multierror.Append(multiErr, err) + } + } + + return multiErr +} + +func remove(dir string, except ...string) error { + var toRemove []string + + if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + for _, exception := range except { + if path == exception || strings.HasPrefix(exception, path) || strings.HasPrefix(path, exception) { + return nil + } + } + + toRemove = append(toRemove, path) + + return nil + }); err != nil { + return err + } + + sort.Sort(sort.Reverse(sort.StringSlice(toRemove))) + + for _, target := range toRemove { + if err := os.RemoveAll(target); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/files/removal_test.go b/pkg/files/removal_test.go new file mode 100644 index 00000000..0475cdd0 --- /dev/null +++ b/pkg/files/removal_test.go @@ -0,0 +1,116 @@ +// 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 . + +package files + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRemove(t *testing.T) { + dir := newTestDir(t, + "subdir1", + "subdir2/subdir3", + ) + defer delTestDir(t, dir) + + createTestFiles(t, dir, + "subdir1/file1", + "subdir1/file2", + "subdir2/file3", + "subdir2/file4", + "subdir2/subdir3/file5", + "subdir2/subdir3/file6", + ) + + require.NoError(t, Remove( + filepath.Join(dir, "subdir1"), + filepath.Join(dir, "subdir2", "file3"), + filepath.Join(dir, "subdir2", "subdir3", "file5"), + ).Do()) + + assert.NoFileExists(t, filepath.Join(dir, "subdir1", "file1")) + assert.NoFileExists(t, filepath.Join(dir, "subdir1", "file2")) + assert.NoFileExists(t, filepath.Join(dir, "subdir2", "file3")) + assert.FileExists(t, filepath.Join(dir, "subdir2", "file4")) + assert.NoFileExists(t, filepath.Join(dir, "subdir2", "subdir3", "file5")) + assert.FileExists(t, filepath.Join(dir, "subdir2", "subdir3", "file6")) +} + +func TestRemoveWithExceptions(t *testing.T) { + dir := newTestDir(t, + "subdir1", + "subdir2/subdir3", + "subdir4", + ) + defer delTestDir(t, dir) + + createTestFiles(t, dir, + "subdir1/file1", + "subdir1/file2", + "subdir2/file3", + "subdir2/file4", + "subdir2/subdir3/file5", + "subdir2/subdir3/file6", + "subdir4/file7", + "subdir4/file8", + ) + + require.NoError(t, Remove(dir).Except( + filepath.Join(dir, "subdir2", "file4"), + filepath.Join(dir, "subdir2", "subdir3", "file6"), + filepath.Join(dir, "subdir4"), + ).Do()) + + assert.NoFileExists(t, filepath.Join(dir, "subdir1", "file1")) + assert.NoFileExists(t, filepath.Join(dir, "subdir1", "file2")) + assert.NoFileExists(t, filepath.Join(dir, "subdir2", "file3")) + assert.FileExists(t, filepath.Join(dir, "subdir2", "file4")) + assert.NoFileExists(t, filepath.Join(dir, "subdir2", "subdir3", "file5")) + assert.FileExists(t, filepath.Join(dir, "subdir2", "subdir3", "file6")) + assert.FileExists(t, filepath.Join(dir, "subdir4", "file7")) + assert.FileExists(t, filepath.Join(dir, "subdir4", "file8")) +} + +func newTestDir(t *testing.T, subdirs ...string) string { + dir, err := ioutil.TempDir("", "test-files-dir") + require.NoError(t, err) + + for _, target := range subdirs { + require.NoError(t, os.MkdirAll(filepath.Join(dir, target), 0700)) + } + + return dir +} + +func createTestFiles(t *testing.T, dir string, files ...string) { + for _, target := range files { + f, err := os.Create(filepath.Join(dir, target)) + require.NoError(t, err) + require.NoError(t, f.Close()) + } +} + +func delTestDir(t *testing.T, dir string) { + require.NoError(t, os.RemoveAll(dir)) +} diff --git a/pkg/message/parser.go b/pkg/message/parser.go index a77128b9..85beaf27 100644 --- a/pkg/message/parser.go +++ b/pkg/message/parser.go @@ -38,6 +38,15 @@ import ( // Parse parses RAW message. func Parse(r io.Reader) (m *pmapi.Message, mimeBody, plainBody string, attReaders []io.Reader, err error) { + defer func() { + r := recover() + if r == nil { + return + } + + err = fmt.Errorf("panic while parsing message: %v", r) + }() + p, err := parser.New(r) if err != nil { return nil, "", "", nil, errors.Wrap(err, "failed to create new parser") diff --git a/pkg/message/parser_test.go b/pkg/message/parser_test.go index f2f8fd2e..bd9732a8 100644 --- a/pkg/message/parser_test.go +++ b/pkg/message/parser_test.go @@ -545,6 +545,20 @@ func TestParseEncodedContentTypeBad(t *testing.T) { require.Error(t, err) } +type panicReader struct{} + +func (panicReader) Read(p []byte) (int, error) { + panic("lol") +} + +func TestParsePanic(t *testing.T) { + var err error + require.NotPanics(t, func() { + _, _, _, _, err = Parse(&panicReader{}) + }) + require.Error(t, err) +} + func getFileReader(filename string) io.Reader { f, err := os.Open(filepath.Join("testdata", filename)) if err != nil { diff --git a/pkg/pmapi/client_types.go b/pkg/pmapi/client_types.go index 354cdc3d..340bc811 100644 --- a/pkg/pmapi/client_types.go +++ b/pkg/pmapi/client_types.go @@ -81,4 +81,6 @@ type Client interface { KeyRingForAddressID(string) (kr *crypto.KeyRing, err error) GetPublicKeysForEmail(string) ([]PublicKey, bool, error) + + DownloadAndVerify(string, string, *crypto.KeyRing) (io.Reader, error) } diff --git a/pkg/pmapi/clientmanager.go b/pkg/pmapi/clientmanager.go index 7526e308..c7cbfc3d 100644 --- a/pkg/pmapi/clientmanager.go +++ b/pkg/pmapi/clientmanager.go @@ -137,10 +137,18 @@ func (cm *ClientManager) SetRoundTripper(rt http.RoundTripper) { cm.roundTripper = rt } +func (cm *ClientManager) GetClientConfig() *ClientConfig { + return cm.config +} + func (cm *ClientManager) SetUserAgent(clientName, clientVersion, os string) { cm.config.UserAgent = formatUserAgent(clientName, clientVersion, os) } +func (cm *ClientManager) GetUserAgent() string { + return cm.config.UserAgent +} + // GetClient returns a client for the given userID. // If the client does not exist already, it is created. func (cm *ClientManager) GetClient(userID string) Client { @@ -366,7 +374,7 @@ func (cm *ClientManager) clearToken(userID string) { cm.tokensLocker.Lock() defer cm.tokensLocker.Unlock() - logrus.WithField("userID", userID).Info("Clearing token") + logrus.WithField("userID", userID).Debug("Clearing token") delete(cm.tokens, userID) } diff --git a/pkg/pmapi/config.go b/pkg/pmapi/config.go index 413be89e..655b6d02 100644 --- a/pkg/pmapi/config.go +++ b/pkg/pmapi/config.go @@ -18,23 +18,23 @@ package pmapi import ( - "net/http" + "strings" + "time" ) // rootURL is the API root URL. -// -// This can be changed using build flags: pmapi_local for "localhost/api", pmapi_dev or pmapi_prod. -// Default is pmapi_prod. -// // It must not contain the protocol! The protocol should be in rootScheme. var rootURL = "api.protonmail.ch" //nolint[gochecknoglobals] -var rootScheme = "https" //nolint[gochecknoglobals] -// The HTTP transport to use by default. -var defaultTransport = &http.Transport{ //nolint[gochecknoglobals] - Proxy: http.ProxyFromEnvironment, +// rootScheme is the scheme to use for connections to the root URL. +var rootScheme = "https" //nolint[gochecknoglobals] + +func GetAPIConfig(configName, appVersion string) *ClientConfig { + return &ClientConfig{ + AppVersion: strings.Title(configName) + "_" + appVersion, + ClientID: configName, + Timeout: 25 * time.Minute, // Overall request timeout (~25MB / 25 mins => ~16kB/s, should be reasonable). + FirstReadTimeout: 30 * time.Second, // 30s to match 30s response header timeout. + MinBytesPerSecond: 1 << 10, // Enforce minimum download speed of 1kB/s. + } } - -// checkTLSCerts controls whether TLS certs are checked against known fingerprints. -// The default is for this to always be done. -var checkTLSCerts = true //nolint[gochecknoglobals] diff --git a/pkg/config/pmapi_prod.go b/pkg/pmapi/config_default.go similarity index 55% rename from pkg/config/pmapi_prod.go rename to pkg/pmapi/config_default.go index 223f9e6c..44044b99 100644 --- a/pkg/config/pmapi_prod.go +++ b/pkg/pmapi/config_default.go @@ -15,43 +15,30 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// +build pmapi_prod +// +build !pmapi_qa -package config +package pmapi import ( "net/http" - "strings" - "time" "github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/pkg/listener" - "github.com/ProtonMail/proton-bridge/pkg/pmapi" ) -func (c *Config) GetAPIConfig() *pmapi.ClientConfig { - return &pmapi.ClientConfig{ - AppVersion: c.getAPIOS() + strings.Title(c.appName) + "_" + c.version, - ClientID: c.appName, - Timeout: 25 * time.Minute, // Overall request timeout (~25MB / 25 mins => ~16kB/s, should be reasonable). - FirstReadTimeout: 30 * time.Second, // 30s to match 30s response header timeout. - MinBytesPerSecond: 1 << 10, // Enforce minimum download speed of 1kB/s. - } -} - -func (c *Config) GetRoundTripper(cm *pmapi.ClientManager, listener listener.Listener) http.RoundTripper { +func GetRoundTripper(cm *ClientManager, listener listener.Listener) http.RoundTripper { // We use a TLS dialer. - basicDialer := pmapi.NewBasicTLSDialer() + basicDialer := NewBasicTLSDialer() // We wrap the TLS dialer in a layer which enforces connections to trusted servers. - pinningDialer := pmapi.NewPinningTLSDialer(basicDialer) + pinningDialer := NewPinningTLSDialer(basicDialer) // We want any pin mismatches to be communicated back to bridge GUI and reported. pinningDialer.SetTLSIssueNotifier(func() { listener.Emit(events.TLSCertIssue, "") }) - pinningDialer.EnableRemoteTLSIssueReporting(c.GetAPIConfig().AppVersion, c.GetAPIConfig().UserAgent) + pinningDialer.EnableRemoteTLSIssueReporting(cm) // We wrap the pinning dialer in a layer which adds "alternative routing" feature. - proxyDialer := pmapi.NewProxyTLSDialer(pinningDialer, cm) + proxyDialer := NewProxyTLSDialer(pinningDialer, cm) - return pmapi.CreateTransportWithDialer(proxyDialer) + return CreateTransportWithDialer(proxyDialer) } diff --git a/pkg/pmapi/config_qa.go b/pkg/pmapi/config_qa.go index 9e748aa3..4dd3c598 100644 --- a/pkg/pmapi/config_qa.go +++ b/pkg/pmapi/config_qa.go @@ -24,6 +24,8 @@ import ( "net/http" "os" "strings" + + "github.com/ProtonMail/proton-bridge/pkg/listener" ) func init() { @@ -37,13 +39,13 @@ func init() { rootURL = fullRootURL rootScheme = "https" } +} + +func GetRoundTripper(_ *ClientManager, _ listener.Listener) http.RoundTripper { + transport := CreateTransportWithDialer(NewBasicTLSDialer()) // TLS certificate of testing environment might be self-signed. - defaultTransport = &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } + transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - // This config disables TLS cert checking. - checkTLSCerts = false + return transport } diff --git a/pkg/pmapi/dialer_pinning.go b/pkg/pmapi/dialer_pinning.go index 84bcb984..7ebd2e48 100644 --- a/pkg/pmapi/dialer_pinning.go +++ b/pkg/pmapi/dialer_pinning.go @@ -30,19 +30,12 @@ type PinningTLSDialer struct { dialer TLSDialer // pinChecker is used to check TLS keys of connections. - pinChecker pinChecker + pinChecker *pinChecker // tlsIssueNotifier is used to notify something when there is a TLS issue. tlsIssueNotifier func() - // appVersion is needed to report TLS mismatches. - appVersion string - - // userAgent is needed to report TLS mismatches. - userAgent string - - // enableRemoteReporting instructs the dialer to report TLS mismatches. - enableRemoteReporting bool + reporter *tlsReporter // A logger for logging messages. log logrus.FieldLogger @@ -63,41 +56,38 @@ func (p *PinningTLSDialer) SetTLSIssueNotifier(notifier func()) { p.tlsIssueNotifier = notifier } -func (p *PinningTLSDialer) EnableRemoteTLSIssueReporting(appVersion, userAgent string) { - p.enableRemoteReporting = true - p.appVersion = appVersion - p.userAgent = userAgent +func (p *PinningTLSDialer) EnableRemoteTLSIssueReporting(cm *ClientManager) { + p.reporter = newTLSReporter(p.pinChecker, cm) } // DialTLS dials the given network/address, returning an error if the certificates don't match the trusted pins. -func (p *PinningTLSDialer) DialTLS(network, address string) (conn net.Conn, err error) { - if conn, err = p.dialer.DialTLS(network, address); err != nil { - return +func (p *PinningTLSDialer) DialTLS(network, address string) (net.Conn, error) { + conn, err := p.dialer.DialTLS(network, address) + if err != nil { + return nil, err } host, port, err := net.SplitHostPort(address) if err != nil { - return + return nil, err } - if err = p.pinChecker.checkCertificate(conn); err != nil { + if err := p.pinChecker.checkCertificate(conn); err != nil { if p.tlsIssueNotifier != nil { go p.tlsIssueNotifier() } - if tlsConn, ok := conn.(*tls.Conn); ok && p.enableRemoteReporting { - p.pinChecker.reportCertIssue( + if tlsConn, ok := conn.(*tls.Conn); ok && p.reporter != nil { + p.reporter.reportCertIssue( TLSReportURI, host, port, tlsConn.ConnectionState(), - p.appVersion, - p.userAgent, ) } - return + return nil, err } - return + return conn, nil } diff --git a/pkg/pmapi/download.go b/pkg/pmapi/download.go new file mode 100644 index 00000000..a7353256 --- /dev/null +++ b/pkg/pmapi/download.go @@ -0,0 +1,74 @@ +// 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 . + +package pmapi + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "net/http" + + "github.com/ProtonMail/gopenpgp/v2/crypto" +) + +// DownloadAndVerify downloads a file and its signature from the given locations `file` and `sig`. +// The file and its signature are verified using the given keyring `kr`. +// If the file is verified successfully, it can be read from the returned reader. +// TLS fingerprinting is used to verify that connections are only made to known servers. +func (c *client) DownloadAndVerify(file, sig string, kr *crypto.KeyRing) (io.Reader, error) { + var fb, sb []byte + + if err := c.fetchFile(file, func(r io.Reader) (err error) { + fb, err = ioutil.ReadAll(r) + return err + }); err != nil { + return nil, err + } + + if err := c.fetchFile(sig, func(r io.Reader) (err error) { + sb, err = ioutil.ReadAll(r) + return err + }); err != nil { + return nil, err + } + + if err := kr.VerifyDetached( + crypto.NewPlainMessage(fb), + crypto.NewPGPSignature(sb), + crypto.GetUnixTime(), + ); err != nil { + return nil, err + } + + return bytes.NewReader(fb), nil +} + +func (c *client) fetchFile(file string, fn func(io.Reader) error) error { + res, err := c.hc.Get(file) + if err != nil { + return err + } + defer func() { _ = res.Body.Close() }() + + if res.StatusCode != http.StatusOK { + return fmt.Errorf("failed to get file: http error %v", res.StatusCode) + } + + return fn(res.Body) +} diff --git a/pkg/pmapi/mocks/mocks.go b/pkg/pmapi/mocks/mocks.go index ecf7103c..e44b71de 100644 --- a/pkg/pmapi/mocks/mocks.go +++ b/pkg/pmapi/mocks/mocks.go @@ -294,6 +294,21 @@ func (mr *MockClientMockRecorder) DeleteMessages(arg0 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessages", reflect.TypeOf((*MockClient)(nil).DeleteMessages), arg0) } +// DownloadAndVerify mocks base method +func (m *MockClient) DownloadAndVerify(arg0, arg1 string, arg2 *crypto.KeyRing) (io.Reader, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DownloadAndVerify", arg0, arg1, arg2) + ret0, _ := ret[0].(io.Reader) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DownloadAndVerify indicates an expected call of DownloadAndVerify +func (mr *MockClientMockRecorder) DownloadAndVerify(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadAndVerify", reflect.TypeOf((*MockClient)(nil).DownloadAndVerify), arg0, arg1, arg2) +} + // EmptyFolder mocks base method func (m *MockClient) EmptyFolder(arg0, arg1 string) error { m.ctrl.T.Helper() diff --git a/pkg/pmapi/pin_checker.go b/pkg/pmapi/pin_checker.go index ce2a1c9c..3b2cf1c7 100644 --- a/pkg/pmapi/pin_checker.go +++ b/pkg/pmapi/pin_checker.go @@ -35,7 +35,6 @@ import ( type pinChecker struct { trustedPins []string - sentReports []sentReport } type sentReport struct { @@ -43,8 +42,8 @@ type sentReport struct { t time.Time } -func newPinChecker(trustedPins []string) pinChecker { - return pinChecker{ +func newPinChecker(trustedPins []string) *pinChecker { + return &pinChecker{ trustedPins: trustedPins, } } @@ -76,8 +75,25 @@ func certFingerprint(cert *x509.Certificate) string { return fmt.Sprintf(`pin-sha256=%q`, base64.StdEncoding.EncodeToString(hash[:])) } +type clientConfigProvider interface { + GetClientConfig() *ClientConfig +} + +type tlsReporter struct { + cm clientConfigProvider + p *pinChecker + sentReports []sentReport +} + +func newTLSReporter(p *pinChecker, cm clientConfigProvider) *tlsReporter { + return &tlsReporter{ + cm: cm, + p: p, + } +} + // reportCertIssue reports a TLS key mismatch. -func (p *pinChecker) reportCertIssue(remoteURI, host, port string, connState tls.ConnectionState, appVersion, userAgent string) { +func (r *tlsReporter) reportCertIssue(remoteURI, host, port string, connState tls.ConnectionState) { var certChain []string if len(connState.VerifiedChains) > 0 { @@ -86,27 +102,29 @@ func (p *pinChecker) reportCertIssue(remoteURI, host, port string, connState tls certChain = marshalCert7468(connState.PeerCertificates) } - r := newTLSReport(host, port, connState.ServerName, certChain, p.trustedPins, appVersion) + cfg := r.cm.GetClientConfig() - if !p.hasRecentlySentReport(r) { - p.recordReport(r) - go r.sendReport(remoteURI, userAgent) + report := newTLSReport(host, port, connState.ServerName, certChain, r.p.trustedPins, cfg.AppVersion) + + if !r.hasRecentlySentReport(report) { + r.recordReport(report) + go report.sendReport(remoteURI, cfg.UserAgent) } } // hasRecentlySentReport returns whether the report was already sent within the last 24 hours. -func (p *pinChecker) hasRecentlySentReport(report tlsReport) bool { +func (r *tlsReporter) hasRecentlySentReport(report tlsReport) bool { var validReports []sentReport - for _, r := range p.sentReports { + for _, r := range r.sentReports { if time.Since(r.t) < 24*time.Hour { validReports = append(validReports, r) } } - p.sentReports = validReports + r.sentReports = validReports - for _, r := range p.sentReports { + for _, r := range r.sentReports { if cmp.Equal(report, r.r) { return true } @@ -116,8 +134,8 @@ func (p *pinChecker) hasRecentlySentReport(report tlsReport) bool { } // recordReport records the given report and the current time so we can check whether we recently sent this report. -func (p *pinChecker) recordReport(r tlsReport) { - p.sentReports = append(p.sentReports, sentReport{r: r, t: time.Now()}) +func (r *tlsReporter) recordReport(report tlsReport) { + r.sentReports = append(r.sentReports, sentReport{r: report, t: time.Now()}) } func marshalCert7468(certs []*x509.Certificate) (pemCerts []string) { diff --git a/pkg/pmapi/pin_checker_test.go b/pkg/pmapi/pin_checker_test.go index eef9649f..1c0ad0f4 100644 --- a/pkg/pmapi/pin_checker_test.go +++ b/pkg/pmapi/pin_checker_test.go @@ -27,6 +27,14 @@ import ( "github.com/stretchr/testify/assert" ) +type fakeClientConfigProvider struct { + version, useragent string +} + +func (c *fakeClientConfigProvider) GetClientConfig() *ClientConfig { + return &ClientConfig{AppVersion: c.version, UserAgent: c.useragent} +} + func TestPinCheckerDoubleReport(t *testing.T) { reportCounter := 0 @@ -34,11 +42,11 @@ func TestPinCheckerDoubleReport(t *testing.T) { reportCounter++ })) - pc := newPinChecker(TrustedAPIPins) + r := newTLSReporter(newPinChecker(TrustedAPIPins), &fakeClientConfigProvider{version: "3", useragent: "useragent"}) // Report the same issue many times. for i := 0; i < 10; i++ { - pc.reportCertIssue(reportServer.URL, "myhost", "443", tls.ConnectionState{}, "3", "useragent") + r.reportCertIssue(reportServer.URL, "myhost", "443", tls.ConnectionState{}) } // We should only report once. @@ -48,7 +56,7 @@ func TestPinCheckerDoubleReport(t *testing.T) { // If we then report something else many times. for i := 0; i < 10; i++ { - pc.reportCertIssue(reportServer.URL, "anotherhost", "443", tls.ConnectionState{}, "3", "useragent") + r.reportCertIssue(reportServer.URL, "anotherhost", "443", tls.ConnectionState{}) } // We should get a second report. diff --git a/pkg/pmapi/users.go b/pkg/pmapi/users.go index f79ef004..83372db5 100644 --- a/pkg/pmapi/users.go +++ b/pkg/pmapi/users.go @@ -120,9 +120,7 @@ func (c *client) UpdateUser() (user *User, err error) { c.user = user sentry.ConfigureScope(func(scope *sentry.Scope) { - scope.SetUser(sentry.User{ - ID: user.ID, - }) + scope.SetUser(sentry.User{ID: user.ID}) }) var tmpList AddressList diff --git a/pkg/ports/ports.go b/pkg/ports/ports.go index 09e93847..6aa16df6 100644 --- a/pkg/ports/ports.go +++ b/pkg/ports/ports.go @@ -18,8 +18,8 @@ package ports import ( + "fmt" "net" - "strconv" ) const ( @@ -31,9 +31,12 @@ func IsPortFree(port int) bool { if !(0 < port && port < maxPortNumber) { return false } - stringPort := ":" + strconv.Itoa(port) - isFree := !isOccupied(stringPort) - return isFree + // First, check localhost only. + if isOccupied(fmt.Sprintf("127.0.0.1:%d", port)) { + return false + } + // Second, check also ports opened to public. + return !isOccupied(fmt.Sprintf(":%d", port)) } func isOccupied(port string) bool { diff --git a/pkg/sentry/report.go b/pkg/sentry/reporter.go similarity index 61% rename from pkg/sentry/report.go rename to pkg/sentry/reporter.go index d7773a91..9f2902e6 100644 --- a/pkg/sentry/report.go +++ b/pkg/sentry/reporter.go @@ -19,28 +19,76 @@ package sentry import ( "errors" + "fmt" + "os" "runtime" "time" + "github.com/ProtonMail/proton-bridge/internal/constants" "github.com/getsentry/sentry-go" - log "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus" ) -var ( - skippedFunctions = []string{} //nolint[gochecknoglobals] -) +var skippedFunctions = []string{} //nolint[gochecknoglobals] -// ReportSentryCrash reports a sentry crash. -func ReportSentryCrash(clientID, appVersion, userAgent string, reportErr error) error { +func init() { // nolint[noinit] + if err := sentry.Init(sentry.ClientOptions{ + Dsn: constants.DSNSentry, + Release: constants.Revision, + BeforeSend: EnhanceSentryEvent, + }); err != nil { + logrus.WithError(err).Error("Failed to initialize sentry options") + } + + sentry.ConfigureScope(func(scope *sentry.Scope) { + scope.SetFingerprint([]string{"{{ default }}"}) + }) +} + +type userAgentProvider interface { + GetUserAgent() string +} + +type Reporter struct { + appName string + appVersion string + uap userAgentProvider +} + +// NewReporter creates new sentry reporter with appName and appVersion to report. +func NewReporter(appName, appVersion string) *Reporter { + return &Reporter{ + appName: appName, + appVersion: appVersion, + } +} + +func (r *Reporter) SetUserAgentProvider(uap userAgentProvider) { + r.uap = uap +} + +// Report reports a sentry crash with stacktrace from all goroutines. +func (r *Reporter) Report(i interface{}) (err error) { SkipDuringUnwind() - if reportErr == nil { + + if os.Getenv("PROTONMAIL_ENV") == "dev" { return nil } + // In case clientManager is not yet created we can get at least OS string. + var userAgent string + if r.uap != nil { + userAgent = r.uap.GetUserAgent() + } else { + userAgent = runtime.GOOS + } + + reportErr := fmt.Errorf("recover: %v", i) + tags := map[string]string{ "OS": runtime.GOOS, - "Client": clientID, - "Version": appVersion, + "Client": r.appName, + "Version": r.appVersion, "UserAgent": userAgent, "UserID": "", } @@ -49,18 +97,17 @@ func ReportSentryCrash(clientID, appVersion, userAgent string, reportErr error) sentry.WithScope(func(scope *sentry.Scope) { SkipDuringUnwind() scope.SetTags(tags) - eventID := sentry.CaptureException(reportErr) - if eventID != nil { + if eventID := sentry.CaptureException(reportErr); eventID != nil { reportID = string(*eventID) } }) if !sentry.Flush(time.Second * 10) { - log.WithField("error", reportErr).Error("Failed to report sentry error") return errors.New("failed to report sentry error") } - log.WithField("error", reportErr).WithField("id", reportID).Warn("Sentry error reported") + logrus.WithField("error", reportErr).WithField("id", reportID).Warn("Sentry error reported") + return nil } diff --git a/pkg/sentry/report_test.go b/pkg/sentry/reporter_test.go similarity index 100% rename from pkg/sentry/report_test.go rename to pkg/sentry/reporter_test.go diff --git a/pkg/config/logs_all.go b/pkg/signature/signature.go similarity index 53% rename from pkg/config/logs_all.go rename to pkg/signature/signature.go index 0bb0a3fa..87cc3a0b 100644 --- a/pkg/config/logs_all.go +++ b/pkg/signature/signature.go @@ -15,35 +15,29 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// +build !build_qa - -package config +// Package signature implements functions to verify files by their detached signatures. +package signature import ( - "github.com/sirupsen/logrus" + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/pkg/errors" ) -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", "debug-client", "debug-server", "debug-client-json", "debug-server-json": - level = logrus.DebugLevel - useFile = false - case "trace": - level = logrus.TraceLevel - useFile = false - default: - level = logrus.InfoLevel +// Verify verifies the given file by its signature using the given armored public key. +func Verify(fileBytes, sigBytes []byte, pubKey string) error { + key, err := crypto.NewKeyFromArmored(pubKey) + if err != nil { + return errors.Wrap(err, "failed to load key") } - return + + kr, err := crypto.NewKeyRing(key) + if err != nil { + return errors.Wrap(err, "failed to create keyring") + } + + return kr.VerifyDetached( + crypto.NewPlainMessage(fileBytes), + crypto.NewPGPSignature(sigBytes), + crypto.GetUnixTime(), + ) } diff --git a/pkg/tar/tar.go b/pkg/tar/tar.go new file mode 100644 index 00000000..86d2b9a6 --- /dev/null +++ b/pkg/tar/tar.go @@ -0,0 +1,76 @@ +// 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 . + +package tar + +import ( + "archive/tar" + "io" + "os" + "path/filepath" + "runtime" + + "github.com/sirupsen/logrus" +) + +func UntarToDir(r io.Reader, dir string) error { + tr := tar.NewReader(r) + + for { + header, err := tr.Next() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + if header == nil { + continue + } + + target := filepath.Join(dir, header.Name) + + switch { + case header.Typeflag == tar.TypeSymlink: + if err := os.Symlink(header.Linkname, target); err != nil { + return err + } + + case header.FileInfo().IsDir(): + if err := os.MkdirAll(target, header.FileInfo().Mode()); err != nil { + return err + } + + default: + f, err := os.Create(target) + if err != nil { + return err + } + if _, err := io.Copy(f, tr); err != nil { // nolint[gosec] + return err + } + if runtime.GOOS != "windows" { + if err := f.Chmod(header.FileInfo().Mode()); err != nil { + return err + } + } + if err := f.Close(); err != nil { + logrus.WithError(err).Error("Failed to close file") + } + } + } +} diff --git a/test/api_checks_test.go b/test/api_checks_test.go index 5734097e..40b6dbb5 100644 --- a/test/api_checks_test.go +++ b/test/api_checks_test.go @@ -20,10 +20,13 @@ package tests import ( "fmt" "regexp" + "runtime" "strings" + "time" "github.com/cucumber/godog" "github.com/cucumber/godog/gherkin" + "github.com/stretchr/testify/assert" ) func APIChecksFeatureContext(s *godog.Suite) { @@ -31,6 +34,7 @@ func APIChecksFeatureContext(s *godog.Suite) { s.Step(`^message is sent with API call$`, messageIsSentWithAPICall) s.Step(`^API mailbox "([^"]*)" for "([^"]*)" has messages$`, apiMailboxForUserHasMessages) s.Step(`^API mailbox "([^"]*)" for address "([^"]*)" of "([^"]*)" has messages$`, apiMailboxForAddressOfUserHasMessages) + s.Step(`^API client manager user-agent is "([^"]*)"$`, clientManagerUserAgent) } func apiIsCalledWith(endpoint string, data *gherkin.DocString) error { @@ -117,3 +121,14 @@ func apiMailboxForAddressOfUserHasMessages(mailboxName, bddAddressID, bddUserID } return nil } + +func clientManagerUserAgent(expectedUserAgent string) error { + expectedUserAgent = strings.ReplaceAll(expectedUserAgent, "[GOOS]", runtime.GOOS) + + assert.Eventually(ctx.GetTestingT(), func() bool { + userAgent := ctx.GetClientManager().GetUserAgent() + return userAgent == expectedUserAgent + }, 5*time.Second, time.Second) + + return nil +} diff --git a/test/context/bridge.go b/test/context/bridge.go index f8db3dc9..40740aad 100644 --- a/test/context/bridge.go +++ b/test/context/bridge.go @@ -21,7 +21,6 @@ import ( "time" "github.com/ProtonMail/proton-bridge/internal/bridge" - "github.com/ProtonMail/proton-bridge/internal/preferences" "github.com/ProtonMail/proton-bridge/internal/users" "github.com/ProtonMail/proton-bridge/pkg/listener" ) @@ -34,7 +33,7 @@ func (ctx *TestContext) GetBridge() *bridge.Bridge { // withBridgeInstance creates a bridge instance for use in the test. // TestContext has this by default once called with env variable TEST_APP=bridge. func (ctx *TestContext) withBridgeInstance() { - ctx.bridge = newBridgeInstance(ctx.t, ctx.cfg, ctx.credStore, ctx.listener, ctx.clientManager) + ctx.bridge = newBridgeInstance(ctx.t, ctx.locations, ctx.cache, ctx.settings, ctx.credStore, ctx.listener, ctx.clientManager) ctx.users = ctx.bridge.Users ctx.addCleanupChecked(ctx.bridge.ClearData, "Cleaning bridge data") } @@ -61,12 +60,13 @@ func (ctx *TestContext) RestartBridge() error { // newBridgeInstance creates a new bridge instance configured to use the given config/credstore. func newBridgeInstance( t *bddT, - cfg *fakeConfig, + locations bridge.Locator, + cache bridge.Cacher, + settings *fakeSettings, credStore users.CredentialsStorer, eventListener listener.Listener, clientManager users.ClientManager, ) *bridge.Bridge { panicHandler := &panicHandler{t: t} - pref := preferences.New(cfg) - return bridge.New(cfg, pref, panicHandler, eventListener, clientManager, credStore) + return bridge.New(locations, cache, settings, panicHandler, eventListener, clientManager, credStore) } diff --git a/test/context/cache.go b/test/context/cache.go new file mode 100644 index 00000000..1313536b --- /dev/null +++ b/test/context/cache.go @@ -0,0 +1,55 @@ +// Copyright (c) 2020 Proton Technologies AG +// +// This file is part of ProtonMail Bridge.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 . + +package context + +import ( + "io/ioutil" + "path/filepath" +) + +type fakeCache struct { + dir string +} + +// newFakeCache creates a temporary folder for files. +// It's expected the test calls `ClearData` before finish to remove it from the file system. +func newFakeCache() *fakeCache { + dir, err := ioutil.TempDir("", "test-cache") + if err != nil { + panic(err) + } + + return &fakeCache{ + dir: dir, + } +} + +// GetDBDir returns folder for db files. +func (c *fakeCache) GetDBDir() string { + return c.dir +} + +// GetIMAPCachePath returns path to file with IMAP status. +func (c *fakeCache) GetIMAPCachePath() string { + return filepath.Join(c.dir, "user_info.json") +} + +// GetTransferDir returns folder for import-export rules files. +func (c *fakeCache) GetTransferDir() string { + return c.dir +} diff --git a/test/context/config.go b/test/context/config.go deleted file mode 100644 index 8bd2d557..00000000 --- a/test/context/config.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) 2021 Proton Technologies AG -// -// This file is part of ProtonMail Bridge.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 . - -package context - -import ( - "io/ioutil" - "math/rand" - "os" - "path/filepath" - - "github.com/ProtonMail/proton-bridge/pkg/config" - "github.com/ProtonMail/proton-bridge/pkg/constants" - "github.com/ProtonMail/proton-bridge/pkg/pmapi" - "github.com/sirupsen/logrus" -) - -type fakeConfig struct { - dir string -} - -// newFakeConfig creates a temporary folder for files. -// It's expected the test calls `ClearData` before finish to remove it from the file system. -func newFakeConfig() *fakeConfig { - dir, err := ioutil.TempDir("", "example") - if err != nil { - panic(err) - } - - cfg := &fakeConfig{ - dir: dir, - } - - // We must generate cert.pem and key.pem to prevent errors when attempting to open them. - if _, err = config.GenerateTLSConfig(cfg.GetTLSCertPath(), cfg.GetTLSKeyPath()); err != nil { - logrus.WithError(err).Fatal() - } - - return cfg -} - -func (c *fakeConfig) ClearData() error { - return os.RemoveAll(c.dir) -} -func (c *fakeConfig) GetAPIConfig() *pmapi.ClientConfig { - return &pmapi.ClientConfig{ - AppVersion: "Bridge_" + constants.Version, - ClientID: "bridge", - } -} -func (c *fakeConfig) GetDBDir() string { - return c.dir -} -func (c *fakeConfig) GetVersion() string { - return constants.Version -} -func (c *fakeConfig) GetLogDir() string { - return c.dir -} -func (c *fakeConfig) GetLogPrefix() string { - return "test" -} -func (c *fakeConfig) GetPreferencesPath() string { - return filepath.Join(c.dir, "prefs.json") -} -func (c *fakeConfig) GetTransferDir() string { - return c.dir -} -func (c *fakeConfig) GetTLSCertPath() string { - return filepath.Join(c.dir, "cert.pem") -} -func (c *fakeConfig) GetTLSKeyPath() string { - return filepath.Join(c.dir, "key.pem") -} -func (c *fakeConfig) GetEventsPath() string { - return filepath.Join(c.dir, "events.json") -} -func (c *fakeConfig) GetIMAPCachePath() string { - return filepath.Join(c.dir, "user_info.json") -} -func (c *fakeConfig) GetDefaultAPIPort() int { - return 21042 -} -func (c *fakeConfig) GetDefaultIMAPPort() int { - return 21100 + rand.Intn(100) -} -func (c *fakeConfig) GetDefaultSMTPPort() int { - return 21200 + rand.Intn(100) -} diff --git a/test/context/context.go b/test/context/context.go index e34ea088..382ee098 100644 --- a/test/context/context.go +++ b/test/context/context.go @@ -41,7 +41,9 @@ type server interface { type TestContext struct { // Base setup for the whole bridge (core & imap & smtp). t *bddT - cfg *fakeConfig + cache *fakeCache + locations *fakeLocations + settings *fakeSettings listener listener.Listener testAccounts *accounts.TestAccounts @@ -92,13 +94,13 @@ type TestContext struct { func New(app string) *TestContext { setLogrusVerbosityFromEnv() - cfg := newFakeConfig() - - cm := pmapi.NewClientManager(cfg.GetAPIConfig()) + cm := pmapi.NewClientManager(pmapi.GetAPIConfig("TODO", "TODO")) ctx := &TestContext{ t: &bddT{}, - cfg: cfg, + cache: newFakeCache(), + locations: newFakeLocations(), + settings: newFakeSettings(), listener: listener.New(), pmapiController: newPMAPIController(cm), clientManager: cm, @@ -115,7 +117,7 @@ func New(app string) *TestContext { } // Ensure that the config is cleaned up after the test is over. - ctx.addCleanupChecked(cfg.ClearData, "Cleaning bridge config data") + ctx.addCleanupChecked(ctx.locations.Clear, "Cleaning bridge config data") // Create bridge or import-export instance under test. switch app { @@ -144,6 +146,11 @@ func (ctx *TestContext) GetPMAPIController() PMAPIController { return ctx.pmapiController } +// GetClientManager returns client manager being used for testing. +func (ctx *TestContext) GetClientManager() *pmapi.ClientManager { + return ctx.clientManager +} + // GetTestingT returns testing.T compatible struct. func (ctx *TestContext) GetTestingT() *bddT { //nolint[golint] return ctx.t diff --git a/test/context/imap.go b/test/context/imap.go index 99d16300..bcbe3775 100644 --- a/test/context/imap.go +++ b/test/context/imap.go @@ -22,9 +22,9 @@ import ( "time" "github.com/ProtonMail/proton-bridge/internal/bridge" + "github.com/ProtonMail/proton-bridge/internal/config/settings" + "github.com/ProtonMail/proton-bridge/internal/config/tls" "github.com/ProtonMail/proton-bridge/internal/imap" - "github.com/ProtonMail/proton-bridge/internal/preferences" - "github.com/ProtonMail/proton-bridge/pkg/config" "github.com/ProtonMail/proton-bridge/test/mocks" "github.com/stretchr/testify/require" ) @@ -53,12 +53,12 @@ func (ctx *TestContext) withIMAPServer() { return } + settingsPath, _ := ctx.locations.ProvideSettingsPath() ph := newPanicHandler(ctx.t) - pref := preferences.New(ctx.cfg) - port := pref.GetInt(preferences.IMAPPortKey) - tls, _ := config.GetTLSConfig(ctx.cfg) + port := ctx.settings.GetInt(settings.IMAPPortKey) + tls, _ := tls.New(settingsPath).GetConfig() - backend := imap.NewIMAPBackend(ph, ctx.listener, ctx.cfg, ctx.bridge) + backend := imap.NewIMAPBackend(ph, ctx.listener, ctx.cache, ctx.bridge) server := imap.NewIMAPServer(true, true, port, tls, backend, ctx.listener) go server.ListenAndServe() diff --git a/test/context/importexport.go b/test/context/importexport.go index 0e53ea72..11c95e39 100644 --- a/test/context/importexport.go +++ b/test/context/importexport.go @@ -31,18 +31,19 @@ func (ctx *TestContext) GetImportExport() *importexport.ImportExport { // withImportExportInstance creates a import-export instance for use in the test. // TestContext has this by default once called with env variable TEST_APP=ie. func (ctx *TestContext) withImportExportInstance() { - ctx.importExport = newImportExportInstance(ctx.t, ctx.cfg, ctx.credStore, ctx.listener, ctx.clientManager) + ctx.importExport = newImportExportInstance(ctx.t, ctx.locations, ctx.cache, ctx.credStore, ctx.listener, ctx.clientManager) ctx.users = ctx.importExport.Users } // newImportExportInstance creates a new import-export instance configured to use the given config/credstore. func newImportExportInstance( t *bddT, - cfg importexport.Configer, + locations importexport.Locator, + cache importexport.Cacher, credStore users.CredentialsStorer, eventListener listener.Listener, clientManager users.ClientManager, ) *importexport.ImportExport { panicHandler := &panicHandler{t: t} - return importexport.New(cfg, panicHandler, eventListener, clientManager, credStore) + return importexport.New(locations, cache, panicHandler, eventListener, clientManager, credStore) } diff --git a/test/context/locations.go b/test/context/locations.go new file mode 100644 index 00000000..98c6ac4c --- /dev/null +++ b/test/context/locations.go @@ -0,0 +1,50 @@ +// Copyright (c) 2020 Proton Technologies AG +// +// This file is part of ProtonMail Bridge.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 . + +package context + +import ( + "io/ioutil" + "os" +) + +type fakeLocations struct { + dir string +} + +func newFakeLocations() *fakeLocations { + dir, err := ioutil.TempDir("", "test-cache") + if err != nil { + panic(err) + } + + return &fakeLocations{ + dir: dir, + } +} + +func (l *fakeLocations) ProvideLogsPath() (string, error) { + return l.dir, nil +} + +func (l *fakeLocations) ProvideSettingsPath() (string, error) { + return l.dir, nil +} + +func (l *fakeLocations) Clear() error { + return os.RemoveAll(l.dir) +} diff --git a/test/context/settings.go b/test/context/settings.go new file mode 100644 index 00000000..7760f972 --- /dev/null +++ b/test/context/settings.go @@ -0,0 +1,50 @@ +// Copyright (c) 2020 Proton Technologies AG +// +// This file is part of ProtonMail Bridge.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 . + +package context + +import ( + "io/ioutil" + "math/rand" + + "github.com/ProtonMail/proton-bridge/internal/config/settings" +) + +type fakeSettings struct { + *settings.Settings + dir string +} + +// newFakeSettings creates a temporary folder for files. +// It's expected the test calls `ClearData` before finish to remove it from the file system. +func newFakeSettings() *fakeSettings { + dir, err := ioutil.TempDir("", "test-settings") + if err != nil { + panic(err) + } + + s := &fakeSettings{ + Settings: settings.New(dir), + dir: dir, + } + + // We should use nonstandard ports to not conflict with bridge. + s.SetInt(settings.IMAPPortKey, 21100+rand.Intn(100)) + s.SetInt(settings.SMTPPortKey, 21200+rand.Intn(100)) + + return s +} diff --git a/test/context/smtp.go b/test/context/smtp.go index 54c158f1..86cae97e 100644 --- a/test/context/smtp.go +++ b/test/context/smtp.go @@ -22,9 +22,9 @@ import ( "time" "github.com/ProtonMail/proton-bridge/internal/bridge" - "github.com/ProtonMail/proton-bridge/internal/preferences" + "github.com/ProtonMail/proton-bridge/internal/config/settings" + "github.com/ProtonMail/proton-bridge/internal/config/tls" "github.com/ProtonMail/proton-bridge/internal/smtp" - "github.com/ProtonMail/proton-bridge/pkg/config" "github.com/ProtonMail/proton-bridge/test/mocks" "github.com/stretchr/testify/require" ) @@ -53,13 +53,13 @@ func (ctx *TestContext) withSMTPServer() { return } + settingsPath, _ := ctx.locations.ProvideSettingsPath() ph := newPanicHandler(ctx.t) - pref := preferences.New(ctx.cfg) - tls, _ := config.GetTLSConfig(ctx.cfg) - port := pref.GetInt(preferences.SMTPPortKey) - useSSL := pref.GetBool(preferences.SMTPSSLKey) + tls, _ := tls.New(settingsPath).GetConfig() + port := ctx.settings.GetInt(settings.SMTPPortKey) + useSSL := ctx.settings.GetBool(settings.SMTPSSLKey) - backend := smtp.NewSMTPBackend(ph, ctx.listener, pref, ctx.bridge) + backend := smtp.NewSMTPBackend(ph, ctx.listener, ctx.settings, ctx.bridge) server := smtp.NewSMTPServer(true, port, useSSL, tls, backend, ctx.listener) go server.ListenAndServe() diff --git a/test/context/users.go b/test/context/users.go index a68f7f61..152da572 100644 --- a/test/context/users.go +++ b/test/context/users.go @@ -96,7 +96,7 @@ func (ctx *TestContext) GetStoreMailbox(username, addressID, mailboxName string) func (ctx *TestContext) GetDatabaseFilePath(userID string) string { // We cannot use store to get information because we need to check db file also when user is deleted from bridge. fileName := fmt.Sprintf("mailbox-%v.db", userID) - return filepath.Join(ctx.cfg.GetDBDir(), fileName) + return filepath.Join(ctx.cache.GetDBDir(), fileName) } // WaitForSync waits for sync to be done. diff --git a/test/fakeapi/download.go b/test/fakeapi/download.go new file mode 100644 index 00000000..4eee95cd --- /dev/null +++ b/test/fakeapi/download.go @@ -0,0 +1,28 @@ +// Copyright (c) 2020 Proton Technologies AG +// +// This file is part of ProtonMail Bridge.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 . + +package fakeapi + +import ( + "io" + + "github.com/ProtonMail/gopenpgp/v2/crypto" +) + +func (api *FakePMAPI) DownloadAndVerify(string, string, *crypto.KeyRing) (io.Reader, error) { + return nil, nil +} diff --git a/test/features/bridge/imap/user_agent.feature b/test/features/bridge/imap/user_agent.feature new file mode 100644 index 00000000..4919ab12 --- /dev/null +++ b/test/features/bridge/imap/user_agent.feature @@ -0,0 +1,24 @@ +Feature: User agent + Background: + Given there is connected user "user" + + Scenario: Get user agent + Given there is IMAP client logged in as "user" + When IMAP client sends ID with argument: + """ + "name" "Foo" "version" "1.4.0" + """ + Then API client manager user-agent is "Foo/1.4.0 ([GOOS])" + + Scenario: Update user agent + Given there is IMAP client logged in as "user" + When IMAP client sends ID with argument: + """ + "name" "Foo" "version" "1.4.0" + """ + Then API client manager user-agent is "Foo/1.4.0 ([GOOS])" + When IMAP client sends ID with argument: + """ + "name" "Bar" "version" "4.2.0" + """ + Then API client manager user-agent is "Bar/4.2.0 ([GOOS])" diff --git a/test/imap_actions_messages_test.go b/test/imap_actions_messages_test.go index 7b0beead..d4cea80a 100644 --- a/test/imap_actions_messages_test.go +++ b/test/imap_actions_messages_test.go @@ -58,6 +58,8 @@ func IMAPActionsMessagesFeatureContext(s *godog.Suite) { s.Step(`^IMAP client "([^"]*)" starts IDLE-ing$`, imapClientNamedStartsIDLEing) s.Step(`^IMAP client sends expunge$`, imapClientExpunge) s.Step(`^IMAP client "([^"]*)" sends expunge$`, imapClientNamedExpunge) + s.Step(`^IMAP client sends ID with argument:$`, imapClientSendsID) + s.Step(`^IMAP client "([^"]*)" sends ID with argument:$`, imapClientNamedSendsID) } func imapClientSendsCommand(command string) error { @@ -284,3 +286,13 @@ func imapClientNamedExpunge(imapClient string) error { ctx.SetIMAPLastResponse(imapClient, res) return nil } + +func imapClientSendsID(data *gherkin.DocString) error { + return imapClientNamedSendsID("imap", data) +} + +func imapClientNamedSendsID(imapClient string, data *gherkin.DocString) error { + res := ctx.GetIMAPClient(imapClient).ID(data.Content) + ctx.SetIMAPLastResponse(imapClient, res) + return nil +} diff --git a/test/main_test.go b/test/main_test.go index 1dcb6f18..c2f81a50 100644 --- a/test/main_test.go +++ b/test/main_test.go @@ -22,7 +22,7 @@ import ( "os" "testing" - "github.com/ProtonMail/proton-bridge/pkg/constants" + "github.com/ProtonMail/proton-bridge/internal/constants" "github.com/cucumber/godog" "github.com/cucumber/godog/colors" ) diff --git a/test/mocks/imap_client.go b/test/mocks/imap_client.go index e7ddc8d0..1d1c7fbe 100644 --- a/test/mocks/imap_client.go +++ b/test/mocks/imap_client.go @@ -231,7 +231,8 @@ func (c *IMAPClient) Expunge() *IMAPResponse { return c.SendCommand("EXPUNGE") } -// IDLE +// Extennsions +// Extennsions: IDLE func (c *IMAPClient) StartIDLE() *IMAPResponse { c.idling = true @@ -242,3 +243,9 @@ func (c *IMAPClient) StopIDLE() { c.idling = false fmt.Fprintf(c.conn, "%s\r\n", "DONE") } + +// Extennsions: ID + +func (c *IMAPClient) ID(request string) *IMAPResponse { + return c.SendCommand(fmt.Sprintf("ID (%v)", request)) +}