mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
GODT-1779: Remove go-imap
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,6 +6,7 @@
|
||||
.*.sw?
|
||||
*~
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# Test files
|
||||
godog.test
|
||||
|
||||
@ -18,17 +18,22 @@
|
||||
---
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:go18
|
||||
|
||||
variables:
|
||||
GOPRIVATE: gitlab.protontech.ch
|
||||
|
||||
before_script:
|
||||
- eval $(ssh-agent -s)
|
||||
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
|
||||
- mkdir -p .cache/bin
|
||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||
- git config --global --unset-all url.git@gitlab.protontech.ch:.insteadOf
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}/
|
||||
- make install-dev-dependencies
|
||||
- git checkout .
|
||||
|
||||
cache:
|
||||
key: go18-mod
|
||||
key: go18-gluon
|
||||
paths:
|
||||
- .cache
|
||||
policy: pull
|
||||
@ -76,7 +81,7 @@ cache-push:
|
||||
script:
|
||||
- echo ""
|
||||
cache:
|
||||
key: go18-mod
|
||||
key: go18-gluon
|
||||
paths:
|
||||
- .cache
|
||||
|
||||
|
||||
8
Makefile
8
Makefile
@ -234,12 +234,8 @@ integration-test-bridge:
|
||||
${MAKE} -C test test-bridge
|
||||
|
||||
mocks:
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/users Locator,PanicHandler,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/pkg/listener Listener > internal/users/mocks/listener_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/store PanicHandler,BridgeUser,ChangeNotifier,Storer > internal/store/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/pkg/listener Listener > internal/store/mocks/utils_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/pkg/pmapi Client,Manager > pkg/pmapi/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/pkg/message Fetcher > pkg/message/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/bridge TLSReporter,ProxyDialer,Autostarter > internal/bridge/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
|
||||
|
||||
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog
|
||||
|
||||
|
||||
@ -17,6 +17,13 @@
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/app"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
/*
|
||||
___....___
|
||||
^^ __..-:'':__:..:__:'':-..__
|
||||
@ -34,41 +41,8 @@ package main
|
||||
~~^_~^~/ \~^-~^~ _~^-~_^~-^~_^~~-^~_~^~-~_~-^~_^/ \~^ ~~_ ^
|
||||
*/
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/app/base"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/app/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
||||
configName = "bridge"
|
||||
updateURLName = "bridge"
|
||||
keychainName = "bridge"
|
||||
cacheVersion = "c11"
|
||||
)
|
||||
|
||||
func main() {
|
||||
base, err := base.New(
|
||||
constants.FullAppName,
|
||||
appUsage,
|
||||
configName,
|
||||
updateURLName,
|
||||
keychainName,
|
||||
cacheVersion,
|
||||
)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to create app base")
|
||||
}
|
||||
// Other instance already running.
|
||||
if base == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := bridge.New(base).Run(os.Args); err != nil {
|
||||
logrus.WithError(err).Fatal("Bridge exited with error")
|
||||
if err := app.New().Run(os.Args); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,13 +26,13 @@ import (
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/crash"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/versioner"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/elastic/go-sysinfo"
|
||||
@ -43,10 +43,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
appName = "Proton Mail Launcher"
|
||||
configName = "bridge"
|
||||
exeName = "bridge"
|
||||
guiName = "bridge-gui"
|
||||
appName = "Proton Mail Launcher"
|
||||
exeName = "bridge"
|
||||
guiName = "bridge-gui"
|
||||
|
||||
FlagCLI = "--cli"
|
||||
FlagCLIShort = "-c"
|
||||
@ -62,12 +61,12 @@ func main() { //nolint:funlen
|
||||
crashHandler := crash.NewHandler(reporter.ReportException)
|
||||
defer crashHandler.HandlePanic()
|
||||
|
||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
|
||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, constants.ConfigName))
|
||||
if err != nil {
|
||||
l.WithError(err).Fatal("Failed to get locations provider")
|
||||
}
|
||||
|
||||
locations := locations.New(locationsProvider, configName)
|
||||
locations := locations.New(locationsProvider, constants.ConfigName)
|
||||
|
||||
logsPath, err := locations.ProvideLogsPath()
|
||||
if err != nil {
|
||||
@ -75,12 +74,10 @@ func main() { //nolint:funlen
|
||||
}
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
|
||||
if err := logging.Init(logsPath); err != nil {
|
||||
l.WithError(err).Fatal("Failed to setup logging")
|
||||
if err := logging.Init(logsPath, os.Getenv("VERBOSITY")); err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to setup logging")
|
||||
}
|
||||
|
||||
logging.SetLevel(os.Getenv("VERBOSITY"))
|
||||
|
||||
updatesPath, err := locations.ProvideUpdatesPath()
|
||||
if err != nil {
|
||||
l.WithError(err).Fatal("Failed to get updates path")
|
||||
@ -137,6 +134,7 @@ func main() { //nolint:funlen
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
// On windows, if you use Run(), a terminal stays open; we don't want that.
|
||||
if //goland:noinspection GoBoolExpressions
|
||||
|
||||
66
go.mod
66
go.mod
@ -5,30 +5,22 @@ go 1.18
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||
github.com/Masterminds/semver/v3 v3.1.1
|
||||
github.com/ProtonMail/gluon v0.11.1-0.20220922143913-ef3617264557
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895
|
||||
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
|
||||
github.com/ProtonMail/go-rfc5322 v0.11.0
|
||||
github.com/ProtonMail/go-srp v0.0.5
|
||||
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10
|
||||
github.com/PuerkitoBio/goquery v1.8.0
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
||||
github.com/bradenaw/juniper v0.8.0
|
||||
github.com/cucumber/godog v0.12.5
|
||||
github.com/cucumber/messages-go/v16 v16.0.1
|
||||
github.com/docker/docker-credential-helpers v0.6.4
|
||||
github.com/docker/docker-credential-helpers v0.6.3
|
||||
github.com/elastic/go-sysinfo v1.8.1
|
||||
github.com/emersion/go-imap v1.2.1
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20210907172056-e3baed77bbe4
|
||||
github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872
|
||||
github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20210907172115-4c2c4843bf69
|
||||
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317
|
||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
|
||||
github.com/emersion/go-message v0.16.0
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
|
||||
github.com/emersion/go-smtp v0.15.0
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/getsentry/sentry-go v0.13.0
|
||||
github.com/go-resty/resty/v2 v2.7.0
|
||||
@ -38,17 +30,14 @@ require (
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba
|
||||
github.com/keybase/go-keychain v0.0.0-20220610143837-c2ce06069005
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||
github.com/keybase/go-keychain v0.0.0
|
||||
github.com/miekg/dns v1.1.50
|
||||
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285
|
||||
github.com/pkg/profile v1.6.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/urfave/cli/v2 v2.16.3
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
gitlab.protontech.ch/go/liteapi v0.30.0
|
||||
golang.org/x/exp v0.0.0-20220921164117-439092de6870
|
||||
golang.org/x/net v0.1.0
|
||||
golang.org/x/sys v0.1.0
|
||||
@ -59,11 +48,19 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.7.0 // indirect
|
||||
entgo.io/ent v0.11.2 // indirect
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f // indirect
|
||||
github.com/ProtonMail/go-srp v0.0.5 // indirect
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chzyer/test v1.0.0 // indirect
|
||||
github.com/cloudflare/circl v1.2.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
@ -71,32 +68,61 @@ require (
|
||||
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
|
||||
github.com/danieljoos/wincred v1.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/elastic/go-windows v1.0.1 // indirect
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
||||
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a // indirect
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.8.1 // indirect
|
||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.0 // indirect
|
||||
github.com/goccy/go-json v0.9.11 // indirect
|
||||
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/flatbuffers v2.0.8+incompatible // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-memdb v1.3.3 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.14.0 // indirect
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.15.9 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.2 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
github.com/zclconf/go-cty v1.11.0 // indirect
|
||||
go.opencensus.io v0.23.0 // 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
|
||||
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 // indirect
|
||||
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa // indirect
|
||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
|
||||
210
go.sum
210
go.sum
@ -1,3 +1,5 @@
|
||||
ariga.io/atlas v0.7.0 h1:daEFdUsyNm7EHyzcMfjWwq/fVv48fCfad+dIGyobY1k=
|
||||
ariga.io/atlas v0.7.0/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
@ -11,18 +13,24 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
entgo.io/ent v0.11.2 h1:UM2/BUhF2FfsxPHRxLjQbhqJNaDdVlOwNIAMLs2jyto=
|
||||
entgo.io/ent v0.11.2/go.mod h1:YGHEQnmmIUgtD5b1ICD5vg74dS3npkNnmC5K+0J+IHU=
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs=
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||
github.com/ProtonMail/gluon v0.11.1-0.20220922143913-ef3617264557 h1:uyiHq7jDgn1p2TeMKRPnVCVs2bHoNL9AYs26UzLYr4I=
|
||||
github.com/ProtonMail/gluon v0.11.1-0.20220922143913-ef3617264557/go.mod h1:9k3URQEASX9XSA+JEcukjIiK3S6aR9GzhLhwccy8AnI=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
@ -31,8 +39,6 @@ github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 h1:NsReiLpErI
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
|
||||
github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac h1:2xU3QncAiS/W3UlWZTkbNKW5WkLzk6Egl1T0xX+sbjs=
|
||||
github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
||||
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
|
||||
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
|
||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0=
|
||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
||||
@ -42,8 +48,6 @@ github.com/ProtonMail/go-rfc5322 v0.11.0 h1:o5Obrm4DpmQEffvgsVqG6S4BKwC1Wat+hYwj
|
||||
github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw=
|
||||
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=
|
||||
github.com/ProtonMail/go-srp v0.0.5/go.mod h1:06iYHtLXW8vjLtccWj++x3MKy65sIT8yZd7nrJF49rs=
|
||||
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
|
||||
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10 h1:EYgkxzwmQvsa6kxxkgP1AwzkFqKHscF2UINxaSn6rdI=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10/go.mod h1:CTRA7/toc/4DxDy5Du4hPDnIZnJvXSeQ8LsRTOUJoyc=
|
||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||
@ -52,16 +56,19 @@ github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
|
||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.mod h1:6AXRstqK+32jeFmw89QGL2748+dj34Av4xc/I9oo9BY=
|
||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220816024939-bc8df83d7b9d/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
@ -72,7 +79,12 @@ github.com/bradenaw/juniper v0.8.0 h1:sdanLNdJbLjcLj993VYIwUHlUVkLzvgiD/x9O7cvvx
|
||||
github.com/bradenaw/juniper v0.8.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bwesterb/go-ristretto v1.2.1/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
@ -81,14 +93,20 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.2.0 h1:NheeISPSUcYftKlfrLuOo4T62FkmD4t4jviLfFFYaec=
|
||||
github.com/cloudflare/circl v1.2.0/go.mod h1:Ch2UgYr6ti2KTtlejELlROl0YIYj7SLjAC8M+INXlMk=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
||||
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
||||
github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE=
|
||||
@ -106,20 +124,22 @@ github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnG
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M=
|
||||
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
|
||||
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/elastic/go-sysinfo v1.8.1 h1:4Yhj+HdV6WjbCRgGdZpPJ8lZQlXZLKDAeIkmQ/VRvi4=
|
||||
github.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM=
|
||||
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
||||
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20210907172056-e3baed77bbe4 h1:U6LL6F1dYqXpVTwEbXhcfU8hgpNvmjB9xeOAiHN695o=
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20210907172056-e3baed77bbe4/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
|
||||
github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872 h1:HGBfonz0q/zq7y3ew+4oy4emHSvk6bkmV0mdDG3E77M=
|
||||
github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
|
||||
github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c h1:khcEdu1yFiZjBgi7gGnQiLhpSgghJ0YTnKD0l4EUqqc=
|
||||
github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c/go.mod h1:iApyhIQBiU4XFyr+3kdJyyGqle82TbQyuP2o+OZHrV0=
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20210907172115-4c2c4843bf69 h1:ltTnRlPdSMMb0a/pg7S31T3g+syYeSS5UVJtiR7ez1Y=
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20210907172115-4c2c4843bf69/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
|
||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:43mBoVwooyLm1+1YVf5nvn1pSFWhw7rOpcrp1Jg/qk0=
|
||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp0FFboaK/bxsrUz1lNrDMUCsZUsKC5YuM4uRVRVs=
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
||||
@ -130,6 +150,10 @@ github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwo
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a h1:cltZpe6s0SJtqK5c/5y2VrIYi8BAtDM6qjmiGYqfTik=
|
||||
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
@ -139,14 +163,31 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
|
||||
github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo=
|
||||
github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
|
||||
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
@ -154,8 +195,16 @@ github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU
|
||||
github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
|
||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
@ -164,20 +213,40 @@ github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+Licev
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v2.0.8+incompatible h1:ivUb1cGomAB101ZM1T0nOiWz9pSrTMoa9+EiY7igmkM=
|
||||
github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
@ -218,6 +287,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/hcl/v2 v2.14.0 h1:jX6+Q38Ly9zaAJlAjnFVyeNSNCKKW8D0wvyg7vij5Wc=
|
||||
github.com/hashicorp/hcl/v2 v2.14.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
@ -230,22 +301,32 @@ github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
|
||||
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
@ -259,6 +340,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||
@ -267,25 +350,34 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE=
|
||||
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
|
||||
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM=
|
||||
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
@ -293,6 +385,7 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
@ -300,18 +393,21 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
|
||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285 h1:d54EL9l+XteliUfUCGsEwwuk65dmmxX85VXF+9T6+50=
|
||||
github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285/go.mod h1:fxIDly1xtudczrZeOOlfaUvd2OPb2qZAPuWdU2BsBTk=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
|
||||
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
@ -323,13 +419,17 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
@ -348,32 +448,44 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/urfave/cli/v2 v2.16.3 h1:gHoFIwpPjoyIMbJp/VFd+/vuD0dAgFK4B6DpEMFJfQk=
|
||||
github.com/urfave/cli/v2 v2.16.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/zclconf/go-cty v1.11.0 h1:726SxLdi2SDnjY+BStqB9J1hNp4+2WlzyXLuimibIe0=
|
||||
github.com/zclconf/go-cty v1.11.0/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA=
|
||||
gitlab.protontech.ch/go/liteapi v0.30.0 h1:ZpYLDC7LH3nn+O0+SgsTx4YNbU2pIj5fu3jcLvTXWbs=
|
||||
gitlab.protontech.ch/go/liteapi v0.30.0/go.mod h1:ixp1LUOxOYuB1qf172GdV0ZT8fOomKxVFtIMZeSWg+I=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
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-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/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=
|
||||
@ -401,6 +513,8 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@ -418,6 +532,9 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
@ -434,14 +551,18 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 h1:0SH2R3f1b1VmIMG7BXbEZCBUu2dKmHschSmjqGUrW8A=
|
||||
golang.org/x/sync v0.0.0-20220907140024-f12130a52804/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -453,13 +574,15 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -474,6 +597,7 @@ 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/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=
|
||||
@ -488,6 +612,7 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
@ -497,10 +622,12 @@ golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa h1:uKcci2q7Qtp6nMTC/AAvfNUAldFtJuHWV9/5QWiypts=
|
||||
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -523,13 +650,27 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 h1:K1zaaMdYBXRyX+cwFnxj7M6zwDyumLQMZ5xqwGvjreQ=
|
||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737/go.mod h1:2r/26NEF3bFmT3eC3aZreahSal0C3Shl8Gi6vyDYqOQ=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
|
||||
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
@ -537,6 +678,7 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
@ -548,13 +690,17 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
|
||||
@ -1,85 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package api provides HTTP API of the Bridge.
|
||||
//
|
||||
// API endpoints:
|
||||
// - /focus, see focusHandler
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/ports"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var log = logrus.WithField("pkg", "api") //nolint:gochecknoglobals
|
||||
|
||||
type apiServer struct {
|
||||
host string
|
||||
settings *settings.Settings
|
||||
eventListener listener.Listener
|
||||
}
|
||||
|
||||
// NewAPIServer returns prepared API server struct.
|
||||
func NewAPIServer(settings *settings.Settings, eventListener listener.Listener) *apiServer { //nolint:revive
|
||||
return &apiServer{
|
||||
host: bridge.Host,
|
||||
settings: settings,
|
||||
eventListener: eventListener,
|
||||
}
|
||||
}
|
||||
|
||||
// Starts the server.
|
||||
func (api *apiServer) ListenAndServe() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/focus", wrapper(api, focusHandler))
|
||||
|
||||
addr := api.getAddress()
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second, // fix gosec G112 (vulnerability to [Slowloris](https://www.cloudflare.com/en-gb/learning/ddos/ddos-attack-tools/slowloris/) attack).
|
||||
}
|
||||
|
||||
log.Info("API listening at ", addr)
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
api.eventListener.Emit(events.ErrorEvent, "API failed: "+err.Error())
|
||||
log.Error("API failed: ", err)
|
||||
}
|
||||
defer server.Close() //nolint:errcheck
|
||||
}
|
||||
|
||||
func (api *apiServer) getAddress() string {
|
||||
port := api.settings.GetInt(settings.APIPortKey)
|
||||
newPort := ports.FindFreePortFrom(port)
|
||||
if newPort != port {
|
||||
api.settings.SetInt(settings.APIPortKey, newPort)
|
||||
}
|
||||
return getAPIAddress(api.host, newPort)
|
||||
}
|
||||
|
||||
func getAPIAddress(host string, port int) string {
|
||||
return fmt.Sprintf("%s:%d", host, port)
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||
)
|
||||
|
||||
// httpHandler with Go's Response and Request.
|
||||
type httpHandler func(http.ResponseWriter, *http.Request)
|
||||
|
||||
// handler with our context.
|
||||
type handler func(handlerContext) error
|
||||
|
||||
type handlerContext struct {
|
||||
req *http.Request
|
||||
resp http.ResponseWriter
|
||||
eventListener listener.Listener
|
||||
}
|
||||
|
||||
func wrapper(api *apiServer, callback handler) httpHandler {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := handlerContext{
|
||||
req: req,
|
||||
resp: w,
|
||||
eventListener: api.eventListener,
|
||||
}
|
||||
err := callback(ctx)
|
||||
if err != nil {
|
||||
log.Error("API callback of ", req.URL, " failed: ", err)
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||
)
|
||||
|
||||
// focusHandler should be called from other instances (attempt to start bridge
|
||||
// for the second time) to get focus in the currently running instance.
|
||||
func focusHandler(ctx handlerContext) error {
|
||||
log.Info("Focus from other instance")
|
||||
ctx.eventListener.Emit(events.SecondInstanceEvent, "")
|
||||
fmt.Fprintf(ctx.resp, "OK")
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckOtherInstanceAndFocus is helper for new instances to check if there is
|
||||
// already a running instance and get it's focus.
|
||||
func CheckOtherInstanceAndFocus(port int) error {
|
||||
addr := getAPIAddress(bridge.Host, port)
|
||||
resp, err := (&http.Client{}).Get("http://" + addr + "/focus")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close() //nolint:errcheck
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
log.Error("Focus error: ", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
149
internal/app/app.go
Normal file
149
internal/app/app.go
Normal file
@ -0,0 +1,149 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/crash"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/focus"
|
||||
bridgeCLI "github.com/ProtonMail/proton-bridge/v2/internal/frontend/cli"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/frontend/grpc"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/restarter"
|
||||
"github.com/pkg/profile"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
flagCPUProfile = "cpu-prof"
|
||||
flagCPUProfileShort = "p"
|
||||
|
||||
flagMemProfile = "mem-prof"
|
||||
flagMemProfileShort = "m"
|
||||
|
||||
flagLogLevel = "log-level"
|
||||
flagLogLevelShort = "l"
|
||||
|
||||
flagCLI = "cli"
|
||||
flagCLIShort = "c"
|
||||
|
||||
flagNoWindow = "no-window"
|
||||
flagNonInteractive = "non-interactive"
|
||||
)
|
||||
|
||||
const (
|
||||
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
||||
)
|
||||
|
||||
func New() *cli.App {
|
||||
app := cli.NewApp()
|
||||
|
||||
app.Name = constants.FullAppName
|
||||
app.Usage = appUsage
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: flagCPUProfile,
|
||||
Aliases: []string{flagCPUProfileShort},
|
||||
Usage: "Generate CPU profile",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagMemProfile,
|
||||
Aliases: []string{flagMemProfileShort},
|
||||
Usage: "Generate memory profile",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flagLogLevel,
|
||||
Aliases: []string{flagLogLevelShort},
|
||||
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagCLI,
|
||||
Aliases: []string{flagCLIShort},
|
||||
Usage: "Use command line interface",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagNoWindow,
|
||||
Usage: "Don't show window after start",
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
app.Action = run
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func run(c *cli.Context) error {
|
||||
// If there's another instance already running, try to raise it and exit.
|
||||
if raised := focus.TryRaise(); raised {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start CPU profile if requested.
|
||||
if c.Bool(flagCPUProfile) {
|
||||
p := profile.Start(profile.CPUProfile, profile.ProfilePath("cpu.pprof"))
|
||||
defer p.Stop()
|
||||
}
|
||||
|
||||
// Start memory profile if requested.
|
||||
if c.Bool(flagMemProfile) {
|
||||
p := profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath("mem.pprof"))
|
||||
defer p.Stop()
|
||||
}
|
||||
|
||||
// Create the restarter.
|
||||
restarter := restarter.New()
|
||||
defer restarter.Restart()
|
||||
|
||||
// Create a user agent that will be used for all requests.
|
||||
identifier := useragent.New()
|
||||
|
||||
// Create a crash handler that will send crash reports to sentry.
|
||||
crashHandler := crash.NewHandler(
|
||||
sentry.NewReporter(constants.FullAppName, constants.Version, identifier).ReportException,
|
||||
crash.ShowErrorNotification(constants.FullAppName),
|
||||
func(r interface{}) error { restarter.Set(true, true); return nil },
|
||||
)
|
||||
defer crashHandler.HandlePanic()
|
||||
|
||||
// Create a locations provider to determine where to store our files.
|
||||
provider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, constants.ConfigName))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create locations provider: %w", err)
|
||||
}
|
||||
|
||||
// Create a new locations object that will be used to provide paths to store files.
|
||||
locations := locations.New(provider, constants.ConfigName)
|
||||
|
||||
// Initialize the logging.
|
||||
if err := initLogging(c, locations, crashHandler); err != nil {
|
||||
return fmt.Errorf("could not initialize logging: %w", err)
|
||||
}
|
||||
|
||||
// Create the bridge.
|
||||
bridge, err := newBridge(locations, identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create bridge: %w", err)
|
||||
}
|
||||
defer bridge.Close(c.Context)
|
||||
|
||||
// Start the frontend.
|
||||
switch {
|
||||
case c.Bool(flagCLI):
|
||||
return bridgeCLI.New(bridge).Loop()
|
||||
|
||||
case c.Bool(flagNonInteractive):
|
||||
select {}
|
||||
|
||||
default:
|
||||
service, err := grpc.NewService(crashHandler, restarter, locations, bridge, !c.Bool(flagNoWindow))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create service: %w", err)
|
||||
}
|
||||
|
||||
return service.Loop()
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package base
|
||||
|
||||
import "strings"
|
||||
|
||||
// StripProcessSerialNumber removes additional flag from macOS.
|
||||
// More info:
|
||||
// http://mirror.informatimago.com/next/developer.apple.com/documentation/Carbon/Reference/Process_Manager/prmref_main/data_type_5.html#//apple_ref/doc/uid/TP30000208/C001951
|
||||
func StripProcessSerialNumber(args []string) []string {
|
||||
res := args[:0]
|
||||
|
||||
for _, arg := range args {
|
||||
if !strings.Contains(arg, "-psn_") {
|
||||
res = append(res, arg)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
@ -1,424 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package base implements a common application base currently shared by bridge and IE.
|
||||
// The base includes the following:
|
||||
// - access to standard filesystem locations like config, cache, logging dirs
|
||||
// - an extensible crash handler
|
||||
// - versioned cache directory
|
||||
// - persistent settings
|
||||
// - event listener
|
||||
// - credentials store
|
||||
// - pmapi Manager
|
||||
//
|
||||
// In addition, the base initialises logging and reacts to command line arguments
|
||||
// which control the log verbosity and enable cpu/memory profiling.
|
||||
package base
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/api"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/cache"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/tls"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/cookies"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/crash"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/versioner"
|
||||
"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/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
flagCPUProfile = "cpu-prof"
|
||||
flagCPUProfileShort = "p"
|
||||
flagMemProfile = "mem-prof"
|
||||
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"
|
||||
)
|
||||
|
||||
type Base struct {
|
||||
SentryReporter *sentry.Reporter
|
||||
CrashHandler *crash.Handler
|
||||
Locations *locations.Locations
|
||||
Settings *settings.Settings
|
||||
Lock *os.File
|
||||
Cache *cache.Cache
|
||||
Listener listener.Listener
|
||||
Creds *credentials.Store
|
||||
CM pmapi.Manager
|
||||
CookieJar *cookies.Jar
|
||||
UserAgent *useragent.UserAgent
|
||||
Updater *updater.Updater
|
||||
Versioner *versioner.Versioner
|
||||
TLS *tls.TLS
|
||||
Autostart *autostart.App
|
||||
|
||||
Name string // the app's name
|
||||
usage string // the app's usage description
|
||||
command string // the command used to launch the app (either the exe path or the launcher path)
|
||||
restart bool // whether the app is currently set to restart
|
||||
launcher string // launcher to be used if not set in args
|
||||
mainExecutable string // mainExecutable the main executable process.
|
||||
|
||||
teardown []func() error // actions to perform when app is exiting
|
||||
}
|
||||
|
||||
func New( //nolint:funlen
|
||||
appName,
|
||||
appUsage,
|
||||
configName,
|
||||
updateURLName,
|
||||
keychainName,
|
||||
cacheVersion string,
|
||||
) (*Base, error) {
|
||||
userAgent := useragent.New()
|
||||
|
||||
sentryReporter := sentry.NewReporter(appName, constants.Version, userAgent)
|
||||
|
||||
crashHandler := crash.NewHandler(
|
||||
sentryReporter.ReportException,
|
||||
crash.ShowErrorNotification(appName),
|
||||
)
|
||||
defer crashHandler.HandlePanic()
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
os.Args = StripProcessSerialNumber(os.Args)
|
||||
|
||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
locations := locations.New(locationsProvider, configName)
|
||||
|
||||
logsPath, err := locations.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := logging.Init(logsPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
|
||||
if err := migrateFiles(configName); err != nil {
|
||||
logrus.WithError(err).Warn("Old config files could not be migrated")
|
||||
}
|
||||
|
||||
if err := locations.Clean(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
settingsPath, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
settingsObj := settings.New(settingsPath)
|
||||
|
||||
lock, err := checkSingleInstance(locations.GetLockFile(), settingsObj)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Warnf("%v is already running", appName)
|
||||
return nil, api.CheckOtherInstanceAndFocus(settingsObj.GetInt(settings.APIPortKey))
|
||||
}
|
||||
|
||||
if err := migrateRebranding(settingsObj, keychainName); err != nil {
|
||||
logrus.WithError(err).Warn("Rebranding migration failed")
|
||||
}
|
||||
|
||||
cachePath, err := locations.ProvideCachePath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cache, err := cache.New(cachePath, cacheVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cache.RemoveOldVersions(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listener := listener.New()
|
||||
events.SetupEvents(listener)
|
||||
|
||||
// If we can't load the keychain for whatever reason,
|
||||
// we signal to frontend and supply a dummy keychain that always returns errors.
|
||||
kc, err := keychain.NewKeychain(settingsObj, keychainName)
|
||||
if err != nil {
|
||||
listener.Emit(events.CredentialsErrorEvent, err.Error())
|
||||
kc = keychain.NewMissingKeychain()
|
||||
}
|
||||
|
||||
cfg := pmapi.NewConfig(configName, constants.Version)
|
||||
cfg.GetUserAgent = userAgent.String
|
||||
cfg.UpgradeApplicationHandler = func() { listener.Emit(events.UpgradeApplicationEvent, "") }
|
||||
cfg.TLSIssueHandler = func() { listener.Emit(events.TLSCertIssue, "") }
|
||||
|
||||
cm := pmapi.New(cfg)
|
||||
|
||||
sentryReporter.SetClientFromManager(cm)
|
||||
|
||||
cm.AddConnectionObserver(pmapi.NewConnectionObserver(
|
||||
func() { listener.Emit(events.InternetConnChangedEvent, events.InternetOff) },
|
||||
func() { listener.Emit(events.InternetConnChangedEvent, events.InternetOn) },
|
||||
))
|
||||
|
||||
jar, err := cookies.NewCookieJar(settingsObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cm.SetCookieJar(jar)
|
||||
|
||||
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kr, err := crypto.NewKeyRing(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatesDir, err := locations.ProvideUpdatesPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
versioner := versioner.New(updatesDir)
|
||||
installer := updater.NewInstaller(versioner)
|
||||
updater := updater.New(
|
||||
cm,
|
||||
installer,
|
||||
settingsObj,
|
||||
kr,
|
||||
semver.MustParse(constants.Version),
|
||||
updateURLName,
|
||||
runtime.GOOS,
|
||||
)
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
autostart := &autostart.App{
|
||||
Name: startupNameForRebranding(appName),
|
||||
DisplayName: appName,
|
||||
Exec: []string{exe, "--" + FlagNoWindow},
|
||||
}
|
||||
|
||||
return &Base{
|
||||
SentryReporter: sentryReporter,
|
||||
CrashHandler: crashHandler,
|
||||
Locations: locations,
|
||||
Settings: settingsObj,
|
||||
Lock: lock,
|
||||
Cache: cache,
|
||||
Listener: listener,
|
||||
Creds: credentials.NewStore(kc),
|
||||
CM: cm,
|
||||
CookieJar: jar,
|
||||
UserAgent: userAgent,
|
||||
Updater: updater,
|
||||
Versioner: versioner,
|
||||
TLS: tls.New(settingsPath),
|
||||
Autostart: autostart,
|
||||
|
||||
Name: appName,
|
||||
usage: appUsage,
|
||||
|
||||
// By default, the command is the app's executable.
|
||||
// This can be changed at runtime by using the "--launcher" flag.
|
||||
command: exe,
|
||||
// By default, the command is the app's executable.
|
||||
// This can be changed at runtime by summoning the SetMainExecutable gRPC call.
|
||||
mainExecutable: exe,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *Base) NewApp(mainLoop func(*Base, *cli.Context) error) *cli.App {
|
||||
app := cli.NewApp()
|
||||
|
||||
app.Name = b.Name
|
||||
app.Usage = b.usage
|
||||
app.Version = constants.Version
|
||||
app.Action = b.wrapMainLoop(mainLoop)
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: flagCPUProfile,
|
||||
Aliases: []string{flagCPUProfileShort},
|
||||
Usage: "Generate CPU profile",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagMemProfile,
|
||||
Aliases: []string{flagMemProfileShort},
|
||||
Usage: "Generate memory profile",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flagLogLevel,
|
||||
Aliases: []string{flagLogLevelShort},
|
||||
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: FlagCLI,
|
||||
Aliases: []string{flagCLIShort},
|
||||
Usage: "Use command line interface",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: FlagNoWindow,
|
||||
Usage: "Don't show window after start",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flagRestart,
|
||||
Usage: "The number of times the application has already restarted",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: FlagLauncher,
|
||||
Usage: "The launcher to use to restart the application",
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
// SetToRestart sets the app to restart the next time it is closed.
|
||||
func (b *Base) SetToRestart() {
|
||||
b.restart = true
|
||||
}
|
||||
|
||||
func (b *Base) ForceLauncher(launcher string) {
|
||||
b.launcher = launcher
|
||||
b.setupLauncher(launcher)
|
||||
}
|
||||
|
||||
func (b *Base) SetMainExecutable(exe string) {
|
||||
logrus.Info("Main Executable set to ", exe)
|
||||
b.mainExecutable = exe
|
||||
}
|
||||
|
||||
// AddTeardownAction adds an action to perform during app teardown.
|
||||
func (b *Base) AddTeardownAction(fn func() error) {
|
||||
b.teardown = append(b.teardown, fn)
|
||||
}
|
||||
|
||||
func (b *Base) wrapMainLoop(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc { //nolint:funlen
|
||||
return func(c *cli.Context) error {
|
||||
defer b.CrashHandler.HandlePanic()
|
||||
defer func() { _ = b.Lock.Close() }()
|
||||
|
||||
// If launcher was used to start the app, use that for restart
|
||||
// and autostart.
|
||||
if launcher := c.String(FlagLauncher); launcher != "" {
|
||||
b.setupLauncher(launcher)
|
||||
}
|
||||
|
||||
if c.Bool(flagCPUProfile) {
|
||||
startCPUProfile()
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
if c.Bool(flagMemProfile) {
|
||||
defer makeMemoryProfile()
|
||||
}
|
||||
|
||||
logging.SetLevel(c.String(flagLogLevel))
|
||||
b.CM.SetLogging(logrus.WithField("pkg", "pmapi"), logrus.GetLevel() == logrus.TraceLevel)
|
||||
|
||||
logrus.
|
||||
WithField("appName", b.Name).
|
||||
WithField("version", constants.Version).
|
||||
WithField("revision", constants.Revision).
|
||||
WithField("build", constants.BuildTime).
|
||||
WithField("runtime", runtime.GOOS).
|
||||
WithField("args", os.Args).
|
||||
Info("Run app")
|
||||
|
||||
b.CrashHandler.AddRecoveryAction(func(interface{}) error {
|
||||
sentry.Flush(2 * time.Second)
|
||||
|
||||
if c.Int(flagRestart) > maxAllowedRestarts {
|
||||
logrus.
|
||||
WithField("restart", c.Int("restart")).
|
||||
Warn("Not restarting, already restarted too many times")
|
||||
os.Exit(1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return b.restartApp(true)
|
||||
})
|
||||
|
||||
if err := appMainLoop(b, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.doTeardown(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b.restart {
|
||||
return b.restartApp(false)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Base) doTeardown() error {
|
||||
for _, action := range b.teardown {
|
||||
if err := action(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Base) setupLauncher(launcher string) {
|
||||
b.command = launcher
|
||||
// Bridge supports no-window option which we should use
|
||||
// for autostart.
|
||||
b.Autostart.Exec = []string{launcher, "--" + FlagNoWindow}
|
||||
}
|
||||
@ -1,131 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// migrateFiles migrates files from their old (pre-refactor) locations to their new locations.
|
||||
// We can remove this eventually.
|
||||
//
|
||||
// | entity | old location | new location |
|
||||
// |-----------|-------------------------------------------|----------------------------------------|
|
||||
// | prefs | ~/.cache/protonmail/<app>/c11/prefs.json | ~/.config/protonmail/<app>/prefs.json |
|
||||
// | c11 1.5.x | ~/.cache/protonmail/<app>/c11 | ~/.cache/protonmail/<app>/cache/c11 |
|
||||
// | c11 1.6.x | ~/.cache/protonmail/<app>/cache/c11 | ~/.config/protonmail/<app>/cache/c11 |
|
||||
// | updates | ~/.cache/protonmail/<app>/updates | ~/.config/protonmail/<app>/updates |.
|
||||
func migrateFiles(configName string) error {
|
||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
locations := locations.New(locationsProvider, configName)
|
||||
userCacheDir := locationsProvider.UserCache()
|
||||
|
||||
if err := migratePrefsFrom15x(locations, userCacheDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := migrateCacheFromBoth15xAnd16x(locations, userCacheDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := migrateUpdatesFrom16x(configName, locations); err != nil { //nolint:revive It is more clear to structure this way
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func migratePrefsFrom15x(locations *locations.Locations, userCacheDir string) error {
|
||||
newSettingsDir, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return moveIfExists(
|
||||
filepath.Join(userCacheDir, "c11", "prefs.json"),
|
||||
filepath.Join(newSettingsDir, "prefs.json"),
|
||||
)
|
||||
}
|
||||
|
||||
func migrateCacheFromBoth15xAnd16x(locations *locations.Locations, userCacheDir string) error {
|
||||
olderCacheDir := userCacheDir
|
||||
newerCacheDir := locations.GetOldCachePath()
|
||||
latestCacheDir, err := locations.ProvideCachePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Migration for versions before 1.6.x.
|
||||
if err := moveIfExists(
|
||||
filepath.Join(olderCacheDir, "c11"),
|
||||
filepath.Join(latestCacheDir, "c11"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Migration for versions 1.6.x.
|
||||
return moveIfExists(
|
||||
filepath.Join(newerCacheDir, "c11"),
|
||||
filepath.Join(latestCacheDir, "c11"),
|
||||
)
|
||||
}
|
||||
|
||||
func migrateUpdatesFrom16x(configName string, locations *locations.Locations) error {
|
||||
// In order to properly update Bridge 1.6.X and higher we need to
|
||||
// change the launcher first. Since this is not part of automatic
|
||||
// updates the migration must wait until manual update. Until that
|
||||
// we need to keep old path.
|
||||
if configName == "bridge" {
|
||||
return nil
|
||||
}
|
||||
|
||||
oldUpdatesPath := locations.GetOldUpdatesPath()
|
||||
// Do not use ProvideUpdatesPath, that creates dir right away.
|
||||
newUpdatesPath := locations.GetUpdatesPath()
|
||||
|
||||
return moveIfExists(oldUpdatesPath, newUpdatesPath)
|
||||
}
|
||||
|
||||
func moveIfExists(source, destination string) error {
|
||||
l := logrus.WithField("source", source).WithField("destination", destination)
|
||||
|
||||
if _, err := os.Stat(source); os.IsNotExist(err) {
|
||||
l.Info("No need to migrate file, source doesn't exist")
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(destination); !os.IsNotExist(err) {
|
||||
// Once migrated, files should not stay in source anymore. Therefore
|
||||
// if some files are still in source location but target already exist,
|
||||
// it's suspicious. Could happen by installing new version, then the
|
||||
// old one because of some reason, and then the new one again.
|
||||
// Good to see as warning because it could be a reason why Bridge is
|
||||
// behaving weirdly, like wrong configuration, or db re-sync and so on.
|
||||
l.Warn("No need to migrate file, target already exists")
|
||||
return nil
|
||||
}
|
||||
|
||||
l.Info("Migrating files")
|
||||
return os.Rename(source, destination)
|
||||
}
|
||||
@ -1,197 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/keychain"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const darwin = "darwin"
|
||||
|
||||
func migrateRebranding(settingsObj *settings.Settings, keychainName string) (result error) {
|
||||
if err := migrateStartupBeforeRebranding(); err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
|
||||
lastUsedVersion := settingsObj.Get(settings.LastVersionKey)
|
||||
|
||||
// Skipping migration: it is first bridge start or cache was cleared.
|
||||
if lastUsedVersion == "" {
|
||||
settingsObj.SetBool(settings.RebrandingMigrationKey, true)
|
||||
return
|
||||
}
|
||||
|
||||
// Skipping rest of migration: already done
|
||||
if settingsObj.GetBool(settings.RebrandingMigrationKey) {
|
||||
return
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows", "linux":
|
||||
// GODT-1260 we would need admin rights to changes desktop files
|
||||
// and start menu items.
|
||||
settingsObj.SetBool(settings.RebrandingMigrationKey, true)
|
||||
case darwin:
|
||||
if shouldContinue, err := isMacBeforeRebranding(); !shouldContinue || err != nil {
|
||||
if err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if err := migrateMacKeychainBeforeRebranding(settingsObj, keychainName); err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
|
||||
settingsObj.SetBool(settings.RebrandingMigrationKey, true)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// migrateMacKeychainBeforeRebranding deals with write access restriction to
|
||||
// mac keychain passwords which are caused by application renaming. The old
|
||||
// passwords are copied under new name in order to have write access afer
|
||||
// renaming.
|
||||
func migrateMacKeychainBeforeRebranding(settingsObj *settings.Settings, keychainName string) error {
|
||||
l := logrus.WithField("pkg", "app/base/migration")
|
||||
l.Warn("Migrating mac keychain")
|
||||
|
||||
helperConstructor, ok := keychain.Helpers["macos-keychain"]
|
||||
if !ok {
|
||||
return errors.New("cannot find macos-keychain helper")
|
||||
}
|
||||
|
||||
oldKC, err := helperConstructor("ProtonMailBridgeService")
|
||||
if err != nil {
|
||||
l.WithError(err).Error("Keychain constructor failed")
|
||||
return err
|
||||
}
|
||||
|
||||
idByURL, err := oldKC.List()
|
||||
if err != nil {
|
||||
l.WithError(err).Error("List old keychain failed")
|
||||
return err
|
||||
}
|
||||
|
||||
newKC, err := keychain.NewKeychain(settingsObj, keychainName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for url, id := range idByURL {
|
||||
li := l.WithField("id", id).WithField("url", url)
|
||||
userID, secret, err := oldKC.Get(url)
|
||||
if err != nil {
|
||||
li.WithField("userID", userID).
|
||||
WithField("err", err).
|
||||
Error("Faild to get old item")
|
||||
continue
|
||||
}
|
||||
|
||||
if _, _, err := newKC.Get(userID); err == nil {
|
||||
li.Warn("Skipping migration, item already exists.")
|
||||
continue
|
||||
}
|
||||
|
||||
if err := newKC.Put(userID, secret); err != nil {
|
||||
li.WithError(err).Error("Failed to migrate user")
|
||||
}
|
||||
|
||||
li.Info("Item migrated")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateStartupBeforeRebranding removes old startup links. The creation of new links is
|
||||
// handled by bridge initialisation.
|
||||
func migrateStartupBeforeRebranding() error {
|
||||
path, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
path = filepath.Join(path, `AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\ProtonMail Bridge.lnk`)
|
||||
case "linux":
|
||||
path = filepath.Join(path, `.config/autostart/ProtonMail Bridge.desktop`)
|
||||
case darwin:
|
||||
path = filepath.Join(path, `Library/LaunchAgents/ProtonMail Bridge.plist`)
|
||||
default:
|
||||
return errors.New("unknown GOOS")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
logrus.WithField("pkg", "app/base/migration").Warn("Migrating autostartup links")
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
// startupNameForRebranding returns the name for autostart launcher based on
|
||||
// type of rebranded instance i.e. update or manual.
|
||||
//
|
||||
// This only affects darwin when udpate re-writes the old startup and then
|
||||
// manual installed it would not run proper exe. Therefore we return "old" name
|
||||
// for updates and "new" name for manual which would be properly migrated.
|
||||
//
|
||||
// For orther (linux and windows) the link is always pointing to launcher which
|
||||
// path didn't changed.
|
||||
func startupNameForRebranding(origin string) string {
|
||||
if runtime.GOOS == darwin {
|
||||
if path, err := os.Executable(); err == nil && strings.Contains(path, "ProtonMail Bridge") {
|
||||
return "ProtonMail Bridge"
|
||||
}
|
||||
}
|
||||
|
||||
// No need to solve for other OS. See comment above.
|
||||
return origin
|
||||
}
|
||||
|
||||
// isBeforeRebranding decide if last used version was older than 2.2.0. If
|
||||
// cannot decide it returns false with error.
|
||||
func isMacBeforeRebranding() (bool, error) {
|
||||
// previous version | update | do mac migration |
|
||||
// | first | false |
|
||||
// cleared-cache | manual | false |
|
||||
// cleared-cache | in-app | false |
|
||||
// old | in-app | false |
|
||||
// old in-app | in-app | false |
|
||||
// old | manual | true |
|
||||
// old in-app | manual | true |
|
||||
// manual | in-app | false |
|
||||
|
||||
// Skip if it was in-app update and not manual
|
||||
if path, err := os.Executable(); err != nil || strings.Contains(path, "ProtonMail Bridge") {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// startCPUProfile starts CPU pprof.
|
||||
func startCPUProfile() {
|
||||
f, err := os.Create("./cpu.pprof")
|
||||
if err != nil {
|
||||
logrus.Fatal("Could not create CPU profile: ", err)
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
logrus.Fatal("Could not start CPU profile: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// makeMemoryProfile generates memory pprof.
|
||||
func makeMemoryProfile() {
|
||||
name := "./mem.pprof"
|
||||
f, err := os.Create(name)
|
||||
if err != nil {
|
||||
logrus.Fatal("Could not create memory profile: ", err)
|
||||
}
|
||||
if abs, err := filepath.Abs(name); err == nil {
|
||||
name = abs
|
||||
}
|
||||
logrus.Info("Writing memory profile to ", name)
|
||||
runtime.GC() // get up-to-date statistics
|
||||
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
logrus.Fatal("Could not write memory profile: ", err)
|
||||
}
|
||||
_ = f.Close()
|
||||
}
|
||||
@ -1,112 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
// maxAllowedRestarts controls after how many crashes the app will give up restarting.
|
||||
const maxAllowedRestarts = 10
|
||||
|
||||
func (b *Base) restartApp(crash bool) error {
|
||||
var args []string
|
||||
|
||||
if crash {
|
||||
args = incrementRestartFlag(os.Args)[1:]
|
||||
defer func() { os.Exit(1) }()
|
||||
} else {
|
||||
args = os.Args[1:]
|
||||
}
|
||||
|
||||
if b.launcher != "" {
|
||||
args = forceLauncherFlag(args, b.launcher)
|
||||
}
|
||||
|
||||
args = append(args, "--wait", b.mainExecutable)
|
||||
|
||||
logrus.
|
||||
WithField("command", b.command).
|
||||
WithField("args", args).
|
||||
Warn("Restarting")
|
||||
|
||||
return execabs.Command(b.command, args...).Start() //nolint:gosec
|
||||
}
|
||||
|
||||
// incrementRestartFlag increments the value of the restart flag.
|
||||
// If no such flag is present, it is added with initial value 1.
|
||||
func incrementRestartFlag(args []string) []string {
|
||||
res := append([]string{}, args...)
|
||||
|
||||
hasFlag := false
|
||||
|
||||
for k, v := range res {
|
||||
if v != "--restart" {
|
||||
continue
|
||||
}
|
||||
|
||||
hasFlag = true
|
||||
|
||||
if k+1 >= len(res) {
|
||||
continue
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(res[k+1])
|
||||
if err != nil {
|
||||
res[k+1] = "1"
|
||||
} else {
|
||||
res[k+1] = strconv.Itoa(n + 1)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasFlag {
|
||||
res = append(res, "--restart", "1")
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// 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...)
|
||||
|
||||
hasFlag := false
|
||||
|
||||
for k, v := range res {
|
||||
if v != "--launcher" {
|
||||
continue
|
||||
}
|
||||
|
||||
if k+1 >= len(res) {
|
||||
continue
|
||||
}
|
||||
|
||||
hasFlag = true
|
||||
res[k+1] = launcher
|
||||
}
|
||||
|
||||
if !hasFlag {
|
||||
res = append(res, "--launcher", launcher)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIncrementRestartFlag(t *testing.T) {
|
||||
tests := []struct {
|
||||
in []string
|
||||
out []string
|
||||
}{
|
||||
{[]string{"./bridge", "--restart", "1"}, []string{"./bridge", "--restart", "2"}},
|
||||
{[]string{"./bridge", "--restart", "2"}, []string{"./bridge", "--restart", "3"}},
|
||||
{[]string{"./bridge", "--other", "--restart", "2"}, []string{"./bridge", "--other", "--restart", "3"}},
|
||||
{[]string{"./bridge", "--restart", "2", "--other"}, []string{"./bridge", "--restart", "3", "--other"}},
|
||||
{[]string{"./bridge", "--restart", "2", "--other", "2"}, []string{"./bridge", "--restart", "3", "--other", "2"}},
|
||||
{[]string{"./bridge"}, []string{"./bridge", "--restart", "1"}},
|
||||
{[]string{"./bridge", "--something"}, []string{"./bridge", "--something", "--restart", "1"}},
|
||||
{[]string{"./bridge", "--something", "--else"}, []string{"./bridge", "--something", "--else", "--restart", "1"}},
|
||||
{[]string{"./bridge", "--restart", "bad"}, []string{"./bridge", "--restart", "1"}},
|
||||
{[]string{"./bridge", "--restart", "bad", "--other"}, []string{"./bridge", "--restart", "1", "--other"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(strings.Join(tt.in, " "), func(t *testing.T) {
|
||||
assert.Equal(t, tt.out, incrementRestartFlag(tt.in))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionLessThan(t *testing.T) {
|
||||
r := require.New(t)
|
||||
|
||||
old := semver.MustParse("1.1.0")
|
||||
current := semver.MustParse("1.1.1")
|
||||
newer := semver.MustParse("1.1.2")
|
||||
|
||||
r.True(old.LessThan(current))
|
||||
r.False(current.LessThan(current))
|
||||
r.False(newer.LessThan(current))
|
||||
}
|
||||
@ -1,101 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/allan-simon/go-singleinstance"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// checkSingleInstance returns error if a bridge instance is already running
|
||||
// This instance should be stop and window of running window should be brought
|
||||
// to focus.
|
||||
//
|
||||
// For macOS and Linux when already running version is older than this instance
|
||||
// it will kill old and continue with this new bridge (i.e. no error returned).
|
||||
func checkSingleInstance(lockFilePath string, settingsObj *settings.Settings) (*os.File, error) {
|
||||
if lock, err := singleinstance.CreateLockFile(lockFilePath); err == nil {
|
||||
// Bridge is not runnig, continue normally
|
||||
return lock, nil
|
||||
}
|
||||
|
||||
if err := runningVersionIsOlder(settingsObj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pid, err := getPID(lockFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := unix.Kill(pid, unix.SIGTERM); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Need to wait some time to release file lock
|
||||
time.Sleep(time.Second)
|
||||
|
||||
return singleinstance.CreateLockFile(lockFilePath)
|
||||
}
|
||||
|
||||
func getPID(lockFilePath string) (int, error) {
|
||||
file, err := os.Open(filepath.Clean(lockFilePath))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
rawPID := make([]byte, 10) // PID is probably up to 7 digits long, 10 should be enough
|
||||
n, err := file.Read(rawPID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.Atoi(strings.TrimSpace(string(rawPID[:n])))
|
||||
}
|
||||
|
||||
func runningVersionIsOlder(settingsObj *settings.Settings) error {
|
||||
currentVer, err := semver.StrictNewVersion(constants.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runningVer, err := semver.StrictNewVersion(settingsObj.Get(settings.LastVersionKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !runningVer.LessThan(currentVer) {
|
||||
return errors.New("running version is not older")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/allan-simon/go-singleinstance"
|
||||
)
|
||||
|
||||
func checkSingleInstance(lockFilePath string, _ *settings.Settings) (*os.File, error) {
|
||||
return singleinstance.CreateLockFile(lockFilePath)
|
||||
}
|
||||
205
internal/app/bridge.go
Normal file
205
internal/app/bridge.go
Normal file
@ -0,0 +1,205 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/certs"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/dialer"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/versioner"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/keychain"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const vaultSecretName = "bridge-vault-key"
|
||||
|
||||
func newBridge(locations *locations.Locations, identifier *useragent.UserAgent) (*bridge.Bridge, error) {
|
||||
// Create the underlying dialer used by the bridge.
|
||||
// It only connects to trusted servers and reports any untrusted servers it finds.
|
||||
pinningDialer := dialer.NewPinningTLSDialer(
|
||||
dialer.NewBasicTLSDialer(constants.APIHost),
|
||||
dialer.NewTLSReporter(constants.APIHost, constants.AppVersion, identifier, dialer.TrustedAPIPins),
|
||||
dialer.NewTLSPinChecker(dialer.TrustedAPIPins),
|
||||
)
|
||||
|
||||
// Create a proxy dialer which switches to a proxy if the request fails.
|
||||
proxyDialer := dialer.NewProxyTLSDialer(pinningDialer, constants.APIHost)
|
||||
|
||||
// Create the autostarter.
|
||||
autostarter, err := newAutostarter()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create autostarter: %w", err)
|
||||
}
|
||||
|
||||
// Create the update installer.
|
||||
updater, err := newUpdater(locations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create updater: %w", err)
|
||||
}
|
||||
|
||||
// Get the current bridge version.
|
||||
version, err := semver.NewVersion(constants.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create version: %w", err)
|
||||
}
|
||||
|
||||
// Create the encVault.
|
||||
encVault, insecure, corrupt, err := newVault(locations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create vault: %w", err)
|
||||
} else if insecure {
|
||||
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
|
||||
} else if corrupt {
|
||||
logrus.Warn("The vault is corrupt and has been wiped")
|
||||
}
|
||||
|
||||
// Install the certificates if needed.
|
||||
if installed := encVault.GetCertsInstalled(); !installed {
|
||||
if err := certs.NewInstaller().InstallCert(encVault.GetBridgeTLSCert()); err != nil {
|
||||
return nil, fmt.Errorf("failed to install certs: %w", err)
|
||||
}
|
||||
|
||||
if err := encVault.SetCertsInstalled(true); err != nil {
|
||||
return nil, fmt.Errorf("failed to set certs installed: %w", err)
|
||||
}
|
||||
|
||||
if err := encVault.SetCertsInstalled(true); err != nil {
|
||||
return nil, fmt.Errorf("could not set certs installed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new bridge.
|
||||
bridge, err := bridge.New(constants.APIHost, locations, encVault, identifier, pinningDialer, proxyDialer, autostarter, updater, version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create bridge: %w", err)
|
||||
}
|
||||
|
||||
// If the vault could not be loaded properly, push errors to the bridge.
|
||||
switch {
|
||||
case insecure:
|
||||
bridge.PushError(vault.ErrInsecure)
|
||||
|
||||
case corrupt:
|
||||
bridge.PushError(vault.ErrCorrupt)
|
||||
}
|
||||
|
||||
return bridge, nil
|
||||
}
|
||||
|
||||
func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error) {
|
||||
var insecure bool
|
||||
|
||||
vaultDir, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not get vault dir: %w", err)
|
||||
}
|
||||
|
||||
var vaultKey []byte
|
||||
|
||||
if key, err := getVaultKey(vaultDir); err != nil {
|
||||
insecure = true
|
||||
} else {
|
||||
vaultKey = key
|
||||
}
|
||||
|
||||
gluonDir, err := locations.ProvideGluonPath()
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not provide gluon path: %w", err)
|
||||
}
|
||||
|
||||
vault, corrupt, err := vault.New(vaultDir, gluonDir, vaultKey)
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
|
||||
}
|
||||
|
||||
return vault, insecure, corrupt, nil
|
||||
}
|
||||
|
||||
func getVaultKey(vaultDir string) ([]byte, error) {
|
||||
helper, err := vault.GetHelper(vaultDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get keychain helper: %w", err)
|
||||
}
|
||||
|
||||
keychain, err := keychain.NewKeychain(helper, constants.KeyChainName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create keychain: %w", err)
|
||||
}
|
||||
|
||||
secrets, err := keychain.List()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not list keychain: %w", err)
|
||||
}
|
||||
|
||||
if !slices.Contains(secrets, vaultSecretName) {
|
||||
tok, err := crypto.RandomToken(32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not generate random token: %w", err)
|
||||
}
|
||||
|
||||
if err := keychain.Put(vaultSecretName, base64.StdEncoding.EncodeToString(tok)); err != nil {
|
||||
return nil, fmt.Errorf("could not put keychain item: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
_, keyEnc, err := keychain.Get(vaultSecretName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get keychain item: %w", err)
|
||||
}
|
||||
|
||||
keyDec, err := base64.StdEncoding.DecodeString(keyEnc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not decode keychain item: %w", err)
|
||||
}
|
||||
|
||||
return keyDec, nil
|
||||
}
|
||||
|
||||
func newAutostarter() (*autostart.App, error) {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &autostart.App{
|
||||
Name: constants.FullAppName,
|
||||
DisplayName: constants.FullAppName,
|
||||
Exec: []string{exe, "--" + flagNoWindow},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newUpdater(locations *locations.Locations) (*updater.Updater, error) {
|
||||
updatesDir, err := locations.ProvideUpdatesPath()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not provide updates path: %w", err)
|
||||
}
|
||||
|
||||
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create key from armored: %w", err)
|
||||
}
|
||||
|
||||
verifier, err := crypto.NewKeyRing(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create key ring: %w", err)
|
||||
}
|
||||
|
||||
return updater.NewUpdater(
|
||||
updater.NewInstaller(versioner.New(updatesDir)),
|
||||
verifier,
|
||||
constants.UpdateName,
|
||||
runtime.GOOS,
|
||||
), nil
|
||||
}
|
||||
@ -1,269 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package bridge implements the bridge CLI application.
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/api"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/app/base"
|
||||
pkgBridge "github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/frontend"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/imap"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/smtp"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/store"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/store/cache"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
flagLogIMAP = "log-imap"
|
||||
flagLogSMTP = "log-smtp"
|
||||
flagNonInteractive = "noninteractive"
|
||||
|
||||
// Memory cache was estimated by empirical usage in past and it was set to 100MB.
|
||||
// NOTE: This value must not be less than maximal size of one email (~30MB).
|
||||
inMemoryCacheLimnit = 100 * (1 << 20)
|
||||
)
|
||||
|
||||
func New(base *base.Base) *cli.App {
|
||||
app := base.NewApp(main)
|
||||
|
||||
app.Flags = append(app.Flags, []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: flagLogIMAP,
|
||||
Usage: "Enable logging of IMAP communications (all|client|server) (may contain decrypted data!)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagLogSMTP,
|
||||
Usage: "Enable logging of SMTP communications (may contain decrypted data!)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagNonInteractive,
|
||||
Usage: "Start Bridge entirely noninteractively",
|
||||
},
|
||||
}...)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func main(b *base.Base, c *cli.Context) error { //nolint:funlen
|
||||
cache, cacheErr := loadMessageCache(b)
|
||||
if cacheErr != nil {
|
||||
logrus.WithError(cacheErr).Error("Could not load local cache.")
|
||||
}
|
||||
|
||||
builder := message.NewBuilder(
|
||||
b.Settings.GetInt(settings.FetchWorkers),
|
||||
b.Settings.GetInt(settings.AttachmentWorkers),
|
||||
)
|
||||
|
||||
bridge := pkgBridge.New(
|
||||
b.Locations,
|
||||
b.Cache,
|
||||
b.Settings,
|
||||
b.SentryReporter,
|
||||
b.CrashHandler,
|
||||
b.Listener,
|
||||
b.TLS,
|
||||
b.UserAgent,
|
||||
cache,
|
||||
builder,
|
||||
b.CM,
|
||||
b.Creds,
|
||||
b.Updater,
|
||||
b.Versioner,
|
||||
b.Autostart,
|
||||
)
|
||||
imapBackend := imap.NewIMAPBackend(b.CrashHandler, b.Listener, b.Cache, b.Settings, bridge)
|
||||
smtpBackend := smtp.NewSMTPBackend(b.CrashHandler, b.Listener, b.Settings, bridge)
|
||||
|
||||
tlsConfig, err := bridge.GetTLSConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cacheErr != nil {
|
||||
bridge.AddError(pkgBridge.ErrLocalCacheUnavailable)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer b.CrashHandler.HandlePanic()
|
||||
api.NewAPIServer(b.Settings, b.Listener).ListenAndServe()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer b.CrashHandler.HandlePanic()
|
||||
imapPort := b.Settings.GetInt(settings.IMAPPortKey)
|
||||
imap.NewIMAPServer(
|
||||
b.CrashHandler,
|
||||
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
|
||||
c.String(flagLogIMAP) == "server" || c.String(flagLogIMAP) == "all",
|
||||
imapPort, tlsConfig, imapBackend, b.UserAgent, b.Listener).ListenAndServe()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer b.CrashHandler.HandlePanic()
|
||||
smtpPort := b.Settings.GetInt(settings.SMTPPortKey)
|
||||
useSSL := b.Settings.GetBool(settings.SMTPSSLKey)
|
||||
smtp.NewSMTPServer(
|
||||
b.CrashHandler,
|
||||
c.Bool(flagLogSMTP),
|
||||
smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe()
|
||||
}()
|
||||
|
||||
// We want to remove old versions if the app exits successfully.
|
||||
b.AddTeardownAction(b.Versioner.RemoveOldVersions)
|
||||
|
||||
// We want cookies to be saved to disk so they are loaded the next time.
|
||||
b.AddTeardownAction(b.CookieJar.PersistCookies)
|
||||
|
||||
var frontendMode string
|
||||
|
||||
switch {
|
||||
case c.Bool(base.FlagCLI):
|
||||
frontendMode = "cli"
|
||||
case c.Bool(flagNonInteractive):
|
||||
return <-(make(chan error)) // Block forever.
|
||||
default:
|
||||
frontendMode = "grpc"
|
||||
}
|
||||
|
||||
f := frontend.New(
|
||||
frontendMode,
|
||||
!c.Bool(base.FlagNoWindow),
|
||||
b.CrashHandler,
|
||||
b.Listener,
|
||||
b.Updater,
|
||||
bridge,
|
||||
b,
|
||||
b.Locations,
|
||||
)
|
||||
|
||||
// Watch for updates routine
|
||||
go func() {
|
||||
ticker := time.NewTicker(constants.UpdateCheckInterval)
|
||||
|
||||
for {
|
||||
checkAndHandleUpdate(b.Updater, f, b.Settings.GetBool(settings.AutoUpdateKey))
|
||||
<-ticker.C
|
||||
}
|
||||
}()
|
||||
|
||||
return f.Loop()
|
||||
}
|
||||
|
||||
func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) {
|
||||
log := logrus.WithField("pkg", "app/bridge")
|
||||
version, err := u.Check()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("An error occurred while checking for updates")
|
||||
return
|
||||
}
|
||||
|
||||
f.WaitUntilFrontendIsReady()
|
||||
|
||||
// Update links in UI
|
||||
f.SetVersion(version)
|
||||
|
||||
if !u.IsUpdateApplicable(version) {
|
||||
log.Info("No need to update")
|
||||
return
|
||||
}
|
||||
|
||||
log.WithField("version", version.Version).Info("An update is available")
|
||||
|
||||
if !autoUpdate {
|
||||
f.NotifyManualUpdate(version, u.CanInstall(version))
|
||||
return
|
||||
}
|
||||
|
||||
if !u.CanInstall(version) {
|
||||
log.Info("A manual update is required")
|
||||
f.NotifySilentUpdateError(updater.ErrManualUpdateRequired)
|
||||
return
|
||||
}
|
||||
|
||||
if err := u.InstallUpdate(version); err != nil {
|
||||
if errors.Cause(err) == updater.ErrDownloadVerify {
|
||||
log.WithError(err).Warning("Skipping update installation due to temporary error")
|
||||
} else {
|
||||
log.WithError(err).Error("The update couldn't be installed")
|
||||
f.NotifySilentUpdateError(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
f.NotifySilentUpdateInstalled()
|
||||
}
|
||||
|
||||
// loadMessageCache loads local cache in case it is enabled in settings and available.
|
||||
// In any other case it is returning in-memory cache. Could also return an error in case
|
||||
// local cache is enabled but unavailable (in-memory cache will be returned nevertheless).
|
||||
func loadMessageCache(b *base.Base) (cache.Cache, error) {
|
||||
if !b.Settings.GetBool(settings.CacheEnabledKey) {
|
||||
return cache.NewInMemoryCache(inMemoryCacheLimnit), nil
|
||||
}
|
||||
|
||||
var compressor cache.Compressor
|
||||
|
||||
// NOTE(GODT-1158): Changing compression is not an option currently
|
||||
// available for user but, if user changes compression setting we have
|
||||
// to nuke the cache.
|
||||
if b.Settings.GetBool(settings.CacheCompressionKey) {
|
||||
compressor = &cache.GZipCompressor{}
|
||||
} else {
|
||||
compressor = &cache.NoopCompressor{}
|
||||
}
|
||||
|
||||
var path string
|
||||
|
||||
if customPath := b.Settings.Get(settings.CacheLocationKey); customPath != "" {
|
||||
path = customPath
|
||||
} else {
|
||||
path = b.Cache.GetDefaultMessageCacheDir()
|
||||
// Store path so it will allways persist if default location
|
||||
// will be changed in new version.
|
||||
b.Settings.Set(settings.CacheLocationKey, path)
|
||||
}
|
||||
|
||||
// To prevent memory peaks we set maximal write concurency for store
|
||||
// build jobs.
|
||||
store.SetBuildAndCacheJobLimit(b.Settings.GetInt(settings.CacheConcurrencyWrite))
|
||||
|
||||
messageCache, err := cache.NewOnDiskCache(path, compressor, cache.Options{
|
||||
MinFreeAbs: uint64(b.Settings.GetInt(settings.CacheMinFreeAbsKey)),
|
||||
MinFreeRat: b.Settings.GetFloat64(settings.CacheMinFreeRatKey),
|
||||
ConcurrentRead: b.Settings.GetInt(settings.CacheConcurrencyRead),
|
||||
ConcurrentWrite: b.Settings.GetInt(settings.CacheConcurrencyWrite),
|
||||
})
|
||||
if err != nil {
|
||||
return cache.NewInMemoryCache(inMemoryCacheLimnit), err
|
||||
}
|
||||
|
||||
return messageCache, nil
|
||||
}
|
||||
28
internal/app/logging.go
Normal file
28
internal/app/logging.go
Normal file
@ -0,0 +1,28 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/crash"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func initLogging(c *cli.Context, locations *locations.Locations, crashHandler *crash.Handler) error {
|
||||
// Get a place to keep our logs.
|
||||
logsPath, err := locations.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not provide logs path: %w", err)
|
||||
}
|
||||
|
||||
// Initialize logging.
|
||||
if err := logging.Init(logsPath, c.String(flagLogLevel)); err != nil {
|
||||
return fmt.Errorf("could not initialize logging: %w", err)
|
||||
}
|
||||
|
||||
// Ensure we dump a stack trace if we crash.
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package bridge provides core functionality of Bridge app.
|
||||
package bridge
|
||||
|
||||
import "github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
|
||||
// IsAutostartEnabled checks if link file exits.
|
||||
func (b *Bridge) IsAutostartEnabled() bool {
|
||||
return b.autostart.IsEnabled()
|
||||
}
|
||||
|
||||
// EnableAutostart creates link and sets the preferences.
|
||||
func (b *Bridge) EnableAutostart() error {
|
||||
b.settings.SetBool(settings.AutostartKey, true)
|
||||
return b.autostart.Enable()
|
||||
}
|
||||
|
||||
// DisableAutostart removes link and sets the preferences.
|
||||
func (b *Bridge) DisableAutostart() error {
|
||||
b.settings.SetBool(settings.AutostartKey, false)
|
||||
return b.autostart.Disable()
|
||||
}
|
||||
@ -1,325 +1,318 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package bridge provides core functionality of Bridge app.
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/tls"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/useragent"
|
||||
"github.com/ProtonMail/gluon"
|
||||
"github.com/ProtonMail/gluon/watcher"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/metrics"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/store/cache"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/users"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||
logrus "github.com/sirupsen/logrus"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/cookies"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/focus"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gitlab.protontech.ch/go/liteapi"
|
||||
)
|
||||
|
||||
var log = logrus.WithField("pkg", "bridge") //nolint:gochecknoglobals
|
||||
|
||||
var ErrLocalCacheUnavailable = errors.New("local cache is unavailable")
|
||||
|
||||
type Bridge struct {
|
||||
*users.Users
|
||||
// vault holds bridge-specific data, such as preferences and known users (authorized or not).
|
||||
vault *vault.Vault
|
||||
|
||||
locations Locator
|
||||
settings SettingsProvider
|
||||
clientManager pmapi.Manager
|
||||
// users holds authorized users.
|
||||
users map[string]*user.User
|
||||
|
||||
// api manages user API clients.
|
||||
api *liteapi.Manager
|
||||
cookieJar *cookies.Jar
|
||||
proxyDialer ProxyDialer
|
||||
identifier Identifier
|
||||
|
||||
// watchers holds all registered event watchers.
|
||||
watchers []*watcher.Watcher[events.Event]
|
||||
watchersLock sync.RWMutex
|
||||
|
||||
// tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers.
|
||||
tlsConfig *tls.Config
|
||||
|
||||
// imapServer is the bridge's IMAP server.
|
||||
imapServer *gluon.Server
|
||||
imapListener net.Listener
|
||||
|
||||
// smtpServer is the bridge's SMTP server.
|
||||
smtpServer *smtp.Server
|
||||
smtpBackend *smtpBackend
|
||||
smtpListener net.Listener
|
||||
|
||||
// updater is the bridge's updater.
|
||||
updater Updater
|
||||
versioner Versioner
|
||||
tls *tls.TLS
|
||||
userAgent *useragent.UserAgent
|
||||
cacheProvider CacheProvider
|
||||
autostart *autostart.App
|
||||
// Bridge's global errors list.
|
||||
errors []error
|
||||
curVersion *semver.Version
|
||||
updateCheckCh chan struct{}
|
||||
|
||||
isAllMailVisible bool
|
||||
isFirstStart bool
|
||||
lastVersion string
|
||||
// focusService is used to raise the bridge window when needed.
|
||||
focusService *focus.FocusService
|
||||
|
||||
// autostarter is the bridge's autostarter.
|
||||
autostarter Autostarter
|
||||
|
||||
// locator is the bridge's locator.
|
||||
locator Locator
|
||||
|
||||
// errors contains errors encountered during startup.
|
||||
errors []error
|
||||
}
|
||||
|
||||
func New( //nolint:funlen
|
||||
locations Locator,
|
||||
cacheProvider CacheProvider,
|
||||
setting SettingsProvider,
|
||||
sentryReporter *sentry.Reporter,
|
||||
panicHandler users.PanicHandler,
|
||||
eventListener listener.Listener,
|
||||
tls *tls.TLS,
|
||||
userAgent *useragent.UserAgent,
|
||||
cache cache.Cache,
|
||||
builder *message.Builder,
|
||||
clientManager pmapi.Manager,
|
||||
credStorer users.CredentialsStorer,
|
||||
updater Updater,
|
||||
versioner Versioner,
|
||||
autostart *autostart.App,
|
||||
) *Bridge {
|
||||
// Allow DoH before starting the app if the user has previously set this setting.
|
||||
// This allows us to start even if protonmail is blocked.
|
||||
if setting.GetBool(settings.AllowProxyKey) {
|
||||
clientManager.AllowProxy()
|
||||
// New creates a new bridge.
|
||||
func New(
|
||||
apiURL string, // the URL of the API to use
|
||||
locator Locator, // the locator to provide paths to store data
|
||||
vault *vault.Vault, // the bridge's encrypted data store
|
||||
identifier Identifier, // the identifier to keep track of the user agent
|
||||
tlsReporter TLSReporter, // the TLS reporter to report TLS errors
|
||||
proxyDialer ProxyDialer, // the DoH dialer
|
||||
autostarter Autostarter, // the autostarter to manage autostart settings
|
||||
updater Updater, // the updater to fetch and install updates
|
||||
curVersion *semver.Version, // the current version of the bridge
|
||||
) (*Bridge, error) {
|
||||
if vault.GetProxyAllowed() {
|
||||
proxyDialer.AllowProxy()
|
||||
} else {
|
||||
proxyDialer.DisallowProxy()
|
||||
}
|
||||
|
||||
u := users.New(
|
||||
locations,
|
||||
panicHandler,
|
||||
eventListener,
|
||||
clientManager,
|
||||
credStorer,
|
||||
newStoreFactory(cacheProvider, sentryReporter, panicHandler, eventListener, cache, builder),
|
||||
cookieJar, err := cookies.NewCookieJar(vault)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create cookie jar: %w", err)
|
||||
}
|
||||
|
||||
api := liteapi.New(
|
||||
liteapi.WithHostURL(apiURL),
|
||||
liteapi.WithAppVersion(constants.AppVersion),
|
||||
liteapi.WithCookieJar(cookieJar),
|
||||
liteapi.WithTransport(&http.Transport{DialTLSContext: proxyDialer.DialTLSContext}),
|
||||
)
|
||||
|
||||
b := &Bridge{
|
||||
Users: u,
|
||||
locations: locations,
|
||||
settings: setting,
|
||||
clientManager: clientManager,
|
||||
updater: updater,
|
||||
versioner: versioner,
|
||||
tls: tls,
|
||||
userAgent: userAgent,
|
||||
cacheProvider: cacheProvider,
|
||||
autostart: autostart,
|
||||
isFirstStart: false,
|
||||
isAllMailVisible: setting.GetBool(settings.IsAllMailVisible),
|
||||
}
|
||||
|
||||
if setting.GetBool(settings.FirstStartKey) {
|
||||
b.isFirstStart = true
|
||||
if err := b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(constants.Version))); err != nil {
|
||||
logrus.WithError(err).Error("Failed to send metric")
|
||||
}
|
||||
setting.SetBool(settings.FirstStartKey, false)
|
||||
}
|
||||
|
||||
// Keep in bridge and update in settings the last used version.
|
||||
b.lastVersion = b.settings.Get(settings.LastVersionKey)
|
||||
b.settings.Set(settings.LastVersionKey, constants.Version)
|
||||
|
||||
go b.heartbeat()
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// heartbeat sends a heartbeat signal once a day.
|
||||
func (b *Bridge) heartbeat() {
|
||||
for range time.Tick(time.Minute) {
|
||||
lastHeartbeatDay, err := strconv.ParseInt(b.settings.Get(settings.LastHeartbeatKey), 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// If we're still on the same day, don't send a heartbeat.
|
||||
if time.Now().YearDay() == int(lastHeartbeatDay) {
|
||||
continue
|
||||
}
|
||||
|
||||
// We're on the next (or a different) day, so send a heartbeat.
|
||||
if err := b.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel)); err != nil {
|
||||
logrus.WithError(err).Error("Failed to send heartbeat")
|
||||
continue
|
||||
}
|
||||
|
||||
// Heartbeat was sent successfully so update the last heartbeat day.
|
||||
b.settings.Set(settings.LastHeartbeatKey, fmt.Sprintf("%v", time.Now().YearDay()))
|
||||
}
|
||||
}
|
||||
|
||||
// GetUpdateChannel returns currently set update channel.
|
||||
func (b *Bridge) GetUpdateChannel() updater.UpdateChannel {
|
||||
return updater.UpdateChannel(b.settings.Get(settings.UpdateChannelKey))
|
||||
}
|
||||
|
||||
// SetUpdateChannel switches update channel.
|
||||
func (b *Bridge) SetUpdateChannel(channel updater.UpdateChannel) {
|
||||
b.settings.Set(settings.UpdateChannelKey, string(channel))
|
||||
}
|
||||
|
||||
func (b *Bridge) resetToLatestStable() error {
|
||||
version, err := b.updater.Check()
|
||||
tlsConfig, err := loadTLSConfig(vault)
|
||||
if err != nil {
|
||||
// If we can not check for updates - just remove all local updates and reset to base installer version.
|
||||
// Not using `b.locations.ClearUpdates()` because `versioner.RemoveOtherVersions` can also handle
|
||||
// case when it is needed to remove currently running verion.
|
||||
if err := b.versioner.RemoveOtherVersions(semver.MustParse("0.0.0")); err != nil {
|
||||
log.WithError(err).Error("Failed to clear updates while downgrading channel")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to load TLS config: %w", err)
|
||||
}
|
||||
|
||||
imapServer, err := newIMAPServer(vault.GetGluonDir(), curVersion, tlsConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create IMAP server: %w", err)
|
||||
}
|
||||
|
||||
smtpBackend, err := newSMTPBackend()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create SMTP backend: %w", err)
|
||||
}
|
||||
|
||||
smtpServer, err := newSMTPServer(smtpBackend, tlsConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create SMTP server: %w", err)
|
||||
}
|
||||
|
||||
focusService, err := focus.NewService()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create focus service: %w", err)
|
||||
}
|
||||
|
||||
bridge := &Bridge{
|
||||
vault: vault,
|
||||
users: make(map[string]*user.User),
|
||||
|
||||
api: api,
|
||||
cookieJar: cookieJar,
|
||||
proxyDialer: proxyDialer,
|
||||
identifier: identifier,
|
||||
|
||||
tlsConfig: tlsConfig,
|
||||
imapServer: imapServer,
|
||||
smtpServer: smtpServer,
|
||||
smtpBackend: smtpBackend,
|
||||
|
||||
updater: updater,
|
||||
curVersion: curVersion,
|
||||
updateCheckCh: make(chan struct{}, 1),
|
||||
|
||||
focusService: focusService,
|
||||
autostarter: autostarter,
|
||||
locator: locator,
|
||||
}
|
||||
|
||||
api.AddStatusObserver(func(status liteapi.Status) {
|
||||
bridge.publish(events.ConnStatus{
|
||||
Status: status,
|
||||
})
|
||||
})
|
||||
|
||||
api.AddErrorHandler(liteapi.AppVersionBadCode, func() {
|
||||
bridge.publish(events.UpdateForced{})
|
||||
})
|
||||
|
||||
api.AddPreRequestHook(func(_ *resty.Client, req *resty.Request) error {
|
||||
req.SetHeader("User-Agent", bridge.identifier.GetUserAgent())
|
||||
return nil
|
||||
})
|
||||
|
||||
go func() {
|
||||
for range tlsReporter.GetTLSIssueCh() {
|
||||
bridge.publish(events.TLSIssue{})
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for range focusService.GetRaiseCh() {
|
||||
bridge.publish(events.Raise{})
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for event := range imapServer.AddWatcher() {
|
||||
bridge.handleIMAPEvent(event)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := bridge.loadUsers(context.Background()); err != nil {
|
||||
return nil, fmt.Errorf("failed to load connected users: %w", err)
|
||||
}
|
||||
|
||||
// If current version is same as upstream stable version - do nothing.
|
||||
if version.Version.Equal(semver.MustParse(constants.Version)) {
|
||||
return nil
|
||||
if err := bridge.serveIMAP(); err != nil {
|
||||
bridge.PushError(ErrServeIMAP)
|
||||
}
|
||||
|
||||
if err := b.updater.InstallUpdate(version); err != nil {
|
||||
return err
|
||||
if err := bridge.serveSMTP(); err != nil {
|
||||
bridge.PushError(ErrServeSMTP)
|
||||
}
|
||||
|
||||
return b.versioner.RemoveOtherVersions(version.Version)
|
||||
if err := bridge.watchForUpdates(); err != nil {
|
||||
bridge.PushError(ErrWatchUpdates)
|
||||
}
|
||||
|
||||
return bridge, nil
|
||||
}
|
||||
|
||||
// FactoryReset will remove all local cache and settings.
|
||||
// It will also downgrade to latest stable version if user is on early version.
|
||||
func (b *Bridge) FactoryReset() {
|
||||
wasEarly := b.GetUpdateChannel() == updater.EarlyChannel
|
||||
// GetEvents returns a channel of events of the given type.
|
||||
// If no types are supplied, all events are returned.
|
||||
func (bridge *Bridge) GetEvents(ofType ...events.Event) (<-chan events.Event, func()) {
|
||||
newWatcher := bridge.addWatcher(ofType...)
|
||||
|
||||
b.settings.Set(settings.UpdateChannelKey, string(updater.StableChannel))
|
||||
return newWatcher.GetChannel(), func() { bridge.remWatcher(newWatcher) }
|
||||
}
|
||||
|
||||
if wasEarly {
|
||||
if err := b.resetToLatestStable(); err != nil {
|
||||
log.WithError(err).Error("Failed to reset to latest stable version")
|
||||
func (bridge *Bridge) FactoryReset(ctx context.Context) error {
|
||||
panic("TODO")
|
||||
}
|
||||
|
||||
func (bridge *Bridge) PushError(err error) {
|
||||
bridge.errors = append(bridge.errors, err)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetErrors() []error {
|
||||
return bridge.errors
|
||||
}
|
||||
|
||||
func (bridge *Bridge) Close(ctx context.Context) error {
|
||||
// Close the IMAP server.
|
||||
if err := bridge.closeIMAP(ctx); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close IMAP server")
|
||||
}
|
||||
|
||||
// Close the SMTP server.
|
||||
if err := bridge.closeSMTP(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close SMTP server")
|
||||
}
|
||||
|
||||
// Close all users.
|
||||
for _, user := range bridge.users {
|
||||
if err := user.Close(ctx); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close user")
|
||||
}
|
||||
}
|
||||
|
||||
if err := b.Users.ClearData(); err != nil {
|
||||
log.WithError(err).Error("Failed to remove bridge data")
|
||||
// Persist the cookies.
|
||||
if err := bridge.cookieJar.PersistCookies(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to persist cookies")
|
||||
}
|
||||
|
||||
if err := b.Users.ClearUsers(); err != nil {
|
||||
log.WithError(err).Error("Failed to remove bridge users")
|
||||
// Close the focus service.
|
||||
bridge.focusService.Close()
|
||||
|
||||
// Save the last version of bridge that was run.
|
||||
if err := bridge.vault.SetLastVersion(bridge.curVersion); err != nil {
|
||||
logrus.WithError(err).Error("Failed to save last version")
|
||||
}
|
||||
}
|
||||
|
||||
// GetKeychainApp returns current keychain helper.
|
||||
func (b *Bridge) GetKeychainApp() string {
|
||||
return b.settings.Get(settings.PreferredKeychainKey)
|
||||
}
|
||||
|
||||
// SetKeychainApp sets current keychain helper.
|
||||
func (b *Bridge) SetKeychainApp(helper string) {
|
||||
b.settings.Set(settings.PreferredKeychainKey, helper)
|
||||
}
|
||||
|
||||
func (b *Bridge) EnableCache() error {
|
||||
if err := b.Users.EnableCache(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.settings.SetBool(settings.CacheEnabledKey, true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bridge) DisableCache() error {
|
||||
if err := b.Users.DisableCache(); err != nil {
|
||||
return err
|
||||
}
|
||||
func (bridge *Bridge) publish(event events.Event) {
|
||||
bridge.watchersLock.RLock()
|
||||
defer bridge.watchersLock.RUnlock()
|
||||
|
||||
b.settings.SetBool(settings.CacheEnabledKey, false)
|
||||
// Reset back to the default location when disabling.
|
||||
b.settings.Set(settings.CacheLocationKey, b.cacheProvider.GetDefaultMessageCacheDir())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bridge) MigrateCache(from, to string) error {
|
||||
if err := b.Users.MigrateCache(from, to); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.settings.Set(settings.CacheLocationKey, to)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetProxyAllowed instructs the app whether to use DoH to access an API proxy if necessary.
|
||||
// It also needs to work before the app is initialised (because we may need to use the proxy at startup).
|
||||
func (b *Bridge) SetProxyAllowed(proxyAllowed bool) {
|
||||
b.settings.SetBool(settings.AllowProxyKey, proxyAllowed)
|
||||
if proxyAllowed {
|
||||
b.clientManager.AllowProxy()
|
||||
} else {
|
||||
b.clientManager.DisallowProxy()
|
||||
}
|
||||
}
|
||||
|
||||
// GetProxyAllowed returns whether use of DoH is enabled to access an API proxy if necessary.
|
||||
func (b *Bridge) GetProxyAllowed() bool {
|
||||
return b.settings.GetBool(settings.AllowProxyKey)
|
||||
}
|
||||
|
||||
// AddError add an error to a global error list if it does not contain it yet. Adding nil is noop.
|
||||
func (b *Bridge) AddError(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if b.HasError(err) {
|
||||
return
|
||||
}
|
||||
|
||||
b.errors = append(b.errors, err)
|
||||
}
|
||||
|
||||
// DelError removes an error from global error list.
|
||||
func (b *Bridge) DelError(err error) {
|
||||
for idx, val := range b.errors {
|
||||
if val == err {
|
||||
b.errors = append(b.errors[:idx], b.errors[idx+1:]...)
|
||||
return
|
||||
for _, watcher := range bridge.watchers {
|
||||
if watcher.IsWatching(event) {
|
||||
if ok := watcher.Send(event); !ok {
|
||||
logrus.WithField("event", event).Warn("Failed to send event to watcher")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HasError returnes true if global error list contains an err.
|
||||
func (b *Bridge) HasError(err error) bool {
|
||||
for _, val := range b.errors {
|
||||
if val == err {
|
||||
return true
|
||||
}
|
||||
func (bridge *Bridge) addWatcher(ofType ...events.Event) *watcher.Watcher[events.Event] {
|
||||
bridge.watchersLock.Lock()
|
||||
defer bridge.watchersLock.Unlock()
|
||||
|
||||
newWatcher := watcher.New(ofType...)
|
||||
|
||||
bridge.watchers = append(bridge.watchers, newWatcher)
|
||||
|
||||
return newWatcher
|
||||
}
|
||||
|
||||
func (bridge *Bridge) remWatcher(oldWatcher *watcher.Watcher[events.Event]) {
|
||||
bridge.watchersLock.Lock()
|
||||
defer bridge.watchersLock.Unlock()
|
||||
|
||||
bridge.watchers = xslices.Filter(bridge.watchers, func(other *watcher.Watcher[events.Event]) bool {
|
||||
return other != oldWatcher
|
||||
})
|
||||
}
|
||||
|
||||
func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
||||
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert(), vault.GetBridgeTLSKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return false
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetLastVersion returns the version which was used in previous execution of
|
||||
// Bridge.
|
||||
func (b *Bridge) GetLastVersion() string {
|
||||
return b.lastVersion
|
||||
}
|
||||
func newListener(port int, useTLS bool, tlsConfig *tls.Config) (net.Listener, error) {
|
||||
if useTLS {
|
||||
tlsListener, err := tls.Listen("tcp", fmt.Sprintf(":%v", port), tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// IsFirstStart returns true when Bridge is running for first time or after
|
||||
// factory reset.
|
||||
func (b *Bridge) IsFirstStart() bool {
|
||||
return b.isFirstStart
|
||||
}
|
||||
return tlsListener, nil
|
||||
}
|
||||
|
||||
// IsAllMailVisible can be called extensively by IMAP. Therefore, it is better
|
||||
// to cache the value instead of reading from settings file.
|
||||
func (b *Bridge) IsAllMailVisible() bool {
|
||||
return b.isAllMailVisible
|
||||
}
|
||||
netListener, err := net.Listen("tcp", fmt.Sprintf(":%v", port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (b *Bridge) SetIsAllMailVisible(isVisible bool) {
|
||||
b.settings.SetBool(settings.IsAllMailVisible, isVisible)
|
||||
b.isAllMailVisible = isVisible
|
||||
return netListener, nil
|
||||
}
|
||||
|
||||
362
internal/bridge/bridge_test.go
Normal file
362
internal/bridge/bridge_test.go
Normal file
@ -0,0 +1,362 @@
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/focus"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gitlab.protontech.ch/go/liteapi"
|
||||
"gitlab.protontech.ch/go/liteapi/server"
|
||||
)
|
||||
|
||||
const (
|
||||
username = "username"
|
||||
password = "password"
|
||||
)
|
||||
|
||||
var (
|
||||
v2_3_0 = semver.MustParse("2.3.0")
|
||||
v2_4_0 = semver.MustParse("2.4.0")
|
||||
)
|
||||
|
||||
func TestBridge_ConnStatus(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Get a stream of connection status events.
|
||||
eventCh, done := bridge.GetEvents(events.ConnStatus{})
|
||||
defer done()
|
||||
|
||||
// Simulate network disconnect.
|
||||
mocks.TLSDialer.SetCanDial(false)
|
||||
|
||||
// Trigger some operation that will fail due to the network disconnect.
|
||||
_, err := bridge.LoginUser(context.Background(), username, password, nil, nil)
|
||||
require.Error(t, err)
|
||||
|
||||
// Wait for the event.
|
||||
require.Equal(t, events.ConnStatus{Status: liteapi.StatusDown}, <-eventCh)
|
||||
|
||||
// Simulate network reconnect.
|
||||
mocks.TLSDialer.SetCanDial(true)
|
||||
|
||||
// Trigger some operation that will succeed due to the network reconnect.
|
||||
userID, err := bridge.LoginUser(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, userID)
|
||||
|
||||
// Wait for the event.
|
||||
require.Equal(t, events.ConnStatus{Status: liteapi.StatusUp}, <-eventCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_TLSIssue(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Get a stream of TLS issue events.
|
||||
tlsEventCh, done := bridge.GetEvents(events.TLSIssue{})
|
||||
defer done()
|
||||
|
||||
// Simulate a TLS issue.
|
||||
go func() {
|
||||
mocks.TLSIssueCh <- struct{}{}
|
||||
}()
|
||||
|
||||
// Wait for the event.
|
||||
require.IsType(t, events.TLSIssue{}, <-tlsEventCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Focus(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Get a stream of TLS issue events.
|
||||
raiseCh, done := bridge.GetEvents(events.Raise{})
|
||||
defer done()
|
||||
|
||||
// Simulate a focus event.
|
||||
focus.TryRaise()
|
||||
|
||||
// Wait for the event.
|
||||
require.IsType(t, events.Raise{}, <-raiseCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_UserAgent(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) {
|
||||
var calls []server.Call
|
||||
|
||||
s.AddCallWatcher(func(call server.Call) {
|
||||
calls = append(calls, call)
|
||||
})
|
||||
|
||||
withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Set the platform to something other than the default.
|
||||
bridge.SetCurrentPlatform("platform")
|
||||
|
||||
// Assert that the user agent then contains the platform.
|
||||
require.Contains(t, bridge.GetCurrentUserAgent(), "platform")
|
||||
|
||||
// Login the user.
|
||||
_, err := bridge.LoginUser(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert that the user agent was sent to the API.
|
||||
require.Contains(t, calls[len(calls)-1].Request.Header.Get("User-Agent"), bridge.GetCurrentUserAgent())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Cookies(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) {
|
||||
var calls []server.Call
|
||||
|
||||
s.AddCallWatcher(func(call server.Call) {
|
||||
calls = append(calls, call)
|
||||
})
|
||||
|
||||
var sessionID string
|
||||
|
||||
// Start bridge and add a user so that API assigns us a session ID via cookie.
|
||||
withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
_, err := bridge.LoginUser(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
cookie, err := calls[len(calls)-1].Request.Cookie("Session-Id")
|
||||
require.NoError(t, err)
|
||||
|
||||
sessionID = cookie.Value
|
||||
})
|
||||
|
||||
// Start bridge again and check that it uses the same session ID.
|
||||
withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
cookie, err := calls[len(calls)-1].Request.Cookie("Session-Id")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, sessionID, cookie.Value)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_CheckUpdate(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Disable autoupdate for this test.
|
||||
require.NoError(t, bridge.SetAutoUpdate(false))
|
||||
|
||||
// Get a stream of update events.
|
||||
updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{}, events.UpdateAvailable{})
|
||||
defer done()
|
||||
|
||||
// We are currently on the latest version.
|
||||
bridge.CheckForUpdates()
|
||||
require.Equal(t, events.UpdateNotAvailable{}, <-updateCh)
|
||||
|
||||
// Simulate a new version being available.
|
||||
mocks.Updater.SetLatestVersion(v2_4_0, v2_3_0)
|
||||
|
||||
// Check for updates.
|
||||
bridge.CheckForUpdates()
|
||||
require.Equal(t, events.UpdateAvailable{
|
||||
Version: updater.VersionInfo{
|
||||
Version: v2_4_0,
|
||||
MinAuto: v2_3_0,
|
||||
RolloutProportion: 1.0,
|
||||
},
|
||||
CanInstall: true,
|
||||
}, <-updateCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_AutoUpdate(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Enable autoupdate for this test.
|
||||
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||
|
||||
// Get a stream of update events.
|
||||
updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{}, events.UpdateInstalled{})
|
||||
defer done()
|
||||
|
||||
// Simulate a new version being available.
|
||||
mocks.Updater.SetLatestVersion(v2_4_0, v2_3_0)
|
||||
|
||||
// Check for updates.
|
||||
bridge.CheckForUpdates()
|
||||
require.Equal(t, events.UpdateInstalled{
|
||||
Version: updater.VersionInfo{
|
||||
Version: v2_4_0,
|
||||
MinAuto: v2_3_0,
|
||||
RolloutProportion: 1.0,
|
||||
},
|
||||
}, <-updateCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_ManualUpdate(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Disable autoupdate for this test.
|
||||
require.NoError(t, bridge.SetAutoUpdate(false))
|
||||
|
||||
// Get a stream of update events.
|
||||
updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{}, events.UpdateAvailable{})
|
||||
defer done()
|
||||
|
||||
// Simulate a new version being available, but it's too new for us.
|
||||
mocks.Updater.SetLatestVersion(v2_4_0, v2_4_0)
|
||||
|
||||
// Check for updates.
|
||||
bridge.CheckForUpdates()
|
||||
require.Equal(t, events.UpdateAvailable{
|
||||
Version: updater.VersionInfo{
|
||||
Version: v2_4_0,
|
||||
MinAuto: v2_4_0,
|
||||
RolloutProportion: 1.0,
|
||||
},
|
||||
CanInstall: false,
|
||||
}, <-updateCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_ForceUpdate(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Get a stream of update events.
|
||||
updateCh, done := bridge.GetEvents(events.UpdateForced{})
|
||||
defer done()
|
||||
|
||||
// Set the minimum accepted app version to something newer than the current version.
|
||||
s.SetMinAppVersion(v2_4_0)
|
||||
|
||||
// Try to login the user. It will fail because the bridge is too old.
|
||||
_, err := bridge.LoginUser(context.Background(), username, password, nil, nil)
|
||||
require.Error(t, err)
|
||||
|
||||
// We should get an update required event.
|
||||
require.Equal(t, events.UpdateForced{}, <-updateCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_BadVaultKey(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) {
|
||||
var userID string
|
||||
|
||||
// Login a user.
|
||||
withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
newUserID, err := bridge.LoginUser(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
userID = newUserID
|
||||
})
|
||||
|
||||
// Start bridge with the correct vault key -- it should load the users correctly.
|
||||
withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.ElementsMatch(t, []string{userID}, bridge.GetUserIDs())
|
||||
})
|
||||
|
||||
// Start bridge with a bad vault key, the vault will be wiped and bridge will show no users.
|
||||
withBridge(t, s.GetHostURL(), locator, []byte("bad"), func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
})
|
||||
|
||||
// Start bridge with a nil vault key, the vault will be wiped and bridge will show no users.
|
||||
withBridge(t, s.GetHostURL(), locator, nil, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// withEnv creates the full test environment and runs the tests.
|
||||
func withEnv(t *testing.T, tests func(server *server.Server, locator bridge.Locator, vaultKey []byte)) {
|
||||
// Create test API.
|
||||
server := server.NewTLS()
|
||||
defer server.Close()
|
||||
|
||||
// Add test user.
|
||||
_, _, err := server.AddUser(username, password, username+"@pm.me")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate a random vault key.
|
||||
vaultKey, err := crypto.RandomToken(32)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Run the tests.
|
||||
tests(server, locations.New(bridge.NewTestLocationsProvider(t), "config-name"), vaultKey)
|
||||
}
|
||||
|
||||
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
|
||||
func withBridge(t *testing.T, apiURL string, locator bridge.Locator, vaultKey []byte, tests func(bridge *bridge.Bridge, mocks *bridge.Mocks)) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Create the mock objects used in the tests.
|
||||
mocks := bridge.NewMocks(t, v2_3_0, v2_3_0)
|
||||
|
||||
// Bridge will enable the proxy by default at startup.
|
||||
mocks.ProxyDialer.EXPECT().AllowProxy()
|
||||
|
||||
// Get the path to the vault.
|
||||
vaultDir, err := locator.ProvideSettingsPath()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create the vault.
|
||||
vault, _, err := vault.New(vaultDir, t.TempDir(), vaultKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a new bridge.
|
||||
bridge, err := bridge.New(
|
||||
apiURL,
|
||||
locator,
|
||||
vault,
|
||||
useragent.New(),
|
||||
mocks.TLSReporter,
|
||||
mocks.ProxyDialer,
|
||||
mocks.Autostarter,
|
||||
mocks.Updater,
|
||||
v2_3_0,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use the bridge.
|
||||
tests(bridge, mocks)
|
||||
|
||||
// Close the bridge.
|
||||
require.NoError(t, bridge.Close(ctx))
|
||||
}
|
||||
|
||||
// must is a helper function that panics on error.
|
||||
func must[T any](val T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func getConnectedUserIDs(t *testing.T, bridge *bridge.Bridge) []string {
|
||||
t.Helper()
|
||||
|
||||
return xslices.Filter(bridge.GetUserIDs(), func(userID string) bool {
|
||||
info, err := bridge.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
|
||||
return info.Connected
|
||||
})
|
||||
}
|
||||
@ -21,67 +21,51 @@ import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
|
||||
"gitlab.protontech.ch/go/liteapi"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxAttachmentSize = 7 * 1024 * 1024 // MaxAttachmentSize 7 MB total limit
|
||||
MaxAttachmentSize = 7 * (1 << 20) // MaxAttachmentSize 7 MB total size of all attachments.
|
||||
MaxCompressedFilesCount = 6
|
||||
)
|
||||
|
||||
var ErrSizeTooLarge = errors.New("file is too big")
|
||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error {
|
||||
var account string
|
||||
|
||||
// ReportBug reports a new bug from the user.
|
||||
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 {
|
||||
accountName = users[0].Username()
|
||||
if info, err := bridge.QueryUserInfo(username); err == nil {
|
||||
account = info.Username
|
||||
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
|
||||
account = bridge.users[userIDs[0]].Name()
|
||||
}
|
||||
|
||||
report := pmapi.ReportBugReq{
|
||||
OS: osType,
|
||||
OSVersion: osVersion,
|
||||
Browser: emailClient,
|
||||
Title: "[Bridge] Bug",
|
||||
Description: description,
|
||||
Username: accountName,
|
||||
Email: address,
|
||||
}
|
||||
var atts []liteapi.ReportBugAttachment
|
||||
|
||||
if attachLogs {
|
||||
logs, err := b.getMatchingLogs(
|
||||
func(filename string) bool {
|
||||
return logging.MatchLogName(filename) && !logging.MatchStackTraceName(filename)
|
||||
},
|
||||
)
|
||||
logs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
||||
return logging.MatchLogName(filename) && !logging.MatchStackTraceName(filename)
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Can't get log files list")
|
||||
return err
|
||||
}
|
||||
|
||||
guiLogs, err := b.getMatchingLogs(
|
||||
func(filename string) bool {
|
||||
return logging.MatchGUILogName(filename) && !logging.MatchStackTraceName(filename)
|
||||
},
|
||||
)
|
||||
guiLogs, err := getMatchingLogs(bridge.locator, 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")
|
||||
return err
|
||||
}
|
||||
|
||||
crashes, err := b.getMatchingLogs(
|
||||
func(filename string) bool {
|
||||
return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename)
|
||||
},
|
||||
)
|
||||
crashes, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
||||
return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename)
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Can't get crash files list")
|
||||
return err
|
||||
}
|
||||
|
||||
var matchFiles []string
|
||||
@ -95,26 +79,42 @@ func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address,
|
||||
|
||||
archive, err := zipFiles(matchFiles)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Can't zip logs and crashes")
|
||||
return err
|
||||
}
|
||||
|
||||
if archive != nil {
|
||||
report.AddAttachment("logs.zip", "application/zip", archive)
|
||||
body, err := io.ReadAll(archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
atts = append(atts, liteapi.ReportBugAttachment{
|
||||
Name: "logs.zip",
|
||||
Filename: "logs.zip",
|
||||
MIMEType: "application/zip",
|
||||
Body: body,
|
||||
})
|
||||
}
|
||||
|
||||
return b.clientManager.ReportBug(context.Background(), report)
|
||||
return bridge.api.ReportBug(ctx, liteapi.ReportBugReq{
|
||||
OS: osType,
|
||||
OSVersion: osVersion,
|
||||
Description: description,
|
||||
Client: client,
|
||||
Username: account,
|
||||
Email: email,
|
||||
}, atts...)
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Bridge) getMatchingLogs(filenameMatchFunc func(string) bool) (filenames []string, err error) {
|
||||
logsPath, err := b.locations.ProvideLogsPath()
|
||||
func getMatchingLogs(locator Locator, filenameMatchFunc func(string) bool) (filenames []string, err error) {
|
||||
logsPath, err := locator.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -131,24 +131,25 @@ func (b *Bridge) getMatchingLogs(filenameMatchFunc func(string) bool) (filenames
|
||||
matchFiles = append(matchFiles, filepath.Join(logsPath, file.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(matchFiles) // Sorted by timestamp: oldest first.
|
||||
|
||||
return matchFiles, nil
|
||||
}
|
||||
|
||||
type LimitedBuffer struct {
|
||||
type limitedBuffer struct {
|
||||
capacity int
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
func NewLimitedBuffer(capacity int) *LimitedBuffer {
|
||||
return &LimitedBuffer{
|
||||
func newLimitedBuffer(capacity int) *limitedBuffer {
|
||||
return &limitedBuffer{
|
||||
capacity: capacity,
|
||||
buf: bytes.NewBuffer(make([]byte, 0, capacity)),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LimitedBuffer) Write(p []byte) (n int, err error) {
|
||||
func (b *limitedBuffer) Write(p []byte) (n int, err error) {
|
||||
if len(p)+b.buf.Len() > b.capacity {
|
||||
return 0, ErrSizeTooLarge
|
||||
}
|
||||
@ -156,7 +157,7 @@ func (b *LimitedBuffer) Write(p []byte) (n int, err error) {
|
||||
return b.buf.Write(p)
|
||||
}
|
||||
|
||||
func (b *LimitedBuffer) Read(p []byte) (n int, err error) {
|
||||
func (b *limitedBuffer) Read(p []byte) (n int, err error) {
|
||||
return b.buf.Read(p)
|
||||
}
|
||||
|
||||
@ -165,14 +166,13 @@ func zipFiles(filenames []string) (io.Reader, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
buf := NewLimitedBuffer(MaxAttachmentSize)
|
||||
buf := newLimitedBuffer(MaxAttachmentSize)
|
||||
|
||||
w := zip.NewWriter(buf)
|
||||
defer w.Close() //nolint:errcheck
|
||||
|
||||
for _, file := range filenames {
|
||||
err := addFileToZip(file, w)
|
||||
if err != nil {
|
||||
if err := addFileToZip(file, w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@ -209,12 +209,9 @@ func addFileToZip(filename string, writer *zip.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(fileWriter, fileReader)
|
||||
if err != nil {
|
||||
if _, err := io.Copy(fileWriter, fileReader); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = fileReader.Close()
|
||||
|
||||
return err
|
||||
return fileReader.Close()
|
||||
}
|
||||
|
||||
@ -1,70 +1,38 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/clientconfig"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/useragent"
|
||||
)
|
||||
|
||||
func (b *Bridge) ConfigureAppleMail(userID, address string) (bool, error) {
|
||||
user, err := b.GetUser(userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
func (bridge *Bridge) ConfigureAppleMail(userID, address string) error {
|
||||
user, ok := bridge.users[userID]
|
||||
if !ok {
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
if address == "" {
|
||||
address = user.GetPrimaryAddress()
|
||||
address = user.Addresses()[0]
|
||||
}
|
||||
|
||||
username := address
|
||||
addresses := address
|
||||
|
||||
if user.IsCombinedAddressMode() {
|
||||
username = user.GetPrimaryAddress()
|
||||
addresses = strings.Join(user.GetAddresses(), ",")
|
||||
}
|
||||
|
||||
var (
|
||||
restart = false
|
||||
smtpSSL = b.settings.GetBool(settings.SMTPSSLKey)
|
||||
)
|
||||
|
||||
// If configuring apple mail for Catalina or newer, users should use SSL.
|
||||
if useragent.IsCatalinaOrNewer() && !smtpSSL {
|
||||
smtpSSL = true
|
||||
restart = true
|
||||
b.settings.SetBool(settings.SMTPSSLKey, true)
|
||||
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
|
||||
if err := bridge.SetSMTPSSL(true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := (&clientconfig.AppleMail{}).Configure(
|
||||
Host,
|
||||
b.settings.GetInt(settings.IMAPPortKey),
|
||||
b.settings.GetInt(settings.SMTPPortKey),
|
||||
false, smtpSSL,
|
||||
username, addresses,
|
||||
user.GetBridgePassword(),
|
||||
); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return restart, nil
|
||||
return (&clientconfig.AppleMail{}).Configure(
|
||||
constants.Host,
|
||||
bridge.vault.GetIMAPPort(),
|
||||
bridge.vault.GetSMTPPort(),
|
||||
bridge.vault.GetIMAPSSL(),
|
||||
bridge.vault.GetSMTPSSL(),
|
||||
address,
|
||||
strings.Join(user.Addresses(), ","),
|
||||
user.BridgePass(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
// Host settings.
|
||||
const (
|
||||
Host = "127.0.0.1"
|
||||
)
|
||||
16
internal/bridge/errors.go
Normal file
16
internal/bridge/errors.go
Normal file
@ -0,0 +1,16 @@
|
||||
package bridge
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrServeIMAP = errors.New("failed to serve IMAP")
|
||||
ErrServeSMTP = errors.New("failed to serve SMTP")
|
||||
ErrWatchUpdates = errors.New("failed to watch for updates")
|
||||
|
||||
ErrNoSuchUser = errors.New("no such user")
|
||||
ErrUserAlreadyExists = errors.New("user already exists")
|
||||
ErrUserAlreadyLoggedIn = errors.New("user already logged in")
|
||||
ErrNotImplemented = errors.New("not implemented")
|
||||
|
||||
ErrSizeTooLarge = errors.New("file is too big")
|
||||
)
|
||||
67
internal/bridge/files.go
Normal file
67
internal/bridge/files.go
Normal file
@ -0,0 +1,67 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func moveDir(from, to string) error {
|
||||
entries, err := os.ReadDir(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
if err := os.Mkdir(filepath.Join(to, entry.Name()), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := moveDir(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(filepath.Join(from, entry.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := move(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return os.Remove(from)
|
||||
}
|
||||
|
||||
func move(from, to string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(to), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Open(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
c, err := os.Create(to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
if err := os.Chmod(to, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := c.ReadFrom(f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Remove(from); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
56
internal/bridge/files_test.go
Normal file
56
internal/bridge/files_test.go
Normal file
@ -0,0 +1,56 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMoveDir(t *testing.T) {
|
||||
from, to := t.TempDir(), t.TempDir()
|
||||
|
||||
// Create some files in from.
|
||||
if err := os.WriteFile(filepath.Join(from, "a"), []byte("a"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(from, "b"), []byte("b"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(from, "c"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(from, "c", "d"), []byte("d"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Move the files.
|
||||
if err := moveDir(from, to); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the files were moved.
|
||||
if _, err := os.Stat(filepath.Join(from, "a")); !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(to, "a")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(from, "b")); !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(to, "b")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(from, "c")); !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(to, "c")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(from, "c", "d")); !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(to, "c", "d")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
117
internal/bridge/imap.go
Normal file
117
internal/bridge/imap.go
Normal file
@ -0,0 +1,117 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon"
|
||||
imapEvents "github.com/ProtonMail/gluon/events"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultClientName = "UnknownClient"
|
||||
defaultClientVersion = "0.0.1"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) GetIMAPPort() int {
|
||||
return bridge.vault.GetIMAPPort()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetIMAPPort(newPort int) error {
|
||||
if newPort == bridge.vault.GetIMAPPort() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetIMAPPort(newPort); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bridge.restartIMAP(context.Background())
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetIMAPSSL() bool {
|
||||
return bridge.vault.GetIMAPSSL()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetIMAPSSL(newSSL bool) error {
|
||||
if newSSL == bridge.vault.GetIMAPSSL() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetIMAPSSL(newSSL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bridge.restartIMAP(context.Background())
|
||||
}
|
||||
|
||||
func (bridge *Bridge) serveIMAP() error {
|
||||
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create IMAP listener: %w", err)
|
||||
}
|
||||
|
||||
bridge.imapListener = imapListener
|
||||
|
||||
return bridge.imapServer.Serve(context.Background(), bridge.imapListener)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) restartIMAP(ctx context.Context) error {
|
||||
if err := bridge.imapListener.Close(); err != nil {
|
||||
logrus.WithError(err).Warn("Failed to close IMAP listener")
|
||||
}
|
||||
|
||||
return bridge.serveIMAP()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) closeIMAP(ctx context.Context) error {
|
||||
if err := bridge.imapServer.Close(ctx); err != nil {
|
||||
logrus.WithError(err).Warn("Failed to close IMAP server")
|
||||
}
|
||||
|
||||
if err := bridge.imapListener.Close(); err != nil {
|
||||
logrus.WithError(err).Warn("Failed to close IMAP listener")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
||||
switch event := event.(type) {
|
||||
case imapEvents.SessionAdded:
|
||||
if !bridge.identifier.HasClient() {
|
||||
bridge.identifier.SetClient(defaultClientName, defaultClientVersion)
|
||||
}
|
||||
|
||||
case imapEvents.IMAPID:
|
||||
bridge.identifier.SetClient(event.IMAPID.Name, event.IMAPID.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func newIMAPServer(gluonDir string, version *semver.Version, tlsConfig *tls.Config) (*gluon.Server, error) {
|
||||
imapServer, err := gluon.New(
|
||||
gluon.WithTLS(tlsConfig),
|
||||
gluon.WithDataDir(gluonDir),
|
||||
gluon.WithVersionInfo(
|
||||
int(version.Major()),
|
||||
int(version.Minor()),
|
||||
int(version.Patch()),
|
||||
constants.FullAppName,
|
||||
"TODO",
|
||||
"TODO",
|
||||
),
|
||||
gluon.WithLogger(
|
||||
logrus.StandardLogger().WriterLevel(logrus.InfoLevel),
|
||||
logrus.StandardLogger().WriterLevel(logrus.InfoLevel),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return imapServer, nil
|
||||
}
|
||||
@ -1,30 +1,13 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
func (b *Bridge) ProvideLogsPath() (string, error) {
|
||||
return b.locations.ProvideLogsPath()
|
||||
func (bridge *Bridge) GetLogsPath() (string, error) {
|
||||
return bridge.locator.ProvideLogsPath()
|
||||
}
|
||||
|
||||
func (b *Bridge) GetLicenseFilePath() string {
|
||||
return b.locations.GetLicenseFilePath()
|
||||
func (bridge *Bridge) GetLicenseFilePath() string {
|
||||
return bridge.locator.GetLicenseFilePath()
|
||||
}
|
||||
|
||||
func (b *Bridge) GetDependencyLicensesLink() string {
|
||||
return b.locations.GetDependencyLicensesLink()
|
||||
func (bridge *Bridge) GetDependencyLicensesLink() string {
|
||||
return bridge.locator.GetDependencyLicensesLink()
|
||||
}
|
||||
|
||||
127
internal/bridge/mocks.go
Normal file
127
internal/bridge/mocks.go
Normal file
@ -0,0 +1,127 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge/mocks"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
type Mocks struct {
|
||||
TLSDialer *TestDialer
|
||||
ProxyDialer *mocks.MockProxyDialer
|
||||
|
||||
TLSReporter *mocks.MockTLSReporter
|
||||
TLSIssueCh chan struct{}
|
||||
|
||||
Updater *TestUpdater
|
||||
Autostarter *mocks.MockAutostarter
|
||||
}
|
||||
|
||||
func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
||||
ctl := gomock.NewController(tb)
|
||||
|
||||
mocks := &Mocks{
|
||||
TLSDialer: NewTestDialer(),
|
||||
ProxyDialer: mocks.NewMockProxyDialer(ctl),
|
||||
|
||||
TLSReporter: mocks.NewMockTLSReporter(ctl),
|
||||
TLSIssueCh: make(chan struct{}),
|
||||
|
||||
Updater: NewTestUpdater(version, minAuto),
|
||||
Autostarter: mocks.NewMockAutostarter(ctl),
|
||||
}
|
||||
|
||||
// When using the proxy dialer, we want to use the test dialer.
|
||||
mocks.ProxyDialer.EXPECT().DialTLSContext(
|
||||
gomock.Any(),
|
||||
gomock.Any(),
|
||||
gomock.Any(),
|
||||
).DoAndReturn(func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return mocks.TLSDialer.DialTLSContext(ctx, network, address)
|
||||
}).AnyTimes()
|
||||
|
||||
// When getting the TLS issue channel, we want to return the test channel.
|
||||
mocks.TLSReporter.EXPECT().GetTLSIssueCh().Return(mocks.TLSIssueCh).AnyTimes()
|
||||
|
||||
return mocks
|
||||
}
|
||||
|
||||
type TestDialer struct {
|
||||
canDial bool
|
||||
}
|
||||
|
||||
func NewTestDialer() *TestDialer {
|
||||
return &TestDialer{
|
||||
canDial: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *TestDialer) DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
||||
if !d.canDial {
|
||||
return nil, errors.New("cannot dial")
|
||||
}
|
||||
|
||||
return (&tls.Dialer{Config: &tls.Config{InsecureSkipVerify: true}}).DialContext(ctx, network, address)
|
||||
}
|
||||
|
||||
func (d *TestDialer) SetCanDial(canDial bool) {
|
||||
d.canDial = canDial
|
||||
}
|
||||
|
||||
type TestLocationsProvider struct {
|
||||
config, cache string
|
||||
}
|
||||
|
||||
func NewTestLocationsProvider(tb testing.TB) *TestLocationsProvider {
|
||||
return &TestLocationsProvider{
|
||||
config: tb.TempDir(),
|
||||
cache: tb.TempDir(),
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *TestLocationsProvider) UserConfig() string {
|
||||
return provider.config
|
||||
}
|
||||
|
||||
func (provider *TestLocationsProvider) UserCache() string {
|
||||
return provider.cache
|
||||
}
|
||||
|
||||
type TestUpdater struct {
|
||||
latest updater.VersionInfo
|
||||
}
|
||||
|
||||
func NewTestUpdater(version, minAuto *semver.Version) *TestUpdater {
|
||||
return &TestUpdater{
|
||||
latest: updater.VersionInfo{
|
||||
Version: version,
|
||||
MinAuto: minAuto,
|
||||
|
||||
RolloutProportion: 1.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (testUpdater *TestUpdater) SetLatestVersion(version, minAuto *semver.Version) {
|
||||
testUpdater.latest = updater.VersionInfo{
|
||||
Version: version,
|
||||
MinAuto: minAuto,
|
||||
|
||||
RolloutProportion: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
func (updater *TestUpdater) GetVersionInfo(downloader updater.Downloader, channel updater.Channel) (updater.VersionInfo, error) {
|
||||
return updater.latest, nil
|
||||
}
|
||||
|
||||
func (updater *TestUpdater) InstallUpdate(downloader updater.Downloader, update updater.VersionInfo) error {
|
||||
return nil
|
||||
}
|
||||
163
internal/bridge/mocks/mocks.go
Normal file
163
internal/bridge/mocks/mocks.go
Normal file
@ -0,0 +1,163 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/ProtonMail/proton-bridge/v2/internal/bridge (interfaces: TLSReporter,ProxyDialer,Autostarter)
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
net "net"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockTLSReporter is a mock of TLSReporter interface.
|
||||
type MockTLSReporter struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockTLSReporterMockRecorder
|
||||
}
|
||||
|
||||
// MockTLSReporterMockRecorder is the mock recorder for MockTLSReporter.
|
||||
type MockTLSReporterMockRecorder struct {
|
||||
mock *MockTLSReporter
|
||||
}
|
||||
|
||||
// NewMockTLSReporter creates a new mock instance.
|
||||
func NewMockTLSReporter(ctrl *gomock.Controller) *MockTLSReporter {
|
||||
mock := &MockTLSReporter{ctrl: ctrl}
|
||||
mock.recorder = &MockTLSReporterMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockTLSReporter) EXPECT() *MockTLSReporterMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetTLSIssueCh mocks base method.
|
||||
func (m *MockTLSReporter) GetTLSIssueCh() <-chan struct{} {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetTLSIssueCh")
|
||||
ret0, _ := ret[0].(<-chan struct{})
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetTLSIssueCh indicates an expected call of GetTLSIssueCh.
|
||||
func (mr *MockTLSReporterMockRecorder) GetTLSIssueCh() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTLSIssueCh", reflect.TypeOf((*MockTLSReporter)(nil).GetTLSIssueCh))
|
||||
}
|
||||
|
||||
// MockProxyDialer is a mock of ProxyDialer interface.
|
||||
type MockProxyDialer struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockProxyDialerMockRecorder
|
||||
}
|
||||
|
||||
// MockProxyDialerMockRecorder is the mock recorder for MockProxyDialer.
|
||||
type MockProxyDialerMockRecorder struct {
|
||||
mock *MockProxyDialer
|
||||
}
|
||||
|
||||
// NewMockProxyDialer creates a new mock instance.
|
||||
func NewMockProxyDialer(ctrl *gomock.Controller) *MockProxyDialer {
|
||||
mock := &MockProxyDialer{ctrl: ctrl}
|
||||
mock.recorder = &MockProxyDialerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockProxyDialer) EXPECT() *MockProxyDialerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AllowProxy mocks base method.
|
||||
func (m *MockProxyDialer) AllowProxy() {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "AllowProxy")
|
||||
}
|
||||
|
||||
// AllowProxy indicates an expected call of AllowProxy.
|
||||
func (mr *MockProxyDialerMockRecorder) AllowProxy() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllowProxy", reflect.TypeOf((*MockProxyDialer)(nil).AllowProxy))
|
||||
}
|
||||
|
||||
// DialTLSContext mocks base method.
|
||||
func (m *MockProxyDialer) DialTLSContext(arg0 context.Context, arg1, arg2 string) (net.Conn, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DialTLSContext", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(net.Conn)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// DialTLSContext indicates an expected call of DialTLSContext.
|
||||
func (mr *MockProxyDialerMockRecorder) DialTLSContext(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DialTLSContext", reflect.TypeOf((*MockProxyDialer)(nil).DialTLSContext), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// DisallowProxy mocks base method.
|
||||
func (m *MockProxyDialer) DisallowProxy() {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "DisallowProxy")
|
||||
}
|
||||
|
||||
// DisallowProxy indicates an expected call of DisallowProxy.
|
||||
func (mr *MockProxyDialerMockRecorder) DisallowProxy() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisallowProxy", reflect.TypeOf((*MockProxyDialer)(nil).DisallowProxy))
|
||||
}
|
||||
|
||||
// MockAutostarter is a mock of Autostarter interface.
|
||||
type MockAutostarter struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockAutostarterMockRecorder
|
||||
}
|
||||
|
||||
// MockAutostarterMockRecorder is the mock recorder for MockAutostarter.
|
||||
type MockAutostarterMockRecorder struct {
|
||||
mock *MockAutostarter
|
||||
}
|
||||
|
||||
// NewMockAutostarter creates a new mock instance.
|
||||
func NewMockAutostarter(ctrl *gomock.Controller) *MockAutostarter {
|
||||
mock := &MockAutostarter{ctrl: ctrl}
|
||||
mock.recorder = &MockAutostarterMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockAutostarter) EXPECT() *MockAutostarterMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Disable mocks base method.
|
||||
func (m *MockAutostarter) Disable() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Disable")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Disable indicates an expected call of Disable.
|
||||
func (mr *MockAutostarterMockRecorder) Disable() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Disable", reflect.TypeOf((*MockAutostarter)(nil).Disable))
|
||||
}
|
||||
|
||||
// Enable mocks base method.
|
||||
func (m *MockAutostarter) Enable() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Enable")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Enable indicates an expected call of Enable.
|
||||
func (mr *MockAutostarterMockRecorder) Enable() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enable", reflect.TypeOf((*MockAutostarter)(nil).Enable))
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by ./release-notes.sh at 'Fri Jan 22 11:01:06 AM CET 2021'. DO NOT EDIT.
|
||||
|
||||
package bridge
|
||||
|
||||
const ReleaseNotes = `
|
||||
`
|
||||
|
||||
const ReleaseFixedBugs = `• Fixed sending error caused by inconsistent use of upper and lower case in sender’s email address
|
||||
`
|
||||
@ -1,44 +1,175 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import "github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
import (
|
||||
"context"
|
||||
|
||||
func (b *Bridge) Get(key settings.Key) string {
|
||||
return b.settings.Get(key)
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) GetKeychainApp() (string, error) {
|
||||
vaultDir, err := bridge.locator.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return vault.GetHelper(vaultDir)
|
||||
}
|
||||
|
||||
func (b *Bridge) Set(key settings.Key, value string) {
|
||||
b.settings.Set(key, value)
|
||||
func (bridge *Bridge) SetKeychainApp(helper string) error {
|
||||
vaultDir, err := bridge.locator.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return vault.SetHelper(vaultDir, helper)
|
||||
}
|
||||
|
||||
func (b *Bridge) GetBool(key settings.Key) bool {
|
||||
return b.settings.GetBool(key)
|
||||
func (bridge *Bridge) GetGluonDir() string {
|
||||
return bridge.vault.GetGluonDir()
|
||||
}
|
||||
|
||||
func (b *Bridge) SetBool(key settings.Key, value bool) {
|
||||
b.settings.SetBool(key, value)
|
||||
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
|
||||
if newGluonDir == bridge.GetGluonDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := bridge.closeIMAP(context.Background()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := moveDir(bridge.GetGluonDir(), newGluonDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetGluonDir(newGluonDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imapServer, err := newIMAPServer(bridge.vault.GetGluonDir(), bridge.curVersion, bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range bridge.users {
|
||||
imapConn, err := user.NewGluonConnector(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := imapServer.LoadUser(context.Background(), imapConn, user.GluonID(), user.GluonKey()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
bridge.imapServer = imapServer
|
||||
|
||||
return bridge.serveIMAP()
|
||||
}
|
||||
|
||||
func (b *Bridge) GetInt(key settings.Key) int {
|
||||
return b.settings.GetInt(key)
|
||||
func (bridge *Bridge) GetProxyAllowed() bool {
|
||||
return bridge.vault.GetProxyAllowed()
|
||||
}
|
||||
|
||||
func (b *Bridge) SetInt(key settings.Key, value int) {
|
||||
b.settings.SetInt(key, value)
|
||||
func (bridge *Bridge) SetProxyAllowed(allowed bool) error {
|
||||
if allowed {
|
||||
bridge.proxyDialer.AllowProxy()
|
||||
} else {
|
||||
bridge.proxyDialer.DisallowProxy()
|
||||
}
|
||||
|
||||
return bridge.vault.SetProxyAllowed(allowed)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetShowAllMail() bool {
|
||||
return bridge.vault.GetShowAllMail()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetShowAllMail(show bool) error {
|
||||
panic("TODO")
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetAutostart() bool {
|
||||
return bridge.vault.GetAutostart()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetAutostart(autostart bool) error {
|
||||
if err := bridge.vault.SetAutostart(autostart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
if autostart {
|
||||
err = bridge.autostarter.Enable()
|
||||
} else {
|
||||
err = bridge.autostarter.Disable()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetAutoUpdate() bool {
|
||||
return bridge.vault.GetAutoUpdate()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetAutoUpdate(autoUpdate bool) error {
|
||||
if bridge.vault.GetAutoUpdate() == autoUpdate {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetAutoUpdate(autoUpdate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.updateCheckCh <- struct{}{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetUpdateChannel() updater.Channel {
|
||||
return updater.Channel(bridge.vault.GetUpdateChannel())
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetUpdateChannel(channel updater.Channel) error {
|
||||
if bridge.vault.GetUpdateChannel() == channel {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetUpdateChannel(channel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.updateCheckCh <- struct{}{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetLastVersion() *semver.Version {
|
||||
return bridge.vault.GetLastVersion()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetFirstStart() bool {
|
||||
return bridge.vault.GetFirstStart()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetFirstStart(firstStart bool) error {
|
||||
return bridge.vault.SetFirstStart(firstStart)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetFirstStartGUI() bool {
|
||||
return bridge.vault.GetFirstStartGUI()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetFirstStartGUI(firstStart bool) error {
|
||||
return bridge.vault.SetFirstStartGUI(firstStart)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetColorScheme() string {
|
||||
return bridge.vault.GetColorScheme()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetColorScheme(colorScheme string) error {
|
||||
return bridge.vault.SetColorScheme(colorScheme)
|
||||
}
|
||||
|
||||
156
internal/bridge/settings_test.go
Normal file
156
internal/bridge/settings_test.go
Normal file
@ -0,0 +1,156 @@
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gitlab.protontech.ch/go/liteapi/server"
|
||||
)
|
||||
|
||||
func TestBridge_Settings_GluonDir(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Create a user.
|
||||
_, err := bridge.LoginUser(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a new location for the Gluon data.
|
||||
newGluonDir := t.TempDir()
|
||||
|
||||
// Move the gluon dir; it should also move the user's data.
|
||||
require.NoError(t, bridge.SetGluonDir(context.Background(), newGluonDir))
|
||||
|
||||
// Check that the new directory is not empty.
|
||||
entries, err := os.ReadDir(newGluonDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// There should be at least one entry.
|
||||
require.NotEmpty(t, entries)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Settings_IMAPPort(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// By default, the port is 1143.
|
||||
require.Equal(t, 1143, bridge.GetIMAPPort())
|
||||
|
||||
// Set the port to 1144.
|
||||
require.NoError(t, bridge.SetIMAPPort(1144))
|
||||
|
||||
// Get the new setting.
|
||||
require.Equal(t, 1144, bridge.GetIMAPPort())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Settings_IMAPSSL(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// By default, IMAP SSL is disabled.
|
||||
require.False(t, bridge.GetIMAPSSL())
|
||||
|
||||
// Enable IMAP SSL.
|
||||
require.NoError(t, bridge.SetIMAPSSL(true))
|
||||
|
||||
// Get the new setting.
|
||||
require.True(t, bridge.GetIMAPSSL())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Settings_SMTPPort(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// By default, the port is 1025.
|
||||
require.Equal(t, 1025, bridge.GetSMTPPort())
|
||||
|
||||
// Set the port to 1024.
|
||||
require.NoError(t, bridge.SetSMTPPort(1024))
|
||||
|
||||
// Get the new setting.
|
||||
require.Equal(t, 1024, bridge.GetSMTPPort())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Settings_SMTPSSL(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// By default, SMTP SSL is disabled.
|
||||
require.False(t, bridge.GetSMTPSSL())
|
||||
|
||||
// Enable SMTP SSL.
|
||||
require.NoError(t, bridge.SetSMTPSSL(true))
|
||||
|
||||
// Get the new setting.
|
||||
require.True(t, bridge.GetSMTPSSL())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Settings_Proxy(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// By default, proxy is allowed.
|
||||
require.True(t, bridge.GetProxyAllowed())
|
||||
|
||||
// Disallow proxy.
|
||||
mocks.ProxyDialer.EXPECT().DisallowProxy()
|
||||
require.NoError(t, bridge.SetProxyAllowed(false))
|
||||
|
||||
// Get the new setting.
|
||||
require.False(t, bridge.GetProxyAllowed())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Settings_Autostart(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// By default, autostart is disabled.
|
||||
require.False(t, bridge.GetAutostart())
|
||||
|
||||
// Enable autostart.
|
||||
mocks.Autostarter.EXPECT().Enable().Return(nil)
|
||||
require.NoError(t, bridge.SetAutostart(true))
|
||||
|
||||
// Get the new setting.
|
||||
require.True(t, bridge.GetAutostart())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Settings_FirstStart(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// By default, first start is true.
|
||||
require.True(t, bridge.GetFirstStart())
|
||||
|
||||
// Set first start to false.
|
||||
require.NoError(t, bridge.SetFirstStart(false))
|
||||
|
||||
// Get the new setting.
|
||||
require.False(t, bridge.GetFirstStart())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Settings_FirstStartGUI(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// By default, first start is true.
|
||||
require.True(t, bridge.GetFirstStartGUI())
|
||||
|
||||
// Set first start to false.
|
||||
require.NoError(t, bridge.SetFirstStartGUI(false))
|
||||
|
||||
// Get the new setting.
|
||||
require.False(t, bridge.GetFirstStartGUI())
|
||||
})
|
||||
})
|
||||
}
|
||||
109
internal/bridge/smtp.go
Normal file
109
internal/bridge/smtp.go
Normal file
@ -0,0 +1,109 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) GetSMTPPort() int {
|
||||
return bridge.vault.GetSMTPPort()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetSMTPPort(newPort int) error {
|
||||
if newPort == bridge.vault.GetSMTPPort() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetSMTPPort(newPort); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bridge.restartSMTP()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetSMTPSSL() bool {
|
||||
return bridge.vault.GetSMTPSSL()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
|
||||
if newSSL == bridge.vault.GetSMTPSSL() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetSMTPSSL(newSSL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bridge.restartSMTP()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) serveSMTP() error {
|
||||
smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create SMTP listener: %w", err)
|
||||
}
|
||||
|
||||
bridge.smtpListener = smtpListener
|
||||
|
||||
go func() {
|
||||
if err := bridge.smtpServer.Serve(bridge.smtpListener); err != nil {
|
||||
logrus.WithError(err).Error("SMTP server stopped")
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) restartSMTP() error {
|
||||
if err := bridge.closeSMTP(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
smtpServer, err := newSMTPServer(bridge.smtpBackend, bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.smtpServer = smtpServer
|
||||
|
||||
return bridge.serveSMTP()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) closeSMTP() error {
|
||||
if err := bridge.smtpServer.Close(); err != nil {
|
||||
logrus.WithError(err).Warn("Failed to close SMTP server")
|
||||
}
|
||||
|
||||
// Don't close the SMTP listener -- it's closed by the server.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newSMTPServer(smtpBackend *smtpBackend, tlsConfig *tls.Config) (*smtp.Server, error) {
|
||||
smtpServer := smtp.NewServer(smtpBackend)
|
||||
|
||||
smtpServer.TLSConfig = tlsConfig
|
||||
smtpServer.Domain = constants.Host
|
||||
smtpServer.AllowInsecureAuth = true
|
||||
smtpServer.MaxLineLength = 1 << 16
|
||||
|
||||
smtpServer.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server {
|
||||
return sasl.NewLoginServer(func(address, password string) error {
|
||||
user, err := conn.Server().Backend.Login(nil, address, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn.SetSession(user)
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
return smtpServer, nil
|
||||
}
|
||||
70
internal/bridge/smtp_backend.go
Normal file
70
internal/bridge/smtp_backend.go
Normal file
@ -0,0 +1,70 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/user"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/emersion/go-smtp"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type smtpBackend struct {
|
||||
users []*user.User
|
||||
usersLock sync.RWMutex
|
||||
}
|
||||
|
||||
func newSMTPBackend() (*smtpBackend, error) {
|
||||
return &smtpBackend{}, nil
|
||||
}
|
||||
|
||||
func (backend *smtpBackend) Login(state *smtp.ConnectionState, username string, password string) (smtp.Session, error) {
|
||||
backend.usersLock.RLock()
|
||||
defer backend.usersLock.RUnlock()
|
||||
|
||||
for _, user := range backend.users {
|
||||
if slices.Contains(user.Addresses(), username) && user.BridgePass() == password {
|
||||
return user.NewSMTPSession(username)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrNoSuchUser
|
||||
}
|
||||
|
||||
func (backend *smtpBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
// addUser adds the given user to the backend.
|
||||
// It returns an error if a user with the same ID already exists.
|
||||
func (backend *smtpBackend) addUser(user *user.User) error {
|
||||
backend.usersLock.Lock()
|
||||
defer backend.usersLock.Unlock()
|
||||
|
||||
for _, u := range backend.users {
|
||||
if u.ID() == user.ID() {
|
||||
return ErrUserAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
backend.users = append(backend.users, user)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeUser removes the given user from the backend.
|
||||
// It returns an error if the user doesn't exist.
|
||||
func (backend *smtpBackend) removeUser(user *user.User) error {
|
||||
backend.usersLock.Lock()
|
||||
defer backend.usersLock.Unlock()
|
||||
|
||||
idx := xslices.Index(backend.users, user)
|
||||
|
||||
if idx < 0 {
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
backend.users = append(backend.users[:idx], backend.users[idx+1:]...)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/store"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/store/cache"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/users"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
|
||||
)
|
||||
|
||||
type storeFactory struct {
|
||||
cacheProvider CacheProvider
|
||||
sentryReporter *sentry.Reporter
|
||||
panicHandler users.PanicHandler
|
||||
eventListener listener.Listener
|
||||
events *store.Events
|
||||
cache cache.Cache
|
||||
builder *message.Builder
|
||||
}
|
||||
|
||||
func newStoreFactory(
|
||||
cacheProvider CacheProvider,
|
||||
sentryReporter *sentry.Reporter,
|
||||
panicHandler users.PanicHandler,
|
||||
eventListener listener.Listener,
|
||||
cache cache.Cache,
|
||||
builder *message.Builder,
|
||||
) *storeFactory {
|
||||
return &storeFactory{
|
||||
cacheProvider: cacheProvider,
|
||||
sentryReporter: sentryReporter,
|
||||
panicHandler: panicHandler,
|
||||
eventListener: eventListener,
|
||||
events: store.NewEvents(cacheProvider.GetIMAPCachePath()),
|
||||
cache: cache,
|
||||
builder: builder,
|
||||
}
|
||||
}
|
||||
|
||||
// New creates new store for given user.
|
||||
func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) {
|
||||
return store.New(
|
||||
f.sentryReporter,
|
||||
f.panicHandler,
|
||||
user,
|
||||
f.eventListener,
|
||||
f.cache,
|
||||
f.builder,
|
||||
getUserStorePath(f.cacheProvider.GetDBDir(), user.ID()),
|
||||
f.events,
|
||||
)
|
||||
}
|
||||
|
||||
// Remove removes all store files for given user.
|
||||
func (f *storeFactory) Remove(userID string) error {
|
||||
return store.RemoveStore(
|
||||
f.events,
|
||||
getUserStorePath(f.cacheProvider.GetDBDir(), userID),
|
||||
userID,
|
||||
)
|
||||
}
|
||||
|
||||
// getUserStorePath returns the file path of the store database for the given userID.
|
||||
func getUserStorePath(storeDir string, userID string) (path string) {
|
||||
return filepath.Join(storeDir, fmt.Sprintf("mailbox-%v.db", userID))
|
||||
}
|
||||
@ -1,64 +1,5 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
pkgTLS "github.com/ProtonMail/proton-bridge/v2/internal/config/tls"
|
||||
"github.com/pkg/errors"
|
||||
logrus "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (b *Bridge) GetTLSConfig() (*tls.Config, error) {
|
||||
if !b.tls.HasCerts() {
|
||||
if err := b.generateTLSCerts(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tlsConfig, err := b.tls.GetConfig()
|
||||
if err == nil {
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
logrus.WithError(err).Error("Failed to load TLS config, regenerating certificates")
|
||||
|
||||
if err := b.generateTLSCerts(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.tls.GetConfig()
|
||||
}
|
||||
|
||||
func (b *Bridge) generateTLSCerts() error {
|
||||
template, err := pkgTLS.NewTLSTemplate()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate TLS template")
|
||||
}
|
||||
|
||||
if err := b.tls.GenerateCerts(template); err != nil {
|
||||
return errors.Wrap(err, "failed to generate TLS certs")
|
||||
}
|
||||
|
||||
if err := b.tls.InstallCerts(); err != nil {
|
||||
return errors.Wrap(err, "failed to install TLS certs")
|
||||
}
|
||||
|
||||
return nil
|
||||
func (bridge *Bridge) GetBridgeTLSCert() ([]byte, []byte) {
|
||||
return bridge.vault.GetBridgeTLSCert(), bridge.vault.GetBridgeTLSKey()
|
||||
}
|
||||
|
||||
@ -1,62 +1,43 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
)
|
||||
|
||||
type Locator interface {
|
||||
ProvideSettingsPath() (string, error)
|
||||
ProvideLogsPath() (string, error)
|
||||
|
||||
GetLicenseFilePath() string
|
||||
GetDependencyLicensesLink() string
|
||||
|
||||
Clear() error
|
||||
ClearUpdates() error
|
||||
}
|
||||
|
||||
type CacheProvider interface {
|
||||
GetIMAPCachePath() string
|
||||
GetDBDir() string
|
||||
GetDefaultMessageCacheDir() string
|
||||
type Identifier interface {
|
||||
GetUserAgent() string
|
||||
HasClient() bool
|
||||
SetClient(name, version string)
|
||||
SetPlatform(platform string)
|
||||
}
|
||||
|
||||
type SettingsProvider interface {
|
||||
Get(key settings.Key) string
|
||||
Set(key settings.Key, value string)
|
||||
type TLSReporter interface {
|
||||
GetTLSIssueCh() <-chan struct{}
|
||||
}
|
||||
|
||||
GetBool(key settings.Key) bool
|
||||
SetBool(key settings.Key, val bool)
|
||||
type ProxyDialer interface {
|
||||
DialTLSContext(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
GetInt(key settings.Key) int
|
||||
SetInt(key settings.Key, val int)
|
||||
AllowProxy()
|
||||
DisallowProxy()
|
||||
}
|
||||
|
||||
type Autostarter interface {
|
||||
Enable() error
|
||||
Disable() error
|
||||
}
|
||||
|
||||
type Updater interface {
|
||||
Check() (updater.VersionInfo, error)
|
||||
IsDowngrade(updater.VersionInfo) bool
|
||||
InstallUpdate(updater.VersionInfo) error
|
||||
}
|
||||
|
||||
type Versioner interface {
|
||||
RemoveOtherVersions(*semver.Version) error
|
||||
GetVersionInfo(downloader updater.Downloader, channel updater.Channel) (updater.VersionInfo, error)
|
||||
InstallUpdate(downloader updater.Downloader, update updater.VersionInfo) error
|
||||
}
|
||||
|
||||
72
internal/bridge/updates.go
Normal file
72
internal/bridge/updates.go
Normal file
@ -0,0 +1,72 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) CheckForUpdates() {
|
||||
bridge.updateCheckCh <- struct{}{}
|
||||
}
|
||||
|
||||
func (bridge *Bridge) watchForUpdates() error {
|
||||
ticker := time.NewTicker(constants.UpdateCheckInterval)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-bridge.updateCheckCh:
|
||||
case <-ticker.C:
|
||||
}
|
||||
|
||||
version, err := bridge.updater.GetVersionInfo(bridge.api, bridge.vault.GetUpdateChannel())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := bridge.handleUpdate(version); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
bridge.updateCheckCh <- struct{}{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUpdate(version updater.VersionInfo) error {
|
||||
switch {
|
||||
case !version.Version.GreaterThan(bridge.curVersion):
|
||||
bridge.publish(events.UpdateNotAvailable{})
|
||||
|
||||
case version.RolloutProportion < bridge.vault.GetUpdateRollout():
|
||||
bridge.publish(events.UpdateNotAvailable{})
|
||||
|
||||
case bridge.curVersion.LessThan(version.MinAuto):
|
||||
bridge.publish(events.UpdateAvailable{
|
||||
Version: version,
|
||||
CanInstall: false,
|
||||
})
|
||||
|
||||
case !bridge.vault.GetAutoUpdate():
|
||||
bridge.publish(events.UpdateAvailable{
|
||||
Version: version,
|
||||
CanInstall: true,
|
||||
})
|
||||
|
||||
default:
|
||||
if err := bridge.updater.InstallUpdate(bridge.api, version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.publish(events.UpdateInstalled{
|
||||
Version: version,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -1,26 +1,9 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
func (b *Bridge) GetCurrentUserAgent() string {
|
||||
return b.userAgent.String()
|
||||
func (bridge *Bridge) GetCurrentUserAgent() string {
|
||||
return bridge.identifier.GetUserAgent()
|
||||
}
|
||||
|
||||
func (b *Bridge) SetCurrentPlatform(platform string) {
|
||||
b.userAgent.SetPlatform(platform)
|
||||
func (bridge *Bridge) SetCurrentPlatform(platform string) {
|
||||
bridge.identifier.SetPlatform(platform)
|
||||
}
|
||||
|
||||
434
internal/bridge/users.go
Normal file
434
internal/bridge/users.go
Normal file
@ -0,0 +1,434 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gitlab.protontech.ch/go/liteapi"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type UserInfo struct {
|
||||
// UserID is the user's API ID.
|
||||
UserID string
|
||||
|
||||
// Username is the user's API username.
|
||||
Username string
|
||||
|
||||
// Connected is true if the user is logged in (has API auth).
|
||||
Connected bool
|
||||
|
||||
// Addresses holds the user's email addresses. The first address is the primary address.
|
||||
Addresses []string
|
||||
|
||||
// AddressMode is the user's address mode.
|
||||
AddressMode AddressMode
|
||||
|
||||
// BridgePass is the user's bridge password.
|
||||
BridgePass string
|
||||
|
||||
// UsedSpace is the amount of space used by the user.
|
||||
UsedSpace int
|
||||
|
||||
// MaxSpace is the total amount of space available to the user.
|
||||
MaxSpace int
|
||||
}
|
||||
|
||||
type AddressMode int
|
||||
|
||||
const (
|
||||
SplitMode AddressMode = iota
|
||||
CombinedMode
|
||||
)
|
||||
|
||||
// GetUserIDs returns the IDs of all known users (authorized or not).
|
||||
func (bridge *Bridge) GetUserIDs() []string {
|
||||
return bridge.vault.GetUserIDs()
|
||||
}
|
||||
|
||||
// GetUserInfo returns info about the given user.
|
||||
func (bridge *Bridge) GetUserInfo(userID string) (UserInfo, error) {
|
||||
vaultUser, err := bridge.vault.GetUser(userID)
|
||||
if err != nil {
|
||||
return UserInfo{}, err
|
||||
}
|
||||
|
||||
user, ok := bridge.users[userID]
|
||||
if !ok {
|
||||
return getUserInfo(vaultUser.UserID(), vaultUser.Username()), nil
|
||||
}
|
||||
|
||||
return getConnUserInfo(user), nil
|
||||
}
|
||||
|
||||
// QueryUserInfo queries the user info by username or address.
|
||||
func (bridge *Bridge) QueryUserInfo(query string) (UserInfo, error) {
|
||||
for userID, user := range bridge.users {
|
||||
if user.Match(query) {
|
||||
return bridge.GetUserInfo(userID)
|
||||
}
|
||||
}
|
||||
|
||||
return UserInfo{}, ErrNoSuchUser
|
||||
}
|
||||
|
||||
// LoginUser authorizes a new bridge user with the given username and password.
|
||||
// If necessary, a TOTP and mailbox password are requested via the callbacks.
|
||||
func (bridge *Bridge) LoginUser(
|
||||
ctx context.Context,
|
||||
username, password string,
|
||||
getTOTP func() (string, error),
|
||||
getKeyPass func() ([]byte, error),
|
||||
) (string, error) {
|
||||
client, auth, err := bridge.api.NewClientWithLogin(ctx, username, password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if auth.TwoFA.Enabled == liteapi.TOTPEnabled {
|
||||
totp, err := getTOTP()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := client.Auth2FA(ctx, liteapi.Auth2FAReq{TwoFactorCode: totp}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
var keyPass []byte
|
||||
|
||||
if auth.PasswordMode == liteapi.TwoPasswordMode {
|
||||
pass, err := getKeyPass()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
keyPass = pass
|
||||
} else {
|
||||
keyPass = []byte(password)
|
||||
}
|
||||
|
||||
apiUser, apiAddrs, userKR, addrKRs, saltedKeyPass, err := client.Unlock(ctx, keyPass)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := bridge.addUser(ctx, client, apiUser, apiAddrs, userKR, addrKRs, auth.UID, auth.RefreshToken, saltedKeyPass); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return apiUser.ID, nil
|
||||
}
|
||||
|
||||
// LogoutUser logs out the given user.
|
||||
func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
|
||||
return bridge.logoutUser(ctx, userID, true, false)
|
||||
}
|
||||
|
||||
// DeleteUser deletes the given user.
|
||||
// If it is authorized, it is logged out first.
|
||||
func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
||||
if bridge.users[userID] != nil {
|
||||
if err := bridge.logoutUser(ctx, userID, true, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := bridge.vault.DeleteUser(userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.publish(events.UserDeleted{
|
||||
UserID: userID,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetAddressMode(userID string) (AddressMode, error) {
|
||||
panic("TODO")
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetAddressMode(userID string, mode AddressMode) error {
|
||||
panic("TODO")
|
||||
}
|
||||
|
||||
// loadUsers loads authorized users from the vault.
|
||||
func (bridge *Bridge) loadUsers(ctx context.Context) error {
|
||||
for _, userID := range bridge.vault.GetUserIDs() {
|
||||
user, err := bridge.vault.GetUser(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user.AuthUID() == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := bridge.loadUser(ctx, user); err != nil {
|
||||
logrus.WithError(err).Error("Failed to load connected user")
|
||||
|
||||
if err := user.Clear(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to clear user")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
|
||||
client, auth, err := bridge.api.NewClientWithRefresh(ctx, user.AuthUID(), user.AuthRef())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create API client: %w", err)
|
||||
}
|
||||
|
||||
apiUser, apiAddrs, userKR, addrKRs, err := client.UnlockSalted(ctx, user.KeyPass())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unlock user: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.addUser(ctx, client, apiUser, apiAddrs, userKR, addrKRs, auth.UID, auth.RefreshToken, user.KeyPass()); err != nil {
|
||||
return fmt.Errorf("failed to add user: %w", err)
|
||||
}
|
||||
|
||||
bridge.publish(events.UserLoggedIn{
|
||||
UserID: user.UserID(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addUser adds a new user with an already salted mailbox password.
|
||||
func (bridge *Bridge) addUser(
|
||||
ctx context.Context,
|
||||
client *liteapi.Client,
|
||||
apiUser liteapi.User,
|
||||
apiAddrs []liteapi.Address,
|
||||
userKR *crypto.KeyRing,
|
||||
addrKRs map[string]*crypto.KeyRing,
|
||||
authUID, authRef string,
|
||||
saltedKeyPass []byte,
|
||||
) error {
|
||||
if _, ok := bridge.users[apiUser.ID]; ok {
|
||||
return ErrUserAlreadyLoggedIn
|
||||
}
|
||||
|
||||
var user *user.User
|
||||
|
||||
if slices.Contains(bridge.vault.GetUserIDs(), apiUser.ID) {
|
||||
existingUser, err := bridge.addExistingUser(ctx, client, apiUser, apiAddrs, userKR, addrKRs, authUID, authRef, saltedKeyPass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user = existingUser
|
||||
} else {
|
||||
newUser, err := bridge.addNewUser(ctx, client, apiUser, apiAddrs, userKR, addrKRs, authUID, authRef, saltedKeyPass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user = newUser
|
||||
}
|
||||
|
||||
go func() {
|
||||
for event := range user.GetNotifyCh() {
|
||||
switch event := event.(type) {
|
||||
case events.UserDeauth:
|
||||
if err := bridge.logoutUser(context.Background(), event.UserID, false, false); err != nil {
|
||||
logrus.WithError(err).Error("Failed to logout user")
|
||||
}
|
||||
}
|
||||
|
||||
bridge.publish(event)
|
||||
}
|
||||
}()
|
||||
|
||||
// Gluon will set the IMAP ID in the context, if known, before making requests on behalf of this user.
|
||||
client.AddPreRequestHook(func(ctx context.Context, req *resty.Request) error {
|
||||
if imapID, ok := imap.GetIMAPIDFromContext(ctx); ok {
|
||||
bridge.identifier.SetClient(imapID.Name, imapID.Version)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
bridge.publish(events.UserLoggedIn{
|
||||
UserID: user.ID(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) addNewUser(
|
||||
ctx context.Context,
|
||||
client *liteapi.Client,
|
||||
apiUser liteapi.User,
|
||||
apiAddrs []liteapi.Address,
|
||||
userKR *crypto.KeyRing,
|
||||
addrKRs map[string]*crypto.KeyRing,
|
||||
authUID, authRef string,
|
||||
saltedKeyPass []byte,
|
||||
) (*user.User, error) {
|
||||
vaultUser, err := bridge.vault.AddUser(apiUser.ID, apiUser.Name, authUID, authRef, saltedKeyPass)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := user.New(ctx, vaultUser, client, apiUser, apiAddrs, userKR, addrKRs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gluonKey, err := crypto.RandomToken(32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imapConn, err := user.NewGluonConnector(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gluonID, err := bridge.imapServer.AddUser(ctx, imapConn, gluonKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := vaultUser.UpdateGluonData(gluonID, gluonKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := bridge.smtpBackend.addUser(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bridge.users[apiUser.ID] = user
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) addExistingUser(
|
||||
ctx context.Context,
|
||||
client *liteapi.Client,
|
||||
apiUser liteapi.User,
|
||||
apiAddrs []liteapi.Address,
|
||||
userKR *crypto.KeyRing,
|
||||
addrKRs map[string]*crypto.KeyRing,
|
||||
authUID, authRef string,
|
||||
saltedKeyPass []byte,
|
||||
) (*user.User, error) {
|
||||
vaultUser, err := bridge.vault.GetUser(apiUser.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := vaultUser.UpdateAuth(authUID, authRef); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := vaultUser.UpdateKeyPass(saltedKeyPass); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := user.New(ctx, vaultUser, client, apiUser, apiAddrs, userKR, addrKRs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imapConn, err := user.NewGluonConnector(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := bridge.imapServer.LoadUser(ctx, imapConn, user.GluonID(), user.GluonKey()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := bridge.smtpBackend.addUser(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bridge.users[apiUser.ID] = user
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// logoutUser closes and removes the user with the given ID.
|
||||
// If withAPI is true, the user will additionally be logged out from API.
|
||||
// If withFiles is true, the user's files will be deleted.
|
||||
func (bridge *Bridge) logoutUser(ctx context.Context, userID string, withAPI, withFiles bool) error {
|
||||
user, ok := bridge.users[userID]
|
||||
if !ok {
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
vaultUser, err := bridge.vault.GetUser(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := bridge.imapServer.RemoveUser(ctx, vaultUser.GluonID(), withFiles); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := bridge.smtpBackend.removeUser(user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if withAPI {
|
||||
if err := user.Logout(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := user.Close(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := vaultUser.Clear(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(bridge.users, userID)
|
||||
|
||||
bridge.publish(events.UserLoggedOut{
|
||||
UserID: userID,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getUserInfo returns information about a disconnected user.
|
||||
func getUserInfo(userID, username string) UserInfo {
|
||||
return UserInfo{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
AddressMode: CombinedMode,
|
||||
}
|
||||
}
|
||||
|
||||
// getConnUserInfo returns information about a connected user.
|
||||
func getConnUserInfo(user *user.User) UserInfo {
|
||||
return UserInfo{
|
||||
Connected: true,
|
||||
UserID: user.ID(),
|
||||
Username: user.Name(),
|
||||
Addresses: user.Addresses(),
|
||||
AddressMode: CombinedMode,
|
||||
BridgePass: user.BridgePass(),
|
||||
UsedSpace: user.UsedSpace(),
|
||||
MaxSpace: user.MaxSpace(),
|
||||
}
|
||||
}
|
||||
286
internal/bridge/users_test.go
Normal file
286
internal/bridge/users_test.go
Normal file
@ -0,0 +1,286 @@
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gitlab.protontech.ch/go/liteapi/server"
|
||||
)
|
||||
|
||||
func TestBridge_WithoutUsers(t *testing.T) {
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Login(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginUser(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The user is now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginLogoutLogin(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID := must(bridge.LoginUser(ctx, username, password, nil, nil))
|
||||
|
||||
// The user is now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// Logout the user.
|
||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||
|
||||
// The user is now disconnected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// Login the user again.
|
||||
newUserID := must(bridge.LoginUser(ctx, username, password, nil, nil))
|
||||
require.Equal(t, userID, newUserID)
|
||||
|
||||
// The user is connected again.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginDeleteLogin(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID := must(bridge.LoginUser(ctx, username, password, nil, nil))
|
||||
|
||||
// The user is now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// Delete the user.
|
||||
require.NoError(t, bridge.DeleteUser(ctx, userID))
|
||||
|
||||
// The user is now gone.
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// Login the user again.
|
||||
newUserID := must(bridge.LoginUser(ctx, username, password, nil, nil))
|
||||
require.Equal(t, userID, newUserID)
|
||||
|
||||
// The user is connected again.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginDeauthLogin(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID := must(bridge.LoginUser(ctx, username, password, nil, nil))
|
||||
|
||||
// Get a channel to receive the deauth event.
|
||||
eventCh, done := bridge.GetEvents(events.UserDeauth{})
|
||||
defer done()
|
||||
|
||||
// Deauth the user.
|
||||
require.NoError(t, s.RevokeUser(userID))
|
||||
|
||||
// The user is eventually disconnected.
|
||||
require.Eventually(t, func() bool {
|
||||
return len(getConnectedUserIDs(t, bridge)) == 0
|
||||
}, 10*time.Second, time.Second)
|
||||
|
||||
// We should get a deauth event.
|
||||
require.IsType(t, events.UserDeauth{}, <-eventCh)
|
||||
|
||||
// Login the user after the disconnection.
|
||||
newUserID := must(bridge.LoginUser(ctx, username, password, nil, nil))
|
||||
require.Equal(t, userID, newUserID)
|
||||
|
||||
// The user is connected again.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginExpireLogin(t *testing.T) {
|
||||
const authLife = 2 * time.Second
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
s.SetAuthLife(authLife)
|
||||
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user. Its auth will only be valid for a short time.
|
||||
userID := must(bridge.LoginUser(ctx, username, password, nil, nil))
|
||||
|
||||
// Wait until the auth expires.
|
||||
time.Sleep(authLife)
|
||||
|
||||
// The user will have to refresh but the logout will still succeed.
|
||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_FailToLoad(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID = must(bridge.LoginUser(ctx, username, password, nil, nil))
|
||||
})
|
||||
|
||||
// Deauth the user while bridge is stopped.
|
||||
require.NoError(t, s.RevokeUser(userID))
|
||||
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// The user is disconnected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginRestart(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID = must(bridge.LoginUser(ctx, username, password, nil, nil))
|
||||
})
|
||||
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// The user is still connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginLogoutRestart(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID = must(bridge.LoginUser(ctx, username, password, nil, nil))
|
||||
|
||||
// Logout the user.
|
||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||
})
|
||||
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// The user is still disconnected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginDeleteRestart(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID = must(bridge.LoginUser(ctx, username, password, nil, nil))
|
||||
|
||||
// Delete the user.
|
||||
require.NoError(t, bridge.DeleteUser(ctx, userID))
|
||||
})
|
||||
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// The user is still gone.
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_BridgePass(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) {
|
||||
var userID, pass string
|
||||
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID = must(bridge.LoginUser(ctx, username, password, nil, nil))
|
||||
|
||||
// Retrieve the bridge pass.
|
||||
pass = must(bridge.GetUserInfo(userID)).BridgePass
|
||||
|
||||
// Log the user out.
|
||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||
|
||||
// Log the user back in.
|
||||
must(bridge.LoginUser(ctx, username, password, nil, nil))
|
||||
|
||||
// The bridge pass should be the same.
|
||||
require.Equal(t, pass, pass)
|
||||
})
|
||||
|
||||
withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// The bridge should load schizofrenic.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// The bridge pass should be the same.
|
||||
require.Equal(t, pass, must(bridge.GetUserInfo(userID)).BridgePass)
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -15,9 +15,31 @@
|
||||
// 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/>.
|
||||
|
||||
package tls
|
||||
package certs
|
||||
|
||||
import "golang.org/x/sys/execabs"
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
func installCert(certPEM []byte) error {
|
||||
name, err := writeToTempFile(certPEM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addTrustedCert(name)
|
||||
}
|
||||
|
||||
func uninstallCert(certPEM []byte) error {
|
||||
name, err := writeToTempFile(certPEM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return removeTrustedCert(name)
|
||||
}
|
||||
|
||||
func addTrustedCert(certPath string) error {
|
||||
return execabs.Command( //nolint:gosec
|
||||
@ -44,10 +66,20 @@ func removeTrustedCert(certPath string) error {
|
||||
).Run()
|
||||
}
|
||||
|
||||
func (t *TLS) InstallCerts() error {
|
||||
return addTrustedCert(t.getTLSCertPath())
|
||||
}
|
||||
// writeToTempFile writes the given data to a temporary file and returns the path.
|
||||
func writeToTempFile(data []byte) (string, error) {
|
||||
f, err := os.CreateTemp("", "tls")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
func (t *TLS) UninstallCerts() error {
|
||||
return removeTrustedCert(t.getTLSCertPath())
|
||||
if _, err := f.Write(data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return f.Name(), nil
|
||||
}
|
||||
@ -15,12 +15,12 @@
|
||||
// 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/>.
|
||||
|
||||
package tls
|
||||
package certs
|
||||
|
||||
func (t *TLS) InstallCerts() error {
|
||||
func installCert([]byte) error {
|
||||
return nil // Linux doesn't have a root cert store.
|
||||
}
|
||||
|
||||
func (t *TLS) UninstallCerts() error {
|
||||
func uninstallCert([]byte) error {
|
||||
return nil // Linux doesn't have a root cert store.
|
||||
}
|
||||
@ -15,12 +15,12 @@
|
||||
// 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/>.
|
||||
|
||||
package tls
|
||||
package certs
|
||||
|
||||
func (t *TLS) InstallCerts() error {
|
||||
func installCert([]byte) error {
|
||||
return nil // NOTE(GODT-986): Install certs to root cert store?
|
||||
}
|
||||
|
||||
func (t *TLS) UninstallCerts() error {
|
||||
func uninstallCert([]byte) error {
|
||||
return nil // NOTE(GODT-986): Uninstall certs from root cert store?
|
||||
}
|
||||
15
internal/certs/installer.go
Normal file
15
internal/certs/installer.go
Normal file
@ -0,0 +1,15 @@
|
||||
package certs
|
||||
|
||||
type Installer struct{}
|
||||
|
||||
func NewInstaller() *Installer {
|
||||
return &Installer{}
|
||||
}
|
||||
|
||||
func (installer *Installer) InstallCert(certPEM []byte) error {
|
||||
return installCert(certPEM)
|
||||
}
|
||||
|
||||
func (installer *Installer) UninstallCert(certPEM []byte) error {
|
||||
return uninstallCert(certPEM)
|
||||
}
|
||||
@ -15,9 +15,10 @@
|
||||
// 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/>.
|
||||
|
||||
package tls
|
||||
package certs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
@ -27,22 +28,13 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type TLS struct {
|
||||
settingsPath string
|
||||
}
|
||||
|
||||
func New(settingsPath string) *TLS {
|
||||
return &TLS{
|
||||
settingsPath: settingsPath,
|
||||
}
|
||||
}
|
||||
// ErrTLSCertExpiresSoon is returned when the TLS certificate is about to expire.
|
||||
var ErrTLSCertExpiresSoon = fmt.Errorf("TLS certificate will expire soon")
|
||||
|
||||
// NewTLSTemplate creates a new TLS template certificate with a random serial number.
|
||||
func NewTLSTemplate() (*x509.Certificate, error) {
|
||||
@ -69,108 +61,40 @@ func NewTLSTemplate() (*x509.Certificate, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewPEMKeyPair return a new TLS private key and certificate in PEM encoded format.
|
||||
func NewPEMKeyPair() (pemCert, pemKey []byte, err error) {
|
||||
template, err := NewTLSTemplate()
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to generate TLS template")
|
||||
}
|
||||
|
||||
// GenerateTLSCert generates a new TLS certificate and returns it as PEM.
|
||||
var GenerateCert = func(template *x509.Certificate) ([]byte, []byte, error) {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to generate private key")
|
||||
}
|
||||
|
||||
pemKey = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to create certificate")
|
||||
}
|
||||
|
||||
pemCert = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
certPEM := new(bytes.Buffer)
|
||||
|
||||
return pemCert, pemKey, nil
|
||||
}
|
||||
|
||||
var ErrTLSCertExpiresSoon = fmt.Errorf("TLS certificate will expire soon")
|
||||
|
||||
// getTLSCertPath returns path to certificate; used for TLS servers (IMAP, SMTP).
|
||||
func (t *TLS) getTLSCertPath() string {
|
||||
return filepath.Join(t.settingsPath, "cert.pem")
|
||||
}
|
||||
|
||||
// getTLSKeyPath returns path to private key; used for TLS servers (IMAP, SMTP).
|
||||
func (t *TLS) getTLSKeyPath() string {
|
||||
return filepath.Join(t.settingsPath, "key.pem")
|
||||
}
|
||||
|
||||
// HasCerts returns whether TLS certs have been generated.
|
||||
func (t *TLS) HasCerts() bool {
|
||||
if _, err := os.Stat(t.getTLSCertPath()); err != nil {
|
||||
return false
|
||||
if err := pem.Encode(certPEM, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(t.getTLSKeyPath()); err != nil {
|
||||
return false
|
||||
keyPEM := new(bytes.Buffer)
|
||||
|
||||
if err := pem.Encode(keyPEM, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GenerateCerts generates certs from the given template.
|
||||
func (t *TLS) GenerateCerts(template *x509.Certificate) error {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate private key")
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create certificate")
|
||||
}
|
||||
|
||||
certOut, err := os.Create(t.getTLSCertPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer certOut.Close() //nolint:errcheck,gosec
|
||||
|
||||
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyOut, err := os.OpenFile(t.getTLSKeyPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer keyOut.Close() //nolint:errcheck,gosec
|
||||
|
||||
return pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||
return certPEM.Bytes(), keyPEM.Bytes(), nil
|
||||
}
|
||||
|
||||
// GetConfig tries to load TLS config or generate new one which is then returned.
|
||||
func (t *TLS) GetConfig() (*tls.Config, error) {
|
||||
c, err := tls.LoadX509KeyPair(t.getTLSCertPath(), t.getTLSKeyPath())
|
||||
func GetConfig(certPEM, keyPEM []byte) (*tls.Config, error) {
|
||||
c, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load keypair")
|
||||
}
|
||||
|
||||
return getConfigFromKeyPair(c)
|
||||
}
|
||||
|
||||
// GetConfigFromPEMKeyPair load a TLS config from PEM encoded certificate and key.
|
||||
func GetConfigFromPEMKeyPair(permCert, pemKey []byte) (*tls.Config, error) {
|
||||
c, err := tls.X509KeyPair(permCert, pemKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load keypair")
|
||||
}
|
||||
|
||||
return getConfigFromKeyPair(c)
|
||||
}
|
||||
|
||||
func getConfigFromKeyPair(c tls.Certificate) (*tls.Config, error) {
|
||||
var err error
|
||||
c.Leaf, err = x509.ParseCertificate(c.Certificate[0])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse certificate")
|
||||
@ -15,10 +15,10 @@
|
||||
// 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/>.
|
||||
|
||||
package tls
|
||||
package certs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"crypto/tls"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -26,12 +26,6 @@ import (
|
||||
)
|
||||
|
||||
func TestGetOldConfig(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "test-tls")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create new tls object.
|
||||
tls := New(dir)
|
||||
|
||||
// Create new TLS template.
|
||||
tlsTemplate, err := NewTLSTemplate()
|
||||
require.NoError(t, err)
|
||||
@ -41,20 +35,15 @@ func TestGetOldConfig(t *testing.T) {
|
||||
tlsTemplate.NotAfter = time.Now()
|
||||
|
||||
// Generate the certs from the template.
|
||||
require.NoError(t, tls.GenerateCerts(tlsTemplate))
|
||||
certPEM, keyPEM, err := GenerateCert(tlsTemplate)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate the config from the certs -- it's going to expire soon so we don't want to use it.
|
||||
_, err = tls.GetConfig()
|
||||
_, err = GetConfig(certPEM, keyPEM)
|
||||
require.Equal(t, err, ErrTLSCertExpiresSoon)
|
||||
}
|
||||
|
||||
func TestGetValidConfig(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "test-tls")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create new tls object.
|
||||
tls := New(dir)
|
||||
|
||||
// Create new TLS template.
|
||||
tlsTemplate, err := NewTLSTemplate()
|
||||
require.NoError(t, err)
|
||||
@ -64,10 +53,11 @@ func TestGetValidConfig(t *testing.T) {
|
||||
tlsTemplate.NotAfter = time.Now().Add(2 * 365 * 24 * time.Hour)
|
||||
|
||||
// Generate the certs from the template.
|
||||
require.NoError(t, tls.GenerateCerts(tlsTemplate))
|
||||
certPEM, keyPEM, err := GenerateCert(tlsTemplate)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate the config from the certs -- it's not going to expire soon so we want to use it.
|
||||
config, err := tls.GetConfig()
|
||||
config, err := GetConfig(certPEM, keyPEM)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(config.Certificates), 1)
|
||||
|
||||
@ -77,9 +67,13 @@ func TestGetValidConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewConfig(t *testing.T) {
|
||||
pemCert, pemKey, err := NewPEMKeyPair()
|
||||
tlsTemplate, err := NewTLSTemplate()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = GetConfigFromPEMKeyPair(pemCert, pemKey)
|
||||
pemCert, pemKey, err := GenerateCert(tlsTemplate)
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := tls.X509KeyPair(pemCert, pemKey)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cert)
|
||||
}
|
||||
@ -23,7 +23,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/mobileconfig"
|
||||
"golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
70
internal/config/cache/cache.go
vendored
70
internal/config/cache/cache.go
vendored
@ -1,70 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package cache provides access to contents inside a cache directory.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/files"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
dir, version string
|
||||
}
|
||||
|
||||
func New(dir, version string) (*Cache, error) {
|
||||
if err := os.MkdirAll(filepath.Join(dir, version), 0o700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Cache{
|
||||
dir: dir,
|
||||
version: version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetDBDir returns folder for db files.
|
||||
func (c *Cache) GetDBDir() string {
|
||||
return c.getCurrentCacheDir()
|
||||
}
|
||||
|
||||
// GetDefaultMessageCacheDir returns folder for cached messages files.
|
||||
func (c *Cache) GetDefaultMessageCacheDir() string {
|
||||
return filepath.Join(c.getCurrentCacheDir(), "messages")
|
||||
}
|
||||
|
||||
// GetIMAPCachePath returns path to file with IMAP status.
|
||||
func (c *Cache) GetIMAPCachePath() string {
|
||||
return filepath.Join(c.getCurrentCacheDir(), "user_info.json")
|
||||
}
|
||||
|
||||
// GetTransferDir returns folder for import-export rules files.
|
||||
func (c *Cache) GetTransferDir() string {
|
||||
return c.getCurrentCacheDir()
|
||||
}
|
||||
|
||||
// RemoveOldVersions removes any cache dirs that are not the current version.
|
||||
func (c *Cache) RemoveOldVersions() error {
|
||||
return files.Remove(c.dir).Except(c.getCurrentCacheDir()).Do()
|
||||
}
|
||||
|
||||
func (c *Cache) getCurrentCacheDir() string {
|
||||
return filepath.Join(c.dir, c.version)
|
||||
}
|
||||
69
internal/config/cache/cache_test.go
vendored
69
internal/config/cache/cache_test.go
vendored
@ -1,69 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRemoveOldVersions(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "test-cache")
|
||||
require.NoError(t, err)
|
||||
|
||||
cache, err := New(dir, "c4")
|
||||
require.NoError(t, err)
|
||||
|
||||
createFilesInDir(t, dir,
|
||||
"unexpected1.txt",
|
||||
"c1/unexpected1.txt",
|
||||
"c2/unexpected2.txt",
|
||||
"c3/unexpected3.txt",
|
||||
"something.txt",
|
||||
)
|
||||
|
||||
require.DirExists(t, filepath.Join(dir, "c4"))
|
||||
require.FileExists(t, filepath.Join(dir, "unexpected1.txt"))
|
||||
require.FileExists(t, filepath.Join(dir, "c1", "unexpected1.txt"))
|
||||
require.FileExists(t, filepath.Join(dir, "c2", "unexpected2.txt"))
|
||||
require.FileExists(t, filepath.Join(dir, "c3", "unexpected3.txt"))
|
||||
require.FileExists(t, filepath.Join(dir, "something.txt"))
|
||||
|
||||
assert.NoError(t, cache.RemoveOldVersions())
|
||||
|
||||
assert.DirExists(t, filepath.Join(dir, "c4"))
|
||||
assert.NoFileExists(t, filepath.Join(dir, "unexpected1.txt"))
|
||||
assert.NoFileExists(t, filepath.Join(dir, "c1", "unexpected1.txt"))
|
||||
assert.NoFileExists(t, filepath.Join(dir, "c2", "unexpected2.txt"))
|
||||
assert.NoFileExists(t, filepath.Join(dir, "c3", "unexpected3.txt"))
|
||||
assert.NoFileExists(t, filepath.Join(dir, "something.txt"))
|
||||
}
|
||||
|
||||
func createFilesInDir(t *testing.T, dir string, files ...string) {
|
||||
for _, target := range files {
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(dir, target)), 0o700))
|
||||
|
||||
f, err := os.Create(filepath.Join(dir, target))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
}
|
||||
}
|
||||
@ -1,151 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type keyValueStore struct {
|
||||
vals map[Key]string
|
||||
path string
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
|
||||
// newKeyValueStore returns loaded preferences.
|
||||
func newKeyValueStore(path string) *keyValueStore {
|
||||
p := &keyValueStore{
|
||||
path: path,
|
||||
lock: &sync.RWMutex{},
|
||||
}
|
||||
if err := p.load(); err != nil {
|
||||
logrus.WithError(err).Warn("Cannot load preferences file, creating new one")
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *keyValueStore) load() error {
|
||||
if p.vals != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
p.vals = make(map[Key]string)
|
||||
|
||||
f, err := os.Open(p.path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close() //nolint:errcheck,gosec
|
||||
|
||||
return json.NewDecoder(f).Decode(&p.vals)
|
||||
}
|
||||
|
||||
func (p *keyValueStore) save() error {
|
||||
if p.vals == nil {
|
||||
return errors.New("cannot save preferences: cache is nil")
|
||||
}
|
||||
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
b, err := json.MarshalIndent(p.vals, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(p.path, b, 0o600)
|
||||
}
|
||||
|
||||
func (p *keyValueStore) setDefault(key Key, value string) {
|
||||
if p.Get(key) == "" {
|
||||
p.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *keyValueStore) Get(key Key) string {
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
|
||||
return p.vals[key]
|
||||
}
|
||||
|
||||
func (p *keyValueStore) GetBool(key Key) bool {
|
||||
return p.Get(key) == "true"
|
||||
}
|
||||
|
||||
func (p *keyValueStore) GetInt(key Key) int {
|
||||
if p.Get(key) == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
value, err := strconv.Atoi(p.Get(key))
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Cannot parse int")
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func (p *keyValueStore) GetFloat64(key Key) float64 {
|
||||
if p.Get(key) == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
value, err := strconv.ParseFloat(p.Get(key), 64)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Cannot parse float64")
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func (p *keyValueStore) Set(key Key, value string) {
|
||||
p.lock.Lock()
|
||||
p.vals[key] = value
|
||||
p.lock.Unlock()
|
||||
|
||||
if err := p.save(); err != nil {
|
||||
logrus.WithError(err).Warn("Cannot save preferences")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *keyValueStore) SetBool(key Key, value bool) {
|
||||
if value {
|
||||
p.Set(key, "true")
|
||||
} else {
|
||||
p.Set(key, "false")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *keyValueStore) SetInt(key Key, value int) {
|
||||
p.Set(key, strconv.Itoa(value))
|
||||
}
|
||||
|
||||
func (p *keyValueStore) SetFloat64(key Key, value float64) {
|
||||
p.Set(key, fmt.Sprintf("%v", value))
|
||||
}
|
||||
@ -1,141 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package settings
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLoadNoKeyValueStore(t *testing.T) {
|
||||
r := require.New(t)
|
||||
pref, clean := newTestEmptyKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
r.Equal("", pref.Get("key"))
|
||||
}
|
||||
|
||||
func TestLoadBadKeyValueStore(t *testing.T) {
|
||||
r := require.New(t)
|
||||
path, clean := newTmpFile(r)
|
||||
defer clean()
|
||||
|
||||
r.NoError(os.WriteFile(path, []byte("{\"key\":\"MISSING_QUOTES"), 0o700))
|
||||
pref := newKeyValueStore(path)
|
||||
r.Equal("", pref.Get("key"))
|
||||
}
|
||||
|
||||
func TestKeyValueStor(t *testing.T) {
|
||||
r := require.New(t)
|
||||
pref, clean := newTestKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
r.Equal("value", pref.Get("str"))
|
||||
r.Equal("42", pref.Get("int"))
|
||||
r.Equal("true", pref.Get("bool"))
|
||||
r.Equal("t", pref.Get("falseBool"))
|
||||
}
|
||||
|
||||
func TestKeyValueStoreGetInt(t *testing.T) {
|
||||
r := require.New(t)
|
||||
pref, clean := newTestKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
r.Equal(0, pref.GetInt("str"))
|
||||
r.Equal(42, pref.GetInt("int"))
|
||||
r.Equal(0, pref.GetInt("bool"))
|
||||
r.Equal(0, pref.GetInt("falseBool"))
|
||||
}
|
||||
|
||||
func TestKeyValueStoreGetBool(t *testing.T) {
|
||||
r := require.New(t)
|
||||
pref, clean := newTestKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
r.Equal(false, pref.GetBool("str"))
|
||||
r.Equal(false, pref.GetBool("int"))
|
||||
r.Equal(true, pref.GetBool("bool"))
|
||||
r.Equal(false, pref.GetBool("falseBool"))
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSetDefault(t *testing.T) {
|
||||
r := require.New(t)
|
||||
pref, clean := newTestEmptyKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
pref.setDefault("key", "value")
|
||||
pref.setDefault("key", "othervalue")
|
||||
r.Equal("value", pref.Get("key"))
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSet(t *testing.T) {
|
||||
r := require.New(t)
|
||||
pref, clean := newTestEmptyKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
pref.Set("str", "value")
|
||||
checkSavedKeyValueStore(r, pref.path, "{\n\t\"str\": \"value\"\n}")
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSetInt(t *testing.T) {
|
||||
r := require.New(t)
|
||||
pref, clean := newTestEmptyKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
pref.SetInt("int", 42)
|
||||
checkSavedKeyValueStore(r, pref.path, "{\n\t\"int\": \"42\"\n}")
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSetBool(t *testing.T) {
|
||||
r := require.New(t)
|
||||
pref, clean := newTestEmptyKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
pref.SetBool("trueBool", true)
|
||||
pref.SetBool("falseBool", false)
|
||||
checkSavedKeyValueStore(r, pref.path, "{\n\t\"falseBool\": \"false\",\n\t\"trueBool\": \"true\"\n}")
|
||||
}
|
||||
|
||||
func newTmpFile(r *require.Assertions) (path string, clean func()) {
|
||||
tmpfile, err := os.CreateTemp("", "pref.*.json")
|
||||
r.NoError(err)
|
||||
defer r.NoError(tmpfile.Close())
|
||||
|
||||
return tmpfile.Name(), func() {
|
||||
r.NoError(os.Remove(tmpfile.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
func newTestEmptyKeyValueStore(r *require.Assertions) (*keyValueStore, func()) {
|
||||
path, clean := newTmpFile(r)
|
||||
return newKeyValueStore(path), clean
|
||||
}
|
||||
|
||||
func newTestKeyValueStore(r *require.Assertions) (*keyValueStore, func()) {
|
||||
path, clean := newTmpFile(r)
|
||||
r.NoError(os.WriteFile(path, []byte("{\"str\":\"value\",\"int\":\"42\",\"bool\":\"true\",\"falseBool\":\"t\"}"), 0o700))
|
||||
return newKeyValueStore(path), clean
|
||||
}
|
||||
|
||||
func checkSavedKeyValueStore(r *require.Assertions, path, expected string) {
|
||||
data, err := os.ReadFile(path)
|
||||
r.NoError(err)
|
||||
r.Equal(expected, string(data))
|
||||
}
|
||||
@ -1,116 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package settings provides access to persistent user settings.
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Key string
|
||||
|
||||
// Keys of preferences in JSON file.
|
||||
const (
|
||||
FirstStartKey Key = "first_time_start"
|
||||
FirstStartGUIKey Key = "first_time_start_gui"
|
||||
LastHeartbeatKey Key = "last_heartbeat"
|
||||
APIPortKey Key = "user_port_api"
|
||||
IMAPPortKey Key = "user_port_imap"
|
||||
SMTPPortKey Key = "user_port_smtp"
|
||||
SMTPSSLKey Key = "user_ssl_smtp"
|
||||
AllowProxyKey Key = "allow_proxy"
|
||||
AutostartKey Key = "autostart"
|
||||
AutoUpdateKey Key = "autoupdate"
|
||||
CookiesKey Key = "cookies"
|
||||
LastVersionKey Key = "last_used_version"
|
||||
UpdateChannelKey Key = "update_channel"
|
||||
RolloutKey Key = "rollout"
|
||||
PreferredKeychainKey Key = "preferred_keychain"
|
||||
CacheEnabledKey Key = "cache_enabled"
|
||||
CacheCompressionKey Key = "cache_compression"
|
||||
CacheLocationKey Key = "cache_location"
|
||||
CacheMinFreeAbsKey Key = "cache_min_free_abs"
|
||||
CacheMinFreeRatKey Key = "cache_min_free_rat"
|
||||
CacheConcurrencyRead Key = "cache_concurrent_read"
|
||||
CacheConcurrencyWrite Key = "cache_concurrent_write"
|
||||
IMAPWorkers Key = "imap_workers"
|
||||
FetchWorkers Key = "fetch_workers"
|
||||
AttachmentWorkers Key = "attachment_workers"
|
||||
ColorScheme Key = "color_scheme"
|
||||
RebrandingMigrationKey Key = "rebranding_migrated"
|
||||
IsAllMailVisible Key = "is_all_mail_visible"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
*keyValueStore
|
||||
|
||||
settingsPath string
|
||||
}
|
||||
|
||||
func New(settingsPath string) *Settings {
|
||||
s := &Settings{
|
||||
keyValueStore: newKeyValueStore(filepath.Join(settingsPath, "prefs.json")),
|
||||
settingsPath: settingsPath,
|
||||
}
|
||||
|
||||
s.setDefaultValues()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultIMAPPort = "1143"
|
||||
DefaultSMTPPort = "1025"
|
||||
DefaultAPIPort = "1042"
|
||||
)
|
||||
|
||||
func (s *Settings) setDefaultValues() {
|
||||
s.setDefault(FirstStartKey, "true")
|
||||
s.setDefault(FirstStartGUIKey, "true")
|
||||
s.setDefault(LastHeartbeatKey, fmt.Sprintf("%v", time.Now().YearDay()))
|
||||
s.setDefault(AllowProxyKey, "true")
|
||||
s.setDefault(AutostartKey, "true")
|
||||
s.setDefault(AutoUpdateKey, "true")
|
||||
s.setDefault(LastVersionKey, "")
|
||||
s.setDefault(UpdateChannelKey, "")
|
||||
s.setDefault(RolloutKey, fmt.Sprintf("%v", rand.Float64())) //nolint:gosec // G404 It is OK to use weak random number generator here
|
||||
s.setDefault(PreferredKeychainKey, "")
|
||||
s.setDefault(CacheEnabledKey, "true")
|
||||
s.setDefault(CacheCompressionKey, "true")
|
||||
s.setDefault(CacheLocationKey, "")
|
||||
s.setDefault(CacheMinFreeAbsKey, "250000000")
|
||||
s.setDefault(CacheMinFreeRatKey, "")
|
||||
s.setDefault(CacheConcurrencyRead, "16")
|
||||
s.setDefault(CacheConcurrencyWrite, "16")
|
||||
s.setDefault(IMAPWorkers, "16")
|
||||
s.setDefault(FetchWorkers, "16")
|
||||
s.setDefault(AttachmentWorkers, "16")
|
||||
s.setDefault(ColorScheme, "")
|
||||
|
||||
s.setDefault(APIPortKey, DefaultAPIPort)
|
||||
s.setDefault(IMAPPortKey, DefaultIMAPPort)
|
||||
s.setDefault(SMTPPortKey, DefaultSMTPPort)
|
||||
|
||||
// By default, stick to STARTTLS. If the user uses catalina+applemail they'll have to change to SSL.
|
||||
s.setDefault(SMTPSSLKey, "false")
|
||||
|
||||
s.setDefault(IsAllMailVisible, "true")
|
||||
}
|
||||
@ -18,17 +18,35 @@
|
||||
// Package constants contains variables that are set via ldflags during build.
|
||||
package constants
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
const VendorName = "protonmail"
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
// Version of the build.
|
||||
// Full app name (to show to the user).
|
||||
FullAppName = ""
|
||||
|
||||
// ConfigName determines the name of the location where bridge stores config files.
|
||||
ConfigName = "bridge"
|
||||
|
||||
// UpdateName is the name of the product appearing in the update URL.
|
||||
UpdateName = "bridge"
|
||||
|
||||
// KeyChainName is the name of the entry in the OS keychain.
|
||||
KeyChainName = "bridge"
|
||||
|
||||
// Version of the build.
|
||||
Version = ""
|
||||
Version = "2.3.0+git"
|
||||
|
||||
// AppVersion is the full rendered version of the app (to be used in request headers).
|
||||
AppVersion = getAPIOS() + cases.Title(language.Und).String(ConfigName) + "_" + Version
|
||||
|
||||
// Revision is current hash of the build.
|
||||
Revision = ""
|
||||
@ -36,9 +54,31 @@ var (
|
||||
// BuildTime stamp of the build.
|
||||
BuildTime = ""
|
||||
|
||||
// BuildVersion is derived from LongVersion and BuildTime.
|
||||
BuildVersion = fmt.Sprintf("%v (%v) %v", Version, Revision, BuildTime)
|
||||
|
||||
// DSNSentry client keys to be able to report crashes to Sentry.
|
||||
DSNSentry = ""
|
||||
|
||||
// BuildVersion is derived from LongVersion and BuildTime.
|
||||
BuildVersion = fmt.Sprintf("%v (%v) %v", Version, Revision, BuildTime)
|
||||
// APIHost is our API address.
|
||||
APIHost = "https://api.protonmail.ch"
|
||||
|
||||
// The host name of the bridge server.
|
||||
Host = "127.0.0.1"
|
||||
)
|
||||
|
||||
func getAPIOS() string {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return "macOS"
|
||||
|
||||
case "linux":
|
||||
return "Linux"
|
||||
|
||||
case "windows":
|
||||
return "Windows"
|
||||
|
||||
default:
|
||||
return "Linux"
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,8 +22,5 @@ package constants
|
||||
|
||||
import "time"
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
// UpdateCheckInterval defines how often we check for new version.
|
||||
UpdateCheckInterval = time.Hour //nolint:gochecknoglobals
|
||||
)
|
||||
// UpdateCheckInterval defines how often we check for new version.
|
||||
const UpdateCheckInterval = time.Hour
|
||||
|
||||
@ -22,8 +22,5 @@ package constants
|
||||
|
||||
import "time"
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
// UpdateCheckInterval defines how often we check for new version
|
||||
UpdateCheckInterval = time.Duration(5 * time.Minute)
|
||||
)
|
||||
// UpdateCheckInterval defines how often we check for new version
|
||||
const UpdateCheckInterval = time.Duration(5 * time.Minute)
|
||||
|
||||
@ -26,28 +26,31 @@ import (
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
)
|
||||
|
||||
type cookiesByHost map[string][]*http.Cookie
|
||||
|
||||
type Persister interface {
|
||||
GetCookies() ([]byte, error)
|
||||
SetCookies([]byte) error
|
||||
}
|
||||
|
||||
// Jar implements http.CookieJar by wrapping the standard library's cookiejar.Jar.
|
||||
// The jar uses a pantry to load cookies at startup and save cookies when set.
|
||||
type Jar struct {
|
||||
jar *cookiejar.Jar
|
||||
settings *settings.Settings
|
||||
cookies cookiesByHost
|
||||
locker sync.Locker
|
||||
jar *cookiejar.Jar
|
||||
persister Persister
|
||||
cookies cookiesByHost
|
||||
locker sync.Locker
|
||||
}
|
||||
|
||||
func NewCookieJar(s *settings.Settings) (*Jar, error) {
|
||||
func NewCookieJar(persister Persister) (*Jar, error) {
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cookiesByHost, err := loadCookies(s)
|
||||
cookiesByHost, err := loadCookies(persister)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -62,10 +65,10 @@ func NewCookieJar(s *settings.Settings) (*Jar, error) {
|
||||
}
|
||||
|
||||
return &Jar{
|
||||
jar: jar,
|
||||
settings: s,
|
||||
cookies: cookiesByHost,
|
||||
locker: &sync.Mutex{},
|
||||
jar: jar,
|
||||
persister: persister,
|
||||
cookies: cookiesByHost,
|
||||
locker: &sync.Mutex{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -101,16 +104,17 @@ func (j *Jar) PersistCookies() error {
|
||||
return err
|
||||
}
|
||||
|
||||
j.settings.Set(settings.CookiesKey, string(rawCookies))
|
||||
|
||||
return nil
|
||||
return j.persister.SetCookies(rawCookies)
|
||||
}
|
||||
|
||||
// loadCookies loads all non-expired cookies from disk.
|
||||
func loadCookies(s *settings.Settings) (cookiesByHost, error) {
|
||||
rawCookies := s.Get(settings.CookiesKey)
|
||||
func loadCookies(persister Persister) (cookiesByHost, error) {
|
||||
rawCookies, err := persister.GetCookies()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rawCookies == "" {
|
||||
if len(rawCookies) == 0 {
|
||||
return make(cookiesByHost), nil
|
||||
}
|
||||
|
||||
|
||||
@ -18,13 +18,15 @@
|
||||
package cookies
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -37,7 +39,7 @@ func TestJarGetSet(t *testing.T) {
|
||||
})
|
||||
defer ts.Close()
|
||||
|
||||
client, _ := getClientWithJar(t, newFakeSettings())
|
||||
client, _ := getClientWithJar(t, newTestPersister(t))
|
||||
|
||||
// Hit a server that sets some cookies.
|
||||
setRes, err := client.Get(ts.URL + "/set")
|
||||
@ -63,7 +65,7 @@ func TestJarLoad(t *testing.T) {
|
||||
defer ts.Close()
|
||||
|
||||
// This will be our "persistent storage" from which the cookie jar should load cookies.
|
||||
s := newFakeSettings()
|
||||
s := newTestPersister(t)
|
||||
|
||||
// This client saves cookies to persistent storage.
|
||||
oldClient, jar := getClientWithJar(t, s)
|
||||
@ -98,7 +100,7 @@ func TestJarExpiry(t *testing.T) {
|
||||
defer ts.Close()
|
||||
|
||||
// This will be our "persistent storage" from which the cookie jar should load cookies.
|
||||
s := newFakeSettings()
|
||||
s := newTestPersister(t)
|
||||
|
||||
// This client saves cookies to persistent storage.
|
||||
oldClient, jar1 := getClientWithJar(t, s)
|
||||
@ -122,9 +124,12 @@ func TestJarExpiry(t *testing.T) {
|
||||
// Save the cookies (expired ones were cleared out).
|
||||
require.NoError(t, jar2.PersistCookies())
|
||||
|
||||
assert.Contains(t, s.Get(settings.CookiesKey), "TestName1")
|
||||
assert.NotContains(t, s.Get(settings.CookiesKey), "TestName2")
|
||||
assert.Contains(t, s.Get(settings.CookiesKey), "TestName3")
|
||||
cookies, err := s.GetCookies()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, string(cookies), "TestName1")
|
||||
assert.NotContains(t, string(cookies), "TestName2")
|
||||
assert.Contains(t, string(cookies), "TestName3")
|
||||
}
|
||||
|
||||
type testCookie struct {
|
||||
@ -132,8 +137,8 @@ type testCookie struct {
|
||||
maxAge int
|
||||
}
|
||||
|
||||
func getClientWithJar(t *testing.T, s *settings.Settings) (*http.Client, *Jar) {
|
||||
jar, err := NewCookieJar(s)
|
||||
func getClientWithJar(t *testing.T, persister Persister) (*http.Client, *Jar) {
|
||||
jar, err := NewCookieJar(persister)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &http.Client{Jar: jar}, jar
|
||||
@ -168,12 +173,26 @@ func getTestServer(t *testing.T, wantCookies []testCookie) *httptest.Server {
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
// newFakeSettings creates a temporary folder for files.
|
||||
func newFakeSettings() *settings.Settings {
|
||||
dir, err := os.MkdirTemp("", "test-settings")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
type testPersister struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func newTestPersister(tb testing.TB) *testPersister {
|
||||
path := filepath.Join(tb.TempDir(), "cookies.json")
|
||||
|
||||
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
|
||||
if err := os.WriteFile(path, []byte{}, 0600); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return settings.New(dir)
|
||||
return &testPersister{path: path}
|
||||
}
|
||||
|
||||
func (p *testPersister) GetCookies() ([]byte, error) {
|
||||
return os.ReadFile(p.path)
|
||||
}
|
||||
|
||||
func (p *testPersister) SetCookies(rawCookies []byte) error {
|
||||
return os.WriteFile(p.path, rawCookies, 0600)
|
||||
}
|
||||
|
||||
@ -41,14 +41,11 @@ func (h *Handler) AddRecoveryAction(action RecoveryAction) *Handler {
|
||||
func (h *Handler) HandlePanic() {
|
||||
sentry.SkipDuringUnwind()
|
||||
|
||||
r := recover()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, action := range h.actions {
|
||||
if err := action(r); err != nil {
|
||||
logrus.WithError(err).Error("Failed to execute recovery action")
|
||||
if r := recover(); r != nil {
|
||||
for _, action := range h.actions {
|
||||
if err := action(r); err != nil {
|
||||
logrus.WithError(err).Error("Failed to execute recovery action")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,9 +15,10 @@
|
||||
// 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/>.
|
||||
|
||||
package pmapi
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -25,13 +26,13 @@ import (
|
||||
)
|
||||
|
||||
type TLSDialer interface {
|
||||
DialTLS(network, address string) (conn net.Conn, err error)
|
||||
DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error)
|
||||
}
|
||||
|
||||
// CreateTransportWithDialer creates an http.Transport that uses the given dialer to make TLS connections.
|
||||
func CreateTransportWithDialer(dialer TLSDialer) *http.Transport {
|
||||
return &http.Transport{
|
||||
DialTLS: dialer.DialTLS,
|
||||
DialTLSContext: dialer.DialTLSContext,
|
||||
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
MaxIdleConns: 100,
|
||||
@ -53,26 +54,24 @@ func CreateTransportWithDialer(dialer TLSDialer) *http.Transport {
|
||||
|
||||
// BasicTLSDialer implements TLSDialer.
|
||||
type BasicTLSDialer struct {
|
||||
cfg Config
|
||||
hostURL string
|
||||
}
|
||||
|
||||
// NewBasicTLSDialer returns a new BasicTLSDialer.
|
||||
func NewBasicTLSDialer(cfg Config) *BasicTLSDialer {
|
||||
func NewBasicTLSDialer(hostURL string) *BasicTLSDialer {
|
||||
return &BasicTLSDialer{
|
||||
cfg: cfg,
|
||||
hostURL: hostURL,
|
||||
}
|
||||
}
|
||||
|
||||
// DialTLS returns a connection to the given address using the given network.
|
||||
func (d *BasicTLSDialer) DialTLS(network, address string) (conn net.Conn, err error) {
|
||||
dialer := &net.Dialer{Timeout: 30 * time.Second} // Alternative Routes spec says this should be a 30s timeout.
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
|
||||
// If we are not dialing the standard API then we should skip cert verification checks.
|
||||
if address != d.cfg.HostURL {
|
||||
tlsConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec
|
||||
}
|
||||
|
||||
return tls.DialWithDialer(dialer, network, address, tlsConfig)
|
||||
func (d *BasicTLSDialer) DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
||||
return (&tls.Dialer{
|
||||
NetDialer: &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
Config: &tls.Config{
|
||||
InsecureSkipVerify: address != d.hostURL,
|
||||
},
|
||||
}).DialContext(ctx, network, address)
|
||||
}
|
||||
@ -15,13 +15,12 @@
|
||||
// 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/>.
|
||||
|
||||
package pmapi
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// TrustedAPIPins contains trusted public keys of the protonmail API and proxies.
|
||||
@ -56,36 +55,37 @@ const TLSReportURI = "https://reports.protonmail.ch/reports/tls"
|
||||
// PinningTLSDialer wraps a TLSDialer to check fingerprints after connecting and
|
||||
// to report errors if the fingerprint check fails.
|
||||
type PinningTLSDialer struct {
|
||||
dialer TLSDialer
|
||||
|
||||
// pinChecker is used to check TLS keys of connections.
|
||||
pinChecker *pinChecker
|
||||
|
||||
reporter *tlsReporter
|
||||
|
||||
// tlsIssueNotifier is used to notify something when there is a TLS issue.
|
||||
tlsIssueNotifier func()
|
||||
|
||||
// A logger for logging messages.
|
||||
log logrus.FieldLogger
|
||||
dialer TLSDialer
|
||||
pinChecker PinChecker
|
||||
reporter Reporter
|
||||
tlsIssueCh chan struct{}
|
||||
}
|
||||
|
||||
// NewPinningTLSDialer constructs a new dialer which only returns tcp connections to servers
|
||||
// Reporter is used to report TLS issues.
|
||||
type Reporter interface {
|
||||
ReportCertIssue(reportURI, host, port string, state tls.ConnectionState)
|
||||
}
|
||||
|
||||
// PinChecker is used to check TLS keys of connections.
|
||||
type PinChecker interface {
|
||||
CheckCertificate(conn net.Conn) error
|
||||
}
|
||||
|
||||
// NewPinningTLSDialer constructs a new dialer which only returns TCP connections to servers
|
||||
// which present known certificates.
|
||||
// If enabled, it reports any invalid certificates it finds.
|
||||
func NewPinningTLSDialer(cfg Config, dialer TLSDialer) *PinningTLSDialer {
|
||||
// It checks pins using the given pinChecker and reports issues using the given reporter.
|
||||
func NewPinningTLSDialer(dialer TLSDialer, reporter Reporter, pinChecker PinChecker) *PinningTLSDialer {
|
||||
return &PinningTLSDialer{
|
||||
dialer: dialer,
|
||||
pinChecker: newPinChecker(TrustedAPIPins),
|
||||
reporter: newTLSReporter(cfg, TrustedAPIPins),
|
||||
tlsIssueNotifier: cfg.TLSIssueHandler,
|
||||
log: logrus.WithField("pkg", "pmapi/tls-pinning"),
|
||||
dialer: dialer,
|
||||
pinChecker: pinChecker,
|
||||
reporter: reporter,
|
||||
tlsIssueCh: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// DialTLS dials the given network/address, returning an error if the certificates don't match the trusted pins.
|
||||
func (p *PinningTLSDialer) DialTLS(network, address string) (net.Conn, error) {
|
||||
conn, err := p.dialer.DialTLS(network, address)
|
||||
func (p *PinningTLSDialer) DialTLSContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
conn, err := p.dialer.DialTLSContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -95,22 +95,20 @@ func (p *PinningTLSDialer) DialTLS(network, address string) (net.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := p.pinChecker.checkCertificate(conn); err != nil {
|
||||
if p.tlsIssueNotifier != nil {
|
||||
go p.tlsIssueNotifier()
|
||||
if err := p.pinChecker.CheckCertificate(conn); err != nil {
|
||||
if tlsConn, ok := conn.(*tls.Conn); ok && p.reporter != nil {
|
||||
p.reporter.ReportCertIssue(TLSReportURI, host, port, tlsConn.ConnectionState())
|
||||
}
|
||||
|
||||
if tlsConn, ok := conn.(*tls.Conn); ok && p.reporter != nil {
|
||||
p.reporter.reportCertIssue(
|
||||
TLSReportURI,
|
||||
host,
|
||||
port,
|
||||
tlsConn.ConnectionState(),
|
||||
)
|
||||
}
|
||||
p.tlsIssueCh <- struct{}{}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// GetTLSIssueCh returns a channel which notifies when a TLS issue is reported.
|
||||
func (p *PinningTLSDialer) GetTLSIssueCh() <-chan struct{} {
|
||||
return p.tlsIssueCh
|
||||
}
|
||||
@ -15,7 +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/>.
|
||||
|
||||
package pmapi
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
@ -30,18 +30,18 @@ import (
|
||||
// ErrTLSMismatch indicates that no TLS fingerprint match could be found.
|
||||
var ErrTLSMismatch = errors.New("no TLS fingerprint match found")
|
||||
|
||||
type pinChecker struct {
|
||||
type TLSPinChecker struct {
|
||||
trustedPins []string
|
||||
}
|
||||
|
||||
func newPinChecker(trustedPins []string) *pinChecker {
|
||||
return &pinChecker{
|
||||
func NewTLSPinChecker(trustedPins []string) *TLSPinChecker {
|
||||
return &TLSPinChecker{
|
||||
trustedPins: trustedPins,
|
||||
}
|
||||
}
|
||||
|
||||
// checkCertificate returns whether the connection presents a known TLS certificate.
|
||||
func (p *pinChecker) checkCertificate(conn net.Conn) error {
|
||||
func (p *TLSPinChecker) CheckCertificate(conn net.Conn) error {
|
||||
tlsConn, ok := conn.(*tls.Conn)
|
||||
if !ok {
|
||||
return errors.New("connection is not a TLS connection")
|
||||
@ -15,17 +15,13 @@
|
||||
// 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/>.
|
||||
|
||||
package pmapi
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
// tlsReport is inspired by https://tools.ietf.org/html/rfc7469#section-3.
|
||||
@ -38,7 +34,7 @@ type tlsReport struct {
|
||||
Hostname string `json:"hostname"`
|
||||
|
||||
// Port to which the UA made original request that failed pin validation.
|
||||
Port int `json:"port"`
|
||||
Port string `json:"port"`
|
||||
|
||||
// EffectiveExpirationDate for noted pins in time.RFC3339 format.
|
||||
EffectiveExpirationDate string `json:"effective-expiration-date"`
|
||||
@ -89,12 +85,9 @@ type tlsReport struct {
|
||||
// newTLSReport constructs a new tlsReport configured with the given app version and known pinned public keys.
|
||||
// Temporal things (current date/time) are not set yet -- they are set when sendReport is called.
|
||||
func newTLSReport(host, port, server string, certChain, knownPins []string, appVersion string) (report tlsReport) {
|
||||
// If we can't parse the port for whatever reason, it doesn't really matter; we should report anyway.
|
||||
intPort, _ := strconv.Atoi(port)
|
||||
|
||||
report = tlsReport{
|
||||
Hostname: host,
|
||||
Port: intPort,
|
||||
Port: port,
|
||||
NotedHostname: server,
|
||||
ServedCertificateChain: certChain,
|
||||
KnownPins: knownPins,
|
||||
@ -105,40 +98,21 @@ func newTLSReport(host, port, server string, certChain, knownPins []string, appV
|
||||
}
|
||||
|
||||
// sendReport posts the given TLS report to the standard TLS Report URI.
|
||||
func (r tlsReport) sendReport(cfg Config, uri string) {
|
||||
func sendReport(report tlsReport, userAgent, appVersion, hostURL, remoteURI string) error {
|
||||
now := time.Now()
|
||||
r.DateTime = now.Format(time.RFC3339)
|
||||
r.EffectiveExpirationDate = now.Add(365 * 24 * 60 * 60 * time.Second).Format(time.RFC3339)
|
||||
|
||||
b, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to marshal TLS report")
|
||||
return
|
||||
report.DateTime = now.Format(time.RFC3339)
|
||||
report.EffectiveExpirationDate = now.Add(365 * 24 * time.Hour).Format(time.RFC3339)
|
||||
|
||||
if _, err := resty.New().
|
||||
SetTransport(CreateTransportWithDialer(NewBasicTLSDialer(hostURL))).
|
||||
SetHeader("User-Agent", userAgent).
|
||||
SetHeader("x-pm-appversion", appVersion).
|
||||
NewRequest().
|
||||
SetBody(report).
|
||||
Post(remoteURI); err != nil {
|
||||
return fmt.Errorf("failed to send TLS report: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", uri, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to create http request")
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", cfg.getUserAgent())
|
||||
req.Header.Set("x-pm-appversion", r.AppVersion)
|
||||
|
||||
logrus.WithField("request", req).Warn("Reporting TLS mismatch")
|
||||
res, err := (&http.Client{Transport: CreateTransportWithDialer(NewBasicTLSDialer(cfg))}).Do(req)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to report TLS mismatch")
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("response", res).Error("Reported TLS mismatch")
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
logrus.WithField("status", http.StatusOK).Error("StatusCode was not OK")
|
||||
}
|
||||
|
||||
_, _ = io.ReadAll(res.Body)
|
||||
_ = res.Body.Close()
|
||||
return nil
|
||||
}
|
||||
@ -15,7 +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/>.
|
||||
|
||||
package pmapi
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -24,6 +24,7 @@ import (
|
||||
"encoding/pem"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/useragent"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -33,21 +34,25 @@ type sentReport struct {
|
||||
t time.Time
|
||||
}
|
||||
|
||||
type tlsReporter struct {
|
||||
cfg Config
|
||||
type TLSReporter struct {
|
||||
hostURL string
|
||||
appVersion string
|
||||
userAgent *useragent.UserAgent
|
||||
trustedPins []string
|
||||
sentReports []sentReport
|
||||
}
|
||||
|
||||
func newTLSReporter(cfg Config, trustedPins []string) *tlsReporter {
|
||||
return &tlsReporter{
|
||||
cfg: cfg,
|
||||
func NewTLSReporter(hostURL, appVersion string, userAgent *useragent.UserAgent, trustedPins []string) *TLSReporter {
|
||||
return &TLSReporter{
|
||||
hostURL: hostURL,
|
||||
appVersion: appVersion,
|
||||
userAgent: userAgent,
|
||||
trustedPins: trustedPins,
|
||||
}
|
||||
}
|
||||
|
||||
// reportCertIssue reports a TLS key mismatch.
|
||||
func (r *tlsReporter) reportCertIssue(remoteURI, host, port string, connState tls.ConnectionState) {
|
||||
func (r *TLSReporter) ReportCertIssue(remoteURI, host, port string, connState tls.ConnectionState) {
|
||||
var certChain []string
|
||||
|
||||
if len(connState.VerifiedChains) > 0 {
|
||||
@ -56,16 +61,19 @@ func (r *tlsReporter) reportCertIssue(remoteURI, host, port string, connState tl
|
||||
certChain = marshalCert7468(connState.PeerCertificates)
|
||||
}
|
||||
|
||||
report := newTLSReport(host, port, connState.ServerName, certChain, r.trustedPins, r.cfg.AppVersion)
|
||||
report := newTLSReport(host, port, connState.ServerName, certChain, r.trustedPins, r.appVersion)
|
||||
|
||||
if !r.hasRecentlySentReport(report) {
|
||||
r.recordReport(report)
|
||||
go report.sendReport(r.cfg, remoteURI)
|
||||
|
||||
if err := sendReport(report, r.userAgent.GetUserAgent(), r.appVersion, r.hostURL, remoteURI); err != nil {
|
||||
logrus.WithError(err).Error("Failed to send TLS pinning report")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hasRecentlySentReport returns whether the report was already sent within the last 24 hours.
|
||||
func (r *tlsReporter) hasRecentlySentReport(report tlsReport) bool {
|
||||
func (r *TLSReporter) hasRecentlySentReport(report tlsReport) bool {
|
||||
var validReports []sentReport
|
||||
|
||||
for _, r := range r.sentReports {
|
||||
@ -86,7 +94,7 @@ func (r *tlsReporter) hasRecentlySentReport(report tlsReport) bool {
|
||||
}
|
||||
|
||||
// recordReport records the given report and the current time so we can check whether we recently sent this report.
|
||||
func (r *tlsReporter) recordReport(report tlsReport) {
|
||||
func (r *TLSReporter) recordReport(report tlsReport) {
|
||||
r.sentReports = append(r.sentReports, sentReport{r: report, t: time.Now()})
|
||||
}
|
||||
|
||||
@ -97,7 +105,7 @@ func marshalCert7468(certs []*x509.Certificate) (pemCerts []string) {
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
}); err != nil {
|
||||
logrus.WithField("pkg", "pmapi/tls-pinning").WithError(err).Error("Failed to encode TLS certificate")
|
||||
logrus.WithError(err).Error("Failed to encode TLS certificate")
|
||||
}
|
||||
pemCerts = append(pemCerts, buffer.String())
|
||||
buffer.Reset()
|
||||
@ -15,7 +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/>.
|
||||
|
||||
package pmapi
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
@ -24,6 +24,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/useragent"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -34,15 +35,11 @@ func TestTLSReporter_DoubleReport(t *testing.T) {
|
||||
reportCounter++
|
||||
}))
|
||||
|
||||
cfg := Config{
|
||||
AppVersion: "3",
|
||||
UserAgent: "useragent",
|
||||
}
|
||||
r := newTLSReporter(cfg, TrustedAPIPins)
|
||||
r := NewTLSReporter("hostURL", "appVersion", useragent.New(), TrustedAPIPins)
|
||||
|
||||
// Report the same issue many times.
|
||||
for i := 0; i < 10; i++ {
|
||||
r.reportCertIssue(reportServer.URL, "myhost", "443", tls.ConnectionState{})
|
||||
r.ReportCertIssue(reportServer.URL, "myhost", "443", tls.ConnectionState{})
|
||||
}
|
||||
|
||||
// We should only report once.
|
||||
@ -52,7 +49,7 @@ func TestTLSReporter_DoubleReport(t *testing.T) {
|
||||
|
||||
// If we then report something else many times.
|
||||
for i := 0; i < 10; i++ {
|
||||
r.reportCertIssue(reportServer.URL, "anotherhost", "443", tls.ConnectionState{})
|
||||
r.ReportCertIssue(reportServer.URL, "anotherhost", "443", tls.ConnectionState{})
|
||||
}
|
||||
|
||||
// We should get a second report.
|
||||
@ -15,112 +15,120 @@
|
||||
// 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/>.
|
||||
|
||||
package pmapi
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/useragent"
|
||||
a "github.com/stretchr/testify/assert"
|
||||
r "github.com/stretchr/testify/require"
|
||||
"gitlab.protontech.ch/go/liteapi"
|
||||
"gitlab.protontech.ch/go/liteapi/server"
|
||||
)
|
||||
|
||||
func TestTLSPinValid(t *testing.T) {
|
||||
called, _, cm := createClientWithPinningDialer(getRootURL())
|
||||
func getRootURL() string {
|
||||
return "https://api.protonmail.ch"
|
||||
}
|
||||
|
||||
func TestTLSPinValid(t *testing.T) {
|
||||
called, _, _, _, cm := createClientWithPinningDialer(getRootURL())
|
||||
|
||||
_, _, _ = cm.NewClientWithLogin(context.Background(), "username", "password")
|
||||
|
||||
_, _ = cm.getAuthInfo(context.Background(), GetAuthInfoReq{Username: "username"})
|
||||
checkTLSIssueHandler(t, 0, called)
|
||||
}
|
||||
|
||||
func TestTLSPinBackup(t *testing.T) {
|
||||
called, dialer, cm := createClientWithPinningDialer(getRootURL())
|
||||
copyTrustedPins(dialer.pinChecker)
|
||||
dialer.pinChecker.trustedPins[1] = dialer.pinChecker.trustedPins[0]
|
||||
dialer.pinChecker.trustedPins[0] = ""
|
||||
called, _, _, checker, cm := createClientWithPinningDialer(getRootURL())
|
||||
copyTrustedPins(checker)
|
||||
checker.trustedPins[1] = checker.trustedPins[0]
|
||||
checker.trustedPins[0] = ""
|
||||
|
||||
_, _, _ = cm.NewClientWithLogin(context.Background(), "username", "password")
|
||||
|
||||
_, _ = cm.getAuthInfo(context.Background(), GetAuthInfoReq{Username: "username"})
|
||||
checkTLSIssueHandler(t, 0, called)
|
||||
}
|
||||
|
||||
func TestTLSPinInvalid(t *testing.T) {
|
||||
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSONResponsefromFile(t, w, "/auth/info/post_response.json", 0)
|
||||
}))
|
||||
defer ts.Close()
|
||||
s := server.NewTLS()
|
||||
defer s.Close()
|
||||
|
||||
called, _, cm := createClientWithPinningDialer(ts.URL)
|
||||
called, _, _, _, cm := createClientWithPinningDialer(s.GetHostURL())
|
||||
|
||||
_, _, _ = cm.NewClientWithLogin(context.Background(), "username", "password")
|
||||
|
||||
_, _ = cm.getAuthInfo(context.Background(), GetAuthInfoReq{Username: "username"})
|
||||
checkTLSIssueHandler(t, 1, called)
|
||||
}
|
||||
|
||||
func TestTLSPinNoMatch(t *testing.T) {
|
||||
skipIfProxyIsSet(t)
|
||||
|
||||
called, dialer, cm := createClientWithPinningDialer(getRootURL())
|
||||
called, _, reporter, checker, cm := createClientWithPinningDialer(getRootURL())
|
||||
|
||||
copyTrustedPins(dialer.pinChecker)
|
||||
for i := 0; i < len(dialer.pinChecker.trustedPins); i++ {
|
||||
dialer.pinChecker.trustedPins[i] = "testing"
|
||||
copyTrustedPins(checker)
|
||||
for i := 0; i < len(checker.trustedPins); i++ {
|
||||
checker.trustedPins[i] = "testing"
|
||||
}
|
||||
|
||||
_, _ = cm.getAuthInfo(context.Background(), GetAuthInfoReq{Username: "username"})
|
||||
_, _ = cm.getAuthInfo(context.Background(), GetAuthInfoReq{Username: "username"})
|
||||
_, _, _ = cm.NewClientWithLogin(context.Background(), "username", "password")
|
||||
_, _, _ = cm.NewClientWithLogin(context.Background(), "username", "password")
|
||||
|
||||
// Check that it will be reported only once per session, but notified every time.
|
||||
r.Equal(t, 1, len(dialer.reporter.sentReports))
|
||||
r.Equal(t, 1, len(reporter.sentReports))
|
||||
checkTLSIssueHandler(t, 2, called)
|
||||
}
|
||||
|
||||
func TestTLSSignedCertWrongPublicKey(t *testing.T) {
|
||||
skipIfProxyIsSet(t)
|
||||
|
||||
_, dialer, _ := createClientWithPinningDialer("")
|
||||
_, err := dialer.DialTLS("tcp", "rsa4096.badssl.com:443")
|
||||
_, dialer, _, _, _ := createClientWithPinningDialer("")
|
||||
_, err := dialer.DialTLSContext(context.Background(), "tcp", "rsa4096.badssl.com:443")
|
||||
r.Error(t, err, "expected dial to fail because of wrong public key")
|
||||
}
|
||||
|
||||
func TestTLSSignedCertTrustedPublicKey(t *testing.T) {
|
||||
skipIfProxyIsSet(t)
|
||||
|
||||
_, dialer, _ := createClientWithPinningDialer("")
|
||||
copyTrustedPins(dialer.pinChecker)
|
||||
dialer.pinChecker.trustedPins = append(dialer.pinChecker.trustedPins, `pin-sha256="LwnIKjNLV3z243ap8y0yXNPghsqE76J08Eq3COvUt2E="`)
|
||||
_, err := dialer.DialTLS("tcp", "rsa4096.badssl.com:443")
|
||||
_, dialer, _, checker, _ := createClientWithPinningDialer("")
|
||||
copyTrustedPins(checker)
|
||||
checker.trustedPins = append(checker.trustedPins, `pin-sha256="LwnIKjNLV3z243ap8y0yXNPghsqE76J08Eq3COvUt2E="`)
|
||||
_, err := dialer.DialTLSContext(context.Background(), "tcp", "rsa4096.badssl.com:443")
|
||||
r.NoError(t, err, "expected dial to succeed because public key is known and cert is signed by CA")
|
||||
}
|
||||
|
||||
func TestTLSSelfSignedCertTrustedPublicKey(t *testing.T) {
|
||||
skipIfProxyIsSet(t)
|
||||
|
||||
_, dialer, _ := createClientWithPinningDialer("")
|
||||
copyTrustedPins(dialer.pinChecker)
|
||||
dialer.pinChecker.trustedPins = append(dialer.pinChecker.trustedPins, `pin-sha256="9SLklscvzMYj8f+52lp5ze/hY0CFHyLSPQzSpYYIBm8="`)
|
||||
_, err := dialer.DialTLS("tcp", "self-signed.badssl.com:443")
|
||||
_, dialer, _, checker, _ := createClientWithPinningDialer("")
|
||||
copyTrustedPins(checker)
|
||||
checker.trustedPins = append(checker.trustedPins, `pin-sha256="9SLklscvzMYj8f+52lp5ze/hY0CFHyLSPQzSpYYIBm8="`)
|
||||
_, err := dialer.DialTLSContext(context.Background(), "tcp", "self-signed.badssl.com:443")
|
||||
r.NoError(t, err, "expected dial to succeed because public key is known despite cert being self-signed")
|
||||
}
|
||||
|
||||
func createClientWithPinningDialer(hostURL string) (*int, *PinningTLSDialer, *manager) {
|
||||
func createClientWithPinningDialer(hostURL string) (*int, *PinningTLSDialer, *TLSReporter, *TLSPinChecker, *liteapi.Manager) {
|
||||
called := 0
|
||||
|
||||
cfg := Config{
|
||||
AppVersion: "Bridge_1.2.4-test",
|
||||
HostURL: hostURL,
|
||||
TLSIssueHandler: func() { called++ },
|
||||
}
|
||||
reporter := NewTLSReporter(hostURL, "appVersion", useragent.New(), TrustedAPIPins)
|
||||
checker := NewTLSPinChecker(TrustedAPIPins)
|
||||
dialer := NewPinningTLSDialer(NewBasicTLSDialer(hostURL), reporter, checker)
|
||||
|
||||
dialer := NewPinningTLSDialer(cfg, NewBasicTLSDialer(cfg))
|
||||
go func() {
|
||||
for range dialer.GetTLSIssueCh() {
|
||||
called++
|
||||
}
|
||||
}()
|
||||
|
||||
cm := newManager(cfg)
|
||||
cm.SetTransport(CreateTransportWithDialer(dialer))
|
||||
|
||||
return &called, dialer, cm
|
||||
return &called, dialer, reporter, checker, liteapi.New(
|
||||
liteapi.WithHostURL(hostURL),
|
||||
liteapi.WithTransport(CreateTransportWithDialer(dialer)),
|
||||
)
|
||||
}
|
||||
|
||||
func copyTrustedPins(pinChecker *pinChecker) {
|
||||
func copyTrustedPins(pinChecker *TLSPinChecker) {
|
||||
copiedPins := make([]string, len(pinChecker.trustedPins))
|
||||
copy(copiedPins, pinChecker.trustedPins)
|
||||
pinChecker.trustedPins = copiedPins
|
||||
@ -15,9 +15,10 @@
|
||||
// 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/>.
|
||||
|
||||
package pmapi
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/url"
|
||||
"sync"
|
||||
@ -27,6 +28,8 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var ErrNoConnection = errors.New("no connection")
|
||||
|
||||
// ProxyTLSDialer wraps a TLSDialer to switch to a proxy if the initial dial fails.
|
||||
type ProxyTLSDialer struct {
|
||||
dialer TLSDialer
|
||||
@ -40,13 +43,13 @@ type ProxyTLSDialer struct {
|
||||
}
|
||||
|
||||
// NewProxyTLSDialer constructs a dialer which provides a proxy-managing layer on top of an underlying dialer.
|
||||
func NewProxyTLSDialer(cfg Config, dialer TLSDialer) *ProxyTLSDialer {
|
||||
func NewProxyTLSDialer(dialer TLSDialer, hostURL string) *ProxyTLSDialer {
|
||||
return &ProxyTLSDialer{
|
||||
dialer: dialer,
|
||||
locker: sync.RWMutex{},
|
||||
directAddress: formatAsAddress(cfg.HostURL),
|
||||
proxyAddress: formatAsAddress(cfg.HostURL),
|
||||
proxyProvider: newProxyProvider(cfg, dohProviders, proxyQuery),
|
||||
directAddress: formatAsAddress(hostURL),
|
||||
proxyAddress: formatAsAddress(hostURL),
|
||||
proxyProvider: newProxyProvider(dialer, hostURL, DoHProviders),
|
||||
proxyUseDuration: proxyUseDuration,
|
||||
}
|
||||
}
|
||||
@ -73,22 +76,21 @@ func formatAsAddress(rawURL string) string {
|
||||
}
|
||||
|
||||
// DialTLS dials the given network/address. If it fails, it retries using a proxy.
|
||||
func (d *ProxyTLSDialer) DialTLS(network, address string) (net.Conn, error) {
|
||||
func (d *ProxyTLSDialer) DialTLSContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
if address == d.directAddress {
|
||||
address = d.proxyAddress
|
||||
}
|
||||
|
||||
conn, err := d.dialer.DialTLS(network, address)
|
||||
conn, err := d.dialer.DialTLSContext(ctx, network, address)
|
||||
if err == nil || !d.allowProxy {
|
||||
return conn, err
|
||||
}
|
||||
|
||||
err = d.switchToReachableServer()
|
||||
if err != nil {
|
||||
if err := d.switchToReachableServer(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return d.dialer.DialTLS(network, d.proxyAddress)
|
||||
return d.dialer.DialTLSContext(ctx, network, d.proxyAddress)
|
||||
}
|
||||
|
||||
// switchToReachableServer switches to using a reachable server (either proxy or standard API).
|
||||
@ -128,6 +130,7 @@ func (d *ProxyTLSDialer) switchToReachableServer() error {
|
||||
}
|
||||
|
||||
d.proxyAddress = proxyAddress
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -15,7 +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/>.
|
||||
|
||||
package pmapi
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -27,6 +27,7 @@ import (
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -35,14 +36,14 @@ const (
|
||||
proxyCacheRefreshTimeout = 20 * time.Second
|
||||
proxyDoHTimeout = 20 * time.Second
|
||||
proxyCanReachTimeout = 20 * time.Second
|
||||
proxyQuery = "dMFYGSLTQOJXXI33ONVQWS3BOMNUA.protonpro.xyz"
|
||||
|
||||
proxyQuery = "dMFYGSLTQOJXXI33ONVQWS3BOMNUA.protonpro.xyz"
|
||||
Quad9Provider = "https://dns11.quad9.net/dns-query"
|
||||
Quad9PortProvider = "https://dns11.quad9.net:5053/dns-query"
|
||||
GoogleProvider = "https://dns.google/dns-query"
|
||||
)
|
||||
|
||||
var dohProviders = []string{ //nolint:gochecknoglobals
|
||||
var DoHProviders = []string{ //nolint:gochecknoglobals
|
||||
Quad9Provider,
|
||||
Quad9PortProvider,
|
||||
GoogleProvider,
|
||||
@ -50,7 +51,9 @@ var dohProviders = []string{ //nolint:gochecknoglobals
|
||||
|
||||
// proxyProvider manages known proxies.
|
||||
type proxyProvider struct {
|
||||
cfg Config
|
||||
dialer TLSDialer
|
||||
|
||||
hostURL string
|
||||
|
||||
// dohLookup is used to look up the given query at the given DoH provider, returning the TXT records>
|
||||
dohLookup func(ctx context.Context, query, provider string) (urls []string, err error)
|
||||
@ -68,11 +71,12 @@ type proxyProvider struct {
|
||||
|
||||
// newProxyProvider creates a new proxyProvider that queries the given DoH providers
|
||||
// to retrieve DNS records for the given query string.
|
||||
func newProxyProvider(cfg Config, providers []string, query string) (p *proxyProvider) { //nolint:unparam
|
||||
func newProxyProvider(dialer TLSDialer, hostURL string, providers []string) (p *proxyProvider) {
|
||||
p = &proxyProvider{
|
||||
cfg: cfg,
|
||||
dialer: dialer,
|
||||
hostURL: hostURL,
|
||||
providers: providers,
|
||||
query: query,
|
||||
query: proxyQuery,
|
||||
cacheRefreshTimeout: proxyCacheRefreshTimeout,
|
||||
dohTimeout: proxyDoHTimeout,
|
||||
canReachTimeout: proxyCanReachTimeout,
|
||||
@ -86,7 +90,7 @@ func newProxyProvider(cfg Config, providers []string, query string) (p *proxyPro
|
||||
|
||||
// findReachableServer returns a working API server (either proxy or standard API).
|
||||
func (p *proxyProvider) findReachableServer() (proxy string, err error) {
|
||||
log.Debug("Trying to find a reachable server")
|
||||
logrus.Debug("Trying to find a reachable server")
|
||||
|
||||
if time.Now().Before(p.lastLookup.Add(proxyLookupWait)) {
|
||||
return "", errors.New("not looking for a proxy, too soon")
|
||||
@ -106,7 +110,7 @@ func (p *proxyProvider) findReachableServer() (proxy string, err error) {
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
apiReachable = p.canReach(p.cfg.HostURL)
|
||||
apiReachable = p.canReach(p.hostURL)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
@ -117,7 +121,7 @@ func (p *proxyProvider) findReachableServer() (proxy string, err error) {
|
||||
wg.Wait()
|
||||
|
||||
if apiReachable {
|
||||
proxy = p.cfg.HostURL
|
||||
proxy = p.hostURL
|
||||
return
|
||||
}
|
||||
|
||||
@ -138,7 +142,7 @@ func (p *proxyProvider) findReachableServer() (proxy string, err error) {
|
||||
// refreshProxyCache loads the latest proxies from the known providers.
|
||||
// If the process takes longer than proxyCacheRefreshTimeout, an error is returned.
|
||||
func (p *proxyProvider) refreshProxyCache() error {
|
||||
log.Info("Refreshing proxy cache")
|
||||
logrus.Info("Refreshing proxy cache")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), p.cacheRefreshTimeout)
|
||||
defer cancel()
|
||||
@ -169,21 +173,19 @@ func (p *proxyProvider) refreshProxyCache() error {
|
||||
|
||||
// canReach returns whether we can reach the given url.
|
||||
func (p *proxyProvider) canReach(url string) bool {
|
||||
log.WithField("url", url).Debug("Trying to ping proxy")
|
||||
logrus.WithField("url", url).Debug("Trying to ping proxy")
|
||||
|
||||
if !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") {
|
||||
url = "https://" + url
|
||||
}
|
||||
|
||||
dialer := NewPinningTLSDialer(p.cfg, NewBasicTLSDialer(p.cfg))
|
||||
|
||||
pinger := resty.New().
|
||||
SetBaseURL(url).
|
||||
SetTimeout(p.canReachTimeout).
|
||||
SetTransport(CreateTransportWithDialer(dialer))
|
||||
SetTransport(CreateTransportWithDialer(p.dialer))
|
||||
|
||||
if _, err := pinger.R().Get("/tests/ping"); err != nil {
|
||||
log.WithField("proxy", url).WithError(err).Warn("Failed to ping proxy")
|
||||
logrus.WithField("proxy", url).WithError(err).Warn("Failed to ping proxy")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -240,15 +242,15 @@ func (p *proxyProvider) defaultDoHLookup(ctx context.Context, query, dohProvider
|
||||
|
||||
select {
|
||||
case data = <-dataChan:
|
||||
log.WithField("data", data).Info("Received TXT records")
|
||||
logrus.WithField("data", data).Info("Received TXT records")
|
||||
return
|
||||
|
||||
case err = <-errChan:
|
||||
log.WithField("provider", dohProvider).WithError(err).Error("Failed to query DNS records")
|
||||
logrus.WithField("provider", dohProvider).WithError(err).Error("Failed to query DNS records")
|
||||
return
|
||||
|
||||
case <-ctx.Done():
|
||||
log.WithField("provider", dohProvider).Error("Timed out querying DNS records")
|
||||
logrus.WithField("provider", dohProvider).Error("Timed out querying DNS records")
|
||||
return []string{}, errors.New("timed out querying DNS records")
|
||||
}
|
||||
}
|
||||
@ -15,7 +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/>.
|
||||
|
||||
package pmapi
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -23,15 +23,15 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/useragent"
|
||||
r "github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/http/httpproxy"
|
||||
)
|
||||
|
||||
func TestProxyProvider_FindProxy(t *testing.T) {
|
||||
proxy := getTrustedServer()
|
||||
defer closeServer(proxy)
|
||||
|
||||
p := newProxyProvider(Config{HostURL: ""}, []string{"not used"}, "not used")
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy.URL}, nil }
|
||||
|
||||
url, err := p.findReachableServer()
|
||||
@ -47,7 +47,7 @@ func TestProxyProvider_FindProxy_ChooseReachableProxy(t *testing.T) {
|
||||
unreachableProxy := getTrustedServer()
|
||||
closeServer(unreachableProxy)
|
||||
|
||||
p := newProxyProvider(Config{HostURL: ""}, []string{"not used"}, "not used")
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
|
||||
return []string{reachableProxy.URL, unreachableProxy.URL}, nil
|
||||
}
|
||||
@ -64,7 +64,11 @@ func TestProxyProvider_FindProxy_ChooseTrustedProxy(t *testing.T) {
|
||||
untrustedProxy := getUntrustedServer()
|
||||
defer closeServer(untrustedProxy)
|
||||
|
||||
p := newProxyProvider(Config{HostURL: ""}, []string{"not used"}, "not used")
|
||||
reporter := NewTLSReporter("", "appVersion", useragent.New(), TrustedAPIPins)
|
||||
checker := NewTLSPinChecker(TrustedAPIPins)
|
||||
dialer := NewPinningTLSDialer(NewBasicTLSDialer(""), reporter, checker)
|
||||
|
||||
p := newProxyProvider(dialer, "", []string{"not used"})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
|
||||
return []string{untrustedProxy.URL, trustedProxy.URL}, nil
|
||||
}
|
||||
@ -81,7 +85,7 @@ func TestProxyProvider_FindProxy_FailIfNoneReachable(t *testing.T) {
|
||||
unreachableProxy2 := getTrustedServer()
|
||||
closeServer(unreachableProxy2)
|
||||
|
||||
p := newProxyProvider(Config{HostURL: ""}, []string{"not used"}, "not used")
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
|
||||
return []string{unreachableProxy1.URL, unreachableProxy2.URL}, nil
|
||||
}
|
||||
@ -97,7 +101,11 @@ func TestProxyProvider_FindProxy_FailIfNoneTrusted(t *testing.T) {
|
||||
untrustedProxy2 := getUntrustedServer()
|
||||
defer closeServer(untrustedProxy2)
|
||||
|
||||
p := newProxyProvider(Config{HostURL: ""}, []string{"not used"}, "not used")
|
||||
reporter := NewTLSReporter("", "appVersion", useragent.New(), TrustedAPIPins)
|
||||
checker := NewTLSPinChecker(TrustedAPIPins)
|
||||
dialer := NewPinningTLSDialer(NewBasicTLSDialer(""), reporter, checker)
|
||||
|
||||
p := newProxyProvider(dialer, "", []string{"not used"})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
|
||||
return []string{untrustedProxy1.URL, untrustedProxy2.URL}, nil
|
||||
}
|
||||
@ -107,7 +115,7 @@ func TestProxyProvider_FindProxy_FailIfNoneTrusted(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProxyProvider_FindProxy_RefreshCacheTimeout(t *testing.T) {
|
||||
p := newProxyProvider(Config{HostURL: ""}, []string{"not used"}, "not used")
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
|
||||
p.cacheRefreshTimeout = 1 * time.Second
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { time.Sleep(2 * time.Second); return nil, nil }
|
||||
|
||||
@ -124,7 +132,7 @@ func TestProxyProvider_FindProxy_CanReachTimeout(t *testing.T) {
|
||||
}))
|
||||
defer closeServer(slowProxy)
|
||||
|
||||
p := newProxyProvider(Config{HostURL: ""}, []string{"not used"}, "not used")
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
|
||||
p.canReachTimeout = 1 * time.Second
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{slowProxy.URL}, nil }
|
||||
|
||||
@ -136,7 +144,7 @@ func TestProxyProvider_FindProxy_CanReachTimeout(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProxyProvider_DoHLookup_Quad9(t *testing.T) {
|
||||
p := newProxyProvider(Config{}, []string{Quad9Provider, GoogleProvider}, proxyQuery)
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider})
|
||||
|
||||
records, err := p.dohLookup(context.Background(), proxyQuery, Quad9Provider)
|
||||
r.NoError(t, err)
|
||||
@ -147,7 +155,7 @@ func TestProxyProvider_DoHLookup_Quad9(t *testing.T) {
|
||||
// port filter. Basic functionality should be covered by other tests. Keeping
|
||||
// code here to be able to run it locally if needed.
|
||||
func DISABLEDTestProxyProviderDoHLookupQuad9Port(t *testing.T) {
|
||||
p := newProxyProvider(Config{}, []string{Quad9PortProvider, GoogleProvider}, proxyQuery)
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider})
|
||||
|
||||
records, err := p.dohLookup(context.Background(), proxyQuery, Quad9PortProvider)
|
||||
r.NoError(t, err)
|
||||
@ -155,7 +163,7 @@ func DISABLEDTestProxyProviderDoHLookupQuad9Port(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProxyProvider_DoHLookup_Google(t *testing.T) {
|
||||
p := newProxyProvider(Config{}, []string{Quad9Provider, GoogleProvider}, proxyQuery)
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider})
|
||||
|
||||
records, err := p.dohLookup(context.Background(), proxyQuery, GoogleProvider)
|
||||
r.NoError(t, err)
|
||||
@ -165,7 +173,7 @@ func TestProxyProvider_DoHLookup_Google(t *testing.T) {
|
||||
func TestProxyProvider_DoHLookup_FindProxy(t *testing.T) {
|
||||
skipIfProxyIsSet(t)
|
||||
|
||||
p := newProxyProvider(Config{}, []string{Quad9Provider, GoogleProvider}, proxyQuery)
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider})
|
||||
|
||||
url, err := p.findReachableServer()
|
||||
r.NoError(t, err)
|
||||
@ -175,18 +183,9 @@ func TestProxyProvider_DoHLookup_FindProxy(t *testing.T) {
|
||||
func TestProxyProvider_DoHLookup_FindProxyFirstProviderUnreachable(t *testing.T) {
|
||||
skipIfProxyIsSet(t)
|
||||
|
||||
p := newProxyProvider(Config{}, []string{"https://unreachable", Quad9Provider, GoogleProvider}, proxyQuery)
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"https://unreachable", Quad9Provider, GoogleProvider})
|
||||
|
||||
url, err := p.findReachableServer()
|
||||
r.NoError(t, err)
|
||||
r.NotEmpty(t, url)
|
||||
}
|
||||
|
||||
// skipIfProxyIsSet skips the tests if HTTPS proxy is set.
|
||||
// Should be used for tests depending on proper certificate checks which
|
||||
// is not possible under our CI setup.
|
||||
func skipIfProxyIsSet(t *testing.T) {
|
||||
if httpproxy.FromEnvironment().HTTPSProxy != "" {
|
||||
t.SkipNow()
|
||||
}
|
||||
}
|
||||
@ -15,7 +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/>.
|
||||
|
||||
package pmapi
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -141,9 +141,10 @@ func TestProxyDialer_UseProxy(t *testing.T) {
|
||||
trustedProxy := getTrustedServer()
|
||||
defer closeServer(trustedProxy)
|
||||
|
||||
cfg := Config{HostURL: ""}
|
||||
d := NewProxyTLSDialer(cfg, NewBasicTLSDialer(cfg))
|
||||
d.proxyProvider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil }
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
|
||||
d.proxyProvider = provider
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil }
|
||||
|
||||
err := d.switchToReachableServer()
|
||||
require.NoError(t, err)
|
||||
@ -158,9 +159,10 @@ func TestProxyDialer_UseProxy_MultipleTimes(t *testing.T) {
|
||||
proxy3 := getTrustedServer()
|
||||
defer closeServer(proxy3)
|
||||
|
||||
cfg := Config{HostURL: ""}
|
||||
d := NewProxyTLSDialer(cfg, NewBasicTLSDialer(cfg))
|
||||
d.proxyProvider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL}, nil }
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
|
||||
d.proxyProvider = provider
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL}, nil }
|
||||
|
||||
err := d.switchToReachableServer()
|
||||
require.NoError(t, err)
|
||||
@ -169,7 +171,7 @@ func TestProxyDialer_UseProxy_MultipleTimes(t *testing.T) {
|
||||
// Have to wait so as to not get rejected.
|
||||
time.Sleep(proxyLookupWait)
|
||||
|
||||
d.proxyProvider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy2.URL}, nil }
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy2.URL}, nil }
|
||||
err = d.switchToReachableServer()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, formatAsAddress(proxy2.URL), d.proxyAddress)
|
||||
@ -177,7 +179,7 @@ func TestProxyDialer_UseProxy_MultipleTimes(t *testing.T) {
|
||||
// Have to wait so as to not get rejected.
|
||||
time.Sleep(proxyLookupWait)
|
||||
|
||||
d.proxyProvider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy3.URL}, nil }
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy3.URL}, nil }
|
||||
err = d.switchToReachableServer()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, formatAsAddress(proxy3.URL), d.proxyAddress)
|
||||
@ -187,11 +189,12 @@ func TestProxyDialer_UseProxy_RevertAfterTime(t *testing.T) {
|
||||
trustedProxy := getTrustedServer()
|
||||
defer closeServer(trustedProxy)
|
||||
|
||||
cfg := Config{HostURL: ""}
|
||||
d := NewProxyTLSDialer(cfg, NewBasicTLSDialer(cfg))
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
|
||||
d.proxyProvider = provider
|
||||
d.proxyUseDuration = time.Second
|
||||
|
||||
d.proxyProvider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil }
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil }
|
||||
err := d.switchToReachableServer()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, formatAsAddress(trustedProxy.URL), d.proxyAddress)
|
||||
@ -203,9 +206,10 @@ func TestProxyDialer_UseProxy_RevertAfterTime(t *testing.T) {
|
||||
func TestProxyDialer_UseProxy_RevertIfProxyStopsWorkingAndOriginalAPIIsReachable(t *testing.T) {
|
||||
trustedProxy := getTrustedServer()
|
||||
|
||||
cfg := Config{HostURL: ""}
|
||||
d := NewProxyTLSDialer(cfg, NewBasicTLSDialer(cfg))
|
||||
d.proxyProvider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil }
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
|
||||
d.proxyProvider = provider
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil }
|
||||
|
||||
err := d.switchToReachableServer()
|
||||
require.NoError(t, err)
|
||||
@ -214,7 +218,7 @@ func TestProxyDialer_UseProxy_RevertIfProxyStopsWorkingAndOriginalAPIIsReachable
|
||||
// Simulate that the proxy stops working and that the standard api is reachable again.
|
||||
closeServer(trustedProxy)
|
||||
d.directAddress = formatAsAddress(getRootURL())
|
||||
d.proxyProvider.cfg.HostURL = getRootURL()
|
||||
provider.hostURL = getRootURL()
|
||||
time.Sleep(proxyLookupWait)
|
||||
|
||||
// We should now find the original API URL if it is working again.
|
||||
@ -232,9 +236,10 @@ func TestProxyDialer_UseProxy_FindSecondAlternativeIfFirstFailsAndAPIIsStillBloc
|
||||
proxy2 := getTrustedServer()
|
||||
defer closeServer(proxy2)
|
||||
|
||||
cfg := Config{HostURL: ""}
|
||||
d := NewProxyTLSDialer(cfg, NewBasicTLSDialer(cfg))
|
||||
d.proxyProvider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL, proxy2.URL}, nil }
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
|
||||
d.proxyProvider = provider
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL, proxy2.URL}, nil }
|
||||
|
||||
err := d.switchToReachableServer()
|
||||
require.NoError(t, err)
|
||||
16
internal/dialer/dialer_test.go
Normal file
16
internal/dialer/dialer_test.go
Normal file
@ -0,0 +1,16 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/http/httpproxy"
|
||||
)
|
||||
|
||||
// skipIfProxyIsSet skips the tests if HTTPS proxy is set.
|
||||
// Should be used for tests depending on proper certificate checks which
|
||||
// is not possible under our CI setup.
|
||||
func skipIfProxyIsSet(t *testing.T) {
|
||||
if httpproxy.FromEnvironment().HTTPSProxy != "" {
|
||||
t.SkipNow()
|
||||
}
|
||||
}
|
||||
13
internal/events/connection.go
Normal file
13
internal/events/connection.go
Normal file
@ -0,0 +1,13 @@
|
||||
package events
|
||||
|
||||
import "gitlab.protontech.ch/go/liteapi"
|
||||
|
||||
type TLSIssue struct {
|
||||
eventBase
|
||||
}
|
||||
|
||||
type ConnStatus struct {
|
||||
eventBase
|
||||
|
||||
Status liteapi.Status
|
||||
}
|
||||
7
internal/events/error.go
Normal file
7
internal/events/error.go
Normal file
@ -0,0 +1,7 @@
|
||||
package events
|
||||
|
||||
type Error struct {
|
||||
eventBase
|
||||
|
||||
Error error
|
||||
}
|
||||
@ -1,60 +1,9 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package events provides names of events used by the event listener in bridge.
|
||||
package events
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||
)
|
||||
|
||||
// Constants of events used by the event listener in bridge.
|
||||
const (
|
||||
ErrorEvent = "error"
|
||||
CredentialsErrorEvent = "credentialsError"
|
||||
CloseConnectionEvent = "closeConnection"
|
||||
LogoutEvent = "logout"
|
||||
AddressChangedEvent = "addressChanged"
|
||||
AddressChangedLogoutEvent = "addressChangedLogout"
|
||||
UserRefreshEvent = "userRefresh"
|
||||
RestartBridgeEvent = "restartBridge"
|
||||
InternetConnChangedEvent = "internetChanged"
|
||||
InternetOff = "internetOff"
|
||||
InternetOn = "internetOn"
|
||||
SecondInstanceEvent = "secondInstance"
|
||||
NoActiveKeyForRecipientEvent = "noActiveKeyForRecipient"
|
||||
UpgradeApplicationEvent = "upgradeApplication"
|
||||
TLSCertIssue = "tlsCertPinningIssue"
|
||||
UserChangeDone = "QMLUserChangedDone"
|
||||
|
||||
// LogoutEventTimeout is the minimum time to permit between logout events being sent.
|
||||
LogoutEventTimeout = 3 * time.Minute
|
||||
)
|
||||
|
||||
// SetupEvents specific to event type and data.
|
||||
func SetupEvents(listener listener.Listener) {
|
||||
listener.SetLimit(LogoutEvent, LogoutEventTimeout)
|
||||
listener.SetBuffer(ErrorEvent)
|
||||
listener.SetBuffer(CredentialsErrorEvent)
|
||||
listener.SetBuffer(InternetConnChangedEvent)
|
||||
listener.SetBuffer(UpgradeApplicationEvent)
|
||||
listener.SetBuffer(TLSCertIssue)
|
||||
listener.SetBuffer(UserRefreshEvent)
|
||||
listener.Book(UserChangeDone)
|
||||
type Event interface {
|
||||
_isEvent()
|
||||
}
|
||||
|
||||
type eventBase struct{}
|
||||
|
||||
func (eventBase) _isEvent() {}
|
||||
|
||||
5
internal/events/raise.go
Normal file
5
internal/events/raise.go
Normal file
@ -0,0 +1,5 @@
|
||||
package events
|
||||
|
||||
type Raise struct {
|
||||
eventBase
|
||||
}
|
||||
24
internal/events/sync.go
Normal file
24
internal/events/sync.go
Normal file
@ -0,0 +1,24 @@
|
||||
package events
|
||||
|
||||
import "time"
|
||||
|
||||
type SyncStarted struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
}
|
||||
|
||||
type SyncProgress struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
Progress float64
|
||||
Elapsed time.Duration
|
||||
Remaining time.Duration
|
||||
}
|
||||
|
||||
type SyncFinished struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
}
|
||||
25
internal/events/update.go
Normal file
25
internal/events/update.go
Normal file
@ -0,0 +1,25 @@
|
||||
package events
|
||||
|
||||
import "github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
|
||||
type UpdateAvailable struct {
|
||||
eventBase
|
||||
|
||||
Version updater.VersionInfo
|
||||
|
||||
CanInstall bool
|
||||
}
|
||||
|
||||
type UpdateNotAvailable struct {
|
||||
eventBase
|
||||
}
|
||||
|
||||
type UpdateInstalled struct {
|
||||
eventBase
|
||||
|
||||
Version updater.VersionInfo
|
||||
}
|
||||
|
||||
type UpdateForced struct {
|
||||
eventBase
|
||||
}
|
||||
52
internal/events/user.go
Normal file
52
internal/events/user.go
Normal file
@ -0,0 +1,52 @@
|
||||
package events
|
||||
|
||||
type UserLoggedIn struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
}
|
||||
|
||||
type UserLoggedOut struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
}
|
||||
|
||||
type UserDeauth struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
}
|
||||
|
||||
type UserDeleted struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
}
|
||||
|
||||
type UserChanged struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
}
|
||||
|
||||
type UserAddressCreated struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
Address string
|
||||
}
|
||||
|
||||
type UserAddressChanged struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
Address string
|
||||
}
|
||||
|
||||
type UserAddressDeleted struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
Address string
|
||||
}
|
||||
32
internal/focus/client.go
Normal file
32
internal/focus/client.go
Normal file
@ -0,0 +1,32 @@
|
||||
package focus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/focus/proto"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
func TryRaise() bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
cc, err := grpc.DialContext(ctx, net.JoinHostPort(Host, fmt.Sprint(Port)), grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, err := proto.NewFocusClient(cc).Raise(ctx, &emptypb.Empty{}); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if err := cc.Close(); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
25
internal/focus/focus_test.go
Normal file
25
internal/focus/focus_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package focus
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFocusRaise(t *testing.T) {
|
||||
// Start the focus service.
|
||||
service, err := NewService()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to dial it, it should succeed.
|
||||
require.True(t, TryRaise())
|
||||
|
||||
// The service should report a raise call.
|
||||
<-service.GetRaiseCh()
|
||||
|
||||
// Stop the service.
|
||||
service.Close()
|
||||
|
||||
// Try to dial it, it should fail.
|
||||
require.False(t, TryRaise())
|
||||
}
|
||||
3
internal/focus/proto/focus.go
Normal file
3
internal/focus/proto/focus.go
Normal file
@ -0,0 +1,3 @@
|
||||
package proto
|
||||
|
||||
//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative focus.proto
|
||||
93
internal/focus/proto/focus.pb.go
Normal file
93
internal/focus/proto/focus.pb.go
Normal file
@ -0,0 +1,93 @@
|
||||
// Copyright (c) 2022 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.1
|
||||
// protoc v3.21.6
|
||||
// source: focus.proto
|
||||
|
||||
package proto
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
var File_focus_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_focus_proto_rawDesc = []byte{
|
||||
0x0a, 0x0b, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x32, 0x40, 0x0a, 0x05, 0x46, 0x6f, 0x63, 0x75, 0x73, 0x12, 0x37, 0x0a, 0x05, 0x52, 0x61,
|
||||
0x69, 0x73, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
|
||||
0x70, 0x74, 0x79, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
|
||||
0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2f, 0x76, 0x32, 0x2f, 0x69, 0x6e,
|
||||
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2f, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var file_focus_proto_goTypes = []interface{}{
|
||||
(*emptypb.Empty)(nil), // 0: google.protobuf.Empty
|
||||
}
|
||||
var file_focus_proto_depIdxs = []int32{
|
||||
0, // 0: proto.Focus.Raise:input_type -> google.protobuf.Empty
|
||||
0, // 1: proto.Focus.Raise:output_type -> google.protobuf.Empty
|
||||
1, // [1:2] is the sub-list for method output_type
|
||||
0, // [0:1] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_focus_proto_init() }
|
||||
func file_focus_proto_init() {
|
||||
if File_focus_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_focus_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 0,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_focus_proto_goTypes,
|
||||
DependencyIndexes: file_focus_proto_depIdxs,
|
||||
}.Build()
|
||||
File_focus_proto = out.File
|
||||
file_focus_proto_rawDesc = nil
|
||||
file_focus_proto_goTypes = nil
|
||||
file_focus_proto_depIdxs = nil
|
||||
}
|
||||
31
internal/focus/proto/focus.proto
Normal file
31
internal/focus/proto/focus.proto
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2022 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
option go_package = "github.com/ProtonMail/proton-bridge/v2/internal/focus/proto";
|
||||
|
||||
package proto;
|
||||
|
||||
//**********************************************************************************************************************
|
||||
// Service Declaration
|
||||
//**********************************************************************************************************************≠––
|
||||
service Focus {
|
||||
rpc Raise(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
}
|
||||
107
internal/focus/proto/focus_grpc.pb.go
Normal file
107
internal/focus/proto/focus_grpc.pb.go
Normal file
@ -0,0 +1,107 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.21.6
|
||||
// source: focus.proto
|
||||
|
||||
package proto
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
|
||||
// FocusClient is the client API for Focus service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type FocusClient interface {
|
||||
Raise(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
}
|
||||
|
||||
type focusClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewFocusClient(cc grpc.ClientConnInterface) FocusClient {
|
||||
return &focusClient{cc}
|
||||
}
|
||||
|
||||
func (c *focusClient) Raise(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, "/proto.Focus/Raise", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// FocusServer is the server API for Focus service.
|
||||
// All implementations must embed UnimplementedFocusServer
|
||||
// for forward compatibility
|
||||
type FocusServer interface {
|
||||
Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||
mustEmbedUnimplementedFocusServer()
|
||||
}
|
||||
|
||||
// UnimplementedFocusServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedFocusServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedFocusServer) Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Raise not implemented")
|
||||
}
|
||||
func (UnimplementedFocusServer) mustEmbedUnimplementedFocusServer() {}
|
||||
|
||||
// UnsafeFocusServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to FocusServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeFocusServer interface {
|
||||
mustEmbedUnimplementedFocusServer()
|
||||
}
|
||||
|
||||
func RegisterFocusServer(s grpc.ServiceRegistrar, srv FocusServer) {
|
||||
s.RegisterService(&Focus_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _Focus_Raise_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(emptypb.Empty)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(FocusServer).Raise(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/proto.Focus/Raise",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(FocusServer).Raise(ctx, req.(*emptypb.Empty))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Focus_ServiceDesc is the grpc.ServiceDesc for Focus service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var Focus_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "proto.Focus",
|
||||
HandlerType: (*FocusServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Raise",
|
||||
Handler: _Focus_Raise_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "focus.proto",
|
||||
}
|
||||
60
internal/focus/service.go
Normal file
60
internal/focus/service.go
Normal file
@ -0,0 +1,60 @@
|
||||
package focus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/focus/proto"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
const (
|
||||
Host = "127.0.0.1"
|
||||
Port = 1042
|
||||
)
|
||||
|
||||
type FocusService struct {
|
||||
proto.UnimplementedFocusServer
|
||||
|
||||
server *grpc.Server
|
||||
listener net.Listener
|
||||
raiseCh chan struct{}
|
||||
}
|
||||
|
||||
func NewService() (*FocusService, error) {
|
||||
listener, err := net.Listen("tcp", net.JoinHostPort(Host, fmt.Sprint(Port)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to listen: %w", err)
|
||||
}
|
||||
|
||||
service := &FocusService{
|
||||
server: grpc.NewServer(),
|
||||
listener: listener,
|
||||
raiseCh: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
proto.RegisterFocusServer(service.server, service)
|
||||
|
||||
go func() {
|
||||
if err := service.server.Serve(listener); err != nil {
|
||||
fmt.Printf("failed to serve: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (service *FocusService) Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
service.raiseCh <- struct{}{}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (service *FocusService) GetRaiseCh() <-chan struct{} {
|
||||
return service.raiseCh
|
||||
}
|
||||
|
||||
func (service *FocusService) Close() {
|
||||
service.server.Stop()
|
||||
}
|
||||
@ -22,7 +22,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/users"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/abiosoft/ishell"
|
||||
)
|
||||
|
||||
@ -35,6 +35,7 @@ func (f *frontendCLI) completeUsernames(args []string) (usernames []string) {
|
||||
if len(args) == 1 {
|
||||
arg = args[0]
|
||||
}
|
||||
|
||||
for _, userID := range f.bridge.GetUserIDs() {
|
||||
user, err := f.bridge.GetUserInfo(userID)
|
||||
if err != nil {
|
||||
@ -50,8 +51,7 @@ func (f *frontendCLI) completeUsernames(args []string) (usernames []string) {
|
||||
// noAccountWrapper is a decorator for functions which need any account to be properly functional.
|
||||
func (f *frontendCLI) noAccountWrapper(callback func(*ishell.Context)) func(*ishell.Context) {
|
||||
return func(c *ishell.Context) {
|
||||
users := f.bridge.GetUserIDs()
|
||||
if len(users) == 0 {
|
||||
if len(f.bridge.GetUserIDs()) == 0 {
|
||||
f.Println("No active accounts. Please add account to continue.")
|
||||
} else {
|
||||
callback(c)
|
||||
@ -59,9 +59,9 @@ func (f *frontendCLI) noAccountWrapper(callback func(*ishell.Context)) func(*ish
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) users.UserInfo {
|
||||
func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) bridge.UserInfo {
|
||||
user := f.getUserByIndexOrName("")
|
||||
if user.ID != "" {
|
||||
if user.UserID != "" {
|
||||
return user
|
||||
}
|
||||
|
||||
@ -69,24 +69,24 @@ func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) users.UserInfo {
|
||||
indexRange := fmt.Sprintf("number between 0 and %d", numberOfAccounts-1)
|
||||
if len(c.Args) == 0 {
|
||||
f.Printf("Please choose %s or username.\n", indexRange)
|
||||
return users.UserInfo{}
|
||||
return bridge.UserInfo{}
|
||||
}
|
||||
arg := c.Args[0]
|
||||
user = f.getUserByIndexOrName(arg)
|
||||
if user.ID == "" {
|
||||
if user.UserID == "" {
|
||||
f.Printf("Wrong input '%s'. Choose %s or username.\n", bold(arg), indexRange)
|
||||
return users.UserInfo{}
|
||||
return bridge.UserInfo{}
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (f *frontendCLI) getUserByIndexOrName(arg string) users.UserInfo {
|
||||
func (f *frontendCLI) getUserByIndexOrName(arg string) bridge.UserInfo {
|
||||
userIDs := f.bridge.GetUserIDs()
|
||||
numberOfAccounts := len(userIDs)
|
||||
if numberOfAccounts == 0 {
|
||||
return users.UserInfo{}
|
||||
return bridge.UserInfo{}
|
||||
}
|
||||
res := make([]users.UserInfo, len(userIDs))
|
||||
res := make([]bridge.UserInfo, len(userIDs))
|
||||
for idx, userID := range userIDs {
|
||||
user, err := f.bridge.GetUserInfo(userID)
|
||||
if err != nil {
|
||||
@ -99,7 +99,7 @@ func (f *frontendCLI) getUserByIndexOrName(arg string) users.UserInfo {
|
||||
}
|
||||
if index, err := strconv.Atoi(arg); err == nil {
|
||||
if index < 0 || index >= numberOfAccounts {
|
||||
return users.UserInfo{}
|
||||
return bridge.UserInfo{}
|
||||
}
|
||||
return res[index]
|
||||
}
|
||||
@ -108,5 +108,5 @@ func (f *frontendCLI) getUserByIndexOrName(arg string) users.UserInfo {
|
||||
return user
|
||||
}
|
||||
}
|
||||
return users.UserInfo{}
|
||||
return bridge.UserInfo{}
|
||||
}
|
||||
|
||||
@ -22,8 +22,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/users"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/abiosoft/ishell"
|
||||
)
|
||||
|
||||
@ -40,7 +39,7 @@ func (f *frontendCLI) listAccounts(c *ishell.Context) {
|
||||
connected = "connected"
|
||||
}
|
||||
mode := "split"
|
||||
if user.Mode == users.CombinedMode {
|
||||
if user.AddressMode == bridge.CombinedMode {
|
||||
mode = "combined"
|
||||
}
|
||||
f.Printf(spacing, idx, user.Username, connected, mode)
|
||||
@ -50,7 +49,7 @@ func (f *frontendCLI) listAccounts(c *ishell.Context) {
|
||||
|
||||
func (f *frontendCLI) showAccountInfo(c *ishell.Context) {
|
||||
user := f.askUserByIndexOrName(c)
|
||||
if user.ID == "" {
|
||||
if user.UserID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
@ -59,8 +58,8 @@ func (f *frontendCLI) showAccountInfo(c *ishell.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if user.Mode == users.CombinedMode {
|
||||
f.showAccountAddressInfo(user, user.Addresses[user.Primary])
|
||||
if user.AddressMode == bridge.CombinedMode {
|
||||
f.showAccountAddressInfo(user, user.Addresses[0])
|
||||
} else {
|
||||
for _, address := range user.Addresses {
|
||||
f.showAccountAddressInfo(user, address)
|
||||
@ -68,25 +67,31 @@ func (f *frontendCLI) showAccountInfo(c *ishell.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) showAccountAddressInfo(user users.UserInfo, address string) {
|
||||
func (f *frontendCLI) showAccountAddressInfo(user bridge.UserInfo, address string) {
|
||||
imapSecurity := "STARTTLS"
|
||||
if f.bridge.GetIMAPSSL() {
|
||||
imapSecurity = "SSL"
|
||||
}
|
||||
|
||||
smtpSecurity := "STARTTLS"
|
||||
if f.bridge.GetBool(settings.SMTPSSLKey) {
|
||||
if f.bridge.GetSMTPSSL() {
|
||||
smtpSecurity = "SSL"
|
||||
}
|
||||
|
||||
f.Println(bold("Configuration for " + address))
|
||||
f.Printf("IMAP Settings\nAddress: %s\nIMAP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n",
|
||||
bridge.Host,
|
||||
f.bridge.GetInt(settings.IMAPPortKey),
|
||||
constants.Host,
|
||||
f.bridge.GetIMAPPort(),
|
||||
address,
|
||||
user.Password,
|
||||
"STARTTLS",
|
||||
user.BridgePass,
|
||||
imapSecurity,
|
||||
)
|
||||
f.Println("")
|
||||
f.Printf("SMTP Settings\nAddress: %s\nSMTP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n",
|
||||
bridge.Host,
|
||||
f.bridge.GetInt(settings.SMTPPortKey),
|
||||
constants.Host,
|
||||
f.bridge.GetSMTPPort(),
|
||||
address,
|
||||
user.Password,
|
||||
user.BridgePass,
|
||||
smtpSecurity,
|
||||
)
|
||||
f.Println("")
|
||||
@ -99,8 +104,8 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { //nolint:funlen
|
||||
loginName := ""
|
||||
if len(c.Args) > 0 {
|
||||
user := f.getUserByIndexOrName(c.Args[0])
|
||||
if user.ID != "" {
|
||||
loginName = user.Addresses[user.Primary]
|
||||
if user.UserID != "" {
|
||||
loginName = user.Addresses[0]
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,41 +124,23 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { //nolint:funlen
|
||||
}
|
||||
|
||||
f.Println("Authenticating ... ")
|
||||
client, auth, err := f.bridge.Login(loginName, []byte(password))
|
||||
|
||||
userID, err := f.bridge.LoginUser(
|
||||
context.Background(),
|
||||
loginName,
|
||||
password,
|
||||
func() (string, error) {
|
||||
return f.readStringInAttempts("Two factor code", c.ReadLine, isNotEmpty), nil
|
||||
},
|
||||
func() ([]byte, error) {
|
||||
return []byte(f.readStringInAttempts("Mailbox password", c.ReadPassword, isNotEmpty)), nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
f.processAPIError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if auth.HasTwoFactor() {
|
||||
twoFactor := f.readStringInAttempts("Two factor code", c.ReadLine, isNotEmpty)
|
||||
if twoFactor == "" {
|
||||
return
|
||||
}
|
||||
|
||||
err = client.Auth2FA(context.Background(), twoFactor)
|
||||
if err != nil {
|
||||
f.processAPIError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
mailboxPassword := password
|
||||
if auth.HasMailboxPassword() {
|
||||
mailboxPassword = f.readStringInAttempts("Mailbox password", c.ReadPassword, isNotEmpty)
|
||||
}
|
||||
if mailboxPassword == "" {
|
||||
return
|
||||
}
|
||||
|
||||
f.Println("Adding account ...")
|
||||
userID, err := f.bridge.FinishLogin(client, auth, []byte(mailboxPassword))
|
||||
if err != nil {
|
||||
log.WithField("username", loginName).WithError(err).Error("Login was unsuccessful")
|
||||
f.Println("Adding account was unsuccessful:", err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := f.bridge.GetUserInfo(userID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -167,11 +154,12 @@ func (f *frontendCLI) logoutAccount(c *ishell.Context) {
|
||||
defer f.ShowPrompt(true)
|
||||
|
||||
user := f.askUserByIndexOrName(c)
|
||||
if user.ID == "" {
|
||||
if user.UserID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if f.yesNoQuestion("Are you sure you want to logout account " + bold(user.Username)) {
|
||||
if err := f.bridge.LogoutUser(user.ID); err != nil {
|
||||
if err := f.bridge.LogoutUser(context.Background(), user.UserID); err != nil {
|
||||
f.printAndLogError("Logging out failed: ", err)
|
||||
}
|
||||
}
|
||||
@ -182,12 +170,12 @@ func (f *frontendCLI) deleteAccount(c *ishell.Context) {
|
||||
defer f.ShowPrompt(true)
|
||||
|
||||
user := f.askUserByIndexOrName(c)
|
||||
if user.ID == "" {
|
||||
if user.UserID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if f.yesNoQuestion("Are you sure you want to " + bold("remove account "+user.Username)) {
|
||||
clearCache := f.yesNoQuestion("Do you want to remove cache for this account")
|
||||
if err := f.bridge.DeleteUser(user.ID, clearCache); err != nil {
|
||||
if err := f.bridge.DeleteUser(context.Background(), user.UserID); err != nil {
|
||||
f.printAndLogError("Cannot delete account: ", err)
|
||||
return
|
||||
}
|
||||
@ -205,10 +193,13 @@ func (f *frontendCLI) deleteAccounts(c *ishell.Context) {
|
||||
for _, userID := range f.bridge.GetUserIDs() {
|
||||
user, err := f.bridge.GetUserInfo(userID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
f.printAndLogError("Cannot get user info: ", err)
|
||||
return
|
||||
}
|
||||
if err := f.bridge.DeleteUser(user.ID, false); err != nil {
|
||||
|
||||
if err := f.bridge.DeleteUser(context.Background(), user.UserID); err != nil {
|
||||
f.printAndLogError("Cannot delete account ", user.Username, ": ", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,37 +214,50 @@ func (f *frontendCLI) deleteEverything(c *ishell.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
f.bridge.FactoryReset()
|
||||
f.bridge.FactoryReset(context.Background())
|
||||
|
||||
c.Println("Everything cleared")
|
||||
|
||||
// Clearing data removes everything (db, preferences, ...) so everything has to be stopped and started again.
|
||||
f.restarter.SetToRestart()
|
||||
|
||||
f.Stop()
|
||||
}
|
||||
|
||||
func (f *frontendCLI) changeMode(c *ishell.Context) {
|
||||
user := f.askUserByIndexOrName(c)
|
||||
if user.ID == "" {
|
||||
if user.UserID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var targetMode users.AddressMode
|
||||
var targetMode bridge.AddressMode
|
||||
|
||||
if user.Mode == users.CombinedMode {
|
||||
targetMode = users.SplitMode
|
||||
if user.AddressMode == bridge.CombinedMode {
|
||||
targetMode = bridge.SplitMode
|
||||
} else {
|
||||
targetMode = users.CombinedMode
|
||||
targetMode = bridge.CombinedMode
|
||||
}
|
||||
|
||||
if !f.yesNoQuestion("Are you sure you want to change the mode for account " + bold(user.Username) + " to " + bold(targetMode)) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := f.bridge.SetAddressMode(user.ID, targetMode); err != nil {
|
||||
if err := f.bridge.SetAddressMode(user.UserID, targetMode); err != nil {
|
||||
f.printAndLogError("Cannot switch address mode:", err)
|
||||
}
|
||||
|
||||
f.Printf("Address mode for account %s changed to %s\n", user.Username, targetMode)
|
||||
}
|
||||
|
||||
func (f *frontendCLI) configureAppleMail(c *ishell.Context) {
|
||||
user := f.askUserByIndexOrName(c)
|
||||
if user.UserID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if !f.yesNoQuestion("Are you sure you want to configure Apple Mail for " + bold(user.Username) + " with address " + bold(user.Addresses[0])) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := f.bridge.ConfigureAppleMail(user.UserID, user.Addresses[0]); err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
|
||||
f.Printf("Apple Mail configured for %v with address %v\n", user.Username, user.Addresses[0])
|
||||
}
|
||||
|
||||
@ -19,11 +19,13 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||
"gitlab.protontech.ch/go/liteapi"
|
||||
|
||||
"github.com/abiosoft/ishell"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -34,30 +36,14 @@ var log = logrus.WithField("pkg", "frontend/cli") //nolint:gochecknoglobals
|
||||
type frontendCLI struct {
|
||||
*ishell.Shell
|
||||
|
||||
eventListener listener.Listener
|
||||
updater types.Updater
|
||||
bridge types.Bridger
|
||||
|
||||
restarter types.Restarter
|
||||
bridge *bridge.Bridge
|
||||
}
|
||||
|
||||
// New returns a new CLI frontend configured with the given options.
|
||||
func New( //nolint:funlen
|
||||
panicHandler types.PanicHandler,
|
||||
|
||||
eventListener listener.Listener,
|
||||
updater types.Updater,
|
||||
bridge types.Bridger,
|
||||
restarter types.Restarter,
|
||||
) *frontendCLI { //nolint:revive
|
||||
func New(bridge *bridge.Bridge) *frontendCLI {
|
||||
fe := &frontendCLI{
|
||||
Shell: ishell.New(),
|
||||
|
||||
eventListener: eventListener,
|
||||
updater: updater,
|
||||
bridge: bridge,
|
||||
|
||||
restarter: restarter,
|
||||
Shell: ishell.New(),
|
||||
bridge: bridge,
|
||||
}
|
||||
|
||||
// Clear commands.
|
||||
@ -66,12 +52,6 @@ func New( //nolint:funlen
|
||||
Help: "remove stored accounts and preferences. (alias: cl)",
|
||||
Aliases: []string{"cl"},
|
||||
}
|
||||
clearCmd.AddCmd(&ishell.Cmd{
|
||||
Name: "cache",
|
||||
Help: "remove stored preferences for accounts (aliases: c, prefs, preferences)",
|
||||
Aliases: []string{"c", "prefs", "preferences"},
|
||||
Func: fe.deleteCache,
|
||||
})
|
||||
clearCmd.AddCmd(&ishell.Cmd{
|
||||
Name: "accounts",
|
||||
Help: "remove all accounts from keychain. (aliases: a, k, keychain)",
|
||||
@ -100,15 +80,30 @@ func New( //nolint:funlen
|
||||
Completer: fe.completeUsernames,
|
||||
})
|
||||
changeCmd.AddCmd(&ishell.Cmd{
|
||||
Name: "port",
|
||||
Help: "change port numbers of IMAP and SMTP servers. (alias: p)",
|
||||
Aliases: []string{"p"},
|
||||
Func: fe.changePort,
|
||||
Name: "change-location",
|
||||
Help: "change the location of the encrypted message cache",
|
||||
Func: fe.setGluonLocation,
|
||||
})
|
||||
changeCmd.AddCmd(&ishell.Cmd{
|
||||
Name: "imap-port",
|
||||
Help: "change port number of IMAP server.",
|
||||
Func: fe.changeIMAPPort,
|
||||
})
|
||||
changeCmd.AddCmd(&ishell.Cmd{
|
||||
Name: "smtp-port",
|
||||
Help: "change port number of SMTP server.",
|
||||
Func: fe.changeSMTPPort,
|
||||
})
|
||||
changeCmd.AddCmd(&ishell.Cmd{
|
||||
Name: "imap-security",
|
||||
Help: "change IMAP SSL settings servers.(alias: ssl-imap, starttls-imap)",
|
||||
Aliases: []string{"ssl-imap", "starttls-imap"},
|
||||
Func: fe.changeIMAPSecurity,
|
||||
})
|
||||
changeCmd.AddCmd(&ishell.Cmd{
|
||||
Name: "smtp-security",
|
||||
Help: "change port numbers of IMAP and SMTP servers.(alias: ssl, starttls)",
|
||||
Aliases: []string{"ssl", "starttls"},
|
||||
Help: "change SMTP SSL settings servers.(alias: ssl-smtp, starttls-smtp)",
|
||||
Aliases: []string{"ssl-smtp", "starttls-smtp"},
|
||||
Func: fe.changeSMTPSecurity,
|
||||
})
|
||||
fe.AddCmd(changeCmd)
|
||||
@ -130,6 +125,22 @@ func New( //nolint:funlen
|
||||
})
|
||||
fe.AddCmd(dohCmd)
|
||||
|
||||
// Apple Mail commands.
|
||||
configureCmd := &ishell.Cmd{
|
||||
Name: "configure-apple-mail",
|
||||
Help: "Configures Apple Mail to use ProtonMail Bridge",
|
||||
Func: fe.configureAppleMail,
|
||||
}
|
||||
fe.AddCmd(configureCmd)
|
||||
|
||||
// TLS commands.
|
||||
exportTLSCmd := &ishell.Cmd{
|
||||
Name: "export-tls",
|
||||
Help: "Export the TLS certificate used by the Bridge",
|
||||
Func: fe.exportTLSCerts,
|
||||
}
|
||||
fe.AddCmd(exportTLSCmd)
|
||||
|
||||
// All mail visibility commands.
|
||||
allMailCmd := &ishell.Cmd{
|
||||
Name: "all-mail-visibility",
|
||||
@ -147,28 +158,6 @@ func New( //nolint:funlen
|
||||
})
|
||||
fe.AddCmd(allMailCmd)
|
||||
|
||||
// Cache-On-Disk commands.
|
||||
codCmd := &ishell.Cmd{
|
||||
Name: "local-cache",
|
||||
Help: "manage the local encrypted message cache",
|
||||
}
|
||||
codCmd.AddCmd(&ishell.Cmd{
|
||||
Name: "enable",
|
||||
Help: "enable the local cache",
|
||||
Func: fe.enableCacheOnDisk,
|
||||
})
|
||||
codCmd.AddCmd(&ishell.Cmd{
|
||||
Name: "disable",
|
||||
Help: "disable the local cache",
|
||||
Func: fe.disableCacheOnDisk,
|
||||
})
|
||||
codCmd.AddCmd(&ishell.Cmd{
|
||||
Name: "change-location",
|
||||
Help: "change the location of the local cache",
|
||||
Func: fe.setCacheOnDiskLocation,
|
||||
})
|
||||
fe.AddCmd(codCmd)
|
||||
|
||||
// Updates commands.
|
||||
updatesCmd := &ishell.Cmd{
|
||||
Name: "updates",
|
||||
@ -224,7 +213,6 @@ func New( //nolint:funlen
|
||||
Aliases: []string{"man"},
|
||||
Func: fe.printManual,
|
||||
})
|
||||
|
||||
fe.AddCmd(&ishell.Cmd{
|
||||
Name: "credits",
|
||||
Help: "print used resources.",
|
||||
@ -267,55 +255,122 @@ func New( //nolint:funlen
|
||||
Completer: fe.completeUsernames,
|
||||
})
|
||||
|
||||
// System commands.
|
||||
fe.AddCmd(&ishell.Cmd{
|
||||
Name: "restart",
|
||||
Help: "restart the bridge.",
|
||||
Func: fe.restart,
|
||||
})
|
||||
go fe.watchEvents()
|
||||
|
||||
go func() {
|
||||
defer panicHandler.HandlePanic()
|
||||
fe.watchEvents()
|
||||
}()
|
||||
return fe
|
||||
}
|
||||
|
||||
func (f *frontendCLI) watchEvents() {
|
||||
errorCh := f.eventListener.ProvideChannel(events.ErrorEvent)
|
||||
credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
|
||||
internetConnChangedCh := f.eventListener.ProvideChannel(events.InternetConnChangedEvent)
|
||||
addressChangedCh := f.eventListener.ProvideChannel(events.AddressChangedEvent)
|
||||
addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
|
||||
logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
|
||||
certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue)
|
||||
for {
|
||||
select {
|
||||
case errorDetails := <-errorCh:
|
||||
f.Println("Bridge failed:", errorDetails)
|
||||
case <-credentialsErrorCh:
|
||||
eventCh, done := f.bridge.GetEvents()
|
||||
defer done()
|
||||
|
||||
// TODO: Better error events.
|
||||
for _, err := range f.bridge.GetErrors() {
|
||||
switch {
|
||||
case errors.Is(err, vault.ErrCorrupt):
|
||||
f.notifyCredentialsError()
|
||||
case stat := <-internetConnChangedCh:
|
||||
if stat == events.InternetOff {
|
||||
|
||||
case errors.Is(err, vault.ErrInsecure):
|
||||
f.notifyCredentialsError()
|
||||
|
||||
case errors.Is(err, bridge.ErrServeIMAP):
|
||||
f.Println("IMAP server error:", err)
|
||||
|
||||
case errors.Is(err, bridge.ErrServeSMTP):
|
||||
f.Println("SMTP server error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
for event := range eventCh {
|
||||
switch event := event.(type) {
|
||||
case events.ConnStatus:
|
||||
switch event.Status {
|
||||
case liteapi.StatusUp:
|
||||
f.notifyInternetOn()
|
||||
|
||||
case liteapi.StatusDown:
|
||||
f.notifyInternetOff()
|
||||
}
|
||||
if stat == events.InternetOn {
|
||||
f.notifyInternetOn()
|
||||
}
|
||||
case address := <-addressChangedCh:
|
||||
f.Printf("Address changed for %s. You may need to reconfigure your email client.", address)
|
||||
case address := <-addressChangedLogoutCh:
|
||||
f.notifyLogout(address)
|
||||
case userID := <-logoutCh:
|
||||
user, err := f.bridge.GetUserInfo(userID)
|
||||
|
||||
case events.UserDeauth:
|
||||
user, err := f.bridge.GetUserInfo(event.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.notifyLogout(user.Username)
|
||||
case <-certIssue:
|
||||
|
||||
case events.UserAddressChanged:
|
||||
user, err := f.bridge.GetUserInfo(event.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.Printf("Address changed for %s. You may need to reconfigure your email client.\n", user.Username)
|
||||
|
||||
case events.UserAddressDeleted:
|
||||
f.notifyLogout(event.Address)
|
||||
|
||||
case events.SyncStarted:
|
||||
user, err := f.bridge.GetUserInfo(event.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.Printf("A sync has begun for %s.\n", user.Username)
|
||||
|
||||
case events.SyncFinished:
|
||||
user, err := f.bridge.GetUserInfo(event.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.Printf("A sync has finished for %s.\n", user.Username)
|
||||
|
||||
case events.SyncProgress:
|
||||
user, err := f.bridge.GetUserInfo(event.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.Printf(
|
||||
"Sync (%v): %.1f%% (Elapsed: %0.1fs, ETA: %0.1fs)\n",
|
||||
user.Username,
|
||||
100*event.Progress,
|
||||
event.Elapsed.Seconds(),
|
||||
event.Remaining.Seconds(),
|
||||
)
|
||||
|
||||
case events.UpdateAvailable:
|
||||
f.Printf("An update is available (version %v)\n", event.Version.Version)
|
||||
|
||||
case events.UpdateForced:
|
||||
f.notifyNeedUpgrade()
|
||||
|
||||
case events.TLSIssue:
|
||||
f.notifyCertIssue()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
errorCh := f.eventListener.ProvideChannel(events.ErrorEvent)
|
||||
credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
|
||||
for {
|
||||
select {
|
||||
case errorDetails := <-errorCh:
|
||||
f.Println("Bridge failed:", errorDetails)
|
||||
case <-credentialsErrorCh:
|
||||
f.notifyCredentialsError()
|
||||
case stat := <-internetConnChangedCh:
|
||||
if stat == events.InternetOff {
|
||||
f.notifyInternetOff()
|
||||
}
|
||||
if stat == events.InternetOn {
|
||||
f.notifyInternetOn()
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// Loop starts the frontend loop with an interactive shell.
|
||||
@ -340,12 +395,3 @@ func (f *frontendCLI) Loop() error {
|
||||
f.Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *frontendCLI) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) {
|
||||
// NOTE: Save the update somewhere so that it can be installed when user chooses "install now".
|
||||
}
|
||||
|
||||
func (f *frontendCLI) WaitUntilFrontendIsReady() {}
|
||||
func (f *frontendCLI) SetVersion(version updater.VersionInfo) {}
|
||||
func (f *frontendCLI) NotifySilentUpdateInstalled() {}
|
||||
func (f *frontendCLI) NotifySilentUpdateError(err error) {}
|
||||
|
||||
@ -18,28 +18,21 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/ports"
|
||||
"github.com/abiosoft/ishell"
|
||||
)
|
||||
|
||||
var currentPort = "" //nolint:gochecknoglobals
|
||||
|
||||
func (f *frontendCLI) restart(c *ishell.Context) {
|
||||
if f.yesNoQuestion("Are you sure you want to restart the Bridge") {
|
||||
f.Println("Restarting Bridge...")
|
||||
f.restarter.SetToRestart()
|
||||
f.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printLogDir(c *ishell.Context) {
|
||||
if path, err := f.bridge.ProvideLogsPath(); err != nil {
|
||||
if path, err := f.bridge.GetLogsPath(); err != nil {
|
||||
f.Println("Failed to determine location of log files")
|
||||
} else {
|
||||
f.Println("Log files are stored in\n\n ", path)
|
||||
@ -50,79 +43,91 @@ func (f *frontendCLI) printManual(c *ishell.Context) {
|
||||
f.Println("More instructions about the Bridge can be found at\n\n https://protonmail.com/bridge")
|
||||
}
|
||||
|
||||
func (f *frontendCLI) deleteCache(c *ishell.Context) {
|
||||
func (f *frontendCLI) printCredits(c *ishell.Context) {
|
||||
for _, pkg := range strings.Split(bridge.Credits, ";") {
|
||||
f.Println(pkg)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) changeIMAPSecurity(c *ishell.Context) {
|
||||
f.ShowPrompt(false)
|
||||
defer f.ShowPrompt(true)
|
||||
|
||||
if !f.yesNoQuestion("Do you really want to remove all stored preferences") {
|
||||
return
|
||||
newSecurity := "SSL"
|
||||
if f.bridge.GetIMAPSSL() {
|
||||
newSecurity = "STARTTLS"
|
||||
}
|
||||
|
||||
if err := f.bridge.ClearData(); err != nil {
|
||||
f.printAndLogError("Cache clear failed: ", err.Error())
|
||||
return
|
||||
msg := fmt.Sprintf("Are you sure you want to change IMAP setting to %q", newSecurity)
|
||||
|
||||
if f.yesNoQuestion(msg) {
|
||||
if err := f.bridge.SetIMAPSSL(!f.bridge.GetIMAPSSL()); err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
f.Println("Cached cleared, restarting bridge")
|
||||
|
||||
// Clearing data removes everything (db, preferences, ...) so everything has to be stopped and started again.
|
||||
f.restarter.SetToRestart()
|
||||
|
||||
f.Stop()
|
||||
}
|
||||
|
||||
func (f *frontendCLI) changeSMTPSecurity(c *ishell.Context) {
|
||||
f.ShowPrompt(false)
|
||||
defer f.ShowPrompt(true)
|
||||
|
||||
isSSL := f.bridge.GetBool(settings.SMTPSSLKey)
|
||||
newSecurity := "SSL"
|
||||
if isSSL {
|
||||
if f.bridge.GetSMTPSSL() {
|
||||
newSecurity = "STARTTLS"
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("Are you sure you want to change SMTP setting to %q and restart the Bridge", newSecurity)
|
||||
msg := fmt.Sprintf("Are you sure you want to change SMTP setting to %q", newSecurity)
|
||||
|
||||
if f.yesNoQuestion(msg) {
|
||||
f.bridge.SetBool(settings.SMTPSSLKey, !isSSL)
|
||||
f.Println("Restarting Bridge...")
|
||||
f.restarter.SetToRestart()
|
||||
f.Stop()
|
||||
if err := f.bridge.SetSMTPSSL(!f.bridge.GetSMTPSSL()); err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) changePort(c *ishell.Context) {
|
||||
func (f *frontendCLI) changeIMAPPort(c *ishell.Context) {
|
||||
f.ShowPrompt(false)
|
||||
defer f.ShowPrompt(true)
|
||||
|
||||
currentPort = f.bridge.Get(settings.IMAPPortKey)
|
||||
newIMAPPort := f.readStringInAttempts("Set IMAP port (current "+currentPort+")", c.ReadLine, f.isPortFree)
|
||||
newIMAPPort := f.readStringInAttempts(fmt.Sprintf("Set IMAP port (current %v)", f.bridge.GetIMAPPort()), c.ReadLine, f.isPortFree)
|
||||
if newIMAPPort == "" {
|
||||
newIMAPPort = currentPort
|
||||
}
|
||||
imapPortChanged := newIMAPPort != currentPort
|
||||
|
||||
currentPort = f.bridge.Get(settings.SMTPPortKey)
|
||||
newSMTPPort := f.readStringInAttempts("Set SMTP port (current "+currentPort+")", c.ReadLine, f.isPortFree)
|
||||
if newSMTPPort == "" {
|
||||
newSMTPPort = currentPort
|
||||
}
|
||||
smtpPortChanged := newSMTPPort != currentPort
|
||||
|
||||
if newIMAPPort == newSMTPPort {
|
||||
f.Println("SMTP and IMAP ports must be different!")
|
||||
f.printAndLogError(errors.New("failed to get new port"))
|
||||
return
|
||||
}
|
||||
|
||||
if imapPortChanged || smtpPortChanged {
|
||||
f.Println("Saving values IMAP:", newIMAPPort, "SMTP:", newSMTPPort)
|
||||
f.bridge.Set(settings.IMAPPortKey, newIMAPPort)
|
||||
f.bridge.Set(settings.SMTPPortKey, newSMTPPort)
|
||||
f.Println("Restarting Bridge...")
|
||||
f.restarter.SetToRestart()
|
||||
f.Stop()
|
||||
} else {
|
||||
f.Println("Nothing changed")
|
||||
newIMAPPortInt, err := strconv.Atoi(newIMAPPort)
|
||||
if err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := f.bridge.SetIMAPPort(newIMAPPortInt); err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) changeSMTPPort(c *ishell.Context) {
|
||||
f.ShowPrompt(false)
|
||||
defer f.ShowPrompt(true)
|
||||
|
||||
newSMTPPort := f.readStringInAttempts(fmt.Sprintf("Set SMTP port (current %v)", f.bridge.GetSMTPPort()), c.ReadLine, f.isPortFree)
|
||||
if newSMTPPort == "" {
|
||||
f.printAndLogError(errors.New("failed to get new port"))
|
||||
return
|
||||
}
|
||||
|
||||
newSMTPPortInt, err := strconv.Atoi(newSMTPPort)
|
||||
if err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := f.bridge.SetSMTPPort(newSMTPPortInt); err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,7 +140,10 @@ func (f *frontendCLI) allowProxy(c *ishell.Context) {
|
||||
f.Println("Bridge is currently set to NOT use alternative routing to connect to Proton if it is being blocked.")
|
||||
|
||||
if f.yesNoQuestion("Are you sure you want to allow bridge to do this") {
|
||||
f.bridge.SetProxyAllowed(true)
|
||||
if err := f.bridge.SetProxyAllowed(true); err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,12 +156,15 @@ func (f *frontendCLI) disallowProxy(c *ishell.Context) {
|
||||
f.Println("Bridge is currently set to use alternative routing to connect to Proton if it is being blocked.")
|
||||
|
||||
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") {
|
||||
f.bridge.SetProxyAllowed(false)
|
||||
if err := f.bridge.SetProxyAllowed(false); err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) hideAllMail(c *ishell.Context) {
|
||||
if !f.bridge.IsAllMailVisible() {
|
||||
if !f.bridge.GetShowAllMail() {
|
||||
f.Println("All Mail folder is not listed in your local client.")
|
||||
return
|
||||
}
|
||||
@ -161,12 +172,15 @@ func (f *frontendCLI) hideAllMail(c *ishell.Context) {
|
||||
f.Println("All Mail folder is listed in your client right now.")
|
||||
|
||||
if f.yesNoQuestion("Do you want to hide All Mail folder") {
|
||||
f.bridge.SetIsAllMailVisible(false)
|
||||
if err := f.bridge.SetShowAllMail(false); err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) showAllMail(c *ishell.Context) {
|
||||
if f.bridge.IsAllMailVisible() {
|
||||
if f.bridge.GetShowAllMail() {
|
||||
f.Println("All Mail folder is listed in your local client.")
|
||||
return
|
||||
}
|
||||
@ -174,68 +188,47 @@ func (f *frontendCLI) showAllMail(c *ishell.Context) {
|
||||
f.Println("All Mail folder is not listed in your client right now.")
|
||||
|
||||
if f.yesNoQuestion("Do you want to show All Mail folder") {
|
||||
f.bridge.SetIsAllMailVisible(true)
|
||||
if err := f.bridge.SetShowAllMail(true); err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) enableCacheOnDisk(c *ishell.Context) {
|
||||
if f.bridge.GetBool(settings.CacheEnabledKey) {
|
||||
f.Println("The local cache is already enabled.")
|
||||
return
|
||||
func (f *frontendCLI) setGluonLocation(c *ishell.Context) {
|
||||
if gluonDir := f.bridge.GetGluonDir(); gluonDir != "" {
|
||||
f.Println("The current message cache location is:", gluonDir)
|
||||
}
|
||||
|
||||
if f.yesNoQuestion("Are you sure you want to enable the local cache") {
|
||||
if err := f.bridge.EnableCache(); err != nil {
|
||||
f.Println("The local cache could not be enabled.")
|
||||
if location := f.readStringInAttempts("Enter a new location for the message cache", c.ReadLine, f.isCacheLocationUsable); location != "" {
|
||||
if err := f.bridge.SetGluonDir(context.Background(), location); err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
|
||||
f.restarter.SetToRestart()
|
||||
f.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) disableCacheOnDisk(c *ishell.Context) {
|
||||
if !f.bridge.GetBool(settings.CacheEnabledKey) {
|
||||
f.Println("The local cache is already disabled.")
|
||||
return
|
||||
}
|
||||
func (f *frontendCLI) exportTLSCerts(c *ishell.Context) {
|
||||
if location := f.readStringInAttempts("Enter a path to which to export the TLS certificate used for IMAP and SMTP", c.ReadLine, f.isCacheLocationUsable); location != "" {
|
||||
cert, key := f.bridge.GetBridgeTLSCert()
|
||||
|
||||
if f.yesNoQuestion("Are you sure you want to disable the local cache") {
|
||||
if err := f.bridge.DisableCache(); err != nil {
|
||||
f.Println("The local cache could not be disabled.")
|
||||
if err := os.WriteFile(filepath.Join(location, "cert.pem"), cert, 0600); err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
|
||||
f.restarter.SetToRestart()
|
||||
f.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) setCacheOnDiskLocation(c *ishell.Context) {
|
||||
if !f.bridge.GetBool(settings.CacheEnabledKey) {
|
||||
f.Println("The local cache must be enabled.")
|
||||
return
|
||||
}
|
||||
|
||||
if location := f.bridge.Get(settings.CacheLocationKey); location != "" {
|
||||
f.Println("The current local cache location is:", location)
|
||||
}
|
||||
|
||||
if location := f.readStringInAttempts("Enter a new location for the cache", c.ReadLine, f.isCacheLocationUsable); location != "" {
|
||||
if err := f.bridge.MigrateCache(f.bridge.Get(settings.CacheLocationKey), location); err != nil {
|
||||
f.Println("The local cache location could not be changed.")
|
||||
if err := os.WriteFile(filepath.Join(location, "key.pem"), key, 0600); err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
|
||||
f.restarter.SetToRestart()
|
||||
f.Stop()
|
||||
f.Println("TLS certificate exported to", location)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) isPortFree(port string) bool {
|
||||
port = strings.ReplaceAll(port, ":", "")
|
||||
if port == "" || port == currentPort {
|
||||
if port == "" {
|
||||
return true
|
||||
}
|
||||
number, err := strconv.Atoi(port)
|
||||
|
||||
@ -18,36 +18,16 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/abiosoft/ishell"
|
||||
)
|
||||
|
||||
func (f *frontendCLI) checkUpdates(c *ishell.Context) {
|
||||
version, err := f.updater.Check()
|
||||
if err != nil {
|
||||
f.Println("An error occurred while checking for updates.")
|
||||
return
|
||||
}
|
||||
|
||||
if f.updater.IsUpdateApplicable(version) {
|
||||
f.Println("An update is available.")
|
||||
} else {
|
||||
f.Println("Your version is up to date.")
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printCredits(c *ishell.Context) {
|
||||
for _, pkg := range strings.Split(bridge.Credits, ";") {
|
||||
f.Println(pkg)
|
||||
}
|
||||
f.bridge.CheckForUpdates()
|
||||
}
|
||||
|
||||
func (f *frontendCLI) enableAutoUpdates(c *ishell.Context) {
|
||||
if f.bridge.GetBool(settings.AutoUpdateKey) {
|
||||
if f.bridge.GetAutoUpdate() {
|
||||
f.Println("Bridge is already set to automatically install updates.")
|
||||
return
|
||||
}
|
||||
@ -55,12 +35,15 @@ func (f *frontendCLI) enableAutoUpdates(c *ishell.Context) {
|
||||
f.Println("Bridge is currently set to NOT automatically install updates.")
|
||||
|
||||
if f.yesNoQuestion("Are you sure you want to allow bridge to do this") {
|
||||
f.bridge.SetBool(settings.AutoUpdateKey, true)
|
||||
if err := f.bridge.SetAutoUpdate(true); err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) disableAutoUpdates(c *ishell.Context) {
|
||||
if !f.bridge.GetBool(settings.AutoUpdateKey) {
|
||||
if !f.bridge.GetAutoUpdate() {
|
||||
f.Println("Bridge is already set to NOT automatically install updates.")
|
||||
return
|
||||
}
|
||||
@ -68,7 +51,10 @@ func (f *frontendCLI) disableAutoUpdates(c *ishell.Context) {
|
||||
f.Println("Bridge is currently set to automatically install updates.")
|
||||
|
||||
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") {
|
||||
f.bridge.SetBool(settings.AutoUpdateKey, false)
|
||||
if err := f.bridge.SetAutoUpdate(false); err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,7 +67,10 @@ func (f *frontendCLI) selectEarlyChannel(c *ishell.Context) {
|
||||
f.Println("Bridge is currently on the stable update channel.")
|
||||
|
||||
if f.yesNoQuestion("Are you sure you want to switch to the early-access update channel") {
|
||||
f.bridge.SetUpdateChannel(updater.EarlyChannel)
|
||||
if err := f.bridge.SetUpdateChannel(updater.EarlyChannel); err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,6 +84,9 @@ func (f *frontendCLI) selectStableChannel(c *ishell.Context) {
|
||||
f.Println("Switching to the stable channel may reset all data!")
|
||||
|
||||
if f.yesNoQuestion("Are you sure you want to switch to the stable update channel") {
|
||||
f.bridge.SetUpdateChannel(updater.StableChannel)
|
||||
if err := f.bridge.SetUpdateChannel(updater.StableChannel); err != nil {
|
||||
f.printAndLogError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,6 @@ package cli
|
||||
import (
|
||||
"strings"
|
||||
|
||||
pmapi "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
@ -67,15 +66,7 @@ func (f *frontendCLI) printAndLogError(args ...interface{}) {
|
||||
}
|
||||
|
||||
func (f *frontendCLI) processAPIError(err error) {
|
||||
log.Warn("API error: ", err)
|
||||
switch err {
|
||||
case pmapi.ErrNoConnection:
|
||||
f.notifyInternetOff()
|
||||
case pmapi.ErrUpgradeApplication:
|
||||
f.notifyNeedUpgrade()
|
||||
default:
|
||||
f.Println("Server error:", err.Error())
|
||||
}
|
||||
f.printAndLogError(err)
|
||||
}
|
||||
|
||||
func (f *frontendCLI) notifyInternetOff() {
|
||||
@ -91,12 +82,7 @@ func (f *frontendCLI) notifyLogout(address string) {
|
||||
}
|
||||
|
||||
func (f *frontendCLI) notifyNeedUpgrade() {
|
||||
version, err := f.updater.Check()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to notify need upgrade")
|
||||
return
|
||||
}
|
||||
f.Println("Please download and install the newest version of application from", version.LandingPage)
|
||||
f.Println("Please download and install the newest version of the application.")
|
||||
}
|
||||
|
||||
func (f *frontendCLI) notifyCredentialsError() {
|
||||
|
||||
@ -1,75 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package frontend provides all interfaces of the Bridge.
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/frontend/cli"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/frontend/grpc"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||
)
|
||||
|
||||
type Frontend interface {
|
||||
Loop() error
|
||||
NotifyManualUpdate(update updater.VersionInfo, canInstall bool)
|
||||
SetVersion(update updater.VersionInfo)
|
||||
NotifySilentUpdateInstalled()
|
||||
NotifySilentUpdateError(error)
|
||||
WaitUntilFrontendIsReady()
|
||||
}
|
||||
|
||||
// New returns initialized frontend based on `frontendType`, which can be `cli` or `grpc`.
|
||||
func New(
|
||||
frontendType string,
|
||||
showWindowOnStart bool,
|
||||
panicHandler types.PanicHandler,
|
||||
eventListener listener.Listener,
|
||||
updater types.Updater,
|
||||
bridge *bridge.Bridge,
|
||||
restarter types.Restarter,
|
||||
locations *locations.Locations,
|
||||
) Frontend {
|
||||
switch frontendType {
|
||||
case "grpc":
|
||||
return grpc.NewService(
|
||||
showWindowOnStart,
|
||||
panicHandler,
|
||||
eventListener,
|
||||
updater,
|
||||
bridge,
|
||||
restarter,
|
||||
locations,
|
||||
)
|
||||
|
||||
case "cli":
|
||||
return cli.New(
|
||||
panicHandler,
|
||||
eventListener,
|
||||
updater,
|
||||
bridge,
|
||||
restarter,
|
||||
)
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user