Compare commits

...

43 Commits

Author SHA1 Message Date
debe87f2f5 Other: Bridge Osney v2.4.8 2022-11-14 09:28:07 +01:00
cca2807256 GODT-2071: fix --no-window flag that was broken on Windows. 2022-11-11 21:34:56 +01:00
7b73f76e78 Other: Bridge Osney v2.4.7 2022-11-11 14:06:56 +01:00
b1eefd6c85 GODT-2078: Launcher inception. 2022-11-11 12:26:06 +01:00
bbcb7ad980 GODT-2039: fix --parent-pid flag is removed from command-line when restarting the application. 2022-11-11 11:10:27 +01:00
984c43cd75 Other: Bridge Osney v2.4.6 2022-11-10 15:41:33 +01:00
ec4c0fdd09 GODT-2019: when signing out and a single user is connected, we do not go back to the welcome screen. 2022-11-10 15:31:56 +01:00
51d4a9c7ee GODT-2071: bridge-gui report error if an orphan bridge is detected. 2022-11-10 15:31:55 +01:00
19930f63e2 GODT-2046: bridge-gui log is included in optional archive sent with bug reports. 2022-11-10 15:31:55 +01:00
3b9a3aaad2 GODT-2039: bridge monitors bridge-gui via its PID. 2022-11-10 15:31:55 +01:00
f5148074fd GODT-2038: interrupt gRPC initialisation of bridge process terminates. 2022-11-10 15:31:54 +01:00
a949a113cf Other: added timestamp to bridge-gui logs.
Changed format to be closer to logrus output.
2022-11-10 15:31:54 +01:00
227e9df419 GODT-2035: bridge-gui log includes Qt version info. 2022-11-10 15:31:53 +01:00
2a6d462be1 GODT-2031: updated bridge description. 2022-11-10 15:31:53 +01:00
bb03fa26cd Other: fix make run-qt target for Darwin. 2022-11-09 17:07:42 +01:00
9eb4703d7a Other: Bridge Osney v2.4.5 2022-11-03 10:09:28 +01:00
105752fc65 GODT-2015: bridge-gui logs to file until gRPC connection is established. 2022-11-02 18:44:44 +01:00
2747e93316 Other: Apply bridge style to community commit. 2022-11-02 16:44:08 +01:00
9548f984eb GODT-2020: fix xdg_{home,cache}_home variables 2022-11-02 16:39:59 +01:00
cb871ce4bc GODT-2016: added more logging of gRPC events at info level. 2022-11-02 15:26:52 +01:00
8ca849b7a8 GODT-2014: bridge quit if gRPC client ends stream. 2022-11-02 14:02:31 +00:00
4bb29b1b5c GODT-2013: CLI flag for frontend is required.
Other: removed --ni flag alias.
2022-11-02 12:03:51 +01:00
e55e893c94 Other: Bump new badssl public key pin
badssl got a new TLS cert last week. We need to bump the pinned key.

This was generated by exporting the TLS cert at rsa4096.badssl.com with
the Chromium browser and running the following program on it:

```
	b, err := os.ReadFile("badssl.pem")
	if err != nil {
		panic(err)
	}

	block, rest := pem.Decode(b)
	if len(rest) > 0 {
		panic("unexpected rest")
	}

	cert, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		panic(err)
	}

	hash := sha256.New()

	if _, err := hash.Write(cert.RawSubjectPublicKeyInfo); err != nil {
		panic(err)
	}

	fmt.Println(base64.StdEncoding.EncodeToString(hash.Sum(nil)))
```
2022-11-02 10:43:49 +01:00
5ab63a290e GODT-1751: fix QML hardcoded links 2022-10-28 15:49:48 +02:00
7c3414b86f Other: Bridge Osney v2.4.4 2022-10-27 17:31:39 +02:00
cec8829032 Other: fix make run-cli for Darwin 2022-10-27 13:13:29 +00:00
78f9f49a8a GODT-1751: switch from protonmail.com to proton.me domain 2022-10-27 13:13:05 +00:00
5a7722fd18 GODT-1645: ignore CVE gobinsec false positive 2022-10-26 06:31:40 +00:00
d111a979f7 GODT-1645: add launcher to gobinsec check 2022-10-26 06:31:40 +00:00
31514c8e31 GODT-1645: add a make target for gobinsec check 2022-10-26 06:31:40 +00:00
af5ce101ef GODT-1645: fix rebase changes 2022-10-26 06:31:40 +00:00
075da27d13 GODT-1645: install upd 1.18 for godog 2022-10-26 06:31:40 +00:00
7b19fb44a4 GODT-1645: WIP Temporary skip scenario 2022-10-26 06:31:40 +00:00
c991946ea7 GODT-1833: Gobinsec wait, nvd api key from CI env var. 2022-10-26 06:31:40 +00:00
f960a3ae38 GODT-1645: Split scenarios for live testing. 2022-10-26 06:31:40 +00:00
73f8811a4b GODT-1645: Changing timeouts to not send too many login attempts. 2022-10-26 06:31:40 +00:00
bc6ec2579a GODT-1938: Account details box values wrap. 2022-10-25 10:48:09 +02:00
35bc7263da GODT-1519: move back to account view after sending bug report. 2022-10-24 09:06:12 +00:00
cc3db00a06 Other: also install vcpkg ARM64 on Intel mac hosts. 2022-10-24 07:54:33 +02:00
7f7961ae0c Other: fix minor typo 2022-10-24 07:37:15 +02:00
aae60b2ef8 GODT-1939: removed vertical overshoot when scrolling. 2022-10-21 18:17:13 +02:00
ab700543b9 GODT-1479: fix 'Open Bridge' button still hovered when status windows opens. 2022-10-20 21:08:19 +02:00
413488f5f4 Other: fix QML error with Qt 6.4 and a typo. 2022-10-20 14:55:17 +02:00
68 changed files with 751 additions and 221 deletions

View File

@ -2,10 +2,62 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/) Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 2.4.8] Osney
### Fixed
* GODT-2071: Fix --no-window flag that was broken on Windows.
## [Bridge 2.4.7] Osney
### Fixed
* GODT-2078: Launcher inception.
* GODT-2039: fix --parent-pid flag is removed from command-line when restarting the application.
## [Bridge 2.4.6] Osney
### Changed
* GODT-2019: When signing out and a single user is connecte* we do not go back to the welcome screen.
* GODT-2071: Bridge-gui report error if an orphan bridge is detected.
* GODT-2046: Bridge-gui log is included in optional archive sent with bug reports.
* GODT-2039: Bridge monitors bridge-gui via its PID.
* GODT-2038: Interrupt gRPC initialisation of bridge process terminates.
* Other: Added timestamp to bridge-gui logs.
* GODT-2035: Bridge-gui log includes Qt version info.
* GODT-2031: Updated bridge description.
### Fixed
* Other: Fix make run-qt target for Darwin.
## [Bridge 2.4.5] Osney
### Changed
* GODT-2015: Bridge-gui logs to file until gRPC connection is established.
* GODT-2016: Added more logging of gRPC events at info level.
* GODT-2013: CLI flag for frontend is required.
### Fixed
* GODT-2020: Fix xdg_{home,cache}_home variables.
* GODT-2014: Bridge quit if gRPC client ends stream.
## [Bridge 2.4.4] Osney
### Changed
* GODT-1751: Switch from protonmail.com to proton.me domain.
### Fixed
* Other: Fix make run-cli for Darwin.
* GODT-1645: Fix CI pipeline.
* GODT-1938: Account details box values wrap.
* Other: Also install vcpkg ARM64 on Intel mac hosts.
* Other: Fix minor typo.
* GODT-1939: removed vertical overshoot when scrolling.
* GODT-1479: fix 'Open Bridge' button still hovered when status windows opens for Windows.
* GODT-1519: Move back to account view after sending bug report.
* Other: fix QML error with Qt 6.4 and a typo.
## [Bridge 2.4.3] Osney ## [Bridge 2.4.3] Osney
## Changed ### Changed
* Other: implemented tokens in bridge-gui-tester. * Other: implemented tokens in bridge-gui-tester.
* GODT-1853: * GODT-1853:
* Upgrade dependencies (including x/crypto). * Upgrade dependencies (including x/crypto).
@ -28,7 +80,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Fixed ### Fixed
* GUI issues: * GUI issues:
* GODT-1894: Fixed typo in alreadyLoggedIn event error message. * GODT-1894: Fixed typo in alreadyLoggedIn event error message.
* GODT-1479: Fix hover on “Open Bridge” in status window on macOS. * GODT-1479: Fix hover on “Open Bridge” in status window on macOS.

View File

