forked from Silverfish/proton-bridge
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| debe87f2f5 | |||
| cca2807256 | |||
| 7b73f76e78 | |||
| b1eefd6c85 | |||
| bbcb7ad980 | |||
| 984c43cd75 | |||
| ec4c0fdd09 | |||
| 51d4a9c7ee | |||
| 19930f63e2 | |||
| 3b9a3aaad2 | |||
| f5148074fd | |||
| a949a113cf | |||
| 227e9df419 | |||
| 2a6d462be1 | |||
| bb03fa26cd | |||
| 9eb4703d7a | |||
| 105752fc65 | |||
| 2747e93316 | |||
| 9548f984eb | |||
| cb871ce4bc | |||
| 8ca849b7a8 | |||
| 4bb29b1b5c | |||
| e55e893c94 | |||
| 5ab63a290e | |||
| 7c3414b86f | |||
| cec8829032 | |||
| 78f9f49a8a | |||
| 5a7722fd18 | |||
| d111a979f7 | |||
| 31514c8e31 | |||
| af5ce101ef | |||
| 075da27d13 | |||
| 7b19fb44a4 | |||
| c991946ea7 | |||
| f960a3ae38 | |||
| 73f8811a4b | |||
| bc6ec2579a | |||
| 35bc7263da | |||
| cc3db00a06 | |||
| 7f7961ae0c | |||
| aae60b2ef8 | |||
| ab700543b9 | |||
| 413488f5f4 |
56
Changelog.md
56
Changelog.md
@ -2,10 +2,62 @@
|
||||
|
||||
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
|
||||
|
||||
## Changed
|
||||
### Changed
|
||||
* Other: implemented tokens in bridge-gui-tester.
|
||||
* GODT-1853:
|
||||
* Upgrade dependencies (including x/crypto).
|
||||
@ -28,7 +80,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
|
||||
|
||||
## Fixed
|
||||
### Fixed
|
||||
* GUI issues:
|
||||
* GODT-1894: Fixed typo in alreadyLoggedIn event error message.
|
||||
* GODT-1479: Fix hover on “Open Bridge” in status window on macOS.
|
||||
|
||||
17
Makefile
17
Makefile
@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||
|
||||
# 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_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
@ -80,6 +80,9 @@ build: build-gui
|
||||
build-gui: ${TGZ_TARGET}
|
||||
|
||||
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-finalize=${go-build}
|
||||
@ -254,6 +257,13 @@ lint-golang:
|
||||
$(info linting with GOMAXPROCS=${GOMAXPROCS})
|
||||
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
|
||||
# Uncomment the "-ci" to fail the job if something can be updated.
|
||||
go list -u -m -json all | go-mod-outdated -update -direct #-ci
|
||||
@ -286,7 +296,11 @@ run: run-qt
|
||||
run-cli: run-nogui
|
||||
|
||||
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}
|
||||
endif
|
||||
|
||||
run-nogui: build-nogui clean-vendor gofiles
|
||||
PROTONMAIL_ENV=dev ./${LAUNCHER_EXE} ${RUN_FLAGS} -c
|
||||
@ -313,6 +327,7 @@ clean: clean-vendor clean-gui clean-vcpkg
|
||||
rm -f ./*.syso
|
||||
rm -f release-notes/bridge.html
|
||||
rm -f release-notes/import-export.html
|
||||
rm -f ${LAUNCHER_EXE} ${BRIDGE_EXE} ${BRIDGE_EXE_NAME}
|
||||
|
||||
|
||||
.PHONY: generate
|
||||
|
||||
@ -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
|
||||
background.
|
||||
|
||||
More details [on the public website](https://protonmail.com/bridge).
|
||||
More details [on the public website](https://proton.me/mail/bridge).
|
||||
|
||||
## Launchers
|
||||
Launchers are binaries used to run the Proton Mail Bridge or Import-Export apps.
|
||||
|
||||
@ -55,6 +55,8 @@ const (
|
||||
)
|
||||
|
||||
func main() { //nolint:funlen
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
l := logrus.WithField("launcher_version", constants.Version)
|
||||
reporter := sentry.NewReporter(appName, constants.Version, useragent.New())
|
||||
|
||||
crashHandler := crash.NewHandler(reporter.ReportException)
|
||||
@ -62,58 +64,69 @@ func main() { //nolint:funlen
|
||||
|
||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
|
||||
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)
|
||||
|
||||
logsPath, err := locations.ProvideLogsPath()
|
||||
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))
|
||||
|
||||
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"))
|
||||
|
||||
updatesPath, err := locations.ProvideUpdatesPath()
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
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)
|
||||
if wait {
|
||||
waitForProcessToFinish(mainExe)
|
||||
@ -134,7 +147,7 @@ func main() { //nolint:funlen
|
||||
}
|
||||
|
||||
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(
|
||||
name string,
|
||||
versioner *versioner.Versioner,
|
||||
ver *versioner.Versioner,
|
||||
kr *crypto.KeyRing,
|
||||
reporter *sentry.Reporter,
|
||||
) (string, error) {
|
||||
versions, err := versioner.ListVersions()
|
||||
versions, err := ver.ListVersions()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to list available versions")
|
||||
}
|
||||
@ -208,7 +221,11 @@ func getPathToUpdatedExecutable(
|
||||
}
|
||||
|
||||
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 {
|
||||
vlog.WithError(err).Error("Files failed verification and will be removed")
|
||||
@ -241,17 +258,6 @@ func getPathToUpdatedExecutable(
|
||||
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.
|
||||
func waitForProcessToFinish(exePath string) {
|
||||
for {
|
||||
|
||||
2
dist/info.rc
vendored
2
dist/info.rc
vendored
@ -3,7 +3,7 @@
|
||||
|
||||
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 INTERNAL_NAME STRINGIZE(EXE_NAME)
|
||||
#define PRODUCT_NAME "Proton Mail Bridge for Windows"
|
||||
|
||||
2
dist/proton-bridge.desktop
vendored
2
dist/proton-bridge.desktop
vendored
@ -3,7 +3,7 @@ Type=Application
|
||||
Version=1.1
|
||||
Name=Proton Mail Bridge
|
||||
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
|
||||
Exec=protonmail-bridge
|
||||
Terminal=false
|
||||
|
||||
8
go.mod
8
go.mod
@ -50,9 +50,9 @@ require (
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
golang.org/x/exp v0.0.0-20220921164117-439092de6870
|
||||
golang.org/x/net v0.0.0-20220921203646-d300de134e69
|
||||
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8
|
||||
golang.org/x/text v0.3.7
|
||||
golang.org/x/net v0.1.0
|
||||
golang.org/x/sys v0.1.0
|
||||
golang.org/x/text v0.4.0
|
||||
google.golang.org/grpc v1.49.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
howett.net/plist v1.0.0
|
||||
@ -93,7 +93,7 @@ require (
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // 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/tools v0.1.12 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 // indirect
|
||||
|
||||
16
go.sum
16
go.sum
@ -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-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-20220919173607-35f4265a4bc0 h1:a5Yg6ylndHHYJqIPrdq0AhvR6KTvDTAvgBtaidhEevY=
|
||||
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
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-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-20220921203646-d300de134e69 h1:hUJpGDpnfwdJW8iNypFjmSY0sCBEL+spFTZ2eO+Sfps=
|
||||
golang.org/x/net v0.0.0-20220921203646-d300de134e69/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||
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-20190226205417-e64efc72b421/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-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-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc=
|
||||
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/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=
|
||||
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.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.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.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@ -69,12 +69,14 @@ const (
|
||||
flagMemProfileShort = "m"
|
||||
flagLogLevel = "log-level"
|
||||
flagLogLevelShort = "l"
|
||||
// FlagCLI indicate to start with command line interface.
|
||||
FlagCLI = "cli"
|
||||
flagCLIShort = "c"
|
||||
flagRestart = "restart"
|
||||
FlagLauncher = "launcher"
|
||||
FlagNoWindow = "no-window"
|
||||
FlagGRPC = "grpc" // FlagGRPC starts the gRPC frontend
|
||||
FlagGRPCShort = "g"
|
||||
FlagCLI = "cli" // FlagCLI indicate to start with command line interface.
|
||||
flagCLIShort = "c"
|
||||
flagRestart = "restart"
|
||||
FlagLauncher = "launcher"
|
||||
FlagNoWindow = "no-window"
|
||||
FlagParentPID = "parent-pid"
|
||||
)
|
||||
|
||||
type Base struct {
|
||||
@ -299,6 +301,11 @@ func (b *Base) NewApp(mainLoop func(*Base, *cli.Context) error) *cli.App {
|
||||
Aliases: []string{flagLogLevelShort},
|
||||
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{
|
||||
Name: FlagCLI,
|
||||
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",
|
||||
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
|
||||
|
||||
@ -20,6 +20,7 @@ package base
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/execabs"
|
||||
@ -38,6 +39,8 @@ func (b *Base) restartApp(crash bool) error {
|
||||
args = os.Args[1:]
|
||||
}
|
||||
|
||||
args = removeFlagWithValue(args, FlagParentPID)
|
||||
|
||||
if b.launcher != "" {
|
||||
args = forceLauncherFlag(args, b.launcher)
|
||||
}
|
||||
@ -85,6 +88,30 @@ func incrementRestartFlag(args []string) []string {
|
||||
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.
|
||||
func forceLauncherFlag(args []string, launcher string) []string {
|
||||
res := append([]string{}, args...)
|
||||
|
||||
@ -61,3 +61,22 @@ func TestVersionLessThan(t *testing.T) {
|
||||
r.False(current.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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,10 +40,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
flagLogIMAP = "log-imap"
|
||||
flagLogSMTP = "log-smtp"
|
||||
flagNonInteractive = "noninteractive"
|
||||
|
||||
flagLogIMAP = "log-imap"
|
||||
flagLogSMTP = "log-smtp"
|
||||
flagNonInteractive = "noninteractive"
|
||||
flagNonInteractiveShort = "n"
|
||||
// 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).
|
||||
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!)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagNonInteractive,
|
||||
Usage: "Start Bridge entirely non-interactively",
|
||||
Name: flagNonInteractive,
|
||||
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
|
||||
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(
|
||||
frontendType,
|
||||
!c.Bool(base.FlagNoWindow),
|
||||
@ -80,6 +86,7 @@ func main(b *base.Base, c *cli.Context) error { //nolint:funlen
|
||||
b.Updater,
|
||||
b,
|
||||
b.Locations,
|
||||
c.Int(base.FlagParentPID),
|
||||
)
|
||||
|
||||
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 {
|
||||
switch {
|
||||
case c.Bool(base.FlagGRPC):
|
||||
return frontend.GRPC
|
||||
case c.Bool(base.FlagCLI):
|
||||
return frontend.CLI
|
||||
case c.Bool(flagNonInteractive):
|
||||
return frontend.NonInteractive
|
||||
default:
|
||||
return frontend.GRPC
|
||||
return frontend.Unknown
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ const (
|
||||
var ErrSizeTooLarge = errors.New("file is too big")
|
||||
|
||||
// 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 {
|
||||
accountName = user.Username()
|
||||
} else if users := b.GetUsers(); len(users) > 0 {
|
||||
@ -65,6 +65,16 @@ func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address,
|
||||
if err != nil {
|
||||
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(
|
||||
func(filename string) bool {
|
||||
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, 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)
|
||||
if err != nil {
|
||||
|
||||
@ -43,11 +43,13 @@ void QMLBackend::init(GRPCConfig const &serviceConfig)
|
||||
{
|
||||
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();
|
||||
|
||||
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.");
|
||||
else
|
||||
throw Exception(QString("Cannot connectToServer to go backend via gRPC: %1").arg(error));
|
||||
|
||||
@ -8,7 +8,7 @@ BEGIN
|
||||
BEGIN
|
||||
BLOCK "040904b0"
|
||||
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 "FileDescription", "${BRIDGE_APP_FULL_NAME}"
|
||||
VALUE "FileVersion", "${BRIDGE_APP_VERSION_COMMA}"
|
||||
|
||||
@ -54,7 +54,6 @@ BRIDGE_APP_FULL_NAME=${BRIDGE_APP_FULL_NAME:-"Proton Mail Bridge"}
|
||||
BRIDGE_VENDOR=${BRIDGE_VENDOR:-"Proton AG"}
|
||||
BUILD_CONFIG=${BRIDGE_GUI_BUILD_CONFIG:-Debug}
|
||||
BUILD_DIR=$(echo "./cmake-build-${BUILD_CONFIG}" | tr '[:upper:]' '[:lower:]')
|
||||
VCPKG_OSX_DEPLOYMENT_TARGET=11.0
|
||||
VCPKG_ROOT="${BRIDGE_REPO_ROOT}/extern/vcpkg"
|
||||
|
||||
git submodule update --init --recursive ${VCPKG_ROOT}
|
||||
@ -70,10 +69,8 @@ ${VCPKG_BOOTSTRAP} -disableMetrics
|
||||
check_exit "Failed to bootstrap vcpkg."
|
||||
|
||||
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
|
||||
check_exit "Failed installing gRPC for macOS / Apple Silicon"
|
||||
fi
|
||||
${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"
|
||||
${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"
|
||||
elif [[ "$OSTYPE" == "linux"* ]]; then
|
||||
|
||||
@ -32,19 +32,33 @@ using namespace bridgepp;
|
||||
namespace
|
||||
{
|
||||
|
||||
/// \brief The file extension for the bridge executable file.
|
||||
/// \brief The file extension for the bridge executable file.
|
||||
#ifdef Q_OS_WIN32
|
||||
QString const exeSuffix = ".exe";
|
||||
#else
|
||||
QString const exeSuffix;
|
||||
#endif
|
||||
|
||||
QString const bridgeLock = "bridge-gui.lock"; ///< file name used for the lock file.
|
||||
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
||||
qint64 const grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
|
||||
QString const bridgeLock = "bridge-gui.lock"; ///< file name used for the lock file.
|
||||
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
||||
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 A null string if the executable could not be located.
|
||||
@ -80,6 +94,26 @@ Log &initLog()
|
||||
{
|
||||
Log &log = app().log();
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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.
|
||||
//****************************************************************************************************************************************************
|
||||
void focusOtherInstance()
|
||||
{
|
||||
QNetworkAccessManager *manager;
|
||||
QNetworkRequest request;
|
||||
manager = new QNetworkAccessManager();
|
||||
QUrl url = getApiUrl();
|
||||
url.setPath("/focus");
|
||||
request.setUrl(url);
|
||||
QNetworkReply* rep = manager->get(request);
|
||||
|
||||
std::unique_ptr<QNetworkReply> reply(networkManager().get(QNetworkRequest(getFocusUrl())));
|
||||
QEventLoop loop;
|
||||
QObject::connect(rep, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
QObject::connect(reply.get(), &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
}
|
||||
|
||||
@ -200,7 +257,10 @@ void launchBridge(QStringList const &args)
|
||||
else
|
||||
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);
|
||||
}
|
||||
|
||||
@ -247,8 +307,8 @@ int main(int argc, char *argv[])
|
||||
{
|
||||
focusOtherInstance();
|
||||
return EXIT_FAILURE;
|
||||
|
||||
}
|
||||
|
||||
QStringList args;
|
||||
QString launcher;
|
||||
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.
|
||||
// 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.
|
||||
log.setEchoInConsole(attach);
|
||||
log.setLevel(logLevel);
|
||||
|
||||
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.
|
||||
GRPCClient::removeServiceConfigFile();
|
||||
launchBridge(args);
|
||||
}
|
||||
|
||||
|
||||
log.debug(QString("Server configuration file will be loaded from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath())));
|
||||
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(attach ? 0 : grpcServiceConfigWaitDelayMs));
|
||||
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath())));
|
||||
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(attach ? 0 : grpcServiceConfigWaitDelayMs, app().bridgeMonitor()));
|
||||
if (!attach)
|
||||
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;
|
||||
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
|
||||
@ -288,7 +355,7 @@ int main(int argc, char *argv[])
|
||||
if (bridgeMonitor)
|
||||
{
|
||||
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.
|
||||
// Restart the full process to be sure there is no more bridge orphans
|
||||
|
||||
@ -43,6 +43,7 @@ Item {
|
||||
clip: true
|
||||
|
||||
anchors.fill: parent
|
||||
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds // Disable the springy effect when scroll reaches top/bottom.
|
||||
|
||||
Item {
|
||||
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)
|
||||
|
||||
@ -852,9 +852,9 @@ Window {
|
||||
property string version: "2.0.X-BridePreview"
|
||||
property url logsPath: 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 landingPageLink: Qt.resolvedUrl("https://protonmail.com/bridge")
|
||||
property url landingPageLink: Qt.resolvedUrl("https://proton.me/mail/bridge#download")
|
||||
|
||||
property string colorSchemeName: "light"
|
||||
function changeColorScheme(newScheme){
|
||||
|
||||
@ -28,6 +28,8 @@ SettingsView {
|
||||
|
||||
property var selectedAddress
|
||||
|
||||
signal bugReportWasSent()
|
||||
|
||||
Label {
|
||||
text: qsTr("Report a problem")
|
||||
colorScheme: root.colorScheme
|
||||
@ -61,7 +63,7 @@ SettingsView {
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
// Rise max length error imidiatly while typing
|
||||
// Rise max length error immediately while typing
|
||||
if (description.text.length > description._maxLength) {
|
||||
validate()
|
||||
}
|
||||
@ -164,6 +166,7 @@ SettingsView {
|
||||
Connections {
|
||||
target: Backend
|
||||
function onReportBugFinished() { sendButton.loading = false }
|
||||
function onBugReportSendSuccess() { root.bugReportWasSent() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -53,6 +53,8 @@ Item {
|
||||
selectByMouse: true
|
||||
selectByKeyboard: true
|
||||
selectionColor: root.colorScheme.text_weak
|
||||
wrapMode: Text.WrapAnywhere
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -359,6 +359,10 @@ Item {
|
||||
onBack: {
|
||||
rightContent.showHelpView()
|
||||
}
|
||||
|
||||
onBugReportWasSent: {
|
||||
rightContent.showAccount()
|
||||
}
|
||||
}
|
||||
|
||||
function showAccount(index) {
|
||||
|
||||
@ -41,7 +41,7 @@ SettingsView {
|
||||
actionIcon: "/qml/icons/ic-external-link.svg"
|
||||
description: qsTr("Get help setting up your client with our instructions and FAQs.")
|
||||
type: SettingsItem.PrimaryButton
|
||||
onClicked: {Qt.openUrlExternally("https://protonmail.com/support/categories/bridge/")}
|
||||
onClicked: {Qt.openUrlExternally("https://proton.me/support/mail")}
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
@ -98,7 +98,7 @@ ApplicationWindow {
|
||||
|
||||
property bool _showSetup: false
|
||||
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) {
|
||||
return 1
|
||||
}
|
||||
@ -112,7 +112,8 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
if (Backend.users.count === 1 && u.loggedIn === false) {
|
||||
return 1
|
||||
showSignIn(u.username)
|
||||
return 0
|
||||
}
|
||||
|
||||
if (contentLayout._showSetup) {
|
||||
|
||||
@ -289,7 +289,7 @@ QtObject {
|
||||
|
||||
property Notification updateForceError: Notification {
|
||||
title: qsTr("Bridge coudn’t 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
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
@ -997,7 +997,7 @@ QtObject {
|
||||
type: Notification.NotificationType.Danger
|
||||
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 {
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
// 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/>.
|
||||
|
||||
import QtQuick
|
||||
import QtQml
|
||||
|
||||
QtObject {
|
||||
@ -22,7 +23,7 @@ QtObject {
|
||||
property var prominent
|
||||
|
||||
// Primary
|
||||
property color primay_norm
|
||||
property color primary_norm
|
||||
|
||||
// Interaction-norm
|
||||
property color interaction_norm
|
||||
|
||||
@ -30,7 +30,7 @@ QtObject {
|
||||
// https://doc.qt.io/qt-5/qtqml-documents-definetypes.html#inline-components
|
||||
|
||||
// component ColorScheme: QtObject {
|
||||
// property color primay_norm
|
||||
// property color primary_norm
|
||||
// ...
|
||||
// }
|
||||
|
||||
@ -40,7 +40,7 @@ QtObject {
|
||||
prominent: lightProminentStyle
|
||||
|
||||
// Primary
|
||||
primay_norm: "#6D4AFF"
|
||||
primary_norm: "#6D4AFF"
|
||||
|
||||
// Interaction-norm
|
||||
interaction_norm: "#6D4AFF"
|
||||
@ -115,7 +115,7 @@ QtObject {
|
||||
prominent: this
|
||||
|
||||
// Primary
|
||||
primay_norm: "#8A6EFF"
|
||||
primary_norm: "#8A6EFF"
|
||||
|
||||
// Interaction-norm
|
||||
interaction_norm: "#6D4AFF"
|
||||
@ -190,7 +190,7 @@ QtObject {
|
||||
prominent: darkProminentStyle
|
||||
|
||||
// Primary
|
||||
primay_norm: "#8A6EFF"
|
||||
primary_norm: "#8A6EFF"
|
||||
|
||||
// Interaction-norm
|
||||
interaction_norm: "#6D4AFF"
|
||||
@ -265,7 +265,7 @@ QtObject {
|
||||
prominent: this
|
||||
|
||||
// Primary
|
||||
primay_norm: "#8A6EFF"
|
||||
primary_norm: "#8A6EFF"
|
||||
|
||||
// Interaction-norm
|
||||
interaction_norm: "#6D4AFF"
|
||||
|
||||
@ -44,6 +44,7 @@ Item {
|
||||
clip: true
|
||||
|
||||
anchors.fill: parent
|
||||
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds // Disable the springy effect when scroll reaches top/bottom.
|
||||
|
||||
Item {
|
||||
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)
|
||||
|
||||
@ -42,7 +42,7 @@ Item {
|
||||
property string name : "Apple Mail"
|
||||
property string iconSource : "/qml/icons/ic-apple-mail.svg"
|
||||
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 : {
|
||||
if (Backend.goos == "darwin") {
|
||||
@ -50,13 +50,13 @@ Item {
|
||||
"name" : "Apple Mail",
|
||||
"iconSource" : "/qml/icons/ic-apple-mail.svg",
|
||||
"haveAutoSetup" : true,
|
||||
"link" : "https://protonmail.com/bridge/applemail"
|
||||
"link" : "https://proton.me/support/protonmail-bridge-clients-apple-mail"
|
||||
})
|
||||
append({
|
||||
"name" : "Microsoft Outlook",
|
||||
"iconSource" : "/qml/icons/ic-microsoft-outlook.svg",
|
||||
"haveAutoSetup" : false,
|
||||
"link" : "https://protonmail.com/bridge/outlook2019-mac"
|
||||
"link" : "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019"
|
||||
})
|
||||
}
|
||||
if (Backend.goos == "windows") {
|
||||
@ -64,7 +64,7 @@ Item {
|
||||
"name" : "Microsoft Outlook",
|
||||
"iconSource" : "/qml/icons/ic-microsoft-outlook.svg",
|
||||
"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",
|
||||
"iconSource" : "/qml/icons/ic-mozilla-thunderbird.svg",
|
||||
"haveAutoSetup" : false,
|
||||
"link" : "https://protonmail.com/bridge/thunderbird"
|
||||
"link" : "https://proton.me/support/protonmail-bridge-clients-windows-thunderbird"
|
||||
})
|
||||
|
||||
append({
|
||||
"name" : "Other",
|
||||
"iconSource" : "/qml/icons/ic-other-mail-clients.svg",
|
||||
"haveAutoSetup" : false,
|
||||
"link" : "https://protonmail.com/bridge/clients"
|
||||
"link" : "https://proton.me/support/protonmail-bridge-configure-client"
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@ -288,7 +288,7 @@ FocusScope {
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
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.topMargin: 24
|
||||
type: Label.LabelType.Body
|
||||
|
||||
@ -43,10 +43,23 @@ Window {
|
||||
signal showSignIn(string username)
|
||||
signal quit()
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
function enableHoverOnOpenBridgeButton() {
|
||||
openBridgeButton.hoverEnabled = true
|
||||
mouseArea.positionChanged.disconnect(enableHoverOnOpenBridgeButton)
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) { // GODT-1479 restore the hover-able status that may have been disabled when clicking on the 'Open Bridge' button.
|
||||
openBridgeButton.hoverEnabled = true
|
||||
if (visible) { // GODT-1479 To avoid a visual glitch where the 'Open bridge button' would appear hovered when the status windows opens,
|
||||
// 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
|
||||
mouseArea.positionChanged.connect(enableHoverOnOpenBridgeButton)
|
||||
} else {
|
||||
menu.close()
|
||||
}
|
||||
|
||||
@ -84,11 +84,13 @@ QString userConfigDir()
|
||||
dir += "/Library/Application Support";
|
||||
#else
|
||||
dir = qgetenv ("XDG_CONFIG_HOME");
|
||||
if (dir.isEmpty())
|
||||
dir = qgetenv ("HOME");
|
||||
if (dir.isEmpty())
|
||||
{
|
||||
dir = qgetenv ("HOME");
|
||||
if (dir.isEmpty())
|
||||
throw Exception("neither $XDG_CONFIG_HOME nor $HOME are defined");
|
||||
dir += "/.config";
|
||||
}
|
||||
#endif
|
||||
QString const folder = QDir(dir).absoluteFilePath(configFolder);
|
||||
QDir().mkpath(folder);
|
||||
@ -115,11 +117,13 @@ QString userCacheDir()
|
||||
dir += "/Library/Caches";
|
||||
#else
|
||||
dir = qgetenv ("XDG_CACHE_HOME");
|
||||
if (dir.isEmpty())
|
||||
dir = qgetenv ("HOME");
|
||||
if (dir.isEmpty())
|
||||
{
|
||||
dir = qgetenv ("HOME");
|
||||
if (dir.isEmpty())
|
||||
throw Exception("neither XDG_CACHE_HOME nor $HOME are defined");
|
||||
dir += "/.cache";
|
||||
}
|
||||
#endif
|
||||
|
||||
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.
|
||||
//****************************************************************************************************************************************************
|
||||
|
||||
@ -29,6 +29,7 @@ namespace bridgepp
|
||||
|
||||
QString userConfigDir(); ///< Get the path of the user configuration 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).
|
||||
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.
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
#include "GRPCClient.h"
|
||||
#include "GRPCUtils.h"
|
||||
#include "../Exception/Exception.h"
|
||||
#include "../ProcessMonitor.h"
|
||||
|
||||
|
||||
using namespace google::protobuf;
|
||||
@ -56,9 +57,10 @@ void GRPCClient::removeServiceConfigFile()
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \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.
|
||||
//****************************************************************************************************************************************************
|
||||
GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs)
|
||||
GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs, ProcessMonitor *serverProcess)
|
||||
{
|
||||
QString const path = grpcServerConfigPath();
|
||||
QFile file(path);
|
||||
@ -68,6 +70,9 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs)
|
||||
bool found = false;
|
||||
while (true)
|
||||
{
|
||||
if (serverProcess && serverProcess->getStatus().ended)
|
||||
throw Exception("Bridge application exited before providing a gRPC service configuration file.");
|
||||
|
||||
if (file.exists())
|
||||
{
|
||||
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[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.
|
||||
//****************************************************************************************************************************************************
|
||||
bool GRPCClient::connectToServer(GRPCConfig const &config, QString &outError)
|
||||
bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serverProcess, QString &outError)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -123,7 +129,10 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, QString &outError)
|
||||
int i = 0;
|
||||
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))))
|
||||
break; // connection established.
|
||||
@ -135,13 +144,13 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, QString &outError)
|
||||
if (channel_->GetState(true) != GRPC_CHANNEL_READY)
|
||||
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 clientConfigPath = createClientConfigFile(clientToken);
|
||||
if (clientConfigPath.isEmpty())
|
||||
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;
|
||||
grpc::Status status = this->checkTokens(QDir::toNativeSeparators(clientConfigPath), returnedClientToken);
|
||||
@ -153,7 +162,7 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, QString &outError)
|
||||
if (!status.ok())
|
||||
throw Exception(QString::fromStdString(status.error_message()));
|
||||
|
||||
log_->debug("gRPC token was validated");
|
||||
log_->info("gRPC token was validated");
|
||||
|
||||
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] callName The call name.
|
||||
|
||||
@ -50,7 +50,7 @@ class GRPCClient : public QObject
|
||||
Q_OBJECT
|
||||
public: // static member functions
|
||||
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.
|
||||
GRPCClient() = default; ///< Default constructor.
|
||||
@ -60,7 +60,7 @@ public: // member functions.
|
||||
GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator.
|
||||
GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator.
|
||||
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 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 logDebug(QString const &message); ///< Log a debug 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 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.
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
|
||||
|
||||
#include "Log.h"
|
||||
#include "../Exception/Exception.h"
|
||||
|
||||
|
||||
namespace bridgepp
|
||||
@ -29,7 +30,7 @@ namespace
|
||||
Log *qtHandlerLog { nullptr }; ///< The log instance handling qt logs.
|
||||
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 {
|
||||
{ Log::Level::Panic, "panic", },
|
||||
{ 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] message The log entry message.
|
||||
/// \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.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -273,17 +303,29 @@ void Log::trace(QString const &message)
|
||||
//****************************************************************************************************************************************************
|
||||
void Log::addEntry(Log::Level level, QString const &message)
|
||||
{
|
||||
QDateTime const dateTime = QDateTime::currentDateTime();
|
||||
QMutexLocker locker(&mutex_);
|
||||
if (qint32(level) > qint32(level_))
|
||||
return;
|
||||
|
||||
emit entryAdded(level, message);
|
||||
|
||||
if (!(echoInConsole_ || file_))
|
||||
return;
|
||||
|
||||
QString const entryStr = logEntryToString(level, dateTime, message) + "\n";
|
||||
if (echoInConsole_)
|
||||
{
|
||||
QTextStream &stream = (qint32(level) <= (qint32(Level::Warn))) ? stderr_ : stdout_;
|
||||
stream << logEntryToString(level, message) << "\n";
|
||||
stream << entryStr;
|
||||
stream.flush();
|
||||
}
|
||||
|
||||
if (file_)
|
||||
{
|
||||
file_->write(entryStr.toLocal8Bit());
|
||||
file_->flush();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@ public: // data types.
|
||||
};
|
||||
|
||||
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 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.
|
||||
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 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.
|
||||
|
||||
public slots:
|
||||
@ -83,7 +85,7 @@ private: // data members
|
||||
mutable QMutex mutex_; ///< The mutex.
|
||||
Level level_ { defaultLevel }; ///< The log level
|
||||
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 stderr_; ///< The stderr stream.
|
||||
};
|
||||
|
||||
@ -33,6 +33,8 @@ ProcessMonitor::ProcessMonitor(QString const &exePath, QStringList const &args,
|
||||
: Worker(parent)
|
||||
, exePath_(exePath)
|
||||
, args_(args)
|
||||
, out_(stdout)
|
||||
, err_(stderr)
|
||||
{
|
||||
QFileInfo fileInfo(exePath);
|
||||
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
|
||||
{
|
||||
{
|
||||
QMutexLocker locker(&statusMutex_);
|
||||
status_.ended = false;
|
||||
status_.pid = -1;
|
||||
}
|
||||
|
||||
emit started();
|
||||
|
||||
QProcess p;
|
||||
p.start(exePath_, args_);
|
||||
p.waitForStarted();
|
||||
|
||||
status_.running = true;
|
||||
status_.pid = p.processId();
|
||||
|
||||
QTextStream out(stdout), err(stderr);
|
||||
QByteArray array;
|
||||
while (!p.waitForFinished(100))
|
||||
{
|
||||
array = p.readAllStandardError();
|
||||
if (!array.isEmpty())
|
||||
{
|
||||
err << array;
|
||||
err.flush();
|
||||
}
|
||||
|
||||
array = p.readAllStandardOutput();
|
||||
if (!array.isEmpty())
|
||||
{
|
||||
out << array;
|
||||
out.flush();
|
||||
}
|
||||
QMutexLocker locker(&statusMutex_);
|
||||
status_.pid = p.processId();
|
||||
}
|
||||
|
||||
status_.running = false;
|
||||
while (!p.waitForFinished(100))
|
||||
{
|
||||
this->forwardProcessOutput(p);
|
||||
}
|
||||
this->forwardProcessOutput(p);
|
||||
|
||||
QMutexLocker locker(&statusMutex_);
|
||||
status_.ended = true;
|
||||
status_.returnCode = p.exitCode();
|
||||
|
||||
emit processExited(status_.returnCode);
|
||||
@ -93,8 +111,9 @@ void ProcessMonitor::run()
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return status of the monitored process
|
||||
//****************************************************************************************************************************************************
|
||||
const ProcessMonitor::MonitorStatus &ProcessMonitor::getStatus()
|
||||
const ProcessMonitor::MonitorStatus ProcessMonitor::getStatus()
|
||||
{
|
||||
QMutexLocker locker(&statusMutex_);
|
||||
return status_;
|
||||
}
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ Q_OBJECT
|
||||
public: // static member functions
|
||||
struct MonitorStatus
|
||||
{
|
||||
bool running = false;
|
||||
bool ended = false;
|
||||
int returnCode = 0;
|
||||
qint64 pid = 0;
|
||||
};
|
||||
@ -49,15 +49,21 @@ public: // member functions.
|
||||
ProcessMonitor &operator=(ProcessMonitor const &) = delete; ///< Disabled assignment operator.
|
||||
ProcessMonitor &operator=(ProcessMonitor &&) = delete; ///< Disabled move assignment operator.
|
||||
void run() override; ///< Run the worker.
|
||||
MonitorStatus const &getStatus();
|
||||
MonitorStatus const getStatus(); ///< Retrieve the current status of the process.
|
||||
|
||||
signals:
|
||||
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
|
||||
QMutex statusMutex_; ///< The status mutex.
|
||||
QString const exePath_; ///< The path to the executable.
|
||||
QStringList args_; ///< arguments to be passed to the brigde.
|
||||
MonitorStatus status_; ///< Status of the monitoring.
|
||||
QTextStream out_; ///< The standard output stream.
|
||||
QTextStream err_; ///< The standard error stream.
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Type describes the available types of frontend.
|
||||
@ -34,6 +35,7 @@ const (
|
||||
CLI Type = iota
|
||||
GRPC
|
||||
NonInteractive
|
||||
Unknown
|
||||
)
|
||||
|
||||
type Frontend interface {
|
||||
@ -45,7 +47,7 @@ type Frontend interface {
|
||||
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(
|
||||
frontendType Type,
|
||||
showWindowOnStart bool,
|
||||
@ -54,6 +56,7 @@ func New(
|
||||
updater types.Updater,
|
||||
restarter types.Restarter,
|
||||
locations *locations.Locations,
|
||||
parentPID int,
|
||||
) Frontend {
|
||||
switch frontendType {
|
||||
case GRPC:
|
||||
@ -64,6 +67,7 @@ func New(
|
||||
updater,
|
||||
restarter,
|
||||
locations,
|
||||
parentPID,
|
||||
)
|
||||
|
||||
case CLI:
|
||||
@ -75,9 +79,13 @@ func New(
|
||||
)
|
||||
|
||||
case NonInteractive:
|
||||
return nil
|
||||
|
||||
case Unknown:
|
||||
fallthrough
|
||||
|
||||
default:
|
||||
return nil
|
||||
logrus.Panicf("Unexpected frontend value %v", frontendType)
|
||||
return nil // return statement is required by compiler, although the above call will panic.
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,6 +40,9 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/keychain"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||
"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/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -59,12 +62,13 @@ const (
|
||||
// Service is the RPC service struct.
|
||||
type Service struct { // nolint:structcheck
|
||||
UnimplementedBridgeServer
|
||||
grpcServer *grpc.Server // the gGRPC server
|
||||
listener net.Listener
|
||||
eventStreamCh chan *StreamEvent
|
||||
eventStreamDoneCh chan struct{}
|
||||
eventQueue []*StreamEvent
|
||||
eventQueueMutex sync.Mutex
|
||||
grpcServer *grpc.Server // the gGRPC server
|
||||
listener net.Listener
|
||||
eventStreamCh chan *StreamEvent
|
||||
eventStreamChMutex sync.RWMutex
|
||||
eventStreamDoneCh chan struct{}
|
||||
eventQueue []*StreamEvent
|
||||
eventQueueMutex sync.Mutex
|
||||
|
||||
panicHandler types.PanicHandler
|
||||
eventListener listener.Listener
|
||||
@ -84,6 +88,8 @@ type Service struct { // nolint:structcheck
|
||||
locations *locations.Locations
|
||||
token string
|
||||
pemCert string
|
||||
parentPID int
|
||||
parentPIDDoneCh chan struct{}
|
||||
}
|
||||
|
||||
// NewService returns a new instance of the service.
|
||||
@ -94,6 +100,7 @@ func NewService(
|
||||
updater types.Updater,
|
||||
restarter types.Restarter,
|
||||
locations *locations.Locations,
|
||||
parentPID int,
|
||||
) *Service {
|
||||
s := Service{
|
||||
UnimplementedBridgeServer: UnimplementedBridgeServer{},
|
||||
@ -109,6 +116,8 @@ func NewService(
|
||||
firstTimeAutostart: sync.Once{},
|
||||
locations: locations,
|
||||
token: uuid.NewString(),
|
||||
parentPID: parentPID,
|
||||
parentPIDDoneCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Initializing.Done is only called sync.Once. Please keep the increment
|
||||
@ -124,6 +133,7 @@ func NewService(
|
||||
}
|
||||
|
||||
func (s *Service) startGRPCServer() {
|
||||
s.log.Info("Starting gRPC server")
|
||||
tlsConfig, pemCert, err := s.generateTLSConfig()
|
||||
if err != nil {
|
||||
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 {
|
||||
s.log.WithError(err).WithField("path", path).Panic("Could not write gRPC service config file")
|
||||
} 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())
|
||||
@ -175,6 +185,12 @@ func (s *Service) Loop(b types.Bridger) error {
|
||||
s.initAutostart()
|
||||
s.startGRPCServer()
|
||||
|
||||
if s.parentPID < 0 {
|
||||
s.log.Info("Not monitoring parent PID")
|
||||
} else {
|
||||
go s.monitorParentPID()
|
||||
}
|
||||
|
||||
defer func() {
|
||||
s.bridge.SetBool(settings.FirstStartGUIKey, false)
|
||||
}()
|
||||
@ -518,3 +534,36 @@ func (s *Service) validateStreamServerToken(
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,12 +95,19 @@ func (s *Service) GuiReady(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empt
|
||||
// Quit implement the Quit gRPC service call.
|
||||
func (s *Service) Quit(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
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.
|
||||
go func() {
|
||||
var err error
|
||||
if s.eventStreamCh != nil {
|
||||
if _, err = s.StopEventStream(ctx, empty); err != nil {
|
||||
if s.parentPID >= 0 {
|
||||
s.parentPIDDoneCh <- struct{}{}
|
||||
}
|
||||
|
||||
if s.isStreamingEvents() {
|
||||
if err := s.stopEventStream(); err != nil {
|
||||
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.
|
||||
s.grpcServer.GracefulStop()
|
||||
}()
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// Restart implement the Restart gRPC service call.
|
||||
|
||||
@ -29,19 +29,19 @@ import (
|
||||
func (s *Service) RunEventStream(request *EventStreamRequest, server Bridge_RunEventStreamServer) error {
|
||||
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.
|
||||
}
|
||||
|
||||
s.bridge.SetCurrentPlatform(request.ClientPlatform)
|
||||
|
||||
s.eventStreamCh = make(chan *StreamEvent)
|
||||
s.createEventStreamChannel()
|
||||
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?
|
||||
defer func() {
|
||||
close(s.eventStreamCh)
|
||||
s.eventStreamCh = nil
|
||||
s.deleteEventStreamChannel()
|
||||
close(s.eventStreamDoneCh)
|
||||
s.eventStreamDoneCh = nil
|
||||
}()
|
||||
@ -70,24 +70,35 @@ func (s *Service) RunEventStream(request *EventStreamRequest, server Bridge_RunE
|
||||
s.log.Debug("Stop Event stream")
|
||||
return err
|
||||
}
|
||||
case <-server.Context().Done():
|
||||
s.log.Info("Client closed the stream, initiating shutdown")
|
||||
s.quit()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, status.Errorf(codes.NotFound, "The service is not streaming")
|
||||
return status.Errorf(codes.NotFound, "The service is not streaming")
|
||||
}
|
||||
|
||||
s.eventStreamDoneCh <- struct{}{}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendEvent sends an event to the via the gRPC event stream.
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
@ -174,3 +185,24 @@ func (s *Service) queueEvent(event *StreamEvent) {
|
||||
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
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ func newGoIMAPServer(tls *tls.Config, backend backend.Backend, address string, u
|
||||
serverID := imapid.ID{
|
||||
imapid.FieldName: "Proton Mail Bridge",
|
||||
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 {
|
||||
|
||||
@ -101,3 +101,7 @@ func getLogName(version, revision string) string {
|
||||
func MatchLogName(name string) bool {
|
||||
return regexp.MustCompile(`^v.*\.log$`).MatchString(name)
|
||||
}
|
||||
|
||||
func MatchGUILogName(name string) bool {
|
||||
return regexp.MustCompile(`^gui_v.*\.log$`).MatchString(name)
|
||||
}
|
||||
|
||||
@ -91,7 +91,7 @@ func (c *controller) ListenAndServe() {
|
||||
}
|
||||
|
||||
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())
|
||||
return
|
||||
}
|
||||
|
||||
@ -153,7 +153,7 @@ func (q *sendRecorder) deleteExpiredKeys() {
|
||||
for key, value := range q.hashes {
|
||||
// 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 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.
|
||||
// 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 {
|
||||
|
||||
@ -245,7 +245,7 @@ func (store *Store) createOrUpdateMessageEvent(msg *pmapi.Message) error {
|
||||
func (store *Store) createOrUpdateMessagesEvent(msgs []*pmapi.Message) error { //nolint:funlen
|
||||
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 {
|
||||
b := tx.Bucket(metadataBucket)
|
||||
for _, msg := range msgs {
|
||||
@ -321,7 +321,7 @@ func clearNonMetadata(onlyMeta *pmapi.Message) {
|
||||
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
|
||||
// not changed if already set. To change these:
|
||||
// * size must be updated by Message.SetSize
|
||||
|
||||
@ -20,4 +20,4 @@
|
||||
|
||||
package updater
|
||||
|
||||
const Host = "https://protonmail.com/download"
|
||||
const Host = "https://proton.me/download"
|
||||
|
||||
@ -38,7 +38,7 @@ type VersionInfo struct {
|
||||
// Installers are the locations of installer files (for manual installation).
|
||||
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
|
||||
|
||||
// ReleaseNotesPage is the address of the page containing the release notes.
|
||||
@ -54,26 +54,26 @@ type VersionInfo struct {
|
||||
// {
|
||||
// "stable": {
|
||||
// "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": [
|
||||
// "https://protonmail.com/.../something.deb",
|
||||
// "https://protonmail.com/.../something.rpm",
|
||||
// "https://protonmail.com/.../PKGBUILD"
|
||||
// "https://proton.me/.../something.deb",
|
||||
// "https://proton.me/.../something.rpm",
|
||||
// "https://proton.me/.../PKGBUILD"
|
||||
// ],
|
||||
// "LandingPage": "https://protonmail.com/bridge",
|
||||
// "ReleaseNotesPage": "https://protonmail.com/.../release_notes.html",
|
||||
// "LandingPage": "https://proton.me/mail/bridge#download",
|
||||
// "ReleaseNotesPage": "https://proton.me/download/{ie,bridge}/{stable,early}_releases.html",
|
||||
// "RolloutProportion": 0.5
|
||||
// },
|
||||
// "early": {
|
||||
// "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": [
|
||||
// "https://protonmail.com/.../something.deb",
|
||||
// "https://protonmail.com/.../something.rpm",
|
||||
// "https://protonmail.com/.../PKGBUILD"
|
||||
// "https://proton.me/.../something.deb",
|
||||
// "https://proton.me/.../something.rpm",
|
||||
// "https://proton.me/.../PKGBUILD"
|
||||
// ],
|
||||
// "LandingPage": "https://protonmail.com/bridge",
|
||||
// "ReleaseNotesPage": "https://protonmail.com/.../release_notes.html",
|
||||
// "LandingPage": "https://proton.me/mail/bridge#download",
|
||||
// "ReleaseNotesPage": "https://proton.me/download/{ie,bridge}/{stable,early}_releases.html",
|
||||
// "RolloutProportion": 0.5
|
||||
// },
|
||||
// "...": {
|
||||
@ -84,8 +84,8 @@ type VersionMap map[string]VersionInfo
|
||||
|
||||
// getVersionFileURL returns the URL of the version file.
|
||||
// For example:
|
||||
// - https://protonmail.com/download/bridge/version_linux.json
|
||||
// - https://protonmail.com/download/ie/version_linux.json
|
||||
// - https://proton.me/download/bridge/version_linux.json
|
||||
// - https://proton.me/download/ie/version_linux.json
|
||||
func (u *Updater) getVersionFileURL() string {
|
||||
return fmt.Sprintf("%v/%v/version_%v.json", Host, u.updateURLName, u.platform)
|
||||
}
|
||||
|
||||
@ -17,6 +17,12 @@
|
||||
|
||||
package versioner
|
||||
|
||||
import "strings"
|
||||
|
||||
func getExeName(name string) string {
|
||||
if strings.HasSuffix(name, ".exe") {
|
||||
return name
|
||||
}
|
||||
|
||||
return name + ".exe"
|
||||
}
|
||||
|
||||
@ -33,10 +33,16 @@ var TrustedAPIPins = []string{ //nolint:gochecknoglobals
|
||||
`pin-sha256="AfMENBVvOS8MnISprtvyPsjKlPooqh8nMB/pvCrpJpw="`, // cold backup
|
||||
|
||||
// protonmail.com
|
||||
// \todo remove when sure no one is using it.
|
||||
`pin-sha256="8joiNBdqaYiQpKskgtkJsqRxF7zN0C0aqfi8DacknnI="`, // current
|
||||
`pin-sha256="JMI8yrbc6jB1FYGyyWRLFTmDNgIszrNEMGlgy972e7w="`, // hot 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
|
||||
`pin-sha256="EU6TS9MO0L/GsDHvVc9D5fChYLNy5JdGYpJw0ccgetM="`, // main
|
||||
`pin-sha256="iKPIHPnDNqdkvOnTClQ8zQAIKG0XavaPkcEo0LBAABA="`, // backup 1
|
||||
|
||||
@ -88,7 +88,7 @@ func TestTLSSignedCertTrustedPublicKey(t *testing.T) {
|
||||
|
||||
_, dialer, _ := createClientWithPinningDialer("")
|
||||
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")
|
||||
r.NoError(t, err, "expected dial to succeed because public key is known and cert is signed by CA")
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ func newManager(cfg Config) *manager {
|
||||
// The resty is increasing the delay between retries up to 1 minute
|
||||
// (SetRetryMaxWaitTime) so for 10 retries the cumulative delay can be
|
||||
// up to 5min.
|
||||
m.rc.SetRetryCount(5)
|
||||
m.rc.SetRetryCount(3)
|
||||
m.rc.SetRetryMaxWaitTime(time.Minute)
|
||||
m.rc.SetRetryAfter(catchRetryAfter)
|
||||
m.rc.AddRetryCondition(m.shouldRetry)
|
||||
|
||||
@ -62,6 +62,10 @@ func (m *manager) NewClientWithLogin(ctx context.Context, username string, passw
|
||||
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{
|
||||
Username: username,
|
||||
ClientProof: base64.StdEncoding.EncodeToString(proofs.ClientProof),
|
||||
|
||||
@ -105,17 +105,27 @@ func logConnReuse(_ *resty.Client, res *resty.Response) error {
|
||||
func catchRetryAfter(_ *resty.Client, res *resty.Response) (time.Duration, error) {
|
||||
if res.StatusCode() == http.StatusTooManyRequests {
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
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())
|
||||
return time.Duration(seconds) * time.Second, nil
|
||||
// Maximum retry time in client is is one minute. But
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
export GO111MODULE=on
|
||||
export BRIDGE_VERSION:=2.4.3+integrationtests
|
||||
export BRIDGE_VERSION:=2.4.8+integrationtests
|
||||
export VERBOSITY?=fatal
|
||||
export TEST_DATA=testdata
|
||||
|
||||
@ -14,6 +14,7 @@ check-godog:
|
||||
@which godog || $(MAKE) install-godog
|
||||
install-godog: check-go
|
||||
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-bridge: FEATURES ?= features
|
||||
@ -25,7 +26,11 @@ test-bridge: check-godog
|
||||
test-live: test-live-bridge test-live-ie
|
||||
test-live-bridge: FEATURES ?= features
|
||||
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!
|
||||
# Provide TEST_ACCOUNTS with your accounts.
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
@ -29,6 +30,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/users"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"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")
|
||||
}
|
||||
|
||||
ctx.addCleanupChecked(func() error {
|
||||
return ctx.bridge.LogoutUser(userID)
|
||||
}, "Logging out user")
|
||||
ctx.addCleanupChecked(
|
||||
func() error {
|
||||
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
|
||||
}
|
||||
|
||||
@ -9,11 +9,13 @@ Feature: IMAP auth
|
||||
When IMAP client authenticates "user" with bad password
|
||||
Then IMAP response is "IMAP error: NO backend/credentials: incorrect password"
|
||||
|
||||
@ignore-live-auth
|
||||
Scenario: Authenticates with disconnected user
|
||||
Given there is disconnected user "user"
|
||||
When IMAP client authenticates "user"
|
||||
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
|
||||
Given there is user "user" which just logged in
|
||||
And there is no internet connection
|
||||
@ -27,12 +29,14 @@ Feature: IMAP auth
|
||||
And 2 seconds pass
|
||||
Then "user" is connected
|
||||
|
||||
@ignore-live-auth
|
||||
Scenario: Authenticates with freshly logged-out user
|
||||
Given there is user "user" which just logged in
|
||||
When "user" logs out
|
||||
And IMAP client authenticates "user"
|
||||
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
|
||||
Given there is user "user" which just logged in
|
||||
When "user" logs out
|
||||
|
||||
@ -125,6 +125,7 @@ Feature: IMAP fetch messages
|
||||
| 1:* |
|
||||
| * |
|
||||
|
||||
@ignore-live
|
||||
Scenario: Fetch of big mailbox
|
||||
Given there are 100 messages in mailbox "Folders/mbox" for "user"
|
||||
And there is IMAP client logged in as "user"
|
||||
|
||||
@ -1,34 +1,43 @@
|
||||
Feature: Delete user
|
||||
@ignore-live-auth
|
||||
Scenario: Deleting connected user
|
||||
Given there is user "user" which just logged in
|
||||
When user deletes "user"
|
||||
Then last response is "OK"
|
||||
And "user" has database file
|
||||
|
||||
@ignore-live-auth
|
||||
Scenario: Deleting connected user with cache
|
||||
Given there is user "user" which just logged in
|
||||
When user deletes "user" with cache
|
||||
Then last response is "OK"
|
||||
And "user" does not have database file
|
||||
|
||||
@ignore-live-auth
|
||||
Scenario: Deleting connected user without database file
|
||||
Given there is user "user" which just logged in
|
||||
And there is no database file for "user"
|
||||
When user deletes "user" with cache
|
||||
Then last response is "OK"
|
||||
|
||||
# THIS IS BLOCKED BY ANTI-ABUSE
|
||||
@ignore-live
|
||||
Scenario: Deleting disconnected user
|
||||
Given there is disconnected user "user"
|
||||
When user deletes "user"
|
||||
Then last response is "OK"
|
||||
And "user" has database file
|
||||
|
||||
# THIS IS BLOCKED BY ANTI-ABUSE
|
||||
@ignore-live
|
||||
Scenario: Deleting disconnected user with cache
|
||||
Given there is disconnected user "user"
|
||||
When user deletes "user" with cache
|
||||
Then last response is "OK"
|
||||
And "user" does not have database file
|
||||
|
||||
# THIS IS BLOCKED BY ANTI-ABUSE
|
||||
@ignore-live
|
||||
Scenario: Deleting disconnected user without database file
|
||||
Given there is disconnected user "user"
|
||||
And there is no database file for "user"
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
Feature: Login for the first time
|
||||
@ignore-live-auth
|
||||
Scenario: Normal login
|
||||
Given there is user "user"
|
||||
When "user" logs in
|
||||
@ -19,6 +20,7 @@ Feature: Login for the first time
|
||||
When "user" logs in with bad password
|
||||
Then last response is "failed to login: Incorrect login credentials. Please try again"
|
||||
|
||||
@ignore-live-auth
|
||||
Scenario: Login without internet connection
|
||||
Given there is no internet connection
|
||||
When "user" logs in
|
||||
@ -34,6 +36,7 @@ Feature: Login for the first time
|
||||
And "user2fa" has running event loop
|
||||
And "user2fa" has non-zero space
|
||||
|
||||
@ignore-live-auth
|
||||
Scenario: Login user with capital letters in address
|
||||
Given there is user "userAddressWithCapitalLetter"
|
||||
When "userAddressWithCapitalLetter" logs in
|
||||
@ -43,6 +46,7 @@ Feature: Login for the first time
|
||||
And "userAddressWithCapitalLetter" has running event loop
|
||||
And "userAddressWithCapitalLetter" has non-zero space
|
||||
|
||||
@ignore-live-auth
|
||||
Scenario: Login user with more addresses
|
||||
Given there is user "userMoreAddresses"
|
||||
When "userMoreAddresses" logs in
|
||||
@ -62,6 +66,7 @@ Feature: Login for the first time
|
||||
And "userDisabledPrimaryAddress" has running event loop
|
||||
And "userDisabledPrimaryAddress" has non-zero space
|
||||
|
||||
@ignore-live-auth
|
||||
Scenario: Login two users
|
||||
Given there is user "user"
|
||||
And there is user "userMoreAddresses"
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
Feature: Re-login
|
||||
@ignore-live-auth
|
||||
Scenario: Re-login with connected user and database file
|
||||
Given there is user "user" which just logged in
|
||||
And there is database file for "user"
|
||||
@ -18,6 +19,7 @@ Feature: Re-login
|
||||
And "user" has database file
|
||||
And "user" has running event loop
|
||||
|
||||
@ignore-live-auth
|
||||
Scenario: Re-login with disconnected user and database file
|
||||
Given there is disconnected user "user"
|
||||
And there is database file for "user"
|
||||
@ -27,6 +29,7 @@ Feature: Re-login
|
||||
And "user" has running event loop
|
||||
And "user" has non-zero space
|
||||
|
||||
@ignore-live-auth
|
||||
Scenario: Re-login with disconnected user and no database file
|
||||
Given there is disconnected user "user"
|
||||
And there is no database file for "user"
|
||||
|
||||
@ -1,11 +1,21 @@
|
||||
---
|
||||
|
||||
wait: true
|
||||
strict: true
|
||||
|
||||
file:
|
||||
name: "./gobinsec-cache.yml"
|
||||
expiration: 24h
|
||||
expiration: "24h"
|
||||
|
||||
ignore:
|
||||
# golang.org/x/net wrong match, we are using 2871e0cb, fixed by 37e1c6af
|
||||
- "CVE-2021-33194"
|
||||
# golang.org/x/net wrong match, we are using v0.1.0, fixed by 37e1c6af in v0.0.xxx
|
||||
- "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"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
##!/bin/bash
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright (c) 2022 Proton AG
|
||||
#
|
||||
|
||||
@ -7,9 +7,9 @@ require github.com/intercloud/gobinsec v0.10.2
|
||||
require (
|
||||
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/memcachier/gomemcache v0.0.0-20170425125614-d027381f7653 // indirect
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@ -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.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
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.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.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/go.mod h1:KoYVbOQexD45AOLfn+gsFB6c3o4ANzP1QKzjE6tZbK0=
|
||||
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-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-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/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/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=
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
|
||||
|
||||
# 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
|
||||
OUTFILE=${INFILE//.md/.html}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user