forked from Silverfish/proton-bridge
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51eb2c42cd | |||
| c94d839fbb | |||
| de586e5f12 | |||
| c32a106898 | |||
| 5b20b6a3d0 | |||
| 3b07121f08 |
@ -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
12
Changelog.md
12
Changelog.md
@ -2,6 +2,18 @@
|
|||||||
|
|
||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
## [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
|
## [Bridge 2.1.2] London
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|||||||
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.2+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
|
||||||
|
|||||||
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_
|
||||||
4
go.mod
4
go.mod
@ -43,12 +43,13 @@ 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-20211119201326-e02f34051621
|
github.com/keybase/go-keychain v0.0.0
|
||||||
github.com/kr/text v0.2.0 // indirect
|
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
|
||||||
@ -79,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
|
||||||
)
|
)
|
||||||
|
|||||||
4
go.sum
4
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=
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -729,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
|
||||||
@ -748,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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -166,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
|
||||||
@ -192,7 +193,7 @@ QtObject {
|
|||||||
text: qsTr("Remind me later")
|
text: qsTr("Remind me later")
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
root.updateManualReady.active = false
|
root.updateManualError.active = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -273,7 +274,7 @@ QtObject {
|
|||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
root.backend.quit()
|
root.backend.quit()
|
||||||
root.updateForce.active = false
|
root.updateForceError.active = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -153,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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"`
|
||||||
@ -213,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) {
|
||||||
|
|||||||
@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@ -28,13 +28,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Pass = "pass-app"
|
Pass = "pass-app"
|
||||||
SecretService = "secret-service"
|
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 isUsable(newDBusHelper("")) {
|
||||||
|
Helpers[SecretServiceDBus] = newDBusHelper
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := exec.LookPath("gnome-keyring"); err == nil && isUsable(newSecretServiceHelper("")) {
|
if _, err := exec.LookPath("gnome-keyring"); err == nil && isUsable(newSecretServiceHelper("")) {
|
||||||
Helpers[SecretService] = newSecretServiceHelper
|
Helpers[SecretService] = newSecretServiceHelper
|
||||||
}
|
}
|
||||||
@ -43,6 +48,8 @@ func init() { // nolint[noinit]
|
|||||||
Helpers[Pass] = newPassHelper
|
Helpers[Pass] = newPassHelper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultHelper = SecretServiceDBus
|
||||||
|
|
||||||
// If Pass is available, use it by default.
|
// If Pass is available, use it by default.
|
||||||
// Otherwise, if SecretService is available, use it by default.
|
// Otherwise, if SecretService is available, use it by default.
|
||||||
if _, ok := Helpers[Pass]; ok {
|
if _, ok := Helpers[Pass]; ok {
|
||||||
@ -52,6 +59,10 @@ func init() { // nolint[noinit]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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.2+integrationtests
|
export BRIDGE_VERSION:=2.1.3+integrationtests
|
||||||
export VERBOSITY?=fatal
|
export VERBOSITY?=fatal
|
||||||
export TEST_DATA=testdata
|
export TEST_DATA=testdata
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user