@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
.PHONY: build build-gui build-nogui build-launcher versioner hasher .PHONY: build build-gui build-nogui build-launcher versioner hasher
# Keep version hardcoded so app build works also without Git repository. # Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=2.4.3+git BRIDGE_APP_VERSION?=2.4.8+git
APP_VERSION:=${BRIDGE_APP_VERSION} APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG APP_VENDOR:=Proton AG
@ -80,6 +80,9 @@ build: build-gui
build-gui: ${TGZ_TARGET} build-gui: ${TGZ_TARGET}
build-nogui: ${EXE_NAME} build-launcher build-nogui: ${EXE_NAME} build-launcher
ifeq "${TARGET_OS}" "darwin"
mv ${BRIDGE_EXE} ${BRIDGE_EXE_NAME}
endif
go-build=go build $(1) -o $(2) $(3) go-build=go build $(1) -o $(2) $(3)
go-build-finalize=${go-build} go-build-finalize=${go-build}
@ -254,6 +257,13 @@ lint-golang:
$(info linting with GOMAXPROCS=${GOMAXPROCS}) $(info linting with GOMAXPROCS=${GOMAXPROCS})
golangci-lint run ./... golangci-lint run ./...
gobinsec: gobinsec-cache.yml build
gobinsec -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}
gobinsec-cache.yml:
./utils/gobinsec_update.sh
cp ./utils/gobinsec_update/gobinsec-cache-valid.yml ./gobinsec-cache.yml
updates: install-go-mod-outdated updates: install-go-mod-outdated
# Uncomment the "-ci" to fail the job if something can be updated. # Uncomment the "-ci" to fail the job if something can be updated.
go list -u -m -json all | go-mod-outdated -update -direct #-ci go list -u -m -json all | go-mod-outdated -update -direct #-ci
@ -286,7 +296,11 @@ run: run-qt
run-cli: run-nogui run-cli: run-nogui
run-qt: build-gui run-qt: build-gui
ifeq "${TARGET_OS}" "darwin"
PROTONMAIL_ENV=dev ${DARWINAPP_CONTENTS}/MacOS/${LAUNCHER_EXE} ${RUN_FLAGS}
else
PROTONMAIL_ENV=dev ./${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE} ${RUN_FLAGS} PROTONMAIL_ENV=dev ./${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE} ${RUN_FLAGS}
endif
run-nogui: build-nogui clean-vendor gofiles run-nogui: build-nogui clean-vendor gofiles
PROTONMAIL_ENV=dev ./${LAUNCHER_EXE} ${RUN_FLAGS} -c PROTONMAIL_ENV=dev ./${LAUNCHER_EXE} ${RUN_FLAGS} -c
@ -313,6 +327,7 @@ clean: clean-vendor clean-gui clean-vcpkg
rm -f ./*.syso rm -f ./*.syso
rm -f release-notes/bridge.html rm -f release-notes/bridge.html
rm -f release-notes/import-export.html rm -f release-notes/import-export.html
rm -f ${LAUNCHER_EXE} ${BRIDGE_EXE} ${BRIDGE_EXE_NAME}
.PHONY: generate .PHONY: generate

View File

@ -22,7 +22,7 @@ to start Bridge on startup is enabled by default.
When the main window is closed, Bridge will continue to run in the When the main window is closed, Bridge will continue to run in the
background. background.
More details [on the public website](https://protonmail.com/bridge). More details [on the public website](https://proton.me/mail/bridge).
## Launchers ## Launchers
Launchers are binaries used to run the Proton Mail Bridge or Import-Export apps. Launchers are binaries used to run the Proton Mail Bridge or Import-Export apps.

View File

@ -55,6 +55,8 @@ const (
) )
func main() { //nolint:funlen func main() { //nolint:funlen
logrus.SetLevel(logrus.DebugLevel)
l := logrus.WithField("launcher_version", constants.Version)
reporter := sentry.NewReporter(appName, constants.Version, useragent.New()) reporter := sentry.NewReporter(appName, constants.Version, useragent.New())
crashHandler := crash.NewHandler(reporter.ReportException) crashHandler := crash.NewHandler(reporter.ReportException)
@ -62,58 +64,69 @@ func main() { //nolint:funlen
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName)) locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
if err != nil { if err != nil {
logrus.WithError(err).Fatal("Failed to get locations provider") l.WithError(err).Fatal("Failed to get locations provider")
} }
locations := locations.New(locationsProvider, configName) locations := locations.New(locationsProvider, configName)
logsPath, err := locations.ProvideLogsPath() logsPath, err := locations.ProvideLogsPath()
if err != nil { if err != nil {
logrus.WithError(err).Fatal("Failed to get logs path") l.WithError(err).Fatal("Failed to get logs path")
} }
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath)) crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
if err := logging.Init(logsPath); err != nil { if err := logging.Init(logsPath); err != nil {
logrus.WithError(err).Fatal("Failed to setup logging") l.WithError(err).Fatal("Failed to setup logging")
} }
logging.SetLevel(os.Getenv("VERBOSITY")) logging.SetLevel(os.Getenv("VERBOSITY"))
updatesPath, err := locations.ProvideUpdatesPath() updatesPath, err := locations.ProvideUpdatesPath()
if err != nil { if err != nil {
logrus.WithError(err).Fatal("Failed to get updates path") l.WithError(err).Fatal("Failed to get updates path")
} }
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey) key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
if err != nil { if err != nil {
logrus.WithError(err).Fatal("Failed to create new verification key") l.WithError(err).Fatal("Failed to create new verification key")
} }
kr, err := crypto.NewKeyRing(key) kr, err := crypto.NewKeyRing(key)
if err != nil { if err != nil {
logrus.WithError(err).Fatal("Failed to create new verification keyring") l.WithError(err).Fatal("Failed to create new verification keyring")
} }
versioner := versioner.New(updatesPath) versioner := versioner.New(updatesPath)
exeToLaunch := guiName
args := os.Args[1:]
if inCLIMode(args) {
exeToLaunch = exeName
}
exe, err := getPathToUpdatedExecutable(exeToLaunch, versioner, kr, reporter)
if err != nil {
if exe, err = getFallbackExecutable(exeToLaunch, versioner); err != nil {
logrus.WithError(err).Fatal("Failed to find any launchable executable")
}
}
launcher, err := os.Executable() launcher, err := os.Executable()
if err != nil { if err != nil {
logrus.WithError(err).Fatal("Failed to determine path to launcher") logrus.WithError(err).Fatal("Failed to determine path to launcher")
} }
l = l.WithField("launcher_path", launcher)
args := os.Args[1:]
exe, err := getPathToUpdatedExecutable(filepath.Base(launcher), versioner, kr, reporter)
if err != nil {
exeToLaunch := guiName
if inCLIMode(args) {
exeToLaunch = exeName
}
l = l.WithField("exe_to_launch", exeToLaunch)
l.WithError(err).Info("No more updates found, looking up bridge executable")
path, err := versioner.GetExecutableInDirectory(exeToLaunch, filepath.Dir(launcher))
if err != nil {
l.WithError(err).Fatal("No executable in launcher directory")
}
exe = path
}
l = l.WithField("exe_path", exe)
args, wait, mainExe := findAndStripWait(args) args, wait, mainExe := findAndStripWait(args)
if wait { if wait {
waitForProcessToFinish(mainExe) waitForProcessToFinish(mainExe)
@ -134,7 +147,7 @@ func main() { //nolint:funlen
} }
if err != nil { if err != nil {
logrus.WithError(err).Fatal("Failed to launch") l.WithError(err).Fatal("Failed to launch")
} }
} }
@ -193,11 +206,11 @@ func findAndStripWait(args []string) ([]string, bool, string) {
func getPathToUpdatedExecutable( func getPathToUpdatedExecutable(
name string, name string,
versioner *versioner.Versioner, ver *versioner.Versioner,
kr *crypto.KeyRing, kr *crypto.KeyRing,
reporter *sentry.Reporter, reporter *sentry.Reporter,
) (string, error) { ) (string, error) {
versions, err := versioner.ListVersions() versions, err := ver.ListVersions()
if err != nil { if err != nil {
return "", errors.Wrap(err, "failed to list available versions") return "", errors.Wrap(err, "failed to list available versions")
} }
@ -208,7 +221,11 @@ func getPathToUpdatedExecutable(
} }
for _, version := range versions { for _, version := range versions {
vlog := logrus.WithField("version", version) vlog := logrus.WithFields(logrus.Fields{
"version": constants.Version,
"check_version": version,
"name": name,
})
if err := version.VerifyFiles(kr); err != nil { if err := version.VerifyFiles(kr); err != nil {
vlog.WithError(err).Error("Files failed verification and will be removed") vlog.WithError(err).Error("Files failed verification and will be removed")
@ -241,17 +258,6 @@ func getPathToUpdatedExecutable(
return "", errors.New("no available newer versions") return "", errors.New("no available newer 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))
}
// waitForProcessToFinish waits until the process with the given path is finished. // waitForProcessToFinish waits until the process with the given path is finished.
func waitForProcessToFinish(exePath string) { func waitForProcessToFinish(exePath string) {
for { for {

2
dist/info.rc vendored
View File

@ -3,7 +3,7 @@
IDI_ICON1 ICON DISCARDABLE STRINGIZE(ICO_FILE) IDI_ICON1 ICON DISCARDABLE STRINGIZE(ICO_FILE)
#define FILE_COMMENTS "The Bridge is an application that runs on your computer in the background and seamlessly encrypts and decrypts your mail as it enters and leaves your computer." #define FILE_COMMENTS "Proton Mail Bridge is a desktop application that runs in the background, encrypting and decrypting messages as they enter and leave your computer."
#define FILE_DESCRIPTION "Proton Mail Bridge" #define FILE_DESCRIPTION "Proton Mail Bridge"
#define INTERNAL_NAME STRINGIZE(EXE_NAME) #define INTERNAL_NAME STRINGIZE(EXE_NAME)
#define PRODUCT_NAME "Proton Mail Bridge for Windows" #define PRODUCT_NAME "Proton Mail Bridge for Windows"

View File

@ -3,7 +3,7 @@ Type=Application
Version=1.1 Version=1.1
Name=Proton Mail Bridge Name=Proton Mail Bridge
GenericName=Proton Mail Bridge for Linux GenericName=Proton Mail Bridge for Linux
Comment=The Bridge is an application that runs on your computer in the background and seamlessly encrypts and decrypts your mail as it enters and leaves your computer. Comment=Proton Mail Bridge is a desktop application that runs in the background, encrypting and decrypting messages as they enter and leave your computer.
Icon=protonmail-bridge Icon=protonmail-bridge
Exec=protonmail-bridge Exec=protonmail-bridge
Terminal=false Terminal=false

8
go.mod
View File

@ -50,9 +50,9 @@ require (
github.com/vmihailenco/msgpack/v5 v5.3.5 github.com/vmihailenco/msgpack/v5 v5.3.5
go.etcd.io/bbolt v1.3.6 go.etcd.io/bbolt v1.3.6
golang.org/x/exp v0.0.0-20220921164117-439092de6870 golang.org/x/exp v0.0.0-20220921164117-439092de6870
golang.org/x/net v0.0.0-20220921203646-d300de134e69 golang.org/x/net v0.1.0
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 golang.org/x/sys v0.1.0
golang.org/x/text v0.3.7 golang.org/x/text v0.4.0
google.golang.org/grpc v1.49.0 google.golang.org/grpc v1.49.0
google.golang.org/protobuf v1.28.1 google.golang.org/protobuf v1.28.1
howett.net/plist v1.0.0 howett.net/plist v1.0.0
@ -93,7 +93,7 @@ require (
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 // indirect golang.org/x/crypto v0.1.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/tools v0.1.12 // indirect golang.org/x/tools v0.1.12 // indirect
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 // indirect google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 // indirect

16
go.sum
View File

@ -375,8 +375,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 h1:a5Yg6ylndHHYJqIPrdq0AhvR6KTvDTAvgBtaidhEevY= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -424,8 +424,8 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220921203646-d300de134e69 h1:hUJpGDpnfwdJW8iNypFjmSY0sCBEL+spFTZ2eO+Sfps= golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
golang.org/x/net v0.0.0-20220921203646-d300de134e69/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -465,8 +465,8 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -474,8 +474,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@ -69,12 +69,14 @@ const (
flagMemProfileShort = "m" flagMemProfileShort = "m"
flagLogLevel = "log-level" flagLogLevel = "log-level"
flagLogLevelShort = "l" flagLogLevelShort = "l"
// FlagCLI indicate to start with command line interface. FlagGRPC = "grpc" // FlagGRPC starts the gRPC frontend
FlagCLI = "cli" FlagGRPCShort = "g"
flagCLIShort = "c" FlagCLI = "cli" // FlagCLI indicate to start with command line interface.
flagRestart = "restart" flagCLIShort = "c"
FlagLauncher = "launcher" flagRestart = "restart"
FlagNoWindow = "no-window" FlagLauncher = "launcher"
FlagNoWindow = "no-window"
FlagParentPID = "parent-pid"
) )
type Base struct { type Base struct {
@ -299,6 +301,11 @@ func (b *Base) NewApp(mainLoop func(*Base, *cli.Context) error) *cli.App {
Aliases: []string{flagLogLevelShort}, Aliases: []string{flagLogLevelShort},
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)", Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
}, },
&cli.BoolFlag{
Name: FlagGRPC,
Aliases: []string{FlagGRPCShort},
Usage: "Start the gRPC service",
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: FlagCLI, Name: FlagCLI,
Aliases: []string{flagCLIShort}, Aliases: []string{flagCLIShort},
@ -318,6 +325,12 @@ func (b *Base) NewApp(mainLoop func(*Base, *cli.Context) error) *cli.App {
Usage: "The launcher to use to restart the application", Usage: "The launcher to use to restart the application",
Hidden: true, Hidden: true,
}, },
&cli.IntFlag{
Name: FlagParentPID,
Usage: "The PID of the process that started the application. Ignored if frontend is not gRPC",
Hidden: true,
Value: -1,
},
} }
return app return app

View File

@ -20,6 +20,7 @@ package base
import ( import (
"os" "os"
"strconv" "strconv"
"strings"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/sys/execabs" "golang.org/x/sys/execabs"
@ -38,6 +39,8 @@ func (b *Base) restartApp(crash bool) error {
args = os.Args[1:] args = os.Args[1:]
} }
args = removeFlagWithValue(args, FlagParentPID)
if b.launcher != "" { if b.launcher != "" {
args = forceLauncherFlag(args, b.launcher) args = forceLauncherFlag(args, b.launcher)
} }
@ -85,6 +88,30 @@ func incrementRestartFlag(args []string) []string {
return res return res
} }
// removeFlagWithValue removes a flag that requires a value from a list of command line parameters.
// The flag can be of the following form `-flag value`, `--flag value`, `-flag=value` or `--flags=value`.
func removeFlagWithValue(argList []string, flag string) []string {
var result []string
for i := 0; i < len(argList); i++ {
arg := argList[i]
// "detect the parameter form "-flag value" or "--paramName value"
if (arg == "-"+flag) || (arg == "--"+flag) {
i++
continue
}
// "detect the form "--flag=value" or "--flag=value"
if strings.HasPrefix(arg, "-"+flag+"=") || (strings.HasPrefix(arg, "--"+flag+"=")) {
continue
}
result = append(result, arg)
}
return result
}
// forceLauncherFlag replace or add the launcher args with the one set in the app. // forceLauncherFlag replace or add the launcher args with the one set in the app.
func forceLauncherFlag(args []string, launcher string) []string { func forceLauncherFlag(args []string, launcher string) []string {
res := append([]string{}, args...) res := append([]string{}, args...)

View File

@ -61,3 +61,22 @@ func TestVersionLessThan(t *testing.T) {
r.False(current.LessThan(current)) r.False(current.LessThan(current))
r.False(newer.LessThan(current)) r.False(newer.LessThan(current))
} }
func TestRemoveFlagWithValue(t *testing.T) {
tests := []struct {
argList []string
flag string
expected []string
}{
{[]string{}, "b", nil},
{[]string{"-a", "-b=value", "-c"}, "b", []string{"-a", "-c"}},
{[]string{"-a", "--b=value", "-c"}, "b", []string{"-a", "-c"}},
{[]string{"-a", "-b", "value", "-c"}, "b", []string{"-a", "-c"}},
{[]string{"-a", "--b", "value", "-c"}, "b", []string{"-a", "-c"}},
{[]string{"-a", "-B=value", "-c"}, "b", []string{"-a", "-B=value", "-c"}},
}
for _, tt := range tests {
require.Equal(t, removeFlagWithValue(tt.argList, tt.flag), tt.expected)
}
}

View File

@ -40,10 +40,10 @@ import (
) )
const ( const (
flagLogIMAP = "log-imap" flagLogIMAP = "log-imap"
flagLogSMTP = "log-smtp" flagLogSMTP = "log-smtp"
flagNonInteractive = "noninteractive" flagNonInteractive = "noninteractive"
flagNonInteractiveShort = "n"
// Memory cache was estimated by empirical usage in the past, and it was set to 100MB. // Memory cache was estimated by empirical usage in the past, and it was set to 100MB.
// NOTE: This value must not be less than maximal size of one email (~30MB). // NOTE: This value must not be less than maximal size of one email (~30MB).
inMemoryCacheLimit = 100 * (1 << 20) inMemoryCacheLimit = 100 * (1 << 20)
@ -62,8 +62,9 @@ func New(base *base.Base) *cli.App {
Usage: "Enable logging of SMTP communications (may contain decrypted data!)", Usage: "Enable logging of SMTP communications (may contain decrypted data!)",
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: flagNonInteractive, Name: flagNonInteractive,
Usage: "Start Bridge entirely non-interactively", Aliases: []string{flagNonInteractiveShort},
Usage: "Start Bridge entirely non-interactively",
}, },
}...) }...)
@ -72,6 +73,11 @@ func New(base *base.Base) *cli.App {
func main(b *base.Base, c *cli.Context) error { //nolint:funlen func main(b *base.Base, c *cli.Context) error { //nolint:funlen
frontendType := getFrontendTypeFromCLIParams(c) frontendType := getFrontendTypeFromCLIParams(c)
if frontendType == frontend.Unknown {
_ = cli.ShowAppHelp(c)
return errors.New("no frontend was specified. Use --grpc, --cli or --noninteractive")
}
f := frontend.New( f := frontend.New(
frontendType, frontendType,
!c.Bool(base.FlagNoWindow), !c.Bool(base.FlagNoWindow),
@ -80,6 +86,7 @@ func main(b *base.Base, c *cli.Context) error { //nolint:funlen
b.Updater, b.Updater,
b, b,
b.Locations, b.Locations,
c.Int(base.FlagParentPID),
) )
cache, cacheErr := loadMessageCache(b) cache, cacheErr := loadMessageCache(b)
@ -171,12 +178,14 @@ func main(b *base.Base, c *cli.Context) error { //nolint:funlen
func getFrontendTypeFromCLIParams(c *cli.Context) frontend.Type { func getFrontendTypeFromCLIParams(c *cli.Context) frontend.Type {
switch { switch {
case c.Bool(base.FlagGRPC):
return frontend.GRPC
case c.Bool(base.FlagCLI): case c.Bool(base.FlagCLI):
return frontend.CLI return frontend.CLI
case c.Bool(flagNonInteractive): case c.Bool(flagNonInteractive):
return frontend.NonInteractive return frontend.NonInteractive
default: default:
return frontend.GRPC return frontend.Unknown
} }
} }

View File

@ -39,7 +39,7 @@ const (
var ErrSizeTooLarge = errors.New("file is too big") var ErrSizeTooLarge = errors.New("file is too big")
// ReportBug reports a new bug from the user. // ReportBug reports a new bug from the user.
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string, attachLogs bool) error { func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string, attachLogs bool) error { //nolint:funlen
if user, err := b.GetUser(address); err == nil { if user, err := b.GetUser(address); err == nil {
accountName = user.Username() accountName = user.Username()
} else if users := b.GetUsers(); len(users) > 0 { } else if users := b.GetUsers(); len(users) > 0 {
@ -65,6 +65,16 @@ func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address,
if err != nil { if err != nil {
log.WithError(err).Error("Can't get log files list") log.WithError(err).Error("Can't get log files list")
} }
guiLogs, err := b.getMatchingLogs(
func(filename string) bool {
return logging.MatchGUILogName(filename) && !logging.MatchStackTraceName(filename)
},
)
if err != nil {
log.WithError(err).Error("Can't get GUI log files list")
}
crashes, err := b.getMatchingLogs( crashes, err := b.getMatchingLogs(
func(filename string) bool { func(filename string) bool {
return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename) return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename)
@ -78,6 +88,10 @@ func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address,
matchFiles = append(matchFiles, logs[max(0, len(logs)-(MaxCompressedFilesCount/2)):]...) matchFiles = append(matchFiles, logs[max(0, len(logs)-(MaxCompressedFilesCount/2)):]...)
matchFiles = append(matchFiles, crashes[max(0, len(crashes)-(MaxCompressedFilesCount/2)):]...) matchFiles = append(matchFiles, crashes[max(0, len(crashes)-(MaxCompressedFilesCount/2)):]...)
if len(guiLogs) > 0 {
// bridge-gui is keeping only one log file and it's small (~ 1kb), so we include it regardless of file count
matchFiles = append(matchFiles, guiLogs[len(guiLogs)-1])
}
archive, err := zipFiles(matchFiles) archive, err := zipFiles(matchFiles)
if err != nil { if err != nil {

View File

@ -43,11 +43,13 @@ void QMLBackend::init(GRPCConfig const &serviceConfig)
{ {
users_ = new UserList(this); users_ = new UserList(this);
app().grpc().setLog(&app().log()); Log& log = app().log();
log.info(QString("Connecting to gRPC service"));
app().grpc().setLog(&log);
this->connectGrpcEvents(); this->connectGrpcEvents();
QString error; QString error;
if (app().grpc().connectToServer(serviceConfig, error)) if (app().grpc().connectToServer(serviceConfig, app().bridgeMonitor(), error))
app().log().info("Connected to backend via gRPC service."); app().log().info("Connected to backend via gRPC service.");
else else
throw Exception(QString("Cannot connectToServer to go backend via gRPC: %1").arg(error)); throw Exception(QString("Cannot connectToServer to go backend via gRPC: %1").arg(error));

View File

@ -8,7 +8,7 @@ BEGIN
BEGIN BEGIN
BLOCK "040904b0" BLOCK "040904b0"
BEGIN BEGIN
VALUE "Comments", "The Bridge is an application that runs on your computer in the background and seamlessly encrypts and decrypts your mail as it enters and leaves your computer." VALUE "Comments", "Proton Mail Bridge is a desktop application that runs in the background, encrypting and decrypting messages as they enter and leave your computer."
VALUE "CompanyName", "${BRIDGE_VENDOR}" VALUE "CompanyName", "${BRIDGE_VENDOR}"
VALUE "FileDescription", "${BRIDGE_APP_FULL_NAME}" VALUE "FileDescription", "${BRIDGE_APP_FULL_NAME}"
VALUE "FileVersion", "${BRIDGE_APP_VERSION_COMMA}" VALUE "FileVersion", "${BRIDGE_APP_VERSION_COMMA}"

View File

@ -54,7 +54,6 @@ BRIDGE_APP_FULL_NAME=${BRIDGE_APP_FULL_NAME:-"Proton Mail Bridge"}
BRIDGE_VENDOR=${BRIDGE_VENDOR:-"Proton AG"} BRIDGE_VENDOR=${BRIDGE_VENDOR:-"Proton AG"}
BUILD_CONFIG=${BRIDGE_GUI_BUILD_CONFIG:-Debug} BUILD_CONFIG=${BRIDGE_GUI_BUILD_CONFIG:-Debug}
BUILD_DIR=$(echo "./cmake-build-${BUILD_CONFIG}" | tr '[:upper:]' '[:lower:]') BUILD_DIR=$(echo "./cmake-build-${BUILD_CONFIG}" | tr '[:upper:]' '[:lower:]')
VCPKG_OSX_DEPLOYMENT_TARGET=11.0
VCPKG_ROOT="${BRIDGE_REPO_ROOT}/extern/vcpkg" VCPKG_ROOT="${BRIDGE_REPO_ROOT}/extern/vcpkg"
git submodule update --init --recursive ${VCPKG_ROOT} git submodule update --init --recursive ${VCPKG_ROOT}
@ -70,10 +69,8 @@ ${VCPKG_BOOTSTRAP} -disableMetrics
check_exit "Failed to bootstrap vcpkg." check_exit "Failed to bootstrap vcpkg."
if [[ "$OSTYPE" == "darwin"* ]]; then if [[ "$OSTYPE" == "darwin"* ]]; then
if [[ "$(uname -m)" == "arm64" ]]; then ${VCPKG_EXE} install grpc:arm64-osx-min-11-0 --overlay-triplets=vcpkg/triplets --clean-after-build
${VCPKG_EXE} install grpc:arm64-osx-min-11-0 --overlay-triplets=vcpkg/triplets --clean-after-build check_exit "Failed installing gRPC for macOS / Apple Silicon"
check_exit "Failed installing gRPC for macOS / Apple Silicon"
fi
${VCPKG_EXE} install grpc:x64-osx-min-11-0 --overlay-triplets=vcpkg/triplets --clean-after-build ${VCPKG_EXE} install grpc:x64-osx-min-11-0 --overlay-triplets=vcpkg/triplets --clean-after-build
check_exit "Failed installing gRPC for macOS / Intel x64" check_exit "Failed installing gRPC for macOS / Intel x64"
elif [[ "$OSTYPE" == "linux"* ]]; then elif [[ "$OSTYPE" == "linux"* ]]; then

View File

@ -32,19 +32,33 @@ using namespace bridgepp;
namespace namespace
{ {
/// \brief The file extension for the bridge executable file. /// \brief The file extension for the bridge executable file.
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
QString const exeSuffix = ".exe"; QString const exeSuffix = ".exe";
#else #else
QString const exeSuffix; QString const exeSuffix;
#endif #endif
QString const bridgeLock = "bridge-gui.lock"; ///< file name used for the lock file. QString const bridgeLock = "bridge-gui.lock"; ///< file name used for the lock file.
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.* QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
qint64 const grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds. qint64 const grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
//****************************************************************************************************************************************************
/// According to Qt doc, one per application is OK, but its use should be restricted to a
/// single thread.
/// \return The network access manager for the application.
//****************************************************************************************************************************************************
QNetworkAccessManager& networkManager()
{
static QNetworkAccessManager nam;
return nam;
} }
} // anonymous namespace
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return The path of the bridge executable. /// \return The path of the bridge executable.
/// \return A null string if the executable could not be located. /// \return A null string if the executable could not be located.
@ -80,6 +94,26 @@ Log &initLog()
{ {
Log &log = app().log(); Log &log = app().log();
log.registerAsQtMessageHandler(); log.registerAsQtMessageHandler();
log.setEchoInConsole(true);
// remove old gui log files
QDir const logsDir(userLogsDir());
for (QFileInfo const fileInfo: logsDir.entryInfoList({ "gui_v*.log" }, QDir::Filter::Files)) // entryInfolist apparently only support wildcards, not regex.
QFile(fileInfo.absoluteFilePath()).remove();
// create new GUI log file
QString error;
if (!log.startWritingToFile(logsDir.absoluteFilePath(QString("gui_v%1_%2.log").arg(PROJECT_VER).arg(QDateTime::currentSecsSinceEpoch())), &error))
log.error(error);
log.info("bridge-gui starting");
QString const qtCompileTimeVersion = QT_VERSION_STR;
QString const qtRuntimeVersion = qVersion();
QString msg = QString("Using Qt %1").arg(qtRuntimeVersion);
if (qtRuntimeVersion != qtCompileTimeVersion)
msg += QString(" (compiled against %1)").arg(qtCompileTimeVersion);
log.info(msg);
return log; return log;
} }
@ -166,21 +200,44 @@ QUrl getApiUrl()
} }
//****************************************************************************************************************************************************
/// \return The URL for the focus endpoint of the bridge API URL.
//****************************************************************************************************************************************************
QUrl getFocusUrl()
{
QUrl url = getApiUrl();
url.setPath("/focus");
return url;
}
//****************************************************************************************************************************************************
/// \return true if an instance of bridge is already running.
//****************************************************************************************************************************************************
bool isBridgeRunning()
{
QTimer timer;
timer.setSingleShot(true);
std::unique_ptr<QNetworkReply> reply(networkManager().get(QNetworkRequest(getFocusUrl())));
QEventLoop loop;
bool timedOut = false;
QObject::connect(&timer, &QTimer::timeout, [&]() { timedOut = true; loop.quit(); });
QObject::connect(reply.get(), &QNetworkReply::finished, &loop, &QEventLoop::quit);
timer.start(1000); // we time out after 1 second and consider no other instance is running.
loop.exec();
return ((!timedOut) && (reply->error() == QNetworkReply::NetworkError::NoError));
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \brief Use api to bring focus on existing bridge instance. /// \brief Use api to bring focus on existing bridge instance.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
void focusOtherInstance() void focusOtherInstance()
{ {
QNetworkAccessManager *manager; std::unique_ptr<QNetworkReply> reply(networkManager().get(QNetworkRequest(getFocusUrl())));
QNetworkRequest request;
manager = new QNetworkAccessManager();
QUrl url = getApiUrl();
url.setPath("/focus");
request.setUrl(url);
QNetworkReply* rep = manager->get(request);
QEventLoop loop; QEventLoop loop;
QObject::connect(rep, &QNetworkReply::finished, &loop, &QEventLoop::quit); QObject::connect(reply.get(), &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec(); loop.exec();
} }
@ -200,7 +257,10 @@ void launchBridge(QStringList const &args)
else else
app().log().debug(QString("Bridge executable path: %1").arg(QDir::toNativeSeparators(bridgeExePath))); app().log().debug(QString("Bridge executable path: %1").arg(QDir::toNativeSeparators(bridgeExePath)));
overseer = std::make_unique<Overseer>(new ProcessMonitor(bridgeExePath, args, nullptr), nullptr); qint64 const pid = qApp->applicationPid();
QStringList const params = QStringList { "--grpc", "--parent-pid", QString::number(pid) } + args ;
app().log().info(QString("Launching bridge process with command \"%1\" %2").arg(bridgeExePath, params.join(" ")));
overseer = std::make_unique<Overseer>(new ProcessMonitor(bridgeExePath, params , nullptr), nullptr);
overseer->startWorker(true); overseer->startWorker(true);
} }
@ -247,8 +307,8 @@ int main(int argc, char *argv[])
{ {
focusOtherInstance(); focusOtherInstance();
return EXIT_FAILURE; return EXIT_FAILURE;
} }
QStringList args; QStringList args;
QString launcher; QString launcher;
bool attach = false; bool attach = false;
@ -258,22 +318,29 @@ int main(int argc, char *argv[])
// In attached mode, we do not intercept stderr and stdout of bridge, as we did not launch it ourselves, so we output the log to the console. // In attached mode, we do not intercept stderr and stdout of bridge, as we did not launch it ourselves, so we output the log to the console.
// When not in attached mode, log entries are forwarded to bridge, which output it on stdout/stderr. bridge-gui's process monitor intercept // When not in attached mode, log entries are forwarded to bridge, which output it on stdout/stderr. bridge-gui's process monitor intercept
// these outputs and output them on the command-line. // these outputs and output them on the command-line.
log.setEchoInConsole(attach);
log.setLevel(logLevel); log.setLevel(logLevel);
if (!attach) if (!attach)
{ {
if (isBridgeRunning())
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.");
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one. // before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
GRPCClient::removeServiceConfigFile(); GRPCClient::removeServiceConfigFile();
launchBridge(args); launchBridge(args);
} }
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath())));
log.debug(QString("Server configuration file will be loaded from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath()))); app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(attach ? 0 : grpcServiceConfigWaitDelayMs, app().bridgeMonitor()));
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(attach ? 0 : grpcServiceConfigWaitDelayMs));
if (!attach) if (!attach)
GRPCClient::removeServiceConfigFile(); GRPCClient::removeServiceConfigFile();
log.debug("Backend was successfully initialized.");
// gRPC communication is established. From now on, log events will be sent to bridge via gRPC. bridge will write these to file,
// and will output then on console if appropriate. If we are not running in attached mode we intercept bridge stdout & stderr and
// display it in our own output and error, so we only continue to log directly to console if we are running in attached mode.
log.setEchoInConsole(attach);
log.info("Backend was successfully initialized.");
log.stopWritingToFile();
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine)); std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
@ -288,7 +355,7 @@ int main(int argc, char *argv[])
if (bridgeMonitor) if (bridgeMonitor)
{ {
const ProcessMonitor::MonitorStatus& status = bridgeMonitor->getStatus(); const ProcessMonitor::MonitorStatus& status = bridgeMonitor->getStatus();
if (!status.running && !attach) if (status.ended && !attach)
{ {
// ProcessMonitor already stopped meaning we are attached to an orphan Bridge. // ProcessMonitor already stopped meaning we are attached to an orphan Bridge.
// Restart the full process to be sure there is no more bridge orphans // Restart the full process to be sure there is no more bridge orphans

View File

@ -43,6 +43,7 @@ Item {
clip: true clip: true
anchors.fill: parent anchors.fill: parent
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds // Disable the springy effect when scroll reaches top/bottom.
Item { Item {
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView) // can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)

View File

@ -852,9 +852,9 @@ Window {
property string version: "2.0.X-BridePreview" property string version: "2.0.X-BridePreview"
property url logsPath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0] property url logsPath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
property url licensePath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0] property url licensePath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
property url releaseNotesLink: Qt.resolvedUrl("https://protonmail.com/download/bridge/early_releases.html") property url releaseNotesLink: Qt.resolvedUrl("https://proton.me/download/bridge/early_releases.html")
property url dependencyLicensesLink: Qt.resolvedUrl("https://github.com/ProtonMail/proton-bridge/v2/blob/master/COPYING_NOTES.md#dependencies") property url dependencyLicensesLink: Qt.resolvedUrl("https://github.com/ProtonMail/proton-bridge/v2/blob/master/COPYING_NOTES.md#dependencies")
property url landingPageLink: Qt.resolvedUrl("https://protonmail.com/bridge") property url landingPageLink: Qt.resolvedUrl("https://proton.me/mail/bridge#download")
property string colorSchemeName: "light" property string colorSchemeName: "light"
function changeColorScheme(newScheme){ function changeColorScheme(newScheme){

View File

@ -28,6 +28,8 @@ SettingsView {
property var selectedAddress property var selectedAddress
signal bugReportWasSent()
Label { Label {
text: qsTr("Report a problem") text: qsTr("Report a problem")
colorScheme: root.colorScheme colorScheme: root.colorScheme
@ -61,7 +63,7 @@ SettingsView {
} }
onTextChanged: { onTextChanged: {
// Rise max length error imidiatly while typing // Rise max length error immediately while typing
if (description.text.length > description._maxLength) { if (description.text.length > description._maxLength) {
validate() validate()
} }
@ -164,6 +166,7 @@ SettingsView {
Connections { Connections {
target: Backend target: Backend
function onReportBugFinished() { sendButton.loading = false } function onReportBugFinished() { sendButton.loading = false }
function onBugReportSendSuccess() { root.bugReportWasSent() }
} }
} }

View File

@ -53,6 +53,8 @@ Item {
selectByMouse: true selectByMouse: true
selectByKeyboard: true selectByKeyboard: true
selectionColor: root.colorScheme.text_weak selectionColor: root.colorScheme.text_weak
wrapMode: Text.WrapAnywhere
Layout.fillWidth: true
} }
} }

View File

@ -359,6 +359,10 @@ Item {
onBack: { onBack: {
rightContent.showHelpView() rightContent.showHelpView()
} }
onBugReportWasSent: {
rightContent.showAccount()
}
} }
function showAccount(index) { function showAccount(index) {

View File

@ -41,7 +41,7 @@ SettingsView {
actionIcon: "/qml/icons/ic-external-link.svg" actionIcon: "/qml/icons/ic-external-link.svg"
description: qsTr("Get help setting up your client with our instructions and FAQs.") description: qsTr("Get help setting up your client with our instructions and FAQs.")
type: SettingsItem.PrimaryButton type: SettingsItem.PrimaryButton
onClicked: {Qt.openUrlExternally("https://protonmail.com/support/categories/bridge/")} onClicked: {Qt.openUrlExternally("https://proton.me/support/mail")}
Layout.fillWidth: true Layout.fillWidth: true
} }

View File

@ -98,7 +98,7 @@ ApplicationWindow {
property bool _showSetup: false property bool _showSetup: false
currentIndex: { currentIndex: {
// show welcome when there are no users or only one non-logged-in user is present // show welcome when there are no users
if (Backend.users.count === 0) { if (Backend.users.count === 0) {
return 1 return 1
} }
@ -112,7 +112,8 @@ ApplicationWindow {
} }
if (Backend.users.count === 1 && u.loggedIn === false) { if (Backend.users.count === 1 && u.loggedIn === false) {
return 1 showSignIn(u.username)
return 0
} }
if (contentLayout._showSetup) { if (contentLayout._showSetup) {

View File

@ -289,7 +289,7 @@ QtObject {
property Notification updateForceError: Notification { property Notification updateForceError: Notification {
title: qsTr("Bridge coudnt update") title: qsTr("Bridge coudnt update")
description: qsTr("You must update manually. Go to: https:/protonmail.com/bridge/download") description: qsTr("You must update manually. Go to: https://proton.me/mail/bridge#download")
brief: title brief: title
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
@ -997,7 +997,7 @@ QtObject {
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
group: Notifications.Group.Dialogs | Notifications.Group.Configuration group: Notifications.Group.Dialogs | Notifications.Group.Configuration
property var supportLink: "https://protonmail.com/support/knowledge-base/macos-keychain-corrupted" property var supportLink: "https://proton.me/support/mail"
Connections { Connections {

View File

@ -15,6 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQml import QtQml
QtObject { QtObject {
@ -22,7 +23,7 @@ QtObject {
property var prominent property var prominent
// Primary // Primary
property color primay_norm property color primary_norm
// Interaction-norm // Interaction-norm
property color interaction_norm property color interaction_norm

View File

@ -30,7 +30,7 @@ QtObject {
// https://doc.qt.io/qt-5/qtqml-documents-definetypes.html#inline-components // https://doc.qt.io/qt-5/qtqml-documents-definetypes.html#inline-components
// component ColorScheme: QtObject { // component ColorScheme: QtObject {
// property color primay_norm // property color primary_norm
// ... // ...
// } // }
@ -40,7 +40,7 @@ QtObject {
prominent: lightProminentStyle prominent: lightProminentStyle
// Primary // Primary
primay_norm: "#6D4AFF" primary_norm: "#6D4AFF"
// Interaction-norm // Interaction-norm
interaction_norm: "#6D4AFF" interaction_norm: "#6D4AFF"
@ -115,7 +115,7 @@ QtObject {
prominent: this prominent: this
// Primary // Primary
primay_norm: "#8A6EFF" primary_norm: "#8A6EFF"
// Interaction-norm // Interaction-norm
interaction_norm: "#6D4AFF" interaction_norm: "#6D4AFF"
@ -190,7 +190,7 @@ QtObject {
prominent: darkProminentStyle prominent: darkProminentStyle
// Primary // Primary
primay_norm: "#8A6EFF" primary_norm: "#8A6EFF"
// Interaction-norm // Interaction-norm
interaction_norm: "#6D4AFF" interaction_norm: "#6D4AFF"
@ -265,7 +265,7 @@ QtObject {
prominent: this prominent: this
// Primary // Primary
primay_norm: "#8A6EFF" primary_norm: "#8A6EFF"
// Interaction-norm // Interaction-norm
interaction_norm: "#6D4AFF" interaction_norm: "#6D4AFF"

View File

@ -44,6 +44,7 @@ Item {
clip: true clip: true
anchors.fill: parent anchors.fill: parent
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds // Disable the springy effect when scroll reaches top/bottom.
Item { Item {
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView) // can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)

View File

@ -42,7 +42,7 @@ Item {
property string name : "Apple Mail" property string name : "Apple Mail"
property string iconSource : "/qml/icons/ic-apple-mail.svg" property string iconSource : "/qml/icons/ic-apple-mail.svg"
property bool haveAutoSetup: true property bool haveAutoSetup: true
property string link: "https://protonmail.com/bridge/applemail" property string link: "https://proton.me/support/protonmail-bridge-clients-apple-mail"
Component.onCompleted : { Component.onCompleted : {
if (Backend.goos == "darwin") { if (Backend.goos == "darwin") {
@ -50,13 +50,13 @@ Item {
"name" : "Apple Mail", "name" : "Apple Mail",
"iconSource" : "/qml/icons/ic-apple-mail.svg", "iconSource" : "/qml/icons/ic-apple-mail.svg",
"haveAutoSetup" : true, "haveAutoSetup" : true,
"link" : "https://protonmail.com/bridge/applemail" "link" : "https://proton.me/support/protonmail-bridge-clients-apple-mail"
}) })
append({ append({
"name" : "Microsoft Outlook", "name" : "Microsoft Outlook",
"iconSource" : "/qml/icons/ic-microsoft-outlook.svg", "iconSource" : "/qml/icons/ic-microsoft-outlook.svg",
"haveAutoSetup" : false, "haveAutoSetup" : false,
"link" : "https://protonmail.com/bridge/outlook2019-mac" "link" : "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019"
}) })
} }
if (Backend.goos == "windows") { if (Backend.goos == "windows") {
@ -64,7 +64,7 @@ Item {
"name" : "Microsoft Outlook", "name" : "Microsoft Outlook",
"iconSource" : "/qml/icons/ic-microsoft-outlook.svg", "iconSource" : "/qml/icons/ic-microsoft-outlook.svg",
"haveAutoSetup" : false, "haveAutoSetup" : false,
"link" : "https://protonmail.com/bridge/outlook2019" "link" : "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2019"
}) })
} }
@ -72,14 +72,14 @@ Item {
"name" : "Mozilla Thunderbird", "name" : "Mozilla Thunderbird",
"iconSource" : "/qml/icons/ic-mozilla-thunderbird.svg", "iconSource" : "/qml/icons/ic-mozilla-thunderbird.svg",
"haveAutoSetup" : false, "haveAutoSetup" : false,
"link" : "https://protonmail.com/bridge/thunderbird" "link" : "https://proton.me/support/protonmail-bridge-clients-windows-thunderbird"
}) })
append({ append({
"name" : "Other", "name" : "Other",
"iconSource" : "/qml/icons/ic-other-mail-clients.svg", "iconSource" : "/qml/icons/ic-other-mail-clients.svg",
"haveAutoSetup" : false, "haveAutoSetup" : false,
"link" : "https://protonmail.com/bridge/clients" "link" : "https://proton.me/support/protonmail-bridge-configure-client"
}) })
} }

View File

@ -288,7 +288,7 @@ FocusScope {
Label { Label {
colorScheme: root.colorScheme colorScheme: root.colorScheme
textFormat: Text.StyledText textFormat: Text.StyledText
text: link("https://protonmail.com/signup", qsTr("Create or upgrade your account")) text: link("https://proton.me/mail/pricing", qsTr("Create or upgrade your account"))
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 24 Layout.topMargin: 24
type: Label.LabelType.Body type: Label.LabelType.Body

View File

@ -43,10 +43,23 @@ Window {
signal showSignIn(string username) signal showSignIn(string username)
signal quit() signal quit()
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
}
function enableHoverOnOpenBridgeButton() {
openBridgeButton.hoverEnabled = true
mouseArea.positionChanged.disconnect(enableHoverOnOpenBridgeButton)
}
onVisibleChanged: { onVisibleChanged: {
if (visible) { // GODT-1479 restore the hover-able status that may have been disabled when clicking on the 'Open Bridge' button. if (visible) { // GODT-1479 To avoid a visual glitch where the 'Open bridge button' would appear hovered when the status windows opens,
openBridgeButton.hoverEnabled = true // we've disabled hover on it when it was last closed. Re-enabling hover here will not work on all platforms. so we temporarily connect
// mouse move event over the window's mouseArea to a function that will re-enable hover on the open bridge button.
openBridgeButton.focus = false openBridgeButton.focus = false
mouseArea.positionChanged.connect(enableHoverOnOpenBridgeButton)
} else { } else {
menu.close() menu.close()
} }

View File

@ -84,11 +84,13 @@ QString userConfigDir()
dir += "/Library/Application Support"; dir += "/Library/Application Support";
#else #else
dir = qgetenv ("XDG_CONFIG_HOME"); dir = qgetenv ("XDG_CONFIG_HOME");
if (dir.isEmpty()) if (dir.isEmpty())
dir = qgetenv ("HOME"); {
dir = qgetenv ("HOME");
if (dir.isEmpty()) if (dir.isEmpty())
throw Exception("neither $XDG_CONFIG_HOME nor $HOME are defined"); throw Exception("neither $XDG_CONFIG_HOME nor $HOME are defined");
dir += "/.config"; dir += "/.config";
}
#endif #endif
QString const folder = QDir(dir).absoluteFilePath(configFolder); QString const folder = QDir(dir).absoluteFilePath(configFolder);
QDir().mkpath(folder); QDir().mkpath(folder);
@ -115,11 +117,13 @@ QString userCacheDir()
dir += "/Library/Caches"; dir += "/Library/Caches";
#else #else
dir = qgetenv ("XDG_CACHE_HOME"); dir = qgetenv ("XDG_CACHE_HOME");
if (dir.isEmpty()) if (dir.isEmpty())
dir = qgetenv ("HOME"); {
dir = qgetenv ("HOME");
if (dir.isEmpty()) if (dir.isEmpty())
throw Exception("neither XDG_CACHE_HOME nor $HOME are defined"); throw Exception("neither XDG_CACHE_HOME nor $HOME are defined");
dir += "/.cache"; dir += "/.cache";
}
#endif #endif
QString const folder = QDir(dir).absoluteFilePath(configFolder); QString const folder = QDir(dir).absoluteFilePath(configFolder);
@ -129,6 +133,17 @@ QString userCacheDir()
} }
//****************************************************************************************************************************************************
/// \return user logs directory used by bridge.
//****************************************************************************************************************************************************
QString userLogsDir()
{
QString const path = QDir(userCacheDir()).absoluteFilePath("logs");
QDir().mkpath(path);
return path;
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return The value GOOS would return for the current platform. /// \return The value GOOS would return for the current platform.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************

View File

@ -29,6 +29,7 @@ namespace bridgepp
QString userConfigDir(); ///< Get the path of the user configuration folder. QString userConfigDir(); ///< Get the path of the user configuration folder.
QString userCacheDir(); ///< Get the path of the user cache folder. QString userCacheDir(); ///< Get the path of the user cache folder.
QString userLogsDir(); ///< Get the path of the user logs folder.
QString goos(); ///< return the value of Go's GOOS for the current platform ("darwin", "linux" and "windows" are supported). QString goos(); ///< return the value of Go's GOOS for the current platform ("darwin", "linux" and "windows" are supported).
qint64 randN(qint64 n); ///< return a random integer in the half open range [0,n) qint64 randN(qint64 n); ///< return a random integer in the half open range [0,n)
QString randomFirstName(); ///< Get a random first name from a pre-determined list. QString randomFirstName(); ///< Get a random first name from a pre-determined list.

View File

@ -19,6 +19,7 @@
#include "GRPCClient.h" #include "GRPCClient.h"
#include "GRPCUtils.h" #include "GRPCUtils.h"
#include "../Exception/Exception.h" #include "../Exception/Exception.h"
#include "../ProcessMonitor.h"
using namespace google::protobuf; using namespace google::protobuf;
@ -56,9 +57,10 @@ void GRPCClient::removeServiceConfigFile()
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] timeoutMs The timeout in milliseconds /// \param[in] timeoutMs The timeout in milliseconds
/// \param[in] serverProcess An optional server process to monitor. If the process it, no need and retry, as connexion cannot be established. Ignored if null.
/// \return The service config. /// \return The service config.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs) GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs, ProcessMonitor *serverProcess)
{ {
QString const path = grpcServerConfigPath(); QString const path = grpcServerConfigPath();
QFile file(path); QFile file(path);
@ -68,6 +70,9 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs)
bool found = false; bool found = false;
while (true) while (true)
{ {
if (serverProcess && serverProcess->getStatus().ended)
throw Exception("Bridge application exited before providing a gRPC service configuration file.");
if (file.exists()) if (file.exists())
{ {
found = true; found = true;
@ -100,9 +105,10 @@ void GRPCClient::setLog(Log *log)
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[out] outError If the function returns false, this variable contains a description of the error. /// \param[out] outError If the function returns false, this variable contains a description of the error.
/// \param[in] serverProcess An optional server process to monitor. If the process it, no need and retry, as connexion cannot be established. Ignored if null.
/// \return true iff the connection was successful. /// \return true iff the connection was successful.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
bool GRPCClient::connectToServer(GRPCConfig const &config, QString &outError) bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serverProcess, QString &outError)
{ {
try try
{ {
@ -123,7 +129,10 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, QString &outError)
int i = 0; int i = 0;
while (true) while (true)
{ {
this->logDebug(QString("Connection to gRPC server at %1. attempt #%2").arg(address).arg(++i)); if (serverProcess && serverProcess->getStatus().ended)
throw Exception("Bridge application ended before gRPC connexion could be established.");
this->logInfo(QString("Connection to gRPC server at %1. attempt #%2").arg(address).arg(++i));
if (channel_->WaitForConnected(gpr_time_add(gpr_now(GPR_CLOCK_REALTIME), gpr_time_from_millis(grpcConnectionRetryDelayMs, GPR_TIMESPAN)))) if (channel_->WaitForConnected(gpr_time_add(gpr_now(GPR_CLOCK_REALTIME), gpr_time_from_millis(grpcConnectionRetryDelayMs, GPR_TIMESPAN))))
break; // connection established. break; // connection established.
@ -135,13 +144,13 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, QString &outError)
if (channel_->GetState(true) != GRPC_CHANNEL_READY) if (channel_->GetState(true) != GRPC_CHANNEL_READY)
throw Exception("connection check failed."); throw Exception("connection check failed.");
this->logDebug("Successfully connected to gRPC server."); this->logInfo("Successfully connected to gRPC server.");
QString const clientToken = QUuid::createUuid().toString(); QString const clientToken = QUuid::createUuid().toString();
QString clientConfigPath = createClientConfigFile(clientToken); QString clientConfigPath = createClientConfigFile(clientToken);
if (clientConfigPath.isEmpty()) if (clientConfigPath.isEmpty())
throw Exception("gRPC client config could not be saved."); throw Exception("gRPC client config could not be saved.");
this->logDebug(QString("Client config file was saved to '%1'").arg(QDir::toNativeSeparators(clientConfigPath))); this->logInfo(QString("Client config file was saved to '%1'").arg(QDir::toNativeSeparators(clientConfigPath)));
QString returnedClientToken; QString returnedClientToken;
grpc::Status status = this->checkTokens(QDir::toNativeSeparators(clientConfigPath), returnedClientToken); grpc::Status status = this->checkTokens(QDir::toNativeSeparators(clientConfigPath), returnedClientToken);
@ -153,7 +162,7 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, QString &outError)
if (!status.ok()) if (!status.ok())
throw Exception(QString::fromStdString(status.error_message())); throw Exception(QString::fromStdString(status.error_message()));
log_->debug("gRPC token was validated"); log_->info("gRPC token was validated");
return true; return true;
} }
@ -933,6 +942,15 @@ void GRPCClient::logError(QString const &message)
} }
//****************************************************************************************************************************************************
/// \param[in] message The event message.
//****************************************************************************************************************************************************
void GRPCClient::logInfo(QString const &message)
{
this->log(Log::Level::Info, message);
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] status The status /// \param[in] status The status
/// \param[in] callName The call name. /// \param[in] callName The call name.

View File

@ -50,7 +50,7 @@ class GRPCClient : public QObject
Q_OBJECT Q_OBJECT
public: // static member functions public: // static member functions
static void removeServiceConfigFile(); ///< Delete the service config file. static void removeServiceConfigFile(); ///< Delete the service config file.
static GRPCConfig waitAndRetrieveServiceConfig(qint64 timeoutMs); ///< Wait and retrieve the service configuration. static GRPCConfig waitAndRetrieveServiceConfig(qint64 timeoutMs, class ProcessMonitor *serverProcess); ///< Wait and retrieve the service configuration.
public: // member functions. public: // member functions.
GRPCClient() = default; ///< Default constructor. GRPCClient() = default; ///< Default constructor.
@ -60,7 +60,7 @@ public: // member functions.
GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator. GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator.
GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator. GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator.
void setLog(Log *log); ///< Set the log for the client. void setLog(Log *log); ///< Set the log for the client.
bool connectToServer(GRPCConfig const &config, QString &outError); ///< Establish connection to the gRPC server. bool connectToServer(GRPCConfig const &config, class ProcessMonitor *serverProcess, QString &outError); ///< Establish connection to the gRPC server.
grpc::Status checkTokens(QString const &clientConfigPath, QString &outReturnedClientToken); ///< Performs a token check. grpc::Status checkTokens(QString const &clientConfigPath, QString &outReturnedClientToken); ///< Performs a token check.
grpc::Status addLogEntry(Log::Level level, QString const &package, QString const &message); ///< Performs the "AddLogEntry" gRPC call. grpc::Status addLogEntry(Log::Level level, QString const &package, QString const &message); ///< Performs the "AddLogEntry" gRPC call.
@ -211,6 +211,8 @@ private:
void logTrace(QString const &message); ///< Log a trace event. void logTrace(QString const &message); ///< Log a trace event.
void logDebug(QString const &message); ///< Log a debug event. void logDebug(QString const &message); ///< Log a debug event.
void logError(QString const &message); ///< Log an error event. void logError(QString const &message); ///< Log an error event.
void logInfo(QString const &message); ///< Log an info event.
grpc::Status logGRPCCallStatus(grpc::Status const &status, QString const &callName, QList<grpc::StatusCode> allowedErrors = {}); ///< Log the status of a gRPC code. grpc::Status logGRPCCallStatus(grpc::Status const &status, QString const &callName, QList<grpc::StatusCode> allowedErrors = {}); ///< Log the status of a gRPC code.
grpc::Status simpleMethod(SimpleMethod method); ///< perform a gRPC call to a bool setter. grpc::Status simpleMethod(SimpleMethod method); ///< perform a gRPC call to a bool setter.
grpc::Status setBool(BoolSetter setter, bool value); ///< perform a gRPC call to a bool setter. grpc::Status setBool(BoolSetter setter, bool value); ///< perform a gRPC call to a bool setter.

View File

@ -17,6 +17,7 @@
#include "Log.h" #include "Log.h"
#include "../Exception/Exception.h"
namespace bridgepp namespace bridgepp
@ -29,7 +30,7 @@ namespace
Log *qtHandlerLog { nullptr }; ///< The log instance handling qt logs. Log *qtHandlerLog { nullptr }; ///< The log instance handling qt logs.
QMutex qtHandlerMutex; ///< A mutex used to access qtHandlerLog. QMutex qtHandlerMutex; ///< A mutex used to access qtHandlerLog.
// Mapping of log levels to string. Maybe used to lookup using both side a a key, so a list of pair is more convenient that a map. // Mapping of log levels to string. Maybe used to lookup using both side a key, so a list of pair is more convenient that a map.
QList<QPair<Log::Level, QString>> const logLevelStrings { QList<QPair<Log::Level, QString>> const logLevelStrings {
{ Log::Level::Panic, "panic", }, { Log::Level::Panic, "panic", },
{ Log::Level::Fatal, "fatal", }, { Log::Level::Fatal, "fatal", },
@ -95,15 +96,15 @@ void qtMessageHandler(QtMsgType type, QMessageLogContext const &, QString const
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \brief return a string representing the log entry /// \brief return a string representing the log entry, in a format similar to the one used by logrus.
/// ///
/// \param[in] level The log entry level. /// \param[in] level The log entry level.
/// \param[in] message The log entry message. /// \param[in] message The log entry message.
/// \return The string for the log entry /// \return The string for the log entry
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
QString Log::logEntryToString(Log::Level level, QString const &message) QString Log::logEntryToString(Log::Level level, QDateTime const &dateTime, QString const &message)
{ {
return QString("[%1] %2").arg(levelToString(level).toUpper(), message); return QString("%1[%2] %3").arg(levelToString(level).left(4).toUpper(), dateTime.toString("MMM dd HH:mm:ss.zzz"), message);
} }
@ -204,6 +205,35 @@ bool Log::echoInConsole() const
} }
//****************************************************************************************************************************************************
/// \param[in] path The path of the file to write to.
/// \param[out] outError if an error occurs and this pointer in not null, on exit it contains a description of the error.
/// \return true if and only if the operation was successful.
//****************************************************************************************************************************************************
bool Log::startWritingToFile(QString const &path, QString *outError)
{
QMutexLocker locker(&mutex_);
file_ = std::make_unique<QFile>(path);
if (file_->open(QIODevice::WriteOnly | QIODevice::Text))
return true;
if (outError)
*outError = QString("Could not open log file '%1' for writing.");
file_.reset();
return false;
}
//****************************************************************************************************************************************************
///
//****************************************************************************************************************************************************
void Log::stopWritingToFile()
{
QMutexLocker locker(&mutex_);
file_.reset();
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] message The message. /// \param[in] message The message.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -273,17 +303,29 @@ void Log::trace(QString const &message)
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
void Log::addEntry(Log::Level level, QString const &message) void Log::addEntry(Log::Level level, QString const &message)
{ {
QDateTime const dateTime = QDateTime::currentDateTime();
QMutexLocker locker(&mutex_); QMutexLocker locker(&mutex_);
if (qint32(level) > qint32(level_)) if (qint32(level) > qint32(level_))
return; return;
emit entryAdded(level, message); emit entryAdded(level, message);
if (!(echoInConsole_ || file_))
return;
QString const entryStr = logEntryToString(level, dateTime, message) + "\n";
if (echoInConsole_) if (echoInConsole_)
{ {
QTextStream &stream = (qint32(level) <= (qint32(Level::Warn))) ? stderr_ : stdout_; QTextStream &stream = (qint32(level) <= (qint32(Level::Warn))) ? stderr_ : stdout_;
stream << logEntryToString(level, message) << "\n"; stream << entryStr;
stream.flush(); stream.flush();
} }
if (file_)
{
file_->write(entryStr.toLocal8Bit());
file_->flush();
}
} }

View File

@ -44,7 +44,7 @@ public: // data types.
}; };
public: // static member functions. public: // static member functions.
static QString logEntryToString(Log::Level level, QString const &message); ///< Return a string describing a log entry. static QString logEntryToString(Log::Level level, QDateTime const &dateTime, QString const &message); ///< Return a string describing a log entry.
static QString levelToString(Log::Level level); ///< return the string for a level. static QString levelToString(Log::Level level); ///< return the string for a level.
static bool stringToLevel(QString const &str, Log::Level& outLevel); ///< parse a level from a string. static bool stringToLevel(QString const &str, Log::Level& outLevel); ///< parse a level from a string.
@ -63,6 +63,8 @@ public: // member functions.
Level level() const; ///< Get the log level. Level level() const; ///< Get the log level.
void setEchoInConsole(bool value); ///< Set if the log entries should be echoed in STDOUT/STDERR. void setEchoInConsole(bool value); ///< Set if the log entries should be echoed in STDOUT/STDERR.
bool echoInConsole() const; ///< Check if the log entries should be echoed in STDOUT/STDERR. bool echoInConsole() const; ///< Check if the log entries should be echoed in STDOUT/STDERR.
bool startWritingToFile(QString const& path, QString *outError = nullptr); ///< Start writing the log to file. Concerns only future entries.
void stopWritingToFile();
void registerAsQtMessageHandler(); ///< Install the Qt message handler. void registerAsQtMessageHandler(); ///< Install the Qt message handler.
public slots: public slots:
@ -83,7 +85,7 @@ private: // data members
mutable QMutex mutex_; ///< The mutex. mutable QMutex mutex_; ///< The mutex.
Level level_ { defaultLevel }; ///< The log level Level level_ { defaultLevel }; ///< The log level
bool echoInConsole_ { false }; ///< Set if the log messages should be sent to STDOUT/STDERR. bool echoInConsole_ { false }; ///< Set if the log messages should be sent to STDOUT/STDERR.
std::unique_ptr<QFile> file_; ///< The file to write the log to.
QTextStream stdout_; ///< The stdout stream. QTextStream stdout_; ///< The stdout stream.
QTextStream stderr_; ///< The stderr stream. QTextStream stderr_; ///< The stderr stream.
}; };

View File

@ -33,6 +33,8 @@ ProcessMonitor::ProcessMonitor(QString const &exePath, QStringList const &args,
: Worker(parent) : Worker(parent)
, exePath_(exePath) , exePath_(exePath)
, args_(args) , args_(args)
, out_(stdout)
, err_(stderr)
{ {
QFileInfo fileInfo(exePath); QFileInfo fileInfo(exePath);
if (!fileInfo.exists()) if (!fileInfo.exists())
@ -42,6 +44,26 @@ ProcessMonitor::ProcessMonitor(QString const &exePath, QStringList const &args,
} }
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void ProcessMonitor::forwardProcessOutput(QProcess &p) {
QByteArray array = p.readAllStandardError();
if (!array.isEmpty())
{
err_ << array;
err_.flush();
}
array = p.readAllStandardOutput();
if (!array.isEmpty())
{
out_ << array;
out_.flush();
}
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
// //
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -49,35 +71,31 @@ void ProcessMonitor::run()
{ {
try try
{ {
{
QMutexLocker locker(&statusMutex_);
status_.ended = false;
status_.pid = -1;
}
emit started(); emit started();
QProcess p; QProcess p;
p.start(exePath_, args_); p.start(exePath_, args_);
p.waitForStarted(); p.waitForStarted();
status_.running = true;
status_.pid = p.processId();
QTextStream out(stdout), err(stderr);
QByteArray array;
while (!p.waitForFinished(100))
{ {
array = p.readAllStandardError(); QMutexLocker locker(&statusMutex_);
if (!array.isEmpty()) status_.pid = p.processId();
{
err << array;
err.flush();
}
array = p.readAllStandardOutput();
if (!array.isEmpty())
{
out << array;
out.flush();
}
} }
status_.running = false; while (!p.waitForFinished(100))
{
this->forwardProcessOutput(p);
}
this->forwardProcessOutput(p);
QMutexLocker locker(&statusMutex_);
status_.ended = true;
status_.returnCode = p.exitCode(); status_.returnCode = p.exitCode();
emit processExited(status_.returnCode); emit processExited(status_.returnCode);
@ -93,8 +111,9 @@ void ProcessMonitor::run()
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return status of the monitored process /// \return status of the monitored process
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
const ProcessMonitor::MonitorStatus &ProcessMonitor::getStatus() const ProcessMonitor::MonitorStatus ProcessMonitor::getStatus()
{ {
QMutexLocker locker(&statusMutex_);
return status_; return status_;
} }

View File

@ -36,7 +36,7 @@ Q_OBJECT
public: // static member functions public: // static member functions
struct MonitorStatus struct MonitorStatus
{ {
bool running = false; bool ended = false;
int returnCode = 0; int returnCode = 0;
qint64 pid = 0; qint64 pid = 0;
}; };
@ -49,15 +49,21 @@ public: // member functions.
ProcessMonitor &operator=(ProcessMonitor const &) = delete; ///< Disabled assignment operator. ProcessMonitor &operator=(ProcessMonitor const &) = delete; ///< Disabled assignment operator.
ProcessMonitor &operator=(ProcessMonitor &&) = delete; ///< Disabled move assignment operator. ProcessMonitor &operator=(ProcessMonitor &&) = delete; ///< Disabled move assignment operator.
void run() override; ///< Run the worker. void run() override; ///< Run the worker.
MonitorStatus const &getStatus(); MonitorStatus const getStatus(); ///< Retrieve the current status of the process.
signals: signals:
void processExited(int code); ///< Slot for the exiting of the process. void processExited(int code); ///< Slot for the exiting of the process.
private: // member functions
void forwardProcessOutput(QProcess &p); ///< Forward the standard output and error from the process to this application standard output and error.
private: // data members private: // data members
QMutex statusMutex_; ///< The status mutex.
QString const exePath_; ///< The path to the executable. QString const exePath_; ///< The path to the executable.
QStringList args_; ///< arguments to be passed to the brigde. QStringList args_; ///< arguments to be passed to the brigde.
MonitorStatus status_; ///< Status of the monitoring. MonitorStatus status_; ///< Status of the monitoring.
QTextStream out_; ///< The standard output stream.
QTextStream err_; ///< The standard error stream.
}; };

View File

@ -25,6 +25,7 @@ import (
"github.com/ProtonMail/proton-bridge/v2/internal/locations" "github.com/ProtonMail/proton-bridge/v2/internal/locations"
"github.com/ProtonMail/proton-bridge/v2/internal/updater" "github.com/ProtonMail/proton-bridge/v2/internal/updater"
"github.com/ProtonMail/proton-bridge/v2/pkg/listener" "github.com/ProtonMail/proton-bridge/v2/pkg/listener"
"github.com/sirupsen/logrus"
) )
// Type describes the available types of frontend. // Type describes the available types of frontend.
@ -34,6 +35,7 @@ const (
CLI Type = iota CLI Type = iota
GRPC GRPC
NonInteractive NonInteractive
Unknown
) )
type Frontend interface { type Frontend interface {
@ -45,7 +47,7 @@ type Frontend interface {
WaitUntilFrontendIsReady() WaitUntilFrontendIsReady()
} }
// New returns initialized frontend based on `frontendType`, which can be `CLI` or `GRPC`. // New returns initialized frontend based on `frontendType`, which can be `CLI` or `GRPC`. Non-interactive will return a nil frontend.
func New( func New(
frontendType Type, frontendType Type,
showWindowOnStart bool, showWindowOnStart bool,
@ -54,6 +56,7 @@ func New(
updater types.Updater, updater types.Updater,
restarter types.Restarter, restarter types.Restarter,
locations *locations.Locations, locations *locations.Locations,
parentPID int,
) Frontend { ) Frontend {
switch frontendType { switch frontendType {
case GRPC: case GRPC:
@ -64,6 +67,7 @@ func New(
updater, updater,
restarter, restarter,
locations, locations,
parentPID,
) )
case CLI: case CLI:
@ -75,9 +79,13 @@ func New(
) )
case NonInteractive: case NonInteractive:
return nil
case Unknown:
fallthrough fallthrough
default: default:
return nil logrus.Panicf("Unexpected frontend value %v", frontendType)
return nil // return statement is required by compiler, although the above call will panic.
} }
} }

View File

@ -40,6 +40,9 @@ import (
"github.com/ProtonMail/proton-bridge/v2/pkg/keychain" "github.com/ProtonMail/proton-bridge/v2/pkg/keychain"
"github.com/ProtonMail/proton-bridge/v2/pkg/listener" "github.com/ProtonMail/proton-bridge/v2/pkg/listener"
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
"github.com/bradenaw/juniper/xslices"
"github.com/elastic/go-sysinfo"
sysinfotypes "github.com/elastic/go-sysinfo/types"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -59,12 +62,13 @@ const (
// Service is the RPC service struct. // Service is the RPC service struct.
type Service struct { // nolint:structcheck type Service struct { // nolint:structcheck
UnimplementedBridgeServer UnimplementedBridgeServer
grpcServer *grpc.Server // the gGRPC server grpcServer *grpc.Server // the gGRPC server
listener net.Listener listener net.Listener
eventStreamCh chan *StreamEvent eventStreamCh chan *StreamEvent
eventStreamDoneCh chan struct{} eventStreamChMutex sync.RWMutex
eventQueue []*StreamEvent eventStreamDoneCh chan struct{}
eventQueueMutex sync.Mutex eventQueue []*StreamEvent
eventQueueMutex sync.Mutex
panicHandler types.PanicHandler panicHandler types.PanicHandler
eventListener listener.Listener eventListener listener.Listener
@ -84,6 +88,8 @@ type Service struct { // nolint:structcheck
locations *locations.Locations locations *locations.Locations
token string token string
pemCert string pemCert string
parentPID int
parentPIDDoneCh chan struct{}
} }
// NewService returns a new instance of the service. // NewService returns a new instance of the service.
@ -94,6 +100,7 @@ func NewService(
updater types.Updater, updater types.Updater,
restarter types.Restarter, restarter types.Restarter,
locations *locations.Locations, locations *locations.Locations,
parentPID int,
) *Service { ) *Service {
s := Service{ s := Service{
UnimplementedBridgeServer: UnimplementedBridgeServer{}, UnimplementedBridgeServer: UnimplementedBridgeServer{},
@ -109,6 +116,8 @@ func NewService(
firstTimeAutostart: sync.Once{}, firstTimeAutostart: sync.Once{},
locations: locations, locations: locations,
token: uuid.NewString(), token: uuid.NewString(),
parentPID: parentPID,
parentPIDDoneCh: make(chan struct{}),
} }
// Initializing.Done is only called sync.Once. Please keep the increment // Initializing.Done is only called sync.Once. Please keep the increment
@ -124,6 +133,7 @@ func NewService(
} }
func (s *Service) startGRPCServer() { func (s *Service) startGRPCServer() {
s.log.Info("Starting gRPC server")
tlsConfig, pemCert, err := s.generateTLSConfig() tlsConfig, pemCert, err := s.generateTLSConfig()
if err != nil { if err != nil {
s.log.WithError(err).Panic("Could not generate gRPC TLS config") s.log.WithError(err).Panic("Could not generate gRPC TLS config")
@ -147,7 +157,7 @@ func (s *Service) startGRPCServer() {
if path, err := s.saveGRPCServerConfigFile(); err != nil { if path, err := s.saveGRPCServerConfigFile(); err != nil {
s.log.WithError(err).WithField("path", path).Panic("Could not write gRPC service config file") s.log.WithError(err).WithField("path", path).Panic("Could not write gRPC service config file")
} else { } else {
s.log.WithField("path", path).Debug("Successfully saved gRPC service config file") s.log.WithField("path", path).Info("Successfully saved gRPC service config file")
} }
s.log.Info("gRPC server listening at ", s.listener.Addr()) s.log.Info("gRPC server listening at ", s.listener.Addr())
@ -175,6 +185,12 @@ func (s *Service) Loop(b types.Bridger) error {
s.initAutostart() s.initAutostart()
s.startGRPCServer() s.startGRPCServer()
if s.parentPID < 0 {
s.log.Info("Not monitoring parent PID")
} else {
go s.monitorParentPID()
}
defer func() { defer func() {
s.bridge.SetBool(settings.FirstStartGUIKey, false) s.bridge.SetBool(settings.FirstStartGUIKey, false)
}() }()
@ -518,3 +534,36 @@ func (s *Service) validateStreamServerToken(
return handler(srv, ss) return handler(srv, ss)
} }
// monitorParentPID check at regular intervals that the parent process is still alive, and if not shuts down the server
// and the applications.
func (s *Service) monitorParentPID() {
s.log.Infof("Starting to monitor parent PID %v", s.parentPID)
ticker := time.NewTicker(5 * time.Second)
for {
select {
case <-ticker.C:
if s.parentPID < 0 {
continue
}
processes, err := sysinfo.Processes() // sysinfo.Process(pid) does not seem to work on Windows.
if err != nil {
s.log.Debug("Could not retrieve process list")
continue
}
if !xslices.Any(processes, func(p sysinfotypes.Process) bool { return p != nil && p.PID() == s.parentPID }) {
s.log.Info("Parent process does not exist anymore. Initiating shutdown")
go s.quit() // quit will write to the parentPIDDoneCh, so we launch a goroutine.
} else {
s.log.Tracef("Parent process %v is still alive", s.parentPID)
}
case <-s.parentPIDDoneCh:
s.log.Infof("Stopping process monitoring for PID %v", s.parentPID)
return
}
}
}

View File

@ -95,12 +95,19 @@ func (s *Service) GuiReady(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empt
// Quit implement the Quit gRPC service call. // Quit implement the Quit gRPC service call.
func (s *Service) Quit(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { func (s *Service) Quit(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
s.log.Debug("Quit") s.log.Debug("Quit")
s.quit()
return &emptypb.Empty{}, nil
}
func (s *Service) quit() {
// Windows is notably slow at Quitting. We do it in a goroutine to speed things up a bit. // Windows is notably slow at Quitting. We do it in a goroutine to speed things up a bit.
go func() { go func() {
var err error if s.parentPID >= 0 {
if s.eventStreamCh != nil { s.parentPIDDoneCh <- struct{}{}
if _, err = s.StopEventStream(ctx, empty); err != nil { }
if s.isStreamingEvents() {
if err := s.stopEventStream(); err != nil {
s.log.WithError(err).Error("Quit failed.") s.log.WithError(err).Error("Quit failed.")
} }
} }
@ -108,8 +115,6 @@ func (s *Service) Quit(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empt
// The following call is launched as a goroutine, as it will wait for current calls to end, including this one. // The following call is launched as a goroutine, as it will wait for current calls to end, including this one.
s.grpcServer.GracefulStop() s.grpcServer.GracefulStop()
}() }()
return &emptypb.Empty{}, nil
} }
// Restart implement the Restart gRPC service call. // Restart implement the Restart gRPC service call.

View File

@ -29,19 +29,19 @@ import (
func (s *Service) RunEventStream(request *EventStreamRequest, server Bridge_RunEventStreamServer) error { func (s *Service) RunEventStream(request *EventStreamRequest, server Bridge_RunEventStreamServer) error {
s.log.Debug("Starting Event stream") s.log.Debug("Starting Event stream")
if s.eventStreamCh != nil { if s.isStreamingEvents() {
return status.Errorf(codes.AlreadyExists, "the service is already streaming") // TO-DO GODT-1667 decide if we want to kill the existing stream. return status.Errorf(codes.AlreadyExists, "the service is already streaming") // TO-DO GODT-1667 decide if we want to kill the existing stream.
} }
s.bridge.SetCurrentPlatform(request.ClientPlatform) s.bridge.SetCurrentPlatform(request.ClientPlatform)
s.eventStreamCh = make(chan *StreamEvent) s.createEventStreamChannel()
s.eventStreamDoneCh = make(chan struct{}) s.eventStreamDoneCh = make(chan struct{})
// TO-DO GODT-1667 We should have a safer we to close this channel? What if an event occur while we are closing? // TO-DO GODT-1667 We should have a safer we to close this channel? What if an event occur while we are closing?
defer func() { defer func() {
close(s.eventStreamCh) close(s.eventStreamCh)
s.eventStreamCh = nil s.deleteEventStreamChannel()
close(s.eventStreamDoneCh) close(s.eventStreamDoneCh)
s.eventStreamDoneCh = nil s.eventStreamDoneCh = nil
}() }()
@ -70,24 +70,35 @@ func (s *Service) RunEventStream(request *EventStreamRequest, server Bridge_RunE
s.log.Debug("Stop Event stream") s.log.Debug("Stop Event stream")
return err return err
} }
case <-server.Context().Done():
s.log.Info("Client closed the stream, initiating shutdown")
s.quit()
return nil
} }
} }
} }
// StopEventStream stops the event stream. // StopEventStream stops the event stream.
func (s *Service) StopEventStream(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) { func (s *Service) StopEventStream(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
return &emptypb.Empty{}, s.stopEventStream()
}
func (s *Service) stopEventStream() error {
s.eventStreamChMutex.RLock()
defer s.eventStreamChMutex.RUnlock()
if s.eventStreamCh == nil { if s.eventStreamCh == nil {
return nil, status.Errorf(codes.NotFound, "The service is not streaming") return status.Errorf(codes.NotFound, "The service is not streaming")
} }
s.eventStreamDoneCh <- struct{}{} s.eventStreamDoneCh <- struct{}{}
return &emptypb.Empty{}, nil return nil
} }
// SendEvent sends an event to the via the gRPC event stream. // SendEvent sends an event to the via the gRPC event stream.
func (s *Service) SendEvent(event *StreamEvent) error { func (s *Service) SendEvent(event *StreamEvent) error {
if s.eventStreamCh == nil { // nobody is connected to the event stream, we queue events if !s.isStreamingEvents() { // nobody is connected to the event stream, we queue events
s.queueEvent(event) s.queueEvent(event)
return nil return nil
} }
@ -174,3 +185,24 @@ func (s *Service) queueEvent(event *StreamEvent) {
s.eventQueue = append(s.eventQueue, event) s.eventQueue = append(s.eventQueue, event)
} }
} }
func (s *Service) isStreamingEvents() bool {
s.eventStreamChMutex.RLock()
defer s.eventStreamChMutex.RUnlock()
return s.eventStreamCh != nil
}
func (s *Service) createEventStreamChannel() {
s.eventStreamChMutex.Lock()
defer s.eventStreamChMutex.Unlock()
s.eventStreamCh = make(chan *StreamEvent)
}
func (s *Service) deleteEventStreamChannel() {
s.eventStreamChMutex.Lock()
defer s.eventStreamChMutex.Unlock()
s.eventStreamCh = nil
}

View File

@ -89,7 +89,7 @@ func newGoIMAPServer(tls *tls.Config, backend backend.Backend, address string, u
serverID := imapid.ID{ serverID := imapid.ID{
imapid.FieldName: "Proton Mail Bridge", imapid.FieldName: "Proton Mail Bridge",
imapid.FieldVendor: "Proton AG", imapid.FieldVendor: "Proton AG",
imapid.FieldSupportURL: "https://protonmail.com/support", imapid.FieldSupportURL: "https://proton.me/support/mail",
} }
server.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server { server.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server {

View File

@ -101,3 +101,7 @@ func getLogName(version, revision string) string {
func MatchLogName(name string) bool { func MatchLogName(name string) bool {
return regexp.MustCompile(`^v.*\.log$`).MatchString(name) return regexp.MustCompile(`^v.*\.log$`).MatchString(name)
} }
func MatchGUILogName(name string) bool {
return regexp.MustCompile(`^gui_v.*\.log$`).MatchString(name)
}

View File

@ -91,7 +91,7 @@ func (c *controller) ListenAndServe() {
} }
if err != nil { if err != nil {
l.WithError(err).Error("Cannot start listner.") l.WithError(err).Error("Cannot start listener.")
c.signals.Emit(events.ErrorEvent, string(c.server.Protocol())+" failed: "+err.Error()) c.signals.Emit(events.ErrorEvent, string(c.server.Protocol())+" failed: "+err.Error())
return return
} }

View File

@ -153,7 +153,7 @@ func (q *sendRecorder) deleteExpiredKeys() {
for key, value := range q.hashes { for key, value := range q.hashes {
// It's hard to find a good expiration time. // It's hard to find a good expiration time.
// On the one hand, a user could set up some cron job sending the same message over and over again (heartbeat). // On the one hand, a user could set up some cron job sending the same message over and over again (heartbeat).
// On the the other, a user could put the device into sleep mode while sending. // On the other, a user could put the device into sleep mode while sending.
// Changing the expiration time will always make one of the edge cases worse. // Changing the expiration time will always make one of the edge cases worse.
// But both edge cases are something we don't care much about. Important thing is we don't send the same message many times. // But both edge cases are something we don't care much about. Important thing is we don't send the same message many times.
if time.Since(value.time) > 30*time.Minute { if time.Since(value.time) > 30*time.Minute {

View File

@ -245,7 +245,7 @@ func (store *Store) createOrUpdateMessageEvent(msg *pmapi.Message) error {
func (store *Store) createOrUpdateMessagesEvent(msgs []*pmapi.Message) error { //nolint:funlen func (store *Store) createOrUpdateMessagesEvent(msgs []*pmapi.Message) error { //nolint:funlen
store.log.WithField("msgs", msgs).Trace("Creating or updating messages in the store") store.log.WithField("msgs", msgs).Trace("Creating or updating messages in the store")
// Strip non meta first to reduce memory (no need to keep all old msg ID data during update). // Strip non-meta first to reduce memory (no need to keep all old msg ID data during update).
err := store.db.View(func(tx *bolt.Tx) error { err := store.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(metadataBucket) b := tx.Bucket(metadataBucket)
for _, msg := range msgs { for _, msg := range msgs {
@ -321,7 +321,7 @@ func clearNonMetadata(onlyMeta *pmapi.Message) {
onlyMeta.Attachments = nil onlyMeta.Attachments = nil
} }
// txUpdateMetadataFromDB changes the the onlyMeta data. // txUpdateMetadataFromDB changes the onlyMeta data.
// If there is stored message in metaBucket the size, header and MIMEType are // If there is stored message in metaBucket the size, header and MIMEType are
// not changed if already set. To change these: // not changed if already set. To change these:
// * size must be updated by Message.SetSize // * size must be updated by Message.SetSize

View File

@ -20,4 +20,4 @@
package updater package updater
const Host = "https://protonmail.com/download" const Host = "https://proton.me/download"

View File

@ -38,7 +38,7 @@ type VersionInfo struct {
// Installers are the locations of installer files (for manual installation). // Installers are the locations of installer files (for manual installation).
Installers []string Installers []string
// LandingPage is the address of the app landing page on protonmail.com. // LandingPage is the address of the app landing page on proton.me
LandingPage string LandingPage string
// ReleaseNotesPage is the address of the page containing the release notes. // ReleaseNotesPage is the address of the page containing the release notes.
@ -54,26 +54,26 @@ type VersionInfo struct {
// { // {
// "stable": { // "stable": {
// "Version": "2.3.4", // "Version": "2.3.4",
// "Package": "https://protonmail.com/.../bridge_2.3.4_linux.tgz", // "Package": "https://proton.me/.../bridge_2.3.4_linux.tgz",
// "Installers": [ // "Installers": [
// "https://protonmail.com/.../something.deb", // "https://proton.me/.../something.deb",
// "https://protonmail.com/.../something.rpm", // "https://proton.me/.../something.rpm",
// "https://protonmail.com/.../PKGBUILD" // "https://proton.me/.../PKGBUILD"
// ], // ],
// "LandingPage": "https://protonmail.com/bridge", // "LandingPage": "https://proton.me/mail/bridge#download",
// "ReleaseNotesPage": "https://protonmail.com/.../release_notes.html", // "ReleaseNotesPage": "https://proton.me/download/{ie,bridge}/{stable,early}_releases.html",
// "RolloutProportion": 0.5 // "RolloutProportion": 0.5
// }, // },
// "early": { // "early": {
// "Version": "2.4.0", // "Version": "2.4.0",
// "Package": "https://protonmail.com/.../bridge_2.4.0_linux.tgz", // "Package": "https://proton.me/.../bridge_2.4.0_linux.tgz",
// "Installers": [ // "Installers": [
// "https://protonmail.com/.../something.deb", // "https://proton.me/.../something.deb",
// "https://protonmail.com/.../something.rpm", // "https://proton.me/.../something.rpm",
// "https://protonmail.com/.../PKGBUILD" // "https://proton.me/.../PKGBUILD"
// ], // ],
// "LandingPage": "https://protonmail.com/bridge", // "LandingPage": "https://proton.me/mail/bridge#download",
// "ReleaseNotesPage": "https://protonmail.com/.../release_notes.html", // "ReleaseNotesPage": "https://proton.me/download/{ie,bridge}/{stable,early}_releases.html",
// "RolloutProportion": 0.5 // "RolloutProportion": 0.5
// }, // },
// "...": { // "...": {
@ -84,8 +84,8 @@ type VersionMap map[string]VersionInfo
// getVersionFileURL returns the URL of the version file. // getVersionFileURL returns the URL of the version file.
// For example: // For example:
// - https://protonmail.com/download/bridge/version_linux.json // - https://proton.me/download/bridge/version_linux.json
// - https://protonmail.com/download/ie/version_linux.json // - https://proton.me/download/ie/version_linux.json
func (u *Updater) getVersionFileURL() string { func (u *Updater) getVersionFileURL() string {
return fmt.Sprintf("%v/%v/version_%v.json", Host, u.updateURLName, u.platform) return fmt.Sprintf("%v/%v/version_%v.json", Host, u.updateURLName, u.platform)
} }

View File

@ -17,6 +17,12 @@
package versioner package versioner
import "strings"
func getExeName(name string) string { func getExeName(name string) string {
if strings.HasSuffix(name, ".exe") {
return name
}
return name + ".exe" return name + ".exe"
} }

View File

@ -33,10 +33,16 @@ var TrustedAPIPins = []string{ //nolint:gochecknoglobals
`pin-sha256="AfMENBVvOS8MnISprtvyPsjKlPooqh8nMB/pvCrpJpw="`, // cold backup `pin-sha256="AfMENBVvOS8MnISprtvyPsjKlPooqh8nMB/pvCrpJpw="`, // cold backup
// protonmail.com // protonmail.com
// \todo remove when sure no one is using it.
`pin-sha256="8joiNBdqaYiQpKskgtkJsqRxF7zN0C0aqfi8DacknnI="`, // current `pin-sha256="8joiNBdqaYiQpKskgtkJsqRxF7zN0C0aqfi8DacknnI="`, // current
`pin-sha256="JMI8yrbc6jB1FYGyyWRLFTmDNgIszrNEMGlgy972e7w="`, // hot backup `pin-sha256="JMI8yrbc6jB1FYGyyWRLFTmDNgIszrNEMGlgy972e7w="`, // hot backup
`pin-sha256="Iu44zU84EOCZ9vx/vz67/MRVrxF1IO4i4NIa8ETwiIY="`, // cold backup `pin-sha256="Iu44zU84EOCZ9vx/vz67/MRVrxF1IO4i4NIa8ETwiIY="`, // cold backup
// proton.me
`pin-sha256="CT56BhOTmj5ZIPgb/xD5mH8rY3BLo/MlhP7oPyJUEDo="`, // current
`pin-sha256="35Dx28/uzN3LeltkCBQ8RHK0tlNSa2kCpCRGNp34Gxc="`, // hot backup
`pin-sha256="qYIukVc63DEITct8sFT7ebIq5qsWmuscaIKeJx+5J5A="`, // col backup
// proxies // proxies
`pin-sha256="EU6TS9MO0L/GsDHvVc9D5fChYLNy5JdGYpJw0ccgetM="`, // main `pin-sha256="EU6TS9MO0L/GsDHvVc9D5fChYLNy5JdGYpJw0ccgetM="`, // main
`pin-sha256="iKPIHPnDNqdkvOnTClQ8zQAIKG0XavaPkcEo0LBAABA="`, // backup 1 `pin-sha256="iKPIHPnDNqdkvOnTClQ8zQAIKG0XavaPkcEo0LBAABA="`, // backup 1

View File

@ -88,7 +88,7 @@ func TestTLSSignedCertTrustedPublicKey(t *testing.T) {
_, dialer, _ := createClientWithPinningDialer("") _, dialer, _ := createClientWithPinningDialer("")
copyTrustedPins(dialer.pinChecker) copyTrustedPins(dialer.pinChecker)
dialer.pinChecker.trustedPins = append(dialer.pinChecker.trustedPins, `pin-sha256="SA4v9d2YY4vX5YQOQ1qZHYTBMCTSD/sxPvyj+JL6+vI="`) dialer.pinChecker.trustedPins = append(dialer.pinChecker.trustedPins, `pin-sha256="LwnIKjNLV3z243ap8y0yXNPghsqE76J08Eq3COvUt2E="`)
_, err := dialer.DialTLS("tcp", "rsa4096.badssl.com:443") _, err := dialer.DialTLS("tcp", "rsa4096.badssl.com:443")
r.NoError(t, err, "expected dial to succeed because public key is known and cert is signed by CA") r.NoError(t, err, "expected dial to succeed because public key is known and cert is signed by CA")
} }

View File

@ -85,7 +85,7 @@ func newManager(cfg Config) *manager {
// The resty is increasing the delay between retries up to 1 minute // The resty is increasing the delay between retries up to 1 minute
// (SetRetryMaxWaitTime) so for 10 retries the cumulative delay can be // (SetRetryMaxWaitTime) so for 10 retries the cumulative delay can be
// up to 5min. // up to 5min.
m.rc.SetRetryCount(5) m.rc.SetRetryCount(3)
m.rc.SetRetryMaxWaitTime(time.Minute) m.rc.SetRetryMaxWaitTime(time.Minute)
m.rc.SetRetryAfter(catchRetryAfter) m.rc.SetRetryAfter(catchRetryAfter)
m.rc.AddRetryCondition(m.shouldRetry) m.rc.AddRetryCondition(m.shouldRetry)

View File

@ -62,6 +62,10 @@ func (m *manager) NewClientWithLogin(ctx context.Context, username string, passw
return nil, nil, err return nil, nil, err
} }
// Do not retry requests after this point. The ephemeral from auth info
// won't be valid any more
ctx = ContextWithoutRetry(ctx)
auth, err := m.auth(ctx, AuthReq{ auth, err := m.auth(ctx, AuthReq{
Username: username, Username: username,
ClientProof: base64.StdEncoding.EncodeToString(proofs.ClientProof), ClientProof: base64.StdEncoding.EncodeToString(proofs.ClientProof),

View File

@ -105,17 +105,27 @@ func logConnReuse(_ *resty.Client, res *resty.Response) error {
func catchRetryAfter(_ *resty.Client, res *resty.Response) (time.Duration, error) { func catchRetryAfter(_ *resty.Client, res *resty.Response) (time.Duration, error) {
if res.StatusCode() == http.StatusTooManyRequests { if res.StatusCode() == http.StatusTooManyRequests {
if after := res.Header().Get("Retry-After"); after != "" { if after := res.Header().Get("Retry-After"); after != "" {
l := log.
WithField("statusCode", res.StatusCode()).
WithField("url", res.Request.URL).
WithField("verb", res.Request.Method)
seconds, err := strconv.Atoi(after) seconds, err := strconv.Atoi(after)
if err != nil { if err != nil {
log.WithError(err).Warning("Cannot convert Retry-After to number") l.WithError(err).Warning("Cannot convert Retry-After to number")
seconds = 10 seconds = 10
} }
// To avoid spikes when all clients retry at the same time, we add some random wait. // To avoid spikes when all clients retry at the same time, we add some random wait.
seconds += rand.Intn(10) //nolint:gosec // It is OK to use weak random number generator here. seconds += rand.Intn(10) //nolint:gosec // It is OK to use weak random number generator here.
l = l.WithField("seconds", seconds).WithField("start", time.Now().Unix())
log.Warningf("Retrying %s after %ds induced by http code %d", res.Request.URL, seconds, res.StatusCode()) // Maximum retry time in client is is one minute. But
return time.Duration(seconds) * time.Second, nil // here wait times can be longer e.g. high API load
l.Warn("Retrying after induced by http code. Waiting now...")
time.Sleep(time.Duration(seconds) * time.Second)
l.Warn("Wait done")
return 0, nil
} }
} }

View File

@ -1,7 +1,7 @@
.PHONY: check-go check-godog install-godog test test-bridge test-live test-live-bridge test-stage test-debug test-live-debug bench .PHONY: check-go check-godog install-godog test test-bridge test-live test-live-bridge test-stage test-debug test-live-debug bench
export GO111MODULE=on export GO111MODULE=on
export BRIDGE_VERSION:=2.4.3+integrationtests export BRIDGE_VERSION:=2.4.8+integrationtests
export VERBOSITY?=fatal export VERBOSITY?=fatal
export TEST_DATA=testdata export TEST_DATA=testdata
@ -14,6 +14,7 @@ check-godog:
@which godog || $(MAKE) install-godog @which godog || $(MAKE) install-godog
install-godog: check-go install-godog: check-go
go install github.com/cucumber/godog/cmd/godog@v0.12.5 go install github.com/cucumber/godog/cmd/godog@v0.12.5
go install github.com/cucumber/godog/cmd/godog@upd-go1.18
test: test-bridge test: test-bridge
test-bridge: FEATURES ?= features test-bridge: FEATURES ?= features
@ -25,7 +26,11 @@ test-bridge: check-godog
test-live: test-live-bridge test-live-ie test-live: test-live-bridge test-live-ie
test-live-bridge: FEATURES ?= features test-live-bridge: FEATURES ?= features
test-live-bridge: check-godog test-live-bridge: check-godog
TEST_ENV=live godog --tags="~@ignore && ~@ignore-live" $(FEATURES) TEST_ENV=live godog --tags="~@ignore && ~@ignore-live && ~@ignore-live-auth" $(FEATURES)
test-live-bridge-auth: check-godog
TEST_ENV=live godog --tags="@ignore-live-auth" $(FEATURES)
# Doesn't work in parallel! # Doesn't work in parallel!
# Provide TEST_ACCOUNTS with your accounts. # Provide TEST_ACCOUNTS with your accounts.

View File

@ -21,6 +21,7 @@ import (
"context" "context"
"fmt" "fmt"
"math/rand" "math/rand"
"os"
"path/filepath" "path/filepath"
"time" "time"
@ -29,6 +30,7 @@ import (
"github.com/ProtonMail/proton-bridge/v2/internal/users" "github.com/ProtonMail/proton-bridge/v2/internal/users"
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -57,9 +59,15 @@ func (ctx *TestContext) LoginUser(username string, password, mailboxPassword []b
return errors.Wrap(err, "failed to finish login") return errors.Wrap(err, "failed to finish login")
} }
ctx.addCleanupChecked(func() error { ctx.addCleanupChecked(
return ctx.bridge.LogoutUser(userID) func() error {
}, "Logging out user") if os.Getenv(EnvName) == EnvLive {
logrus.Warn("Pausing user.Logout by 2 minutes to not hit issues with too many login attempts.")
time.Sleep(2 * time.Minute)
}
return ctx.bridge.LogoutUser(userID)
}, "Logging out user",
)
return nil return nil
} }

View File

@ -9,11 +9,13 @@ Feature: IMAP auth
When IMAP client authenticates "user" with bad password When IMAP client authenticates "user" with bad password
Then IMAP response is "IMAP error: NO backend/credentials: incorrect password" Then IMAP response is "IMAP error: NO backend/credentials: incorrect password"
@ignore-live-auth
Scenario: Authenticates with disconnected user Scenario: Authenticates with disconnected user
Given there is disconnected user "user" Given there is disconnected user "user"
When IMAP client authenticates "user" When IMAP client authenticates "user"
Then IMAP response is "IMAP error: NO account is logged out, use the app to login again" Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
@ignore-live-auth
Scenario: Authenticates with connected user that was loaded without internet Scenario: Authenticates with connected user that was loaded without internet
Given there is user "user" which just logged in Given there is user "user" which just logged in
And there is no internet connection And there is no internet connection
@ -27,12 +29,14 @@ Feature: IMAP auth
And 2 seconds pass And 2 seconds pass
Then "user" is connected Then "user" is connected
@ignore-live-auth
Scenario: Authenticates with freshly logged-out user Scenario: Authenticates with freshly logged-out user
Given there is user "user" which just logged in Given there is user "user" which just logged in
When "user" logs out When "user" logs out
And IMAP client authenticates "user" And IMAP client authenticates "user"
Then IMAP response is "IMAP error: NO account is logged out, use the app to login again" Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
@ignore-live-auth
Scenario: Authenticates user which was re-logged in Scenario: Authenticates user which was re-logged in
Given there is user "user" which just logged in Given there is user "user" which just logged in
When "user" logs out When "user" logs out

View File

@ -125,6 +125,7 @@ Feature: IMAP fetch messages
| 1:* | | 1:* |
| * | | * |
@ignore-live
Scenario: Fetch of big mailbox Scenario: Fetch of big mailbox
Given there are 100 messages in mailbox "Folders/mbox" for "user" Given there are 100 messages in mailbox "Folders/mbox" for "user"
And there is IMAP client logged in as "user" And there is IMAP client logged in as "user"

View File

@ -1,34 +1,43 @@
Feature: Delete user Feature: Delete user
@ignore-live-auth
Scenario: Deleting connected user Scenario: Deleting connected user
Given there is user "user" which just logged in Given there is user "user" which just logged in
When user deletes "user" When user deletes "user"
Then last response is "OK" Then last response is "OK"
And "user" has database file And "user" has database file
@ignore-live-auth
Scenario: Deleting connected user with cache Scenario: Deleting connected user with cache
Given there is user "user" which just logged in Given there is user "user" which just logged in
When user deletes "user" with cache When user deletes "user" with cache
Then last response is "OK" Then last response is "OK"
And "user" does not have database file And "user" does not have database file
@ignore-live-auth
Scenario: Deleting connected user without database file Scenario: Deleting connected user without database file
Given there is user "user" which just logged in Given there is user "user" which just logged in
And there is no database file for "user" And there is no database file for "user"
When user deletes "user" with cache When user deletes "user" with cache
Then last response is "OK" Then last response is "OK"
# THIS IS BLOCKED BY ANTI-ABUSE
@ignore-live
Scenario: Deleting disconnected user Scenario: Deleting disconnected user
Given there is disconnected user "user" Given there is disconnected user "user"
When user deletes "user" When user deletes "user"
Then last response is "OK" Then last response is "OK"
And "user" has database file And "user" has database file
# THIS IS BLOCKED BY ANTI-ABUSE
@ignore-live
Scenario: Deleting disconnected user with cache Scenario: Deleting disconnected user with cache
Given there is disconnected user "user" Given there is disconnected user "user"
When user deletes "user" with cache When user deletes "user" with cache
Then last response is "OK" Then last response is "OK"
And "user" does not have database file And "user" does not have database file
# THIS IS BLOCKED BY ANTI-ABUSE
@ignore-live
Scenario: Deleting disconnected user without database file Scenario: Deleting disconnected user without database file
Given there is disconnected user "user" Given there is disconnected user "user"
And there is no database file for "user" And there is no database file for "user"

View File

@ -1,4 +1,5 @@
Feature: Login for the first time Feature: Login for the first time
@ignore-live-auth
Scenario: Normal login Scenario: Normal login
Given there is user "user" Given there is user "user"
When "user" logs in When "user" logs in
@ -19,6 +20,7 @@ Feature: Login for the first time
When "user" logs in with bad password When "user" logs in with bad password
Then last response is "failed to login: Incorrect login credentials. Please try again" Then last response is "failed to login: Incorrect login credentials. Please try again"
@ignore-live-auth
Scenario: Login without internet connection Scenario: Login without internet connection
Given there is no internet connection Given there is no internet connection
When "user" logs in When "user" logs in
@ -34,6 +36,7 @@ Feature: Login for the first time
And "user2fa" has running event loop And "user2fa" has running event loop
And "user2fa" has non-zero space And "user2fa" has non-zero space
@ignore-live-auth
Scenario: Login user with capital letters in address Scenario: Login user with capital letters in address
Given there is user "userAddressWithCapitalLetter" Given there is user "userAddressWithCapitalLetter"
When "userAddressWithCapitalLetter" logs in When "userAddressWithCapitalLetter" logs in
@ -43,6 +46,7 @@ Feature: Login for the first time
And "userAddressWithCapitalLetter" has running event loop And "userAddressWithCapitalLetter" has running event loop
And "userAddressWithCapitalLetter" has non-zero space And "userAddressWithCapitalLetter" has non-zero space
@ignore-live-auth
Scenario: Login user with more addresses Scenario: Login user with more addresses
Given there is user "userMoreAddresses" Given there is user "userMoreAddresses"
When "userMoreAddresses" logs in When "userMoreAddresses" logs in
@ -62,6 +66,7 @@ Feature: Login for the first time
And "userDisabledPrimaryAddress" has running event loop And "userDisabledPrimaryAddress" has running event loop
And "userDisabledPrimaryAddress" has non-zero space And "userDisabledPrimaryAddress" has non-zero space
@ignore-live-auth
Scenario: Login two users Scenario: Login two users
Given there is user "user" Given there is user "user"
And there is user "userMoreAddresses" And there is user "userMoreAddresses"

View File

@ -1,4 +1,5 @@
Feature: Re-login Feature: Re-login
@ignore-live-auth
Scenario: Re-login with connected user and database file Scenario: Re-login with connected user and database file
Given there is user "user" which just logged in Given there is user "user" which just logged in
And there is database file for "user" And there is database file for "user"
@ -18,6 +19,7 @@ Feature: Re-login
And "user" has database file And "user" has database file
And "user" has running event loop And "user" has running event loop
@ignore-live-auth
Scenario: Re-login with disconnected user and database file Scenario: Re-login with disconnected user and database file
Given there is disconnected user "user" Given there is disconnected user "user"
And there is database file for "user" And there is database file for "user"
@ -27,6 +29,7 @@ Feature: Re-login
And "user" has running event loop And "user" has running event loop
And "user" has non-zero space And "user" has non-zero space
@ignore-live-auth
Scenario: Re-login with disconnected user and no database file Scenario: Re-login with disconnected user and no database file
Given there is disconnected user "user" Given there is disconnected user "user"
And there is no database file for "user" And there is no database file for "user"

View File

@ -1,11 +1,21 @@
--- ---
wait: true wait: true
strict: true
file: file:
name: "./gobinsec-cache.yml" name: "./gobinsec-cache.yml"
expiration: 24h expiration: "24h"
ignore: ignore:
# golang.org/x/net wrong match, we are using 2871e0cb, fixed by 37e1c6af # golang.org/x/net wrong match, we are using v0.1.0, fixed by 37e1c6af in v0.0.xxx
- "CVE-2021-33194" - "CVE-2021-33194"
# golang.org/x/crypto wrong match, we are using v0.1.0 all of this have been fixed in vO.O.xx
- "CVE-2019-11840"
- "CVE-2020-29652"
- "CVE-2021-43565"
- "CVE-2022-27191"
- "CVE-2020-9283"
- "CVE-2017-3204"
# golang.org/x/text wrong match, we are using v0.4.0, fixed in a previous version
- "CVE-2020-14040"

View File

@ -1,4 +1,4 @@
##!/bin/bash #!/bin/bash
# Copyright (c) 2022 Proton AG # Copyright (c) 2022 Proton AG
# #

View File

@ -7,9 +7,9 @@ require github.com/intercloud/gobinsec v0.10.2
require ( require (
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
github.com/fatih/color v1.13.0 // indirect github.com/fatih/color v1.13.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-isatty v0.0.16 // indirect
github.com/memcachier/gomemcache v0.0.0-20170425125614-d027381f7653 // indirect github.com/memcachier/gomemcache v0.0.0-20170425125614-d027381f7653 // indirect
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect golang.org/x/sys v0.1.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@ -7,9 +7,13 @@ github.com/intercloud/gobinsec v0.10.2/go.mod h1:Y/AMKT0aQM40WDkTqlEe18W/IL6ZUuu
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/memcachier/gomemcache v0.0.0-20170425125614-d027381f7653 h1:222emoxOt/bCmNHp8Xt0Pr5Am3gIbqRKFpb4CQ9O2SI= github.com/memcachier/gomemcache v0.0.0-20170425125614-d027381f7653 h1:222emoxOt/bCmNHp8Xt0Pr5Am3gIbqRKFpb4CQ9O2SI=
github.com/memcachier/gomemcache v0.0.0-20170425125614-d027381f7653/go.mod h1:KoYVbOQexD45AOLfn+gsFB6c3o4ANzP1QKzjE6tZbK0= github.com/memcachier/gomemcache v0.0.0-20170425125614-d027381f7653/go.mod h1:KoYVbOQexD45AOLfn+gsFB6c3o4ANzP1QKzjE6tZbK0=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -18,7 +22,12 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -19,7 +19,7 @@
# Generate HTML release notes # Generate HTML release notes
# hosted at https://protonmail.com/download/{ie,bridge}/{stable,early}_releases.html # hosted at https://proton.me/download/{ie,bridge}/{stable,early}_releases.html
INFILE=$1 INFILE=$1
OUTFILE=${INFILE//.md/.html} OUTFILE=${INFILE//.md/.html}