forked from Silverfish/proton-bridge
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51eb2c42cd | |||
| c94d839fbb | |||
| de586e5f12 | |||
| c32a106898 | |||
| 5b20b6a3d0 | |||
| 3b07121f08 | |||
| a53bc4b027 | |||
| 478345e277 | |||
| 0ed78f1ccb | |||
| 6671dd38ea | |||
| 2d5ea669a5 | |||
| c7eb7234a2 | |||
| 73d1fe2f65 | |||
| cf75ea739f | |||
| c920c53243 | |||
| 63379001e3 | |||
| aa8cc3fc4b |
@ -147,8 +147,6 @@ build-linux-qa:
|
|||||||
.build-darwin-base:
|
.build-darwin-base:
|
||||||
extends: .build-base
|
extends: .build-base
|
||||||
before_script:
|
before_script:
|
||||||
- eval $(ssh-agent -s)
|
|
||||||
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
|
|
||||||
- export PATH=/usr/local/bin:$PATH
|
- export PATH=/usr/local/bin:$PATH
|
||||||
- export PATH=/usr/local/opt/git/bin:$PATH
|
- export PATH=/usr/local/opt/git/bin:$PATH
|
||||||
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
||||||
|
|||||||
0
.gitmodules
vendored
0
.gitmodules
vendored
@ -1,7 +1,9 @@
|
|||||||
# Building ProtonMail Bridge and Import-Export app
|
# Building ProtonMail Bridge and Import-Export app
|
||||||
|
|
||||||
## Prerequisites
|
## 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
|
* Go 1.13
|
||||||
* Bash with basic build utils: make, gcc, sed, find, grep, ...
|
* 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/)
|
* For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
|
||||||
|
|||||||
32
Changelog.md
32
Changelog.md
@ -2,7 +2,37 @@
|
|||||||
|
|
||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
## [Bridge 2.1.0] London
|
## [Bridge 2.1.3] London
|
||||||
|
|
||||||
|
## Added
|
||||||
|
GODT-1525: Add keybase/go-keychain/secretservice as new keychain helper.
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
GODT-1527: Change bug report description.
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
GODT-1537: Manual in-app update mechanism.
|
||||||
|
|
||||||
|
|
||||||
|
## [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
|
## Added
|
||||||
* GODT-1376: Add first userID to sentry scope.
|
* GODT-1376: Add first userID to sentry scope.
|
||||||
|
|||||||
4
Makefile
4
Makefile
@ -10,7 +10,7 @@ TARGET_OS?=${GOOS}
|
|||||||
.PHONY: build build-nogui build-launcher versioner hasher
|
.PHONY: build build-nogui build-launcher versioner hasher
|
||||||
|
|
||||||
# Keep version hardcoded so app build works also without Git repository.
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
BRIDGE_APP_VERSION?=2.1.1+git
|
BRIDGE_APP_VERSION?=2.1.3+git
|
||||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||||
SRC_ICO:=logo.ico
|
SRC_ICO:=logo.ico
|
||||||
SRC_ICNS:=Bridge.icns
|
SRC_ICNS:=Bridge.icns
|
||||||
@ -85,7 +85,7 @@ hasher:
|
|||||||
|
|
||||||
${TGZ_TARGET}: ${DEPLOY_DIR}/${TARGET_OS}
|
${TGZ_TARGET}: ${DEPLOY_DIR}/${TARGET_OS}
|
||||||
rm -f $@
|
rm -f $@
|
||||||
cd ${DEPLOY_DIR}/${TARGET_OS} && tar czf ../../../../$@ .
|
cd ${DEPLOY_DIR}/${TARGET_OS} && tar -czvf ../../../../$@ .
|
||||||
|
|
||||||
${DEPLOY_DIR}/linux: ${EXE_TARGET}
|
${DEPLOY_DIR}/linux: ${EXE_TARGET}
|
||||||
cp -pf ./internal/frontend/share/${SRC_SVG} ${DEPLOY_DIR}/linux/logo.svg
|
cp -pf ./internal/frontend/share/${SRC_SVG} ${DEPLOY_DIR}/linux/logo.svg
|
||||||
|
|||||||
@ -53,10 +53,12 @@ the user for a password.
|
|||||||
|
|
||||||
## Keychain
|
## Keychain
|
||||||
You need to have a keychain in order to run the ProtonMail Bridge. On Mac or
|
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
|
Windows, Bridge uses native credential managers. On Linux, use `secret-service` freedesktop.org API
|
||||||
[Gnome keyring](https://wiki.gnome.org/Projects/GnomeKeyring/)
|
(e.g. [Gnome keyring](https://wiki.gnome.org/Projects/GnomeKeyring/))
|
||||||
or
|
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
|
## Environment Variables
|
||||||
|
|||||||
103
doc/updates.md
Normal file
103
doc/updates.md
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
# Update mechanism of Bridge
|
||||||
|
|
||||||
|
There are mulitple options how to change version of application:
|
||||||
|
* Automatic in-app update
|
||||||
|
* Manual in-app update
|
||||||
|
* Manual install
|
||||||
|
|
||||||
|
In-app update ends with restarting bridge into new version. Automatic in-app
|
||||||
|
update is downloading, verifying and installing the new version immediatelly
|
||||||
|
without user confirmation. For manual in-app update user needs to confirm first.
|
||||||
|
Update is done from special update file published on website.
|
||||||
|
|
||||||
|
The manual installation requires user to download, verify and install manually
|
||||||
|
using installer for given OS.
|
||||||
|
|
||||||
|
The bridge is installed and executed differently for given OS:
|
||||||
|
|
||||||
|
* Windows and Linux apps are using launcher mechanism:
|
||||||
|
* There is system protected installation path which is created on first
|
||||||
|
install. It contains bridge exe and launcher exe. When users starts
|
||||||
|
bridge the launcher is executed first. It will check update path compare
|
||||||
|
version with installed one. The newer version then is then executed.
|
||||||
|
* Update mechanism means to replace files in update folder which is located
|
||||||
|
in user space.
|
||||||
|
|
||||||
|
* macOS app does not use launcher
|
||||||
|
* No launcher, only one executable
|
||||||
|
* In-App udpate replaces the bridge files in installation path directly
|
||||||
|
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph Frontend
|
||||||
|
U[User requests<br>version check]
|
||||||
|
ManIns((Notify user about<br>manual install<br>is needed))
|
||||||
|
R((Notify user<br>about restart))
|
||||||
|
ManUp((Notify user about<br>manual update))
|
||||||
|
NF((Notify user about<br>force update))
|
||||||
|
|
||||||
|
ManUp -->|Install| InstFront[Install]
|
||||||
|
InstFront -->|Ok| R
|
||||||
|
InstFront -->|Error| ManIns
|
||||||
|
|
||||||
|
U --> CheckFront[Check online]
|
||||||
|
CheckFront -->|Ok| IAFront{Is new version<br>and applicable?}
|
||||||
|
CheckFront -->|Error| ManIns
|
||||||
|
|
||||||
|
IAFront -->|No| Latest((Notify user<br>has latest version))
|
||||||
|
IAFront -->|Yes| CanInstall{Can update?}
|
||||||
|
CanInstall -->|No| ManIns
|
||||||
|
CanInstall -->|Yes| NotifOrInstall{Is automatic<br>update enabled?}
|
||||||
|
NotifOrInstall -->|Manual| ManUp
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
subgraph Backend
|
||||||
|
W[Wait for next check]
|
||||||
|
|
||||||
|
W --> Check[Check online]
|
||||||
|
|
||||||
|
Check --> NV{Has new<br>version?}
|
||||||
|
Check -->|Error| W
|
||||||
|
NV -->|No new version| W
|
||||||
|
IA{Is install<br>applicable?}
|
||||||
|
NV -->|New version<br>available| IA
|
||||||
|
IA -->|Local rollout<br>not enough| W
|
||||||
|
IA -->|Yes| AU{Is automatic\nupdate enabled?}
|
||||||
|
|
||||||
|
AU -->|Yes| CanUp{Can update?}
|
||||||
|
CanUp -->|No| ManIns
|
||||||
|
|
||||||
|
CanUp -->|Yes| Ins[Install]
|
||||||
|
Ins -->|Error| ManIns
|
||||||
|
Ins -->|Ok| R
|
||||||
|
|
||||||
|
AU -->|No| ManUp
|
||||||
|
ManUp -->|Ignore| W
|
||||||
|
|
||||||
|
|
||||||
|
F[Force update]
|
||||||
|
F --> NF
|
||||||
|
end
|
||||||
|
|
||||||
|
ManIns --> Web[Open web page]
|
||||||
|
NF --> Web
|
||||||
|
ManUp --> Web
|
||||||
|
R --> Re[Restart]
|
||||||
|
NF --> Q[Quit bridge]
|
||||||
|
NotifOrInstall -->|Automatic| W
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The non-trivial is to combine the update with setting change:
|
||||||
|
* turn off/on automatic in-app updates
|
||||||
|
* change from stable to beta or back
|
||||||
|
|
||||||
|
_TODO fill flow chart details_
|
||||||
|
|
||||||
|
|
||||||
|
We are not support downgrade functionality. Only some circumstances can lead to
|
||||||
|
downgrading the app version.
|
||||||
|
|
||||||
|
_TODO fill flow chart details_
|
||||||
6
go.mod
6
go.mod
@ -43,15 +43,18 @@ require (
|
|||||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||||
github.com/getsentry/sentry-go v0.12.0
|
github.com/getsentry/sentry-go v0.12.0
|
||||||
github.com/go-resty/resty/v2 v2.6.0
|
github.com/go-resty/resty/v2 v2.6.0
|
||||||
|
github.com/godbus/dbus v4.1.0+incompatible
|
||||||
github.com/golang/mock v1.4.4
|
github.com/golang/mock v1.4.4
|
||||||
github.com/google/go-cmp v0.5.5
|
github.com/google/go-cmp v0.5.5
|
||||||
github.com/google/uuid v1.1.1
|
github.com/google/uuid v1.1.1
|
||||||
github.com/hashicorp/go-multierror v1.1.0
|
github.com/hashicorp/go-multierror v1.1.0
|
||||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7
|
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
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||||
github.com/miekg/dns v1.1.41
|
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/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
|
||||||
github.com/olekukonko/tablewriter v0.0.4 // indirect
|
github.com/olekukonko/tablewriter v0.0.4 // indirect
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
@ -77,4 +80,5 @@ replace (
|
|||||||
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac
|
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac
|
||||||
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753
|
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753
|
||||||
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57
|
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57
|
||||||
|
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe
|
||||||
)
|
)
|
||||||
|
|||||||
6
go.sum
6
go.sum
@ -102,6 +102,8 @@ github.com/cucumber/godog v0.12.1/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6T
|
|||||||
github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
|
github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
|
||||||
github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY=
|
github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY=
|
||||||
github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
|
github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
|
||||||
|
github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe h1:KRj3wdvA9yE92prNmOjS7x5DOqoyjxqdE30qnrmTasc=
|
||||||
|
github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY=
|
||||||
github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
|
github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
|
||||||
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
|
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@ -164,6 +166,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
|
|||||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
|
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||||
|
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
@ -265,6 +269,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/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 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-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/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/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
|||||||
@ -108,10 +108,6 @@ func New(
|
|||||||
if err := b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(constants.Version))); err != nil {
|
if err := b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(constants.Version))); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to send metric")
|
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)
|
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.
|
// ReportBug reports a new bug from the user.
|
||||||
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string, attachLogs bool) error {
|
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string, attachLogs bool) error {
|
||||||
|
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{
|
report := pmapi.ReportBugReq{
|
||||||
OS: osType,
|
OS: osType,
|
||||||
OSVersion: osVersion,
|
OSVersion: osVersion,
|
||||||
|
|||||||
@ -101,10 +101,10 @@ func (f *frontendCLI) notifyNeedUpgrade() {
|
|||||||
f.Println("Please download and install the newest version of application from", version.LandingPage)
|
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.
|
// Print in 80-column width.
|
||||||
f.Println("ProtonMail Bridge is not able to detect a supported password manager")
|
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.")
|
f.Println("and restart the application.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -133,7 +133,7 @@ QtObject {
|
|||||||
return Qt.point(_x, _y)
|
return Qt.point(_x, _y)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fir to the right
|
// fit to the right
|
||||||
_x = iconRect.right
|
_x = iconRect.right
|
||||||
if (isInInterval(_x, screenRect.left, screenRect.right - width)) {
|
if (isInInterval(_x, screenRect.left, screenRect.right - width)) {
|
||||||
// position preferebly in the vertical center but bound to the screen rect
|
// position preferebly in the vertical center but bound to the screen rect
|
||||||
|
|||||||
@ -55,6 +55,26 @@ Window {
|
|||||||
function getCursorPos() {
|
function getCursorPos() {
|
||||||
return BridgePreview.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() {
|
function quit() {
|
||||||
if (bridge !== undefined && bridge !== null) {
|
if (bridge !== undefined && bridge !== null) {
|
||||||
bridge.destroy()
|
bridge.destroy()
|
||||||
@ -367,18 +387,7 @@ Window {
|
|||||||
|
|
||||||
text: "Open Bridge"
|
text: "Open Bridge"
|
||||||
enabled: bridge === undefined || bridge === null
|
enabled: bridge === undefined || bridge === null
|
||||||
onClicked: {
|
onClicked: root.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 } )
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
@ -589,7 +598,15 @@ Window {
|
|||||||
text: "No keychain"
|
text: "No keychain"
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.hasNoKeychain()
|
root.notifyHasNoKeychain()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "Rebuild keychain"
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
onClicked: {
|
||||||
|
root.notifyRebuildKeychain()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -712,6 +729,9 @@ Window {
|
|||||||
console.log("check updates")
|
console.log("check updates")
|
||||||
}
|
}
|
||||||
signal checkUpdatesFinished()
|
signal checkUpdatesFinished()
|
||||||
|
function installUpdate() {
|
||||||
|
console.log("manuall install update triggered")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
property bool isDiskCacheEnabled: true
|
property bool isDiskCacheEnabled: true
|
||||||
@ -731,7 +751,19 @@ Window {
|
|||||||
property bool isAutomaticUpdateOn : true
|
property bool isAutomaticUpdateOn : true
|
||||||
function toggleAutomaticUpdate(makeItActive) {
|
function toggleAutomaticUpdate(makeItActive) {
|
||||||
console.debug("-> silent updates", makeItActive, root.isAutomaticUpdateOn)
|
console.debug("-> silent updates", makeItActive, root.isAutomaticUpdateOn)
|
||||||
root.isAutomaticUpdateOn = makeItActive
|
var callback = function () {
|
||||||
|
root.isAutomaticUpdateOn = makeItActive;
|
||||||
|
console.debug("-> CHANGED silent updates", makeItActive, root.isAutomaticUpdateOn)
|
||||||
|
}
|
||||||
|
atimer.onTriggered.connect(callback)
|
||||||
|
atimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: atimer
|
||||||
|
interval: 2000
|
||||||
|
running: false
|
||||||
|
repeat: false
|
||||||
}
|
}
|
||||||
|
|
||||||
property bool isAutostartOn : true // Example of settings with loading state
|
property bool isAutostartOn : true // Example of settings with loading state
|
||||||
@ -815,7 +847,8 @@ Window {
|
|||||||
root.changeKeychainFinished()
|
root.changeKeychainFinished()
|
||||||
}
|
}
|
||||||
signal changeKeychainFinished()
|
signal changeKeychainFinished()
|
||||||
signal hasNoKeychain()
|
signal notifyHasNoKeychain()
|
||||||
|
signal notifyRebuildKeychain()
|
||||||
|
|
||||||
signal noActiveKeyForRecipient(string email)
|
signal noActiveKeyForRecipient(string email)
|
||||||
signal showMainWindow()
|
signal showMainWindow()
|
||||||
|
|||||||
@ -127,19 +127,10 @@ SettingsView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TextEdit {
|
TextEdit {
|
||||||
text: {
|
text: qsTr("Reports are not end-to-end encrypted, please do not send any sensitive information.")
|
||||||
var address = "bridge@protonmail.com"
|
|
||||||
var mailTo = `<a href="mailto://${address}">${address}</a>`
|
|
||||||
return "<style>a:link { color: " + root.colorScheme.interaction_norm + "; }</style>" +qsTr(
|
|
||||||
"These reports are not end-to-end encrypted. In case of sensitive information, contact us at %1."
|
|
||||||
).arg(mailTo)
|
|
||||||
}
|
|
||||||
onLinkActivated: Qt.openUrlExternally(link)
|
|
||||||
textFormat: Text.RichText
|
|
||||||
|
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: root.colorScheme.text_weak
|
color: root.colorScheme.text_weak
|
||||||
font.family: ProtonStyle.font_family
|
font.family: ProtonStyle.font_family
|
||||||
@ -181,7 +172,7 @@ SettingsView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isValidEmail(text){
|
function isValidEmail(text){
|
||||||
var reEmail = /\w+@\w+\.\w+/
|
var reEmail = /^[^@]+@[^@]+\.[A-Za-z]+\s*$/
|
||||||
return reEmail.test(text)
|
return reEmail.test(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -182,6 +182,7 @@ ApplicationWindow {
|
|||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notifications: root.notifications
|
notifications: root.notifications
|
||||||
mainWindow: root
|
mainWindow: root
|
||||||
|
backend: root.backend
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLocalCacheSettings() { contentWrapper.showLocalCacheSettings() }
|
function showLocalCacheSettings() { contentWrapper.showLocalCacheSettings() }
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import Notifications 1.0
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
property var backend
|
||||||
|
|
||||||
property ColorScheme colorScheme
|
property ColorScheme colorScheme
|
||||||
property var notifications
|
property var notifications
|
||||||
@ -51,8 +52,11 @@ Item {
|
|||||||
notification: root.notifications.updateManualReady
|
notification: root.notifications.updateManualReady
|
||||||
|
|
||||||
Switch {
|
Switch {
|
||||||
|
id:autoUpdate
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Update automatically in the future")
|
text: qsTr("Update automatically in the future")
|
||||||
|
checked: root.backend.isAutomaticUpdateOn
|
||||||
|
onClicked: root.backend.toggleAutomaticUpdate(autoUpdate.checked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,4 +119,9 @@ Item {
|
|||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.noKeychain
|
notification: root.notifications.noKeychain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotificationDialog {
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
notification: root.notifications.rebuildKeychain
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,7 +73,8 @@ QtObject {
|
|||||||
root.enableLocalCache,
|
root.enableLocalCache,
|
||||||
root.resetBridge,
|
root.resetBridge,
|
||||||
root.deleteAccount,
|
root.deleteAccount,
|
||||||
root.noKeychain
|
root.noKeychain,
|
||||||
|
root.rebuildKeychain
|
||||||
]
|
]
|
||||||
|
|
||||||
// Connection
|
// Connection
|
||||||
@ -165,8 +166,9 @@ QtObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property Notification updateManualError: Notification {
|
property Notification updateManualError: Notification {
|
||||||
description: qsTr("Bridge couldn’t update")
|
title: qsTr("Bridge couldn’t update")
|
||||||
brief: description
|
brief: title
|
||||||
|
description: qsTr("Please follow manual installation in order to update Bridge.")
|
||||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||||
type: Notification.NotificationType.Warning
|
type: Notification.NotificationType.Warning
|
||||||
group: Notifications.Group.Update
|
group: Notifications.Group.Update
|
||||||
@ -191,7 +193,7 @@ QtObject {
|
|||||||
text: qsTr("Remind me later")
|
text: qsTr("Remind me later")
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
root.updateManualReady.active = false
|
root.updateManualError.active = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -272,7 +274,7 @@ QtObject {
|
|||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
root.backend.quit()
|
root.backend.quit()
|
||||||
root.updateForce.active = false
|
root.updateForceError.active = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -870,7 +872,7 @@ QtObject {
|
|||||||
|
|
||||||
property Notification noKeychain: Notification {
|
property Notification noKeychain: Notification {
|
||||||
title: qsTr("No keychain available")
|
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
|
brief: title
|
||||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||||
type: Notification.NotificationType.Danger
|
type: Notification.NotificationType.Danger
|
||||||
@ -879,7 +881,7 @@ QtObject {
|
|||||||
Connections {
|
Connections {
|
||||||
target: root.backend
|
target: root.backend
|
||||||
|
|
||||||
onHasNoKeychain: {
|
onNotifyHasNoKeychain: {
|
||||||
root.noKeychain.active = true
|
root.noKeychain.active = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -891,6 +893,45 @@ QtObject {
|
|||||||
onTriggered: {
|
onTriggered: {
|
||||||
root.backend.quit()
|
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
|
log *logrus.Entry
|
||||||
initializing sync.WaitGroup
|
initializing sync.WaitGroup
|
||||||
initializationDone sync.Once
|
initializationDone sync.Once
|
||||||
|
firstTimeAutostart sync.Once
|
||||||
|
|
||||||
app *widgets.QApplication
|
app *widgets.QApplication
|
||||||
engine *qml.QQmlEngine
|
engine *qml.QQmlEngine
|
||||||
@ -152,7 +153,7 @@ func (f *FrontendQt) NotifySilentUpdateInstalled() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FrontendQt) NotifySilentUpdateError(err error) {
|
func (f *FrontendQt) NotifySilentUpdateError(err error) {
|
||||||
f.log.WithError(err).Warn("Update failed, asking for manual.")
|
f.log.WithError(err).Warn("In-app update failed, asking for manual.")
|
||||||
f.qml.UpdateManualError()
|
f.qml.UpdateManualError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/pkg/keychain"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (f *FrontendQt) watchEvents() {
|
func (f *FrontendQt) watchEvents() {
|
||||||
@ -64,7 +65,11 @@ func (f *FrontendQt) watchEvents() {
|
|||||||
if strings.Contains(errorDetails, "SMTP failed") {
|
if strings.Contains(errorDetails, "SMTP failed") {
|
||||||
f.qml.PortIssueSMTP()
|
f.qml.PortIssueSMTP()
|
||||||
}
|
}
|
||||||
case <-credentialsErrorCh:
|
case reason := <-credentialsErrorCh:
|
||||||
|
if reason == keychain.ErrMacKeychainRebuild.Error() {
|
||||||
|
f.qml.NotifyRebuildKeychain()
|
||||||
|
continue
|
||||||
|
}
|
||||||
f.qml.NotifyHasNoKeychain()
|
f.qml.NotifyHasNoKeychain()
|
||||||
case email := <-noActiveKeyForRecipientCh:
|
case email := <-noActiveKeyForRecipientCh:
|
||||||
f.qml.NoActiveKeyForRecipient(email)
|
f.qml.NoActiveKeyForRecipient(email)
|
||||||
|
|||||||
@ -53,7 +53,7 @@ func (f *FrontendQt) reportBug(description, address, emailClient string, include
|
|||||||
core.QSysInfo_ProductType(),
|
core.QSysInfo_ProductType(),
|
||||||
core.QSysInfo_PrettyProductName(),
|
core.QSysInfo_PrettyProductName(),
|
||||||
description,
|
description,
|
||||||
"Unknown account",
|
address,
|
||||||
address,
|
address,
|
||||||
emailClient,
|
emailClient,
|
||||||
includeLogs,
|
includeLogs,
|
||||||
|
|||||||
@ -44,6 +44,8 @@ func (f *FrontendQt) initiateQtApplication() error {
|
|||||||
|
|
||||||
core.QCoreApplication_SetApplicationName(f.programName)
|
core.QCoreApplication_SetApplicationName(f.programName)
|
||||||
core.QCoreApplication_SetApplicationVersion(f.programVersion)
|
core.QCoreApplication_SetApplicationVersion(f.programVersion)
|
||||||
|
core.QCoreApplication_SetOrganizationName("Proton AG")
|
||||||
|
core.QCoreApplication_SetOrganizationDomain("proton.ch")
|
||||||
|
|
||||||
// High DPI scaling for windows.
|
// High DPI scaling for windows.
|
||||||
core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, false)
|
core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, false)
|
||||||
|
|||||||
@ -76,6 +76,15 @@ func (f *FrontendQt) changeLocalCache(enableDiskCache bool, diskCachePath *core.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FrontendQt) setIsAutostartOn() {
|
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())
|
f.qml.SetIsAutostartOn(f.bridge.IsAutostartEnabled())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var checkingUpdates = sync.Mutex{}
|
var checkingUpdates = sync.Mutex{}
|
||||||
@ -62,9 +63,18 @@ func (f *FrontendQt) checkUpdatesAndNotify(isRequestFromUser bool) {
|
|||||||
|
|
||||||
if !f.updater.CanInstall(f.newVersionInfo) {
|
if !f.updater.CanInstall(f.newVersionInfo) {
|
||||||
f.log.Debug("A manual update is required")
|
f.log.Debug("A manual update is required")
|
||||||
f.qml.UpdateManualReady(f.newVersionInfo.Version.String())
|
f.qml.UpdateManualError()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if f.settings.GetBool(settings.AutoUpdateKey) {
|
||||||
|
// NOOP will update eventually
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isRequestFromUser {
|
||||||
|
f.qml.UpdateManualReady(f.newVersionInfo.Version.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FrontendQt) updateForce() {
|
func (f *FrontendQt) updateForce() {
|
||||||
@ -113,3 +123,26 @@ func (f *FrontendQt) toggleBeta(makeItEnabled bool) {
|
|||||||
// Immediately check the updates to set the correct landing page link.
|
// Immediately check the updates to set the correct landing page link.
|
||||||
f.checkUpdates()
|
f.checkUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FrontendQt) installUpdate() {
|
||||||
|
checkingUpdates.Lock()
|
||||||
|
defer checkingUpdates.Unlock()
|
||||||
|
|
||||||
|
if !f.updater.CanInstall(f.newVersionInfo) {
|
||||||
|
f.log.Warning("Skipping update installation, current version too old")
|
||||||
|
f.qml.UpdateManualError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.updater.InstallUpdate(f.newVersionInfo); err != nil {
|
||||||
|
if errors.Cause(err) == updater.ErrDownloadVerify {
|
||||||
|
f.log.WithError(err).Warning("Skipping update installation due to temporary error")
|
||||||
|
} else {
|
||||||
|
f.log.WithError(err).Error("The update couldn't be installed")
|
||||||
|
f.qml.UpdateManualError()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f.qml.UpdateSilentRestartNeeded()
|
||||||
|
}
|
||||||
|
|||||||
@ -81,6 +81,7 @@ type QMLBackend struct {
|
|||||||
_ func() `signal:"updateIsLatestVersion"`
|
_ func() `signal:"updateIsLatestVersion"`
|
||||||
_ func() `slot:"checkUpdates"`
|
_ func() `slot:"checkUpdates"`
|
||||||
_ func() `signal:"checkUpdatesFinished"`
|
_ func() `signal:"checkUpdatesFinished"`
|
||||||
|
_ func() `slot:"installUpdate"`
|
||||||
|
|
||||||
_ bool `property:"isDiskCacheEnabled"`
|
_ bool `property:"isDiskCacheEnabled"`
|
||||||
_ core.QUrl `property:"diskCachePath"`
|
_ core.QUrl `property:"diskCachePath"`
|
||||||
@ -142,6 +143,7 @@ type QMLBackend struct {
|
|||||||
_ func(keychain string) `slot:"changeKeychain"`
|
_ func(keychain string) `slot:"changeKeychain"`
|
||||||
_ func() `signal:"changeKeychainFinished"`
|
_ func() `signal:"changeKeychainFinished"`
|
||||||
_ func() `signal:"notifyHasNoKeychain"`
|
_ func() `signal:"notifyHasNoKeychain"`
|
||||||
|
_ func() `signal:"notifyRebuildKeychain"`
|
||||||
|
|
||||||
_ func(email string) `signal:noActiveKeyForRecipient`
|
_ func(email string) `signal:noActiveKeyForRecipient`
|
||||||
_ func() `signal:showMainWindow`
|
_ func() `signal:showMainWindow`
|
||||||
@ -212,6 +214,13 @@ func (q *QMLBackend) setup(f *FrontendQt) {
|
|||||||
}()
|
}()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
q.ConnectInstallUpdate(func() {
|
||||||
|
go func() {
|
||||||
|
defer f.panicHandler.HandlePanic()
|
||||||
|
f.installUpdate()
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
f.setIsDiskCacheEnabled()
|
f.setIsDiskCacheEnabled()
|
||||||
f.setDiskCachePath()
|
f.setDiskCachePath()
|
||||||
q.ConnectChangeLocalCache(func(e bool, d *core.QUrl) {
|
q.ConnectChangeLocalCache(func(e bool, d *core.QUrl) {
|
||||||
|
|||||||
@ -194,7 +194,7 @@ func (store *Store) BuildAndCacheMessage(ctx context.Context, messageID string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (store *Store) checkAndRemoveDeletedMessage(err error, msgID string) {
|
func (store *Store) checkAndRemoveDeletedMessage(err error, msgID string) {
|
||||||
if _, ok := err.(pmapi.ErrUnprocessableEntity); !ok {
|
if !pmapi.IsUnprocessableEntity(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l := store.log.WithError(err).WithField("msgID", msgID)
|
l := store.log.WithError(err).WithField("msgID", msgID)
|
||||||
|
|||||||
@ -127,6 +127,6 @@ func TestCooldownNotSooner(t *testing.T) {
|
|||||||
assert.True(t, testCooldown.isTooSoon())
|
assert.True(t, testCooldown.isTooSoon())
|
||||||
|
|
||||||
// After given wait time it shouldn't be soon anymore.
|
// After given wait time it shouldn't be soon anymore.
|
||||||
time.Sleep(waitTime / 2)
|
time.Sleep(waitTime/2 + time.Millisecond)
|
||||||
assert.False(t, testCooldown.isTooSoon())
|
assert.False(t, testCooldown.isTooSoon())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
// 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")
|
l.WithError(err).WithField("errors", loop.errCounter).Error("Error skipped")
|
||||||
loop.errCounter++
|
loop.errCounter++
|
||||||
if loop.errCounter == errMaxSentry {
|
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...")
|
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 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")
|
msgLog.WithError(err).Warn("Skipping message update because message exists neither in local DB nor on API")
|
||||||
err = nil
|
err = nil
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -69,6 +69,7 @@ func newUser(
|
|||||||
|
|
||||||
creds, err := credStorer.Get(userID)
|
creds, err := credStorer.Get(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
notifyKeychainRepair(eventListener, err)
|
||||||
return nil, nil, errors.Wrap(err, "failed to load user credentials")
|
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)
|
creds, err := u.credStorer.UpdateToken(u.userID, auth.UID, auth.RefreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
notifyKeychainRepair(u.listener, err)
|
||||||
u.log.WithError(err).Error("Failed to update refresh token in credentials store")
|
u.log.WithError(err).Error("Failed to update refresh token in credentials store")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -223,7 +225,7 @@ func (u *User) UpdateSpace(apiUser *pmapi.User) {
|
|||||||
// values from client.CurrentUser()
|
// values from client.CurrentUser()
|
||||||
if apiUser == nil {
|
if apiUser == nil {
|
||||||
var err error
|
var err error
|
||||||
apiUser, err = u.client.GetUser(pmapi.ContextWithoutRetry(context.Background()))
|
apiUser, err = u.GetClient().GetUser(pmapi.ContextWithoutRetry(context.Background()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.log.WithError(err).Warning("Cannot update user space")
|
u.log.WithError(err).Warning("Cannot update user space")
|
||||||
return
|
return
|
||||||
@ -280,16 +282,21 @@ func (u *User) unlockIfNecessary() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch errors.Cause(err) {
|
if pmapi.IsFailedAuth(err) || pmapi.IsFailedUnlock(err) {
|
||||||
case pmapi.ErrNoConnection, pmapi.ErrUpgradeApplication:
|
|
||||||
u.log.WithError(err).Warn("Could not unlock user")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if logoutErr := u.logout(); logoutErr != nil {
|
if logoutErr := u.logout(); logoutErr != nil {
|
||||||
u.log.WithError(logoutErr).Warn("Could not logout user")
|
u.log.WithError(logoutErr).Warn("Could not logout user")
|
||||||
}
|
}
|
||||||
return errors.Wrap(err, "failed to unlock user")
|
return errors.Wrap(err, "failed to unlock 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 nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsCombinedAddressMode returns whether user is set in combined or split mode.
|
// 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)
|
return u.store.GetAddressID(address)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if u.client == nil {
|
||||||
|
return "", errors.New("bridge account is not fully connected to server")
|
||||||
|
}
|
||||||
|
|
||||||
addresses := u.client.Addresses()
|
addresses := u.client.Addresses()
|
||||||
pmapiAddress := addresses.ByEmail(address)
|
pmapiAddress := addresses.ByEmail(address)
|
||||||
if pmapiAddress != nil {
|
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())
|
creds, err := u.credStorer.UpdateEmails(u.userID, u.client.Addresses().ActiveEmails())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
notifyKeychainRepair(u.listener, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -436,6 +448,7 @@ func (u *User) SwitchAddressMode() error {
|
|||||||
|
|
||||||
creds, err := u.credStorer.SwitchAddressMode(u.userID)
|
creds, err := u.credStorer.SwitchAddressMode(u.userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
notifyKeychainRepair(u.listener, err)
|
||||||
return errors.Wrap(err, "could not switch credentials store address mode")
|
return errors.Wrap(err, "could not switch credentials store address mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -473,15 +486,19 @@ func (u *User) Logout() error {
|
|||||||
return nil
|
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")
|
u.log.WithError(err).Warn("Failed to delete auth")
|
||||||
}
|
}
|
||||||
|
|
||||||
creds, err := u.credStorer.Logout(u.userID)
|
creds, err := u.credStorer.Logout(u.userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
notifyKeychainRepair(u.listener, err)
|
||||||
u.log.WithError(err).Warn("Could not log user out from credentials store")
|
u.log.WithError(err).Warn("Could not log user out from credentials store")
|
||||||
|
|
||||||
if err := u.credStorer.Delete(u.userID); err != nil {
|
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")
|
u.log.WithError(err).Error("Could not delete user from credentials store")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -170,7 +170,7 @@ func TestCheckBridgeLoginLoggedOut(t *testing.T) {
|
|||||||
// Mock init of user.
|
// Mock init of user.
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil),
|
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil),
|
||||||
m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any()),
|
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),
|
m.pmapiClient.EXPECT().Addresses().Return(nil),
|
||||||
|
|
||||||
// Mock CheckBridgeLogin.
|
// Mock CheckBridgeLogin.
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||||
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
r "github.com/stretchr/testify/require"
|
r "github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -46,7 +47,7 @@ func TestNewUserUnlockFails(t *testing.T) {
|
|||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
|
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
|
||||||
m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any()),
|
m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any()),
|
||||||
m.pmapiClient.EXPECT().IsUnlocked().Return(false),
|
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.
|
// Handle of unlock error.
|
||||||
m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil),
|
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/events"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/metrics"
|
"github.com/ProtonMail/proton-bridge/internal/metrics"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
"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/listener"
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
@ -130,6 +131,7 @@ func (u *Users) loadUsersFromCredentialsStore() error {
|
|||||||
|
|
||||||
userIDs, err := u.credStorer.List()
|
userIDs, err := u.credStorer.List()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
notifyKeychainRepair(u.events, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,14 +180,17 @@ func (u *Users) loadConnectedUser(ctx context.Context, user *User, creds *creden
|
|||||||
return connectErr
|
return connectErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pmapi.IsFailedAuth(connectErr) {
|
||||||
if logoutErr := user.logout(); logoutErr != nil {
|
if logoutErr := user.logout(); logoutErr != nil {
|
||||||
logrus.WithError(logoutErr).Warn("Could not logout user")
|
logrus.WithError(logoutErr).Warn("Could not logout user")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return errors.Wrap(err, "could not refresh token")
|
return errors.Wrap(err, "could not refresh token")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the user's credentials with the latest auth used to connect this user.
|
// 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 {
|
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")
|
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.
|
// 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 {
|
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")
|
return nil, errors.Wrap(err, "failed to load user credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the password in case the user changed it.
|
// Update the password in case the user changed it.
|
||||||
creds, err := u.credStorer.UpdatePassword(apiUser.ID, passphrase)
|
creds, err := u.credStorer.UpdatePassword(apiUser.ID, passphrase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
notifyKeychainRepair(u.events, err)
|
||||||
return nil, errors.Wrap(err, "failed to update password of user in credentials store")
|
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()
|
defer u.lock.Unlock()
|
||||||
|
|
||||||
if _, err := u.credStorer.Add(apiUser.ID, apiUser.Name, auth.UID, auth.RefreshToken, passphrase, client.Addresses().ActiveEmails()); err != nil {
|
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")
|
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 {
|
if err := u.credStorer.Delete(userID); err != nil {
|
||||||
|
notifyKeychainRepair(u.events, err)
|
||||||
log.WithError(err).Error("Cannot remove user")
|
log.WithError(err).Error("Cannot remove user")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -441,3 +450,9 @@ func (u *Users) crashBandicoot(username string) {
|
|||||||
panic("Your wish is my command… I crash!")
|
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/events"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||||
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
r "github.com/stretchr/testify/require"
|
r "github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -80,11 +81,11 @@ func TestNewUsersWithConnectedUserWithBadToken(t *testing.T) {
|
|||||||
m := initMocks(t)
|
m := initMocks(t)
|
||||||
defer m.ctrl.Finish()
|
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.clientManager.EXPECT().NewClient("uid", "", "acc", time.Time{}).Return(m.pmapiClient)
|
||||||
m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any())
|
m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any())
|
||||||
m.pmapiClient.EXPECT().IsUnlocked().Return(false)
|
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.pmapiClient.EXPECT().AuthDelete(gomock.Any())
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
|
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.UserRefreshEvent, "user")
|
||||||
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
|
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
|
||||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
|
||||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
||||||
|
|
||||||
checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
|
checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
|
||||||
|
|||||||
@ -37,7 +37,6 @@ import (
|
|||||||
pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
|
pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
|
||||||
tests "github.com/ProtonMail/proton-bridge/test"
|
tests "github.com/ProtonMail/proton-bridge/test"
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
r "github.com/stretchr/testify/require"
|
r "github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -331,7 +330,7 @@ func mockInitDisconnectedUser(m mocks) {
|
|||||||
m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any()),
|
m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any()),
|
||||||
|
|
||||||
// Mock of store initialisation for the unauthorized user.
|
// 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),
|
m.pmapiClient.EXPECT().Addresses().Return(nil),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,16 @@ func init() { // nolint[noinit]
|
|||||||
defaultHelper = MacOSKeychain
|
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) {
|
func newMacOSHelper(url string) (credentials.Helper, error) {
|
||||||
return &macOSHelper{url: url}, nil
|
return &macOSHelper{url: url}, nil
|
||||||
}
|
}
|
||||||
@ -76,7 +86,7 @@ func (h *macOSHelper) Add(creds *credentials.Credentials) error {
|
|||||||
|
|
||||||
query := newQuery(hostURL, userID)
|
query := newQuery(hostURL, userID)
|
||||||
query.SetData([]byte(creds.Secret))
|
query.SetData([]byte(creds.Secret))
|
||||||
return keychain.AddItem(query)
|
return parseError(keychain.AddItem(query))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *macOSHelper) Delete(secretURL string) error {
|
func (h *macOSHelper) Delete(secretURL string) error {
|
||||||
@ -87,7 +97,7 @@ func (h *macOSHelper) Delete(secretURL string) error {
|
|||||||
|
|
||||||
query := newQuery(hostURL, userID)
|
query := newQuery(hostURL, userID)
|
||||||
|
|
||||||
return keychain.DeleteItem(query)
|
return parseError(keychain.DeleteItem(query))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *macOSHelper) Get(secretURL string) (string, string, error) {
|
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)
|
results, err := keychain.QueryItem(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", parseError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(results) == 0 {
|
if len(results) == 0 {
|
||||||
@ -121,7 +131,7 @@ func (h *macOSHelper) List() (map[string]string, error) {
|
|||||||
|
|
||||||
userIDs, err := keychain.GetGenericPasswordAccounts(h.url)
|
userIDs, err := keychain.GetGenericPasswordAccounts(h.url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, parseError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, userID := range userIDs {
|
for _, userID := range userIDs {
|
||||||
|
|||||||
220
pkg/keychain/helper_dbus_linux.go
Normal file
220
pkg/keychain/helper_dbus_linux.go
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
// 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 keychain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker-credential-helpers/credentials"
|
||||||
|
"github.com/godbus/dbus"
|
||||||
|
"github.com/keybase/go-keychain/secretservice"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
serverAtt = "server"
|
||||||
|
labelAtt = "label"
|
||||||
|
usernameAtt = "username"
|
||||||
|
|
||||||
|
defaulDomain = "protonmail/bridge/users/"
|
||||||
|
defaultLabel = "Docker Credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getSession() (*secretservice.SecretService, *secretservice.Session, error) {
|
||||||
|
service, err := secretservice.NewService()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := service.OpenSession(secretservice.AuthenticationDHAES)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return service, session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTimeout(f func() error) error {
|
||||||
|
err := f()
|
||||||
|
if err == secretservice.ErrPromptTimedOut {
|
||||||
|
return f()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getItems(service *secretservice.SecretService, attributes map[string]string) ([]dbus.ObjectPath, error) {
|
||||||
|
if err := unlock(service); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []dbus.ObjectPath
|
||||||
|
err := handleTimeout(func() error {
|
||||||
|
var err error
|
||||||
|
items, err = service.SearchCollection(
|
||||||
|
secretservice.DefaultCollection,
|
||||||
|
attributes,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func unlock(service *secretservice.SecretService) error {
|
||||||
|
return handleTimeout(func() error {
|
||||||
|
return service.Unlock([]dbus.ObjectPath{secretservice.DefaultCollection})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecretServiceDBusHelper is wrapper around keybase/go-keychain/secretservice
|
||||||
|
// library.
|
||||||
|
type SecretServiceDBusHelper struct{}
|
||||||
|
|
||||||
|
// Add appends credentials to the store.
|
||||||
|
func (s *SecretServiceDBusHelper) Add(creds *credentials.Credentials) error {
|
||||||
|
service, session, err := getSession()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer service.CloseSession(session)
|
||||||
|
|
||||||
|
if err := unlock(service); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := session.NewSecret([]byte(creds.Secret))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes := map[string]string{
|
||||||
|
usernameAtt: creds.Username,
|
||||||
|
serverAtt: creds.ServerURL,
|
||||||
|
labelAtt: defaultLabel,
|
||||||
|
"xdg:schema": "io.docker.Credentials",
|
||||||
|
"docker_cli": "1",
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleTimeout(func() error {
|
||||||
|
_, err = service.CreateItem(
|
||||||
|
secretservice.DefaultCollection,
|
||||||
|
secretservice.NewSecretProperties(creds.ServerURL, attributes),
|
||||||
|
secret,
|
||||||
|
secretservice.ReplaceBehaviorReplace,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes credentials from the store.
|
||||||
|
func (s *SecretServiceDBusHelper) Delete(serverURL string) error {
|
||||||
|
service, session, err := getSession()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer service.CloseSession(session)
|
||||||
|
|
||||||
|
items, err := getItems(service, map[string]string{
|
||||||
|
labelAtt: defaultLabel,
|
||||||
|
serverAtt: serverURL,
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(items) == 0 || err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleTimeout(func() error {
|
||||||
|
return service.DeleteItem(items[0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves credentials from the store.
|
||||||
|
// It returns username and secret as strings.
|
||||||
|
func (s *SecretServiceDBusHelper) Get(serverURL string) (string, string, error) {
|
||||||
|
service, session, err := getSession()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
defer service.CloseSession(session)
|
||||||
|
|
||||||
|
if err := unlock(service); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := getItems(service, map[string]string{
|
||||||
|
labelAtt: defaultLabel,
|
||||||
|
serverAtt: serverURL,
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(items) == 0 || err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
item := items[0]
|
||||||
|
|
||||||
|
attributes, err := service.GetAttributes(item)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretPlaintext []byte
|
||||||
|
err = handleTimeout(func() error {
|
||||||
|
var err error
|
||||||
|
secretPlaintext, err = service.GetSecret(item, *session)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes[usernameAtt], string(secretPlaintext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns the stored serverURLs and their associated usernames.
|
||||||
|
func (s *SecretServiceDBusHelper) List() (map[string]string, error) {
|
||||||
|
userIDByURL := make(map[string]string)
|
||||||
|
|
||||||
|
service, session, err := getSession()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer service.CloseSession(session)
|
||||||
|
|
||||||
|
items, err := getItems(service, map[string]string{labelAtt: defaultLabel})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, it := range items {
|
||||||
|
attributes, err := service.GetAttributes(it)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(attributes[serverAtt], defaulDomain) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDByURL[attributes[serverAtt]] = attributes[usernameAtt]
|
||||||
|
}
|
||||||
|
|
||||||
|
return userIDByURL, nil
|
||||||
|
}
|
||||||
@ -29,34 +29,45 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
Pass = "pass-app"
|
Pass = "pass-app"
|
||||||
GnomeKeyring = "gnome-keyring"
|
SecretService = "secret-service"
|
||||||
|
SecretServiceDBus = "secret-service-dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() { // nolint[noinit]
|
func init() { // nolint[noinit]
|
||||||
Helpers = make(map[string]helperConstructor)
|
Helpers = make(map[string]helperConstructor)
|
||||||
|
|
||||||
if _, err := exec.LookPath("pass"); err == nil {
|
if isUsable(newDBusHelper("")) {
|
||||||
|
Helpers[SecretServiceDBus] = newDBusHelper
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
Helpers[Pass] = newPassHelper
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := exec.LookPath("gnome-keyring"); err == nil {
|
defaultHelper = SecretServiceDBus
|
||||||
Helpers[GnomeKeyring] = newGnomeKeyringHelper
|
|
||||||
}
|
|
||||||
|
|
||||||
// If Pass is available, use it by default.
|
// If Pass is available, use it by default.
|
||||||
// Otherwise, if GnomeKeyring is available, use it by default.
|
// Otherwise, if SecretService is available, use it by default.
|
||||||
if _, ok := Helpers[Pass]; ok && isUsable(newPassHelper("")) {
|
if _, ok := Helpers[Pass]; ok {
|
||||||
defaultHelper = Pass
|
defaultHelper = Pass
|
||||||
} else if _, ok := Helpers[GnomeKeyring]; ok && isUsable(newGnomeKeyringHelper("")) {
|
} else if _, ok := Helpers[SecretService]; ok {
|
||||||
defaultHelper = GnomeKeyring
|
defaultHelper = SecretService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newDBusHelper(string) (credentials.Helper, error) {
|
||||||
|
return &SecretServiceDBusHelper{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func newPassHelper(string) (credentials.Helper, error) {
|
func newPassHelper(string) (credentials.Helper, error) {
|
||||||
return &pass.Pass{}, nil
|
return &pass.Pass{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGnomeKeyringHelper(string) (credentials.Helper, error) {
|
func newSecretServiceHelper(string) (credentials.Helper, error) {
|
||||||
return &secretservice.Secretservice{}, nil
|
return &secretservice.Secretservice{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,9 @@ var (
|
|||||||
// ErrNoKeychain indicates that no suitable keychain implementation could be loaded.
|
// ErrNoKeychain indicates that no suitable keychain implementation could be loaded.
|
||||||
ErrNoKeychain = errors.New("no keychain") // nolint[noglobals]
|
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 holds all discovered keychain helpers. It is populated in init().
|
||||||
Helpers map[string]helperConstructor // nolint[noglobals]
|
Helpers map[string]helperConstructor // nolint[noglobals]
|
||||||
|
|
||||||
|
|||||||
@ -34,7 +34,7 @@ var (
|
|||||||
wantOutput = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
wantOutput = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||||
testProcessSleep = 100 // ms
|
testProcessSleep = 100 // ms
|
||||||
runParallelTimeOverhead = 150 // ms
|
runParallelTimeOverhead = 150 // ms
|
||||||
windowsCIExtra = 250 // ms - estimated experimentally
|
windowsCIExtra = 500 // ms - estimated experimentally
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParallel(t *testing.T) {
|
func TestParallel(t *testing.T) {
|
||||||
|
|||||||
@ -19,11 +19,11 @@ package pmapi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Address statuses.
|
// Address statuses.
|
||||||
@ -201,7 +201,7 @@ func (c *client) unlockAddress(passphrase []byte, address *Address) error {
|
|||||||
|
|
||||||
kr, err := address.Keys.UnlockAll(passphrase, c.userKeyRing)
|
kr, err := address.Keys.UnlockAll(passphrase, c.userKeyRing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "cannot unlock address keys for "+address.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.addrKeyRing[address.ID] = kr
|
c.addrKeyRing[address.ID] = kr
|
||||||
|
|||||||
@ -51,7 +51,7 @@ type TwoFAInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (twoFAInfo TwoFAInfo) hasTwoFactor() bool {
|
func (twoFAInfo TwoFAInfo) hasTwoFactor() bool {
|
||||||
return twoFAInfo.Enabled > 0
|
return twoFAInfo.Enabled > TwoFADisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
type TwoFAStatus int
|
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)
|
auth, err := c.manager.authRefresh(ctx, c.uid, c.ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != ErrNoConnection {
|
if IsFailedAuth(err) {
|
||||||
c.sendAuthRefresh(nil)
|
c.sendAuthRefresh(nil)
|
||||||
}
|
}
|
||||||
return err
|
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"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
a "github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/require"
|
||||||
r "github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAutomaticAuthRefresh(t *testing.T) {
|
func TestAutomaticAuthRefresh(t *testing.T) {
|
||||||
var wantAuthRefresh = &AuthRefresh{
|
r := require.New(t)
|
||||||
UID: "testUID",
|
|
||||||
AccessToken: "testAcc",
|
|
||||||
RefreshToken: "testRef",
|
|
||||||
ExpiresIn: 100,
|
|
||||||
}
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
mux.HandleFunc("/auth/refresh", func(w http.ResponseWriter, r *http.Request) {
|
currentTokens := newTestRefreshToken(r)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
testUID := currentTokens.UID
|
||||||
|
testAcc := currentTokens.AccessToken
|
||||||
|
testRef := currentTokens.RefreshToken
|
||||||
|
currentTokens.ExpiresIn = 100
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(wantAuthRefresh); err != nil {
|
mux.HandleFunc("/auth/refresh", currentTokens.handleAuthRefresh)
|
||||||
panic(err)
|
mux.HandleFunc("/addresses", currentTokens.handleAuthCheckOnly)
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
mux.HandleFunc("/addresses", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
ts := httptest.NewServer(mux)
|
ts := httptest.NewServer(mux)
|
||||||
|
|
||||||
var gotAuthRefresh *AuthRefresh
|
var gotAuthRefresh *AuthRefresh
|
||||||
|
|
||||||
c := New(Config{HostURL: ts.URL}).
|
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 })
|
c.AddAuthRefreshHandler(func(auth *AuthRefresh) { gotAuthRefresh = auth })
|
||||||
|
|
||||||
// Make a request with an access token that already expired one second ago.
|
// Make a request with an access token that already expired one second ago.
|
||||||
_, err := c.GetAddresses(context.Background())
|
_, err := c.GetAddresses(context.Background())
|
||||||
r.NoError(t, err)
|
r.NoError(err)
|
||||||
|
|
||||||
|
wantAuthRefresh := currentTokens.wantAuthRefresh()
|
||||||
|
|
||||||
// The auth callback should have been called.
|
// 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
|
cl := c.(*client) //nolint[forcetypeassert] we want to panic here
|
||||||
a.Equal(t, wantAuthRefresh.AccessToken, cl.acc)
|
r.Equal(wantAuthRefresh.AccessToken, cl.acc)
|
||||||
a.Equal(t, wantAuthRefresh.RefreshToken, cl.ref)
|
r.Equal(wantAuthRefresh.RefreshToken, cl.ref)
|
||||||
a.WithinDuration(t, expiresIn(100), cl.exp, time.Second)
|
r.WithinDuration(expiresIn(100), cl.exp, time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test401AuthRefresh(t *testing.T) {
|
func Test401AuthRefresh(t *testing.T) {
|
||||||
var wantAuthRefresh = &AuthRefresh{
|
r := require.New(t)
|
||||||
UID: "testUID",
|
currentTokens := newTestRefreshToken(r)
|
||||||
AccessToken: "testAcc",
|
testUID := currentTokens.UID
|
||||||
RefreshToken: "testRef",
|
testRef := currentTokens.RefreshToken
|
||||||
}
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
mux.HandleFunc("/auth/refresh", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/auth/refresh", currentTokens.handleAuthRefresh)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
mux.HandleFunc("/addresses", currentTokens.handleAuthCheckOnly)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ts := httptest.NewServer(mux)
|
ts := httptest.NewServer(mux)
|
||||||
|
|
||||||
var gotAuthRefresh *AuthRefresh
|
var gotAuthRefresh *AuthRefresh
|
||||||
|
|
||||||
// Create a new client.
|
// Create a new client.
|
||||||
c := New(Config{HostURL: ts.URL}).
|
m := New(Config{HostURL: ts.URL})
|
||||||
NewClient("uid", "acc", "ref", time.Now().Add(time.Hour))
|
c := m.NewClient(testUID, "oldAccToken", testRef, time.Now().Add(time.Hour))
|
||||||
|
|
||||||
// Register an auth handler.
|
// Register an auth handler.
|
||||||
c.AddAuthRefreshHandler(func(auth *AuthRefresh) { gotAuthRefresh = auth })
|
c.AddAuthRefreshHandler(func(auth *AuthRefresh) { gotAuthRefresh = auth })
|
||||||
|
|
||||||
// The first request will fail with 401, triggering a refresh and retry.
|
// The first request will fail with 401, triggering a refresh and retry.
|
||||||
_, err := c.GetAddresses(context.Background())
|
_, err := c.GetAddresses(context.Background())
|
||||||
r.NoError(t, err)
|
r.NoError(err)
|
||||||
|
|
||||||
// The auth callback should have been called.
|
// 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) {
|
func Test401RevokedAuth(t *testing.T) {
|
||||||
|
r := require.New(t)
|
||||||
|
currentTokens := newTestRefreshToken(r)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
mux.HandleFunc("/auth/refresh", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/auth/refresh", currentTokens.handleAuthRefresh)
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
mux.HandleFunc("/addresses", currentTokens.handleAuthCheckOnly)
|
||||||
})
|
|
||||||
|
|
||||||
mux.HandleFunc("/addresses", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
})
|
|
||||||
|
|
||||||
ts := httptest.NewServer(mux)
|
ts := httptest.NewServer(mux)
|
||||||
|
|
||||||
c := New(Config{HostURL: ts.URL}).
|
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 request will fail with 401, triggering a refresh.
|
||||||
// The retry will also fail with 401, returning an error.
|
// The retry will also fail with 401, returning an error.
|
||||||
_, err := c.GetAddresses(context.Background())
|
_, err := c.GetAddresses(context.Background())
|
||||||
r.EqualError(t, err, ErrUnauthorized.Error())
|
r.True(IsFailedAuth(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test401RevokedAuthTokenUpdate(t *testing.T) {
|
func Test401OldRefreshToken(t *testing.T) {
|
||||||
var oldAuth = &AuthRefresh{
|
r := require.New(t)
|
||||||
UID: "UID",
|
currentTokens := newTestRefreshToken(r)
|
||||||
AccessToken: "oldAcc",
|
|
||||||
RefreshToken: "oldRef",
|
|
||||||
ExpiresIn: 3600,
|
|
||||||
}
|
|
||||||
|
|
||||||
var newAuth = &AuthRefresh{
|
|
||||||
UID: "UID",
|
|
||||||
AccessToken: "newAcc",
|
|
||||||
RefreshToken: "newRef",
|
|
||||||
}
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
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")
|
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)
|
panic(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
mux.HandleFunc("/addresses", func(w http.ResponseWriter, r *http.Request) {
|
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)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Header.Get("Authorization") == ("Bearer " + newAuth.AccessToken) {
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
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)
|
ts := httptest.NewServer(mux)
|
||||||
|
m := New(Config{HostURL: ts.URL})
|
||||||
c := New(Config{HostURL: ts.URL}).
|
c, _, err := m.NewClientWithRefresh(context.Background(), testUID, testRef)
|
||||||
NewClient(oldAuth.UID, oldAuth.AccessToken, oldAuth.RefreshToken, time.Now().Add(time.Hour))
|
r.NoError(err)
|
||||||
|
|
||||||
// The request will fail with 401, triggering a refresh. After the refresh it should succeed.
|
// The request will fail with 401, triggering a refresh. After the refresh it should succeed.
|
||||||
_, err := c.GetAddresses(context.Background())
|
_, err = c.UpdateUser(context.Background())
|
||||||
r.NoError(t, err)
|
r.NoError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuth2FA(t *testing.T) {
|
func TestAuth2FA(t *testing.T) {
|
||||||
|
r := require.New(t)
|
||||||
twoFACode := "code"
|
twoFACode := "code"
|
||||||
|
|
||||||
finish, c := newTestClientCallbacks(t,
|
finish, c := newTestClientCallbacks(t,
|
||||||
func(tb testing.TB, w http.ResponseWriter, req *http.Request) string {
|
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
|
var twoFAreq auth2FAReq
|
||||||
r.NoError(t, json.NewDecoder(req.Body).Decode(&twoFAreq))
|
r.NoError(json.NewDecoder(req.Body).Decode(&twoFAreq))
|
||||||
r.Equal(t, twoFAreq.TwoFactorCode, twoFACode)
|
r.Equal(twoFAreq.TwoFactorCode, twoFACode)
|
||||||
|
|
||||||
return "/auth/2fa/post_response.json"
|
return "/auth/2fa/post_response.json"
|
||||||
},
|
},
|
||||||
@ -205,31 +236,33 @@ func TestAuth2FA(t *testing.T) {
|
|||||||
defer finish()
|
defer finish()
|
||||||
|
|
||||||
err := c.Auth2FA(context.Background(), twoFACode)
|
err := c.Auth2FA(context.Background(), twoFACode)
|
||||||
r.NoError(t, err)
|
r.NoError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuth2FA_Fail(t *testing.T) {
|
func TestAuth2FA_Fail(t *testing.T) {
|
||||||
|
r := require.New(t)
|
||||||
finish, c := newTestClientCallbacks(t,
|
finish, c := newTestClientCallbacks(t,
|
||||||
func(tb testing.TB, w http.ResponseWriter, req *http.Request) string {
|
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"
|
return "/auth/2fa/post_401_bad_password.json"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
defer finish()
|
defer finish()
|
||||||
|
|
||||||
err := c.Auth2FA(context.Background(), "code")
|
err := c.Auth2FA(context.Background(), "code")
|
||||||
r.Equal(t, ErrBad2FACode, err)
|
r.Equal(ErrBad2FACode, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuth2FA_Retry(t *testing.T) {
|
func TestAuth2FA_Retry(t *testing.T) {
|
||||||
|
r := require.New(t)
|
||||||
finish, c := newTestClientCallbacks(t,
|
finish, c := newTestClientCallbacks(t,
|
||||||
func(tb testing.TB, w http.ResponseWriter, req *http.Request) string {
|
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"
|
return "/auth/2fa/post_422_bad_password.json"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
defer finish()
|
defer finish()
|
||||||
|
|
||||||
err := c.Auth2FA(context.Background(), "code")
|
err := c.Auth2FA(context.Background(), "code")
|
||||||
r.Equal(t, ErrBad2FACodeTryAgain, err)
|
r.Equal(ErrBad2FACodeTryAgain, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,8 +19,6 @@ package pmapi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Unlock unlocks all the user and address keys using the given passphrase, creating user and address keyrings.
|
// 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.
|
// 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.
|
// Should only be used internally by methods that first lock the lock.
|
||||||
func (c *client) unlock(ctx context.Context, passphrase []byte) (err error) {
|
func (c *client) unlock(ctx context.Context, passphrase []byte) error {
|
||||||
if _, err = c.CurrentUser(ctx); err != nil {
|
if _, err := c.CurrentUser(ctx); err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.userKeyRing == nil {
|
if c.userKeyRing == nil {
|
||||||
if err = c.unlockUser(passphrase); err != nil {
|
if err := c.unlockUser(passphrase); err != nil {
|
||||||
return errors.Wrap(err, "failed to unlock user")
|
return ErrUnlockFailed{err}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, address := range c.addresses {
|
for _, address := range c.addresses {
|
||||||
if c.addrKeyRing[address.ID] == nil {
|
if c.addrKeyRing[address.ID] == nil {
|
||||||
if err = c.unlockAddress(passphrase, address); err != nil {
|
if err := c.unlockAddress(passphrase, address); err != nil {
|
||||||
return errors.Wrap(err, "failed to unlock address")
|
return ErrUnlockFailed{err}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) ReloadKeys(ctx context.Context, passphrase []byte) (err error) {
|
func (c *client) ReloadKeys(ctx context.Context, passphrase []byte) (err error) {
|
||||||
|
|||||||
@ -60,11 +60,16 @@ func formatAsAddress(rawURL string) string {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
host := url.Host
|
||||||
|
if host == "" {
|
||||||
|
host = url.Path
|
||||||
|
}
|
||||||
|
|
||||||
port := "443"
|
port := "443"
|
||||||
if url.Scheme == "http" {
|
if url.Scheme == "http" {
|
||||||
port = "80"
|
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.
|
// DialTLS dials the given network/address. If it fails, it retries using a proxy.
|
||||||
|
|||||||
@ -36,11 +36,16 @@ const (
|
|||||||
proxyDoHTimeout = 20 * time.Second
|
proxyDoHTimeout = 20 * time.Second
|
||||||
proxyCanReachTimeout = 20 * time.Second
|
proxyCanReachTimeout = 20 * time.Second
|
||||||
proxyQuery = "dMFYGSLTQOJXXI33ONVQWS3BOMNUA.protonpro.xyz"
|
proxyQuery = "dMFYGSLTQOJXXI33ONVQWS3BOMNUA.protonpro.xyz"
|
||||||
|
|
||||||
|
Quad9Provider = "https://dns11.quad9.net/dns-query"
|
||||||
|
Quad9PortProvider = "https://dns11.quad9.net:5053/dns-query"
|
||||||
|
GoogleProvider = "https://dns.google/dns-query"
|
||||||
)
|
)
|
||||||
|
|
||||||
var dohProviders = []string{ //nolint[gochecknoglobals]
|
var dohProviders = []string{ //nolint[gochecknoglobals]
|
||||||
"https://dns11.quad9.net/dns-query",
|
Quad9Provider,
|
||||||
"https://dns.google/dns-query",
|
Quad9PortProvider,
|
||||||
|
GoogleProvider,
|
||||||
}
|
}
|
||||||
|
|
||||||
// proxyProvider manages known proxies.
|
// proxyProvider manages known proxies.
|
||||||
|
|||||||
@ -27,12 +27,6 @@ import (
|
|||||||
"golang.org/x/net/http/httpproxy"
|
"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) {
|
func TestProxyProvider_FindProxy(t *testing.T) {
|
||||||
proxy := getTrustedServer()
|
proxy := getTrustedServer()
|
||||||
defer closeServer(proxy)
|
defer closeServer(proxy)
|
||||||
@ -142,17 +136,28 @@ func TestProxyProvider_FindProxy_CanReachTimeout(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestProxyProvider_DoHLookup_Quad9(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.NoError(t, err)
|
||||||
r.NotEmpty(t, records)
|
r.NotEmpty(t, records)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProxyProvider_DoHLookup_Google(t *testing.T) {
|
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.NoError(t, err)
|
||||||
r.NotEmpty(t, records)
|
r.NotEmpty(t, records)
|
||||||
}
|
}
|
||||||
@ -160,7 +165,7 @@ func TestProxyProvider_DoHLookup_Google(t *testing.T) {
|
|||||||
func TestProxyProvider_DoHLookup_FindProxy(t *testing.T) {
|
func TestProxyProvider_DoHLookup_FindProxy(t *testing.T) {
|
||||||
skipIfProxyIsSet(t)
|
skipIfProxyIsSet(t)
|
||||||
|
|
||||||
p := newProxyProvider(Config{}, []string{TestQuad9Provider, TestGoogleProvider}, TestDoHQuery)
|
p := newProxyProvider(Config{}, []string{Quad9Provider, GoogleProvider}, proxyQuery)
|
||||||
|
|
||||||
url, err := p.findReachableServer()
|
url, err := p.findReachableServer()
|
||||||
r.NoError(t, err)
|
r.NoError(t, err)
|
||||||
@ -170,7 +175,7 @@ func TestProxyProvider_DoHLookup_FindProxy(t *testing.T) {
|
|||||||
func TestProxyProvider_DoHLookup_FindProxyFirstProviderUnreachable(t *testing.T) {
|
func TestProxyProvider_DoHLookup_FindProxyFirstProviderUnreachable(t *testing.T) {
|
||||||
skipIfProxyIsSet(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()
|
url, err := p.findReachableServer()
|
||||||
r.NoError(t, err)
|
r.NoError(t, err)
|
||||||
|
|||||||
@ -251,3 +251,18 @@ func TestProxyDialer_UseProxy_FindSecondAlternativeIfFirstFailsAndAPIIsStillBloc
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, formatAsAddress(proxy2.URL), d.proxyAddress)
|
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")
|
ErrPasswordWrong = errors.New("wrong password")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrUnprocessableEntity ...
|
||||||
type ErrUnprocessableEntity struct {
|
type ErrUnprocessableEntity struct {
|
||||||
OriginalError error
|
OriginalError error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsUnprocessableEntity(err error) bool {
|
||||||
|
_, ok := err.(ErrUnprocessableEntity)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
func (err ErrUnprocessableEntity) Error() string {
|
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()
|
||||||
|
}
|
||||||
|
|||||||
@ -33,6 +33,7 @@ type manager struct {
|
|||||||
|
|
||||||
isDown bool
|
isDown bool
|
||||||
locker sync.Locker
|
locker sync.Locker
|
||||||
|
refreshingAuth sync.Locker
|
||||||
connectionObservers []ConnectionObserver
|
connectionObservers []ConnectionObserver
|
||||||
proxyDialer *ProxyTLSDialer
|
proxyDialer *ProxyTLSDialer
|
||||||
|
|
||||||
@ -50,6 +51,7 @@ func newManager(cfg Config) *manager {
|
|||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
rc: resty.New().EnableTrace(),
|
rc: resty.New().EnableTrace(),
|
||||||
locker: &sync.Mutex{},
|
locker: &sync.Mutex{},
|
||||||
|
refreshingAuth: &sync.Mutex{},
|
||||||
pingMutex: &sync.RWMutex{},
|
pingMutex: &sync.RWMutex{},
|
||||||
isPinging: false,
|
isPinging: false,
|
||||||
setSentryUserIDOnce: sync.Once{},
|
setSentryUserIDOnce: sync.Once{},
|
||||||
|
|||||||
@ -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) {
|
func (m *manager) authRefresh(ctx context.Context, uid, ref string) (*AuthRefresh, error) {
|
||||||
|
m.refreshingAuth.Lock()
|
||||||
|
defer m.refreshingAuth.Unlock()
|
||||||
|
|
||||||
var req = authRefreshReq{
|
var req = authRefreshReq{
|
||||||
UID: uid,
|
UID: uid,
|
||||||
RefreshToken: ref,
|
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"))
|
_, err := wrapNoConnection(m.r(ctx).SetBody(req).SetResult(&res).Post("/auth/refresh"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if IsBadRequest(err) || IsUnprocessableEntity(err) {
|
||||||
|
err = ErrAuthFailed{err}
|
||||||
|
}
|
||||||
return nil, 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 {
|
if apiErr, ok := res.Error().(*Error); ok {
|
||||||
switch {
|
switch {
|
||||||
case apiErr.Code == errCodeUpgradeApplication:
|
case apiErr.Code == errCodeUpgradeApplication:
|
||||||
err = ErrUpgradeApplication
|
|
||||||
if m.cfg.UpgradeApplicationHandler != nil {
|
if m.cfg.UpgradeApplicationHandler != nil {
|
||||||
m.cfg.UpgradeApplicationHandler()
|
m.cfg.UpgradeApplicationHandler()
|
||||||
}
|
}
|
||||||
|
return ErrUpgradeApplication
|
||||||
case apiErr.Code == errCodePasswordWrong:
|
case apiErr.Code == errCodePasswordWrong:
|
||||||
err = ErrPasswordWrong
|
return ErrPasswordWrong
|
||||||
case apiErr.Code == errCodeAuthPaidPlanRequired:
|
case apiErr.Code == errCodeAuthPaidPlanRequired:
|
||||||
err = ErrPaidPlanRequired
|
return ErrPaidPlanRequired
|
||||||
case res.StatusCode() == http.StatusUnprocessableEntity:
|
|
||||||
err = ErrUnprocessableEntity{apiErr}
|
|
||||||
default:
|
default:
|
||||||
err = apiErr
|
err = apiErr
|
||||||
}
|
}
|
||||||
@ -76,6 +74,13 @@ func (m *manager) catchAPIError(_ *resty.Client, res *resty.Response) error {
|
|||||||
err = errors.New(res.Status())
|
err = errors.New(res.Status())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch res.StatusCode() {
|
||||||
|
case http.StatusUnprocessableEntity:
|
||||||
|
err = ErrUnprocessableEntity{err}
|
||||||
|
case http.StatusBadRequest:
|
||||||
|
err = ErrBadRequest{err}
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
return 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
|
.PHONY: check-go check-godog install-godog test test-bridge test-live test-live-bridge test-stage test-debug test-live-debug bench
|
||||||
|
|
||||||
export GO111MODULE=on
|
export GO111MODULE=on
|
||||||
export BRIDGE_VERSION:=2.1.1+integrationtests
|
export BRIDGE_VERSION:=2.1.3+integrationtests
|
||||||
export VERBOSITY?=fatal
|
export VERBOSITY?=fatal
|
||||||
export TEST_DATA=testdata
|
export TEST_DATA=testdata
|
||||||
|
|
||||||
|
|||||||
@ -172,3 +172,7 @@ func (ctx *TestContext) MessagePreparationStarted(username string) {
|
|||||||
func (ctx *TestContext) MessagePreparationFinished(username string) {
|
func (ctx *TestContext) MessagePreparationFinished(username string) {
|
||||||
ctx.pmapiController.UnlockEvents(username)
|
ctx.pmapiController.UnlockEvents(username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *TestContext) CredentialsFailsOnWrite(shouldFail bool) {
|
||||||
|
ctx.credStore.(*fakeCredStore).failOnWrite = shouldFail
|
||||||
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// bridgePassword is password to be used for IMAP or SMTP under tests.
|
// bridgePassword is password to be used for IMAP or SMTP under tests.
|
||||||
@ -28,6 +29,8 @@ const bridgePassword = "bridgepassword"
|
|||||||
|
|
||||||
type fakeCredStore struct {
|
type fakeCredStore struct {
|
||||||
credentials map[string]*credentials.Credentials
|
credentials map[string]*credentials.Credentials
|
||||||
|
|
||||||
|
failOnWrite bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// newFakeCredStore returns a fake credentials store (optionally configured with the given credentials).
|
// 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) {
|
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
|
bridgePassword := bridgePassword
|
||||||
if c, ok := c.credentials[userID]; ok {
|
if c, ok := c.credentials[userID]; ok {
|
||||||
bridgePassword = c.BridgePassword
|
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) {
|
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
|
return c.credentials[userID], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeCredStore) UpdateEmails(userID string, emails []string) (*credentials.Credentials, error) {
|
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
|
return c.credentials[userID], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeCredStore) UpdatePassword(userID string, password []byte) (*credentials.Credentials, error) {
|
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)
|
creds, err := c.Get(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
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)
|
creds, err := c.Get(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
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].APIToken = ""
|
||||||
c.credentials[userID].MailboxPassword = []byte{}
|
c.credentials[userID].MailboxPassword = []byte{}
|
||||||
return c.credentials[userID], nil
|
return c.credentials[userID], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeCredStore) Delete(userID string) error {
|
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)
|
delete(c.credentials, userID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,6 +46,7 @@ type PMAPIController interface {
|
|||||||
LockEvents(username string)
|
LockEvents(username string)
|
||||||
UnlockEvents(username string)
|
UnlockEvents(username string)
|
||||||
RemoveUserMessageWithoutEvent(username, messageID string) error
|
RemoveUserMessageWithoutEvent(username, messageID string) error
|
||||||
|
RevokeSession(username string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPMAPIController(listener listener.Listener) (PMAPIController, pmapi.Manager) {
|
func newPMAPIController(listener listener.Listener) (PMAPIController, pmapi.Manager) {
|
||||||
|
|||||||
@ -250,3 +250,10 @@ func (ctl *Controller) RemoveUserMessageWithoutEvent(username string, messageID
|
|||||||
|
|
||||||
return errors.New("message not found")
|
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) {
|
func (ctl *Controller) refreshSessionIfAuthorized(uid, ref string) (*fakeSession, error) {
|
||||||
session, ok := ctl.sessionsByUID[uid]
|
session, ok := ctl.sessionsByUID[uid]
|
||||||
if !ok {
|
if !ok || session.uid != uid {
|
||||||
return nil, pmapi.ErrUnauthorized
|
return nil, pmapi.ErrAuthFailed{OriginalError: errors.New("bad uid")}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ref != session.ref {
|
if ref != session.ref {
|
||||||
return nil, pmapi.ErrUnauthorized
|
return nil, pmapi.ErrAuthFailed{OriginalError: errors.New("bad refresh token")}
|
||||||
}
|
}
|
||||||
|
|
||||||
session.ref = ctl.tokenGenerator.next("ref")
|
session.ref = ctl.tokenGenerator.next("ref")
|
||||||
|
|||||||
@ -133,14 +133,32 @@ func (api *FakePMAPI) authRefresh() error {
|
|||||||
|
|
||||||
session, err := api.controller.refreshSessionIfAuthorized(api.uid, api.ref)
|
session, err := api.controller.refreshSessionIfAuthorized(api.uid, api.ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if pmapi.IsFailedAuth(err) {
|
||||||
|
go api.handleAuth(nil)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
api.ref = session.ref
|
api.ref = session.ref
|
||||||
api.acc = session.acc
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *FakePMAPI) handleAuth(auth *pmapi.AuthRefresh) {
|
||||||
|
for _, handle := range api.authHandlers {
|
||||||
|
handle(auth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (api *FakePMAPI) setUser(username string) error {
|
func (api *FakePMAPI) setUser(username string) error {
|
||||||
api.username = username
|
api.username = username
|
||||||
api.log = api.log.WithField("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)
|
session, err := m.controller.refreshSessionIfAuthorized(uid, ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, pmapi.ErrUnauthorized
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
user, ok := m.controller.usersByUsername[session.username]
|
user, ok := m.controller.usersByUsername[session.username]
|
||||||
|
|||||||
@ -82,6 +82,10 @@ func (api *FakePMAPI) UpdateUser(context.Context) (*pmapi.User, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := api.checkAndRecordCall(GET, "/addresses", nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return api.user, nil
|
return api.user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -79,3 +79,12 @@ Feature: Start bridge
|
|||||||
And "user" does not have loaded store
|
And "user" does not have loaded store
|
||||||
And "user" does not have running event loop
|
And "user" does not have running event loop
|
||||||
And "user" has zero space
|
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
|
||||||
|
|
||||||
@ -60,3 +60,7 @@ func (ctl *Controller) GetAuthClient(username string) pmapi.Client {
|
|||||||
}
|
}
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctl *Controller) RevokeSession(username string) error {
|
||||||
|
return errors.New("revoke live session not implemented")
|
||||||
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ func UsersActionsFeatureContext(s *godog.ScenarioContext) {
|
|||||||
s.Step(`^user deletes "([^"]*)"$`, userDeletesUser)
|
s.Step(`^user deletes "([^"]*)"$`, userDeletesUser)
|
||||||
s.Step(`^user deletes "([^"]*)" with cache$`, userDeletesUserWithCache)
|
s.Step(`^user deletes "([^"]*)" with cache$`, userDeletesUserWithCache)
|
||||||
s.Step(`^"([^"]*)" swaps address "([^"]*)" with address "([^"]*)"$`, swapsAddressWithAddress)
|
s.Step(`^"([^"]*)" swaps address "([^"]*)" with address "([^"]*)"$`, swapsAddressWithAddress)
|
||||||
|
s.Step(`^session was revoked for "([^"]*)"$`, sessionRevoked)
|
||||||
}
|
}
|
||||||
|
|
||||||
func userLogsIn(bddUserID string) error {
|
func userLogsIn(bddUserID string) error {
|
||||||
@ -123,3 +124,8 @@ func swapsAddressWithAddress(bddUserID, bddAddressID1, bddAddressID2 string) err
|
|||||||
|
|
||||||
return ctx.GetPMAPIController().ReorderAddresses(account.User(), addressIDs)
|
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 database file for "([^"]*)"$`, thereIsDatabaseFileForUser)
|
||||||
s.Step(`^there is no database file for "([^"]*)"$`, thereIsNoDatabaseFileForUser)
|
s.Step(`^there is no database file for "([^"]*)"$`, thereIsNoDatabaseFileForUser)
|
||||||
s.Step(`^there is "([^"]*)" in "([^"]*)" address mode$`, thereIsUserWithAddressMode)
|
s.Step(`^there is "([^"]*)" in "([^"]*)" address mode$`, thereIsUserWithAddressMode)
|
||||||
|
s.Step(`^credentials? (?:are|is) locked$`, credentialsAreLocked)
|
||||||
}
|
}
|
||||||
|
|
||||||
func thereIsUser(bddUserID string) error {
|
func thereIsUser(bddUserID string) error {
|
||||||
@ -150,3 +151,8 @@ func thereIsUserWithAddressMode(bddUserID, wantAddressMode string) error {
|
|||||||
ctx.EventuallySyncIsFinishedForUsername(user.Username())
|
ctx.EventuallySyncIsFinishedForUsername(user.Username())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func credentialsAreLocked() error {
|
||||||
|
ctx.CredentialsFailsOnWrite(true)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user