mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a53bc4b027 | |||
| 478345e277 | |||
| 0ed78f1ccb | |||
| 6671dd38ea | |||
| 2d5ea669a5 | |||
| c7eb7234a2 | |||
| 73d1fe2f65 | |||
| cf75ea739f | |||
| c920c53243 | |||
| 63379001e3 | |||
| aa8cc3fc4b | |||
| 61e4ca5814 | |||
| 8e0693ab03 | |||
| a3d2df9d38 | |||
| f9f4ce996d | |||
| fc69b9aabb |
@ -48,7 +48,7 @@ lint:
|
||||
tags:
|
||||
- medium
|
||||
|
||||
test:
|
||||
test-linux:
|
||||
stage: test
|
||||
only:
|
||||
- branches
|
||||
@ -65,6 +65,14 @@ test:
|
||||
tags:
|
||||
- medium
|
||||
|
||||
test-windows:
|
||||
extends: .build-windows-base
|
||||
stage: test
|
||||
only:
|
||||
- branches
|
||||
script:
|
||||
- make test
|
||||
|
||||
test-integration:
|
||||
stage: test
|
||||
only:
|
||||
@ -96,6 +104,7 @@ build-qml:
|
||||
- cd internal/frontend/qml
|
||||
- tar -cvzf ../../../bridge_qml.tgz ./*
|
||||
|
||||
|
||||
.build-base:
|
||||
stage: build
|
||||
only:
|
||||
@ -134,6 +143,7 @@ build-linux-qa:
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
|
||||
|
||||
.build-darwin-base:
|
||||
extends: .build-base
|
||||
before_script:
|
||||
@ -170,6 +180,40 @@ build-darwin-qa:
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
|
||||
|
||||
.build-windows-base:
|
||||
extends: .build-base
|
||||
before_script:
|
||||
- export GOROOT=/c/Go
|
||||
- export PATH=$GOROOT/bin:$PATH
|
||||
- export GOARCH=amd64
|
||||
- export GOPATH=~/go
|
||||
- export GO111MODULE=on
|
||||
- export PATH=$GOPATH/bin:$PATH
|
||||
- export MSYSTEM=
|
||||
- export PATH=$PATH:/c/grrrQt/5.13.2/mingw73_64/bin
|
||||
tags:
|
||||
- windows-bridge
|
||||
|
||||
build-windows:
|
||||
extends: .build-windows-base
|
||||
artifacts:
|
||||
name: "bridge-windows-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
|
||||
build-windows-qa:
|
||||
extends: .build-windows-base
|
||||
only:
|
||||
- web
|
||||
- branches
|
||||
script:
|
||||
- BUILD_TAGS="build_qa" make build
|
||||
artifacts:
|
||||
name: "bridge-windows-qa-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
|
||||
# Stage: MIRROR
|
||||
|
||||
mirror-repo:
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
# Building ProtonMail Bridge and Import-Export app
|
||||
|
||||
## Prerequisites
|
||||
* 64-bit OS (the go-rfc5322 module cannot currently be compiled for 32-bit OSes)
|
||||
* 64-bit AMD OS:
|
||||
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
|
||||
- the Apple M1 builds are not supported yet due to dependencies
|
||||
* Go 1.13
|
||||
* Bash with basic build utils: make, gcc, sed, find, grep, ...
|
||||
* For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
|
||||
|
||||
29
Changelog.md
29
Changelog.md
@ -2,6 +2,35 @@
|
||||
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
## [Bridge 2.1.2] London
|
||||
|
||||
## Added
|
||||
* GODT-1522: Rebuild macOS keychain notification.
|
||||
* GODT-1437 Add new proxy provider (Quad9 with port).
|
||||
* GODT-1516: Return notification on missing keychain.
|
||||
|
||||
## Changed
|
||||
* GODT-1451: Do not check for gnome keyring to allow other implementations of secret-service API. Thanks to @remgodow.
|
||||
* GODT-1516 GODT-1451: KeepassXC is crashing on start. We need to block it until it's fixed.
|
||||
|
||||
## Fixed
|
||||
* GODT-1524: Logout issues with macOS.
|
||||
* GODT-1503 GODT-1492: Improve email validation and username in bug report.
|
||||
* GODT-1507: Enable autostart after Qt setup.
|
||||
* GODT-1515: Do not crash when bridge users got disconnected.
|
||||
|
||||
|
||||
## [Bridge 2.1.1] London
|
||||
|
||||
## Added
|
||||
* GODT-1376: Add first userID to sentry scope.
|
||||
* GODT-1375: Add host architecture to sentry reports.
|
||||
* GODT-1364: Add windows CI machine for tests, and build.
|
||||
|
||||
### Fixed
|
||||
* GODT-1499: Remove message from DB once it is not on server any more.
|
||||
|
||||
|
||||
## [Bridge 2.1.0] London
|
||||
|
||||
### Fixed
|
||||
|
||||
2
Makefile
2
Makefile
@ -10,7 +10,7 @@ TARGET_OS?=${GOOS}
|
||||
.PHONY: build build-nogui build-launcher versioner hasher
|
||||
|
||||
# Keep version hardcoded so app build works also without Git repository.
|
||||
BRIDGE_APP_VERSION?=2.1.0+git
|
||||
BRIDGE_APP_VERSION?=2.1.2+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
SRC_ICO:=logo.ico
|
||||
SRC_ICNS:=Bridge.icns
|
||||
|
||||
@ -53,10 +53,12 @@ the user for a password.
|
||||
|
||||
## Keychain
|
||||
You need to have a keychain in order to run the ProtonMail Bridge. On Mac or
|
||||
Windows, Bridge uses native credential managers. On Linux, use
|
||||
[Gnome keyring](https://wiki.gnome.org/Projects/GnomeKeyring/)
|
||||
Windows, Bridge uses native credential managers. On Linux, use `secret-service` freedesktop.org API
|
||||
(e.g. [Gnome keyring](https://wiki.gnome.org/Projects/GnomeKeyring/))
|
||||
or
|
||||
[pass](https://www.passwordstore.org/).
|
||||
[pass](https://www.passwordstore.org/). We are working on allowing other secret
|
||||
services (e.g. KeepassXC), but for now only gnome-keyring is usable without
|
||||
major problems.
|
||||
|
||||
|
||||
## Environment Variables
|
||||
|
||||
10
go.mod
10
go.mod
@ -28,6 +28,8 @@ require (
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
||||
github.com/cucumber/godog v0.12.1
|
||||
github.com/cucumber/messages-go/v16 v16.0.1
|
||||
github.com/elastic/go-sysinfo v1.7.1
|
||||
github.com/elastic/go-windows v1.0.1 // indirect
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a
|
||||
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
|
||||
github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c
|
||||
@ -46,13 +48,16 @@ require (
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7
|
||||
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d
|
||||
github.com/keybase/go-keychain v0.0.0-20211119201326-e02f34051621
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/miekg/dns v1.1.41
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
|
||||
github.com/olekukonko/tablewriter v0.0.4 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285
|
||||
github.com/sirupsen/logrus v1.7.0
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
@ -64,8 +69,9 @@ require (
|
||||
github.com/vmihailenco/msgpack/v5 v5.1.3
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
golang.org/x/net v0.0.0-20211008194852-3b03d305991f
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
|
||||
golang.org/x/sys v0.0.0-20220111092808-5a964db01320
|
||||
golang.org/x/text v0.3.7
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
|
||||
23
go.sum
23
go.sum
@ -113,6 +113,11 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn
|
||||
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/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||
github.com/elastic/go-sysinfo v1.7.1 h1:Wx4DSARcKLllpKT2TnFVdSUJOsybqMYCNQZq1/wO+s0=
|
||||
github.com/elastic/go-sysinfo v1.7.1/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0=
|
||||
github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
|
||||
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-20190308131241-25671c986a6a h1:bMdSPm6sssuOFpIaveu3XGAijMS3Tq2S3EqFZmZxidc=
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
|
||||
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0=
|
||||
@ -179,6 +184,7 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
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.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
@ -241,6 +247,9 @@ github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0Gqw
|
||||
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc=
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
|
||||
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.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@ -256,6 +265,8 @@ github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7
|
||||
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
|
||||
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d h1:gVjhBCfVGl32RIBooOANzfw+0UqX8HU+yPlMv8vypcg=
|
||||
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d/go.mod h1:W6EbaYmb4RldPn0N3gvVHjY1wmU59kbymhW9NATWhwY=
|
||||
github.com/keybase/go-keychain v0.0.0-20211119201326-e02f34051621 h1:aMQ7pA4f06yOVXSulygyGvy4xA94fyzjUGs0iqQdMOI=
|
||||
github.com/keybase/go-keychain v0.0.0-20211119201326-e02f34051621/go.mod h1:enrU/ug069Om7vWxuFE6nikLI2BZNwevMiGSo43Kt5w=
|
||||
github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
@ -347,7 +358,10 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:
|
||||
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=
|
||||
github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
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=
|
||||
@ -509,6 +523,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
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-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -532,11 +547,13 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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-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-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -547,6 +564,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY=
|
||||
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -622,6 +641,7 @@ gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
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=
|
||||
@ -635,4 +655,7 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
|
||||
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.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
|
||||
@ -189,6 +189,8 @@ func New( // nolint[funlen]
|
||||
|
||||
cm := pmapi.New(cfg)
|
||||
|
||||
sentryReporter.SetClientFromManager(cm)
|
||||
|
||||
cm.AddConnectionObserver(pmapi.NewConnectionObserver(
|
||||
func() { listener.Emit(events.InternetOffEvent, "") },
|
||||
func() { listener.Emit(events.InternetOnEvent, "") },
|
||||
|
||||
@ -108,10 +108,6 @@ func New(
|
||||
if err := b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(constants.Version))); err != nil {
|
||||
logrus.WithError(err).Error("Failed to send metric")
|
||||
}
|
||||
|
||||
if err := b.EnableAutostart(); err != nil {
|
||||
log.WithError(err).Error("Failed to enable autostart")
|
||||
}
|
||||
setting.SetBool(settings.FirstStartKey, false)
|
||||
}
|
||||
|
||||
|
||||
@ -39,6 +39,12 @@ var ErrSizeTooLarge = errors.New("file is too big")
|
||||
|
||||
// ReportBug reports a new bug from the user.
|
||||
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string, attachLogs bool) error {
|
||||
if user, err := b.GetUser(address); err == nil {
|
||||
accountName = user.Username()
|
||||
} else if users := b.GetUsers(); len(users) > 0 {
|
||||
accountName = users[0].Username()
|
||||
}
|
||||
|
||||
report := pmapi.ReportBugReq{
|
||||
OS: osType,
|
||||
OSVersion: osVersion,
|
||||
|
||||
@ -25,81 +25,118 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testPrefFilePath = "/tmp/pref.json"
|
||||
|
||||
func TestLoadNoKeyValueStore(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
require.Equal(t, "", pref.Get("key"))
|
||||
r := require.New(t)
|
||||
pref, clean := newTestEmptyKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
r.Equal("", pref.Get("key"))
|
||||
}
|
||||
|
||||
func TestLoadBadKeyValueStore(t *testing.T) {
|
||||
require.NoError(t, ioutil.WriteFile(testPrefFilePath, []byte("{\"key\":\"value"), 0700))
|
||||
pref := newKeyValueStore(testPrefFilePath)
|
||||
require.Equal(t, "", pref.Get("key"))
|
||||
r := require.New(t)
|
||||
path, clean := newTmpFile(r)
|
||||
defer clean()
|
||||
|
||||
r.NoError(ioutil.WriteFile(path, []byte("{\"key\":\"MISSING_QUOTES"), 0700))
|
||||
pref := newKeyValueStore(path)
|
||||
r.Equal("", pref.Get("key"))
|
||||
}
|
||||
|
||||
func TestKeyValueStoreGet(t *testing.T) {
|
||||
pref := newTestKeyValueStore(t)
|
||||
require.Equal(t, "value", pref.Get("str"))
|
||||
require.Equal(t, "42", pref.Get("int"))
|
||||
require.Equal(t, "true", pref.Get("bool"))
|
||||
require.Equal(t, "t", pref.Get("falseBool"))
|
||||
func 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) {
|
||||
pref := newTestKeyValueStore(t)
|
||||
require.Equal(t, 0, pref.GetInt("str"))
|
||||
require.Equal(t, 42, pref.GetInt("int"))
|
||||
require.Equal(t, 0, pref.GetInt("bool"))
|
||||
require.Equal(t, 0, pref.GetInt("falseBool"))
|
||||
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) {
|
||||
pref := newTestKeyValueStore(t)
|
||||
require.Equal(t, false, pref.GetBool("str"))
|
||||
require.Equal(t, false, pref.GetBool("int"))
|
||||
require.Equal(t, true, pref.GetBool("bool"))
|
||||
require.Equal(t, false, pref.GetBool("falseBool"))
|
||||
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) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
r := require.New(t)
|
||||
pref, clean := newTestEmptyKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
pref.setDefault("key", "value")
|
||||
pref.setDefault("key", "othervalue")
|
||||
require.Equal(t, "value", pref.Get("key"))
|
||||
r.Equal("value", pref.Get("key"))
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSet(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
r := require.New(t)
|
||||
pref, clean := newTestEmptyKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
pref.Set("str", "value")
|
||||
checkSavedKeyValueStore(t, "{\n\t\"str\": \"value\"\n}")
|
||||
checkSavedKeyValueStore(r, pref.path, "{\n\t\"str\": \"value\"\n}")
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSetInt(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
r := require.New(t)
|
||||
pref, clean := newTestEmptyKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
pref.SetInt("int", 42)
|
||||
checkSavedKeyValueStore(t, "{\n\t\"int\": \"42\"\n}")
|
||||
checkSavedKeyValueStore(r, pref.path, "{\n\t\"int\": \"42\"\n}")
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSetBool(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
r := require.New(t)
|
||||
pref, clean := newTestEmptyKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
pref.SetBool("trueBool", true)
|
||||
pref.SetBool("falseBool", false)
|
||||
checkSavedKeyValueStore(t, "{\n\t\"falseBool\": \"false\",\n\t\"trueBool\": \"true\"\n}")
|
||||
checkSavedKeyValueStore(r, pref.path, "{\n\t\"falseBool\": \"false\",\n\t\"trueBool\": \"true\"\n}")
|
||||
}
|
||||
|
||||
func newTestEmptyKeyValueStore(t *testing.T) *keyValueStore {
|
||||
require.NoError(t, os.RemoveAll(testPrefFilePath))
|
||||
return newKeyValueStore(testPrefFilePath)
|
||||
func newTmpFile(r *require.Assertions) (path string, clean func()) {
|
||||
tmpfile, err := ioutil.TempFile("", "pref.*.json")
|
||||
r.NoError(err)
|
||||
defer r.NoError(tmpfile.Close())
|
||||
|
||||
return tmpfile.Name(), func() {
|
||||
r.NoError(os.Remove(tmpfile.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
func newTestKeyValueStore(t *testing.T) *keyValueStore {
|
||||
require.NoError(t, ioutil.WriteFile(testPrefFilePath, []byte("{\"str\":\"value\",\"int\":\"42\",\"bool\":\"true\",\"falseBool\":\"t\"}"), 0700))
|
||||
return newKeyValueStore(testPrefFilePath)
|
||||
func newTestEmptyKeyValueStore(r *require.Assertions) (*keyValueStore, func()) {
|
||||
path, clean := newTmpFile(r)
|
||||
return newKeyValueStore(path), clean
|
||||
}
|
||||
|
||||
func checkSavedKeyValueStore(t *testing.T, expected string) {
|
||||
data, err := ioutil.ReadFile(testPrefFilePath)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, string(data))
|
||||
func newTestKeyValueStore(r *require.Assertions) (*keyValueStore, func()) {
|
||||
path, clean := newTmpFile(r)
|
||||
r.NoError(ioutil.WriteFile(path, []byte("{\"str\":\"value\",\"int\":\"42\",\"bool\":\"true\",\"falseBool\":\"t\"}"), 0700))
|
||||
return newKeyValueStore(path), clean
|
||||
}
|
||||
|
||||
func checkSavedKeyValueStore(r *require.Assertions, path, expected string) {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
r.NoError(err)
|
||||
r.Equal(expected, string(data))
|
||||
}
|
||||
|
||||
@ -101,10 +101,10 @@ func (f *frontendCLI) notifyNeedUpgrade() {
|
||||
f.Println("Please download and install the newest version of application from", version.LandingPage)
|
||||
}
|
||||
|
||||
func (f *frontendCLI) notifyCredentialsError() { // nolint[unused]
|
||||
func (f *frontendCLI) notifyCredentialsError() {
|
||||
// Print in 80-column width.
|
||||
f.Println("ProtonMail Bridge is not able to detect a supported password manager")
|
||||
f.Println("(pass, gnome-keyring). Please install and set up a supported password manager")
|
||||
f.Println("(secret-service or pass). Please install and set up a supported password manager")
|
||||
f.Println("and restart the application.")
|
||||
}
|
||||
|
||||
|
||||
@ -55,6 +55,26 @@ Window {
|
||||
function getCursorPos() {
|
||||
return BridgePreview.getCursorPos()
|
||||
}
|
||||
|
||||
function restart() {
|
||||
root.quit()
|
||||
console.log("Restarting....")
|
||||
root.openBridge()
|
||||
}
|
||||
|
||||
function openBridge() {
|
||||
bridge = bridgeComponent.createObject()
|
||||
var showSetupGuide = false
|
||||
if (showSetupGuide) {
|
||||
var newUserObject = root.userComponent.createObject(root)
|
||||
newUserObject.username = "LerooooyJenkins@protonmail.com"
|
||||
newUserObject.loggedIn = true
|
||||
newUserObject.setupGuideSeen = false
|
||||
root.users.append( { object: newUserObject } )
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function quit() {
|
||||
if (bridge !== undefined && bridge !== null) {
|
||||
bridge.destroy()
|
||||
@ -367,18 +387,7 @@ Window {
|
||||
|
||||
text: "Open Bridge"
|
||||
enabled: bridge === undefined || bridge === null
|
||||
onClicked: {
|
||||
bridge = bridgeComponent.createObject()
|
||||
var showSetupGuide = false
|
||||
if (showSetupGuide) {
|
||||
var newUserObject = root.userComponent.createObject(root)
|
||||
newUserObject.username = "LerooooyJenkins@protonmail.com"
|
||||
newUserObject.loggedIn = true
|
||||
newUserObject.setupGuideSeen = false
|
||||
root.users.append( { object: newUserObject } )
|
||||
}
|
||||
|
||||
}
|
||||
onClicked: root.openBridge()
|
||||
}
|
||||
|
||||
Button {
|
||||
@ -589,7 +598,15 @@ Window {
|
||||
text: "No keychain"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.hasNoKeychain()
|
||||
root.notifyHasNoKeychain()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Rebuild keychain"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.notifyRebuildKeychain()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -815,7 +832,8 @@ Window {
|
||||
root.changeKeychainFinished()
|
||||
}
|
||||
signal changeKeychainFinished()
|
||||
signal hasNoKeychain()
|
||||
signal notifyHasNoKeychain()
|
||||
signal notifyRebuildKeychain()
|
||||
|
||||
signal noActiveKeyForRecipient(string email)
|
||||
signal showMainWindow()
|
||||
|
||||
@ -181,7 +181,7 @@ SettingsView {
|
||||
}
|
||||
|
||||
function isValidEmail(text){
|
||||
var reEmail = /\w+@\w+\.\w+/
|
||||
var reEmail = /^[^@]+@[^@]+\.[A-Za-z]+\s*$/
|
||||
return reEmail.test(text)
|
||||
}
|
||||
|
||||
|
||||
@ -115,4 +115,9 @@ Item {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.noKeychain
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.rebuildKeychain
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,7 +73,8 @@ QtObject {
|
||||
root.enableLocalCache,
|
||||
root.resetBridge,
|
||||
root.deleteAccount,
|
||||
root.noKeychain
|
||||
root.noKeychain,
|
||||
root.rebuildKeychain
|
||||
]
|
||||
|
||||
// Connection
|
||||
@ -870,7 +871,7 @@ QtObject {
|
||||
|
||||
property Notification noKeychain: Notification {
|
||||
title: qsTr("No keychain available")
|
||||
description: qsTr("Bridge is not able to detected a supported password manager (pass, gnome-keyring). Please install and setup supported password manager and restart the application.")
|
||||
description: qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup supported password manager and restart the application.")
|
||||
brief: title
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
@ -879,7 +880,7 @@ QtObject {
|
||||
Connections {
|
||||
target: root.backend
|
||||
|
||||
onHasNoKeychain: {
|
||||
onNotifyHasNoKeychain: {
|
||||
root.noKeychain.active = true
|
||||
}
|
||||
}
|
||||
@ -891,6 +892,45 @@ QtObject {
|
||||
onTriggered: {
|
||||
root.backend.quit()
|
||||
}
|
||||
},
|
||||
Action {
|
||||
text: qsTr("Restart Bridge")
|
||||
|
||||
onTriggered: {
|
||||
root.backend.restart()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
property Notification rebuildKeychain: Notification {
|
||||
title: qsTr("Your macOS keychain might be corrupted")
|
||||
description: qsTr("Bridge is not able to access your macOS keychain. Please consult the instructions on our support page.")
|
||||
brief: title
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
|
||||
|
||||
property var supportLink: "https://protonmail.com/support/knowledge-base/macos-keychain-corrupted"
|
||||
|
||||
|
||||
Connections {
|
||||
target: root.backend
|
||||
|
||||
onNotifyRebuildKeychain: {
|
||||
console.log("notifications")
|
||||
root.rebuildKeychain.active = true
|
||||
}
|
||||
}
|
||||
|
||||
action: [
|
||||
Action {
|
||||
text: qsTr("Open the support page")
|
||||
|
||||
onTriggered: {
|
||||
Qt.openUrlExternally(root.rebuildKeychain.supportLink)
|
||||
root.backend.quit()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -61,6 +61,7 @@ type FrontendQt struct {
|
||||
log *logrus.Entry
|
||||
initializing sync.WaitGroup
|
||||
initializationDone sync.Once
|
||||
firstTimeAutostart sync.Once
|
||||
|
||||
app *widgets.QApplication
|
||||
engine *qml.QQmlEngine
|
||||
|
||||
@ -26,6 +26,7 @@ import (
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/keychain"
|
||||
)
|
||||
|
||||
func (f *FrontendQt) watchEvents() {
|
||||
@ -64,7 +65,11 @@ func (f *FrontendQt) watchEvents() {
|
||||
if strings.Contains(errorDetails, "SMTP failed") {
|
||||
f.qml.PortIssueSMTP()
|
||||
}
|
||||
case <-credentialsErrorCh:
|
||||
case reason := <-credentialsErrorCh:
|
||||
if reason == keychain.ErrMacKeychainRebuild.Error() {
|
||||
f.qml.NotifyRebuildKeychain()
|
||||
continue
|
||||
}
|
||||
f.qml.NotifyHasNoKeychain()
|
||||
case email := <-noActiveKeyForRecipientCh:
|
||||
f.qml.NoActiveKeyForRecipient(email)
|
||||
|
||||
@ -53,7 +53,7 @@ func (f *FrontendQt) reportBug(description, address, emailClient string, include
|
||||
core.QSysInfo_ProductType(),
|
||||
core.QSysInfo_PrettyProductName(),
|
||||
description,
|
||||
"Unknown account",
|
||||
address,
|
||||
address,
|
||||
emailClient,
|
||||
includeLogs,
|
||||
|
||||
@ -44,6 +44,8 @@ func (f *FrontendQt) initiateQtApplication() error {
|
||||
|
||||
core.QCoreApplication_SetApplicationName(f.programName)
|
||||
core.QCoreApplication_SetApplicationVersion(f.programVersion)
|
||||
core.QCoreApplication_SetOrganizationName("Proton AG")
|
||||
core.QCoreApplication_SetOrganizationDomain("proton.ch")
|
||||
|
||||
// High DPI scaling for windows.
|
||||
core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, false)
|
||||
|
||||
@ -76,6 +76,15 @@ func (f *FrontendQt) changeLocalCache(enableDiskCache bool, diskCachePath *core.
|
||||
}
|
||||
|
||||
func (f *FrontendQt) setIsAutostartOn() {
|
||||
// GODT-1507 Windows: autostart needs to be created after Qt is initialized.
|
||||
f.firstTimeAutostart.Do(func() {
|
||||
if !f.bridge.IsFirstStart() {
|
||||
return
|
||||
}
|
||||
if err := f.bridge.EnableAutostart(); err != nil {
|
||||
f.log.WithError(err).Error("Failed to enable autostart")
|
||||
}
|
||||
})
|
||||
f.qml.SetIsAutostartOn(f.bridge.IsAutostartEnabled())
|
||||
}
|
||||
|
||||
|
||||
@ -142,6 +142,7 @@ type QMLBackend struct {
|
||||
_ func(keychain string) `slot:"changeKeychain"`
|
||||
_ func() `signal:"changeKeychainFinished"`
|
||||
_ func() `signal:"notifyHasNoKeychain"`
|
||||
_ func() `signal:"notifyRebuildKeychain"`
|
||||
|
||||
_ func(email string) `signal:noActiveKeyForRecipient`
|
||||
_ func() `signal:showMainWindow`
|
||||
|
||||
49
internal/sentry/hostarch_darwin.go
Normal file
49
internal/sentry/hostarch_darwin.go
Normal file
@ -0,0 +1,49 @@
|
||||
// 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/>.
|
||||
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"github.com/elastic/go-sysinfo"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const translatedProcDarwin = "sysctl.proc_translated"
|
||||
|
||||
func getHostAarch() string {
|
||||
host, err := sysinfo.Host()
|
||||
if err != nil {
|
||||
return "not-detected"
|
||||
}
|
||||
|
||||
// It is not possible to retrieve real hardware architecture once using
|
||||
// rosetta. But it is possible to detect the process translation if
|
||||
// rosetta is used.
|
||||
res, err := unix.SysctlRaw(translatedProcDarwin)
|
||||
if err != nil || len(res) > 4 {
|
||||
return host.Info().Architecture + "_err"
|
||||
}
|
||||
|
||||
if res[0] == 1 {
|
||||
return host.Info().Architecture + "_rosetta"
|
||||
}
|
||||
|
||||
return host.Info().Architecture
|
||||
}
|
||||
31
internal/sentry/hostarch_default.go
Normal file
31
internal/sentry/hostarch_default.go
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/>.
|
||||
|
||||
//go:build !darwin
|
||||
// +build !darwin
|
||||
|
||||
package sentry
|
||||
|
||||
import "github.com/elastic/go-sysinfo"
|
||||
|
||||
func getHostAarch() string {
|
||||
host, err := sysinfo.Host()
|
||||
if err != nil {
|
||||
return "not-detected"
|
||||
}
|
||||
return host.Info().Architecture
|
||||
}
|
||||
@ -20,18 +20,20 @@ package sentry
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var skippedFunctions = []string{} //nolint[gochecknoglobals]
|
||||
|
||||
func init() { // nolint[noinit]
|
||||
func init() { //nolint[noinit, gochecknoinits]
|
||||
if err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: constants.DSNSentry,
|
||||
Release: constants.Revision,
|
||||
@ -42,13 +44,20 @@ func init() { // nolint[noinit]
|
||||
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetFingerprint([]string{"{{ default }}"})
|
||||
scope.SetTag("UserID", "not-defined")
|
||||
})
|
||||
|
||||
sentry.Logger = log.New(
|
||||
logrus.WithField("pkg", "sentry-go").WriterLevel(logrus.WarnLevel),
|
||||
"", 0,
|
||||
)
|
||||
}
|
||||
|
||||
type Reporter struct {
|
||||
appName string
|
||||
appVersion string
|
||||
userAgent fmt.Stringer
|
||||
hostArch string
|
||||
}
|
||||
|
||||
// NewReporter creates new sentry reporter with appName and appVersion to report.
|
||||
@ -57,6 +66,7 @@ func NewReporter(appName, appVersion string, userAgent fmt.Stringer) *Reporter {
|
||||
appName: appName,
|
||||
appVersion: appVersion,
|
||||
userAgent: userAgent,
|
||||
hostArch: getHostAarch(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,7 +119,7 @@ func (r *Reporter) scopedReport(context map[string]interface{}, doReport func())
|
||||
"Client": r.appName,
|
||||
"Version": r.appVersion,
|
||||
"UserAgent": r.userAgent.String(),
|
||||
"UserID": "",
|
||||
"HostArch": r.hostArch,
|
||||
}
|
||||
|
||||
sentry.WithScope(func(scope *sentry.Scope) {
|
||||
@ -179,3 +189,6 @@ func isFunctionFilteredOut(function string) bool {
|
||||
func Flush(maxWaiTime time.Duration) {
|
||||
sentry.Flush(maxWaiTime)
|
||||
}
|
||||
|
||||
func (r *Reporter) SetClientFromManager(cm pmapi.Manager) {
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import (
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/internal/store/cache"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
@ -126,6 +127,7 @@ func (store *Store) getCachedMessage(messageID string) ([]byte, error) {
|
||||
|
||||
literal, err := job.GetResult()
|
||||
if err != nil {
|
||||
store.checkAndRemoveDeletedMessage(err, messageID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -184,8 +186,21 @@ func (store *Store) BuildAndCacheMessage(ctx context.Context, messageID string)
|
||||
|
||||
literal, err := job.GetResult()
|
||||
if err != nil {
|
||||
store.checkAndRemoveDeletedMessage(err, messageID)
|
||||
return err
|
||||
}
|
||||
|
||||
return store.cache.Set(store.user.ID(), messageID, literal)
|
||||
}
|
||||
|
||||
func (store *Store) checkAndRemoveDeletedMessage(err error, msgID string) {
|
||||
if !pmapi.IsUnprocessableEntity(err) {
|
||||
return
|
||||
}
|
||||
l := store.log.WithError(err).WithField("msgID", msgID)
|
||||
l.Warn("Deleting message which was not found on API")
|
||||
|
||||
if deleteErr := store.deleteMessageEvent(msgID); deleteErr != nil {
|
||||
l.WithField("deleteErr", deleteErr).Error("Failed to delete non-existed API message from DB")
|
||||
}
|
||||
}
|
||||
|
||||
8
internal/store/cache/disk.go
vendored
8
internal/store/cache/disk.go
vendored
@ -63,10 +63,16 @@ func NewOnDiskCache(path string, cmp Compressor, opts Options) (Cache, error) {
|
||||
}
|
||||
|
||||
file, err := ioutil.TempFile(path, "tmp")
|
||||
defer func() {
|
||||
file.Close() //nolint[errcheck]
|
||||
os.Remove(file.Name()) //nolint[errcheck]
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot open test write target: %w", err)
|
||||
}
|
||||
if _, err := file.Write([]byte("test-write")); err != nil {
|
||||
return nil, fmt.Errorf("cannot write to target: %w", err)
|
||||
}
|
||||
os.Remove(file.Name()) //nolint[errcheck]
|
||||
|
||||
usage := du.NewDiskUsage(path)
|
||||
|
||||
|
||||
@ -243,7 +243,7 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun
|
||||
}
|
||||
|
||||
// All errors except ErrUnauthorized (which is not possible to recover from) are ignored.
|
||||
if err != nil && errors.Cause(err) != pmapi.ErrUnauthorized {
|
||||
if err != nil && !pmapi.IsFailedAuth(errors.Cause(err)) && errors.Cause(err) != pmapi.ErrUnauthorized {
|
||||
l.WithError(err).WithField("errors", loop.errCounter).Error("Error skipped")
|
||||
loop.errCounter++
|
||||
if loop.errCounter == errMaxSentry {
|
||||
@ -477,7 +477,7 @@ func (loop *eventLoop) processMessages(eventLog *logrus.Entry, messages []*pmapi
|
||||
msgLog.WithError(err).Warning("Message was not present in DB. Trying fetch...")
|
||||
|
||||
if msg, err = loop.client().GetMessage(context.Background(), message.ID); err != nil {
|
||||
if _, ok := err.(pmapi.ErrUnprocessableEntity); ok {
|
||||
if pmapi.IsUnprocessableEntity(err) {
|
||||
msgLog.WithError(err).Warn("Skipping message update because message exists neither in local DB nor on API")
|
||||
err = nil
|
||||
continue
|
||||
|
||||
@ -229,7 +229,10 @@ func (u *Users) MigrateCache(srcPath, dstPath string) error {
|
||||
// (read-only is conserved). Do copy instead.
|
||||
tmp, err := ioutil.TempFile(srcPath, "tmp")
|
||||
if err == nil {
|
||||
defer os.Remove(tmp.Name()) //nolint[errcheck]
|
||||
defer func() {
|
||||
tmp.Close() //nolint[errcheck]
|
||||
os.Remove(tmp.Name()) //nolint[errcheck]
|
||||
}()
|
||||
|
||||
if err := os.Rename(srcPath, dstPath); err == nil {
|
||||
return nil
|
||||
|
||||
@ -69,6 +69,7 @@ func newUser(
|
||||
|
||||
creds, err := credStorer.Get(userID)
|
||||
if err != nil {
|
||||
notifyKeychainRepair(eventListener, err)
|
||||
return nil, nil, errors.Wrap(err, "failed to load user credentials")
|
||||
}
|
||||
|
||||
@ -162,6 +163,7 @@ func (u *User) handleAuthRefresh(auth *pmapi.AuthRefresh) {
|
||||
|
||||
creds, err := u.credStorer.UpdateToken(u.userID, auth.UID, auth.RefreshToken)
|
||||
if err != nil {
|
||||
notifyKeychainRepair(u.listener, err)
|
||||
u.log.WithError(err).Error("Failed to update refresh token in credentials store")
|
||||
return
|
||||
}
|
||||
@ -223,7 +225,7 @@ func (u *User) UpdateSpace(apiUser *pmapi.User) {
|
||||
// values from client.CurrentUser()
|
||||
if apiUser == nil {
|
||||
var err error
|
||||
apiUser, err = u.client.GetUser(pmapi.ContextWithoutRetry(context.Background()))
|
||||
apiUser, err = u.GetClient().GetUser(pmapi.ContextWithoutRetry(context.Background()))
|
||||
if err != nil {
|
||||
u.log.WithError(err).Warning("Cannot update user space")
|
||||
return
|
||||
@ -280,16 +282,21 @@ func (u *User) unlockIfNecessary() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch errors.Cause(err) {
|
||||
case pmapi.ErrNoConnection, pmapi.ErrUpgradeApplication:
|
||||
u.log.WithError(err).Warn("Could not unlock user")
|
||||
return nil
|
||||
if pmapi.IsFailedAuth(err) || pmapi.IsFailedUnlock(err) {
|
||||
if logoutErr := u.logout(); logoutErr != nil {
|
||||
u.log.WithError(logoutErr).Warn("Could not logout user")
|
||||
}
|
||||
return errors.Wrap(err, "failed to unlock user")
|
||||
}
|
||||
|
||||
if logoutErr := u.logout(); logoutErr != nil {
|
||||
u.log.WithError(logoutErr).Warn("Could not logout user")
|
||||
switch errors.Cause(err) {
|
||||
case pmapi.ErrNoConnection, pmapi.ErrUpgradeApplication:
|
||||
u.log.WithError(err).Warn("Skipping unlock for known reason")
|
||||
default:
|
||||
u.log.WithError(err).Error("Unknown unlock issue")
|
||||
}
|
||||
return errors.Wrap(err, "failed to unlock user")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsCombinedAddressMode returns whether user is set in combined or split mode.
|
||||
@ -345,6 +352,10 @@ func (u *User) GetAddressID(address string) (id string, err error) {
|
||||
return u.store.GetAddressID(address)
|
||||
}
|
||||
|
||||
if u.client == nil {
|
||||
return "", errors.New("bridge account is not fully connected to server")
|
||||
}
|
||||
|
||||
addresses := u.client.Addresses()
|
||||
pmapiAddress := addresses.ByEmail(address)
|
||||
if pmapiAddress != nil {
|
||||
@ -399,6 +410,7 @@ func (u *User) UpdateUser(ctx context.Context) error {
|
||||
|
||||
creds, err := u.credStorer.UpdateEmails(u.userID, u.client.Addresses().ActiveEmails())
|
||||
if err != nil {
|
||||
notifyKeychainRepair(u.listener, err)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -436,6 +448,7 @@ func (u *User) SwitchAddressMode() error {
|
||||
|
||||
creds, err := u.credStorer.SwitchAddressMode(u.userID)
|
||||
if err != nil {
|
||||
notifyKeychainRepair(u.listener, err)
|
||||
return errors.Wrap(err, "could not switch credentials store address mode")
|
||||
}
|
||||
|
||||
@ -473,15 +486,19 @@ func (u *User) Logout() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := u.client.AuthDelete(context.Background()); err != nil {
|
||||
if u.client == nil {
|
||||
u.log.Warn("Failed to delete auth: no client")
|
||||
} else if err := u.client.AuthDelete(context.Background()); err != nil {
|
||||
u.log.WithError(err).Warn("Failed to delete auth")
|
||||
}
|
||||
|
||||
creds, err := u.credStorer.Logout(u.userID)
|
||||
if err != nil {
|
||||
notifyKeychainRepair(u.listener, err)
|
||||
u.log.WithError(err).Warn("Could not log user out from credentials store")
|
||||
|
||||
if err := u.credStorer.Delete(u.userID); err != nil {
|
||||
notifyKeychainRepair(u.listener, err)
|
||||
u.log.WithError(err).Error("Could not delete user from credentials store")
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -170,7 +170,7 @@ func TestCheckBridgeLoginLoggedOut(t *testing.T) {
|
||||
// Mock init of user.
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil),
|
||||
m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any()),
|
||||
m.pmapiClient.EXPECT().ListLabels(gomock.Any()).Return(nil, errors.New("ErrUnauthorized")),
|
||||
m.pmapiClient.EXPECT().ListLabels(gomock.Any()).Return(nil, pmapi.ErrUnauthorized),
|
||||
m.pmapiClient.EXPECT().Addresses().Return(nil),
|
||||
|
||||
// Mock CheckBridgeLogin.
|
||||
|
||||
@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
r "github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -46,7 +47,7 @@ func TestNewUserUnlockFails(t *testing.T) {
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
|
||||
m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any()),
|
||||
m.pmapiClient.EXPECT().IsUnlocked().Return(false),
|
||||
m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(errors.New("bad password")),
|
||||
m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(pmapi.ErrUnlockFailed{OriginalError: errors.New("bad password")}),
|
||||
|
||||
// Handle of unlock error.
|
||||
m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil),
|
||||
|
||||
@ -27,6 +27,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/metrics"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/keychain"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
@ -130,6 +131,7 @@ func (u *Users) loadUsersFromCredentialsStore() error {
|
||||
|
||||
userIDs, err := u.credStorer.List()
|
||||
if err != nil {
|
||||
notifyKeychainRepair(u.events, err)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -178,14 +180,17 @@ func (u *Users) loadConnectedUser(ctx context.Context, user *User, creds *creden
|
||||
return connectErr
|
||||
}
|
||||
|
||||
if logoutErr := user.logout(); logoutErr != nil {
|
||||
logrus.WithError(logoutErr).Warn("Could not logout user")
|
||||
if pmapi.IsFailedAuth(connectErr) {
|
||||
if logoutErr := user.logout(); logoutErr != nil {
|
||||
logrus.WithError(logoutErr).Warn("Could not logout user")
|
||||
}
|
||||
}
|
||||
return errors.Wrap(err, "could not refresh token")
|
||||
}
|
||||
|
||||
// Update the user's credentials with the latest auth used to connect this user.
|
||||
if creds, err = u.credStorer.UpdateToken(creds.UserID, auth.UID, auth.RefreshToken); err != nil {
|
||||
notifyKeychainRepair(u.events, err)
|
||||
return errors.Wrap(err, "could not create get user's refresh token")
|
||||
}
|
||||
|
||||
@ -224,12 +229,14 @@ func (u *Users) FinishLogin(client pmapi.Client, auth *pmapi.Auth, password []by
|
||||
|
||||
// Update the user's credentials with the latest auth used to connect this user.
|
||||
if _, err := u.credStorer.UpdateToken(auth.UserID, auth.UID, auth.RefreshToken); err != nil {
|
||||
notifyKeychainRepair(u.events, err)
|
||||
return nil, errors.Wrap(err, "failed to load user credentials")
|
||||
}
|
||||
|
||||
// Update the password in case the user changed it.
|
||||
creds, err := u.credStorer.UpdatePassword(apiUser.ID, passphrase)
|
||||
if err != nil {
|
||||
notifyKeychainRepair(u.events, err)
|
||||
return nil, errors.Wrap(err, "failed to update password of user in credentials store")
|
||||
}
|
||||
|
||||
@ -258,6 +265,7 @@ func (u *Users) addNewUser(client pmapi.Client, apiUser *pmapi.User, auth *pmapi
|
||||
defer u.lock.Unlock()
|
||||
|
||||
if _, err := u.credStorer.Add(apiUser.ID, apiUser.Name, auth.UID, auth.RefreshToken, passphrase, client.Addresses().ActiveEmails()); err != nil {
|
||||
notifyKeychainRepair(u.events, err)
|
||||
return errors.Wrap(err, "failed to add user credentials to credentials store")
|
||||
}
|
||||
|
||||
@ -382,6 +390,7 @@ func (u *Users) DeleteUser(userID string, clearStore bool) error {
|
||||
}
|
||||
|
||||
if err := u.credStorer.Delete(userID); err != nil {
|
||||
notifyKeychainRepair(u.events, err)
|
||||
log.WithError(err).Error("Cannot remove user")
|
||||
return err
|
||||
}
|
||||
@ -441,3 +450,9 @@ func (u *Users) crashBandicoot(username string) {
|
||||
panic("Your wish is my command… I crash!")
|
||||
}
|
||||
}
|
||||
|
||||
func notifyKeychainRepair(l listener.Listener, err error) {
|
||||
if err == keychain.ErrMacKeychainRebuild {
|
||||
l.Emit(events.CredentialsErrorEvent, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ import (
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
r "github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -80,11 +81,11 @@ func TestNewUsersWithConnectedUserWithBadToken(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.clientManager.EXPECT().NewClientWithRefresh(gomock.Any(), "uid", "acc").Return(nil, nil, errors.New("bad token"))
|
||||
m.clientManager.EXPECT().NewClientWithRefresh(gomock.Any(), "uid", "acc").Return(nil, nil, pmapi.ErrAuthFailed{OriginalError: errors.New("bad token")})
|
||||
m.clientManager.EXPECT().NewClient("uid", "", "acc", time.Time{}).Return(m.pmapiClient)
|
||||
m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any())
|
||||
m.pmapiClient.EXPECT().IsUnlocked().Return(false)
|
||||
m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(errors.New("not authorized"))
|
||||
m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(pmapi.ErrAuthFailed{OriginalError: errors.New("not authorized")})
|
||||
m.pmapiClient.EXPECT().AuthDelete(gomock.Any())
|
||||
|
||||
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
|
||||
@ -93,7 +94,6 @@ func TestNewUsersWithConnectedUserWithBadToken(t *testing.T) {
|
||||
|
||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
||||
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
|
||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
||||
|
||||
checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
|
||||
|
||||
@ -37,7 +37,6 @@ import (
|
||||
pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
|
||||
tests "github.com/ProtonMail/proton-bridge/test"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
r "github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -175,6 +174,7 @@ func initMocks(t *testing.T) mocks {
|
||||
|
||||
cacheFile, err := ioutil.TempFile("", "bridge-store-cache-*.db")
|
||||
r.NoError(t, err, "could not get temporary file for store cache")
|
||||
r.NoError(t, cacheFile.Close())
|
||||
|
||||
m := mocks{
|
||||
t: t,
|
||||
@ -201,6 +201,7 @@ func initMocks(t *testing.T) mocks {
|
||||
|
||||
dbFile, err := ioutil.TempFile(t.TempDir(), "bridge-store-db-*.db")
|
||||
r.NoError(t, err, "could not get temporary file for store db")
|
||||
r.NoError(t, dbFile.Close())
|
||||
|
||||
return store.New(
|
||||
sentryReporter,
|
||||
@ -329,7 +330,7 @@ func mockInitDisconnectedUser(m mocks) {
|
||||
m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any()),
|
||||
|
||||
// Mock of store initialisation for the unauthorized user.
|
||||
m.pmapiClient.EXPECT().ListLabels(gomock.Any()).Return(nil, errors.New("ErrUnauthorized")),
|
||||
m.pmapiClient.EXPECT().ListLabels(gomock.Any()).Return(nil, pmapi.ErrUnauthorized),
|
||||
m.pmapiClient.EXPECT().Addresses().Return(nil),
|
||||
)
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ import (
|
||||
// RemoveOldVersions is a noop on darwin; we don't test it there.
|
||||
|
||||
func TestRemoveOldVersions(t *testing.T) {
|
||||
updates, err := ioutil.TempDir("", "updates")
|
||||
updates, err := ioutil.TempDir(t.TempDir(), "updates")
|
||||
require.NoError(t, err)
|
||||
|
||||
v := newTestVersioner(t, "myCoolApp", updates, "2.3.4-beta", "2.3.4", "2.3.5", "2.4.0")
|
||||
|
||||
@ -65,11 +65,13 @@ func makeDummyVersionDirectory(t *testing.T, exeName, updates, version string) s
|
||||
exe, err := os.Create(filepath.Join(target, getExeName(exeName)))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, exe)
|
||||
require.NoError(t, exe.Close())
|
||||
require.NoError(t, os.Chmod(exe.Name(), 0700))
|
||||
|
||||
sig, err := os.Create(filepath.Join(target, getExeName(exeName)+".sig"))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, sig)
|
||||
require.NoError(t, sig.Close())
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
@ -40,6 +40,16 @@ func init() { // nolint[noinit]
|
||||
defaultHelper = MacOSKeychain
|
||||
}
|
||||
|
||||
func parseError(original error) error {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(original.Error(), "25293") {
|
||||
return ErrMacKeychainRebuild
|
||||
}
|
||||
return original
|
||||
}
|
||||
|
||||
func newMacOSHelper(url string) (credentials.Helper, error) {
|
||||
return &macOSHelper{url: url}, nil
|
||||
}
|
||||
@ -76,7 +86,7 @@ func (h *macOSHelper) Add(creds *credentials.Credentials) error {
|
||||
|
||||
query := newQuery(hostURL, userID)
|
||||
query.SetData([]byte(creds.Secret))
|
||||
return keychain.AddItem(query)
|
||||
return parseError(keychain.AddItem(query))
|
||||
}
|
||||
|
||||
func (h *macOSHelper) Delete(secretURL string) error {
|
||||
@ -87,7 +97,7 @@ func (h *macOSHelper) Delete(secretURL string) error {
|
||||
|
||||
query := newQuery(hostURL, userID)
|
||||
|
||||
return keychain.DeleteItem(query)
|
||||
return parseError(keychain.DeleteItem(query))
|
||||
}
|
||||
|
||||
func (h *macOSHelper) Get(secretURL string) (string, string, error) {
|
||||
@ -102,7 +112,7 @@ func (h *macOSHelper) Get(secretURL string) (string, string, error) {
|
||||
|
||||
results, err := keychain.QueryItem(query)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", "", parseError(err)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
@ -121,7 +131,7 @@ func (h *macOSHelper) List() (map[string]string, error) {
|
||||
|
||||
userIDs, err := keychain.GetGenericPasswordAccounts(h.url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, parseError(err)
|
||||
}
|
||||
|
||||
for _, userID := range userIDs {
|
||||
|
||||
@ -28,27 +28,27 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
Pass = "pass-app"
|
||||
GnomeKeyring = "gnome-keyring"
|
||||
Pass = "pass-app"
|
||||
SecretService = "secret-service"
|
||||
)
|
||||
|
||||
func init() { // nolint[noinit]
|
||||
Helpers = make(map[string]helperConstructor)
|
||||
|
||||
if _, err := exec.LookPath("pass"); err == nil {
|
||||
if _, err := exec.LookPath("gnome-keyring"); err == nil && isUsable(newSecretServiceHelper("")) {
|
||||
Helpers[SecretService] = newSecretServiceHelper
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath("pass"); err == nil && isUsable(newPassHelper("")) {
|
||||
Helpers[Pass] = newPassHelper
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath("gnome-keyring"); err == nil {
|
||||
Helpers[GnomeKeyring] = newGnomeKeyringHelper
|
||||
}
|
||||
|
||||
// If Pass is available, use it by default.
|
||||
// Otherwise, if GnomeKeyring is available, use it by default.
|
||||
if _, ok := Helpers[Pass]; ok && isUsable(newPassHelper("")) {
|
||||
// Otherwise, if SecretService is available, use it by default.
|
||||
if _, ok := Helpers[Pass]; ok {
|
||||
defaultHelper = Pass
|
||||
} else if _, ok := Helpers[GnomeKeyring]; ok && isUsable(newGnomeKeyringHelper("")) {
|
||||
defaultHelper = GnomeKeyring
|
||||
} else if _, ok := Helpers[SecretService]; ok {
|
||||
defaultHelper = SecretService
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ func newPassHelper(string) (credentials.Helper, error) {
|
||||
return &pass.Pass{}, nil
|
||||
}
|
||||
|
||||
func newGnomeKeyringHelper(string) (credentials.Helper, error) {
|
||||
func newSecretServiceHelper(string) (credentials.Helper, error) {
|
||||
return &secretservice.Secretservice{}, nil
|
||||
}
|
||||
|
||||
|
||||
@ -37,6 +37,9 @@ var (
|
||||
// ErrNoKeychain indicates that no suitable keychain implementation could be loaded.
|
||||
ErrNoKeychain = errors.New("no keychain") // nolint[noglobals]
|
||||
|
||||
// ErrMacKeychainRebuild is returned on macOS with blocked or corrupted keychain.
|
||||
ErrMacKeychainRebuild = errors.New("keychain error -25293")
|
||||
|
||||
// Helpers holds all discovered keychain helpers. It is populated in init().
|
||||
Helpers map[string]helperConstructor // nolint[noglobals]
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -33,6 +34,7 @@ var (
|
||||
wantOutput = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
testProcessSleep = 100 // ms
|
||||
runParallelTimeOverhead = 150 // ms
|
||||
windowsCIExtra = 250 // ms - estimated experimentally
|
||||
)
|
||||
|
||||
func TestParallel(t *testing.T) {
|
||||
@ -56,6 +58,9 @@ func TestParallel(t *testing.T) {
|
||||
|
||||
wantMinDuration := int(math.Ceil(float64(len(testInput))/float64(workers))) * testProcessSleep
|
||||
wantMaxDuration := wantMinDuration + runParallelTimeOverhead
|
||||
if runtime.GOOS == "windows" {
|
||||
wantMaxDuration += windowsCIExtra
|
||||
}
|
||||
r.True(t, duration.Nanoseconds() > int64(wantMinDuration*1000000), "Duration too short: %v (expected: %v)", duration, wantMinDuration)
|
||||
r.True(t, duration.Nanoseconds() < int64(wantMaxDuration*1000000), "Duration too long: %v (expected: %v)", duration, wantMaxDuration)
|
||||
})
|
||||
|
||||
@ -19,11 +19,11 @@ package pmapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Address statuses.
|
||||
@ -201,7 +201,7 @@ func (c *client) unlockAddress(passphrase []byte, address *Address) error {
|
||||
|
||||
kr, err := address.Keys.UnlockAll(passphrase, c.userKeyRing)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "cannot unlock address keys for "+address.ID)
|
||||
}
|
||||
|
||||
c.addrKeyRing[address.ID] = kr
|
||||
|
||||
@ -51,7 +51,7 @@ type TwoFAInfo struct {
|
||||
}
|
||||
|
||||
func (twoFAInfo TwoFAInfo) hasTwoFactor() bool {
|
||||
return twoFAInfo.Enabled > 0
|
||||
return twoFAInfo.Enabled > TwoFADisabled
|
||||
}
|
||||
|
||||
type TwoFAStatus int
|
||||
@ -185,7 +185,7 @@ func (c *client) authRefresh(ctx context.Context) error {
|
||||
|
||||
auth, err := c.manager.authRefresh(ctx, c.uid, c.ref)
|
||||
if err != nil {
|
||||
if err != ErrNoConnection {
|
||||
if IsFailedAuth(err) {
|
||||
c.sendAuthRefresh(nil)
|
||||
}
|
||||
return err
|
||||
|
||||
122
pkg/pmapi/auth_server_test.go
Normal file
122
pkg/pmapi/auth_server_test.go
Normal file
@ -0,0 +1,122 @@
|
||||
// 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/>.
|
||||
|
||||
package pmapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testRefreshResponse struct {
|
||||
Code int
|
||||
AccessToken string
|
||||
ExpiresIn int
|
||||
TokenType string
|
||||
Scope string
|
||||
Scopes []string
|
||||
UID string
|
||||
RefreshToken string
|
||||
LocalID int
|
||||
|
||||
r *require.Assertions
|
||||
}
|
||||
|
||||
var tokenID = 0
|
||||
|
||||
func newTestRefreshToken(r *require.Assertions) testRefreshResponse {
|
||||
tokenID++
|
||||
scopes := []string{
|
||||
"full",
|
||||
"self",
|
||||
"parent",
|
||||
"user",
|
||||
"loggedin",
|
||||
"paid",
|
||||
"nondelinquent",
|
||||
"mail",
|
||||
"verified",
|
||||
}
|
||||
return testRefreshResponse{
|
||||
Code: 1000,
|
||||
AccessToken: fmt.Sprintf("acc%d", tokenID),
|
||||
ExpiresIn: 3600,
|
||||
TokenType: "Bearer",
|
||||
Scope: strings.Join(scopes, " "),
|
||||
Scopes: scopes,
|
||||
UID: fmt.Sprintf("uid%d", tokenID),
|
||||
RefreshToken: fmt.Sprintf("ref%d", tokenID),
|
||||
r: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *testRefreshResponse) isCorrectRefreshToken(body io.ReadCloser) int {
|
||||
request := authRefreshReq{}
|
||||
err := json.NewDecoder(body).Decode(&request)
|
||||
r.r.NoError(body.Close())
|
||||
r.r.NoError(err)
|
||||
|
||||
if r.UID != request.UID {
|
||||
return http.StatusUnprocessableEntity
|
||||
}
|
||||
if r.RefreshToken != request.RefreshToken {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (r *testRefreshResponse) handleAuthRefresh(response http.ResponseWriter, request *http.Request) {
|
||||
if code := r.isCorrectRefreshToken(request.Body); code != http.StatusOK {
|
||||
response.WriteHeader(code)
|
||||
return
|
||||
}
|
||||
|
||||
tokenID++
|
||||
r.AccessToken = fmt.Sprintf("acc%d", tokenID)
|
||||
r.RefreshToken = fmt.Sprintf("ref%d", tokenID)
|
||||
|
||||
response.Header().Set("Content-Type", "application/json")
|
||||
response.WriteHeader(http.StatusOK)
|
||||
r.r.NoError(json.NewEncoder(response).Encode(r))
|
||||
}
|
||||
|
||||
func (r *testRefreshResponse) wantAuthRefresh() AuthRefresh {
|
||||
return AuthRefresh{
|
||||
UID: r.UID,
|
||||
AccessToken: r.AccessToken,
|
||||
RefreshToken: r.RefreshToken,
|
||||
ExpiresIn: int64(r.ExpiresIn),
|
||||
Scopes: r.Scopes,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *testRefreshResponse) isAuthorized(header http.Header) bool {
|
||||
return header.Get("x-pm-uid") == r.UID && header.Get("Authorization") == "Bearer "+r.AccessToken
|
||||
}
|
||||
|
||||
func (r *testRefreshResponse) handleAuthCheckOnly(response http.ResponseWriter, request *http.Request) {
|
||||
if r.isAuthorized(request.Header) {
|
||||
response.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
response.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
@ -25,179 +25,210 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
a "github.com/stretchr/testify/assert"
|
||||
r "github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAutomaticAuthRefresh(t *testing.T) {
|
||||
var wantAuthRefresh = &AuthRefresh{
|
||||
UID: "testUID",
|
||||
AccessToken: "testAcc",
|
||||
RefreshToken: "testRef",
|
||||
ExpiresIn: 100,
|
||||
}
|
||||
|
||||
r := require.New(t)
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/auth/refresh", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
currentTokens := newTestRefreshToken(r)
|
||||
testUID := currentTokens.UID
|
||||
testAcc := currentTokens.AccessToken
|
||||
testRef := currentTokens.RefreshToken
|
||||
currentTokens.ExpiresIn = 100
|
||||
|
||||
if err := json.NewEncoder(w).Encode(wantAuthRefresh); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/addresses", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
mux.HandleFunc("/auth/refresh", currentTokens.handleAuthRefresh)
|
||||
mux.HandleFunc("/addresses", currentTokens.handleAuthCheckOnly)
|
||||
|
||||
ts := httptest.NewServer(mux)
|
||||
|
||||
var gotAuthRefresh *AuthRefresh
|
||||
|
||||
c := New(Config{HostURL: ts.URL}).
|
||||
NewClient("uid", "acc", "ref", time.Now().Add(-time.Second))
|
||||
NewClient(testUID, testAcc, testRef, time.Now().Add(-time.Second))
|
||||
|
||||
c.AddAuthRefreshHandler(func(auth *AuthRefresh) { gotAuthRefresh = auth })
|
||||
|
||||
// Make a request with an access token that already expired one second ago.
|
||||
_, err := c.GetAddresses(context.Background())
|
||||
r.NoError(t, err)
|
||||
r.NoError(err)
|
||||
|
||||
wantAuthRefresh := currentTokens.wantAuthRefresh()
|
||||
|
||||
// The auth callback should have been called.
|
||||
a.Equal(t, *wantAuthRefresh, *gotAuthRefresh)
|
||||
r.NotNil(gotAuthRefresh)
|
||||
r.Equal(wantAuthRefresh, *gotAuthRefresh)
|
||||
|
||||
cl := c.(*client) //nolint[forcetypeassert] we want to panic here
|
||||
a.Equal(t, wantAuthRefresh.AccessToken, cl.acc)
|
||||
a.Equal(t, wantAuthRefresh.RefreshToken, cl.ref)
|
||||
a.WithinDuration(t, expiresIn(100), cl.exp, time.Second)
|
||||
r.Equal(wantAuthRefresh.AccessToken, cl.acc)
|
||||
r.Equal(wantAuthRefresh.RefreshToken, cl.ref)
|
||||
r.WithinDuration(expiresIn(100), cl.exp, time.Second)
|
||||
}
|
||||
|
||||
func Test401AuthRefresh(t *testing.T) {
|
||||
var wantAuthRefresh = &AuthRefresh{
|
||||
UID: "testUID",
|
||||
AccessToken: "testAcc",
|
||||
RefreshToken: "testRef",
|
||||
}
|
||||
r := require.New(t)
|
||||
currentTokens := newTestRefreshToken(r)
|
||||
testUID := currentTokens.UID
|
||||
testRef := currentTokens.RefreshToken
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/auth/refresh", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(wantAuthRefresh); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
var call int
|
||||
|
||||
mux.HandleFunc("/addresses", func(w http.ResponseWriter, r *http.Request) {
|
||||
call++
|
||||
|
||||
if call == 1 {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/auth/refresh", currentTokens.handleAuthRefresh)
|
||||
mux.HandleFunc("/addresses", currentTokens.handleAuthCheckOnly)
|
||||
|
||||
ts := httptest.NewServer(mux)
|
||||
|
||||
var gotAuthRefresh *AuthRefresh
|
||||
|
||||
// Create a new client.
|
||||
c := New(Config{HostURL: ts.URL}).
|
||||
NewClient("uid", "acc", "ref", time.Now().Add(time.Hour))
|
||||
m := New(Config{HostURL: ts.URL})
|
||||
c := m.NewClient(testUID, "oldAccToken", testRef, time.Now().Add(time.Hour))
|
||||
|
||||
// Register an auth handler.
|
||||
c.AddAuthRefreshHandler(func(auth *AuthRefresh) { gotAuthRefresh = auth })
|
||||
|
||||
// The first request will fail with 401, triggering a refresh and retry.
|
||||
_, err := c.GetAddresses(context.Background())
|
||||
r.NoError(t, err)
|
||||
r.NoError(err)
|
||||
|
||||
// The auth callback should have been called.
|
||||
r.Equal(t, *wantAuthRefresh, *gotAuthRefresh)
|
||||
r.NotNil(gotAuthRefresh)
|
||||
r.Equal(currentTokens.wantAuthRefresh(), *gotAuthRefresh)
|
||||
}
|
||||
|
||||
func Test401RevokedAuth(t *testing.T) {
|
||||
r := require.New(t)
|
||||
currentTokens := newTestRefreshToken(r)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/auth/refresh", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/addresses", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
})
|
||||
mux.HandleFunc("/auth/refresh", currentTokens.handleAuthRefresh)
|
||||
mux.HandleFunc("/addresses", currentTokens.handleAuthCheckOnly)
|
||||
|
||||
ts := httptest.NewServer(mux)
|
||||
|
||||
c := New(Config{HostURL: ts.URL}).
|
||||
NewClient("uid", "acc", "ref", time.Now().Add(time.Hour))
|
||||
NewClient("badUID", "badAcc", "badRef", time.Now().Add(time.Hour))
|
||||
|
||||
// The request will fail with 401, triggering a refresh.
|
||||
// The retry will also fail with 401, returning an error.
|
||||
_, err := c.GetAddresses(context.Background())
|
||||
r.EqualError(t, err, ErrUnauthorized.Error())
|
||||
r.True(IsFailedAuth(err))
|
||||
}
|
||||
|
||||
func Test401RevokedAuthTokenUpdate(t *testing.T) {
|
||||
var oldAuth = &AuthRefresh{
|
||||
UID: "UID",
|
||||
AccessToken: "oldAcc",
|
||||
RefreshToken: "oldRef",
|
||||
ExpiresIn: 3600,
|
||||
}
|
||||
|
||||
var newAuth = &AuthRefresh{
|
||||
UID: "UID",
|
||||
AccessToken: "newAcc",
|
||||
RefreshToken: "newRef",
|
||||
}
|
||||
func Test401OldRefreshToken(t *testing.T) {
|
||||
r := require.New(t)
|
||||
currentTokens := newTestRefreshToken(r)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/auth/refresh", func(w http.ResponseWriter, r *http.Request) {
|
||||
mux.HandleFunc("/auth/refresh", currentTokens.handleAuthRefresh)
|
||||
mux.HandleFunc("/addresses", currentTokens.handleAuthCheckOnly)
|
||||
|
||||
ts := httptest.NewServer(mux)
|
||||
|
||||
c := New(Config{HostURL: ts.URL}).
|
||||
NewClient(currentTokens.UID, "oldAcc", "oldRef", time.Now().Add(time.Hour))
|
||||
|
||||
// The request will fail with 401, triggering a refresh.
|
||||
// The retry will also fail with 401, returning an error.
|
||||
_, err := c.GetAddresses(context.Background())
|
||||
r.True(IsFailedAuth(err))
|
||||
}
|
||||
|
||||
func Test401NoAccessToken(t *testing.T) {
|
||||
r := require.New(t)
|
||||
currentTokens := newTestRefreshToken(r)
|
||||
testUID := currentTokens.UID
|
||||
testRef := currentTokens.RefreshToken
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/auth/refresh", currentTokens.handleAuthRefresh)
|
||||
mux.HandleFunc("/addresses", currentTokens.handleAuthCheckOnly)
|
||||
|
||||
ts := httptest.NewServer(mux)
|
||||
|
||||
c := New(Config{HostURL: ts.URL}).
|
||||
NewClient(testUID, "", testRef, time.Now().Add(time.Hour))
|
||||
|
||||
// The request will fail with 401, triggering a refresh. After the refresh it should succeed.
|
||||
_, err := c.GetAddresses(context.Background())
|
||||
r.NoError(err)
|
||||
}
|
||||
|
||||
func Test401ExpiredAuthUpdateUser(t *testing.T) {
|
||||
r := require.New(t)
|
||||
mux := http.NewServeMux()
|
||||
currentTokens := newTestRefreshToken(r)
|
||||
testUID := currentTokens.UID
|
||||
testRef := currentTokens.RefreshToken
|
||||
|
||||
mux.HandleFunc("/auth/refresh", currentTokens.handleAuthRefresh)
|
||||
|
||||
mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !currentTokens.isAuthorized(r.Header) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(newAuth); err != nil {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
respObj := struct {
|
||||
Code int
|
||||
User *User
|
||||
}{
|
||||
Code: 1000,
|
||||
User: &User{
|
||||
ID: "MJLke8kWh1BBvG95JBIrZvzpgsZ94hNNgjNHVyhXMiv4g9cn6SgvqiIFR5cigpml2LD_iUk_3DkV29oojTt3eA==",
|
||||
Name: "jason",
|
||||
UsedSpace: &usedSpace,
|
||||
},
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(respObj); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/addresses", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") == ("Bearer " + oldAuth.AccessToken) {
|
||||
if !currentTokens.isAuthorized(r.Header) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("Authorization") == ("Bearer " + newAuth.AccessToken) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
respObj := struct {
|
||||
Code int
|
||||
Addresses []*Address
|
||||
}{
|
||||
Code: 1000,
|
||||
Addresses: []*Address{},
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(respObj); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
ts := httptest.NewServer(mux)
|
||||
|
||||
c := New(Config{HostURL: ts.URL}).
|
||||
NewClient(oldAuth.UID, oldAuth.AccessToken, oldAuth.RefreshToken, time.Now().Add(time.Hour))
|
||||
m := New(Config{HostURL: ts.URL})
|
||||
c, _, err := m.NewClientWithRefresh(context.Background(), testUID, testRef)
|
||||
r.NoError(err)
|
||||
|
||||
// The request will fail with 401, triggering a refresh. After the refresh it should succeed.
|
||||
_, err := c.GetAddresses(context.Background())
|
||||
r.NoError(t, err)
|
||||
_, err = c.UpdateUser(context.Background())
|
||||
r.NoError(err)
|
||||
}
|
||||
|
||||
func TestAuth2FA(t *testing.T) {
|
||||
r := require.New(t)
|
||||
twoFACode := "code"
|
||||
|
||||
finish, c := newTestClientCallbacks(t,
|
||||
func(tb testing.TB, w http.ResponseWriter, req *http.Request) string {
|
||||
r.NoError(t, checkMethodAndPath(req, "POST", "/auth/2fa"))
|
||||
r.NoError(checkMethodAndPath(req, "POST", "/auth/2fa"))
|
||||
|
||||
var twoFAreq auth2FAReq
|
||||
r.NoError(t, json.NewDecoder(req.Body).Decode(&twoFAreq))
|
||||
r.Equal(t, twoFAreq.TwoFactorCode, twoFACode)
|
||||
r.NoError(json.NewDecoder(req.Body).Decode(&twoFAreq))
|
||||
r.Equal(twoFAreq.TwoFactorCode, twoFACode)
|
||||
|
||||
return "/auth/2fa/post_response.json"
|
||||
},
|
||||
@ -205,31 +236,33 @@ func TestAuth2FA(t *testing.T) {
|
||||
defer finish()
|
||||
|
||||
err := c.Auth2FA(context.Background(), twoFACode)
|
||||
r.NoError(t, err)
|
||||
r.NoError(err)
|
||||
}
|
||||
|
||||
func TestAuth2FA_Fail(t *testing.T) {
|
||||
r := require.New(t)
|
||||
finish, c := newTestClientCallbacks(t,
|
||||
func(tb testing.TB, w http.ResponseWriter, req *http.Request) string {
|
||||
r.NoError(t, checkMethodAndPath(req, "POST", "/auth/2fa"))
|
||||
r.NoError(checkMethodAndPath(req, "POST", "/auth/2fa"))
|
||||
return "/auth/2fa/post_401_bad_password.json"
|
||||
},
|
||||
)
|
||||
defer finish()
|
||||
|
||||
err := c.Auth2FA(context.Background(), "code")
|
||||
r.Equal(t, ErrBad2FACode, err)
|
||||
r.Equal(ErrBad2FACode, err)
|
||||
}
|
||||
|
||||
func TestAuth2FA_Retry(t *testing.T) {
|
||||
r := require.New(t)
|
||||
finish, c := newTestClientCallbacks(t,
|
||||
func(tb testing.TB, w http.ResponseWriter, req *http.Request) string {
|
||||
r.NoError(t, checkMethodAndPath(req, "POST", "/auth/2fa"))
|
||||
r.NoError(checkMethodAndPath(req, "POST", "/auth/2fa"))
|
||||
return "/auth/2fa/post_422_bad_password.json"
|
||||
},
|
||||
)
|
||||
defer finish()
|
||||
|
||||
err := c.Auth2FA(context.Background(), "code")
|
||||
r.Equal(t, ErrBad2FACodeTryAgain, err)
|
||||
r.Equal(ErrBad2FACodeTryAgain, err)
|
||||
}
|
||||
|
||||
@ -19,8 +19,6 @@ package pmapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Unlock unlocks all the user and address keys using the given passphrase, creating user and address keyrings.
|
||||
@ -34,26 +32,26 @@ func (c *client) Unlock(ctx context.Context, passphrase []byte) (err error) {
|
||||
|
||||
// unlock unlocks the user's keys but without locking the keyring lock first.
|
||||
// Should only be used internally by methods that first lock the lock.
|
||||
func (c *client) unlock(ctx context.Context, passphrase []byte) (err error) {
|
||||
if _, err = c.CurrentUser(ctx); err != nil {
|
||||
return
|
||||
func (c *client) unlock(ctx context.Context, passphrase []byte) error {
|
||||
if _, err := c.CurrentUser(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.userKeyRing == nil {
|
||||
if err = c.unlockUser(passphrase); err != nil {
|
||||
return errors.Wrap(err, "failed to unlock user")
|
||||
if err := c.unlockUser(passphrase); err != nil {
|
||||
return ErrUnlockFailed{err}
|
||||
}
|
||||
}
|
||||
|
||||
for _, address := range c.addresses {
|
||||
if c.addrKeyRing[address.ID] == nil {
|
||||
if err = c.unlockAddress(passphrase, address); err != nil {
|
||||
return errors.Wrap(err, "failed to unlock address")
|
||||
if err := c.unlockAddress(passphrase, address); err != nil {
|
||||
return ErrUnlockFailed{err}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) ReloadKeys(ctx context.Context, passphrase []byte) (err error) {
|
||||
|
||||
@ -82,4 +82,5 @@ type AuthRefreshHandler func(*AuthRefresh)
|
||||
type clientManager interface {
|
||||
r(context.Context) *resty.Request
|
||||
authRefresh(context.Context, string, string) (*AuthRefresh, error)
|
||||
setSentryUserID(userID string)
|
||||
}
|
||||
|
||||
@ -60,11 +60,16 @@ func formatAsAddress(rawURL string) string {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
host := url.Host
|
||||
if host == "" {
|
||||
host = url.Path
|
||||
}
|
||||
|
||||
port := "443"
|
||||
if url.Scheme == "http" {
|
||||
port = "80"
|
||||
}
|
||||
return net.JoinHostPort(url.Host, port)
|
||||
return net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
// DialTLS dials the given network/address. If it fails, it retries using a proxy.
|
||||
|
||||
@ -36,11 +36,16 @@ const (
|
||||
proxyDoHTimeout = 20 * time.Second
|
||||
proxyCanReachTimeout = 20 * time.Second
|
||||
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]
|
||||
"https://dns11.quad9.net/dns-query",
|
||||
"https://dns.google/dns-query",
|
||||
Quad9Provider,
|
||||
Quad9PortProvider,
|
||||
GoogleProvider,
|
||||
}
|
||||
|
||||
// proxyProvider manages known proxies.
|
||||
|
||||
@ -27,12 +27,6 @@ import (
|
||||
"golang.org/x/net/http/httpproxy"
|
||||
)
|
||||
|
||||
const (
|
||||
TestDoHQuery = "dMFYGSLTQOJXXI33ONVQWS3BOMNUA.protonpro.xyz"
|
||||
TestQuad9Provider = "https://dns11.quad9.net/dns-query"
|
||||
TestGoogleProvider = "https://dns.google/dns-query"
|
||||
)
|
||||
|
||||
func TestProxyProvider_FindProxy(t *testing.T) {
|
||||
proxy := getTrustedServer()
|
||||
defer closeServer(proxy)
|
||||
@ -142,17 +136,28 @@ func TestProxyProvider_FindProxy_CanReachTimeout(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProxyProvider_DoHLookup_Quad9(t *testing.T) {
|
||||
p := newProxyProvider(Config{}, []string{TestQuad9Provider, TestGoogleProvider}, TestDoHQuery)
|
||||
p := newProxyProvider(Config{}, []string{Quad9Provider, GoogleProvider}, proxyQuery)
|
||||
|
||||
records, err := p.dohLookup(context.Background(), TestDoHQuery, TestQuad9Provider)
|
||||
records, err := p.dohLookup(context.Background(), proxyQuery, Quad9Provider)
|
||||
r.NoError(t, err)
|
||||
r.NotEmpty(t, records)
|
||||
}
|
||||
|
||||
// DISABLEDTestProxyProvider_DoHLookup_Quad9Port cannot run on CI due to custom
|
||||
// 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)
|
||||
|
||||
records, err := p.dohLookup(context.Background(), proxyQuery, Quad9PortProvider)
|
||||
r.NoError(t, err)
|
||||
r.NotEmpty(t, records)
|
||||
}
|
||||
|
||||
func TestProxyProvider_DoHLookup_Google(t *testing.T) {
|
||||
p := newProxyProvider(Config{}, []string{TestQuad9Provider, TestGoogleProvider}, TestDoHQuery)
|
||||
p := newProxyProvider(Config{}, []string{Quad9Provider, GoogleProvider}, proxyQuery)
|
||||
|
||||
records, err := p.dohLookup(context.Background(), TestDoHQuery, TestGoogleProvider)
|
||||
records, err := p.dohLookup(context.Background(), proxyQuery, GoogleProvider)
|
||||
r.NoError(t, err)
|
||||
r.NotEmpty(t, records)
|
||||
}
|
||||
@ -160,7 +165,7 @@ func TestProxyProvider_DoHLookup_Google(t *testing.T) {
|
||||
func TestProxyProvider_DoHLookup_FindProxy(t *testing.T) {
|
||||
skipIfProxyIsSet(t)
|
||||
|
||||
p := newProxyProvider(Config{}, []string{TestQuad9Provider, TestGoogleProvider}, TestDoHQuery)
|
||||
p := newProxyProvider(Config{}, []string{Quad9Provider, GoogleProvider}, proxyQuery)
|
||||
|
||||
url, err := p.findReachableServer()
|
||||
r.NoError(t, err)
|
||||
@ -170,7 +175,7 @@ func TestProxyProvider_DoHLookup_FindProxy(t *testing.T) {
|
||||
func TestProxyProvider_DoHLookup_FindProxyFirstProviderUnreachable(t *testing.T) {
|
||||
skipIfProxyIsSet(t)
|
||||
|
||||
p := newProxyProvider(Config{}, []string{"https://unreachable", TestQuad9Provider, TestGoogleProvider}, TestDoHQuery)
|
||||
p := newProxyProvider(Config{}, []string{"https://unreachable", Quad9Provider, GoogleProvider}, proxyQuery)
|
||||
|
||||
url, err := p.findReachableServer()
|
||||
r.NoError(t, err)
|
||||
|
||||
@ -251,3 +251,18 @@ func TestProxyDialer_UseProxy_FindSecondAlternativeIfFirstFailsAndAPIIsStillBloc
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, formatAsAddress(proxy2.URL), d.proxyAddress)
|
||||
}
|
||||
|
||||
func TestFormatAsAddress(t *testing.T) {
|
||||
r := require.New(t)
|
||||
testData := map[string]string{
|
||||
"sub.domain.tld": "sub.domain.tld:443",
|
||||
"http://sub.domain.tld": "sub.domain.tld:80",
|
||||
"https://sub.domain.tld": "sub.domain.tld:443",
|
||||
"ftp://sub.domain.tld": "sub.domain.tld:443",
|
||||
"//sub.domain.tld": "sub.domain.tld:443",
|
||||
}
|
||||
|
||||
for rawURL, wantURL := range testData {
|
||||
r.Equal(wantURL, formatAsAddress(rawURL))
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,10 +31,58 @@ var (
|
||||
ErrPasswordWrong = errors.New("wrong password")
|
||||
)
|
||||
|
||||
// ErrUnprocessableEntity ...
|
||||
type ErrUnprocessableEntity struct {
|
||||
originalError error
|
||||
OriginalError error
|
||||
}
|
||||
|
||||
func IsUnprocessableEntity(err error) bool {
|
||||
_, ok := err.(ErrUnprocessableEntity)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUnprocessableEntity) Error() string {
|
||||
return err.originalError.Error()
|
||||
return err.OriginalError.Error()
|
||||
}
|
||||
|
||||
// ErrBadRequest ...
|
||||
type ErrBadRequest struct {
|
||||
OriginalError error
|
||||
}
|
||||
|
||||
func IsBadRequest(err error) bool {
|
||||
_, ok := err.(ErrBadRequest)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrBadRequest) Error() string {
|
||||
return err.OriginalError.Error()
|
||||
}
|
||||
|
||||
// ErrAuthFailed ...
|
||||
type ErrAuthFailed struct {
|
||||
OriginalError error
|
||||
}
|
||||
|
||||
func IsFailedAuth(err error) bool {
|
||||
_, ok := err.(ErrAuthFailed)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrAuthFailed) Error() string {
|
||||
return err.OriginalError.Error()
|
||||
}
|
||||
|
||||
// ErrUnlockFailed ...
|
||||
type ErrUnlockFailed struct {
|
||||
OriginalError error
|
||||
}
|
||||
|
||||
func IsFailedUnlock(err error) bool {
|
||||
_, ok := err.(ErrUnlockFailed)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUnlockFailed) Error() string {
|
||||
return err.OriginalError.Error()
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
@ -32,11 +33,13 @@ type manager struct {
|
||||
|
||||
isDown bool
|
||||
locker sync.Locker
|
||||
refreshingAuth sync.Locker
|
||||
connectionObservers []ConnectionObserver
|
||||
proxyDialer *ProxyTLSDialer
|
||||
|
||||
pingMutex *sync.RWMutex
|
||||
isPinging bool
|
||||
pingMutex *sync.RWMutex
|
||||
isPinging bool
|
||||
setSentryUserIDOnce sync.Once
|
||||
}
|
||||
|
||||
func New(cfg Config) Manager {
|
||||
@ -45,11 +48,13 @@ func New(cfg Config) Manager {
|
||||
|
||||
func newManager(cfg Config) *manager {
|
||||
m := &manager{
|
||||
cfg: cfg,
|
||||
rc: resty.New().EnableTrace(),
|
||||
locker: &sync.Mutex{},
|
||||
pingMutex: &sync.RWMutex{},
|
||||
isPinging: false,
|
||||
cfg: cfg,
|
||||
rc: resty.New().EnableTrace(),
|
||||
locker: &sync.Mutex{},
|
||||
refreshingAuth: &sync.Mutex{},
|
||||
pingMutex: &sync.RWMutex{},
|
||||
isPinging: false,
|
||||
setSentryUserIDOnce: sync.Once{},
|
||||
}
|
||||
|
||||
proxyDialer, transport := newProxyDialerAndTransport(cfg)
|
||||
@ -158,3 +163,11 @@ func (m *manager) handleRequestFailure(req *resty.Request, err error) {
|
||||
|
||||
go m.pingUntilSuccess()
|
||||
}
|
||||
|
||||
func (m *manager) setSentryUserID(userID string) {
|
||||
m.setSentryUserIDOnce.Do(func() {
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetTag("UserID", userID)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -102,6 +102,9 @@ func (m *manager) auth(ctx context.Context, req AuthReq) (*Auth, error) {
|
||||
}
|
||||
|
||||
func (m *manager) authRefresh(ctx context.Context, uid, ref string) (*AuthRefresh, error) {
|
||||
m.refreshingAuth.Lock()
|
||||
defer m.refreshingAuth.Unlock()
|
||||
|
||||
var req = authRefreshReq{
|
||||
UID: uid,
|
||||
RefreshToken: ref,
|
||||
@ -117,6 +120,9 @@ func (m *manager) authRefresh(ctx context.Context, uid, ref string) (*AuthRefres
|
||||
|
||||
_, err := wrapNoConnection(m.r(ctx).SetBody(req).SetResult(&res).Post("/auth/refresh"))
|
||||
if err != nil {
|
||||
if IsBadRequest(err) || IsUnprocessableEntity(err) {
|
||||
err = ErrAuthFailed{err}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@ -59,16 +59,14 @@ func (m *manager) catchAPIError(_ *resty.Client, res *resty.Response) error {
|
||||
if apiErr, ok := res.Error().(*Error); ok {
|
||||
switch {
|
||||
case apiErr.Code == errCodeUpgradeApplication:
|
||||
err = ErrUpgradeApplication
|
||||
if m.cfg.UpgradeApplicationHandler != nil {
|
||||
m.cfg.UpgradeApplicationHandler()
|
||||
}
|
||||
return ErrUpgradeApplication
|
||||
case apiErr.Code == errCodePasswordWrong:
|
||||
err = ErrPasswordWrong
|
||||
return ErrPasswordWrong
|
||||
case apiErr.Code == errCodeAuthPaidPlanRequired:
|
||||
err = ErrPaidPlanRequired
|
||||
case res.StatusCode() == http.StatusUnprocessableEntity:
|
||||
err = ErrUnprocessableEntity{apiErr}
|
||||
return ErrPaidPlanRequired
|
||||
default:
|
||||
err = apiErr
|
||||
}
|
||||
@ -76,6 +74,13 @@ func (m *manager) catchAPIError(_ *resty.Client, res *resty.Response) error {
|
||||
err = errors.New(res.Status())
|
||||
}
|
||||
|
||||
switch res.StatusCode() {
|
||||
case http.StatusUnprocessableEntity:
|
||||
err = ErrUnprocessableEntity{err}
|
||||
case http.StatusBadRequest:
|
||||
err = ErrBadRequest{err}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@ -21,7 +21,6 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -126,7 +125,7 @@ func (c *client) UpdateUser(ctx context.Context) (*User, error) {
|
||||
|
||||
c.user = user
|
||||
c.addresses = addresses
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) { scope.SetUser(sentry.User{ID: user.ID}) })
|
||||
c.manager.setSentryUserID(user.ID)
|
||||
|
||||
return user, err
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
.PHONY: check-go check-godog install-godog test test-bridge test-live test-live-bridge test-stage test-debug test-live-debug bench
|
||||
|
||||
export GO111MODULE=on
|
||||
export BRIDGE_VERSION:=2.1.0+integrationtests
|
||||
export BRIDGE_VERSION:=2.1.2+integrationtests
|
||||
export VERBOSITY?=fatal
|
||||
export TEST_DATA=testdata
|
||||
|
||||
|
||||
@ -172,3 +172,7 @@ func (ctx *TestContext) MessagePreparationStarted(username string) {
|
||||
func (ctx *TestContext) MessagePreparationFinished(username string) {
|
||||
ctx.pmapiController.UnlockEvents(username)
|
||||
}
|
||||
|
||||
func (ctx *TestContext) CredentialsFailsOnWrite(shouldFail bool) {
|
||||
ctx.credStore.(*fakeCredStore).failOnWrite = shouldFail
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// bridgePassword is password to be used for IMAP or SMTP under tests.
|
||||
@ -28,6 +29,8 @@ const bridgePassword = "bridgepassword"
|
||||
|
||||
type fakeCredStore struct {
|
||||
credentials map[string]*credentials.Credentials
|
||||
|
||||
failOnWrite bool
|
||||
}
|
||||
|
||||
// newFakeCredStore returns a fake credentials store (optionally configured with the given credentials).
|
||||
@ -52,6 +55,9 @@ func (c *fakeCredStore) List() (userIDs []string, err error) {
|
||||
}
|
||||
|
||||
func (c *fakeCredStore) Add(userID, userName, uid, ref string, mailboxPassword []byte, emails []string) (*credentials.Credentials, error) {
|
||||
if c.failOnWrite {
|
||||
return nil, errors.New("An invalid attempt to change the owner of an item. (-25244)")
|
||||
}
|
||||
bridgePassword := bridgePassword
|
||||
if c, ok := c.credentials[userID]; ok {
|
||||
bridgePassword = c.BridgePassword
|
||||
@ -73,14 +79,23 @@ func (c *fakeCredStore) Get(userID string) (*credentials.Credentials, error) {
|
||||
}
|
||||
|
||||
func (c *fakeCredStore) SwitchAddressMode(userID string) (*credentials.Credentials, error) {
|
||||
if c.failOnWrite {
|
||||
return nil, errors.New("An invalid attempt to change the owner of an item. (-25244)")
|
||||
}
|
||||
return c.credentials[userID], nil
|
||||
}
|
||||
|
||||
func (c *fakeCredStore) UpdateEmails(userID string, emails []string) (*credentials.Credentials, error) {
|
||||
if c.failOnWrite {
|
||||
return nil, errors.New("An invalid attempt to change the owner of an item. (-25244)")
|
||||
}
|
||||
return c.credentials[userID], nil
|
||||
}
|
||||
|
||||
func (c *fakeCredStore) UpdatePassword(userID string, password []byte) (*credentials.Credentials, error) {
|
||||
if c.failOnWrite {
|
||||
return nil, errors.New("An invalid attempt to change the owner of an item. (-25244)")
|
||||
}
|
||||
creds, err := c.Get(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -90,6 +105,9 @@ func (c *fakeCredStore) UpdatePassword(userID string, password []byte) (*credent
|
||||
}
|
||||
|
||||
func (c *fakeCredStore) UpdateToken(userID, uid, ref string) (*credentials.Credentials, error) {
|
||||
if c.failOnWrite {
|
||||
return nil, errors.New("An invalid attempt to change the owner of an item. (-25244)")
|
||||
}
|
||||
creds, err := c.Get(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -99,12 +117,18 @@ func (c *fakeCredStore) UpdateToken(userID, uid, ref string) (*credentials.Crede
|
||||
}
|
||||
|
||||
func (c *fakeCredStore) Logout(userID string) (*credentials.Credentials, error) {
|
||||
if c.failOnWrite {
|
||||
return nil, errors.New("An invalid attempt to change the owner of an item. (-25244)")
|
||||
}
|
||||
c.credentials[userID].APIToken = ""
|
||||
c.credentials[userID].MailboxPassword = []byte{}
|
||||
return c.credentials[userID], nil
|
||||
}
|
||||
|
||||
func (c *fakeCredStore) Delete(userID string) error {
|
||||
if c.failOnWrite {
|
||||
return errors.New("An invalid attempt to change the owner of an item. (-25244)")
|
||||
}
|
||||
delete(c.credentials, userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -45,6 +45,8 @@ type PMAPIController interface {
|
||||
GetCalls(method, path string) [][]byte
|
||||
LockEvents(username string)
|
||||
UnlockEvents(username string)
|
||||
RemoveUserMessageWithoutEvent(username, messageID string) error
|
||||
RevokeSession(username string) error
|
||||
}
|
||||
|
||||
func newPMAPIController(listener listener.Listener) (PMAPIController, pmapi.Manager) {
|
||||
|
||||
@ -234,3 +234,26 @@ func (ctl *Controller) LockEvents(string) {}
|
||||
|
||||
// UnlockEvents doesn't needs to be implemented for fakeAPI.
|
||||
func (ctl *Controller) UnlockEvents(string) {}
|
||||
|
||||
func (ctl *Controller) RemoveUserMessageWithoutEvent(username string, messageID string) error {
|
||||
msgs, ok := ctl.messagesByUsername[username]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, message := range msgs {
|
||||
if message.ID == messageID {
|
||||
ctl.messagesByUsername[username] = append(msgs[:i], msgs[i+1:]...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("message not found")
|
||||
}
|
||||
|
||||
func (ctl *Controller) RevokeSession(username string) error {
|
||||
for _, session := range ctl.sessionsByUID {
|
||||
session.uid = "revoked"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -74,12 +74,12 @@ func (ctl *Controller) createSession(username string, hasFullScope bool) *fakeSe
|
||||
|
||||
func (ctl *Controller) refreshSessionIfAuthorized(uid, ref string) (*fakeSession, error) {
|
||||
session, ok := ctl.sessionsByUID[uid]
|
||||
if !ok {
|
||||
return nil, pmapi.ErrUnauthorized
|
||||
if !ok || session.uid != uid {
|
||||
return nil, pmapi.ErrAuthFailed{OriginalError: errors.New("bad uid")}
|
||||
}
|
||||
|
||||
if ref != session.ref {
|
||||
return nil, pmapi.ErrUnauthorized
|
||||
return nil, pmapi.ErrAuthFailed{OriginalError: errors.New("bad refresh token")}
|
||||
}
|
||||
|
||||
session.ref = ctl.tokenGenerator.next("ref")
|
||||
|
||||
@ -133,14 +133,32 @@ func (api *FakePMAPI) authRefresh() error {
|
||||
|
||||
session, err := api.controller.refreshSessionIfAuthorized(api.uid, api.ref)
|
||||
if err != nil {
|
||||
if pmapi.IsFailedAuth(err) {
|
||||
go api.handleAuth(nil)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
api.ref = session.ref
|
||||
api.acc = session.acc
|
||||
|
||||
go api.handleAuth(&pmapi.AuthRefresh{
|
||||
UID: api.uid,
|
||||
AccessToken: api.acc,
|
||||
RefreshToken: api.ref,
|
||||
ExpiresIn: 7200,
|
||||
Scopes: []string{"full", "self", "user", "mail"},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) handleAuth(auth *pmapi.AuthRefresh) {
|
||||
for _, handle := range api.authHandlers {
|
||||
handle(auth)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) setUser(username string) error {
|
||||
api.username = username
|
||||
api.log = api.log.WithField("username", username)
|
||||
|
||||
@ -69,7 +69,7 @@ func (m *fakePMAPIManager) NewClientWithRefresh(_ context.Context, uid, ref stri
|
||||
|
||||
session, err := m.controller.refreshSessionIfAuthorized(uid, ref)
|
||||
if err != nil {
|
||||
return nil, nil, pmapi.ErrUnauthorized
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
user, ok := m.controller.usersByUsername[session.username]
|
||||
|
||||
@ -37,7 +37,7 @@ func (api *FakePMAPI) GetMessage(_ context.Context, apiID string) (*pmapi.Messag
|
||||
if msg := api.getMessage(apiID); msg != nil {
|
||||
return msg, nil
|
||||
}
|
||||
return nil, fmt.Errorf("message %s not found", apiID)
|
||||
return nil, pmapi.ErrUnprocessableEntity{OriginalError: fmt.Errorf("message %s not found", apiID)}
|
||||
}
|
||||
|
||||
// ListMessages does not implement following filters:
|
||||
|
||||
@ -82,6 +82,10 @@ func (api *FakePMAPI) UpdateUser(context.Context) (*pmapi.User, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := api.checkAndRecordCall(GET, "/addresses", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return api.user, nil
|
||||
}
|
||||
|
||||
|
||||
@ -177,3 +177,14 @@ Feature: IMAP fetch messages
|
||||
# We had bug to incorectly set empty date, so let's make sure
|
||||
# there is no reference anywhere in the response.
|
||||
And IMAP response does not contain "\nDate: Thu, 01 Jan 1970"
|
||||
|
||||
Scenario: Fetch of message which was deleted without event processed
|
||||
Given there are 10 messages in mailbox "INBOX" for "user"
|
||||
And message "5" was deleted forever without event processed for "user"
|
||||
And there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "INBOX"
|
||||
When IMAP client fetches bodies "1:*"
|
||||
Then IMAP response is "NO"
|
||||
When IMAP client fetches bodies "1:*"
|
||||
Then IMAP response is "OK"
|
||||
And IMAP response has 9 messages
|
||||
|
||||
@ -79,3 +79,12 @@ Feature: Start bridge
|
||||
And "user" does not have loaded store
|
||||
And "user" does not have running event loop
|
||||
And "user" has zero space
|
||||
|
||||
Scenario: Start with connected user, database file and internet connection, but no write access to credentials
|
||||
Given there is user "user" which just logged in
|
||||
And credentials are locked
|
||||
And there is database file for "user"
|
||||
When bridge starts
|
||||
Then "user" is connected
|
||||
When IMAP client authenticates "user"
|
||||
Then IMAP response is "NO"
|
||||
|
||||
17
test/features/users/revoked_session.feature
Normal file
17
test/features/users/revoked_session.feature
Normal file
@ -0,0 +1,17 @@
|
||||
Feature: Session deleted on API
|
||||
|
||||
@ignore-live
|
||||
Scenario: Session revoked after start
|
||||
Given there is connected user "user"
|
||||
When session was revoked for "user"
|
||||
And the event loop of "user" loops once
|
||||
Then "user" is disconnected
|
||||
|
||||
|
||||
@ignore-live
|
||||
Scenario: Starting with revoked session
|
||||
Given there is user "user" which just logged in
|
||||
And session was revoked for "user"
|
||||
When bridge starts
|
||||
Then "user" is disconnected
|
||||
|
||||
@ -104,3 +104,14 @@ func (ctl *Controller) GetMessages(username, labelID string) ([]*pmapi.Message,
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func (ctl *Controller) RemoveUserMessageWithoutEvent(username string, messageID string) error {
|
||||
client, err := getPersistentClient(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addMessageIDToSkipEventOnceDeleted(messageID)
|
||||
|
||||
return client.DeleteMessages(context.Background(), []string{messageID})
|
||||
}
|
||||
|
||||
@ -48,7 +48,8 @@ var persistentClients = struct {
|
||||
byName map[string]clientAuthGetter
|
||||
saltByName map[string]string
|
||||
|
||||
eventsPaused sync.WaitGroup
|
||||
eventsPaused sync.WaitGroup
|
||||
skipDeletedMessageID map[string]struct{}
|
||||
}{}
|
||||
|
||||
type persistentClient struct {
|
||||
@ -79,7 +80,40 @@ func (pc *persistentClient) GetEvent(ctx context.Context, eventID string) (*pmap
|
||||
if !ok {
|
||||
return nil, errors.New("cannot convert to normal client")
|
||||
}
|
||||
return normalClient.GetEvent(ctx, eventID)
|
||||
|
||||
event, err := normalClient.GetEvent(ctx, eventID)
|
||||
if err != nil {
|
||||
return event, err
|
||||
}
|
||||
|
||||
return skipDeletedMessageIDs(event), nil
|
||||
}
|
||||
|
||||
func addMessageIDToSkipEventOnceDeleted(msgID string) {
|
||||
if persistentClients.skipDeletedMessageID == nil {
|
||||
persistentClients.skipDeletedMessageID = map[string]struct{}{}
|
||||
}
|
||||
persistentClients.skipDeletedMessageID[msgID] = struct{}{}
|
||||
}
|
||||
|
||||
func skipDeletedMessageIDs(event *pmapi.Event) *pmapi.Event {
|
||||
if len(event.Messages) == 0 {
|
||||
return event
|
||||
}
|
||||
|
||||
n := 0
|
||||
for i, m := range event.Messages {
|
||||
if _, ok := persistentClients.skipDeletedMessageID[m.ID]; ok && m.Action == pmapi.EventDelete {
|
||||
delete(persistentClients.skipDeletedMessageID, m.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
event.Messages[i] = m
|
||||
n++
|
||||
}
|
||||
event.Messages = event.Messages[:n]
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func SetupPersistentClients() {
|
||||
|
||||
@ -60,3 +60,7 @@ func (ctl *Controller) GetAuthClient(username string) pmapi.Client {
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func (ctl *Controller) RevokeSession(username string) error {
|
||||
return errors.New("revoke live session not implemented")
|
||||
}
|
||||
|
||||
@ -38,6 +38,7 @@ func StoreSetupFeatureContext(s *godog.ScenarioContext) {
|
||||
s.Step(`^there are messages for "([^"]*)" as follows$`, thereAreSomeMessagesForUserAsFollows)
|
||||
s.Step(`^there are (\d+) messages in mailbox(?:es)? "([^"]*)" for address "([^"]*)" of "([^"]*)"$`, thereAreSomeMessagesInMailboxesForAddressOfUser)
|
||||
s.Step(`^wait for Sphinx to create duplication indices$`, waitForSphinx)
|
||||
s.Step(`^message(?:s)? "([^"]*)" (?:was|were) deleted forever without event processed for "([^"]*)"$`, messageWasDeletedWithoutEvent)
|
||||
}
|
||||
|
||||
func thereIsUserWithMailboxes(bddUserID string, mailboxes *godog.Table) error {
|
||||
@ -319,3 +320,16 @@ func waitForSphinx() error {
|
||||
time.Sleep(15 * time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
func messageWasDeletedWithoutEvent(bddMessageID, bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
apiID, err := ctx.GetAPIMessageID(account.Username(), bddMessageID)
|
||||
if err != nil {
|
||||
return internalError(err, "getting BDD message ID %s", bddMessageID)
|
||||
}
|
||||
|
||||
return ctx.GetPMAPIController().RemoveUserMessageWithoutEvent(account.Username(), apiID)
|
||||
}
|
||||
|
||||
@ -29,6 +29,7 @@ func UsersActionsFeatureContext(s *godog.ScenarioContext) {
|
||||
s.Step(`^user deletes "([^"]*)"$`, userDeletesUser)
|
||||
s.Step(`^user deletes "([^"]*)" with cache$`, userDeletesUserWithCache)
|
||||
s.Step(`^"([^"]*)" swaps address "([^"]*)" with address "([^"]*)"$`, swapsAddressWithAddress)
|
||||
s.Step(`^session was revoked for "([^"]*)"$`, sessionRevoked)
|
||||
}
|
||||
|
||||
func userLogsIn(bddUserID string) error {
|
||||
@ -123,3 +124,8 @@ func swapsAddressWithAddress(bddUserID, bddAddressID1, bddAddressID2 string) err
|
||||
|
||||
return ctx.GetPMAPIController().ReorderAddresses(account.User(), addressIDs)
|
||||
}
|
||||
|
||||
func sessionRevoked(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
return ctx.GetPMAPIController().RevokeSession(account.Username())
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ func UsersSetupFeatureContext(s *godog.ScenarioContext) {
|
||||
s.Step(`^there is database file for "([^"]*)"$`, thereIsDatabaseFileForUser)
|
||||
s.Step(`^there is no database file for "([^"]*)"$`, thereIsNoDatabaseFileForUser)
|
||||
s.Step(`^there is "([^"]*)" in "([^"]*)" address mode$`, thereIsUserWithAddressMode)
|
||||
s.Step(`^credentials? (?:are|is) locked$`, credentialsAreLocked)
|
||||
}
|
||||
|
||||
func thereIsUser(bddUserID string) error {
|
||||
@ -150,3 +151,8 @@ func thereIsUserWithAddressMode(bddUserID, wantAddressMode string) error {
|
||||
ctx.EventuallySyncIsFinishedForUsername(user.Username())
|
||||
return nil
|
||||
}
|
||||
|
||||
func credentialsAreLocked() error {
|
||||
ctx.CredentialsFailsOnWrite(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user