forked from Silverfish/proton-bridge
Compare commits
538 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27889b8085 | |||
| 2cd7735468 | |||
| 8990f2d1d6 | |||
| 7bc608ce6c | |||
| 01c7daaba7 | |||
| 8408a5fdc0 | |||
| 828fe0e86e | |||
| 5c3179df48 | |||
| 618cb27ac1 | |||
| 83a569b366 | |||
| 70244071ea | |||
| 2bb0008eb4 | |||
| 906141e2ae | |||
| 6ac8a4c0bc | |||
| 0827d81617 | |||
| e71e56f7fe | |||
| 7510ba2541 | |||
| a78b2dee46 | |||
| 2cce1c7b2a | |||
| 7533dc952d | |||
| 3408e8427d | |||
| 5795a6a2f0 | |||
| 9f64e8a6fa | |||
| f176174fca | |||
| 2747f3b492 | |||
| 1c374b59d3 | |||
| ae7ae2886f | |||
| b902f1490f | |||
| 8e5040a357 | |||
| 9881011043 | |||
| d4b8f3e1c2 | |||
| b7fff07197 | |||
| 7fa81a7aca | |||
| e0d1e67d4b | |||
| 8049c47aa8 | |||
| 37a46465ba | |||
| ece6d7b2d7 | |||
| 3d4c73f8af | |||
| ed36273755 | |||
| b7be599769 | |||
| c3484dc062 | |||
| 578a12529c | |||
| e601245f01 | |||
| ad1fb47b0d | |||
| e852c5a22f | |||
| 61287d05bf | |||
| 555453bc1a | |||
| cb81175fa0 | |||
| 627bf25791 | |||
| 8d1015caba | |||
| f2db2b9b1d | |||
| 4b6d0d035e | |||
| 57e9310510 | |||
| fd09769ccc | |||
| 029c798eff | |||
| b81fa5ed39 | |||
| 1375f42869 | |||
| 7cb9d62f0c | |||
| 6bd8c6ceb6 | |||
| f954f89747 | |||
| 82788e39f0 | |||
| 0df4f41269 | |||
| a2ab5df7ce | |||
| 1395f1c990 | |||
| c473e987f4 | |||
| b97ffc16ea | |||
| 355ae5f046 | |||
| 1abda7555d | |||
| 520361f7f3 | |||
| c8c9e911f6 | |||
| eb62056755 | |||
| 294d1edfee | |||
| febab47124 | |||
| 8160fe5448 | |||
| 9169499087 | |||
| 81facfd05f | |||
| 054d9b3f09 | |||
| a95eb759ca | |||
| d1f140ebcb | |||
| 721cd9f319 | |||
| e6a780ebd4 | |||
| 9868fae735 | |||
| c22037462e | |||
| 1f0312573a | |||
| 46c0463e43 | |||
| e05b99a0f1 | |||
| 48dfdabaf4 | |||
| 7ed8d76d84 | |||
| 9e6cbcb35e | |||
| 8c2096e813 | |||
| 2972e1273f | |||
| 0ce0e4765b | |||
| 1517dd81e6 | |||
| b517e3cd5b | |||
| a240c4531a | |||
| 7d84ab37f6 | |||
| 6bdcdf7fd2 | |||
| 5e8e92b765 | |||
| 2006984b47 | |||
| aaa8a35ea8 | |||
| 204e320df4 | |||
| 24a0ed41b9 | |||
| 3a08c1cdb6 | |||
| 2ff5731b39 | |||
| eb2423b0ed | |||
| e60bbaa60f | |||
| 65cc1d5ccf | |||
| f17b630b12 | |||
| 50da1e4704 | |||
| 515a8689e9 | |||
| 04b30fd694 | |||
| e5095b2154 | |||
| 1e48ab4b9c | |||
| fe5e8ce7f7 | |||
| 319d51cb80 | |||
| 14cad02b5a | |||
| bc30a9db68 | |||
| c7cfcb29f6 | |||
| e087a7972e | |||
| 49b3c18903 | |||
| 4f3748a4f0 | |||
| 27cbcc6f5e | |||
| ed2d70dd15 | |||
| ae87d7b236 | |||
| 31fb878bbd | |||
| 59278913ca | |||
| 2023df3ef8 | |||
| 48cf89b1a6 | |||
| 223b14e556 | |||
| 112d79c2be | |||
| 2aec508e43 | |||
| d0b13a8684 | |||
| 28d78453b7 | |||
| 04f2dd1a0b | |||
| 8b0024d53e | |||
| 098685ec8b | |||
| 6c9293ec14 | |||
| 8cbbfb0e34 | |||
| bbbc18b959 | |||
| 5aa495b240 | |||
| c08d0eff7a | |||
| 34213d1607 | |||
| 82aa0b270c | |||
| 847d6de6bf | |||
| 739fe826b3 | |||
| 6bf67917fb | |||
| 4c4c592f31 | |||
| 8a08d146bc | |||
| da41398340 | |||
| d74873be31 | |||
| b9ffa96e8b | |||
| 8a666dc8cc | |||
| 78fc5ec458 | |||
| d093488522 | |||
| 1e29a5210f | |||
| c548ba85fe | |||
| 8bb60afabd | |||
| 8b5cb7729c | |||
| f8d7b98d05 | |||
| bc7912e8fb | |||
| 0812491f09 | |||
| af542a2fc1 | |||
| 039d1b7f99 | |||
| 4ded8784fc | |||
| 943d95a725 | |||
| 75b788b793 | |||
| 048a83c8c9 | |||
| e92badef0e | |||
| 924a423488 | |||
| 1ad821b2b7 | |||
| 62d62474fb | |||
| 1dbc9a1366 | |||
| 99745ac067 | |||
| dbfb7572a8 | |||
| a213b48f93 | |||
| b0f939bfaf | |||
| 075e1ef236 | |||
| 88ad98ed37 | |||
| 0a972285a6 | |||
| 358a2e5266 | |||
| 5bb2eeafb7 | |||
| 94f84625d4 | |||
| 34e4625b64 | |||
| a797c01943 | |||
| b72de5e3a4 | |||
| bf4afae5d9 | |||
| 29dcd5450f | |||
| f1160a11af | |||
| d9762010fa | |||
| 93d9ae32fc | |||
| cb42358e2f | |||
| 8f420d728c | |||
| df818bc2b8 | |||
| 2fbecc4675 | |||
| a553ced979 | |||
| 82987a1835 | |||
| 3c4e8730ac | |||
| d738fdff57 | |||
| d4c1016e55 | |||
| b1ce2dd73f | |||
| d066e32719 | |||
| 7f6094750e | |||
| e4c08be28e | |||
| 7e03de0a21 | |||
| d4da325e57 | |||
| 5a4f733518 | |||
| fd80848fcd | |||
| cab5ee6752 | |||
| 0bc99dbd4f | |||
| 83339da26c | |||
| 8749d5dc7d | |||
| 2bda47fcad | |||
| 85c0d6f837 | |||
| 04d9fa8f9e | |||
| 7b7a2068ea | |||
| 4a31017332 | |||
| 784896434d | |||
| d376b88cf0 | |||
| c7a5b8559c | |||
| fd0c262645 | |||
| 4f7cb43c8f | |||
| 2f40b030ec | |||
| d2b1b9d34c | |||
| b594b5f90a | |||
| 94e219137e | |||
| a26db09e54 | |||
| 351c019310 | |||
| 14fbdb5e04 | |||
| dabc9717d1 | |||
| 5adbf74cbe | |||
| 3e54885ea0 | |||
| 247e676b41 | |||
| cb04dabea8 | |||
| 7df2d70dbf | |||
| 350544e801 | |||
| d6260d960c | |||
| d0fb3509cc | |||
| 798cd5caf3 | |||
| 35fa43f47c | |||
| 4df8ce1b58 | |||
| 83c7396f2d | |||
| 709922c383 | |||
| 16978b8949 | |||
| 7745b68228 | |||
| 036a416a25 | |||
| c9808d07df | |||
| 8e34b51c77 | |||
| afc5307a23 | |||
| 1919610793 | |||
| 6fbf6d90dc | |||
| 828385b049 | |||
| 6bbaf03f1f | |||
| 6fdc8bd379 | |||
| 0f125196a6 | |||
| 974735d415 | |||
| 472b96795f | |||
| 81f4ef609b | |||
| 80d3f7d179 | |||
| c4343e0124 | |||
| 04b6571cb8 | |||
| a7a7d9a3d4 | |||
| 395e7b54f6 | |||
| c20143c212 | |||
| 1729c085c7 | |||
| bf29090ffa | |||
| ca132881f9 | |||
| d47b5b99c5 | |||
| e0ff30e9a8 | |||
| 7c62312220 | |||
| b36972ce71 | |||
| 023e7b2d32 | |||
| e10cd2a3ed | |||
| 209c315a76 | |||
| 7fe2c094a9 | |||
| 23d3e54ddb | |||
| 6b2b98a262 | |||
| fba8568474 | |||
| 2a97939807 | |||
| a74b025de3 | |||
| cec44be7c3 | |||
| a4852c1b36 | |||
| ef2dea89b4 | |||
| 593d86f3a7 | |||
| fd63611b41 | |||
| 4dc32dc7f2 | |||
| 1e845adc17 | |||
| fb6435e30d | |||
| 6ee71d238b | |||
| d330000c8d | |||
| 1517a7b665 | |||
| da33a6c48c | |||
| 89e07921f1 | |||
| 2450511555 | |||
| da1ee99c53 | |||
| 1c922ca083 | |||
| 14a578f319 | |||
| 4a5c411665 | |||
| 0de30afba1 | |||
| 245f2afeac | |||
| 0f81286ff5 | |||
| f01c70e506 | |||
| 03e14154a6 | |||
| 509a767e50 | |||
| e7526f2e78 | |||
| 9d69a2e565 | |||
| 705875cff2 | |||
| 39b366ee69 | |||
| edd326efd9 | |||
| 4f634689c2 | |||
| db429bd838 | |||
| cce372fc50 | |||
| df7479f506 | |||
| 0badd69409 | |||
| c953b8030a | |||
| ba9368426c | |||
| 2cb739027b | |||
| 120ac6c480 | |||
| 6ac68984f2 | |||
| 51633e000b | |||
| b536b8707e | |||
| 3b5f931f06 | |||
| bf15eebd2d | |||
| 4fc22e25ba | |||
| 0f9a9a377b | |||
| d79e6f2704 | |||
| e9672e6bba | |||
| 9670e29d9f | |||
| 612fb7ad7b | |||
| 1da1188351 | |||
| 39433fe707 | |||
| 3b0bc1ca15 | |||
| cc9ad17ea5 | |||
| f965d01922 | |||
| 7cb9546d21 | |||
| debe87f2f5 | |||
| cca2807256 | |||
| 7b73f76e78 | |||
| b1eefd6c85 | |||
| bbcb7ad980 | |||
| 984c43cd75 | |||
| ec4c0fdd09 | |||
| 51d4a9c7ee | |||
| 19930f63e2 | |||
| 3b9a3aaad2 | |||
| f5148074fd | |||
| a949a113cf | |||
| 227e9df419 | |||
| 2a6d462be1 | |||
| bb03fa26cd | |||
| 9eb4703d7a | |||
| 105752fc65 | |||
| 2747e93316 | |||
| 9548f984eb | |||
| cb871ce4bc | |||
| 8ca849b7a8 | |||
| 4bb29b1b5c | |||
| e55e893c94 | |||
| 5ab63a290e | |||
| 7c3414b86f | |||
| cec8829032 | |||
| 78f9f49a8a | |||
| 5a7722fd18 | |||
| d111a979f7 | |||
| 31514c8e31 | |||
| af5ce101ef | |||
| 075da27d13 | |||
| 7b19fb44a4 | |||
| c991946ea7 | |||
| f960a3ae38 | |||
| 73f8811a4b | |||
| bc6ec2579a | |||
| 35bc7263da | |||
| cc3db00a06 | |||
| 7f7961ae0c | |||
| aae60b2ef8 | |||
| ab700543b9 | |||
| 413488f5f4 | |||
| 0ceee14952 | |||
| b4b998df08 | |||
| d6bb165de5 | |||
| ac69f63c89 | |||
| ce5b6c9f64 | |||
| 9d800324af | |||
| e0603f741f | |||
| ce006d0e5b | |||
| 5fb9a9f164 | |||
| 351cd29050 | |||
| 6c160b719a | |||
| d1a7ca7822 | |||
| ee515394c0 | |||
| b2efed71d3 | |||
| 9035dc6bf7 | |||
| 6e5a25dac4 | |||
| af80b07b01 | |||
| 0d93fdf23d | |||
| db2379e2fd | |||
| 9a3900114b | |||
| 6b1d689621 | |||
| 2f7ce565f0 | |||
| 77b9cab07b | |||
| f9fe4e9c3d | |||
| cd32f0ff6b | |||
| 756a796e1d | |||
| 72e949c644 | |||
| 58ba3b012e | |||
| 1854256a93 | |||
| a31bf17469 | |||
| ca7d7ab675 | |||
| 20c802a1e5 | |||
| d1cbf4f06c | |||
| 86443252b1 | |||
| 653727fd12 | |||
| 7a3354f654 | |||
| e9ebee180e | |||
| a93259f3bd | |||
| 8f6c012fb3 | |||
| a635b023f6 | |||
| 1cc7ea5ca7 | |||
| 1b9f874db5 | |||
| cf5ae8f291 | |||
| 96878e2247 | |||
| 80fad573fa | |||
| 1d2a1eee81 | |||
| baecdc4d4f | |||
| 310e6ffc0d | |||
| 13f6e50354 | |||
| f76aec8b5a | |||
| 40fb9de15e | |||
| 0630edc626 | |||
| f2ef6fa12f | |||
| e9616a2d3e | |||
| de5a4cd8cb | |||
| 3b18f12ff2 | |||
| 88bb7a7e5b | |||
| 8fe4ce456f | |||
| 8a7c56e8fd | |||
| 43ac21fd66 | |||
| 17a854e8e1 | |||
| 09c67dd557 | |||
| d837b409e8 | |||
| 994a000e36 | |||
| 5ae50047e0 | |||
| 4e47e7ac2a | |||
| 22a3549599 | |||
| 8bb2a399cc | |||
| 2780dc6a67 | |||
| 421129029d | |||
| cd35df6cc5 | |||
| 61c787b1c7 | |||
| 3c6f80e520 | |||
| 8592153c0f | |||
| 958334bd49 | |||
| 6bbe2d0e00 | |||
| 9786deef48 | |||
| 9af1c1671c | |||
| ce743fe95d | |||
| a9c038bcb6 | |||
| 35bc5de40f | |||
| fb1494fc81 | |||
| e3da0fe255 | |||
| 4443e39785 | |||
| 796c617569 | |||
| 275a92ae93 | |||
| 35d2cc9be7 | |||
| 264c2b2f90 | |||
| a520d636e8 | |||
| 090aaf8ee3 | |||
| 34a9d1d125 | |||
| 40b3f77db0 | |||
| 0c7453684b | |||
| 310c6a1ccf | |||
| 4c52a12507 | |||
| 1a8e4c953d | |||
| 8bf33e211d | |||
| c49c296d2b | |||
| ee5a126c1c | |||
| e4f08f79c3 | |||
| 2aaec3b6bd | |||
| 743a2f8dac | |||
| aa5c3042da | |||
| f221fead4a | |||
| af51018e02 | |||
| ed904c2bdd | |||
| 4ed9625959 | |||
| 42e9b6d2f3 | |||
| a8788feb50 | |||
| 22a8aab151 | |||
| f44d1c4b9d | |||
| a28bd09365 | |||
| 345cc45a3e | |||
| 3f189c430b | |||
| 207ff70680 | |||
| 5113d52444 | |||
| fd8abc168d | |||
| 033139677b | |||
| 2e4128dcfe | |||
| 0a1f349901 | |||
| 62a589b6ad | |||
| 055829dcf8 | |||
| 649364beb5 | |||
| d3f9756bdb | |||
| 7447d9a55a | |||
| 70511dd0f2 | |||
| 664f81249c | |||
| 8f2e616e07 | |||
| 72708d6e2c | |||
| 7a633ee8c8 | |||
| c11fe3e1ab | |||
| a4e54f063d | |||
| b5321f8993 | |||
| bcf799732f | |||
| 13ba2182c2 | |||
| 0d25c607e7 | |||
| b3f8866ef7 | |||
| 9bb16dec48 | |||
| bdb35f1c1d | |||
| d421b5aa5a | |||
| 1ec05e8a6c | |||
| 5b941013de | |||
| a93ed35eee | |||
| 76469969f3 | |||
| 8b39ea4acb | |||
| 252ca9a5f9 | |||
| c4eb1a0f5b | |||
| 1e2f4e9ebb | |||
| 2a7aefac45 | |||
| ea39e2d842 | |||
| fc5879a204 | |||
| 5ae2229e37 | |||
| 12e5ce0ff0 | |||
| 5ef3774d11 | |||
| 654e816e6b | |||
| 7cad7bcddb | |||
| 136d514cf7 | |||
| 6e48345d54 | |||
| 8ebdb466f7 | |||
| 1ed7b690a5 | |||
| 5c28a3eda7 |
10
.gitignore
vendored
10
.gitignore
vendored
@ -6,11 +6,13 @@
|
||||
.*.sw?
|
||||
*~
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# Test files
|
||||
godog.test
|
||||
debug.test
|
||||
coverage.html
|
||||
gobinsec-cache*.yml
|
||||
|
||||
# Run files
|
||||
mem.pprof
|
||||
@ -31,3 +33,11 @@ vendor-cache
|
||||
cmd/Desktop-Bridge/deploy
|
||||
cmd/Import-Export/deploy
|
||||
proton-bridge
|
||||
cmd/Desktop-Bridge/*.exe
|
||||
cmd/launcher/*.exe
|
||||
|
||||
# Jetbrains (CLion, Golang) cmake build dirs
|
||||
cmake-build-*/
|
||||
|
||||
# Doxygen doc files
|
||||
_doc/
|
||||
|
||||
240
.gitlab-ci.yml
240
.gitlab-ci.yml
@ -16,92 +16,99 @@
|
||||
# along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
---
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:latest
|
||||
image: harbor.protontech.ch/docker.io/library/golang:1.18
|
||||
|
||||
variables:
|
||||
GOPRIVATE: gitlab.protontech.ch
|
||||
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
|
||||
|
||||
before_script:
|
||||
- eval $(ssh-agent -s)
|
||||
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
|
||||
|
||||
- mkdir -p .cache/bin
|
||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||
|
||||
- make install-dev-dependencies
|
||||
|
||||
cache:
|
||||
key: go-mod
|
||||
paths:
|
||||
- .cache
|
||||
policy: pull
|
||||
- apt update && apt-get -y install libsecret-1-dev
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
|
||||
stages:
|
||||
- cache
|
||||
- test
|
||||
- build
|
||||
- check
|
||||
- mirror
|
||||
|
||||
# Stage: CACHE
|
||||
.rules-branch-and-MR-always:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- when: never
|
||||
|
||||
.rules-branch-and-MR-manual:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-branch-manual-MR-always:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-branch-manual-MR-always-allow-failure:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: true
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
# This will ensure latest dependency versions and updates the cache for
|
||||
# all other following jobs which only pull the cache.
|
||||
cache-push:
|
||||
stage: cache
|
||||
only:
|
||||
- branches
|
||||
script:
|
||||
- echo ""
|
||||
cache:
|
||||
key: go-mod
|
||||
paths:
|
||||
- .cache
|
||||
|
||||
# Stage: TEST
|
||||
|
||||
lint:
|
||||
stage: test
|
||||
only:
|
||||
- branches
|
||||
before_script:
|
||||
- mkdir -p .cache/bin
|
||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||
extends:
|
||||
- .rules-branch-and-MR-always
|
||||
script:
|
||||
- env GOMAXPROCS=$(( ${CI_TAG_CPU} / 2 )) make lint
|
||||
- make lint
|
||||
tags:
|
||||
- medium
|
||||
|
||||
test-linux:
|
||||
stage: test
|
||||
only:
|
||||
- branches
|
||||
extends:
|
||||
- .rules-branch-manual-MR-always
|
||||
script:
|
||||
- apt-get -y install pass gnupg rng-tools
|
||||
# First have enough of entropy (cat /proc/sys/kernel/random/entropy_avail).
|
||||
- rngd -r /dev/urandom
|
||||
# Generate GPG key without password for the password manager.
|
||||
- gpg --batch --yes --passphrase '' --quick-generate-key 'tester@example.com'
|
||||
# Use the last created GPG ID for the password manager.
|
||||
- pass init `gpg --list-keys | grep "^ " | tail -1 | tr -d '[:space:]'`
|
||||
# Then finally run the tests
|
||||
- make test
|
||||
tags:
|
||||
- medium
|
||||
|
||||
test-windows:
|
||||
extends: .build-windows-base
|
||||
test-linux-race:
|
||||
stage: test
|
||||
only:
|
||||
- branches
|
||||
extends:
|
||||
- .rules-branch-manual-MR-always-allow-failure
|
||||
script:
|
||||
- make test
|
||||
- make test-race
|
||||
tags:
|
||||
- medium
|
||||
|
||||
test-integration:
|
||||
stage: test
|
||||
only:
|
||||
- branches
|
||||
extends:
|
||||
- .rules-branch-manual-MR-always
|
||||
script:
|
||||
- VERBOSITY=debug make -C test test
|
||||
- make test-integration
|
||||
tags:
|
||||
- large
|
||||
|
||||
test-integration-race:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-always-allow-failure
|
||||
script:
|
||||
- make test-integration-race
|
||||
tags:
|
||||
- large
|
||||
|
||||
@ -112,33 +119,29 @@ dependency-updates:
|
||||
|
||||
# Stage: BUILD
|
||||
|
||||
build-qml:
|
||||
tags:
|
||||
- small
|
||||
only:
|
||||
- branches
|
||||
stage: build
|
||||
artifacts:
|
||||
name: "bridge-qml-$CI_COMMIT_SHORT_SHA"
|
||||
expire_in: 1 day
|
||||
paths:
|
||||
- bridge_qml.tgz
|
||||
script:
|
||||
- cd internal/frontend/qml
|
||||
- tar -cvzf ../../../bridge_qml.tgz ./*
|
||||
|
||||
|
||||
.build-base:
|
||||
stage: build
|
||||
only:
|
||||
- manual
|
||||
needs: ["lint"]
|
||||
rules:
|
||||
# GODT-1833: use `=~ /qa/` after mac and windows runners are fixed
|
||||
- if: $CI_JOB_NAME =~ /build-linux-qa/ && $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
before_script:
|
||||
- mkdir -p .cache/bin
|
||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||
- export PATH=$PATH:$QT6DIR/bin
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
script:
|
||||
- make build
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
artifacts:
|
||||
# Note: The latest artifacts for refs are locked against deletion, and kept
|
||||
# regardless of the expiry time. Introduced in GitLab 13.0 behind a
|
||||
@ -147,19 +150,25 @@ build-qml:
|
||||
when: always
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
- vault-editor
|
||||
tags:
|
||||
- large
|
||||
|
||||
build-linux:
|
||||
extends: .build-base
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:qt6
|
||||
variables:
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||
cache:
|
||||
key: linux-vcpkg
|
||||
paths:
|
||||
- .cache
|
||||
when: 'always'
|
||||
artifacts:
|
||||
name: "bridge-linux-$CI_COMMIT_SHORT_SHA"
|
||||
|
||||
build-linux-qa:
|
||||
extends: build-linux
|
||||
only:
|
||||
- web
|
||||
- branches
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
artifacts:
|
||||
@ -177,10 +186,13 @@ build-linux-qa:
|
||||
- export GOPATH=~/go
|
||||
- export PATH=$GOPATH/bin:$PATH
|
||||
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
script:
|
||||
- go version
|
||||
- make build
|
||||
- make build-nogui
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
cache: {}
|
||||
tags:
|
||||
- macOS
|
||||
@ -192,9 +204,6 @@ build-darwin:
|
||||
|
||||
build-darwin-qa:
|
||||
extends: .build-darwin-base
|
||||
only:
|
||||
- web
|
||||
- branches
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
artifacts:
|
||||
@ -204,17 +213,22 @@ build-darwin-qa:
|
||||
.build-windows-base:
|
||||
extends: .build-base
|
||||
before_script:
|
||||
- export GOROOT=/c/Go
|
||||
- export GOROOT=/c/Go1.18/
|
||||
- export PATH=$GOROOT/bin:$PATH
|
||||
- export GOARCH=amd64
|
||||
- export GOPATH=~/go
|
||||
- export GOPATH=~/go18
|
||||
- export GO111MODULE=on
|
||||
- export PATH=$GOPATH/bin:$PATH
|
||||
- export PATH="${GOPATH}/bin:${PATH}"
|
||||
- export MSYSTEM=
|
||||
- export PATH=$PATH:/c/grrrQt/5.13.2/mingw73_64/bin
|
||||
- export QT6DIR=/c/grrrQt/6.3.1/msvc2019_64
|
||||
- export PATH=$PATH:${QT6DIR}/bin
|
||||
- export PATH="/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin:$PATH"
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
script:
|
||||
- make build
|
||||
- make build-nogui
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
tags:
|
||||
- windows-bridge
|
||||
|
||||
@ -225,61 +239,9 @@ build-windows:
|
||||
|
||||
build-windows-qa:
|
||||
extends: .build-windows-base
|
||||
only:
|
||||
- web
|
||||
- branches
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
artifacts:
|
||||
name: "bridge-windows-qa-$CI_COMMIT_SHORT_SHA"
|
||||
|
||||
# Stage: CHECK
|
||||
check-gobinsec:
|
||||
stage: check
|
||||
only:
|
||||
- branches
|
||||
cache:
|
||||
key: gobinsec-cache
|
||||
paths:
|
||||
- gobinsec-cache.yml
|
||||
policy: pull-push
|
||||
before_script:
|
||||
- mkdir build
|
||||
- tar -xzf bridge_linux_*.tgz -C build
|
||||
script:
|
||||
- "[ ! -f ./gobinsec-cache.yml ] && wget bridgeteam.protontech.ch/bridgeteam/gobinsec-cache.yml"
|
||||
- cat ./gobinsec-cache.yml
|
||||
- gobinsec -cache -config utils/gobinsec_conf.yml build/proton-bridge
|
||||
|
||||
|
||||
|
||||
# Stage: MIRROR
|
||||
|
||||
mirror-repo:
|
||||
stage: mirror
|
||||
only:
|
||||
refs:
|
||||
- master
|
||||
script:
|
||||
- |
|
||||
cat <<EOF > ~/.ssh/config
|
||||
Host github.com
|
||||
Hostname ssh.github.com
|
||||
User git
|
||||
Port 443
|
||||
ProxyCommand connect-proxy -H $http_proxy %h %p
|
||||
EOF
|
||||
- ssh-keyscan -t rsa ${CI_SERVER_HOST} > ~/.ssh/known_hosts
|
||||
- |
|
||||
cat <<EOF >> ~/.ssh/known_hosts
|
||||
# ssh.github.com:443 SSH-2.0-babeld-2e9d163d
|
||||
[ssh.github.com]:443 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
|
||||
EOF
|
||||
- echo "$mirror_key" | tr -d '\r' | ssh-add - > /dev/null
|
||||
- ssh-add -l
|
||||
- git clone "$CI_REPOSITORY_URL" --branch master _REPO_CLONE;
|
||||
- cd _REPO_CLONE
|
||||
- git remote add public $mirror_url
|
||||
- git push public master
|
||||
# Pushing the latest tag from master history
|
||||
- git push public "$(git describe --tags --abbrev=0 || echo master)"
|
||||
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "submodules/vcpkg"]
|
||||
path = extern/vcpkg
|
||||
url = https://github.com/Microsoft/vcpkg.git
|
||||
@ -3,6 +3,7 @@ run:
|
||||
timeout: 10m
|
||||
skip-dirs:
|
||||
- pkg/mime
|
||||
- extern
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
@ -12,6 +13,11 @@ issues:
|
||||
- should have comment (\([^)]+\) )?or be unexported
|
||||
# For now we are missing a lot of comments.
|
||||
- at least one file in a package should have a package comment
|
||||
# Package comments.
|
||||
- "package-comments: should have a package comment"
|
||||
# Migration uses underscores to make versions clearer.
|
||||
- "var-naming: don't use underscores in Go names"
|
||||
- "ST1003: should not use underscores in Go names"
|
||||
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
@ -21,6 +27,17 @@ issues:
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gosec
|
||||
- goconst
|
||||
- dogsled
|
||||
- path: test
|
||||
linters:
|
||||
- dupl
|
||||
- funlen
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gosec
|
||||
- goconst
|
||||
- dogsled
|
||||
|
||||
linters-settings:
|
||||
godox:
|
||||
@ -105,4 +122,3 @@ linters:
|
||||
# - testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false]
|
||||
# - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false]
|
||||
# - wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false]
|
||||
|
||||
|
||||
40
BUILDS.md
40
BUILDS.md
@ -1,13 +1,12 @@
|
||||
# Building Proton Mail Bridge and Import-Export app
|
||||
# Building Proton Mail Bridge
|
||||
|
||||
## Prerequisites
|
||||
* 64-bit AMD OS:
|
||||
* 64-bit 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.18
|
||||
* 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/)
|
||||
* GCC (linux, windows) or Xcode (macOS)
|
||||
- For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
|
||||
* GCC (linux), msvc (windows) or Xcode (macOS)
|
||||
* Windres (windows)
|
||||
* libglvnd and libsecret development files (linux)
|
||||
|
||||
@ -16,16 +15,11 @@ To enable the sending of crash reports using Sentry please set the
|
||||
Otherwise, the sending of crash reports will be disabled.
|
||||
|
||||
## Build
|
||||
In order to build Bridge or Import-Export app with Qt interface we are using
|
||||
[Qt Go Binding](https://github.com/therecipe/qt). The dependencies and
|
||||
installation of this tool is part of `make build` target. If you have issues
|
||||
with installation of therecipe/qt we recommend to follow [this
|
||||
wiki](https://github.com/therecipe/qt/wiki/Installation-on-Linux)
|
||||
|
||||
Please note that `$(go env GOPATH)/bin` must be in your `PATH` to ensure
|
||||
binaries installed by `therecipe/qt` (such as `qtdeploy`) are found. Also,
|
||||
before you start build **on Windows**, please unset the `MSYSTEM` variable
|
||||
In order to build Bridge app with Qt interface we are using
|
||||
[Qt 6.3](https://doc.qt.io/qt-6/gettingstarted.html).
|
||||
|
||||
Please note that qmake path must be in your `PATH` to ensure Qt to be found.
|
||||
Also, before you start build **on Windows**, please unset the `MSYSTEM` variable
|
||||
|
||||
```bash
|
||||
export MSYSTEM=
|
||||
@ -54,24 +48,12 @@ make build-nogui
|
||||
* Bridge always has the option (whether built with Qt or without) to use a CLI interface by starting it with the argument `-c`
|
||||
* NOTE: You still need to setup supported keychain on your system
|
||||
|
||||
### Build Import-Export
|
||||
* in project root run
|
||||
|
||||
```bash
|
||||
make build-ie
|
||||
```
|
||||
|
||||
* The result will be stored in `./cmd/Import-Export/deploy/${GOOS}/`
|
||||
* for `linux`, the binary will have the name of the project directory (e.g `proton-bridge`)
|
||||
* for `windows`, the binary will have the file extension `.exe` (e.g `proton-bridge.exe`)
|
||||
* for `darwin`, the application will be created with name of the project directory (e.g `proton-bridge.app`)
|
||||
|
||||
### Launchers
|
||||
## Launchers
|
||||
Launchers are only included in official distributions and provide the public
|
||||
key used to verify signed app binaries, allowing the automatic update feature.
|
||||
See README for more information.
|
||||
|
||||
### Tags
|
||||
## Tags
|
||||
Note that repository contains both Bridge and Import-Export apps and they are
|
||||
not released together. Therefore, each app has own tag prefix. Bridge tags
|
||||
starts with `br-` and Import-Export tags starts with `ie-`. Both tags continue
|
||||
|
||||
107
COPYING_NOTES.md
107
COPYING_NOTES.md
@ -21,71 +21,116 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [Qt](https://www.qt.io/) | Available under [multiple licences](https://www.qt.io/licensing)
|
||||
|
||||
<!-- START AUTOGEN -->
|
||||
* [docker-credential-helpers](https://github.com/docker/docker-credential-helpers) available under [license](https://github.com/docker/docker-credential-helpers/blob/master/LICENSE)
|
||||
* [go-imap](https://github.com/emersion/go-imap) available under [license](https://github.com/emersion/go-imap/blob/master/LICENSE)
|
||||
* [notificator](https://github.com/0xAX/notificator) available under [license](https://github.com/0xAX/notificator/blob/master/LICENSE)
|
||||
* [semver](https://github.com/Masterminds/semver/v3) available under [license](https://github.com/Masterminds/semver/v3/blob/master/LICENSE)
|
||||
* [bcrypt](https://github.com/ProtonMail/bcrypt) available under [license](https://github.com/ProtonMail/bcrypt/blob/master/LICENSE)
|
||||
* [gluon](https://github.com/ProtonMail/gluon) available under [license](https://github.com/ProtonMail/gluon/blob/master/LICENSE)
|
||||
* [go-autostart](https://github.com/ProtonMail/go-autostart) available under [license](https://github.com/ProtonMail/go-autostart/blob/master/LICENSE)
|
||||
* [go-crypto](https://github.com/ProtonMail/go-crypto) available under [license](https://github.com/ProtonMail/go-crypto/blob/master/LICENSE)
|
||||
* [go-imap-id](https://github.com/ProtonMail/go-imap-id) available under [license](https://github.com/ProtonMail/go-imap-id/blob/master/LICENSE)
|
||||
* [go-proton-api](https://github.com/ProtonMail/go-proton-api) available under [license](https://github.com/ProtonMail/go-proton-api/blob/master/LICENSE)
|
||||
* [go-rfc5322](https://github.com/ProtonMail/go-rfc5322) available under [license](https://github.com/ProtonMail/go-rfc5322/blob/master/LICENSE)
|
||||
* [go-srp](https://github.com/ProtonMail/go-srp) available under [license](https://github.com/ProtonMail/go-srp/blob/master/LICENSE)
|
||||
* [go-vcard](https://github.com/ProtonMail/go-vcard) available under [license](https://github.com/ProtonMail/go-vcard/blob/master/LICENSE)
|
||||
* [gopenpgp](https://github.com/ProtonMail/gopenpgp/v2) available under [license](https://github.com/ProtonMail/gopenpgp/v2/blob/master/LICENSE)
|
||||
* [goquery](https://github.com/PuerkitoBio/goquery) available under [license](https://github.com/PuerkitoBio/goquery/blob/master/LICENSE)
|
||||
* [ishell](https://github.com/abiosoft/ishell) available under [license](https://github.com/abiosoft/ishell/blob/master/LICENSE)
|
||||
* [readline](https://github.com/abiosoft/readline) available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
|
||||
* [go-singleinstance](https://github.com/allan-simon/go-singleinstance) available under [license](https://github.com/allan-simon/go-singleinstance/blob/master/LICENSE)
|
||||
* [logex](https://github.com/chzyer/logex) available under [license](https://github.com/chzyer/logex/blob/master/LICENSE)
|
||||
* [test](https://github.com/chzyer/test) available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
|
||||
* [juniper](https://github.com/bradenaw/juniper) available under [license](https://github.com/bradenaw/juniper/blob/master/LICENSE)
|
||||
* [godog](https://github.com/cucumber/godog) available under [license](https://github.com/cucumber/godog/blob/master/LICENSE)
|
||||
* [messages-go](https://github.com/cucumber/messages-go/v16) available under [license](https://github.com/cucumber/messages-go/v16/blob/master/LICENSE)
|
||||
* [docker-credential-helpers](https://github.com/docker/docker-credential-helpers) available under [license](https://github.com/docker/docker-credential-helpers/blob/master/LICENSE)
|
||||
* [go-sysinfo](https://github.com/elastic/go-sysinfo) available under [license](https://github.com/elastic/go-sysinfo/blob/master/LICENSE)
|
||||
* [go-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE)
|
||||
* [go-imap-appendlimit](https://github.com/emersion/go-imap-appendlimit) available under [license](https://github.com/emersion/go-imap-appendlimit/blob/master/LICENSE)
|
||||
* [go-imap-move](https://github.com/emersion/go-imap-move) available under [license](https://github.com/emersion/go-imap-move/blob/master/LICENSE)
|
||||
* [go-imap-quota](https://github.com/emersion/go-imap-quota) available under [license](https://github.com/emersion/go-imap-quota/blob/master/LICENSE)
|
||||
* [go-imap-unselect](https://github.com/emersion/go-imap-unselect) available under [license](https://github.com/emersion/go-imap-unselect/blob/master/LICENSE)
|
||||
* [go-imap](https://github.com/emersion/go-imap) available under [license](https://github.com/emersion/go-imap/blob/master/LICENSE)
|
||||
* [go-imap-id](https://github.com/emersion/go-imap-id) available under [license](https://github.com/emersion/go-imap-id/blob/master/LICENSE)
|
||||
* [go-message](https://github.com/emersion/go-message) available under [license](https://github.com/emersion/go-message/blob/master/LICENSE)
|
||||
* [go-sasl](https://github.com/emersion/go-sasl) available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
|
||||
* [go-smtp](https://github.com/emersion/go-smtp) available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
|
||||
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
|
||||
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
|
||||
* [color](https://github.com/fatih/color) available under [license](https://github.com/fatih/color/blob/master/LICENSE)
|
||||
* [go-shlex](https://github.com/flynn-archive/go-shlex) available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)
|
||||
* [sentry-go](https://github.com/getsentry/sentry-go) available under [license](https://github.com/getsentry/sentry-go/blob/master/LICENSE)
|
||||
* [resty](https://github.com/go-resty/resty/v2) available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
|
||||
* [go-json](https://github.com/goccy/go-json) available under [license](https://github.com/goccy/go-json/blob/master/LICENSE)
|
||||
* [dbus](https://github.com/godbus/dbus) available under [license](https://github.com/godbus/dbus/blob/master/LICENSE)
|
||||
* [mock](https://github.com/golang/mock) available under [license](https://github.com/golang/mock/blob/master/LICENSE)
|
||||
* [go-cmp](https://github.com/google/go-cmp) available under [license](https://github.com/google/go-cmp/blob/master/LICENSE)
|
||||
* [uuid](https://github.com/google/uuid) available under [license](https://github.com/google/uuid/blob/master/LICENSE)
|
||||
* [go-multierror](https://github.com/hashicorp/go-multierror) available under [license](https://github.com/hashicorp/go-multierror/blob/master/LICENSE)
|
||||
* [bcrypt](https://github.com/jameskeane/bcrypt) available under [license](https://github.com/jameskeane/bcrypt/blob/master/LICENSE)
|
||||
* [html2text](https://github.com/jaytaylor/html2text) available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
|
||||
* [go-keychain](https://github.com/keybase/go-keychain) available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
|
||||
* [text](https://github.com/kr/text) available under [license](https://github.com/kr/text/blob/master/LICENSE)
|
||||
* [aurora](https://github.com/logrusorgru/aurora) available under [license](https://github.com/logrusorgru/aurora/blob/master/LICENSE)
|
||||
* [go-runewidth](https://github.com/mattn/go-runewidth) available under [license](https://github.com/mattn/go-runewidth/blob/master/LICENSE)
|
||||
* [dns](https://github.com/miekg/dns) available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
|
||||
* [pretty](https://github.com/niemeyer/pretty) available under [license](https://github.com/niemeyer/pretty/blob/master/LICENSE)
|
||||
* [jsondiff](https://github.com/nsf/jsondiff) available under [license](https://github.com/nsf/jsondiff/blob/master/LICENSE)
|
||||
* [tablewriter](https://github.com/olekukonko/tablewriter) available under [license](https://github.com/olekukonko/tablewriter/blob/master/LICENSE)
|
||||
* [errors](https://github.com/pkg/errors) available under [license](https://github.com/pkg/errors/blob/master/LICENSE)
|
||||
* [procfs](https://github.com/prometheus/procfs) available under [license](https://github.com/prometheus/procfs/blob/master/LICENSE)
|
||||
* [du](https://github.com/ricochet2200/go-disk-usage/du) available under [license](https://github.com/ricochet2200/go-disk-usage/du/blob/master/LICENSE)
|
||||
* [profile](https://github.com/pkg/profile) available under [license](https://github.com/pkg/profile/blob/master/LICENSE)
|
||||
* [logrus](https://github.com/sirupsen/logrus) available under [license](https://github.com/sirupsen/logrus/blob/master/LICENSE)
|
||||
* [bom](https://github.com/ssor/bom) available under [license](https://github.com/ssor/bom/blob/master/LICENSE)
|
||||
* [testify](https://github.com/stretchr/testify) available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
|
||||
* [qt](https://github.com/therecipe/qt) available under [license](https://github.com/therecipe/qt/blob/master/LICENSE)
|
||||
* [cli](https://github.com/urfave/cli/v2) available under [license](https://github.com/urfave/cli/v2/blob/master/LICENSE)
|
||||
* [msgpack](https://github.com/vmihailenco/msgpack/v5) available under [license](https://github.com/vmihailenco/msgpack/v5/blob/master/LICENSE)
|
||||
* [bbolt](https://go.etcd.io/bbolt) available under [license](https://github.com/etcd-io/bbolt/blob/master/LICENSE)
|
||||
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
|
||||
* [goleak](https://go.uber.org/goleak)
|
||||
* [exp](https://golang.org/x/exp) available under [license](https://cs.opensource.google/go/x/exp/+/master:LICENSE)
|
||||
* [net](https://golang.org/x/net) available under [license](https://cs.opensource.google/go/x/net/+/master:LICENSE)
|
||||
* [sys](https://golang.org/x/sys) available under [license](https://cs.opensource.google/go/x/sys/+/master:LICENSE)
|
||||
* [text](https://golang.org/x/text) available under [license](https://cs.opensource.google/go/x/text/+/master:LICENSE)
|
||||
* [grpc](https://google.golang.org/grpc) available under [license](https://github.com/grpc/grpc-go/blob/master/LICENSE)
|
||||
* [protobuf](https://google.golang.org/protobuf) available under [license](https://github.com/protocolbuffers/protobuf/blob/main/LICENSE)
|
||||
* [plist](https://howett.net/plist) available under [license](https://github.com/DHowett/go-plist/blob/main/LICENSE)
|
||||
* [atlas](https://ariga.io/atlas)
|
||||
* [ent](https://entgo.io/ent)
|
||||
* [bcrypt](https://github.com/ProtonMail/bcrypt) available under [license](https://github.com/ProtonMail/bcrypt/blob/master/LICENSE)
|
||||
* [go-crypto](https://github.com/ProtonMail/go-crypto) available under [license](https://github.com/ProtonMail/go-crypto/blob/master/LICENSE)
|
||||
* [go-mime](https://github.com/ProtonMail/go-mime) available under [license](https://github.com/ProtonMail/go-mime/blob/master/LICENSE)
|
||||
* [go-srp](https://github.com/ProtonMail/go-srp) available under [license](https://github.com/ProtonMail/go-srp/blob/master/LICENSE)
|
||||
* [readline](https://github.com/abiosoft/readline) available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
|
||||
* [levenshtein](https://github.com/agext/levenshtein) available under [license](https://github.com/agext/levenshtein/blob/master/LICENSE)
|
||||
* [cascadia](https://github.com/andybalholm/cascadia) available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
|
||||
* [antlr](https://github.com/antlr/antlr4/runtime/Go/antlr) available under [license](https://github.com/antlr/antlr4/runtime/Go/antlr/blob/master/LICENSE)
|
||||
* [go-textseg](https://github.com/apparentlymart/go-textseg/v13) available under [license](https://github.com/apparentlymart/go-textseg/v13/blob/master/LICENSE)
|
||||
* [test](https://github.com/chzyer/test) available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
|
||||
* [circl](https://github.com/cloudflare/circl) available under [license](https://github.com/cloudflare/circl/blob/master/LICENSE)
|
||||
* [go-md2man](https://github.com/cpuguy83/go-md2man/v2) available under [license](https://github.com/cpuguy83/go-md2man/v2/blob/master/LICENSE)
|
||||
* [saferith](https://github.com/cronokirby/saferith) available under [license](https://github.com/cronokirby/saferith/blob/master/LICENSE)
|
||||
* [gherkin-go](https://github.com/cucumber/gherkin-go/v19) available under [license](https://github.com/cucumber/gherkin-go/v19/blob/master/LICENSE)
|
||||
* [wincred](https://github.com/danieljoos/wincred) available under [license](https://github.com/danieljoos/wincred/blob/master/LICENSE)
|
||||
* [go-spew](https://github.com/davecgh/go-spew) available under [license](https://github.com/davecgh/go-spew/blob/master/LICENSE)
|
||||
* [go-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE)
|
||||
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
|
||||
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
|
||||
* [go-shlex](https://github.com/flynn-archive/go-shlex) available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)
|
||||
* [sse](https://github.com/gin-contrib/sse) available under [license](https://github.com/gin-contrib/sse/blob/master/LICENSE)
|
||||
* [gin](https://github.com/gin-gonic/gin) available under [license](https://github.com/gin-gonic/gin/blob/master/LICENSE)
|
||||
* [inflect](https://github.com/go-openapi/inflect) available under [license](https://github.com/go-openapi/inflect/blob/master/LICENSE)
|
||||
* [locales](https://github.com/go-playground/locales) available under [license](https://github.com/go-playground/locales/blob/master/LICENSE)
|
||||
* [universal-translator](https://github.com/go-playground/universal-translator) available under [license](https://github.com/go-playground/universal-translator/blob/master/LICENSE)
|
||||
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE)
|
||||
* [uuid](https://github.com/gofrs/uuid) available under [license](https://github.com/gofrs/uuid/blob/master/LICENSE)
|
||||
* [protobuf](https://github.com/golang/protobuf) available under [license](https://github.com/golang/protobuf/blob/master/LICENSE)
|
||||
* [errwrap](https://github.com/hashicorp/errwrap) available under [license](https://github.com/hashicorp/errwrap/blob/master/LICENSE)
|
||||
* [go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) available under [license](https://github.com/hashicorp/go-immutable-radix/blob/master/LICENSE)
|
||||
* [go-memdb](https://github.com/hashicorp/go-memdb) available under [license](https://github.com/hashicorp/go-memdb/blob/master/LICENSE)
|
||||
* [golang-lru](https://github.com/hashicorp/golang-lru) available under [license](https://github.com/hashicorp/golang-lru/blob/master/LICENSE)
|
||||
* [hcl](https://github.com/hashicorp/hcl/v2) available under [license](https://github.com/hashicorp/hcl/v2/blob/master/LICENSE)
|
||||
* [multierror](https://github.com/joeshaw/multierror) available under [license](https://github.com/joeshaw/multierror/blob/master/LICENSE)
|
||||
* [go](https://github.com/json-iterator/go) available under [license](https://github.com/json-iterator/go/blob/master/LICENSE)
|
||||
* [go-urn](https://github.com/leodido/go-urn) available under [license](https://github.com/leodido/go-urn/blob/master/LICENSE)
|
||||
* [go-colorable](https://github.com/mattn/go-colorable) available under [license](https://github.com/mattn/go-colorable/blob/master/LICENSE)
|
||||
* [go-isatty](https://github.com/mattn/go-isatty) available under [license](https://github.com/mattn/go-isatty/blob/master/LICENSE)
|
||||
* [go-runewidth](https://github.com/mattn/go-runewidth) available under [license](https://github.com/mattn/go-runewidth/blob/master/LICENSE)
|
||||
* [go-sqlite3](https://github.com/mattn/go-sqlite3) available under [license](https://github.com/mattn/go-sqlite3/blob/master/LICENSE)
|
||||
* [go-wordwrap](https://github.com/mitchellh/go-wordwrap) available under [license](https://github.com/mitchellh/go-wordwrap/blob/master/LICENSE)
|
||||
* [concurrent](https://github.com/modern-go/concurrent) available under [license](https://github.com/modern-go/concurrent/blob/master/LICENSE)
|
||||
* [reflect2](https://github.com/modern-go/reflect2) available under [license](https://github.com/modern-go/reflect2/blob/master/LICENSE)
|
||||
* [tablewriter](https://github.com/olekukonko/tablewriter) available under [license](https://github.com/olekukonko/tablewriter/blob/master/LICENSE)
|
||||
* [go-toml](https://github.com/pelletier/go-toml/v2) available under [license](https://github.com/pelletier/go-toml/v2/blob/master/LICENSE)
|
||||
* [go-difflib](https://github.com/pmezard/go-difflib) available under [license](https://github.com/pmezard/go-difflib/blob/master/LICENSE)
|
||||
* [procfs](https://github.com/prometheus/procfs) available under [license](https://github.com/prometheus/procfs/blob/master/LICENSE)
|
||||
* [uniseg](https://github.com/rivo/uniseg) available under [license](https://github.com/rivo/uniseg/blob/master/LICENSE)
|
||||
* [blackfriday](https://github.com/russross/blackfriday/v2) available under [license](https://github.com/russross/blackfriday/v2/blob/master/LICENSE)
|
||||
* [pflag](https://github.com/spf13/pflag) available under [license](https://github.com/spf13/pflag/blob/master/LICENSE)
|
||||
* [bom](https://github.com/ssor/bom) available under [license](https://github.com/ssor/bom/blob/master/LICENSE)
|
||||
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
|
||||
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
|
||||
* [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE)
|
||||
* [go-cty](https://github.com/zclconf/go-cty) available under [license](https://github.com/zclconf/go-cty/blob/master/LICENSE)
|
||||
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
|
||||
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
|
||||
* [sync](https://golang.org/x/sync) available under [license](https://cs.opensource.google/go/x/sync/+/master:LICENSE)
|
||||
* [tools](https://golang.org/x/tools) available under [license](https://cs.opensource.google/go/x/tools/+/master:LICENSE)
|
||||
* [genproto](https://google.golang.org/genproto)
|
||||
gopkg.in/yaml.v2
|
||||
gopkg.in/yaml.v3
|
||||
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
|
||||
* [go-imap](https://github.com/ProtonMail/go-imap) available under [license](https://github.com/ProtonMail/go-imap/blob/master/LICENSE)
|
||||
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
|
||||
|
||||
257
Changelog.md
257
Changelog.md
@ -2,6 +2,263 @@
|
||||
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
## [Bridge 3.0.4] Perth Narrows
|
||||
|
||||
### Changed
|
||||
* Other: Do not list \Deleted flag for All Mail.
|
||||
* Other: Disable perma-delete for expunge on Spam folder.
|
||||
|
||||
### Fixed
|
||||
* Other: Ensure expunge feature test pushes to error stack.
|
||||
* GODT-2170: Use client-side draft update in integration tests.
|
||||
* GODT-2170: Improving test server behaviour.
|
||||
* GODT-2170: Update draft event means delete old and create new message.
|
||||
* GODT-2170: User create draft route: first steps.
|
||||
|
||||
## [Bridge 3.0.3] Perth Narrows
|
||||
|
||||
### Fixed
|
||||
* GPA v0.1.4: fix token expiration mechanism.
|
||||
|
||||
## [Bridge 3.0.2] Perth Narrows
|
||||
|
||||
### Changed
|
||||
* GODT-2157: Add Sentry to Bridge-Gui.
|
||||
* GODT-2153: Use file socket for bridge gRPC on linux & macOS.
|
||||
* GODT-2150: Do not forward --no-window flag.
|
||||
* GODT-2154: Allow noninteractive mode from launcher.
|
||||
* Other: update gui tester to support latest changes in gRPC implementation.
|
||||
* Other: GUI Tester supports the 3 states of user (Signed out/Locked/Connected).
|
||||
* Other: Bump gluon version to drop non-UTF-8 commands.
|
||||
|
||||
### Fixed
|
||||
* Other: Wipe vault properly on factory reset.
|
||||
* GODT-2160: Prevent double closing of bridge if restart fails.
|
||||
* GODT-2041: Crash after factory reset.
|
||||
* GODT-2114: Sanitize attachment disposition.
|
||||
* GODT-1910: Fix GUI not being notified of SMTP SSL being turned on by ConfigureAppleMail.
|
||||
* GODT-1910: Fix save button state not being updated after being clicked once.
|
||||
* GODT-2159: Improve 429 retry.
|
||||
* GODT-1989: Handle Move with Append and Expunge.
|
||||
* Other: SetMailServerSettings is async as it should.
|
||||
* Other: Include sentry dll for Windows deploy.
|
||||
* Other: Ensure context is string in sentry reports.
|
||||
* GODT-2160: Ensure we can safely move cache file.
|
||||
|
||||
## [Bridge 3.0.1] Perth Narrows
|
||||
|
||||
### Changed
|
||||
* GODT-2151: Sync backwards to please product people.
|
||||
|
||||
### Fixed
|
||||
* GODT-2149: Sort logs by timestamp when clearing.
|
||||
* GODT-2137: Set sentry sync transport.
|
||||
|
||||
## [Bridge 3.0.0] Perth Narrows
|
||||
|
||||
### Changed
|
||||
* Other(chore): Bump major version to v3.
|
||||
* Other: Switch from liteapi to go-proton-api.
|
||||
* GODT-2085: Ensure minimum sync worker count.
|
||||
* Other: Switch to mail-api.proton.me.
|
||||
* GODT-2120: Encrypt gluon store with gzip.
|
||||
* GODT-1910: Use a single view for IMAP & SMTP SSL options.
|
||||
* GODT-1846: Remove restart cues* implement restart-less behaviour.
|
||||
* GODT-1975: Migrate keychain secrets.
|
||||
* GODT-1976: Migrate app settings from prefs.json.
|
||||
* GODT-2100: Load users in parallel at startup.
|
||||
* GODT-2108: Implement C++ Focus gRPC service client in bridge-gui.
|
||||
* GODT-2091: Animated "Connecting..." label.
|
||||
* GODT-2003: Introduces 3 phases user state (SignedOut/Locked/Connected).
|
||||
* GODT-2056: Kill old bridge from v2 lock file.
|
||||
* GODT-2086: Changing the wording for signing in.
|
||||
* GODT-2070: Implement SASL login for SMTP.
|
||||
* Other: Use liteapi instead of pmapi.
|
||||
* GODT-1609: Store password as byte array.
|
||||
* GODT-1650: Gluon integration.
|
||||
* GODT-1779: Remove go-imap.
|
||||
|
||||
### Fixed
|
||||
* Other: Retry sync after cooldown if it fails.
|
||||
* GODT-2142: Also permit split by comma in References header.
|
||||
* GODT-2085: Use time.Since* structured logging.
|
||||
* GODT-2139: Validate key pass during login.
|
||||
* GODT-2111: Fix restart.
|
||||
* GODT-2085: Revise sync algorithm.
|
||||
* GODT-2134: Fix dock icon on macOS when launched with '--no-window'.
|
||||
* GODT-2131: If refresh token is revoked* user gets signed out.
|
||||
* GODT-2119: Only show supported label IDs to clients.
|
||||
* GODT-2002: Wait for API events to be applied after send.
|
||||
* GODT-2105: Ensure ClientVersion is set in bug report request.
|
||||
* GODT-2107: Update user list after session revoke.
|
||||
* GODT-2040: Bump UID validity when clearing sync status.
|
||||
* GODT-2045: Timeouts should be considered network issues.
|
||||
* GODT-2122: Handle check for updates failure.
|
||||
* GODT-2033: Only set user agent from IMAP ID if not empty.
|
||||
* GODT-2110: Force attachment disposition if content ID is missing.
|
||||
* GODT-2081: If keychain cannot be loaded do not wipe Vault and use a temp one.
|
||||
* GODT-2103: Trigger the version changed event.
|
||||
* GODT-2047: Clear last event ID when clearing sync status.
|
||||
* GODT-2109: Removed log message "Parent process XXX is still alive".
|
||||
* GODT-1913: Pass reporter to gluon* limit restarts* add crash handlers.
|
||||
* GODT-2037: Handle and log API refresh event.
|
||||
* GODT-2029: Handle deadlock when reordering user addresses.
|
||||
* GODT-2021: Remove gluon data when deleting user.
|
||||
* GODT-2030: Rework deletion check on expunge.
|
||||
* GODT-1977: Fix launcher for v2 to v3 updates.
|
||||
* GODT-2048: Add missing special use attributes.
|
||||
* GODT-2034: Basic vault migration ability (proof of concept).
|
||||
* GODT-1978: Auto-updates from v2 to v3.
|
||||
* GODT-1954: Draft message support.
|
||||
* GODT-2022: Fix change between address modes.
|
||||
* GODT-1993: Use more efficient filtering for message deletion.
|
||||
* GODT-2004: Ensure log files don't have color formatting.
|
||||
* GODT-2011: Use new app version format.
|
||||
* GODT-2010: Add better logging for app focus feature.
|
||||
* GODT-2008: Ensure user's addresses are returned in sorted order.
|
||||
* GODT-2002: Poll after SMTP send.
|
||||
* GODT-1982: Updated gRPC and GUI for disk cache.
|
||||
* GODT-1984: Handle permanent message deletion.
|
||||
* GODT-1916: Use XDG_DATA_HOME to store persistent data on linux.
|
||||
* GODT-1974: Store everything in v3 path.
|
||||
* GODT-1986: Handle case where an address has no decryption entities.
|
||||
* GODT-1777: Message de-duplication in SMTP and IMAP.
|
||||
* GODT-1940: Fix message encryption.
|
||||
* GODT-1742: Implement hide All Mail.
|
||||
* GODT-1813: Cleanup old go-imap cache files.
|
||||
* GODT-1650: Implement Connector.CreateMessage.
|
||||
* GODT-1901: Allow to set IMAP SSL from UI.
|
||||
* GODT-1816: Connect Gluon Logs to bridge Logs.
|
||||
* GODT-1657: Stable sync.
|
||||
* GODT-1815: Gluon User management error.
|
||||
|
||||
## [Bridge 2.4.8] Osney
|
||||
|
||||
### Fixed
|
||||
* GODT-2071: Fix --no-window flag that was broken on Windows.
|
||||
|
||||
## [Bridge 2.4.7] Osney
|
||||
|
||||
### Fixed
|
||||
* GODT-2078: Launcher inception.
|
||||
* GODT-2039: fix --parent-pid flag is removed from command-line when restarting the application.
|
||||
|
||||
## [Bridge 2.4.6] Osney
|
||||
|
||||
### Changed
|
||||
* GODT-2019: When signing out and a single user is connecte* we do not go back to the welcome screen.
|
||||
* GODT-2071: Bridge-gui report error if an orphan bridge is detected.
|
||||
* GODT-2046: Bridge-gui log is included in optional archive sent with bug reports.
|
||||
* GODT-2039: Bridge monitors bridge-gui via its PID.
|
||||
* GODT-2038: Interrupt gRPC initialisation of bridge process terminates.
|
||||
* Other: Added timestamp to bridge-gui logs.
|
||||
* GODT-2035: Bridge-gui log includes Qt version info.
|
||||
* GODT-2031: Updated bridge description.
|
||||
|
||||
### Fixed
|
||||
* Other: Fix make run-qt target for Darwin.
|
||||
|
||||
## [Bridge 2.4.5] Osney
|
||||
|
||||
### Changed
|
||||
* GODT-2015: Bridge-gui logs to file until gRPC connection is established.
|
||||
* GODT-2016: Added more logging of gRPC events at info level.
|
||||
* GODT-2013: CLI flag for frontend is required.
|
||||
|
||||
### Fixed
|
||||
* GODT-2020: Fix xdg_{home,cache}_home variables.
|
||||
* GODT-2014: Bridge quit if gRPC client ends stream.
|
||||
|
||||
## [Bridge 2.4.4] Osney
|
||||
|
||||
### Changed
|
||||
* GODT-1751: Switch from protonmail.com to proton.me domain.
|
||||
|
||||
### Fixed
|
||||
* Other: Fix make run-cli for Darwin.
|
||||
* GODT-1645: Fix CI pipeline.
|
||||
* GODT-1938: Account details box values wrap.
|
||||
* Other: Also install vcpkg ARM64 on Intel mac hosts.
|
||||
* Other: Fix minor typo.
|
||||
* GODT-1939: removed vertical overshoot when scrolling.
|
||||
* GODT-1479: fix 'Open Bridge' button still hovered when status windows opens for Windows.
|
||||
* GODT-1519: Move back to account view after sending bug report.
|
||||
* Other: fix QML error with Qt 6.4 and a typo.
|
||||
|
||||
## [Bridge 2.4.3] Osney
|
||||
|
||||
### Changed
|
||||
* Other: implemented tokens in bridge-gui-tester.
|
||||
* GODT-1853:
|
||||
* Upgrade dependencies (including x/crypto).
|
||||
* Ignore for CVE-2021-33194 false positive + add several try to gobinsec.
|
||||
* GODT-1853: Improve pipeline:
|
||||
* Update gobinsec cache.
|
||||
* Test-windows branch manual, MR always.
|
||||
* Build needs test-linux and lint to start and keep vcpkg cache on linux.
|
||||
* Builds manuall except linux-qa.
|
||||
* GODT-1893: Bridge-gui sends bridge's log to stdout, stderr.
|
||||
* GODT-1932: Frontend is instantiated before bridge.
|
||||
* GODT-1929: Changed gRPC wait timeout.
|
||||
* Other: gRPC TLS server is generated for every session.
|
||||
* GODT-1917: gRPC service should use random port.
|
||||
* GODT-1924: gRPC identity validation with tokens.
|
||||
* GODT-1344: Notifications for ApiCertError and NoActiveKeyForRecipient.
|
||||
* GODT-1941: Update documentation.
|
||||
* Other: Update golangci-lint to v1.50.0.
|
||||
* GODT-1936: check gRPC server token via interceptors.
|
||||
|
||||
|
||||
|
||||
### Fixed
|
||||
* GUI issues:
|
||||
* GODT-1894: Fixed typo in alreadyLoggedIn event error message.
|
||||
* GODT-1479: Fix hover on “Open Bridge” in status window on macOS.
|
||||
* GODT-1899: Status window menu now closes when window is dismissed.
|
||||
* GODT-1851: Port field error label now wraps.
|
||||
* GODT-1566: GUI shows error notifications for IMAP/SMTP port errors on startup.
|
||||
* GODT-1926: Clear port error messages when cancelling the dialog.
|
||||
* Other: Fixed cocoa related warnings in bridge-gui on macOS.
|
||||
* Build issues:
|
||||
* GODT-1675: Add resrource file to both launcher and bridge-go.
|
||||
* Other: Add WlShellIntegration lib for rpm package.
|
||||
* GODT-1935: Fix resource file generation for both Launcher and Bridge.
|
||||
* GODT-1942: Use `qmake` to find the `QT6DIR`.
|
||||
* Provide launcher for make run-cli target.
|
||||
* GODT-1931: Fixed bridge crash when checking for update while offline.
|
||||
|
||||
## [Bridge 2.4.0] Osney
|
||||
|
||||
### Added
|
||||
* GODT-1551: Upgrade to Qt 6:
|
||||
* Change the app architecture.
|
||||
* Drop therecipe/qt dependency.
|
||||
* Update to go1.18.
|
||||
* Update to Qt 6.3.2.
|
||||
* GODT-1170 GODT-1675: Native Mac M1 release.
|
||||
|
||||
|
||||
## [Bridge 2.3.0] Nihonbashi
|
||||
|
||||
### Added
|
||||
* GODT-1739: Opt-out All Mail visibility in settings file.
|
||||
* GODT-1794: CLI wording.
|
||||
* GODT-1794: Add confirmation dialog and change wording.
|
||||
* GODT-1741: GUI and CLI settings to change visibility of All Mail folder.
|
||||
* GODT-1740: Opt-out All Mail visibility in settings file.
|
||||
|
||||
### Changed
|
||||
* GODT-1737: Improve logging during import.
|
||||
* GODT-1754: Add logs for unilateral updates and SEARCH.
|
||||
|
||||
### Fixed
|
||||
* GODT-1840: Use Safe map for mailboxID cache.
|
||||
* GODT-1795: Fix automatic installation of profile for AppleMail on macOS Ventura beta (qt 5).
|
||||
* GODT-1833: Fix gobinsec cache.
|
||||
* GODT-1799: Fix dependency link.
|
||||
* Other: Update SSL certificate fingerprint for test.
|
||||
|
||||
|
||||
## [Bridge 2.2.2] Millau
|
||||
|
||||
### Added
|
||||
|
||||
316
Makefile
316
Makefile
@ -5,165 +5,178 @@ export GO111MODULE=on
|
||||
GOOS:=$(shell go env GOOS)
|
||||
TARGET_CMD?=Desktop-Bridge
|
||||
TARGET_OS?=${GOOS}
|
||||
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||
|
||||
## Build
|
||||
.PHONY: build build-nogui build-launcher versioner hasher
|
||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||
|
||||
# Keep version hardcoded so app build works also without Git repository.
|
||||
BRIDGE_APP_VERSION?=2.2.2+git
|
||||
BRIDGE_APP_VERSION?=3.0.4+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
APP_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
SRC_ICO:=bridge.ico
|
||||
SRC_ICNS:=Bridge.icns
|
||||
SRC_SVG:=bridge.svg
|
||||
EXE_NAME:=proton-bridge
|
||||
CONFIGNAME:=bridge
|
||||
REVISION:=$(shell git rev-parse --short=10 HEAD)
|
||||
BUILD_TIME:=$(shell date +%FT%T%z)
|
||||
MACOS_MIN_VERSION=11.0
|
||||
|
||||
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
|
||||
BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS}
|
||||
BUILD_FLAGS_GUI:=-tags='${BUILD_TAGS} build_qt'
|
||||
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/v2/internal/constants.,Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
|
||||
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/v3/internal/constants., Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
|
||||
GO_LDFLAGS+=-X "github.com/ProtonMail/proton-bridge/v3/internal/constants.FullAppName=${APP_FULL_NAME}"
|
||||
|
||||
ifneq "${BUILD_LDFLAGS}" ""
|
||||
GO_LDFLAGS+=${BUILD_LDFLAGS}
|
||||
GO_LDFLAGS+=${BUILD_LDFLAGS}
|
||||
endif
|
||||
GO_LDFLAGS_LAUNCHER:=${GO_LDFLAGS}
|
||||
ifeq "${TARGET_OS}" "windows"
|
||||
GO_LDFLAGS_LAUNCHER+=-H=windowsgui
|
||||
#GO_LDFLAGS+=-H=windowsgui # Disabled so we can inspect trace logs from the bridge for debugging.
|
||||
GO_LDFLAGS_LAUNCHER+=-H=windowsgui # Having this flag prevent a temporary cmd.exe window from popping when starting the application on Windows 11.
|
||||
endif
|
||||
|
||||
BUILD_FLAGS+=-ldflags '${GO_LDFLAGS}'
|
||||
BUILD_FLAGS_GUI+=-ldflags '${GO_LDFLAGS}'
|
||||
BUILD_FLAGS_GUI+=-ldflags "${GO_LDFLAGS}"
|
||||
BUILD_FLAGS_LAUNCHER+=-ldflags '${GO_LDFLAGS_LAUNCHER}'
|
||||
|
||||
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
|
||||
ICO_FILES:=
|
||||
DIRNAME:=$(shell basename ${CURDIR})
|
||||
EXE:=${EXE_NAME}
|
||||
EXE_QT:=${DIRNAME}
|
||||
|
||||
LAUNCHER_EXE:=proton-bridge
|
||||
BRIDGE_EXE=bridge
|
||||
BRIDGE_GUI_EXE_NAME=bridge-gui
|
||||
BRIDGE_GUI_EXE=${BRIDGE_GUI_EXE_NAME}
|
||||
LAUNCHER_PATH:=cmd/launcher
|
||||
|
||||
ifeq "${TARGET_OS}" "windows"
|
||||
EXE:=${EXE}.exe
|
||||
EXE_QT:=${EXE_QT}.exe
|
||||
RESOURCE_FILE:=resource.syso
|
||||
BRIDGE_EXE:=${BRIDGE_EXE}.exe
|
||||
BRIDGE_GUI_EXE:=${BRIDGE_GUI_EXE}.exe
|
||||
LAUNCHER_EXE:=${LAUNCHER_EXE}.exe
|
||||
RESOURCE_FILE:=resource.syso
|
||||
endif
|
||||
ifeq "${TARGET_OS}" "darwin"
|
||||
DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents
|
||||
EXE:=${EXE}.app
|
||||
EXE_QT:=${EXE_QT}.app
|
||||
EXE_BINARY_DARWIN:=/Contents/MacOS/${EXE_NAME}
|
||||
BRIDGE_EXE_NAME:=${BRIDGE_EXE}
|
||||
BRIDGE_EXE:=${BRIDGE_EXE}.app
|
||||
BRIDGE_GUI_EXE:=${BRIDGE_GUI_EXE}.app
|
||||
EXE_BINARY_DARWIN:=Contents/MacOS/${BRIDGE_GUI_EXE_NAME}
|
||||
EXE_TARGET_DARWIN:=${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}.app
|
||||
DARWINAPP_CONTENTS:=${EXE_TARGET_DARWIN}/Contents
|
||||
endif
|
||||
EXE_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE}
|
||||
EXE_QT_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE_QT}
|
||||
EXE_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${BRIDGE_EXE}
|
||||
EXE_GUI_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${BRIDGE_GUI_EXE}
|
||||
|
||||
TGZ_TARGET:=bridge_${TARGET_OS}_${REVISION}.tgz
|
||||
|
||||
ifdef QT_API
|
||||
VENDOR_TARGET:=prepare-vendor update-qt-docs
|
||||
VENDOR_TARGET:=prepare-vendor update-qt-docs
|
||||
else
|
||||
VENDOR_TARGET=update-vendor
|
||||
VENDOR_TARGET=update-vendor
|
||||
endif
|
||||
|
||||
build: ${TGZ_TARGET}
|
||||
build: build-gui
|
||||
|
||||
build-nogui: gofiles
|
||||
go build ${BUILD_FLAGS} -o ${EXE_NAME} cmd/${TARGET_CMD}/main.go
|
||||
build-gui: ${TGZ_TARGET}
|
||||
|
||||
build-nogui: ${EXE_NAME} build-launcher
|
||||
ifeq "${TARGET_OS}" "darwin"
|
||||
mv ${BRIDGE_EXE} ${BRIDGE_EXE_NAME}
|
||||
endif
|
||||
|
||||
go-build=go build $(1) -o $(2) $(3)
|
||||
go-build-finalize=${go-build}
|
||||
ifeq "${GOOS}-$(shell uname -m)" "darwin-arm64"
|
||||
go-build-finalize= \
|
||||
MACOSX_DEPLOYMENT_TARGET=${MACOS_MIN_VERSION} CGO_ENABLED=1 CGO_CFLAGS="-mmacosx-version-min=${MACOS_MIN_VERSION}" GOARCH=arm64 $(call go-build,$(1),$(2)_arm,$(3)) && \
|
||||
MACOSX_DEPLOYMENT_TARGET=${MACOS_MIN_VERSION} CGO_ENABLED=1 CGO_CFLAGS="-mmacosx-version-min=${MACOS_MIN_VERSION}" GOARCH=amd64 $(call go-build,$(1),$(2)_amd,$(3)) && \
|
||||
lipo -create -output $(2) $(2)_arm $(2)_amd && rm -f $(2)_arm $(2)_amd
|
||||
endif
|
||||
|
||||
ifeq "${GOOS}" "windows"
|
||||
PRERESOURCECMD:=cp ./resource.syso ./cmd/launcher/resource.syso
|
||||
POSTRESOURCECMD:=rm -f ./cmd/launcher/resource.syso
|
||||
go-build-finalize= \
|
||||
powershell Copy-Item ${ROOT_DIR}/${RESOURCE_FILE} ${4} && \
|
||||
$(call go-build,$(1),$(2),$(3)) && \
|
||||
powershell Remove-Item ${4} -Force
|
||||
endif
|
||||
|
||||
${EXE_NAME}: gofiles ${RESOURCE_FILE}
|
||||
$(call go-build-finalize,${BUILD_FLAGS},"${LAUNCHER_EXE}","./cmd/${TARGET_CMD}/","${ROOT_DIR}/cmd/${TARGET_CMD}/${RESOURCE_FILE}")
|
||||
mv ${LAUNCHER_EXE} ${BRIDGE_EXE}
|
||||
|
||||
build-launcher: ${RESOURCE_FILE}
|
||||
${PRERESOURCECMD}
|
||||
go build ${BUILD_FLAGS_LAUNCHER} -o launcher-${EXE} ./cmd/launcher/
|
||||
${POSTRESOURCECMD}
|
||||
$(call go-build-finalize,${BUILD_FLAGS_LAUNCHER},"${LAUNCHER_EXE}","${ROOT_DIR}/${LAUNCHER_PATH}/","${ROOT_DIR}/${LAUNCHER_PATH}/${RESOURCE_FILE}")
|
||||
|
||||
versioner:
|
||||
go build ${BUILD_FLAGS} -o versioner utils/versioner/main.go
|
||||
|
||||
vault-editor:
|
||||
go build -tags debug -o vault-editor utils/vault-editor/main.go
|
||||
|
||||
hasher:
|
||||
go build -o hasher utils/hasher/main.go
|
||||
|
||||
${TGZ_TARGET}: ${DEPLOY_DIR}/${TARGET_OS}
|
||||
rm -f $@
|
||||
cd ${DEPLOY_DIR}/${TARGET_OS} && tar -czvf ../../../../$@ .
|
||||
tar -czvf $@ -C ${DEPLOY_DIR}/${TARGET_OS} .
|
||||
|
||||
${DEPLOY_DIR}/linux: ${EXE_TARGET}
|
||||
${DEPLOY_DIR}/linux: ${EXE_TARGET} build-launcher
|
||||
cp -pf ./dist/${SRC_SVG} ${DEPLOY_DIR}/linux/logo.svg
|
||||
cp -pf ./LICENSE ${DEPLOY_DIR}/linux/
|
||||
cp -pf ./Changelog.md ${DEPLOY_DIR}/linux/
|
||||
cp -pf ./dist/${EXE_NAME}.desktop ${DEPLOY_DIR}/linux/
|
||||
mv ${LAUNCHER_EXE} ${DEPLOY_DIR}/linux/
|
||||
|
||||
${DEPLOY_DIR}/darwin: ${EXE_TARGET}
|
||||
if [ "${DIRNAME}" != "${EXE_NAME}" ]; then \
|
||||
mv ${EXE_TARGET}/Contents/MacOS/{${DIRNAME},${EXE_NAME}}; \
|
||||
perl -i -pe"s/>${DIRNAME}/>${EXE_NAME}/g" ${EXE_TARGET}/Contents/Info.plist; \
|
||||
fi
|
||||
${DEPLOY_DIR}/darwin: ${EXE_TARGET} build-launcher
|
||||
mv ${EXE_GUI_TARGET} ${EXE_TARGET_DARWIN}
|
||||
mv ${EXE_TARGET} ${DARWINAPP_CONTENTS}/MacOS/${BRIDGE_EXE_NAME}
|
||||
perl -i -pe"s/>${BRIDGE_GUI_EXE_NAME}/>${LAUNCHER_EXE}/g" ${DARWINAPP_CONTENTS}/Info.plist
|
||||
cp ./dist/${SRC_ICNS} ${DARWINAPP_CONTENTS}/Resources/${SRC_ICNS}
|
||||
cp LICENSE ${DARWINAPP_CONTENTS}/Resources/
|
||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework"
|
||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework"
|
||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngineCore.framework"
|
||||
./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}${EXE_BINARY_DARWIN}"
|
||||
mv ${LAUNCHER_EXE} ${DARWINAPP_CONTENTS}/MacOS/${LAUNCHER_EXE}
|
||||
./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET_DARWIN}/${EXE_BINARY_DARWIN}"
|
||||
|
||||
${DEPLOY_DIR}/windows: ${EXE_TARGET}
|
||||
${DEPLOY_DIR}/windows: ${EXE_TARGET} build-launcher
|
||||
cp ./dist/${SRC_ICO} ${DEPLOY_DIR}/windows/logo.ico
|
||||
cp LICENSE ${DEPLOY_DIR}/windows/
|
||||
|
||||
QT_BUILD_TARGET:=build desktop
|
||||
ifneq "${GOOS}" "${TARGET_OS}"
|
||||
ifeq "${TARGET_OS}" "windows"
|
||||
QT_BUILD_TARGET:=-docker build windows_64_shared
|
||||
endif
|
||||
endif
|
||||
|
||||
${EXE_TARGET}: check-has-go gofiles ${RESOURCE_FILE} ${VENDOR_TARGET}
|
||||
rm -rf deploy ${TARGET_OS} ${DEPLOY_DIR}
|
||||
cp cmd/${TARGET_CMD}/main.go .
|
||||
qtdeploy ${BUILD_FLAGS_GUI} ${QT_BUILD_TARGET}
|
||||
mv deploy cmd/${TARGET_CMD}
|
||||
if [ "${EXE_QT_TARGET}" != "${EXE_TARGET}" ]; then mv ${EXE_QT_TARGET} ${EXE_TARGET}; fi
|
||||
rm -rf ${TARGET_OS} main.go
|
||||
cp LICENSE ${DEPLOY_DIR}/windows/LICENSE.txt
|
||||
mv ${LAUNCHER_EXE} ${DEPLOY_DIR}/windows/$(notdir ${LAUNCHER_EXE})
|
||||
# plugins are installed in a plugins folder while needs to be near the exe
|
||||
cp -rf ${DEPLOY_DIR}/windows/plugins/* ${DEPLOY_DIR}/windows/.
|
||||
rm -rf ${DEPLOY_DIR}/windows/plugins
|
||||
|
||||
${EXE_TARGET}: check-build-essentials ${EXE_NAME}
|
||||
cd internal/frontend/bridge-gui/bridge-gui && \
|
||||
BRIDGE_APP_FULL_NAME="${APP_FULL_NAME}" \
|
||||
BRIDGE_VENDOR="${APP_VENDOR}" \
|
||||
BRIDGE_APP_VERSION=${APP_VERSION} \
|
||||
BRIDGE_REVISION=${REVISION} \
|
||||
BRIDGE_BUILD_TIME=${BUILD_TIME} \
|
||||
BRIDGE_GUI_BUILD_CONFIG=Release \
|
||||
BRIDGE_INSTALL_PATH=${ROOT_DIR}/${DEPLOY_DIR}/${GOOS} \
|
||||
./build.sh install
|
||||
mv "${ROOT_DIR}/${BRIDGE_EXE}" "$(ROOT_DIR)/${EXE_TARGET}"
|
||||
|
||||
WINDRES_YEAR:=$(shell date +%Y)
|
||||
APP_VERSION_COMMA:=$(shell echo "${APP_VERSION}" | sed -e 's/[^0-9,.]*//g' -e 's/\./,/g')
|
||||
resource.syso: ./dist/info.rc ./dist/${SRC_ICO} .FORCE
|
||||
${RESOURCE_FILE}: ./dist/info.rc ./dist/${SRC_ICO} .FORCE
|
||||
rm -f ./*.syso
|
||||
windres --target=pe-x86-64 -I ./internal/frontend/share/ -D ICO_FILE=${SRC_ICO} -D EXE_NAME="${EXE_NAME}" -D FILE_VERSION="${APP_VERSION}" -D ORIGINAL_FILE_NAME="${EXE}" -D PRODUCT_VERSION="${APP_VERSION}" -D FILE_VERSION_COMMA=${APP_VERSION_COMMA} -D YEAR=${WINDRES_YEAR} -o $@ $<
|
||||
|
||||
## Rules for therecipe/qt
|
||||
.PHONY: prepare-vendor update-vendor update-qt-docs
|
||||
THERECIPE_ENV:=github.com/therecipe/env_${TARGET_OS}_amd64_513
|
||||
|
||||
# vendor folder will be deleted by gomod hence we cache the big repo
|
||||
# therecipe/env in order to download it only once
|
||||
vendor-cache/${THERECIPE_ENV}:
|
||||
git clone https://${THERECIPE_ENV}.git vendor-cache/${THERECIPE_ENV}
|
||||
if [ "${TARGET_OS}" == "darwin" ]; then cp -f "./utils/QTBUG-88600/libqcocoa.dylib" "./vendor-cache/${THERECIPE_ENV}/5.13.0/clang_64/plugins/platforms/"; fi;
|
||||
|
||||
# The command used to make symlinks is different on windows.
|
||||
# So if the GOOS is windows and we aren't crossbuilding (in which case the host os would still be *nix)
|
||||
# we need to change the LINKCMD to something windowsy.
|
||||
LINKCMD:=ln -sf ${CURDIR}/vendor-cache/${THERECIPE_ENV} vendor/${THERECIPE_ENV}
|
||||
ifeq "${GOOS}" "windows"
|
||||
WINDIR:=$(subst /c/,c:\\,${CURDIR})/vendor-cache/${THERECIPE_ENV}
|
||||
LINKCMD:=cmd //c 'mklink $(subst /,\,vendor\${THERECIPE_ENV} ${WINDIR})'
|
||||
endif
|
||||
|
||||
prepare-vendor:
|
||||
go install -v -tags=no_env github.com/therecipe/qt/cmd/...
|
||||
go mod vendor
|
||||
|
||||
# update-vendor is PHONY because we need to make sure that we always have updated vendor
|
||||
update-vendor: vendor-cache/${THERECIPE_ENV} prepare-vendor
|
||||
${LINKCMD}
|
||||
|
||||
update-qt-docs:
|
||||
go get github.com/therecipe/qt/internal/binding/files/docs/$(QT_API)
|
||||
windres --target=pe-x86-64 \
|
||||
-I ./internal/frontend/share/ \
|
||||
-D ICO_FILE=${SRC_ICO} \
|
||||
-D EXE_NAME="${EXE_NAME}" \
|
||||
-D FILE_VERSION="${APP_VERSION}" \
|
||||
-D ORIGINAL_FILE_NAME="${EXE}" \
|
||||
-D PRODUCT_VERSION="${APP_VERSION}" \
|
||||
-D FILE_VERSION_COMMA=${APP_VERSION_COMMA} \
|
||||
-D YEAR=${WINDRES_YEAR} \
|
||||
-o ./${RESOURCE_FILE} $<
|
||||
|
||||
## Dev dependencies
|
||||
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
|
||||
LINTVER:="v1.39.0"
|
||||
LINTVER:="v1.50.0"
|
||||
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
|
||||
|
||||
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
|
||||
@ -177,16 +190,29 @@ install-linter: check-has-go
|
||||
curl -sfL $(LINTSRC) | sh -s -- -b $(shell go env GOPATH)/bin $(LINTVER)
|
||||
|
||||
install-go-mod-outdated:
|
||||
which go-mod-outdated || go get -u github.com/psampaz/go-mod-outdated
|
||||
which go-mod-outdated || go install github.com/psampaz/go-mod-outdated@latest
|
||||
|
||||
install-git-hooks:
|
||||
cp utils/githooks/* .git/hooks/
|
||||
chmod +x .git/hooks/*
|
||||
|
||||
## Checks, mocks and docs
|
||||
.PHONY: check-has-go add-license change-copyright-year test bench coverage mocks lint-license lint-golang lint updates doc release-notes
|
||||
.PHONY: check-has-go check-build-essentials add-license change-copyright-year test bench coverage mocks lint-license lint-golang lint updates doc release-notes
|
||||
check-has-go:
|
||||
@which go || (echo "Install Go-lang!" && exit 1)
|
||||
go version
|
||||
|
||||
|
||||
check_is_installed=if ! which $(1) > /dev/null; then echo "Please install $(1)"; exit 1; fi
|
||||
check-build-essentials:
|
||||
@$(call check_is_installed,zip)
|
||||
@$(call check_is_installed,unzip)
|
||||
@$(call check_is_installed,tar)
|
||||
@$(call check_is_installed,curl)
|
||||
ifneq "${GOOS}" "windows"
|
||||
@$(call check_is_installed,cmake)
|
||||
@$(call check_is_installed,ninja)
|
||||
endif
|
||||
|
||||
add-license:
|
||||
./utils/missing_license.sh add
|
||||
@ -195,26 +221,19 @@ change-copyright-year:
|
||||
./utils/missing_license.sh change-year
|
||||
|
||||
test: gofiles
|
||||
@# Listing packages manually to not run Qt folder (which needs to run qtsetup first) and integration tests.
|
||||
go test -coverprofile=/tmp/coverage.out -run=${TESTRUN} \
|
||||
./internal/api/... \
|
||||
./internal/bridge/... \
|
||||
./internal/config/... \
|
||||
./internal/constants/... \
|
||||
./internal/cookies/... \
|
||||
./internal/crash/... \
|
||||
./internal/events/... \
|
||||
./internal/frontend/cli/... \
|
||||
./internal/imap/... \
|
||||
./internal/locations/... \
|
||||
./internal/logging/... \
|
||||
./internal/metrics/... \
|
||||
./internal/smtp/... \
|
||||
./internal/store/... \
|
||||
./internal/updater/... \
|
||||
./internal/users/... \
|
||||
./internal/versioner/... \
|
||||
./pkg/...
|
||||
go test -v -timeout=5m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
|
||||
test-race: gofiles
|
||||
go test -v -timeout=30m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
|
||||
test-integration: gofiles
|
||||
go test -v -timeout=10m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
|
||||
|
||||
test-integration-debug: gofiles
|
||||
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1
|
||||
|
||||
test-integration-race: gofiles
|
||||
go test -v -timeout=60m -p=1 -count=1 -race -failfast github.com/ProtonMail/proton-bridge/v3/tests
|
||||
|
||||
bench:
|
||||
go test -run '^$$' -bench=. -memprofile bench_mem.pprof -cpuprofile bench_cpu.pprof ./internal/store
|
||||
@ -224,16 +243,12 @@ bench:
|
||||
coverage: test
|
||||
go tool cover -html=/tmp/coverage.out -o=coverage.html
|
||||
|
||||
integration-test-bridge:
|
||||
${MAKE} -C test test-bridge
|
||||
|
||||
mocks:
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/users Locator,PanicHandler,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/pkg/listener Listener > internal/users/mocks/listener_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/store PanicHandler,BridgeUser,ChangeNotifier,Storer > internal/store/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/pkg/listener Listener > internal/store/mocks/utils_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/pkg/pmapi Client,Manager > pkg/pmapi/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/pkg/message Fetcher > pkg/message/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/bridge TLSReporter,ProxyController,Autostarter > tmp
|
||||
mv tmp internal/bridge/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/async PanicHandler > internal/bridge/mocks/async_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/gluon/reporter Reporter > internal/bridge/mocks/gluon_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
|
||||
|
||||
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog
|
||||
|
||||
@ -251,6 +266,13 @@ lint-golang:
|
||||
$(info linting with GOMAXPROCS=${GOMAXPROCS})
|
||||
golangci-lint run ./...
|
||||
|
||||
gobinsec: gobinsec-cache.yml build
|
||||
gobinsec -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}
|
||||
|
||||
gobinsec-cache.yml:
|
||||
./utils/gobinsec_update.sh
|
||||
cp ./utils/gobinsec_update/gobinsec-cache-valid.yml ./gobinsec-cache.yml
|
||||
|
||||
updates: install-go-mod-outdated
|
||||
# Uncomment the "-ci" to fail the job if something can be updated.
|
||||
go list -u -m -json all | go-mod-outdated -update -direct #-ci
|
||||
@ -271,47 +293,57 @@ gofiles: ./internal/bridge/credits.go
|
||||
cd ./utils/ && ./credits.sh bridge
|
||||
|
||||
## Run and debug
|
||||
.PHONY: run run-qt run-qt-cli run-nogui run-nogui-cli run-debug run-qml-preview clean-vendor clean-frontend-qt clean-frontend-qt-common clean
|
||||
.PHONY: run run-qt run-qt-cli run-nogui run-cli run-noninteractive run-debug run-qml-preview clean-vendor clean-frontend-qt clean-frontend-qt-common clean
|
||||
|
||||
LOG?=debug
|
||||
LOG_IMAP?=client # client/server/all, or empty to turn it off
|
||||
LOG_SMTP?=--log-smtp # empty to turn it off
|
||||
RUN_FLAGS?=-m -l=${LOG} --log-imap=${LOG_IMAP} ${LOG_SMTP}
|
||||
RUN_FLAGS?=-l=${LOG} --log-imap=${LOG_IMAP} ${LOG_SMTP}
|
||||
|
||||
run: run-nogui-cli
|
||||
run: run-qt
|
||||
|
||||
run-qt: ${EXE_TARGET}
|
||||
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} 2>&1 | tee last.log
|
||||
run-qt-cli: ${EXE_TARGET}
|
||||
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c
|
||||
run-cli: run-nogui
|
||||
|
||||
run-nogui: clean-vendor gofiles
|
||||
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} | tee last.log
|
||||
run-nogui-cli: clean-vendor gofiles
|
||||
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} -c
|
||||
run-noninteractive: build-nogui clean-vendor gofiles
|
||||
PROTONMAIL_ENV=dev ./${LAUNCHER_EXE} ${RUN_FLAGS} -n
|
||||
|
||||
run-qt: build-gui
|
||||
ifeq "${TARGET_OS}" "darwin"
|
||||
PROTONMAIL_ENV=dev ${DARWINAPP_CONTENTS}/MacOS/${LAUNCHER_EXE} ${RUN_FLAGS}
|
||||
else
|
||||
PROTONMAIL_ENV=dev ./${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE} ${RUN_FLAGS}
|
||||
endif
|
||||
|
||||
run-nogui: build-nogui clean-vendor gofiles
|
||||
PROTONMAIL_ENV=dev ./${LAUNCHER_EXE} ${RUN_FLAGS} -c
|
||||
|
||||
run-debug:
|
||||
PROTONMAIL_ENV=dev dlv debug --build-flags "${BUILD_FLAGS}" cmd/${TARGET_CMD}/main.go -- ${RUN_FLAGS} --noninteractive
|
||||
dlv debug ./cmd/Desktop-Bridge/main.go -- -l=debug
|
||||
|
||||
run-qml-preview:
|
||||
find internal/frontend/qml/ -iname '*qmlc' | xargs rm -f
|
||||
bridge_preview internal/frontend/qml/Bridge_test.qml
|
||||
|
||||
|
||||
clean-frontend-qt:
|
||||
$(MAKE) -C internal/frontend -f Makefile.local clean
|
||||
|
||||
clean-vendor: clean-frontend-qt clean-frontend-qt-common
|
||||
clean-vendor:
|
||||
rm -rf ./vendor
|
||||
|
||||
clean: clean-vendor
|
||||
clean-gui:
|
||||
cd internal/frontend/bridge-gui/ && \
|
||||
rm -f Version.h && \
|
||||
rm -rf cmake-build-*/
|
||||
|
||||
clean-vcpkg:
|
||||
git submodule deinit -f ./extern/vcpkg
|
||||
rm -rf ./.git/submodule/vcpkg
|
||||
rm -rf ./extern/vcpkg
|
||||
git checkout -- extern/vcpkg
|
||||
|
||||
clean: clean-vendor clean-gui clean-vcpkg
|
||||
rm -rf vendor-cache
|
||||
rm -rf cmd/Desktop-Bridge/deploy
|
||||
rm -rf cmd/Import-Export/deploy
|
||||
rm -f build last.log mem.pprof main.go
|
||||
rm -f resource.syso
|
||||
rm -f ./*.syso
|
||||
rm -f release-notes/bridge.html
|
||||
rm -f release-notes/import-export.html
|
||||
rm -f ${LAUNCHER_EXE} ${BRIDGE_EXE} ${BRIDGE_EXE_NAME}
|
||||
|
||||
|
||||
.PHONY: generate
|
||||
generate:
|
||||
|
||||
17
README.md
17
README.md
@ -22,22 +22,7 @@ to start Bridge on startup is enabled by default.
|
||||
When the main window is closed, Bridge will continue to run in the
|
||||
background.
|
||||
|
||||
More details [on the public website](https://protonmail.com/bridge).
|
||||
|
||||
## Description Import-Export app
|
||||
Proton Mail Import-Export app for importing and exporting messages.
|
||||
|
||||
To transfer messages, firstly log in using your Proton Mail credentials.
|
||||
For import, expand your account, and pick the address to which to import
|
||||
messages from IMAP server or local EML or MBOX files. For export, pick
|
||||
the whole account or only a specific address. Then, in both cases,
|
||||
configure transfer rules (match source and target mailboxes, set time
|
||||
range limits and so on) and hit start. Once the transfer is complete,
|
||||
check the results.
|
||||
|
||||
More details [on the public website](https://protonmail.com/import-export).
|
||||
|
||||
The Import-Export app is developed in separate branch `master-ie`.
|
||||
More details [on the public website](https://proton.me/mail/bridge).
|
||||
|
||||
## Launchers
|
||||
Launchers are binaries used to run the Proton Mail Bridge or Import-Export apps.
|
||||
|
||||
@ -17,6 +17,15 @@
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/app"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
/*
|
||||
___....___
|
||||
^^ __..-:'':__:..:__:'':-..__
|
||||
@ -34,41 +43,8 @@ package main
|
||||
~~^_~^~/ \~^-~^~ _~^-~_^~-^~_^~~-^~_~^~-~_~-^~_^/ \~^ ~~_ ^
|
||||
*/
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/app/base"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/app/bridge"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
appName = "Proton Mail Bridge"
|
||||
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
||||
configName = "bridge"
|
||||
updateURLName = "bridge"
|
||||
keychainName = "bridge"
|
||||
cacheVersion = "c11"
|
||||
)
|
||||
|
||||
func main() {
|
||||
base, err := base.New(
|
||||
appName,
|
||||
appUsage,
|
||||
configName,
|
||||
updateURLName,
|
||||
keychainName,
|
||||
cacheVersion,
|
||||
)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to create app base")
|
||||
}
|
||||
// Other instance already running.
|
||||
if base == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := bridge.New(base).Run(os.Args); err != nil {
|
||||
logrus.WithError(err).Fatal("Bridge exited with error")
|
||||
if err := app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") })); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,133 +22,201 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/crash"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/versioner"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/elastic/go-sysinfo"
|
||||
"github.com/elastic/go-sysinfo/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
const (
|
||||
appName = "Proton Mail Launcher"
|
||||
configName = "bridge"
|
||||
exeName = "proton-bridge"
|
||||
appName = "Proton Mail Launcher"
|
||||
exeName = "bridge"
|
||||
guiName = "bridge-gui"
|
||||
|
||||
FlagCLI = "cli"
|
||||
FlagCLIShort = "c"
|
||||
FlagNonInteractive = "noninteractive"
|
||||
FlagNonInteractiveShort = "n"
|
||||
FlagLauncher = "--launcher"
|
||||
FlagWait = "--wait"
|
||||
)
|
||||
|
||||
func main() { //nolint:funlen
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
l := logrus.WithField("launcher_version", constants.Version)
|
||||
|
||||
reporter := sentry.NewReporter(appName, constants.Version, useragent.New())
|
||||
|
||||
crashHandler := crash.NewHandler(reporter.ReportException)
|
||||
defer crashHandler.HandlePanic()
|
||||
|
||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
|
||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, constants.ConfigName))
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to get locations provider")
|
||||
l.WithError(err).Fatal("Failed to get locations provider")
|
||||
}
|
||||
|
||||
locations := locations.New(locationsProvider, configName)
|
||||
locations := locations.New(locationsProvider, constants.ConfigName)
|
||||
|
||||
logsPath, err := locations.ProvideLogsPath()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to get logs path")
|
||||
l.WithError(err).Fatal("Failed to get logs path")
|
||||
}
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
|
||||
if err := logging.Init(logsPath); err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to setup logging")
|
||||
if err := logging.Init(logsPath, os.Getenv("VERBOSITY")); err != nil {
|
||||
l.WithError(err).Fatal("Failed to setup logging")
|
||||
}
|
||||
|
||||
logging.SetLevel(os.Getenv("VERBOSITY"))
|
||||
|
||||
updatesPath, err := locations.ProvideUpdatesPath()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to get updates path")
|
||||
l.WithError(err).Fatal("Failed to get updates path")
|
||||
}
|
||||
|
||||
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to create new verification key")
|
||||
l.WithError(err).Fatal("Failed to create new verification key")
|
||||
}
|
||||
|
||||
kr, err := crypto.NewKeyRing(key)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to create new verification keyring")
|
||||
l.WithError(err).Fatal("Failed to create new verification keyring")
|
||||
}
|
||||
|
||||
versioner := versioner.New(updatesPath)
|
||||
|
||||
exe, err := getPathToUpdatedExecutable(exeName, versioner, kr, reporter)
|
||||
if err != nil {
|
||||
if exe, err = getFallbackExecutable(exeName, versioner); err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to find any launchable executable")
|
||||
}
|
||||
}
|
||||
|
||||
launcher, err := os.Executable()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to determine path to launcher")
|
||||
}
|
||||
|
||||
cmd := execabs.Command(exe, appendLauncherPath(launcher, os.Args[1:])...) //nolint:gosec
|
||||
l = l.WithField("launcher_path", launcher)
|
||||
|
||||
args := os.Args[1:]
|
||||
|
||||
exe, err := getPathToUpdatedExecutable(filepath.Base(launcher), versioner, kr, reporter)
|
||||
if err != nil {
|
||||
exeToLaunch := guiName
|
||||
if inCLIMode(args) {
|
||||
exeToLaunch = exeName
|
||||
}
|
||||
|
||||
l = l.WithField("exe_to_launch", exeToLaunch)
|
||||
l.WithError(err).Info("No more updates found, looking up bridge executable")
|
||||
|
||||
path, err := versioner.GetExecutableInDirectory(exeToLaunch, filepath.Dir(launcher))
|
||||
if err != nil {
|
||||
l.WithError(err).Fatal("No executable in launcher directory")
|
||||
}
|
||||
|
||||
exe = path
|
||||
}
|
||||
|
||||
l = l.WithField("exe_path", exe)
|
||||
|
||||
args, wait, mainExe := findAndStripWait(args)
|
||||
if wait {
|
||||
waitForProcessToFinish(mainExe)
|
||||
}
|
||||
|
||||
cmd := execabs.Command(exe, appendLauncherPath(launcher, args)...) //nolint:gosec
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
// On windows, if you use Run(), a terminal stays open; we don't want that.
|
||||
if runtime.GOOS == "windows" {
|
||||
if //goland:noinspection GoBoolExpressions
|
||||
runtime.GOOS == "windows" {
|
||||
err = cmd.Start()
|
||||
} else {
|
||||
err = cmd.Run()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to launch")
|
||||
l.WithError(err).Fatal("Failed to launch")
|
||||
}
|
||||
}
|
||||
|
||||
// appendLauncherPath add launcher path if missing.
|
||||
func appendLauncherPath(path string, args []string) []string {
|
||||
if !sliceContains(args, FlagLauncher) {
|
||||
res := append([]string{}, args...)
|
||||
res = append(res, FlagLauncher, path)
|
||||
return res
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// sliceContains checks if a value is present in a list.
|
||||
func sliceContains[T comparable](list []T, s T) bool {
|
||||
return xslices.Any(list, func(arg T) bool { return arg == s })
|
||||
}
|
||||
|
||||
// inCLIMode detect if CLI mode is asked.
|
||||
func inCLIMode(args []string) bool {
|
||||
return hasFlag(args, FlagCLI) || hasFlag(args, FlagCLIShort) || hasFlag(args, FlagNonInteractive) || hasFlag(args, FlagNonInteractiveShort)
|
||||
}
|
||||
|
||||
// hasFlag checks if a flag is present in a list.
|
||||
func hasFlag(args []string, flag string) bool {
|
||||
return xslices.Any(args, func(arg string) bool { return (arg == "-"+flag) || (arg == "--"+flag) })
|
||||
}
|
||||
|
||||
// findAndStrip check if a value is present in s list and remove all occurrences of the value from this list.
|
||||
func findAndStrip[T comparable](slice []T, v T) (strippedList []T, found bool) {
|
||||
strippedList = xslices.Filter(slice, func(value T) bool {
|
||||
return value != v
|
||||
})
|
||||
return strippedList, len(strippedList) != len(slice)
|
||||
}
|
||||
|
||||
// findAndStripWait Check for waiter flag get its value and clean them both.
|
||||
func findAndStripWait(args []string) ([]string, bool, string) {
|
||||
res := append([]string{}, args...)
|
||||
|
||||
hasFlag := false
|
||||
var value string
|
||||
|
||||
for k, v := range res {
|
||||
if v != "--launcher" {
|
||||
if v != FlagWait {
|
||||
continue
|
||||
}
|
||||
|
||||
hasFlag = true
|
||||
|
||||
if k+1 >= len(res) {
|
||||
continue
|
||||
}
|
||||
|
||||
res[k+1] = path
|
||||
hasFlag = true
|
||||
value = res[k+1]
|
||||
}
|
||||
|
||||
if !hasFlag {
|
||||
res = append(res, "--launcher", path)
|
||||
if hasFlag {
|
||||
res, _ = findAndStrip(res, FlagWait)
|
||||
res, _ = findAndStrip(res, value)
|
||||
}
|
||||
|
||||
return res
|
||||
return res, hasFlag, value
|
||||
}
|
||||
|
||||
func getPathToUpdatedExecutable(
|
||||
name string,
|
||||
versioner *versioner.Versioner,
|
||||
ver *versioner.Versioner,
|
||||
kr *crypto.KeyRing,
|
||||
reporter *sentry.Reporter,
|
||||
) (string, error) {
|
||||
versions, err := versioner.ListVersions()
|
||||
versions, err := ver.ListVersions()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to list available versions")
|
||||
}
|
||||
@ -159,7 +227,11 @@ func getPathToUpdatedExecutable(
|
||||
}
|
||||
|
||||
for _, version := range versions {
|
||||
vlog := logrus.WithField("version", version)
|
||||
vlog := logrus.WithFields(logrus.Fields{
|
||||
"version": constants.Version,
|
||||
"check_version": version,
|
||||
"name": name,
|
||||
})
|
||||
|
||||
if err := version.VerifyFiles(kr); err != nil {
|
||||
vlog.WithError(err).Error("Files failed verification and will be removed")
|
||||
@ -192,13 +264,45 @@ func getPathToUpdatedExecutable(
|
||||
return "", errors.New("no available newer versions")
|
||||
}
|
||||
|
||||
func getFallbackExecutable(name string, versioner *versioner.Versioner) (string, error) {
|
||||
logrus.Info("Searching for fallback executable")
|
||||
// waitForProcessToFinish waits until the process with the given path is finished.
|
||||
func waitForProcessToFinish(exePath string) {
|
||||
for {
|
||||
processes, err := sysinfo.Processes()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Could not determine running processes")
|
||||
return
|
||||
}
|
||||
|
||||
launcher, err := os.Executable()
|
||||
exeInfo, err := os.Stat(exePath)
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithField("file", exeInfo).Error("Could not retrieve file info")
|
||||
return
|
||||
}
|
||||
|
||||
if xslices.Any(processes, func(process types.Process) bool {
|
||||
info, err := process.Info()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Trace("Could not retrieve process info")
|
||||
return false
|
||||
}
|
||||
|
||||
return sameFile(exeInfo, info.Exe)
|
||||
}) {
|
||||
logrus.Infof("Waiting for %v to finish.", exeInfo.Name())
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func sameFile(info os.FileInfo, path string) bool {
|
||||
pathInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to determine path to launcher")
|
||||
logrus.WithError(err).WithField("file", path).Error("Could not retrieve file info")
|
||||
return false
|
||||
}
|
||||
|
||||
return versioner.GetExecutableInDirectory(name, filepath.Dir(launcher))
|
||||
return os.SameFile(pathInfo, info)
|
||||
}
|
||||
|
||||
58
cmd/launcher/main_test.go
Normal file
58
cmd/launcher/main_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSliceContains(t *testing.T) {
|
||||
assert.True(t, sliceContains([]string{"a", "b", "c"}, "a"))
|
||||
assert.True(t, sliceContains([]int{1, 2, 3}, 2))
|
||||
assert.False(t, sliceContains([]string{"a", "b", "c"}, "A"))
|
||||
assert.False(t, sliceContains([]int{1, 2, 3}, 4))
|
||||
assert.False(t, sliceContains([]string{}, "a"))
|
||||
assert.True(t, sliceContains([]string{"a", "a"}, "a"))
|
||||
}
|
||||
|
||||
func TestFindAndStrip(t *testing.T) {
|
||||
list := []string{"a", "b", "c", "c", "b", "c"}
|
||||
|
||||
result, found := findAndStrip(list, "a")
|
||||
assert.True(t, found)
|
||||
assert.True(t, xslices.Equal(result, []string{"b", "c", "c", "b", "c"}))
|
||||
|
||||
result, found = findAndStrip(list, "c")
|
||||
assert.True(t, found)
|
||||
assert.True(t, xslices.Equal(result, []string{"a", "b", "b"}))
|
||||
|
||||
result, found = findAndStrip([]string{"c", "c", "c"}, "c")
|
||||
assert.True(t, found)
|
||||
assert.True(t, xslices.Equal(result, []string{}))
|
||||
|
||||
result, found = findAndStrip(list, "A")
|
||||
assert.False(t, found)
|
||||
assert.True(t, xslices.Equal(result, list))
|
||||
|
||||
result, found = findAndStrip([]string{}, "a")
|
||||
assert.False(t, found)
|
||||
assert.True(t, xslices.Equal(result, []string{}))
|
||||
}
|
||||
22
dist/bridgeMacOS.svg
vendored
Normal file
22
dist/bridgeMacOS.svg
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 260 260" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g>
|
||||
<g transform="matrix(1.27944,0,0,1.35453,-34.9539,-16.0513)">
|
||||
<path d="M40,62.391C40,38.979 58.979,20 82.391,20L177.609,20C201.021,20 220,38.979 220,62.391L220,157.609C220,181.021 201.021,200 177.609,200L82.391,200C58.979,200 40,181.021 40,157.609L40,62.391Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.41874,0,0,1.41874,-55.214,-18.9171)">
|
||||
<path d="M129.748,48.657C101.923,48.657 79.369,71.21 79.369,99.035L79.369,149.747C79.369,155.139 83.74,159.509 89.131,159.509L171.407,159.509C176.22,159.509 180.126,155.604 180.126,150.79L180.126,99.035C180.126,71.214 157.572,48.657 129.748,48.657ZM158.746,98.755L136.726,117.305C132.752,120.655 126.939,120.655 122.965,117.305L100.945,98.755C100.945,83.014 113.708,70.251 129.45,70.251L130.242,70.251C145.983,70.251 158.746,83.014 158.746,98.755Z" style="fill:rgb(109,74,255);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.41874,0,0,1.41874,-55.214,-18.9171)">
|
||||
<path d="M129.748,48.657C101.923,48.657 79.369,71.21 79.369,99.035L79.369,149.748C79.369,155.139 83.74,159.509 89.131,159.509L171.407,159.509C176.22,159.509 180.126,155.604 180.126,150.79L180.126,99.035C180.126,71.214 157.572,48.657 129.748,48.657ZM158.746,98.755L136.726,117.305C132.752,120.655 126.939,120.655 122.965,117.305L100.945,98.755C100.945,83.014 113.708,70.251 129.45,70.251L130.242,70.251C145.983,70.251 158.746,83.014 158.746,98.755Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.41874,0,0,1.41874,-55.214,-18.9171)">
|
||||
<path d="M136.764,117.529C134.468,119.388 128.499,121.99 122.989,117.529C117.479,113.069 106.044,103.208 101.015,98.835L101.041,98.835L100.946,98.756C100.946,83.014 113.709,70.251 129.45,70.251L130.242,70.251C145.984,70.251 158.746,83.014 158.746,98.756L158.652,98.835L158.737,98.835L158.737,159.51L171.407,159.51C176.221,159.51 180.126,155.604 180.126,150.79L180.126,99.035C180.126,71.214 157.573,48.657 129.748,48.657C101.923,48.657 79.37,71.211 79.37,99.035L79.37,102.219L110.526,129.008C112.822,131.195 118.857,134.256 124.629,129.008C130.401,123.761 135.124,119.169 136.764,117.529Z" style="fill:url(#_Radial2);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(15.0864,-42.636,42.636,15.0864,82.9769,172.3)"><stop offset="0" style="stop-color:rgb(40,176,232);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(197,183,255);stop-opacity:0"/></linearGradient>
|
||||
<radialGradient id="_Radial2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-94.8157,-85.2706,69.7189,-77.5232,174.186,168.693)"><stop offset="0" style="stop-color:rgb(226,219,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(109,74,255);stop-opacity:1"/></radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
2
dist/info.rc
vendored
2
dist/info.rc
vendored
@ -3,7 +3,7 @@
|
||||
|
||||
IDI_ICON1 ICON DISCARDABLE STRINGIZE(ICO_FILE)
|
||||
|
||||
#define FILE_COMMENTS "The Bridge is an application that runs on your computer in the background and seamlessly encrypts and decrypts your mail as it enters and leaves your computer."
|
||||
#define FILE_COMMENTS "Proton Mail Bridge is a desktop application that runs in the background, encrypting and decrypting messages as they enter and leave your computer."
|
||||
#define FILE_DESCRIPTION "Proton Mail Bridge"
|
||||
#define INTERNAL_NAME STRINGIZE(EXE_NAME)
|
||||
#define PRODUCT_NAME "Proton Mail Bridge for Windows"
|
||||
|
||||
2
dist/proton-bridge.desktop
vendored
2
dist/proton-bridge.desktop
vendored
@ -3,7 +3,7 @@ Type=Application
|
||||
Version=1.1
|
||||
Name=Proton Mail Bridge
|
||||
GenericName=Proton Mail Bridge for Linux
|
||||
Comment=The Bridge is an application that runs on your computer in the background and seamlessly encrypts and decrypts your mail as it enters and leaves your computer.
|
||||
Comment=Proton Mail Bridge is a desktop application that runs in the background, encrypting and decrypting messages as they enter and leave your computer.
|
||||
Icon=protonmail-bridge
|
||||
Exec=protonmail-bridge
|
||||
Terminal=false
|
||||
|
||||
1
extern/vcpkg
vendored
Submodule
1
extern/vcpkg
vendored
Submodule
Submodule extern/vcpkg added at f93ba152d5
171
go.mod
171
go.mod
@ -1,80 +1,121 @@
|
||||
module github.com/ProtonMail/proton-bridge/v2
|
||||
module github.com/ProtonMail/proton-bridge/v3
|
||||
|
||||
go 1.15
|
||||
go 1.18
|
||||
|
||||
// These dependencies are `replace`d below, so the version numbers should be ignored.
|
||||
// They are in a separate require block to highlight this.
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||
github.com/Masterminds/semver/v3 v3.1.1
|
||||
github.com/ProtonMail/gluon v0.14.2-0.20221202093012-ad1570c49c8c
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/ProtonMail/go-proton-api v0.1.5
|
||||
github.com/ProtonMail/go-rfc5322 v0.11.0
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10
|
||||
github.com/PuerkitoBio/goquery v1.8.0
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
||||
github.com/bradenaw/juniper v0.8.0
|
||||
github.com/cucumber/godog v0.12.5
|
||||
github.com/cucumber/messages-go/v16 v16.0.1
|
||||
github.com/docker/docker-credential-helpers v0.6.3
|
||||
github.com/emersion/go-imap v1.0.6
|
||||
github.com/elastic/go-sysinfo v1.8.1
|
||||
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317
|
||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
|
||||
github.com/emersion/go-message v0.16.0
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
|
||||
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/getsentry/sentry-go v0.15.0
|
||||
github.com/go-resty/resty/v2 v2.7.0
|
||||
github.com/goccy/go-json v0.9.11
|
||||
github.com/godbus/dbus v4.1.0+incompatible
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba
|
||||
github.com/keybase/go-keychain v0.0.0
|
||||
github.com/miekg/dns v1.1.50
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/profile v1.6.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/urfave/cli/v2 v2.20.3
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||
go.uber.org/goleak v1.2.0
|
||||
golang.org/x/exp v0.0.0-20221023144134-a1e5550cf13e
|
||||
golang.org/x/net v0.1.0
|
||||
golang.org/x/sys v0.1.0
|
||||
golang.org/x/text v0.4.0
|
||||
google.golang.org/grpc v1.50.1
|
||||
google.golang.org/protobuf v1.28.1
|
||||
howett.net/plist v1.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1
|
||||
github.com/Masterminds/semver/v3 v3.1.0
|
||||
ariga.io/atlas v0.7.0 // indirect
|
||||
entgo.io/ent v0.11.2 // indirect
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220623141421-5afb4c282135
|
||||
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
|
||||
github.com/ProtonMail/go-rfc5322 v0.8.0
|
||||
github.com/ProtonMail/go-srp v0.0.5
|
||||
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.7
|
||||
github.com/PuerkitoBio/goquery v1.5.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f // indirect
|
||||
github.com/ProtonMail/go-srp v0.0.5 // indirect
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc
|
||||
github.com/chzyer/logex v1.1.10 // indirect
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
||||
github.com/cucumber/godog v0.12.1
|
||||
github.com/cucumber/messages-go/v16 v16.0.1
|
||||
github.com/elastic/go-sysinfo v1.7.1
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||
github.com/chzyer/test v1.0.0 // indirect
|
||||
github.com/cloudflare/circl v1.2.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
|
||||
github.com/danieljoos/wincred v1.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/elastic/go-windows v1.0.1 // indirect
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a
|
||||
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
|
||||
github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26
|
||||
github.com/emersion/go-message v0.12.1-0.20201221184100-40c3f864532b
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
|
||||
github.com/emersion/go-smtp v0.14.0
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594
|
||||
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
||||
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a // indirect
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/getsentry/sentry-go v0.12.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/google/go-cmp v0.5.5
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/jameskeane/bcrypt v0.0.0-20120420032655-c3cd44c1e20f // indirect
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7
|
||||
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/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/miekg/dns v1.1.41
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
|
||||
github.com/olekukonko/tablewriter v0.0.4 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285
|
||||
github.com/sirupsen/logrus v1.7.0
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.8.1 // indirect
|
||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.1 // indirect
|
||||
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-memdb v1.3.3 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.14.0 // indirect
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.2 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e
|
||||
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d // indirect
|
||||
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d // indirect
|
||||
github.com/urfave/cli/v2 v2.2.0
|
||||
github.com/vmihailenco/msgpack/v5 v5.1.3
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4
|
||||
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b
|
||||
golang.org/x/text v0.3.7
|
||||
howett.net/plist v1.0.0
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
github.com/zclconf/go-cty v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.1.0 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 // indirect
|
||||
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa // indirect
|
||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
|
||||
517
go.sum
517
go.sum
@ -1,3 +1,5 @@
|
||||
ariga.io/atlas v0.7.0 h1:daEFdUsyNm7EHyzcMfjWwq/fVv48fCfad+dIGyobY1k=
|
||||
ariga.io/atlas v0.7.0/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
@ -11,231 +13,220 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 h1:j9HaafapDbPbGRDku6e/HRs6KBMcKHiWcm1/9Sbxnl4=
|
||||
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
entgo.io/ent v0.11.2 h1:UM2/BUhF2FfsxPHRxLjQbhqJNaDdVlOwNIAMLs2jyto=
|
||||
entgo.io/ent v0.11.2/go.mod h1:YGHEQnmmIUgtD5b1ICD5vg74dS3npkNnmC5K+0J+IHU=
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs=
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
||||
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
|
||||
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
|
||||
github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
|
||||
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57 h1:pHA4K54ifoogVLunGGHi3xyF5Nz4x+Uh3dJuy3NwGQQ=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
github.com/ProtonMail/gluon v0.14.2-0.20221202093012-ad1570c49c8c h1:DzVlJERHOHDQjYz/P12VlORS4rF2Ii83cWcYHsXGdng=
|
||||
github.com/ProtonMail/gluon v0.14.2-0.20221202093012-ad1570c49c8c/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20211221144345-a4f6767435ab h1:5FiL/TCaiKCss/BLMIACDxxadYrx767l9kh0qYX+sLQ=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20211221144345-a4f6767435ab/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220113124808-70ae35bab23f/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220623141421-5afb4c282135 h1:xDc/cFH/hwyr9KyWc0sm26lpsscqtfZBvU8NpRLHwJ0=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220623141421-5afb4c282135/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220822140716-1678d6eb0cbe/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 h1:NsReiLpErIPzRrnogAXYwSoU7txA977LjDGrbkewJbg=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
|
||||
github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac h1:2xU3QncAiS/W3UlWZTkbNKW5WkLzk6Egl1T0xX+sbjs=
|
||||
github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
||||
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
|
||||
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
|
||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0=
|
||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f h1:CGq7OieOz3wyQJ1fO8S0eO9TCW1JyvLrf8fhzz1i8ko=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
||||
github.com/ProtonMail/go-rfc5322 v0.8.0 h1:7emrf75n3CDIduQflx7aT1nJa5h/kGsiFKUYX/+IAkU=
|
||||
github.com/ProtonMail/go-rfc5322 v0.8.0/go.mod h1:BwpTbkJxkMGkc+pC84AXZnwuWOisEULBpfPIyIKS/Us=
|
||||
github.com/ProtonMail/go-srp v0.0.1 h1:J0O9Zb5XTC6iDrB7feH41cu+TUEB+l7uHctXIK6oS2o=
|
||||
github.com/ProtonMail/go-srp v0.0.1/go.mod h1:Uvv5cqSGCs8MTZ8sbKiCkBnaB6/OA3eq2mc77tl2VVA=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f h1:4IWzKjHzZxdrW9k4zl/qCwenOVHDbVDADPPHFLjs0Oc=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
|
||||
github.com/ProtonMail/go-proton-api v0.1.5 h1:6RJO3jXP3opFfGqrXNvFTefdD4MlfxKjIebT8r5ROf8=
|
||||
github.com/ProtonMail/go-proton-api v0.1.5/go.mod h1:jqvJ2HqLHqiPJoEb+BTIB1IF7wvr6p+8ZfA6PO2NRNk=
|
||||
github.com/ProtonMail/go-rfc5322 v0.11.0 h1:o5Obrm4DpmQEffvgsVqG6S4BKwC1Wat+hYwjIp2YcCY=
|
||||
github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw=
|
||||
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=
|
||||
github.com/ProtonMail/go-srp v0.0.5/go.mod h1:06iYHtLXW8vjLtccWj++x3MKy65sIT8yZd7nrJF49rs=
|
||||
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
|
||||
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.1 h1:b3El0zabaKi73u4sRnb3hOOUczuKuYpN8wnp7wRsZSc=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.1/go.mod h1:RFjoVjfhV8f78tjz/fLrp/OXkugL3QmWsiJq/fsQYA4=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.7 h1:V3xeelvXgJiZXZuPtSSE+uYbtPw4RmbmyPqXDAESPhg=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.7/go.mod h1:ZW1KxHNG6q5LMgFKf9Ap/d2eVYeyGf5+fAUEAjJWtmo=
|
||||
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10 h1:EYgkxzwmQvsa6kxxkgP1AwzkFqKHscF2UINxaSn6rdI=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10/go.mod h1:CTRA7/toc/4DxDy5Du4hPDnIZnJvXSeQ8LsRTOUJoyc=
|
||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:mZca0/HZ/XWXP9txkfdl2GH6mUzBqAlyJz3u5Lg8fuA=
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
|
||||
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c h1:j/C2kxPfyE0d87/ggAjIsCV5Cdkqmjb+O0W8W+1J+IY=
|
||||
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.mod h1:6AXRstqK+32jeFmw89QGL2748+dj34Av4xc/I9oo9BY=
|
||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220816024939-bc8df83d7b9d/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/bradenaw/juniper v0.8.0 h1:sdanLNdJbLjcLj993VYIwUHlUVkLzvgiD/x9O7cvvxk=
|
||||
github.com/bradenaw/juniper v0.8.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bwesterb/go-ristretto v1.2.1/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.2.0 h1:NheeISPSUcYftKlfrLuOo4T62FkmD4t4jviLfFFYaec=
|
||||
github.com/cloudflare/circl v1.2.0/go.mod h1:Ch2UgYr6ti2KTtlejELlROl0YIYj7SLjAC8M+INXlMk=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cronokirby/saferith v0.31.0 h1:TIlhldetKLeGAb19bZvWiuwQEzfzwSPthDEyJ9Ah8xs=
|
||||
github.com/cronokirby/saferith v0.31.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
||||
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
||||
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
||||
github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE=
|
||||
github.com/cucumber/gherkin-go/v19 v19.0.3/go.mod h1:jY/NP6jUtRSArQQJ5h1FXOUgk5fZK24qtE7vKi776Vw=
|
||||
github.com/cucumber/godog v0.12.1 h1:IhWVYFKDReM5WsuA9AuRLRPWOyvFNO9UBUKrNfLPais=
|
||||
github.com/cucumber/godog v0.12.1/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6Tm9t5pIc=
|
||||
github.com/cucumber/godog v0.12.5 h1:FZIy6VCfMbmGHts9qd6UjBMT9abctws/pQYO/ZcwOVs=
|
||||
github.com/cucumber/godog v0.12.5/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6Tm9t5pIc=
|
||||
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/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/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
|
||||
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
|
||||
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||
github.com/elastic/go-sysinfo v1.7.1 h1:Wx4DSARcKLllpKT2TnFVdSUJOsybqMYCNQZq1/wO+s0=
|
||||
github.com/elastic/go-sysinfo v1.7.1/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0=
|
||||
github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
|
||||
github.com/elastic/go-sysinfo v1.8.1 h1:4Yhj+HdV6WjbCRgGdZpPJ8lZQlXZLKDAeIkmQ/VRvi4=
|
||||
github.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM=
|
||||
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
||||
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a h1:bMdSPm6sssuOFpIaveu3XGAijMS3Tq2S3EqFZmZxidc=
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
|
||||
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0=
|
||||
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
|
||||
github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c h1:khcEdu1yFiZjBgi7gGnQiLhpSgghJ0YTnKD0l4EUqqc=
|
||||
github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c/go.mod h1:iApyhIQBiU4XFyr+3kdJyyGqle82TbQyuP2o+OZHrV0=
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8=
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
|
||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:43mBoVwooyLm1+1YVf5nvn1pSFWhw7rOpcrp1Jg/qk0=
|
||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp0FFboaK/bxsrUz1lNrDMUCsZUsKC5YuM4uRVRVs=
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-smtp v0.14.0 h1:RYW203p+EcPjL8Z/ZpT9lZ6iOc8MG1MQzEx1UKEkXlA=
|
||||
github.com/emersion/go-smtp v0.14.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d h1:hFRM6zCBSc+Xa0rBOqSlG6Qe9dKC/2vLhGAuZlWxTsc=
|
||||
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 h1:n9qx98xiS5V4x2WIpPC2rr9mUM5ri9r/YhCEKbhCHro=
|
||||
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5/go.mod h1:WIi9g8OKJQHXtQbx7GExlo6UAFaui9WDMYabJ+Be4WI=
|
||||
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a h1:cltZpe6s0SJtqK5c/5y2VrIYi8BAtDM6qjmiGYqfTik=
|
||||
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
github.com/getsentry/sentry-go v0.12.0 h1:era7g0re5iY13bHSdN/xMkyV+5zZppjRVQhZrXCaEIk=
|
||||
github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c=
|
||||
github.com/getsentry/sentry-go v0.15.0 h1:CP9bmA7pralrVUedYZsmIHWpq/pBtXTSew7xvVpfLaA=
|
||||
github.com/getsentry/sentry-go v0.15.0/go.mod h1:RZPJKSw+adu8PBNygiri/A98FqVr2HtRckJk9XVxJ9I=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
||||
github.com/go-resty/resty/v2 v2.6.0 h1:joIR5PNLM2EFqqESUjCMGXrWmXNHEU9CEiK813oKYS4=
|
||||
github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q=
|
||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
|
||||
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/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/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc=
|
||||
github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e h1:XWcjeEtTFTOVA9Fs1w7n2XBftk5ib4oZrhzWk0B+3eA=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.0 h1:8exGP7ego3OmkfksihtSouGMZ+hQrhxx+FVELeXpVPE=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-memdb v1.3.0 h1:xdXq34gBOMEloa9rlGStLxmfX/dyIK8htOv36dQUwHU=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-memdb v1.3.0/go.mod h1:Mluclgwib3R93Hk5fxEfiRhB+6Dar64wWh71LpNSe3g=
|
||||
github.com/hashicorp/go-memdb v1.3.3 h1:oGfEWrFuxtIUF3W2q/Jzt6G85TrMk9ey6XfYLvVe1Wo=
|
||||
github.com/hashicorp/go-memdb v1.3.3/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
@ -243,129 +234,98 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/hcl/v2 v2.14.0 h1:jX6+Q38Ly9zaAJlAjnFVyeNSNCKKW8D0wvyg7vij5Wc=
|
||||
github.com/hashicorp/hcl/v2 v2.14.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
|
||||
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
|
||||
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
|
||||
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
|
||||
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
|
||||
github.com/jameskeane/bcrypt v0.0.0-20120420032655-c3cd44c1e20f/go.mod h1:u+9Snq0w+ZdYKi8BBoaxnEwWu0fY4Kvu9ByFpM51t1s=
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc=
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba h1:QFQpJdgbON7I0jr2hYW7Bs+XV0qjc3d5tZoDnRFnqTg=
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
|
||||
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
|
||||
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
|
||||
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
|
||||
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
|
||||
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d h1:gVjhBCfVGl32RIBooOANzfw+0UqX8HU+yPlMv8vypcg=
|
||||
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d/go.mod h1:W6EbaYmb4RldPn0N3gvVHjY1wmU59kbymhW9NATWhwY=
|
||||
github.com/keybase/go-keychain v0.0.0-20211119201326-e02f34051621 h1:aMQ7pA4f06yOVXSulygyGvy4xA94fyzjUGs0iqQdMOI=
|
||||
github.com/keybase/go-keychain v0.0.0-20211119201326-e02f34051621/go.mod h1:enrU/ug069Om7vWxuFE6nikLI2BZNwevMiGSo43Kt5w=
|
||||
github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758=
|
||||
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
|
||||
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
|
||||
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM=
|
||||
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
@ -376,31 +336,29 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285 h1:d54EL9l+XteliUfUCGsEwwuk65dmmxX85VXF+9T6+50=
|
||||
github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285/go.mod h1:fxIDly1xtudczrZeOOlfaUvd2OPb2qZAPuWdU2BsBTk=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
|
||||
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
@ -409,92 +367,72 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk=
|
||||
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
|
||||
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d h1:hAZyEG2swPRWjF0kqqdGERXUazYnRJdAk4a58f14z7Y=
|
||||
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc=
|
||||
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d h1:AJRoBel/g9cDS+yE8BcN3E+TDD/xNAguG21aoR8DAIE=
|
||||
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
|
||||
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/vmihailenco/msgpack/v5 v5.1.3 h1:FwC9KPjyW8OqTUqMt6rQw9y50vA2cTLXPKCcBCRbQgg=
|
||||
github.com/vmihailenco/msgpack/v5 v5.1.3/go.mod h1:C5gboKD0TJPqWDTVTtrQNfRbiBwHZGo8UTqP/9/XvLI=
|
||||
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
|
||||
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/urfave/cli/v2 v2.20.3 h1:lOgGidH/N5loaigd9HjFsOIhXSTrzl7tBpHswZ428w4=
|
||||
github.com/urfave/cli/v2 v2.20.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/zclconf/go-cty v1.11.0 h1:726SxLdi2SDnjY+BStqB9J1hNp4+2WlzyXLuimibIe0=
|
||||
github.com/zclconf/go-cty v1.11.0/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20221023144134-a1e5550cf13e h1:SkwG94eNiiYJhbeDE018Grw09HIN/KB9NlRmZsrzfWs=
|
||||
golang.org/x/exp v0.0.0-20221023144134-a1e5550cf13e/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@ -503,6 +441,7 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
@ -511,10 +450,11 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -522,23 +462,19 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20211008194852-3b03d305991f h1:1scJEYZBaF48BaG6tYbtxmLcXqwYGSfGcMoStTqkkIw=
|
||||
golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -547,77 +483,60 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 h1:0SH2R3f1b1VmIMG7BXbEZCBUu2dKmHschSmjqGUrW8A=
|
||||
golang.org/x/sync v0.0.0-20220907140024-f12130a52804/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY=
|
||||
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8=
|
||||
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
@ -628,12 +547,15 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69 h1:yBHHx+XZqXJBm6Exke3N7V9gnlsyXxoCPEb1yVenjfk=
|
||||
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa h1:uKcci2q7Qtp6nMTC/AAvfNUAldFtJuHWV9/5QWiypts=
|
||||
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
@ -652,39 +574,42 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 h1:K1zaaMdYBXRyX+cwFnxj7M6zwDyumLQMZ5xqwGvjreQ=
|
||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737/go.mod h1:2r/26NEF3bFmT3eC3aZreahSal0C3Shl8Gi6vyDYqOQ=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
|
||||
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
|
||||
@ -1,83 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package api provides HTTP API of the Bridge.
|
||||
//
|
||||
// API endpoints:
|
||||
// * /focus, see focusHandler
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/ports"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var log = logrus.WithField("pkg", "api") //nolint:gochecknoglobals
|
||||
|
||||
type apiServer struct {
|
||||
host string
|
||||
settings *settings.Settings
|
||||
eventListener listener.Listener
|
||||
}
|
||||
|
||||
// NewAPIServer returns prepared API server struct.
|
||||
func NewAPIServer(settings *settings.Settings, eventListener listener.Listener) *apiServer { //nolint:revive
|
||||
return &apiServer{
|
||||
host: bridge.Host,
|
||||
settings: settings,
|
||||
eventListener: eventListener,
|
||||
}
|
||||
}
|
||||
|
||||
// Starts the server.
|
||||
func (api *apiServer) ListenAndServe() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/focus", wrapper(api, focusHandler))
|
||||
|
||||
addr := api.getAddress()
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
log.Info("API listening at ", addr)
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
api.eventListener.Emit(events.ErrorEvent, "API failed: "+err.Error())
|
||||
log.Error("API failed: ", err)
|
||||
}
|
||||
defer server.Close() //nolint:errcheck
|
||||
}
|
||||
|
||||
func (api *apiServer) getAddress() string {
|
||||
port := api.settings.GetInt(settings.APIPortKey)
|
||||
newPort := ports.FindFreePortFrom(port)
|
||||
if newPort != port {
|
||||
api.settings.SetInt(settings.APIPortKey, newPort)
|
||||
}
|
||||
return getAPIAddress(api.host, newPort)
|
||||
}
|
||||
|
||||
func getAPIAddress(host string, port int) string {
|
||||
return fmt.Sprintf("%s:%d", host, port)
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||
)
|
||||
|
||||
// httpHandler with Go's Response and Request.
|
||||
type httpHandler func(http.ResponseWriter, *http.Request)
|
||||
|
||||
// handler with our context.
|
||||
type handler func(handlerContext) error
|
||||
|
||||
type handlerContext struct {
|
||||
req *http.Request
|
||||
resp http.ResponseWriter
|
||||
eventListener listener.Listener
|
||||
}
|
||||
|
||||
func wrapper(api *apiServer, callback handler) httpHandler {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := handlerContext{
|
||||
req: req,
|
||||
resp: w,
|
||||
eventListener: api.eventListener,
|
||||
}
|
||||
err := callback(ctx)
|
||||
if err != nil {
|
||||
log.Error("API callback of ", req.URL, " failed: ", err)
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||
)
|
||||
|
||||
// focusHandler should be called from other instances (attempt to start bridge
|
||||
// for the second time) to get focus in the currently running instance.
|
||||
func focusHandler(ctx handlerContext) error {
|
||||
log.Info("Focus from other instance")
|
||||
ctx.eventListener.Emit(events.SecondInstanceEvent, "")
|
||||
fmt.Fprintf(ctx.resp, "OK")
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckOtherInstanceAndFocus is helper for new instances to check if there is
|
||||
// already a running instance and get it's focus.
|
||||
func CheckOtherInstanceAndFocus(port int) error {
|
||||
addr := getAPIAddress(bridge.Host, port)
|
||||
resp, err := (&http.Client{}).Get("http://" + addr + "/focus")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close() //nolint:errcheck
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
log.Error("Focus error: ", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
409
internal/app/app.go
Normal file
409
internal/app/app.go
Normal file
@ -0,0 +1,409 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/restarter"
|
||||
"github.com/pkg/profile"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Visible flags.
|
||||
const (
|
||||
flagCPUProfile = "cpu-prof"
|
||||
flagCPUProfileShort = "p"
|
||||
|
||||
flagMemProfile = "mem-prof"
|
||||
flagMemProfileShort = "m"
|
||||
|
||||
flagLogLevel = "log-level"
|
||||
flagLogLevelShort = "l"
|
||||
|
||||
flagGRPC = "grpc"
|
||||
flagGRPCShort = "g"
|
||||
|
||||
flagCLI = "cli"
|
||||
flagCLIShort = "c"
|
||||
|
||||
flagNonInteractive = "noninteractive"
|
||||
flagNonInteractiveShort = "n"
|
||||
|
||||
flagLogIMAP = "log-imap"
|
||||
flagLogSMTP = "log-smtp"
|
||||
)
|
||||
|
||||
// Hidden flags.
|
||||
const (
|
||||
flagLauncher = "launcher"
|
||||
flagNoWindow = "no-window"
|
||||
flagParentPID = "parent-pid"
|
||||
)
|
||||
|
||||
const (
|
||||
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
||||
)
|
||||
|
||||
func New() *cli.App { //nolint:funlen
|
||||
app := cli.NewApp()
|
||||
|
||||
app.Name = constants.FullAppName
|
||||
app.Usage = appUsage
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: flagCPUProfile,
|
||||
Aliases: []string{flagCPUProfileShort},
|
||||
Usage: "Generate CPU profile",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagMemProfile,
|
||||
Aliases: []string{flagMemProfileShort},
|
||||
Usage: "Generate memory profile",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flagLogLevel,
|
||||
Aliases: []string{flagLogLevelShort},
|
||||
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagGRPC,
|
||||
Aliases: []string{flagGRPCShort},
|
||||
Usage: "Start the gRPC service",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagCLI,
|
||||
Aliases: []string{flagCLIShort},
|
||||
Usage: "Start the command line interface",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagNonInteractive,
|
||||
Aliases: []string{flagNonInteractiveShort},
|
||||
Usage: "Start the app in non-interactive mode",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flagLogIMAP,
|
||||
Usage: "Enable logging of IMAP communications (all|client|server) (may contain decrypted data!)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagLogSMTP,
|
||||
Usage: "Enable logging of SMTP communications (may contain decrypted data!)",
|
||||
},
|
||||
|
||||
// Hidden flags
|
||||
&cli.BoolFlag{
|
||||
Name: flagNoWindow,
|
||||
Usage: "Don't show window after start",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flagLauncher,
|
||||
Usage: "The launcher used to start the app",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: flagParentPID,
|
||||
Usage: "Process ID of the parent",
|
||||
Hidden: true,
|
||||
Value: -1,
|
||||
},
|
||||
}
|
||||
|
||||
app.Action = run
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func run(c *cli.Context) error { //nolint:funlen
|
||||
// Get the current bridge version.
|
||||
version, err := semver.NewVersion(constants.Version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create version: %w", err)
|
||||
}
|
||||
|
||||
// Create a user agent that will be used for all requests.
|
||||
identifier := useragent.New()
|
||||
|
||||
// Create a new Sentry client that will be used to report crashes etc.
|
||||
reporter := sentry.NewReporter(constants.FullAppName, constants.Version, identifier)
|
||||
|
||||
// Determine the exe that should be used to restart/autostart the app.
|
||||
// By default, this is the launcher, if used. Otherwise, we try to get
|
||||
// the current exe, and fall back to os.Args[0] if that fails.
|
||||
var exe string
|
||||
|
||||
if launcher := c.String(flagLauncher); launcher != "" {
|
||||
exe = launcher
|
||||
} else if executable, err := os.Executable(); err == nil {
|
||||
exe = executable
|
||||
} else {
|
||||
exe = os.Args[0]
|
||||
}
|
||||
|
||||
migrationErr := migrateOldVersions()
|
||||
|
||||
// Run with profiling if requested.
|
||||
return withProfiler(c, func() error {
|
||||
// Restart the app if requested.
|
||||
return withRestarter(exe, func(restarter *restarter.Restarter) error {
|
||||
// Handle crashes with various actions.
|
||||
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
|
||||
// Load the locations where we store our files.
|
||||
return WithLocations(func(locations *locations.Locations) error {
|
||||
// Migrate the keychain helper.
|
||||
if err := migrateKeychainHelper(locations); err != nil {
|
||||
logrus.WithError(err).Error("Failed to migrate keychain helper")
|
||||
}
|
||||
|
||||
// Initialize logging.
|
||||
return withLogging(c, crashHandler, locations, func() error {
|
||||
// If there was an error during migration, log it now.
|
||||
if migrationErr != nil {
|
||||
logrus.WithError(migrationErr).Error("Failed to migrate old app data")
|
||||
}
|
||||
|
||||
// Ensure we are the only instance running.
|
||||
return withSingleInstance(locations, version, func() error {
|
||||
// Unlock the encrypted vault.
|
||||
return WithVault(locations, func(vault *vault.Vault, insecure, corrupt bool) error {
|
||||
if !vault.Migrated() {
|
||||
// Migrate old settings into the vault.
|
||||
if err := migrateOldSettings(vault); err != nil {
|
||||
logrus.WithError(err).Error("Failed to migrate old settings")
|
||||
}
|
||||
|
||||
// Migrate old accounts into the vault.
|
||||
if err := migrateOldAccounts(locations, vault); err != nil {
|
||||
logrus.WithError(err).Error("Failed to migrate old accounts")
|
||||
}
|
||||
|
||||
// The vault has been migrated.
|
||||
if err := vault.SetMigrated(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to mark vault as migrated")
|
||||
}
|
||||
}
|
||||
|
||||
// Load the cookies from the vault.
|
||||
return withCookieJar(vault, func(cookieJar http.CookieJar) error {
|
||||
// Create a new bridge instance.
|
||||
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, vault, cookieJar, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
|
||||
if insecure {
|
||||
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
|
||||
b.PushError(bridge.ErrVaultInsecure)
|
||||
}
|
||||
|
||||
if corrupt {
|
||||
logrus.Warn("The vault is corrupt and has been wiped")
|
||||
b.PushError(bridge.ErrVaultCorrupt)
|
||||
}
|
||||
|
||||
// Run the frontend.
|
||||
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// If there's another instance already running, try to raise it and exit.
|
||||
func withSingleInstance(locations *locations.Locations, version *semver.Version, fn func() error) error {
|
||||
logrus.Debug("Checking for other instances")
|
||||
defer logrus.Debug("Single instance stopped")
|
||||
|
||||
lock, err := checkSingleInstance(locations.GetLockFile(), version)
|
||||
if err != nil {
|
||||
logrus.Info("Another instance is already running; raising it")
|
||||
|
||||
if ok := focus.TryRaise(); !ok {
|
||||
return fmt.Errorf("another instance is already running but it could not be raised")
|
||||
}
|
||||
|
||||
logrus.Info("The other instance has been raised")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := lock.Close(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close lock file")
|
||||
}
|
||||
}()
|
||||
|
||||
return fn()
|
||||
}
|
||||
|
||||
// Initialize our logging system.
|
||||
func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locations.Locations, fn func() error) error {
|
||||
logrus.Debug("Initializing logging")
|
||||
defer logrus.Debug("Logging stopped")
|
||||
|
||||
// Get a place to keep our logs.
|
||||
logsPath, err := locations.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not provide logs path: %w", err)
|
||||
}
|
||||
|
||||
logrus.WithField("path", logsPath).Debug("Received logs path")
|
||||
|
||||
// Initialize logging.
|
||||
if err := logging.Init(logsPath, c.String(flagLogLevel)); err != nil {
|
||||
return fmt.Errorf("could not initialize logging: %w", err)
|
||||
}
|
||||
|
||||
// Ensure we dump a stack trace if we crash.
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
|
||||
logrus.
|
||||
WithField("appName", constants.FullAppName).
|
||||
WithField("version", constants.Version).
|
||||
WithField("revision", constants.Revision).
|
||||
WithField("build", constants.BuildTime).
|
||||
WithField("runtime", runtime.GOOS).
|
||||
WithField("args", os.Args).
|
||||
Info("Run app")
|
||||
|
||||
return fn()
|
||||
}
|
||||
|
||||
// WithLocations provides access to locations where we store our files.
|
||||
func WithLocations(fn func(*locations.Locations) error) error {
|
||||
logrus.Debug("Creating locations")
|
||||
defer logrus.Debug("Locations stopped")
|
||||
|
||||
// Create a locations provider to determine where to store our files.
|
||||
provider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, constants.ConfigName))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create locations provider: %w", err)
|
||||
}
|
||||
|
||||
// Create a new locations object that will be used to provide paths to store files.
|
||||
locations := locations.New(provider, constants.ConfigName)
|
||||
defer func() {
|
||||
if err := locations.Clean(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to clean locations")
|
||||
}
|
||||
}()
|
||||
|
||||
return fn(locations)
|
||||
}
|
||||
|
||||
// Start profiling if requested.
|
||||
func withProfiler(c *cli.Context, fn func() error) error {
|
||||
defer logrus.Debug("Profiler stopped")
|
||||
|
||||
if c.Bool(flagCPUProfile) {
|
||||
logrus.Debug("Running with CPU profiling")
|
||||
defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop()
|
||||
}
|
||||
|
||||
if c.Bool(flagMemProfile) {
|
||||
logrus.Debug("Running with memory profiling")
|
||||
defer profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath(".")).Stop()
|
||||
}
|
||||
|
||||
return fn()
|
||||
}
|
||||
|
||||
// Restart the app if necessary.
|
||||
func withRestarter(exe string, fn func(*restarter.Restarter) error) error {
|
||||
logrus.Debug("Creating restarter")
|
||||
defer logrus.Debug("Restarter stopped")
|
||||
|
||||
restarter := restarter.New(exe)
|
||||
defer restarter.Restart()
|
||||
|
||||
return fn(restarter)
|
||||
}
|
||||
|
||||
// Handle crashes if they occur.
|
||||
func withCrashHandler(restarter *restarter.Restarter, reporter *sentry.Reporter, fn func(*crash.Handler, <-chan struct{}) error) error {
|
||||
logrus.Debug("Creating crash handler")
|
||||
defer logrus.Debug("Crash handler stopped")
|
||||
|
||||
crashHandler := crash.NewHandler(crash.ShowErrorNotification(constants.FullAppName))
|
||||
defer crashHandler.HandlePanic()
|
||||
|
||||
// On crash, send crash report to Sentry.
|
||||
crashHandler.AddRecoveryAction(reporter.ReportException)
|
||||
|
||||
// On crash, notify the user and restart the app.
|
||||
crashHandler.AddRecoveryAction(crash.ShowErrorNotification(constants.FullAppName))
|
||||
|
||||
// On crash, restart the app.
|
||||
crashHandler.AddRecoveryAction(func(any) error { restarter.Set(true, true); return nil })
|
||||
|
||||
// quitCh is closed when the app is quitting.
|
||||
quitCh := make(chan struct{})
|
||||
|
||||
// On crash, quit the app.
|
||||
crashHandler.AddRecoveryAction(func(any) error { close(quitCh); return nil })
|
||||
|
||||
return fn(crashHandler, quitCh)
|
||||
}
|
||||
|
||||
// Use a custom cookie jar to persist values across runs.
|
||||
func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
|
||||
logrus.Debug("Creating cookie jar")
|
||||
defer logrus.Debug("Cookie jar stopped")
|
||||
|
||||
// Create the underlying cookie jar.
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create cookie jar: %w", err)
|
||||
}
|
||||
|
||||
// Create the cookie jar which persists to the vault.
|
||||
persister, err := cookies.NewCookieJar(jar, vault)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create cookie jar: %w", err)
|
||||
}
|
||||
|
||||
// Persist the cookies to the vault when we close.
|
||||
defer func() {
|
||||
logrus.Debug("Persisting cookies")
|
||||
|
||||
if err := persister.PersistCookies(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to persist cookies")
|
||||
}
|
||||
}()
|
||||
|
||||
return fn(persister)
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package base
|
||||
|
||||
import "strings"
|
||||
|
||||
// StripProcessSerialNumber removes additional flag from macOS.
|
||||
// More info:
|
||||
// http://mirror.informatimago.com/next/developer.apple.com/documentation/Carbon/Reference/Process_Manager/prmref_main/data_type_5.html#//apple_ref/doc/uid/TP30000208/C001951
|
||||
func StripProcessSerialNumber(args []string) []string {
|
||||
res := args[:0]
|
||||
|
||||
for _, arg := range args {
|
||||
if !strings.Contains(arg, "-psn_") {
|
||||
res = append(res, arg)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
@ -1,404 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package base implements a common application base currently shared by bridge and IE.
|
||||
// The base includes the following:
|
||||
// - access to standard filesystem locations like config, cache, logging dirs
|
||||
// - an extensible crash handler
|
||||
// - versioned cache directory
|
||||
// - persistent settings
|
||||
// - event listener
|
||||
// - credentials store
|
||||
// - pmapi Manager
|
||||
// In addition, the base initialises logging and reacts to command line arguments
|
||||
// which control the log verbosity and enable cpu/memory profiling.
|
||||
package base
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/api"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/cache"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/tls"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/cookies"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/crash"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/versioner"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/keychain"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
flagCPUProfile = "cpu-prof"
|
||||
flagCPUProfileShort = "p"
|
||||
flagMemProfile = "mem-prof"
|
||||
flagMemProfileShort = "m"
|
||||
flagLogLevel = "log-level"
|
||||
flagLogLevelShort = "l"
|
||||
// FlagCLI indicate to start with command line interface.
|
||||
FlagCLI = "cli"
|
||||
flagCLIShort = "c"
|
||||
flagRestart = "restart"
|
||||
FlagLauncher = "launcher"
|
||||
FlagNoWindow = "no-window"
|
||||
)
|
||||
|
||||
type Base struct {
|
||||
SentryReporter *sentry.Reporter
|
||||
CrashHandler *crash.Handler
|
||||
Locations *locations.Locations
|
||||
Settings *settings.Settings
|
||||
Lock *os.File
|
||||
Cache *cache.Cache
|
||||
Listener listener.Listener
|
||||
Creds *credentials.Store
|
||||
CM pmapi.Manager
|
||||
CookieJar *cookies.Jar
|
||||
UserAgent *useragent.UserAgent
|
||||
Updater *updater.Updater
|
||||
Versioner *versioner.Versioner
|
||||
TLS *tls.TLS
|
||||
Autostart *autostart.App
|
||||
|
||||
Name string // the app's name
|
||||
usage string // the app's usage description
|
||||
command string // the command used to launch the app (either the exe path or the launcher path)
|
||||
restart bool // whether the app is currently set to restart
|
||||
|
||||
teardown []func() error // actions to perform when app is exiting
|
||||
}
|
||||
|
||||
func New( //nolint:funlen
|
||||
appName,
|
||||
appUsage,
|
||||
configName,
|
||||
updateURLName,
|
||||
keychainName,
|
||||
cacheVersion string,
|
||||
) (*Base, error) {
|
||||
userAgent := useragent.New()
|
||||
|
||||
sentryReporter := sentry.NewReporter(appName, constants.Version, userAgent)
|
||||
|
||||
crashHandler := crash.NewHandler(
|
||||
sentryReporter.ReportException,
|
||||
crash.ShowErrorNotification(appName),
|
||||
)
|
||||
defer crashHandler.HandlePanic()
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
os.Args = StripProcessSerialNumber(os.Args)
|
||||
|
||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
locations := locations.New(locationsProvider, configName)
|
||||
|
||||
logsPath, err := locations.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := logging.Init(logsPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
|
||||
if err := migrateFiles(configName); err != nil {
|
||||
logrus.WithError(err).Warn("Old config files could not be migrated")
|
||||
}
|
||||
|
||||
if err := locations.Clean(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
settingsPath, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
settingsObj := settings.New(settingsPath)
|
||||
|
||||
lock, err := checkSingleInstance(locations.GetLockFile(), settingsObj)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Warnf("%v is already running", appName)
|
||||
return nil, api.CheckOtherInstanceAndFocus(settingsObj.GetInt(settings.APIPortKey))
|
||||
}
|
||||
|
||||
if err := migrateRebranding(settingsObj, keychainName); err != nil {
|
||||
logrus.WithError(err).Warn("Rebranding migration failed")
|
||||
}
|
||||
|
||||
cachePath, err := locations.ProvideCachePath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cache, err := cache.New(cachePath, cacheVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cache.RemoveOldVersions(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listener := listener.New()
|
||||
events.SetupEvents(listener)
|
||||
|
||||
// If we can't load the keychain for whatever reason,
|
||||
// we signal to frontend and supply a dummy keychain that always returns errors.
|
||||
kc, err := keychain.NewKeychain(settingsObj, keychainName)
|
||||
if err != nil {
|
||||
listener.Emit(events.CredentialsErrorEvent, err.Error())
|
||||
kc = keychain.NewMissingKeychain()
|
||||
}
|
||||
|
||||
cfg := pmapi.NewConfig(configName, constants.Version)
|
||||
cfg.GetUserAgent = userAgent.String
|
||||
cfg.UpgradeApplicationHandler = func() { listener.Emit(events.UpgradeApplicationEvent, "") }
|
||||
cfg.TLSIssueHandler = func() { listener.Emit(events.TLSCertIssue, "") }
|
||||
|
||||
cm := pmapi.New(cfg)
|
||||
|
||||
sentryReporter.SetClientFromManager(cm)
|
||||
|
||||
cm.AddConnectionObserver(pmapi.NewConnectionObserver(
|
||||
func() { listener.Emit(events.InternetConnChangedEvent, events.InternetOff) },
|
||||
func() { listener.Emit(events.InternetConnChangedEvent, events.InternetOn) },
|
||||
))
|
||||
|
||||
jar, err := cookies.NewCookieJar(settingsObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cm.SetCookieJar(jar)
|
||||
|
||||
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kr, err := crypto.NewKeyRing(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatesDir, err := locations.ProvideUpdatesPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
versioner := versioner.New(updatesDir)
|
||||
installer := updater.NewInstaller(versioner)
|
||||
updater := updater.New(
|
||||
cm,
|
||||
installer,
|
||||
settingsObj,
|
||||
kr,
|
||||
semver.MustParse(constants.Version),
|
||||
updateURLName,
|
||||
runtime.GOOS,
|
||||
)
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
autostart := &autostart.App{
|
||||
Name: startupNameForRebranding(appName),
|
||||
DisplayName: appName,
|
||||
Exec: []string{exe, "--" + FlagNoWindow},
|
||||
}
|
||||
|
||||
return &Base{
|
||||
SentryReporter: sentryReporter,
|
||||
CrashHandler: crashHandler,
|
||||
Locations: locations,
|
||||
Settings: settingsObj,
|
||||
Lock: lock,
|
||||
Cache: cache,
|
||||
Listener: listener,
|
||||
Creds: credentials.NewStore(kc),
|
||||
CM: cm,
|
||||
CookieJar: jar,
|
||||
UserAgent: userAgent,
|
||||
Updater: updater,
|
||||
Versioner: versioner,
|
||||
TLS: tls.New(settingsPath),
|
||||
Autostart: autostart,
|
||||
|
||||
Name: appName,
|
||||
usage: appUsage,
|
||||
|
||||
// By default, the command is the app's executable.
|
||||
// This can be changed at runtime by using the "--launcher" flag.
|
||||
command: exe,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *Base) NewApp(mainLoop func(*Base, *cli.Context) error) *cli.App {
|
||||
app := cli.NewApp()
|
||||
|
||||
app.Name = b.Name
|
||||
app.Usage = b.usage
|
||||
app.Version = constants.Version
|
||||
app.Action = b.wrapMainLoop(mainLoop)
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: flagCPUProfile,
|
||||
Aliases: []string{flagCPUProfileShort},
|
||||
Usage: "Generate CPU profile",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagMemProfile,
|
||||
Aliases: []string{flagMemProfileShort},
|
||||
Usage: "Generate memory profile",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flagLogLevel,
|
||||
Aliases: []string{flagLogLevelShort},
|
||||
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: FlagCLI,
|
||||
Aliases: []string{flagCLIShort},
|
||||
Usage: "Use command line interface",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: FlagNoWindow,
|
||||
Usage: "Don't show window after start",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flagRestart,
|
||||
Usage: "The number of times the application has already restarted",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: FlagLauncher,
|
||||
Usage: "The launcher to use to restart the application",
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
// SetToRestart sets the app to restart the next time it is closed.
|
||||
func (b *Base) SetToRestart() {
|
||||
b.restart = true
|
||||
}
|
||||
|
||||
// AddTeardownAction adds an action to perform during app teardown.
|
||||
func (b *Base) AddTeardownAction(fn func() error) {
|
||||
b.teardown = append(b.teardown, fn)
|
||||
}
|
||||
|
||||
func (b *Base) wrapMainLoop(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc { //nolint:funlen
|
||||
return func(c *cli.Context) error {
|
||||
defer b.CrashHandler.HandlePanic()
|
||||
defer func() { _ = b.Lock.Close() }()
|
||||
|
||||
// If launcher was used to start the app, use that for restart
|
||||
// and autostart.
|
||||
if launcher := c.String(FlagLauncher); launcher != "" {
|
||||
b.command = launcher
|
||||
// Bridge supports no-window option which we should use
|
||||
// for autostart.
|
||||
b.Autostart.Exec = []string{launcher, "--" + FlagNoWindow}
|
||||
}
|
||||
|
||||
if c.Bool(flagCPUProfile) {
|
||||
startCPUProfile()
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
if c.Bool(flagMemProfile) {
|
||||
defer makeMemoryProfile()
|
||||
}
|
||||
|
||||
logging.SetLevel(c.String(flagLogLevel))
|
||||
b.CM.SetLogging(logrus.WithField("pkg", "pmapi"), logrus.GetLevel() == logrus.TraceLevel)
|
||||
|
||||
logrus.
|
||||
WithField("appName", b.Name).
|
||||
WithField("version", constants.Version).
|
||||
WithField("revision", constants.Revision).
|
||||
WithField("build", constants.BuildTime).
|
||||
WithField("runtime", runtime.GOOS).
|
||||
WithField("args", os.Args).
|
||||
Info("Run app")
|
||||
|
||||
b.CrashHandler.AddRecoveryAction(func(interface{}) error {
|
||||
sentry.Flush(2 * time.Second)
|
||||
|
||||
if c.Int(flagRestart) > maxAllowedRestarts {
|
||||
logrus.
|
||||
WithField("restart", c.Int("restart")).
|
||||
Warn("Not restarting, already restarted too many times")
|
||||
os.Exit(1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return b.restartApp(true)
|
||||
})
|
||||
|
||||
if err := appMainLoop(b, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.doTeardown(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b.restart {
|
||||
return b.restartApp(false)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Base) doTeardown() error {
|
||||
for _, action := range b.teardown {
|
||||
if err := action(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -1,131 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// migrateFiles migrates files from their old (pre-refactor) locations to their new locations.
|
||||
// We can remove this eventually.
|
||||
//
|
||||
// | entity | old location | new location |
|
||||
// |-----------|-------------------------------------------|----------------------------------------|
|
||||
// | prefs | ~/.cache/protonmail/<app>/c11/prefs.json | ~/.config/protonmail/<app>/prefs.json |
|
||||
// | c11 1.5.x | ~/.cache/protonmail/<app>/c11 | ~/.cache/protonmail/<app>/cache/c11 |
|
||||
// | c11 1.6.x | ~/.cache/protonmail/<app>/cache/c11 | ~/.config/protonmail/<app>/cache/c11 |
|
||||
// | updates | ~/.cache/protonmail/<app>/updates | ~/.config/protonmail/<app>/updates |.
|
||||
func migrateFiles(configName string) error {
|
||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
locations := locations.New(locationsProvider, configName)
|
||||
userCacheDir := locationsProvider.UserCache()
|
||||
|
||||
if err := migratePrefsFrom15x(locations, userCacheDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := migrateCacheFromBoth15xAnd16x(locations, userCacheDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := migrateUpdatesFrom16x(configName, locations); err != nil { //nolint:revive It is more clear to structure this way
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func migratePrefsFrom15x(locations *locations.Locations, userCacheDir string) error {
|
||||
newSettingsDir, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return moveIfExists(
|
||||
filepath.Join(userCacheDir, "c11", "prefs.json"),
|
||||
filepath.Join(newSettingsDir, "prefs.json"),
|
||||
)
|
||||
}
|
||||
|
||||
func migrateCacheFromBoth15xAnd16x(locations *locations.Locations, userCacheDir string) error {
|
||||
olderCacheDir := userCacheDir
|
||||
newerCacheDir := locations.GetOldCachePath()
|
||||
latestCacheDir, err := locations.ProvideCachePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Migration for versions before 1.6.x.
|
||||
if err := moveIfExists(
|
||||
filepath.Join(olderCacheDir, "c11"),
|
||||
filepath.Join(latestCacheDir, "c11"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Migration for versions 1.6.x.
|
||||
return moveIfExists(
|
||||
filepath.Join(newerCacheDir, "c11"),
|
||||
filepath.Join(latestCacheDir, "c11"),
|
||||
)
|
||||
}
|
||||
|
||||
func migrateUpdatesFrom16x(configName string, locations *locations.Locations) error {
|
||||
// In order to properly update Bridge 1.6.X and higher we need to
|
||||
// change the launcher first. Since this is not part of automatic
|
||||
// updates the migration must wait until manual update. Until that
|
||||
// we need to keep old path.
|
||||
if configName == "bridge" {
|
||||
return nil
|
||||
}
|
||||
|
||||
oldUpdatesPath := locations.GetOldUpdatesPath()
|
||||
// Do not use ProvideUpdatesPath, that creates dir right away.
|
||||
newUpdatesPath := locations.GetUpdatesPath()
|
||||
|
||||
return moveIfExists(oldUpdatesPath, newUpdatesPath)
|
||||
}
|
||||
|
||||
func moveIfExists(source, destination string) error {
|
||||
l := logrus.WithField("source", source).WithField("destination", destination)
|
||||
|
||||
if _, err := os.Stat(source); os.IsNotExist(err) {
|
||||
l.Info("No need to migrate file, source doesn't exist")
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(destination); !os.IsNotExist(err) {
|
||||
// Once migrated, files should not stay in source anymore. Therefore
|
||||
// if some files are still in source location but target already exist,
|
||||
// it's suspicious. Could happen by installing new version, then the
|
||||
// old one because of some reason, and then the new one again.
|
||||
// Good to see as warning because it could be a reason why Bridge is
|
||||
// behaving weirdly, like wrong configuration, or db re-sync and so on.
|
||||
l.Warn("No need to migrate file, target already exists")
|
||||
return nil
|
||||
}
|
||||
|
||||
l.Info("Migrating files")
|
||||
return os.Rename(source, destination)
|
||||
}
|
||||
@ -1,197 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/keychain"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const darwin = "darwin"
|
||||
|
||||
func migrateRebranding(settingsObj *settings.Settings, keychainName string) (result error) {
|
||||
if err := migrateStartupBeforeRebranding(); err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
|
||||
lastUsedVersion := settingsObj.Get(settings.LastVersionKey)
|
||||
|
||||
// Skipping migration: it is first bridge start or cache was cleared.
|
||||
if lastUsedVersion == "" {
|
||||
settingsObj.SetBool(settings.RebrandingMigrationKey, true)
|
||||
return
|
||||
}
|
||||
|
||||
// Skipping rest of migration: already done
|
||||
if settingsObj.GetBool(settings.RebrandingMigrationKey) {
|
||||
return
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows", "linux":
|
||||
// GODT-1260 we would need admin rights to changes desktop files
|
||||
// and start menu items.
|
||||
settingsObj.SetBool(settings.RebrandingMigrationKey, true)
|
||||
case darwin:
|
||||
if shouldContinue, err := isMacBeforeRebranding(); !shouldContinue || err != nil {
|
||||
if err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if err := migrateMacKeychainBeforeRebranding(settingsObj, keychainName); err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
|
||||
settingsObj.SetBool(settings.RebrandingMigrationKey, true)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// migrateMacKeychainBeforeRebranding deals with write access restriction to
|
||||
// mac keychain passwords which are caused by application renaming. The old
|
||||
// passwords are copied under new name in order to have write access afer
|
||||
// renaming.
|
||||
func migrateMacKeychainBeforeRebranding(settingsObj *settings.Settings, keychainName string) error {
|
||||
l := logrus.WithField("pkg", "app/base/migration")
|
||||
l.Warn("Migrating mac keychain")
|
||||
|
||||
helperConstructor, ok := keychain.Helpers["macos-keychain"]
|
||||
if !ok {
|
||||
return errors.New("cannot find macos-keychain helper")
|
||||
}
|
||||
|
||||
oldKC, err := helperConstructor("ProtonMailBridgeService")
|
||||
if err != nil {
|
||||
l.WithError(err).Error("Keychain constructor failed")
|
||||
return err
|
||||
}
|
||||
|
||||
idByURL, err := oldKC.List()
|
||||
if err != nil {
|
||||
l.WithError(err).Error("List old keychain failed")
|
||||
return err
|
||||
}
|
||||
|
||||
newKC, err := keychain.NewKeychain(settingsObj, keychainName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for url, id := range idByURL {
|
||||
li := l.WithField("id", id).WithField("url", url)
|
||||
userID, secret, err := oldKC.Get(url)
|
||||
if err != nil {
|
||||
li.WithField("userID", userID).
|
||||
WithField("err", err).
|
||||
Error("Faild to get old item")
|
||||
continue
|
||||
}
|
||||
|
||||
if _, _, err := newKC.Get(userID); err == nil {
|
||||
li.Warn("Skipping migration, item already exists.")
|
||||
continue
|
||||
}
|
||||
|
||||
if err := newKC.Put(userID, secret); err != nil {
|
||||
li.WithError(err).Error("Failed to migrate user")
|
||||
}
|
||||
|
||||
li.Info("Item migrated")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateStartupBeforeRebranding removes old startup links. The creation of new links is
|
||||
// handled by bridge initialisation.
|
||||
func migrateStartupBeforeRebranding() error {
|
||||
path, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
path = filepath.Join(path, `AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\ProtonMail Bridge.lnk`)
|
||||
case "linux":
|
||||
path = filepath.Join(path, `.config/autostart/ProtonMail Bridge.desktop`)
|
||||
case darwin:
|
||||
path = filepath.Join(path, `Library/LaunchAgents/ProtonMail Bridge.plist`)
|
||||
default:
|
||||
return errors.New("unknown GOOS")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
logrus.WithField("pkg", "app/base/migration").Warn("Migrating autostartup links")
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
// startupNameForRebranding returns the name for autostart launcher based on
|
||||
// type of rebranded instance i.e. update or manual.
|
||||
//
|
||||
// This only affects darwin when udpate re-writes the old startup and then
|
||||
// manual installed it would not run proper exe. Therefore we return "old" name
|
||||
// for updates and "new" name for manual which would be properly migrated.
|
||||
//
|
||||
// For orther (linux and windows) the link is always pointing to launcher which
|
||||
// path didn't changed.
|
||||
func startupNameForRebranding(origin string) string {
|
||||
if runtime.GOOS == darwin {
|
||||
if path, err := os.Executable(); err == nil && strings.Contains(path, "ProtonMail Bridge") {
|
||||
return "ProtonMail Bridge"
|
||||
}
|
||||
}
|
||||
|
||||
// No need to solve for other OS. See comment above.
|
||||
return origin
|
||||
}
|
||||
|
||||
// isBeforeRebranding decide if last used version was older than 2.2.0. If
|
||||
// cannot decide it returns false with error.
|
||||
func isMacBeforeRebranding() (bool, error) {
|
||||
// previous version | update | do mac migration |
|
||||
// | first | false |
|
||||
// cleared-cache | manual | false |
|
||||
// cleared-cache | in-app | false |
|
||||
// old | in-app | false |
|
||||
// old in-app | in-app | false |
|
||||
// old | manual | true |
|
||||
// old in-app | manual | true |
|
||||
// manual | in-app | false |
|
||||
|
||||
// Skip if it was in-app update and not manual
|
||||
if path, err := os.Executable(); err != nil || strings.Contains(path, "ProtonMail Bridge") {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// startCPUProfile starts CPU pprof.
|
||||
func startCPUProfile() {
|
||||
f, err := os.Create("./cpu.pprof")
|
||||
if err != nil {
|
||||
logrus.Fatal("Could not create CPU profile: ", err)
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
logrus.Fatal("Could not start CPU profile: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// makeMemoryProfile generates memory pprof.
|
||||
func makeMemoryProfile() {
|
||||
name := "./mem.pprof"
|
||||
f, err := os.Create(name)
|
||||
if err != nil {
|
||||
logrus.Fatal("Could not create memory profile: ", err)
|
||||
}
|
||||
if abs, err := filepath.Abs(name); err == nil {
|
||||
name = abs
|
||||
}
|
||||
logrus.Info("Writing memory profile to ", name)
|
||||
runtime.GC() // get up-to-date statistics
|
||||
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
logrus.Fatal("Could not write memory profile: ", err)
|
||||
}
|
||||
_ = f.Close()
|
||||
}
|
||||
@ -1,80 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
// maxAllowedRestarts controls after how many crashes the app will give up restarting.
|
||||
const maxAllowedRestarts = 10
|
||||
|
||||
func (b *Base) restartApp(crash bool) error {
|
||||
var args []string
|
||||
|
||||
if crash {
|
||||
args = incrementRestartFlag(os.Args)[1:]
|
||||
defer func() { os.Exit(1) }()
|
||||
} else {
|
||||
args = os.Args[1:]
|
||||
}
|
||||
|
||||
logrus.
|
||||
WithField("command", b.command).
|
||||
WithField("args", args).
|
||||
Warn("Restarting")
|
||||
|
||||
return execabs.Command(b.command, args...).Start() //nolint:gosec
|
||||
}
|
||||
|
||||
// incrementRestartFlag increments the value of the restart flag.
|
||||
// If no such flag is present, it is added with initial value 1.
|
||||
func incrementRestartFlag(args []string) []string {
|
||||
res := append([]string{}, args...)
|
||||
|
||||
hasFlag := false
|
||||
|
||||
for k, v := range res {
|
||||
if v != "--restart" {
|
||||
continue
|
||||
}
|
||||
|
||||
hasFlag = true
|
||||
|
||||
if k+1 >= len(res) {
|
||||
continue
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(res[k+1])
|
||||
if err != nil {
|
||||
res[k+1] = "1"
|
||||
} else {
|
||||
res[k+1] = strconv.Itoa(n + 1)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasFlag {
|
||||
res = append(res, "--restart", "1")
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIncrementRestartFlag(t *testing.T) {
|
||||
tests := []struct {
|
||||
in []string
|
||||
out []string
|
||||
}{
|
||||
{[]string{"./bridge", "--restart", "1"}, []string{"./bridge", "--restart", "2"}},
|
||||
{[]string{"./bridge", "--restart", "2"}, []string{"./bridge", "--restart", "3"}},
|
||||
{[]string{"./bridge", "--other", "--restart", "2"}, []string{"./bridge", "--other", "--restart", "3"}},
|
||||
{[]string{"./bridge", "--restart", "2", "--other"}, []string{"./bridge", "--restart", "3", "--other"}},
|
||||
{[]string{"./bridge", "--restart", "2", "--other", "2"}, []string{"./bridge", "--restart", "3", "--other", "2"}},
|
||||
{[]string{"./bridge"}, []string{"./bridge", "--restart", "1"}},
|
||||
{[]string{"./bridge", "--something"}, []string{"./bridge", "--something", "--restart", "1"}},
|
||||
{[]string{"./bridge", "--something", "--else"}, []string{"./bridge", "--something", "--else", "--restart", "1"}},
|
||||
{[]string{"./bridge", "--restart", "bad"}, []string{"./bridge", "--restart", "1"}},
|
||||
{[]string{"./bridge", "--restart", "bad", "--other"}, []string{"./bridge", "--restart", "1", "--other"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(strings.Join(tt.in, " "), func(t *testing.T) {
|
||||
assert.Equal(t, tt.out, incrementRestartFlag(tt.in))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionLessThan(t *testing.T) {
|
||||
r := require.New(t)
|
||||
|
||||
old := semver.MustParse("1.1.0")
|
||||
current := semver.MustParse("1.1.1")
|
||||
newer := semver.MustParse("1.1.2")
|
||||
|
||||
r.True(old.LessThan(current))
|
||||
r.False(current.LessThan(current))
|
||||
r.False(newer.LessThan(current))
|
||||
}
|
||||
@ -1,101 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/allan-simon/go-singleinstance"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// checkSingleInstance returns error if a bridge instance is already running
|
||||
// This instance should be stop and window of running window should be brought
|
||||
// to focus.
|
||||
//
|
||||
// For macOS and Linux when already running version is older than this instance
|
||||
// it will kill old and continue with this new bridge (i.e. no error returned).
|
||||
func checkSingleInstance(lockFilePath string, settingsObj *settings.Settings) (*os.File, error) {
|
||||
if lock, err := singleinstance.CreateLockFile(lockFilePath); err == nil {
|
||||
// Bridge is not runnig, continue normally
|
||||
return lock, nil
|
||||
}
|
||||
|
||||
if err := runningVersionIsOlder(settingsObj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pid, err := getPID(lockFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := unix.Kill(pid, unix.SIGTERM); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Need to wait some time to release file lock
|
||||
time.Sleep(time.Second)
|
||||
|
||||
return singleinstance.CreateLockFile(lockFilePath)
|
||||
}
|
||||
|
||||
func getPID(lockFilePath string) (int, error) {
|
||||
file, err := os.Open(filepath.Clean(lockFilePath))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
rawPID := make([]byte, 10) // PID is probably up to 7 digits long, 10 should be enough
|
||||
n, err := file.Read(rawPID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.Atoi(strings.TrimSpace(string(rawPID[:n])))
|
||||
}
|
||||
|
||||
func runningVersionIsOlder(settingsObj *settings.Settings) error {
|
||||
currentVer, err := semver.StrictNewVersion(constants.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runningVer, err := semver.StrictNewVersion(settingsObj.Get(settings.LastVersionKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !runningVer.LessThan(currentVer) {
|
||||
return errors.New("running version is not older")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
163
internal/app/bridge.go
Normal file
163
internal/app/bridge.go
Normal file
@ -0,0 +1,163 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/dialer"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const vaultSecretName = "bridge-vault-key"
|
||||
|
||||
// deleteOldGoIMAPFiles Set with `-ldflags -X app.deleteOldGoIMAPFiles=true` to enable cleanup of old imap cache data.
|
||||
var deleteOldGoIMAPFiles bool //nolint:gochecknoglobals
|
||||
|
||||
// withBridge creates creates and tears down the bridge.
|
||||
func withBridge( //nolint:funlen
|
||||
c *cli.Context,
|
||||
exe string,
|
||||
locations *locations.Locations,
|
||||
version *semver.Version,
|
||||
identifier *useragent.UserAgent,
|
||||
crashHandler *crash.Handler,
|
||||
reporter *sentry.Reporter,
|
||||
vault *vault.Vault,
|
||||
cookieJar http.CookieJar,
|
||||
fn func(*bridge.Bridge, <-chan events.Event) error,
|
||||
) error {
|
||||
logrus.Debug("Creating bridge")
|
||||
defer logrus.Debug("Bridge stopped")
|
||||
|
||||
// Delete old go-imap cache files
|
||||
if deleteOldGoIMAPFiles {
|
||||
logrus.Debug("Deleting old go-imap cache files")
|
||||
|
||||
if err := locations.CleanGoIMAPCache(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to remove old go-imap cache")
|
||||
}
|
||||
}
|
||||
|
||||
// Create the underlying dialer used by the bridge.
|
||||
// It only connects to trusted servers and reports any untrusted servers it finds.
|
||||
pinningDialer := dialer.NewPinningTLSDialer(
|
||||
dialer.NewBasicTLSDialer(constants.APIHost),
|
||||
dialer.NewTLSReporter(constants.APIHost, constants.AppVersion(version.Original()), identifier, dialer.TrustedAPIPins),
|
||||
dialer.NewTLSPinChecker(dialer.TrustedAPIPins),
|
||||
)
|
||||
|
||||
// Create a proxy dialer which switches to a proxy if the request fails.
|
||||
proxyDialer := dialer.NewProxyTLSDialer(pinningDialer, constants.APIHost)
|
||||
|
||||
// Create the autostarter.
|
||||
autostarter := newAutostarter(exe)
|
||||
|
||||
// Create the update installer.
|
||||
updater, err := newUpdater(locations)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create updater: %w", err)
|
||||
}
|
||||
|
||||
// Create a new bridge.
|
||||
bridge, eventCh, err := bridge.New(
|
||||
// The app stuff.
|
||||
locations,
|
||||
vault,
|
||||
autostarter,
|
||||
updater,
|
||||
version,
|
||||
|
||||
// The API stuff.
|
||||
constants.APIHost,
|
||||
cookieJar,
|
||||
identifier,
|
||||
pinningDialer,
|
||||
dialer.CreateTransportWithDialer(proxyDialer),
|
||||
proxyDialer,
|
||||
|
||||
// Crash and report stuff
|
||||
crashHandler,
|
||||
reporter,
|
||||
|
||||
// The logging stuff.
|
||||
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
|
||||
c.String(flagLogIMAP) == "server" || c.String(flagLogIMAP) == "all",
|
||||
c.Bool(flagLogSMTP),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create bridge: %w", err)
|
||||
}
|
||||
|
||||
// Ensure we close bridge when we exit.
|
||||
defer bridge.Close(c.Context)
|
||||
|
||||
return fn(bridge, eventCh)
|
||||
}
|
||||
|
||||
func newAutostarter(exe string) *autostart.App {
|
||||
logrus.Debug("Creating autostarter")
|
||||
|
||||
return &autostart.App{
|
||||
Name: constants.FullAppName,
|
||||
DisplayName: constants.FullAppName,
|
||||
Exec: []string{exe, "--" + flagNoWindow},
|
||||
}
|
||||
}
|
||||
|
||||
func newUpdater(locations *locations.Locations) (*updater.Updater, error) {
|
||||
updatesDir, err := locations.ProvideUpdatesPath()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not provide updates path: %w", err)
|
||||
}
|
||||
|
||||
logrus.WithField("updates", updatesDir).Debug("Creating updater")
|
||||
|
||||
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create key from armored: %w", err)
|
||||
}
|
||||
|
||||
verifier, err := crypto.NewKeyRing(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create key ring: %w", err)
|
||||
}
|
||||
|
||||
return updater.NewUpdater(
|
||||
updater.NewInstaller(versioner.New(updatesDir)),
|
||||
verifier,
|
||||
constants.UpdateName,
|
||||
runtime.GOOS,
|
||||
), nil
|
||||
}
|
||||
@ -1,316 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package bridge implements the bridge CLI application.
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/api"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/app/base"
|
||||
pkgBridge "github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
pkgTLS "github.com/ProtonMail/proton-bridge/v2/internal/config/tls"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/frontend"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/imap"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/smtp"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/store"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/store/cache"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
flagLogIMAP = "log-imap"
|
||||
flagLogSMTP = "log-smtp"
|
||||
flagNonInteractive = "noninteractive"
|
||||
|
||||
// Memory cache was estimated by empirical usage in past and it was set to 100MB.
|
||||
// NOTE: This value must not be less than maximal size of one email (~30MB).
|
||||
inMemoryCacheLimnit = 100 * (1 << 20)
|
||||
)
|
||||
|
||||
func New(base *base.Base) *cli.App {
|
||||
app := base.NewApp(mailLoop)
|
||||
|
||||
app.Flags = append(app.Flags, []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: flagLogIMAP,
|
||||
Usage: "Enable logging of IMAP communications (all|client|server) (may contain decrypted data!)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagLogSMTP,
|
||||
Usage: "Enable logging of SMTP communications (may contain decrypted data!)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagNonInteractive,
|
||||
Usage: "Start Bridge entirely noninteractively",
|
||||
},
|
||||
}...)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func mailLoop(b *base.Base, c *cli.Context) error { //nolint:funlen
|
||||
tlsConfig, err := loadTLSConfig(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// GODT-1481: Always turn off reporting of unencrypted recipient in v2.
|
||||
b.Settings.SetBool(settings.ReportOutgoingNoEncKey, false)
|
||||
|
||||
cache, cacheErr := loadMessageCache(b)
|
||||
if cacheErr != nil {
|
||||
logrus.WithError(cacheErr).Error("Could not load local cache.")
|
||||
}
|
||||
|
||||
builder := message.NewBuilder(
|
||||
b.Settings.GetInt(settings.FetchWorkers),
|
||||
b.Settings.GetInt(settings.AttachmentWorkers),
|
||||
)
|
||||
|
||||
bridge := pkgBridge.New(
|
||||
b.Locations,
|
||||
b.Cache,
|
||||
b.Settings,
|
||||
b.SentryReporter,
|
||||
b.CrashHandler,
|
||||
b.Listener,
|
||||
cache,
|
||||
builder,
|
||||
b.CM,
|
||||
b.Creds,
|
||||
b.Updater,
|
||||
b.Versioner,
|
||||
b.Autostart,
|
||||
)
|
||||
imapBackend := imap.NewIMAPBackend(b.CrashHandler, b.Listener, b.Cache, b.Settings, bridge)
|
||||
smtpBackend := smtp.NewSMTPBackend(b.CrashHandler, b.Listener, b.Settings, bridge)
|
||||
|
||||
if cacheErr != nil {
|
||||
bridge.AddError(pkgBridge.ErrLocalCacheUnavailable)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer b.CrashHandler.HandlePanic()
|
||||
api.NewAPIServer(b.Settings, b.Listener).ListenAndServe()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer b.CrashHandler.HandlePanic()
|
||||
imapPort := b.Settings.GetInt(settings.IMAPPortKey)
|
||||
imap.NewIMAPServer(
|
||||
b.CrashHandler,
|
||||
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
|
||||
c.String(flagLogIMAP) == "server" || c.String(flagLogIMAP) == "all",
|
||||
imapPort, tlsConfig, imapBackend, b.UserAgent, b.Listener).ListenAndServe()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer b.CrashHandler.HandlePanic()
|
||||
smtpPort := b.Settings.GetInt(settings.SMTPPortKey)
|
||||
useSSL := b.Settings.GetBool(settings.SMTPSSLKey)
|
||||
smtp.NewSMTPServer(
|
||||
b.CrashHandler,
|
||||
c.Bool(flagLogSMTP),
|
||||
smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe()
|
||||
}()
|
||||
|
||||
// We want to remove old versions if the app exits successfully.
|
||||
b.AddTeardownAction(b.Versioner.RemoveOldVersions)
|
||||
|
||||
// We want cookies to be saved to disk so they are loaded the next time.
|
||||
b.AddTeardownAction(b.CookieJar.PersistCookies)
|
||||
|
||||
var frontendMode string
|
||||
|
||||
switch {
|
||||
case c.Bool(base.FlagCLI):
|
||||
frontendMode = "cli"
|
||||
case c.Bool(flagNonInteractive):
|
||||
return <-(make(chan error)) // Block forever.
|
||||
default:
|
||||
frontendMode = "qt"
|
||||
}
|
||||
|
||||
f := frontend.New(
|
||||
constants.Version,
|
||||
constants.BuildVersion,
|
||||
b.Name,
|
||||
frontendMode,
|
||||
!c.Bool(base.FlagNoWindow),
|
||||
b.CrashHandler,
|
||||
b.Locations,
|
||||
b.Settings,
|
||||
b.Listener,
|
||||
b.Updater,
|
||||
b.UserAgent,
|
||||
bridge,
|
||||
smtpBackend,
|
||||
b,
|
||||
)
|
||||
|
||||
// Watch for updates routine
|
||||
go func() {
|
||||
ticker := time.NewTicker(constants.UpdateCheckInterval)
|
||||
|
||||
for {
|
||||
checkAndHandleUpdate(b.Updater, f, b.Settings.GetBool(settings.AutoUpdateKey))
|
||||
<-ticker.C
|
||||
}
|
||||
}()
|
||||
|
||||
return f.Loop()
|
||||
}
|
||||
|
||||
func loadTLSConfig(b *base.Base) (*tls.Config, error) {
|
||||
if !b.TLS.HasCerts() {
|
||||
if err := generateTLSCerts(b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tlsConfig, err := b.TLS.GetConfig()
|
||||
if err == nil {
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
logrus.WithError(err).Error("Failed to load TLS config, regenerating certificates")
|
||||
|
||||
if err := generateTLSCerts(b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.TLS.GetConfig()
|
||||
}
|
||||
|
||||
func generateTLSCerts(b *base.Base) error {
|
||||
template, err := pkgTLS.NewTLSTemplate()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate TLS template")
|
||||
}
|
||||
|
||||
if err := b.TLS.GenerateCerts(template); err != nil {
|
||||
return errors.Wrap(err, "failed to generate TLS certs")
|
||||
}
|
||||
|
||||
if err := b.TLS.InstallCerts(); err != nil {
|
||||
return errors.Wrap(err, "failed to install TLS certs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) {
|
||||
log := logrus.WithField("pkg", "app/bridge")
|
||||
version, err := u.Check()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("An error occurred while checking for updates")
|
||||
return
|
||||
}
|
||||
|
||||
f.WaitUntilFrontendIsReady()
|
||||
|
||||
// Update links in UI
|
||||
f.SetVersion(version)
|
||||
|
||||
if !u.IsUpdateApplicable(version) {
|
||||
log.Info("No need to update")
|
||||
return
|
||||
}
|
||||
|
||||
log.WithField("version", version.Version).Info("An update is available")
|
||||
|
||||
if !autoUpdate {
|
||||
f.NotifyManualUpdate(version, u.CanInstall(version))
|
||||
return
|
||||
}
|
||||
|
||||
if !u.CanInstall(version) {
|
||||
log.Info("A manual update is required")
|
||||
f.NotifySilentUpdateError(updater.ErrManualUpdateRequired)
|
||||
return
|
||||
}
|
||||
|
||||
if err := u.InstallUpdate(version); err != nil {
|
||||
if errors.Cause(err) == updater.ErrDownloadVerify {
|
||||
log.WithError(err).Warning("Skipping update installation due to temporary error")
|
||||
} else {
|
||||
log.WithError(err).Error("The update couldn't be installed")
|
||||
f.NotifySilentUpdateError(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
f.NotifySilentUpdateInstalled()
|
||||
}
|
||||
|
||||
// loadMessageCache loads local cache in case it is enabled in settings and available.
|
||||
// In any other case it is returning in-memory cache. Could also return an error in case
|
||||
// local cache is enabled but unavailable (in-memory cache will be returned nevertheless).
|
||||
func loadMessageCache(b *base.Base) (cache.Cache, error) {
|
||||
if !b.Settings.GetBool(settings.CacheEnabledKey) {
|
||||
return cache.NewInMemoryCache(inMemoryCacheLimnit), nil
|
||||
}
|
||||
|
||||
var compressor cache.Compressor
|
||||
|
||||
// NOTE(GODT-1158): Changing compression is not an option currently
|
||||
// available for user but, if user changes compression setting we have
|
||||
// to nuke the cache.
|
||||
if b.Settings.GetBool(settings.CacheCompressionKey) {
|
||||
compressor = &cache.GZipCompressor{}
|
||||
} else {
|
||||
compressor = &cache.NoopCompressor{}
|
||||
}
|
||||
|
||||
var path string
|
||||
|
||||
if customPath := b.Settings.Get(settings.CacheLocationKey); customPath != "" {
|
||||
path = customPath
|
||||
} else {
|
||||
path = b.Cache.GetDefaultMessageCacheDir()
|
||||
// Store path so it will allways persist if default location
|
||||
// will be changed in new version.
|
||||
b.Settings.Set(settings.CacheLocationKey, path)
|
||||
}
|
||||
|
||||
// To prevent memory peaks we set maximal write concurency for store
|
||||
// build jobs.
|
||||
store.SetBuildAndCacheJobLimit(b.Settings.GetInt(settings.CacheConcurrencyWrite))
|
||||
|
||||
messageCache, err := cache.NewOnDiskCache(path, compressor, cache.Options{
|
||||
MinFreeAbs: uint64(b.Settings.GetInt(settings.CacheMinFreeAbsKey)),
|
||||
MinFreeRat: b.Settings.GetFloat64(settings.CacheMinFreeRatKey),
|
||||
ConcurrentRead: b.Settings.GetInt(settings.CacheConcurrencyRead),
|
||||
ConcurrentWrite: b.Settings.GetInt(settings.CacheConcurrencyWrite),
|
||||
})
|
||||
if err != nil {
|
||||
return cache.NewInMemoryCache(inMemoryCacheLimnit), err
|
||||
}
|
||||
|
||||
return messageCache, nil
|
||||
}
|
||||
69
internal/app/frontend.go
Normal file
69
internal/app/frontend.go
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
bridgeCLI "github.com/ProtonMail/proton-bridge/v3/internal/frontend/cli"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/frontend/grpc"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/restarter"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func runFrontend(
|
||||
c *cli.Context,
|
||||
crashHandler *crash.Handler,
|
||||
restarter *restarter.Restarter,
|
||||
locations *locations.Locations,
|
||||
bridge *bridge.Bridge,
|
||||
eventCh <-chan events.Event,
|
||||
quitCh <-chan struct{},
|
||||
parentPID int,
|
||||
) error {
|
||||
logrus.Debug("Running frontend")
|
||||
defer logrus.Debug("Frontend stopped")
|
||||
|
||||
switch {
|
||||
case c.Bool(flagCLI):
|
||||
return bridgeCLI.New(bridge, restarter, eventCh).Loop()
|
||||
|
||||
case c.Bool(flagNonInteractive):
|
||||
select {}
|
||||
|
||||
case c.Bool(flagGRPC):
|
||||
service, err := grpc.NewService(crashHandler, restarter, locations, bridge, eventCh, quitCh, !c.Bool(flagNoWindow), parentPID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create service: %w", err)
|
||||
}
|
||||
|
||||
return service.Loop()
|
||||
|
||||
default:
|
||||
if err := cli.ShowAppHelp(c); err != nil {
|
||||
logrus.WithError(err).Error("Failed to show app help")
|
||||
}
|
||||
|
||||
return fmt.Errorf("no frontend specified, use --cli, --grpc or --noninteractive")
|
||||
}
|
||||
}
|
||||
315
internal/app/migration.go
Normal file
315
internal/app/migration.go
Normal file
@ -0,0 +1,315 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/legacy/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/allan-simon/go-singleinstance"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// nolint:gosec
|
||||
func migrateKeychainHelper(locations *locations.Locations) error {
|
||||
logrus.Info("Migrating keychain helper")
|
||||
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user config dir: %w", err)
|
||||
}
|
||||
|
||||
b, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "prefs.json"))
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to read old prefs file: %w", err)
|
||||
}
|
||||
|
||||
var prefs struct {
|
||||
Helper string `json:"preferred_keychain"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, &prefs); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal old prefs file: %w", err)
|
||||
}
|
||||
|
||||
settings, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get settings path: %w", err)
|
||||
}
|
||||
|
||||
return vault.SetHelper(settings, prefs.Helper)
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
func migrateOldSettings(v *vault.Vault) error {
|
||||
logrus.Info("Migrating settings")
|
||||
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user config dir: %w", err)
|
||||
}
|
||||
|
||||
b, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "prefs.json"))
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to read old prefs file: %w", err)
|
||||
}
|
||||
|
||||
return migratePrefsToVault(v, b)
|
||||
}
|
||||
|
||||
func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
|
||||
logrus.Info("Migrating accounts")
|
||||
|
||||
settings, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get settings path: %w", err)
|
||||
}
|
||||
|
||||
helper, err := vault.GetHelper(settings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get helper: %w", err)
|
||||
}
|
||||
|
||||
keychain, err := keychain.NewKeychain(helper, "bridge")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create keychain: %w", err)
|
||||
}
|
||||
|
||||
store := credentials.NewStore(keychain)
|
||||
|
||||
users, err := store.List()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create credentials store: %w", err)
|
||||
}
|
||||
|
||||
for _, userID := range users {
|
||||
logrus.WithField("userID", userID).Info("Migrating account")
|
||||
|
||||
creds, err := store.Get(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
authUID, authRef, err := creds.SplitAPIToken()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to split api token: %w", err)
|
||||
}
|
||||
|
||||
user, err := v.AddUser(creds.UserID, creds.EmailList()[0], authUID, authRef, creds.MailboxPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add user: %w", err)
|
||||
}
|
||||
|
||||
if err := user.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close user: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func migratePrefsToVault(vault *vault.Vault, b []byte) error {
|
||||
var prefs struct {
|
||||
IMAPPort int `json:"user_port_imap,,string"`
|
||||
SMTPPort int `json:"user_port_smtp,,string"`
|
||||
SMTPSSL bool `json:"user_ssl_smtp,,string"`
|
||||
|
||||
AutoUpdate bool `json:"autoupdate,,string"`
|
||||
UpdateChannel updater.Channel `json:"update_channel"`
|
||||
UpdateRollout float64 `json:"rollout,,string"`
|
||||
|
||||
FirstStart bool `json:"first_time_start,,string"`
|
||||
FirstStartGUI bool `json:"first_time_start_gui,,string"`
|
||||
ColorScheme string `json:"color_scheme"`
|
||||
LastVersion *semver.Version `json:"last_used_version"`
|
||||
Autostart bool `json:"autostart,,string"`
|
||||
|
||||
AllowProxy bool `json:"allow_proxy,,string"`
|
||||
FetchWorkers int `json:"fetch_workers,,string"`
|
||||
AttachmentWorkers int `json:"attachment_workers,,string"`
|
||||
ShowAllMail bool `json:"is_all_mail_visible,,string"`
|
||||
|
||||
Cookies string `json:"cookies"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, &prefs); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal old prefs file: %w", err)
|
||||
}
|
||||
|
||||
var errs error
|
||||
|
||||
if err := vault.SetIMAPPort(prefs.IMAPPort); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate IMAP port: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetSMTPPort(prefs.SMTPPort); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate SMTP port: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetSMTPSSL(prefs.SMTPSSL); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate SMTP SSL: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetAutoUpdate(prefs.AutoUpdate); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate auto update: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetUpdateChannel(prefs.UpdateChannel); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate update channel: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetUpdateRollout(prefs.UpdateRollout); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate rollout: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetFirstStart(prefs.FirstStart); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate first start: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetFirstStartGUI(prefs.FirstStartGUI); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate first start GUI: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetColorScheme(prefs.ColorScheme); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate color scheme: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetLastVersion(prefs.LastVersion); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate last version: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetAutostart(prefs.Autostart); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate autostart: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetProxyAllowed(prefs.AllowProxy); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate allow proxy: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetShowAllMail(prefs.ShowAllMail); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate show all mail: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetSyncWorkers(prefs.FetchWorkers); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate sync workers: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetSyncAttPool(prefs.AttachmentWorkers); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate sync attachment pool: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetCookies([]byte(prefs.Cookies)); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate cookies: %w", err))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func migrateOldVersions() (allErrors error) {
|
||||
cacheDir, cacheError := os.UserCacheDir()
|
||||
if cacheError != nil {
|
||||
allErrors = multierror.Append(allErrors, errors.Wrap(cacheError, "cannot get os cache"))
|
||||
return // not need to continue for now (with more migrations might be still ok to continue)
|
||||
}
|
||||
|
||||
if err := killV2AppAndRemoveV2LockFiles(filepath.Join(cacheDir, "protonmail", "bridge", "bridge.lock")); err != nil {
|
||||
allErrors = multierror.Append(allErrors, errors.Wrap(err, "cannot migrate lockfiles"))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func killV2AppAndRemoveV2LockFiles(lockFilePathV2 string) error {
|
||||
l := logrus.WithField("path", lockFilePathV2)
|
||||
|
||||
if _, err := os.Stat(lockFilePathV2); os.IsNotExist(err) {
|
||||
l.Debug("no v2 lockfile")
|
||||
return nil
|
||||
}
|
||||
|
||||
lock, err := singleinstance.CreateLockFile(lockFilePathV2)
|
||||
|
||||
if err == nil {
|
||||
l.Debug("no other v2 instance is running")
|
||||
|
||||
if errClose := lock.Close(); errClose != nil {
|
||||
l.WithError(errClose).Error("Cannot close lock file")
|
||||
}
|
||||
|
||||
return os.Remove(lockFilePathV2)
|
||||
}
|
||||
|
||||
// The other instance is an older version, so we should kill it.
|
||||
pid, err := getPID(lockFilePathV2)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot get v2 pid")
|
||||
}
|
||||
|
||||
if err := killPID(pid); err != nil {
|
||||
return errors.Wrapf(err, "cannot kill v2 app (PID %d)", pid)
|
||||
}
|
||||
|
||||
// Need to wait some time to release file lock
|
||||
time.Sleep(time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPID(lockFilePath string) (int, error) {
|
||||
file, err := os.Open(filepath.Clean(lockFilePath))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
rawPID := make([]byte, 10) // PID is probably up to 7 digits long, 10 should be enough
|
||||
n, err := file.Read(rawPID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.Atoi(strings.TrimSpace(string(rawPID[:n])))
|
||||
}
|
||||
|
||||
func killPID(pid int) error {
|
||||
p, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Kill()
|
||||
}
|
||||
81
internal/app/migration_test.go
Normal file
81
internal/app/migration_test.go
Normal file
@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMigratePrefsToVault(t *testing.T) {
|
||||
// Create a new vault.
|
||||
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"))
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
|
||||
// load the old prefs file.
|
||||
b, err := os.ReadFile(filepath.Join("testdata", "prefs.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Migrate the old prefs file to the new vault.
|
||||
require.NoError(t, migratePrefsToVault(vault, b))
|
||||
|
||||
// Check that the IMAP and SMTP prefs are migrated.
|
||||
require.Equal(t, 2143, vault.GetIMAPPort())
|
||||
require.Equal(t, 2025, vault.GetSMTPPort())
|
||||
require.True(t, vault.GetSMTPSSL())
|
||||
|
||||
// Check that the update channel is migrated.
|
||||
require.True(t, vault.GetAutoUpdate())
|
||||
require.Equal(t, updater.EarlyChannel, vault.GetUpdateChannel())
|
||||
require.Equal(t, 0.4849529004202015, vault.GetUpdateRollout())
|
||||
|
||||
// Check that the app settings have been migrated.
|
||||
require.False(t, vault.GetFirstStart())
|
||||
require.True(t, vault.GetFirstStartGUI())
|
||||
require.Equal(t, "blablabla", vault.GetColorScheme())
|
||||
require.Equal(t, "2.3.0+git", vault.GetLastVersion().String())
|
||||
require.True(t, vault.GetAutostart())
|
||||
|
||||
// Check that the other app settings have been migrated.
|
||||
require.Equal(t, 16, vault.SyncWorkers())
|
||||
require.Equal(t, 16, vault.SyncAttPool())
|
||||
require.False(t, vault.GetProxyAllowed())
|
||||
require.False(t, vault.GetShowAllMail())
|
||||
|
||||
// Check that the cookies have been migrated.
|
||||
jar, err := cookiejar.New(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
cookies, err := cookies.NewCookieJar(jar, vault)
|
||||
require.NoError(t, err)
|
||||
|
||||
url, err := url.Parse("https://api.protonmail.ch")
|
||||
require.NoError(t, err)
|
||||
|
||||
// There should be a cookie for the API.
|
||||
require.NotEmpty(t, cookies.Cookies(url))
|
||||
}
|
||||
70
internal/app/singleinstance.go
Normal file
70
internal/app/singleinstance.go
Normal file
@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||
"github.com/allan-simon/go-singleinstance"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// checkSingleInstance checks if another instance of the application is already running.
|
||||
// It tries to create a lock file at the given path.
|
||||
// If it succeeds, it returns the lock file and a nil error.
|
||||
//
|
||||
// For macOS and Linux when already running version is older than this instance
|
||||
// it will kill old and continue with this new bridge (i.e. no error returned).
|
||||
func checkSingleInstance(lockFilePath string, curVersion *semver.Version) (*os.File, error) {
|
||||
if lock, err := singleinstance.CreateLockFile(lockFilePath); err == nil {
|
||||
logrus.WithField("path", lockFilePath).Debug("Created lock file; no other instance is running")
|
||||
return lock, nil
|
||||
}
|
||||
|
||||
logrus.Debug("Failed to create lock file; another instance is running")
|
||||
|
||||
// We couldn't create the lock file, so another instance is probably running.
|
||||
// Check if it's an older version of the app.
|
||||
lastVersion, ok := focus.TryVersion()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to determine version of running instance")
|
||||
}
|
||||
|
||||
if !lastVersion.LessThan(curVersion) {
|
||||
return nil, fmt.Errorf("running instance is newer than this one")
|
||||
}
|
||||
|
||||
// The other instance is an older version, so we should kill it.
|
||||
pid, err := getPID(lockFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := killPID(pid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Need to wait some time to release file lock
|
||||
time.Sleep(time.Second)
|
||||
|
||||
return singleinstance.CreateLockFile(lockFilePath)
|
||||
}
|
||||
31
internal/app/testdata/prefs.json
vendored
Normal file
31
internal/app/testdata/prefs.json
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"allow_proxy": "false",
|
||||
"attachment_workers": "16",
|
||||
"autostart": "true",
|
||||
"autoupdate": "true",
|
||||
"cache_compression": "true",
|
||||
"cache_concurrent_read": "16",
|
||||
"cache_concurrent_write": "16",
|
||||
"cache_enabled": "true",
|
||||
"cache_location": "/home/user/.config/protonmail/bridge/cache/c11/messages",
|
||||
"cache_min_free_abs": "250000000",
|
||||
"cache_min_free_rat": "",
|
||||
"color_scheme": "blablabla",
|
||||
"cookies": "{\"https://api.protonmail.ch\":[{\"Name\":\"Session-Id\",\"Value\":\"blablablablablablablablabla\",\"Path\":\"/\",\"Domain\":\"protonmail.ch\",\"Expires\":\"2023-02-19T00:20:40.269424437+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":true,\"SameSite\":0,\"Raw\":\"Session-Id=blablablablablablablablabla; Domain=protonmail.ch; Path=/; HttpOnly; Secure; Max-Age=7776000\",\"Unparsed\":null},{\"Name\":\"Tag\",\"Value\":\"default\",\"Path\":\"/\",\"Domain\":\"\",\"Expires\":\"2023-02-19T00:20:40.269428627+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":false,\"SameSite\":0,\"Raw\":\"Tag=default; Path=/; Secure; Max-Age=7776000\",\"Unparsed\":null}],\"https://protonmail.com\":[{\"Name\":\"Session-Id\",\"Value\":\"blablablablablablablablabla\",\"Path\":\"/\",\"Domain\":\"protonmail.com\",\"Expires\":\"2023-02-19T00:20:18.315084712+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":true,\"SameSite\":0,\"Raw\":\"Session-Id=Y3q2Mh-ClvqL6LWeYdfyPgAAABI; Domain=protonmail.com; Path=/; HttpOnly; Secure; Max-Age=7776000\",\"Unparsed\":null},{\"Name\":\"Tag\",\"Value\":\"redirect\",\"Path\":\"/\",\"Domain\":\"\",\"Expires\":\"2023-02-19T00:20:18.315087646+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":false,\"SameSite\":0,\"Raw\":\"Tag=redirect; Path=/; Secure; Max-Age=7776000\",\"Unparsed\":null}]}",
|
||||
"fetch_workers": "16",
|
||||
"first_time_start": "false",
|
||||
"first_time_start_gui": "true",
|
||||
"imap_workers": "16",
|
||||
"is_all_mail_visible": "false",
|
||||
"last_heartbeat": "325",
|
||||
"last_used_version": "2.3.0+git",
|
||||
"preferred_keychain": "secret-service",
|
||||
"rebranding_migrated": "true",
|
||||
"report_outgoing_email_without_encryption": "false",
|
||||
"rollout": "0.4849529004202015",
|
||||
"user_port_api": "1042",
|
||||
"update_channel": "early",
|
||||
"user_port_imap": "2143",
|
||||
"user_port_smtp": "2025",
|
||||
"user_ssl_smtp": "true"
|
||||
}
|
||||
143
internal/app/vault.go
Normal file
143
internal/app/vault.go
Normal file
@ -0,0 +1,143 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func WithVault(locations *locations.Locations, fn func(*vault.Vault, bool, bool) error) error {
|
||||
logrus.Debug("Creating vault")
|
||||
defer logrus.Debug("Vault stopped")
|
||||
|
||||
// Create the encVault.
|
||||
encVault, insecure, corrupt, err := newVault(locations)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create vault: %w", err)
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"insecure": insecure,
|
||||
"corrupt": corrupt,
|
||||
}).Debug("Vault created")
|
||||
|
||||
// Install the certificates if needed.
|
||||
if installed := encVault.GetCertsInstalled(); !installed {
|
||||
logrus.Debug("Installing certificates")
|
||||
|
||||
if err := certs.NewInstaller().InstallCert(encVault.GetBridgeTLSCert()); err != nil {
|
||||
return fmt.Errorf("failed to install certs: %w", err)
|
||||
}
|
||||
|
||||
if err := encVault.SetCertsInstalled(true); err != nil {
|
||||
return fmt.Errorf("failed to set certs installed: %w", err)
|
||||
}
|
||||
|
||||
logrus.Debug("Certificates successfully installed")
|
||||
}
|
||||
|
||||
// GODT-1950: Add teardown actions (e.g. to close the vault).
|
||||
|
||||
return fn(encVault, insecure, corrupt)
|
||||
}
|
||||
|
||||
func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error) {
|
||||
vaultDir, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not get vault dir: %w", err)
|
||||
}
|
||||
|
||||
logrus.WithField("vaultDir", vaultDir).Debug("Loading vault from directory")
|
||||
|
||||
var (
|
||||
vaultKey []byte
|
||||
insecure bool
|
||||
)
|
||||
|
||||
if key, err := getVaultKey(vaultDir); err != nil {
|
||||
insecure = true
|
||||
|
||||
// We store the insecure vault in a separate directory
|
||||
vaultDir = path.Join(vaultDir, "insecure")
|
||||
} else {
|
||||
vaultKey = key
|
||||
}
|
||||
|
||||
gluonDir, err := locations.ProvideGluonPath()
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not provide gluon path: %w", err)
|
||||
}
|
||||
|
||||
vault, corrupt, err := vault.New(vaultDir, gluonDir, vaultKey)
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
|
||||
}
|
||||
|
||||
return vault, insecure, corrupt, nil
|
||||
}
|
||||
|
||||
func getVaultKey(vaultDir string) ([]byte, error) {
|
||||
helper, err := vault.GetHelper(vaultDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get keychain helper: %w", err)
|
||||
}
|
||||
|
||||
keychain, err := keychain.NewKeychain(helper, constants.KeyChainName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create keychain: %w", err)
|
||||
}
|
||||
|
||||
secrets, err := keychain.List()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not list keychain: %w", err)
|
||||
}
|
||||
|
||||
if !slices.Contains(secrets, vaultSecretName) {
|
||||
tok, err := crypto.RandomToken(32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not generate random token: %w", err)
|
||||
}
|
||||
|
||||
if err := keychain.Put(vaultSecretName, base64.StdEncoding.EncodeToString(tok)); err != nil {
|
||||
return nil, fmt.Errorf("could not put keychain item: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
_, keyEnc, err := keychain.Get(vaultSecretName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get keychain item: %w", err)
|
||||
}
|
||||
|
||||
keyDec, err := base64.StdEncoding.DecodeString(keyEnc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not decode keychain item: %w", err)
|
||||
}
|
||||
|
||||
return keyDec, nil
|
||||
}
|
||||
82
internal/async/context.go
Normal file
82
internal/async/context.go
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package async
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Abortable collects groups of functions that can be aborted by calling Abort.
|
||||
type Abortable struct {
|
||||
abortFunc []context.CancelFunc
|
||||
abortLock sync.RWMutex
|
||||
}
|
||||
|
||||
func (a *Abortable) Do(ctx context.Context, fn func(context.Context)) {
|
||||
fn(a.newCancelCtx(ctx))
|
||||
}
|
||||
|
||||
func (a *Abortable) Abort() {
|
||||
a.abortLock.RLock()
|
||||
defer a.abortLock.RUnlock()
|
||||
|
||||
for _, fn := range a.abortFunc {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Abortable) newCancelCtx(ctx context.Context) context.Context {
|
||||
a.abortLock.Lock()
|
||||
defer a.abortLock.Unlock()
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
a.abortFunc = append(a.abortFunc, cancel)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// RangeContext iterates over the given channel until the context is canceled or the
|
||||
// channel is closed.
|
||||
func RangeContext[T any](ctx context.Context, ch <-chan T, fn func(T)) {
|
||||
for {
|
||||
select {
|
||||
case v, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
fn(v)
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ForwardContext forwards all values from the src channel to the dst channel until the
|
||||
// context is canceled or the src channel is closed.
|
||||
func ForwardContext[T any](ctx context.Context, dst chan<- T, src <-chan T) {
|
||||
RangeContext(ctx, src, func(v T) {
|
||||
select {
|
||||
case dst <- v:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
})
|
||||
}
|
||||
233
internal/async/group.go
Normal file
233
internal/async/group.go
Normal file
@ -0,0 +1,233 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package async
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PanicHandler interface {
|
||||
HandlePanic()
|
||||
}
|
||||
|
||||
// Group is forked and improved version of "github.com/bradenaw/juniper/xsync.Group".
|
||||
//
|
||||
// It manages a group of goroutines. The main change to original is posibility
|
||||
// to wait passed function to finish without canceling it's context and adding
|
||||
// PanicHandler.
|
||||
type Group struct {
|
||||
baseCtx context.Context
|
||||
ctx context.Context
|
||||
jobCtx context.Context
|
||||
cancel context.CancelFunc
|
||||
finish context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
|
||||
panicHandler PanicHandler
|
||||
}
|
||||
|
||||
// NewGroup returns a Group ready for use. The context passed to any of the f functions will be a
|
||||
// descendant of ctx.
|
||||
func NewGroup(ctx context.Context, panicHandler PanicHandler) *Group {
|
||||
bgCtx, cancel := context.WithCancel(ctx)
|
||||
jobCtx, finish := context.WithCancel(ctx)
|
||||
return &Group{
|
||||
baseCtx: ctx,
|
||||
ctx: bgCtx,
|
||||
jobCtx: jobCtx,
|
||||
cancel: cancel,
|
||||
finish: finish,
|
||||
panicHandler: panicHandler,
|
||||
}
|
||||
}
|
||||
|
||||
// Once calls f once from another goroutine.
|
||||
func (g *Group) Once(f func(ctx context.Context)) {
|
||||
g.wg.Add(1)
|
||||
go func() {
|
||||
defer g.handlePanic()
|
||||
|
||||
f(g.ctx)
|
||||
g.wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
// jitterDuration returns a random duration in [d - jitter, d + jitter].
|
||||
func jitterDuration(d time.Duration, jitter time.Duration) time.Duration {
|
||||
return d + time.Duration(float64(jitter)*((rand.Float64()*2)-1)) //nolint:gosec
|
||||
}
|
||||
|
||||
// Periodic spawns a goroutine that calls f once per interval +/- jitter.
|
||||
func (g *Group) Periodic(
|
||||
interval time.Duration,
|
||||
jitter time.Duration,
|
||||
f func(ctx context.Context),
|
||||
) {
|
||||
g.wg.Add(1)
|
||||
go func() {
|
||||
defer g.handlePanic()
|
||||
|
||||
defer g.wg.Done()
|
||||
|
||||
t := time.NewTimer(jitterDuration(interval, jitter))
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
if g.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-g.jobCtx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
}
|
||||
|
||||
t.Reset(jitterDuration(interval, jitter))
|
||||
f(g.ctx)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Trigger spawns a goroutine which calls f whenever the returned function is called. If f is
|
||||
// already running when triggered, f will run again immediately when it finishes.
|
||||
func (g *Group) Trigger(f func(ctx context.Context)) func() {
|
||||
c := make(chan struct{}, 1)
|
||||
g.wg.Add(1)
|
||||
go func() {
|
||||
defer g.handlePanic()
|
||||
|
||||
defer g.wg.Done()
|
||||
|
||||
for {
|
||||
if g.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-g.jobCtx.Done():
|
||||
return
|
||||
case <-c:
|
||||
}
|
||||
f(g.ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
return func() {
|
||||
select {
|
||||
case c <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PeriodicOrTrigger spawns a goroutine which calls f whenever the returned function is called. If
|
||||
// f is already running when triggered, f will run again immediately when it finishes. Also calls f
|
||||
// when it has been interval+/-jitter since the last trigger.
|
||||
func (g *Group) PeriodicOrTrigger(
|
||||
interval time.Duration,
|
||||
jitter time.Duration,
|
||||
f func(ctx context.Context),
|
||||
) func() {
|
||||
c := make(chan struct{}, 1)
|
||||
g.wg.Add(1)
|
||||
go func() {
|
||||
defer g.handlePanic()
|
||||
|
||||
defer g.wg.Done()
|
||||
|
||||
t := time.NewTimer(jitterDuration(interval, jitter))
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
if g.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-g.jobCtx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
t.Reset(jitterDuration(interval, jitter))
|
||||
case <-c:
|
||||
if !t.Stop() {
|
||||
<-t.C
|
||||
}
|
||||
t.Reset(jitterDuration(interval, jitter))
|
||||
}
|
||||
f(g.ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
return func() {
|
||||
select {
|
||||
case c <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Group) resetCtx() {
|
||||
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
|
||||
g.ctx, g.cancel = context.WithCancel(g.baseCtx)
|
||||
}
|
||||
|
||||
// Cancel is send to all of the spawn goroutines and ends periodic
|
||||
// or trigger routines.
|
||||
func (g *Group) Cancel() {
|
||||
g.cancel()
|
||||
g.finish()
|
||||
g.resetCtx()
|
||||
}
|
||||
|
||||
// Finish will ends all periodic or polls routines. It will let
|
||||
// currently running functions to finish (cancel is not sent).
|
||||
//
|
||||
// It is not safe to call Wait concurrently with any other method on g.
|
||||
func (g *Group) Finish() {
|
||||
g.finish()
|
||||
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
|
||||
}
|
||||
|
||||
// CancelAndWait cancels the context passed to any of the spawned goroutines and waits for all spawned
|
||||
// goroutines to exit.
|
||||
//
|
||||
// It is not safe to call Wait concurrently with any other method on g.
|
||||
func (g *Group) CancelAndWait() {
|
||||
g.finish()
|
||||
g.cancel()
|
||||
g.wg.Wait()
|
||||
g.resetCtx()
|
||||
}
|
||||
|
||||
// WaitToFinish will ends all periodic or polls routines. It will wait for
|
||||
// currently running functions to finish (cancel is not sent).
|
||||
//
|
||||
// It is not safe to call Wait concurrently with any other method on g.
|
||||
func (g *Group) WaitToFinish() {
|
||||
g.finish()
|
||||
g.wg.Wait()
|
||||
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
|
||||
}
|
||||
|
||||
func (g *Group) handlePanic() {
|
||||
if g.panicHandler != nil {
|
||||
g.panicHandler.HandlePanic()
|
||||
}
|
||||
}
|
||||
45
internal/bridge/api.go
Normal file
45
internal/bridge/api.go
Normal file
@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// defaultAPIOptions returns a set of default API options for the given parameters.
|
||||
func defaultAPIOptions(
|
||||
apiURL string,
|
||||
version *semver.Version,
|
||||
cookieJar http.CookieJar,
|
||||
transport http.RoundTripper,
|
||||
poolSize int,
|
||||
) []proton.Option {
|
||||
return []proton.Option{
|
||||
proton.WithHostURL(apiURL),
|
||||
proton.WithAppVersion(constants.AppVersion(version.Original())),
|
||||
proton.WithCookieJar(cookieJar),
|
||||
proton.WithTransport(transport),
|
||||
proton.WithAttPoolSize(poolSize),
|
||||
proton.WithLogger(logrus.StandardLogger()),
|
||||
}
|
||||
}
|
||||
@ -13,24 +13,26 @@
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !build_qa
|
||||
// +build !build_qa
|
||||
|
||||
package pmapi
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
func getRootURL() string {
|
||||
return "https://api.protonmail.ch"
|
||||
}
|
||||
|
||||
func newProxyDialerAndTransport(cfg Config) (*ProxyTLSDialer, http.RoundTripper) {
|
||||
basicDialer := NewBasicTLSDialer(cfg)
|
||||
pinningDialer := NewPinningTLSDialer(cfg, basicDialer)
|
||||
proxyDialer := NewProxyTLSDialer(cfg, pinningDialer)
|
||||
return proxyDialer, CreateTransportWithDialer(proxyDialer)
|
||||
// newAPIOptions returns a set of API options for the given parameters.
|
||||
func newAPIOptions(
|
||||
apiURL string,
|
||||
version *semver.Version,
|
||||
cookieJar http.CookieJar,
|
||||
transport http.RoundTripper,
|
||||
poolSize int,
|
||||
) []proton.Option {
|
||||
return defaultAPIOptions(apiURL, version, cookieJar, transport, poolSize)
|
||||
}
|
||||
53
internal/bridge/api_qa.go
Normal file
53
internal/bridge/api_qa.go
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build build_qa
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
// newAPIOptions returns a set of API options for the given parameters.
|
||||
func newAPIOptions(
|
||||
apiURL string,
|
||||
version *semver.Version,
|
||||
cookieJar http.CookieJar,
|
||||
transport http.RoundTripper,
|
||||
poolSize int,
|
||||
) []proton.Option {
|
||||
opt := defaultAPIOptions(apiURL, version, cookieJar, transport, poolSize)
|
||||
|
||||
if host := os.Getenv("BRIDGE_API_HOST"); host != "" {
|
||||
opt = append(opt, proton.WithHostURL(host))
|
||||
}
|
||||
|
||||
if debug := os.Getenv("BRIDGE_API_DEBUG"); debug != "" {
|
||||
opt = append(opt, proton.WithDebug(true))
|
||||
}
|
||||
|
||||
if skipVerify := os.Getenv("BRIDGE_API_SKIP_VERIFY"); skipVerify != "" {
|
||||
opt = append(opt, proton.WithSkipVerifyProofs())
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
@ -13,292 +13,546 @@
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package bridge provides core functionality of Bridge app.
|
||||
// Package bridge implements the Bridge, which acts as the backend to the UI.
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/metrics"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/store/cache"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/users"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||
logrus "github.com/sirupsen/logrus"
|
||||
"github.com/ProtonMail/gluon"
|
||||
imapEvents "github.com/ProtonMail/gluon/events"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/gluon/watcher"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var log = logrus.WithField("pkg", "bridge") //nolint:gochecknoglobals
|
||||
|
||||
var ErrLocalCacheUnavailable = errors.New("local cache is unavailable")
|
||||
|
||||
type Bridge struct {
|
||||
*users.Users
|
||||
// vault holds bridge-specific data, such as preferences and known users (authorized or not).
|
||||
vault *vault.Vault
|
||||
|
||||
locations Locator
|
||||
settings SettingsProvider
|
||||
clientManager pmapi.Manager
|
||||
updater Updater
|
||||
versioner Versioner
|
||||
cacheProvider CacheProvider
|
||||
autostart *autostart.App
|
||||
// Bridge's global errors list.
|
||||
// users holds authorized users.
|
||||
users map[string]*user.User
|
||||
usersLock safe.RWMutex
|
||||
|
||||
// api manages user API clients.
|
||||
api *proton.Manager
|
||||
proxyCtl ProxyController
|
||||
identifier Identifier
|
||||
|
||||
// tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers.
|
||||
tlsConfig *tls.Config
|
||||
|
||||
// imapServer is the bridge's IMAP server.
|
||||
imapServer *gluon.Server
|
||||
imapListener net.Listener
|
||||
imapEventCh chan imapEvents.Event
|
||||
|
||||
// smtpServer is the bridge's SMTP server.
|
||||
smtpServer *smtp.Server
|
||||
smtpListener net.Listener
|
||||
|
||||
// updater is the bridge's updater.
|
||||
updater Updater
|
||||
installCh chan installJob
|
||||
|
||||
// curVersion is the current version of the bridge,
|
||||
// newVersion is the version that was installed by the updater.
|
||||
curVersion *semver.Version
|
||||
newVersion *semver.Version
|
||||
newVersionLock safe.RWMutex
|
||||
|
||||
// focusService is used to raise the bridge window when needed.
|
||||
focusService *focus.Service
|
||||
|
||||
// autostarter is the bridge's autostarter.
|
||||
autostarter Autostarter
|
||||
|
||||
// locator is the bridge's locator.
|
||||
locator Locator
|
||||
|
||||
// crashHandler
|
||||
crashHandler async.PanicHandler
|
||||
|
||||
// reporter
|
||||
reporter reporter.Reporter
|
||||
|
||||
// watchers holds all registered event watchers.
|
||||
watchers []*watcher.Watcher[events.Event]
|
||||
watchersLock sync.RWMutex
|
||||
|
||||
// errors contains errors encountered during startup.
|
||||
errors []error
|
||||
|
||||
isFirstStart bool
|
||||
lastVersion string
|
||||
// These control the bridge's IMAP and SMTP logging behaviour.
|
||||
logIMAPClient bool
|
||||
logIMAPServer bool
|
||||
logSMTP bool
|
||||
|
||||
// tasks manages the bridge's goroutines.
|
||||
tasks *async.Group
|
||||
|
||||
// goLoad triggers a load of disconnected users from the vault.
|
||||
goLoad func()
|
||||
|
||||
// goUpdate triggers a check/install of updates.
|
||||
goUpdate func()
|
||||
}
|
||||
|
||||
func New(
|
||||
locations Locator,
|
||||
cacheProvider CacheProvider,
|
||||
setting SettingsProvider,
|
||||
sentryReporter *sentry.Reporter,
|
||||
panicHandler users.PanicHandler,
|
||||
eventListener listener.Listener,
|
||||
cache cache.Cache,
|
||||
builder *message.Builder,
|
||||
clientManager pmapi.Manager,
|
||||
credStorer users.CredentialsStorer,
|
||||
updater Updater,
|
||||
versioner Versioner,
|
||||
autostart *autostart.App,
|
||||
) *Bridge {
|
||||
// Allow DoH before starting the app if the user has previously set this setting.
|
||||
// This allows us to start even if protonmail is blocked.
|
||||
if setting.GetBool(settings.AllowProxyKey) {
|
||||
clientManager.AllowProxy()
|
||||
}
|
||||
// New creates a new bridge.
|
||||
func New( //nolint:funlen
|
||||
locator Locator, // the locator to provide paths to store data
|
||||
vault *vault.Vault, // the bridge's encrypted data store
|
||||
autostarter Autostarter, // the autostarter to manage autostart settings
|
||||
updater Updater, // the updater to fetch and install updates
|
||||
curVersion *semver.Version, // the current version of the bridge
|
||||
|
||||
u := users.New(
|
||||
locations,
|
||||
panicHandler,
|
||||
eventListener,
|
||||
clientManager,
|
||||
credStorer,
|
||||
newStoreFactory(cacheProvider, sentryReporter, panicHandler, eventListener, cache, builder),
|
||||
apiURL string, // the URL of the API to use
|
||||
cookieJar http.CookieJar, // the cookie jar to use
|
||||
identifier Identifier, // the identifier to keep track of the user agent
|
||||
tlsReporter TLSReporter, // the TLS reporter to report TLS errors
|
||||
roundTripper http.RoundTripper, // the round tripper to use for API requests
|
||||
proxyCtl ProxyController, // the DoH controller
|
||||
crashHandler async.PanicHandler,
|
||||
reporter reporter.Reporter,
|
||||
|
||||
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
|
||||
logSMTP bool, // whether to log SMTP activity
|
||||
) (*Bridge, <-chan events.Event, error) {
|
||||
// api is the user's API manager.
|
||||
api := proton.New(newAPIOptions(apiURL, curVersion, cookieJar, roundTripper, vault.SyncAttPool())...)
|
||||
|
||||
// tasks holds all the bridge's background tasks.
|
||||
tasks := async.NewGroup(context.Background(), crashHandler)
|
||||
|
||||
// imapEventCh forwards IMAP events from gluon instances to the bridge for processing.
|
||||
imapEventCh := make(chan imapEvents.Event)
|
||||
|
||||
// bridge is the bridge.
|
||||
bridge, err := newBridge(
|
||||
tasks,
|
||||
imapEventCh,
|
||||
|
||||
locator,
|
||||
vault,
|
||||
autostarter,
|
||||
updater,
|
||||
curVersion,
|
||||
crashHandler,
|
||||
reporter,
|
||||
|
||||
api,
|
||||
identifier,
|
||||
proxyCtl,
|
||||
logIMAPClient, logIMAPServer, logSMTP,
|
||||
)
|
||||
|
||||
b := &Bridge{
|
||||
Users: u,
|
||||
locations: locations,
|
||||
settings: setting,
|
||||
clientManager: clientManager,
|
||||
updater: updater,
|
||||
versioner: versioner,
|
||||
cacheProvider: cacheProvider,
|
||||
autostart: autostart,
|
||||
isFirstStart: false,
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create bridge: %w", err)
|
||||
}
|
||||
|
||||
if setting.GetBool(settings.FirstStartKey) {
|
||||
b.isFirstStart = true
|
||||
if err := b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(constants.Version))); err != nil {
|
||||
logrus.WithError(err).Error("Failed to send metric")
|
||||
// Get an event channel for all events (individual events can be subscribed to later).
|
||||
eventCh, _ := bridge.GetEvents()
|
||||
|
||||
// Initialize all of bridge's background tasks and operations.
|
||||
if err := bridge.init(tlsReporter); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to initialize bridge: %w", err)
|
||||
}
|
||||
|
||||
// Start serving IMAP.
|
||||
if err := bridge.serveIMAP(); err != nil {
|
||||
bridge.PushError(ErrServeIMAP)
|
||||
}
|
||||
|
||||
// Start serving SMTP.
|
||||
if err := bridge.serveSMTP(); err != nil {
|
||||
bridge.PushError(ErrServeSMTP)
|
||||
}
|
||||
|
||||
return bridge, eventCh, nil
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func newBridge(
|
||||
tasks *async.Group,
|
||||
imapEventCh chan imapEvents.Event,
|
||||
|
||||
locator Locator,
|
||||
vault *vault.Vault,
|
||||
autostarter Autostarter,
|
||||
updater Updater,
|
||||
curVersion *semver.Version,
|
||||
crashHandler async.PanicHandler,
|
||||
reporter reporter.Reporter,
|
||||
|
||||
api *proton.Manager,
|
||||
identifier Identifier,
|
||||
proxyCtl ProxyController,
|
||||
|
||||
logIMAPClient, logIMAPServer, logSMTP bool,
|
||||
) (*Bridge, error) {
|
||||
tlsConfig, err := loadTLSConfig(vault)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load TLS config: %w", err)
|
||||
}
|
||||
|
||||
gluonDir, err := getGluonDir(vault)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Gluon directory: %w", err)
|
||||
}
|
||||
|
||||
imapServer, err := newIMAPServer(
|
||||
gluonDir,
|
||||
curVersion,
|
||||
tlsConfig,
|
||||
reporter,
|
||||
logIMAPClient,
|
||||
logIMAPServer,
|
||||
imapEventCh,
|
||||
tasks,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create IMAP server: %w", err)
|
||||
}
|
||||
|
||||
focusService, err := focus.NewService(curVersion)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create focus service: %w", err)
|
||||
}
|
||||
|
||||
bridge := &Bridge{
|
||||
vault: vault,
|
||||
|
||||
users: make(map[string]*user.User),
|
||||
usersLock: safe.NewRWMutex(),
|
||||
|
||||
api: api,
|
||||
proxyCtl: proxyCtl,
|
||||
identifier: identifier,
|
||||
|
||||
tlsConfig: tlsConfig,
|
||||
imapServer: imapServer,
|
||||
imapEventCh: imapEventCh,
|
||||
|
||||
updater: updater,
|
||||
installCh: make(chan installJob),
|
||||
|
||||
curVersion: curVersion,
|
||||
newVersion: curVersion,
|
||||
newVersionLock: safe.NewRWMutex(),
|
||||
|
||||
crashHandler: crashHandler,
|
||||
reporter: reporter,
|
||||
|
||||
focusService: focusService,
|
||||
autostarter: autostarter,
|
||||
locator: locator,
|
||||
|
||||
logIMAPClient: logIMAPClient,
|
||||
logIMAPServer: logIMAPServer,
|
||||
logSMTP: logSMTP,
|
||||
|
||||
tasks: tasks,
|
||||
}
|
||||
|
||||
bridge.smtpServer = newSMTPServer(bridge, tlsConfig, logSMTP)
|
||||
|
||||
return bridge, nil
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
||||
// Enable or disable the proxy at startup.
|
||||
if bridge.vault.GetProxyAllowed() {
|
||||
bridge.proxyCtl.AllowProxy()
|
||||
} else {
|
||||
bridge.proxyCtl.DisallowProxy()
|
||||
}
|
||||
|
||||
// Handle connection up/down events.
|
||||
bridge.api.AddStatusObserver(func(status proton.Status) {
|
||||
logrus.Info("API status changed: ", status)
|
||||
|
||||
switch {
|
||||
case status == proton.StatusUp:
|
||||
bridge.publish(events.ConnStatusUp{})
|
||||
bridge.tasks.Once(bridge.onStatusUp)
|
||||
|
||||
case status == proton.StatusDown:
|
||||
bridge.publish(events.ConnStatusDown{})
|
||||
bridge.tasks.Once(bridge.onStatusDown)
|
||||
}
|
||||
setting.SetBool(settings.FirstStartKey, false)
|
||||
})
|
||||
|
||||
// If any call returns a bad version code, we need to update.
|
||||
bridge.api.AddErrorHandler(proton.AppVersionBadCode, func() {
|
||||
logrus.Warn("App version is bad")
|
||||
bridge.publish(events.UpdateForced{})
|
||||
})
|
||||
|
||||
// Ensure all outgoing headers have the correct user agent.
|
||||
bridge.api.AddPreRequestHook(func(_ *resty.Client, req *resty.Request) error {
|
||||
req.SetHeader("User-Agent", bridge.identifier.GetUserAgent())
|
||||
return nil
|
||||
})
|
||||
|
||||
// Log all manager API requests (client requests are logged separately).
|
||||
bridge.api.AddPostRequestHook(func(_ *resty.Client, r *resty.Response) error {
|
||||
if _, ok := proton.ClientIDFromContext(r.Request.Context()); !ok {
|
||||
logrus.Infof("[MANAGER] %v: %v %v", r.Status(), r.Request.Method, r.Request.URL)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// Publish a TLS issue event if a TLS issue is encountered.
|
||||
bridge.tasks.Once(func(ctx context.Context) {
|
||||
async.RangeContext(ctx, tlsReporter.GetTLSIssueCh(), func(struct{}) {
|
||||
logrus.Warn("TLS issue encountered")
|
||||
bridge.publish(events.TLSIssue{})
|
||||
})
|
||||
})
|
||||
|
||||
// Publish a raise event if the focus service is called.
|
||||
bridge.tasks.Once(func(ctx context.Context) {
|
||||
async.RangeContext(ctx, bridge.focusService.GetRaiseCh(), func(struct{}) {
|
||||
logrus.Info("Focus service requested raise")
|
||||
bridge.publish(events.Raise{})
|
||||
})
|
||||
})
|
||||
|
||||
// Handle any IMAP events that are forwarded to the bridge from gluon.
|
||||
bridge.tasks.Once(func(ctx context.Context) {
|
||||
async.RangeContext(ctx, bridge.imapEventCh, func(event imapEvents.Event) {
|
||||
logrus.WithField("event", fmt.Sprintf("%T", event)).Debug("Received IMAP event")
|
||||
bridge.handleIMAPEvent(event)
|
||||
})
|
||||
})
|
||||
|
||||
// Attempt to lazy load users when triggered.
|
||||
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
|
||||
logrus.Info("Loading users")
|
||||
|
||||
if err := bridge.loadUsers(ctx); err != nil {
|
||||
logrus.WithError(err).Error("Failed to load users")
|
||||
} else {
|
||||
bridge.publish(events.AllUsersLoaded{})
|
||||
}
|
||||
})
|
||||
defer bridge.goLoad()
|
||||
|
||||
// Check for updates when triggered.
|
||||
bridge.goUpdate = bridge.tasks.PeriodicOrTrigger(constants.UpdateCheckInterval, 0, func(ctx context.Context) {
|
||||
logrus.Info("Checking for updates")
|
||||
|
||||
version, err := bridge.updater.GetVersionInfo(ctx, bridge.api, bridge.vault.GetUpdateChannel())
|
||||
if err != nil {
|
||||
bridge.publish(events.UpdateCheckFailed{Error: err})
|
||||
} else {
|
||||
bridge.handleUpdate(version)
|
||||
}
|
||||
})
|
||||
defer bridge.goUpdate()
|
||||
|
||||
// Install updates when available.
|
||||
bridge.tasks.Once(func(ctx context.Context) {
|
||||
async.RangeContext(ctx, bridge.installCh, func(job installJob) {
|
||||
bridge.installUpdate(ctx, job)
|
||||
})
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEvents returns a channel of events of the given type.
|
||||
// If no types are supplied, all events are returned.
|
||||
func (bridge *Bridge) GetEvents(ofType ...events.Event) (<-chan events.Event, context.CancelFunc) {
|
||||
watcher := bridge.addWatcher(ofType...)
|
||||
|
||||
return watcher.GetChannel(), func() { bridge.remWatcher(watcher) }
|
||||
}
|
||||
|
||||
func (bridge *Bridge) PushError(err error) {
|
||||
bridge.errors = append(bridge.errors, err)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetErrors() []error {
|
||||
return bridge.errors
|
||||
}
|
||||
|
||||
func (bridge *Bridge) Close(ctx context.Context) {
|
||||
logrus.Info("Closing bridge")
|
||||
|
||||
// Close the IMAP server.
|
||||
if err := bridge.closeIMAP(ctx); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close IMAP server")
|
||||
}
|
||||
|
||||
// Keep in bridge and update in settings the last used version.
|
||||
b.lastVersion = b.settings.Get(settings.LastVersionKey)
|
||||
b.settings.Set(settings.LastVersionKey, constants.Version)
|
||||
// Close the SMTP server.
|
||||
if err := bridge.closeSMTP(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close SMTP server")
|
||||
}
|
||||
|
||||
go b.heartbeat()
|
||||
// Close all users.
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.Close()
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
// Stop all ongoing tasks.
|
||||
bridge.tasks.CancelAndWait()
|
||||
|
||||
// Close the focus service.
|
||||
bridge.focusService.Close()
|
||||
|
||||
// Close the watchers.
|
||||
bridge.watchersLock.Lock()
|
||||
defer bridge.watchersLock.Unlock()
|
||||
|
||||
for _, watcher := range bridge.watchers {
|
||||
watcher.Close()
|
||||
}
|
||||
|
||||
bridge.watchers = nil
|
||||
|
||||
// Save the last version of bridge that was run.
|
||||
if err := bridge.vault.SetLastVersion(bridge.curVersion); err != nil {
|
||||
logrus.WithError(err).Error("Failed to save last version")
|
||||
}
|
||||
}
|
||||
|
||||
func (bridge *Bridge) publish(event events.Event) {
|
||||
bridge.watchersLock.RLock()
|
||||
defer bridge.watchersLock.RUnlock()
|
||||
|
||||
logrus.WithField("event", event).Debug("Publishing event")
|
||||
|
||||
for _, watcher := range bridge.watchers {
|
||||
if watcher.IsWatching(event) {
|
||||
if ok := watcher.Send(event); !ok {
|
||||
logrus.WithField("event", event).Warn("Failed to send event to watcher")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (bridge *Bridge) addWatcher(ofType ...events.Event) *watcher.Watcher[events.Event] {
|
||||
bridge.watchersLock.Lock()
|
||||
defer bridge.watchersLock.Unlock()
|
||||
|
||||
watcher := watcher.New(ofType...)
|
||||
|
||||
bridge.watchers = append(bridge.watchers, watcher)
|
||||
|
||||
return watcher
|
||||
}
|
||||
|
||||
func (bridge *Bridge) remWatcher(watcher *watcher.Watcher[events.Event]) {
|
||||
bridge.watchersLock.Lock()
|
||||
defer bridge.watchersLock.Unlock()
|
||||
|
||||
idx := xslices.Index(bridge.watchers, watcher)
|
||||
|
||||
if idx < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
bridge.watchers = append(bridge.watchers[:idx], bridge.watchers[idx+1:]...)
|
||||
|
||||
watcher.Close()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) onStatusUp(ctx context.Context) {
|
||||
logrus.Info("Handling API status up")
|
||||
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.OnStatusUp(ctx)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
bridge.goLoad()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
||||
logrus.Info("Handling API status down")
|
||||
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.OnStatusDown(ctx)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case <-time.After(backoff):
|
||||
logrus.Info("Pinging API")
|
||||
|
||||
if err := bridge.api.Ping(ctx); err != nil {
|
||||
logrus.WithError(err).Warn("Ping failed, API is still unreachable")
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
||||
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert(), vault.GetBridgeTLSKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newListener(port int, useTLS bool, tlsConfig *tls.Config) (net.Listener, error) {
|
||||
if useTLS {
|
||||
tlsListener, err := tls.Listen("tcp", fmt.Sprintf("%v:%v", constants.Host, port), tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tlsListener, nil
|
||||
}
|
||||
|
||||
netListener, err := net.Listen("tcp", fmt.Sprintf("%v:%v", constants.Host, port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return netListener, nil
|
||||
}
|
||||
|
||||
func min(a, b time.Duration) time.Duration {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// heartbeat sends a heartbeat signal once a day.
|
||||
func (b *Bridge) heartbeat() {
|
||||
for range time.Tick(time.Minute) {
|
||||
lastHeartbeatDay, err := strconv.ParseInt(b.settings.Get(settings.LastHeartbeatKey), 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// If we're still on the same day, don't send a heartbeat.
|
||||
if time.Now().YearDay() == int(lastHeartbeatDay) {
|
||||
continue
|
||||
}
|
||||
|
||||
// We're on the next (or a different) day, so send a heartbeat.
|
||||
if err := b.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel)); err != nil {
|
||||
logrus.WithError(err).Error("Failed to send heartbeat")
|
||||
continue
|
||||
}
|
||||
|
||||
// Heartbeat was sent successfully so update the last heartbeat day.
|
||||
b.settings.Set(settings.LastHeartbeatKey, fmt.Sprintf("%v", time.Now().YearDay()))
|
||||
}
|
||||
}
|
||||
|
||||
// GetUpdateChannel returns currently set update channel.
|
||||
func (b *Bridge) GetUpdateChannel() updater.UpdateChannel {
|
||||
return updater.UpdateChannel(b.settings.Get(settings.UpdateChannelKey))
|
||||
}
|
||||
|
||||
// SetUpdateChannel switches update channel.
|
||||
func (b *Bridge) SetUpdateChannel(channel updater.UpdateChannel) {
|
||||
b.settings.Set(settings.UpdateChannelKey, string(channel))
|
||||
}
|
||||
|
||||
func (b *Bridge) resetToLatestStable() error {
|
||||
version, err := b.updater.Check()
|
||||
if err != nil {
|
||||
// If we can not check for updates - just remove all local updates and reset to base installer version.
|
||||
// Not using `b.locations.ClearUpdates()` because `versioner.RemoveOtherVersions` can also handle
|
||||
// case when it is needed to remove currently running verion.
|
||||
if err := b.versioner.RemoveOtherVersions(semver.MustParse("0.0.0")); err != nil {
|
||||
log.WithError(err).Error("Failed to clear updates while downgrading channel")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If current version is same as upstream stable version - do nothing.
|
||||
if version.Version.Equal(semver.MustParse(constants.Version)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := b.updater.InstallUpdate(version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.versioner.RemoveOtherVersions(version.Version)
|
||||
}
|
||||
|
||||
// FactoryReset will remove all local cache and settings.
|
||||
// It will also downgrade to latest stable version if user is on early version.
|
||||
func (b *Bridge) FactoryReset() {
|
||||
wasEarly := b.GetUpdateChannel() == updater.EarlyChannel
|
||||
|
||||
b.settings.Set(settings.UpdateChannelKey, string(updater.StableChannel))
|
||||
|
||||
if wasEarly {
|
||||
if err := b.resetToLatestStable(); err != nil {
|
||||
log.WithError(err).Error("Failed to reset to latest stable version")
|
||||
}
|
||||
}
|
||||
|
||||
if err := b.Users.ClearData(); err != nil {
|
||||
log.WithError(err).Error("Failed to remove bridge data")
|
||||
}
|
||||
|
||||
if err := b.Users.ClearUsers(); err != nil {
|
||||
log.WithError(err).Error("Failed to remove bridge users")
|
||||
}
|
||||
}
|
||||
|
||||
// GetKeychainApp returns current keychain helper.
|
||||
func (b *Bridge) GetKeychainApp() string {
|
||||
return b.settings.Get(settings.PreferredKeychainKey)
|
||||
}
|
||||
|
||||
// SetKeychainApp sets current keychain helper.
|
||||
func (b *Bridge) SetKeychainApp(helper string) {
|
||||
b.settings.Set(settings.PreferredKeychainKey, helper)
|
||||
}
|
||||
|
||||
func (b *Bridge) EnableCache() error {
|
||||
if err := b.Users.EnableCache(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.settings.SetBool(settings.CacheEnabledKey, true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bridge) DisableCache() error {
|
||||
if err := b.Users.DisableCache(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.settings.SetBool(settings.CacheEnabledKey, false)
|
||||
// Reset back to the default location when disabling.
|
||||
b.settings.Set(settings.CacheLocationKey, b.cacheProvider.GetDefaultMessageCacheDir())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bridge) MigrateCache(from, to string) error {
|
||||
if err := b.Users.MigrateCache(from, to); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.settings.Set(settings.CacheLocationKey, to)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetProxyAllowed instructs the app whether to use DoH to access an API proxy if necessary.
|
||||
// It also needs to work before the app is initialised (because we may need to use the proxy at startup).
|
||||
func (b *Bridge) SetProxyAllowed(proxyAllowed bool) {
|
||||
b.settings.SetBool(settings.AllowProxyKey, proxyAllowed)
|
||||
if proxyAllowed {
|
||||
b.clientManager.AllowProxy()
|
||||
} else {
|
||||
b.clientManager.DisallowProxy()
|
||||
}
|
||||
}
|
||||
|
||||
// GetProxyAllowed returns whether use of DoH is enabled to access an API proxy if necessary.
|
||||
func (b *Bridge) GetProxyAllowed() bool {
|
||||
return b.settings.GetBool(settings.AllowProxyKey)
|
||||
}
|
||||
|
||||
// AddError add an error to a global error list if it does not contain it yet. Adding nil is noop.
|
||||
func (b *Bridge) AddError(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if b.HasError(err) {
|
||||
return
|
||||
}
|
||||
|
||||
b.errors = append(b.errors, err)
|
||||
}
|
||||
|
||||
// DelError removes an error from global error list.
|
||||
func (b *Bridge) DelError(err error) {
|
||||
for idx, val := range b.errors {
|
||||
if val == err {
|
||||
b.errors = append(b.errors[:idx], b.errors[idx+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HasError returnes true if global error list contains an err.
|
||||
func (b *Bridge) HasError(err error) bool {
|
||||
for _, val := range b.errors {
|
||||
if val == err {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetLastVersion returns the version which was used in previous execution of
|
||||
// Bridge.
|
||||
func (b *Bridge) GetLastVersion() string {
|
||||
return b.lastVersion
|
||||
}
|
||||
|
||||
// IsFirstStart returns true when Bridge is running for first time or after
|
||||
// factory reset.
|
||||
func (b *Bridge) IsFirstStart() bool {
|
||||
return b.isFirstStart
|
||||
}
|
||||
|
||||
644
internal/bridge/bridge_test.go
Normal file
644
internal/bridge/bridge_test.go
Normal file
@ -0,0 +1,644 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/go-proton-api/server/backend"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/tests"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
username = "username"
|
||||
password = []byte("password")
|
||||
|
||||
v2_3_0 = semver.MustParse("2.3.0")
|
||||
v2_4_0 = semver.MustParse("2.4.0")
|
||||
)
|
||||
|
||||
func init() {
|
||||
user.EventPeriod = 100 * time.Millisecond
|
||||
user.EventJitter = 0
|
||||
backend.GenerateKey = tests.FastGenerateKey
|
||||
certs.GenerateCert = tests.FastGenerateCert
|
||||
}
|
||||
|
||||
func TestBridge_ConnStatus(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Get a stream of connection status events.
|
||||
eventCh, done := bridge.GetEvents(events.ConnStatusUp{}, events.ConnStatusDown{})
|
||||
defer done()
|
||||
|
||||
// Simulate network disconnect.
|
||||
netCtl.Disable()
|
||||
|
||||
// Trigger some operation that will fail due to the network disconnect.
|
||||
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.Error(t, err)
|
||||
|
||||
// Wait for the event.
|
||||
require.Equal(t, events.ConnStatusDown{}, <-eventCh)
|
||||
|
||||
// Simulate network reconnect.
|
||||
netCtl.Enable()
|
||||
|
||||
// Trigger some operation that will succeed due to the network reconnect.
|
||||
userID, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, userID)
|
||||
|
||||
// Wait for the event.
|
||||
require.Equal(t, events.ConnStatusUp{}, <-eventCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_TLSIssue(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Get a stream of TLS issue events.
|
||||
tlsEventCh, done := bridge.GetEvents(events.TLSIssue{})
|
||||
defer done()
|
||||
|
||||
// Simulate a TLS issue.
|
||||
go func() {
|
||||
mocks.TLSIssueCh <- struct{}{}
|
||||
}()
|
||||
|
||||
// Wait for the event.
|
||||
require.IsType(t, events.TLSIssue{}, <-tlsEventCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Focus(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Get a stream of TLS issue events.
|
||||
raiseCh, done := bridge.GetEvents(events.Raise{})
|
||||
defer done()
|
||||
|
||||
// Simulate a focus event.
|
||||
focus.TryRaise()
|
||||
|
||||
// Wait for the event.
|
||||
require.IsType(t, events.Raise{}, <-raiseCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_UserAgent(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
var (
|
||||
calls []server.Call
|
||||
lock sync.Mutex
|
||||
)
|
||||
|
||||
s.AddCallWatcher(func(call server.Call) {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
calls = append(calls, call)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Set the platform to something other than the default.
|
||||
bridge.SetCurrentPlatform("platform")
|
||||
|
||||
// Assert that the user agent then contains the platform.
|
||||
require.Contains(t, bridge.GetCurrentUserAgent(), "platform")
|
||||
|
||||
// Login the user.
|
||||
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
// Assert that the user agent was sent to the API.
|
||||
require.Contains(t, calls[len(calls)-1].RequestHeader.Get("User-Agent"), bridge.GetCurrentUserAgent())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Cookies(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
var (
|
||||
sessionIDs []string
|
||||
sessionIDsLock sync.RWMutex
|
||||
)
|
||||
|
||||
// Save any session IDs we use.
|
||||
s.AddCallWatcher(func(call server.Call) {
|
||||
cookie, err := (&http.Request{Header: call.RequestHeader}).Cookie("Session-Id")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sessionIDsLock.Lock()
|
||||
defer sessionIDsLock.Unlock()
|
||||
|
||||
sessionIDs = append(sessionIDs, cookie.Value)
|
||||
})
|
||||
|
||||
// Start bridge and add a user so that API assigns us a session ID via cookie.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
// Start bridge again and check that it uses the same session ID.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// ...
|
||||
})
|
||||
|
||||
// We should have used just one session ID.
|
||||
sessionIDsLock.Lock()
|
||||
defer sessionIDsLock.Unlock()
|
||||
|
||||
require.Len(t, xslices.Unique(sessionIDs), 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_CheckUpdate(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Disable autoupdate for this test.
|
||||
require.NoError(t, bridge.SetAutoUpdate(false))
|
||||
|
||||
// Get a stream of update not available events.
|
||||
noUpdateCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
|
||||
defer done()
|
||||
|
||||
// We are currently on the latest version.
|
||||
bridge.CheckForUpdates()
|
||||
|
||||
// we should receive an event indicating that no update is available.
|
||||
require.Equal(t, events.UpdateNotAvailable{}, <-noUpdateCh)
|
||||
|
||||
// Simulate a new version being available.
|
||||
mocks.Updater.SetLatestVersion(v2_4_0, v2_3_0)
|
||||
|
||||
// Get a stream of update available events.
|
||||
updateCh, done := bridge.GetEvents(events.UpdateAvailable{})
|
||||
defer done()
|
||||
|
||||
// Check for updates.
|
||||
bridge.CheckForUpdates()
|
||||
|
||||
// We should receive an event indicating that an update is available.
|
||||
require.Equal(t, events.UpdateAvailable{
|
||||
Version: updater.VersionInfo{
|
||||
Version: v2_4_0,
|
||||
MinAuto: v2_3_0,
|
||||
RolloutProportion: 1.0,
|
||||
},
|
||||
Silent: false,
|
||||
Compatible: true,
|
||||
}, <-updateCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_AutoUpdate(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Enable autoupdate for this test.
|
||||
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||
|
||||
// Get a stream of update events.
|
||||
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
|
||||
defer done()
|
||||
|
||||
// Simulate a new version being available.
|
||||
mocks.Updater.SetLatestVersion(v2_4_0, v2_3_0)
|
||||
|
||||
// Check for updates.
|
||||
bridge.CheckForUpdates()
|
||||
|
||||
// We should receive an event indicating that the update was silently installed.
|
||||
require.Equal(t, events.UpdateInstalled{
|
||||
Version: updater.VersionInfo{
|
||||
Version: v2_4_0,
|
||||
MinAuto: v2_3_0,
|
||||
RolloutProportion: 1.0,
|
||||
},
|
||||
Silent: true,
|
||||
}, <-updateCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_ManualUpdate(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Disable autoupdate for this test.
|
||||
require.NoError(t, bridge.SetAutoUpdate(false))
|
||||
|
||||
// Get a stream of update available events.
|
||||
updateCh, done := bridge.GetEvents(events.UpdateAvailable{})
|
||||
defer done()
|
||||
|
||||
// Simulate a new version being available, but it's too new for us.
|
||||
mocks.Updater.SetLatestVersion(v2_4_0, v2_4_0)
|
||||
|
||||
// Check for updates.
|
||||
bridge.CheckForUpdates()
|
||||
|
||||
// We should receive an event indicating an update is available, but we can't install it.
|
||||
require.Equal(t, events.UpdateAvailable{
|
||||
Version: updater.VersionInfo{
|
||||
Version: v2_4_0,
|
||||
MinAuto: v2_4_0,
|
||||
RolloutProportion: 1.0,
|
||||
},
|
||||
Silent: false,
|
||||
Compatible: false,
|
||||
}, <-updateCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_ForceUpdate(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Get a stream of update events.
|
||||
updateCh, done := bridge.GetEvents(events.UpdateForced{})
|
||||
defer done()
|
||||
|
||||
// Set the minimum accepted app version to something newer than the current version.
|
||||
s.SetMinAppVersion(v2_4_0)
|
||||
|
||||
// Try to login the user. It will fail because the bridge is too old.
|
||||
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.Error(t, err)
|
||||
|
||||
// We should get an update required event.
|
||||
require.Equal(t, events.UpdateForced{}, <-updateCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_BadVaultKey(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
var userID string
|
||||
|
||||
// Login a user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
newUserID, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
userID = newUserID
|
||||
})
|
||||
|
||||
// Start bridge with the correct vault key -- it should load the users correctly.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.ElementsMatch(t, []string{userID}, bridge.GetUserIDs())
|
||||
})
|
||||
|
||||
// Start bridge with a bad vault key, the vault will be wiped and bridge will show no users.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, []byte("bad"), func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
})
|
||||
|
||||
// Start bridge with a nil vault key, the vault will be wiped and bridge will show no users.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, nil, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_MissingGluonDir(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
var gluonDir string
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Move the gluon dir.
|
||||
require.NoError(t, bridge.SetGluonDir(ctx, t.TempDir()))
|
||||
|
||||
// Get the gluon dir.
|
||||
gluonDir = bridge.GetGluonDir()
|
||||
})
|
||||
|
||||
// The user removes the gluon dir while bridge is not running.
|
||||
require.NoError(t, os.RemoveAll(gluonDir))
|
||||
|
||||
// Bridge starts but can't find the gluon dir; there should be no error.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// ...
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_AddressWithoutKeys(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
m := proton.New(
|
||||
proton.WithHostURL(s.GetHostURL()),
|
||||
proton.WithTransport(proton.InsecureTransport()),
|
||||
)
|
||||
defer m.Close()
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Create a user which will have an address without keys.
|
||||
userID, _, err := s.CreateUser("nokeys", "nokeys@pm.me", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an additional address for the user; it will not have keys.
|
||||
aliasAddrID, err := s.CreateAddress(userID, "alias@pm.me", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an API client so we can remove the address keys.
|
||||
c, _, err := m.NewClientWithLogin(ctx, "nokeys", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
// Get the alias address.
|
||||
aliasAddr, err := c.GetAddress(ctx, aliasAddrID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Remove the address keys.
|
||||
require.NoError(t, s.RemoveAddressKey(userID, aliasAddrID, aliasAddr.Keys[0].ID))
|
||||
|
||||
// Watch for sync finished event.
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
// We should be able to log the user in.
|
||||
require.NoError(t, getErr(bridge.LoginFull(context.Background(), "nokeys", []byte("password"), nil, nil)))
|
||||
require.NoError(t, err)
|
||||
|
||||
// The sync should eventually finish for the user without keys.
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_FactoryReset(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// The settings should be their default values.
|
||||
require.True(t, bridge.GetAutoUpdate())
|
||||
require.Equal(t, updater.StableChannel, bridge.GetUpdateChannel())
|
||||
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Change some settings.
|
||||
require.NoError(t, bridge.SetAutoUpdate(false))
|
||||
require.NoError(t, bridge.SetUpdateChannel(updater.EarlyChannel))
|
||||
|
||||
// The user is now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// The settings should be changed.
|
||||
require.False(t, bridge.GetAutoUpdate())
|
||||
require.Equal(t, updater.EarlyChannel, bridge.GetUpdateChannel())
|
||||
|
||||
// Perform a factory reset.
|
||||
bridge.FactoryReset(ctx)
|
||||
|
||||
// The user is gone.
|
||||
require.Equal(t, []string{}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// The settings should be reset.
|
||||
require.True(t, bridge.GetAutoUpdate())
|
||||
require.Equal(t, updater.StableChannel, bridge.GetUpdateChannel())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_ChangeCacheDirectoryFailsBetweenDifferentVolumes(t *testing.T) {
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skip("Test only necessary on windows")
|
||||
}
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Change directory
|
||||
err := bridge.SetGluonDir(ctx, "XX:\\")
|
||||
require.Error(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
newCacheDir := t.TempDir()
|
||||
currentCacheDir := bridge.GetGluonDir()
|
||||
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The user is now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// Change directory
|
||||
err = bridge.SetGluonDir(ctx, newCacheDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.ReadDir(currentCacheDir)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
|
||||
require.Equal(t, newCacheDir, bridge.GetGluonDir())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// withEnv creates the full test environment and runs the tests.
|
||||
func withEnv(t *testing.T, tests func(context.Context, *server.Server, *proton.NetCtl, bridge.Locator, []byte), opts ...server.Option) {
|
||||
server := server.New(opts...)
|
||||
defer server.Close()
|
||||
|
||||
// Add test user.
|
||||
_, _, err := server.CreateUser(username, username+"@pm.me", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate a random vault key.
|
||||
vaultKey, err := crypto.RandomToken(32)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a context used for the test.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Create a net controller so we can simulate network connectivity issues.
|
||||
netCtl := proton.NewNetCtl()
|
||||
|
||||
// Create a locations object to provide temporary locations for bridge data during the test.
|
||||
locations := locations.New(bridge.NewTestLocationsProvider(t.TempDir()), "config-name")
|
||||
|
||||
// Run the tests.
|
||||
tests(ctx, server, netCtl, locations, vaultKey)
|
||||
}
|
||||
|
||||
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
|
||||
func withBridge(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
apiURL string,
|
||||
netCtl *proton.NetCtl,
|
||||
locator bridge.Locator,
|
||||
vaultKey []byte,
|
||||
tests func(*bridge.Bridge, *bridge.Mocks),
|
||||
) {
|
||||
// Create the mock objects used in the tests.
|
||||
mocks := bridge.NewMocks(t, v2_3_0, v2_3_0)
|
||||
defer mocks.Close()
|
||||
|
||||
// Bridge will enable the proxy by default at startup.
|
||||
mocks.ProxyCtl.EXPECT().AllowProxy()
|
||||
|
||||
// Get the path to the vault.
|
||||
vaultDir, err := locator.ProvideSettingsPath()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create the vault.
|
||||
vault, _, err := vault.New(vaultDir, t.TempDir(), vaultKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a new cookie jar.
|
||||
cookieJar, err := cookies.NewCookieJar(bridge.NewTestCookieJar(), vault)
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, cookieJar.PersistCookies()) }()
|
||||
|
||||
// Create a new bridge.
|
||||
bridge, eventCh, err := bridge.New(
|
||||
// The app stuff.
|
||||
locator,
|
||||
vault,
|
||||
mocks.Autostarter,
|
||||
mocks.Updater,
|
||||
v2_3_0,
|
||||
|
||||
// The API stuff.
|
||||
apiURL,
|
||||
cookieJar,
|
||||
useragent.New(),
|
||||
mocks.TLSReporter,
|
||||
proton.NewDialer(netCtl, &tls.Config{InsecureSkipVerify: true}).GetRoundTripper(),
|
||||
mocks.ProxyCtl,
|
||||
mocks.CrashHandler,
|
||||
mocks.Reporter,
|
||||
|
||||
// The logging stuff.
|
||||
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
|
||||
os.Getenv("BRIDGE_LOG_IMAP_SERVER") == "1",
|
||||
os.Getenv("BRIDGE_LOG_SMTP") == "1",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, bridge.GetErrors())
|
||||
|
||||
// Wait for bridge to finish loading users.
|
||||
waitForEvent(t, eventCh, events.AllUsersLoaded{})
|
||||
|
||||
// Set random IMAP and SMTP ports for the tests.
|
||||
require.NoError(t, bridge.SetIMAPPort(0))
|
||||
require.NoError(t, bridge.SetSMTPPort(0))
|
||||
|
||||
// Close the bridge when done.
|
||||
defer bridge.Close(ctx)
|
||||
|
||||
// Use the bridge.
|
||||
tests(bridge, mocks)
|
||||
}
|
||||
|
||||
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, wantEvent T) {
|
||||
t.Helper()
|
||||
|
||||
for event := range eventCh {
|
||||
switch event.(type) { // nolint:gocritic
|
||||
case T:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// must is a helper function that panics on error.
|
||||
func must[T any](val T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func getConnectedUserIDs(t *testing.T, b *bridge.Bridge) []string {
|
||||
t.Helper()
|
||||
|
||||
return xslices.Filter(b.GetUserIDs(), func(userID string) bool {
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
|
||||
return info.State == bridge.Connected
|
||||
})
|
||||
}
|
||||
|
||||
func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
|
||||
outCh := make(chan Out)
|
||||
|
||||
go func() {
|
||||
defer close(outCh)
|
||||
|
||||
for in := range inCh {
|
||||
out, ok := any(in).(Out)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unexpected type %T", in))
|
||||
}
|
||||
|
||||
outCh <- out
|
||||
}
|
||||
}()
|
||||
|
||||
return outCh, done
|
||||
}
|
||||
@ -21,92 +21,121 @@ import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxAttachmentSize = 7 * 1024 * 1024 // 7 MB total limit
|
||||
MaxTotalAttachmentSize = 7 * (1 << 20)
|
||||
MaxCompressedFilesCount = 6
|
||||
)
|
||||
|
||||
var ErrSizeTooLarge = errors.New("file is too big")
|
||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error { //nolint:funlen
|
||||
var account string
|
||||
|
||||
// ReportBug reports a new bug from the user.
|
||||
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string, attachLogs bool) error {
|
||||
if user, err := b.GetUser(address); err == nil {
|
||||
accountName = user.Username()
|
||||
} else if users := b.GetUsers(); len(users) > 0 {
|
||||
accountName = users[0].Username()
|
||||
if info, err := bridge.QueryUserInfo(username); err == nil {
|
||||
account = info.Username
|
||||
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
|
||||
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
|
||||
account = user.Username()
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
report := pmapi.ReportBugReq{
|
||||
OS: osType,
|
||||
OSVersion: osVersion,
|
||||
Browser: emailClient,
|
||||
Title: "[Bridge] Bug",
|
||||
Description: description,
|
||||
Username: accountName,
|
||||
Email: address,
|
||||
}
|
||||
var atts []proton.ReportBugAttachment
|
||||
|
||||
if attachLogs {
|
||||
logs, err := b.getMatchingLogs(
|
||||
func(filename string) bool {
|
||||
return logging.MatchLogName(filename) && !logging.MatchStackTraceName(filename)
|
||||
},
|
||||
)
|
||||
logs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
||||
return logging.MatchLogName(filename) && !logging.MatchStackTraceName(filename)
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Can't get log files list")
|
||||
return err
|
||||
}
|
||||
crashes, err := b.getMatchingLogs(
|
||||
func(filename string) bool {
|
||||
return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename)
|
||||
},
|
||||
)
|
||||
|
||||
crashes, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
||||
return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename)
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Can't get crash files list")
|
||||
return err
|
||||
}
|
||||
|
||||
guiLogs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
||||
return logging.MatchGUILogName(filename) && !logging.MatchStackTraceName(filename)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var matchFiles []string
|
||||
|
||||
// Include bridge logs, up to a maximum amount.
|
||||
matchFiles = append(matchFiles, logs[max(0, len(logs)-(MaxCompressedFilesCount/2)):]...)
|
||||
|
||||
// Include crash logs, up to a maximum amount.
|
||||
matchFiles = append(matchFiles, crashes[max(0, len(crashes)-(MaxCompressedFilesCount/2)):]...)
|
||||
|
||||
// bridge-gui keeps just one small (~ 1kb) log file; we always include it.
|
||||
if len(guiLogs) > 0 {
|
||||
matchFiles = append(matchFiles, guiLogs[len(guiLogs)-1])
|
||||
}
|
||||
|
||||
archive, err := zipFiles(matchFiles)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Can't zip logs and crashes")
|
||||
return err
|
||||
}
|
||||
|
||||
if archive != nil {
|
||||
report.AddAttachment("logs.zip", "application/zip", archive)
|
||||
body, err := io.ReadAll(archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
atts = append(atts, proton.ReportBugAttachment{
|
||||
Name: "logs.zip",
|
||||
Filename: "logs.zip",
|
||||
MIMEType: "application/zip",
|
||||
Body: body,
|
||||
})
|
||||
}
|
||||
|
||||
return b.clientManager.ReportBug(context.Background(), report)
|
||||
return bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
||||
OS: osType,
|
||||
OSVersion: osVersion,
|
||||
|
||||
Title: "[Bridge] Bug",
|
||||
Description: description,
|
||||
|
||||
Client: client,
|
||||
ClientType: proton.ClientTypeEmail,
|
||||
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
|
||||
|
||||
Username: account,
|
||||
Email: email,
|
||||
}, atts...)
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Bridge) getMatchingLogs(filenameMatchFunc func(string) bool) (filenames []string, err error) {
|
||||
logsPath, err := b.locations.ProvideLogsPath()
|
||||
func getMatchingLogs(locator Locator, filenameMatchFunc func(string) bool) (filenames []string, err error) {
|
||||
logsPath, err := locator.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(logsPath)
|
||||
files, err := os.ReadDir(logsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -118,24 +147,25 @@ func (b *Bridge) getMatchingLogs(filenameMatchFunc func(string) bool) (filenames
|
||||
matchFiles = append(matchFiles, filepath.Join(logsPath, file.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(matchFiles) // Sorted by timestamp: oldest first.
|
||||
|
||||
return matchFiles, nil
|
||||
}
|
||||
|
||||
type LimitedBuffer struct {
|
||||
type limitedBuffer struct {
|
||||
capacity int
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
func NewLimitedBuffer(capacity int) *LimitedBuffer {
|
||||
return &LimitedBuffer{
|
||||
func newLimitedBuffer(capacity int) *limitedBuffer {
|
||||
return &limitedBuffer{
|
||||
capacity: capacity,
|
||||
buf: bytes.NewBuffer(make([]byte, 0, capacity)),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LimitedBuffer) Write(p []byte) (n int, err error) {
|
||||
func (b *limitedBuffer) Write(p []byte) (n int, err error) {
|
||||
if len(p)+b.buf.Len() > b.capacity {
|
||||
return 0, ErrSizeTooLarge
|
||||
}
|
||||
@ -143,7 +173,7 @@ func (b *LimitedBuffer) Write(p []byte) (n int, err error) {
|
||||
return b.buf.Write(p)
|
||||
}
|
||||
|
||||
func (b *LimitedBuffer) Read(p []byte) (n int, err error) {
|
||||
func (b *limitedBuffer) Read(p []byte) (n int, err error) {
|
||||
return b.buf.Read(p)
|
||||
}
|
||||
|
||||
@ -152,14 +182,13 @@ func zipFiles(filenames []string) (io.Reader, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
buf := NewLimitedBuffer(MaxAttachmentSize)
|
||||
buf := newLimitedBuffer(MaxTotalAttachmentSize)
|
||||
|
||||
w := zip.NewWriter(buf)
|
||||
defer w.Close() //nolint:errcheck
|
||||
|
||||
for _, file := range filenames {
|
||||
err := addFileToZip(file, w)
|
||||
if err != nil {
|
||||
if err := addFileToZip(file, w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@ -196,12 +225,9 @@ func addFileToZip(filename string, writer *zip.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(fileWriter, fileReader)
|
||||
if err != nil {
|
||||
if _, err := io.Copy(fileWriter, fileReader); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = fileReader.Close()
|
||||
|
||||
return err
|
||||
return fileReader.Close()
|
||||
}
|
||||
|
||||
75
internal/bridge/configure.go
Normal file
75
internal/bridge/configure.go
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/clientconfig"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ConfigureAppleMail configures apple mail for the given userID and address.
|
||||
// If configuring apple mail for Catalina or newer, it ensures Bridge is using SSL.
|
||||
func (bridge *Bridge) ConfigureAppleMail(userID, address string) error {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"userID": userID,
|
||||
"address": logging.Sensitive(address),
|
||||
}).Info("Configuring Apple Mail")
|
||||
|
||||
return safe.RLockRet(func() error {
|
||||
user, ok := bridge.users[userID]
|
||||
if !ok {
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
if address == "" {
|
||||
address = user.Emails()[0]
|
||||
}
|
||||
|
||||
username := address
|
||||
addresses := address
|
||||
|
||||
if user.GetAddressMode() == vault.CombinedMode {
|
||||
username = user.Emails()[0]
|
||||
addresses = strings.Join(user.Emails(), ",")
|
||||
}
|
||||
|
||||
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
|
||||
if err := bridge.SetSMTPSSL(true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return (&clientconfig.AppleMail{}).Configure(
|
||||
constants.Host,
|
||||
bridge.vault.GetIMAPPort(),
|
||||
bridge.vault.GetSMTPPort(),
|
||||
bridge.vault.GetIMAPSSL(),
|
||||
bridge.vault.GetSMTPSSL(),
|
||||
username,
|
||||
addresses,
|
||||
user.BridgePass(),
|
||||
)
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
@ -13,26 +13,24 @@
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package bridge provides core functionality of Bridge app.
|
||||
package bridge
|
||||
|
||||
import "github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
import "errors"
|
||||
|
||||
// IsAutostartEnabled checks if link file exits.
|
||||
func (b *Bridge) IsAutostartEnabled() bool {
|
||||
return b.autostart.IsEnabled()
|
||||
}
|
||||
var (
|
||||
ErrVaultInsecure = errors.New("the vault is insecure")
|
||||
ErrVaultCorrupt = errors.New("the vault is corrupt")
|
||||
|
||||
// EnableAutostart creates link and sets the preferences.
|
||||
func (b *Bridge) EnableAutostart() error {
|
||||
b.settings.SetBool(settings.AutostartKey, true)
|
||||
return b.autostart.Enable()
|
||||
}
|
||||
ErrServeIMAP = errors.New("failed to serve IMAP")
|
||||
ErrServeSMTP = errors.New("failed to serve SMTP")
|
||||
ErrWatchUpdates = errors.New("failed to watch for updates")
|
||||
|
||||
// DisableAutostart removes link and sets the preferences.
|
||||
func (b *Bridge) DisableAutostart() error {
|
||||
b.settings.SetBool(settings.AutostartKey, false)
|
||||
return b.autostart.Disable()
|
||||
}
|
||||
ErrNoSuchUser = errors.New("no such user")
|
||||
ErrUserAlreadyExists = errors.New("user already exists")
|
||||
ErrUserAlreadyLoggedIn = errors.New("the user is already logged in")
|
||||
ErrNotImplemented = errors.New("not implemented")
|
||||
|
||||
ErrSizeTooLarge = errors.New("file is too big")
|
||||
)
|
||||
64
internal/bridge/files.go
Normal file
64
internal/bridge/files.go
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func moveDir(from, to string) error {
|
||||
entries, err := os.ReadDir(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
if err := os.Mkdir(filepath.Join(to, entry.Name()), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := moveDir(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(filepath.Join(from, entry.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := moveFile(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return os.Remove(from)
|
||||
}
|
||||
|
||||
func moveFile(from, to string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(to), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Rename(from, to); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
73
internal/bridge/files_test.go
Normal file
73
internal/bridge/files_test.go
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMoveDir(t *testing.T) {
|
||||
from, to := t.TempDir(), t.TempDir()
|
||||
|
||||
// Create some files in from.
|
||||
if err := os.WriteFile(filepath.Join(from, "a"), []byte("a"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(from, "b"), []byte("b"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(from, "c"), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(from, "c", "d"), []byte("d"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Move the files.
|
||||
if err := moveDir(from, to); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the files were moved.
|
||||
if _, err := os.Stat(filepath.Join(from, "a")); !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(to, "a")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(from, "b")); !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(to, "b")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(from, "c")); !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(to, "c")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(from, "c", "d")); !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(to, "c", "d")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@ -13,15 +13,14 @@
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package message
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/go-rfc5322"
|
||||
pmmime "github.com/ProtonMail/proton-bridge/v2/pkg/mime"
|
||||
)
|
||||
|
||||
func init() { //nolint:gochecknoinits
|
||||
rfc5322.CharsetReader = pmmime.CharsetReader
|
||||
func (bridge *Bridge) GetCurrentUserAgent() string {
|
||||
return bridge.identifier.GetUserAgent()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetCurrentPlatform(platform string) {
|
||||
bridge.identifier.SetPlatform(platform)
|
||||
}
|
||||
331
internal/bridge/imap.go
Normal file
331
internal/bridge/imap.go
Normal file
@ -0,0 +1,331 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon"
|
||||
imapEvents "github.com/ProtonMail/gluon/events"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/gluon/store"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultClientName = "UnknownClient"
|
||||
defaultClientVersion = "0.0.1"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) serveIMAP() error {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
logrus.Info("Starting IMAP server")
|
||||
|
||||
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create IMAP listener: %w", err)
|
||||
}
|
||||
|
||||
bridge.imapListener = imapListener
|
||||
|
||||
if err := bridge.imapServer.Serve(context.Background(), bridge.imapListener); err != nil {
|
||||
return fmt.Errorf("failed to serve IMAP: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
|
||||
return fmt.Errorf("failed to set IMAP port: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) restartIMAP() error {
|
||||
logrus.Info("Restarting IMAP server")
|
||||
|
||||
if bridge.imapListener != nil {
|
||||
if err := bridge.imapListener.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP listener: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return bridge.serveIMAP()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) closeIMAP(ctx context.Context) error {
|
||||
logrus.Info("Closing IMAP server")
|
||||
|
||||
if bridge.imapServer != nil {
|
||||
if err := bridge.imapServer.Close(ctx); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP server: %w", err)
|
||||
}
|
||||
bridge.imapServer = nil
|
||||
}
|
||||
|
||||
if bridge.imapListener != nil {
|
||||
if err := bridge.imapListener.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP listener: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addIMAPUser connects the given user to gluon.
|
||||
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
imapConn, err := user.NewIMAPConnectors()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create IMAP connectors: %w", err)
|
||||
}
|
||||
|
||||
for addrID, imapConn := range imapConn {
|
||||
log := logrus.WithFields(logrus.Fields{
|
||||
"userID": user.ID(),
|
||||
"addrID": addrID,
|
||||
})
|
||||
|
||||
if gluonID, ok := user.GetGluonID(addrID); ok {
|
||||
log.WithField("gluonID", gluonID).Info("Loading existing IMAP user")
|
||||
|
||||
if err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
|
||||
return fmt.Errorf("failed to load IMAP user: %w", err)
|
||||
}
|
||||
} else {
|
||||
log.Info("Creating new IMAP user")
|
||||
|
||||
gluonID, err := bridge.imapServer.AddUser(ctx, imapConn, user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||
}
|
||||
|
||||
if err := user.SetGluonID(addrID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to set IMAP user ID: %w", err)
|
||||
}
|
||||
|
||||
log.WithField("gluonID", gluonID).Info("Created new IMAP user")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeIMAPUser disconnects the given user from gluon, optionally also removing its files.
|
||||
func (bridge *Bridge) removeIMAPUser(ctx context.Context, user *user.User, withData bool) error {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"userID": user.ID(),
|
||||
"withData": withData,
|
||||
}).Debug("Removing IMAP user")
|
||||
|
||||
for addrID, gluonID := range user.GetGluonIDs() {
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, withData); err != nil {
|
||||
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
||||
}
|
||||
|
||||
if withData {
|
||||
if err := user.RemoveGluonID(addrID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove IMAP user ID: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
||||
switch event := event.(type) {
|
||||
case imapEvents.UserAdded:
|
||||
for labelID, count := range event.Counts {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"gluonID": event.UserID,
|
||||
"labelID": labelID,
|
||||
"count": count,
|
||||
}).Info("Received mailbox message count")
|
||||
}
|
||||
|
||||
case imapEvents.SessionAdded:
|
||||
if !bridge.identifier.HasClient() {
|
||||
bridge.identifier.SetClient(defaultClientName, defaultClientVersion)
|
||||
}
|
||||
|
||||
case imapEvents.IMAPID:
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"sessionID": event.SessionID,
|
||||
"name": event.IMAPID.Name,
|
||||
"version": event.IMAPID.Version,
|
||||
}).Info("Received IMAP ID")
|
||||
|
||||
if event.IMAPID.Name != "" && event.IMAPID.Version != "" {
|
||||
bridge.identifier.SetClient(event.IMAPID.Name, event.IMAPID.Version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getGluonDir(encVault *vault.Vault) (string, error) {
|
||||
empty, exists, err := isEmpty(encVault.GetGluonDir())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to check if gluon dir is empty: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
if err := os.MkdirAll(encVault.GetGluonDir(), 0o700); err != nil {
|
||||
return "", fmt.Errorf("failed to create gluon dir: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if empty {
|
||||
if err := encVault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
|
||||
return user.ClearSyncStatus()
|
||||
}); err != nil {
|
||||
return "", fmt.Errorf("failed to reset user sync status: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return encVault.GetGluonDir(), nil
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func newIMAPServer(
|
||||
gluonDir string,
|
||||
version *semver.Version,
|
||||
tlsConfig *tls.Config,
|
||||
reporter reporter.Reporter,
|
||||
logClient, logServer bool,
|
||||
eventCh chan<- imapEvents.Event,
|
||||
tasks *async.Group,
|
||||
) (*gluon.Server, error) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"gluonDir": gluonDir,
|
||||
"version": version,
|
||||
"logClient": logClient,
|
||||
"logServer": logServer,
|
||||
}).Info("Creating IMAP server")
|
||||
|
||||
if logClient || logServer {
|
||||
log := logrus.WithField("protocol", "IMAP")
|
||||
log.Warning("================================================")
|
||||
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
||||
log.Warning("================================================")
|
||||
}
|
||||
|
||||
var imapClientLog io.Writer
|
||||
|
||||
if logClient {
|
||||
imapClientLog = logging.NewIMAPLogger()
|
||||
} else {
|
||||
imapClientLog = io.Discard
|
||||
}
|
||||
|
||||
var imapServerLog io.Writer
|
||||
|
||||
if logServer {
|
||||
imapServerLog = logging.NewIMAPLogger()
|
||||
} else {
|
||||
imapServerLog = io.Discard
|
||||
}
|
||||
|
||||
imapServer, err := gluon.New(
|
||||
gluon.WithTLS(tlsConfig),
|
||||
gluon.WithDataDir(gluonDir),
|
||||
gluon.WithStoreBuilder(new(storeBuilder)),
|
||||
gluon.WithLogger(imapClientLog, imapServerLog),
|
||||
getGluonVersionInfo(version),
|
||||
gluon.WithReporter(reporter),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tasks.Once(func(ctx context.Context) {
|
||||
async.ForwardContext(ctx, eventCh, imapServer.AddWatcher())
|
||||
})
|
||||
|
||||
tasks.Once(func(ctx context.Context) {
|
||||
async.RangeContext(ctx, imapServer.GetErrorCh(), func(err error) {
|
||||
logrus.WithError(err).Error("IMAP server error")
|
||||
})
|
||||
})
|
||||
|
||||
return imapServer, nil
|
||||
}
|
||||
|
||||
func getGluonVersionInfo(version *semver.Version) gluon.Option {
|
||||
return gluon.WithVersionInfo(
|
||||
int(version.Major()),
|
||||
int(version.Minor()),
|
||||
int(version.Patch()),
|
||||
constants.FullAppName,
|
||||
"TODO",
|
||||
"TODO",
|
||||
)
|
||||
}
|
||||
|
||||
// isEmpty returns whether the given directory is empty.
|
||||
// If the directory does not exist, the second return value is false.
|
||||
func isEmpty(dir string) (bool, bool, error) {
|
||||
if _, err := os.Stat(dir); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return false, false, fmt.Errorf("failed to stat %s: %w", dir, err)
|
||||
}
|
||||
|
||||
return true, false, nil
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return false, false, fmt.Errorf("failed to read dir %s: %w", dir, err)
|
||||
}
|
||||
|
||||
return len(entries) == 0, true, nil
|
||||
}
|
||||
|
||||
type storeBuilder struct{}
|
||||
|
||||
func (*storeBuilder) New(path, userID string, passphrase []byte) (store.Store, error) {
|
||||
return store.NewOnDiskStore(
|
||||
filepath.Join(path, userID),
|
||||
passphrase,
|
||||
store.WithCompressor(new(store.GZipCompressor)),
|
||||
)
|
||||
}
|
||||
|
||||
func (*storeBuilder) Delete(path, userID string) error {
|
||||
return os.RemoveAll(filepath.Join(path, userID))
|
||||
}
|
||||
@ -13,20 +13,18 @@
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package store
|
||||
package bridge
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// itob returns a 4-byte big endian representation of v.
|
||||
func itob(v uint32) []byte {
|
||||
b := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(b, v)
|
||||
return b
|
||||
func (bridge *Bridge) GetLogsPath() (string, error) {
|
||||
return bridge.locator.ProvideLogsPath()
|
||||
}
|
||||
|
||||
// btoi returns the uint32 represented by b.
|
||||
func btoi(b []byte) uint32 {
|
||||
return binary.BigEndian.Uint32(b)
|
||||
func (bridge *Bridge) GetLicenseFilePath() string {
|
||||
return bridge.locator.GetLicenseFilePath()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetDependencyLicensesLink() string {
|
||||
return bridge.locator.GetDependencyLicensesLink()
|
||||
}
|
||||
@ -13,22 +13,24 @@
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package store
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func init() { //nolint:gochecknoinits
|
||||
logrus.SetLevel(logrus.ErrorLevel)
|
||||
switch os.Getenv("VERBOSITY") {
|
||||
case "trace":
|
||||
logrus.SetLevel(logrus.TraceLevel)
|
||||
case "debug":
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
func TestMain(m *testing.M) {
|
||||
if level := os.Getenv("BRIDGE_LOG_LEVEL"); level != "" {
|
||||
if parsed, err := logrus.ParseLevel(level); err == nil {
|
||||
logrus.SetLevel(parsed)
|
||||
}
|
||||
}
|
||||
|
||||
goleak.VerifyTestMain(m, goleak.IgnoreCurrent())
|
||||
}
|
||||
151
internal/bridge/mocks.go
Normal file
151
internal/bridge/mocks.go
Normal file
@ -0,0 +1,151 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
type Mocks struct {
|
||||
ProxyCtl *mocks.MockProxyController
|
||||
TLSReporter *mocks.MockTLSReporter
|
||||
TLSIssueCh chan struct{}
|
||||
|
||||
Updater *TestUpdater
|
||||
Autostarter *mocks.MockAutostarter
|
||||
|
||||
CrashHandler *mocks.MockPanicHandler
|
||||
Reporter *mocks.MockReporter
|
||||
}
|
||||
|
||||
func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
||||
ctl := gomock.NewController(tb)
|
||||
|
||||
mocks := &Mocks{
|
||||
ProxyCtl: mocks.NewMockProxyController(ctl),
|
||||
TLSReporter: mocks.NewMockTLSReporter(ctl),
|
||||
TLSIssueCh: make(chan struct{}),
|
||||
|
||||
Updater: NewTestUpdater(version, minAuto),
|
||||
Autostarter: mocks.NewMockAutostarter(ctl),
|
||||
|
||||
CrashHandler: mocks.NewMockPanicHandler(ctl),
|
||||
Reporter: mocks.NewMockReporter(ctl),
|
||||
}
|
||||
|
||||
// When getting the TLS issue channel, we want to return the test channel.
|
||||
mocks.TLSReporter.EXPECT().GetTLSIssueCh().Return(mocks.TLSIssueCh).AnyTimes()
|
||||
|
||||
// This is called at he end of any go-routine:
|
||||
mocks.CrashHandler.EXPECT().HandlePanic().AnyTimes()
|
||||
|
||||
return mocks
|
||||
}
|
||||
|
||||
func (mocks *Mocks) Close() {
|
||||
close(mocks.TLSIssueCh)
|
||||
}
|
||||
|
||||
type TestCookieJar struct {
|
||||
cookies map[string][]*http.Cookie
|
||||
}
|
||||
|
||||
func NewTestCookieJar() *TestCookieJar {
|
||||
return &TestCookieJar{
|
||||
cookies: make(map[string][]*http.Cookie),
|
||||
}
|
||||
}
|
||||
|
||||
func (j *TestCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
|
||||
j.cookies[u.Host] = cookies
|
||||
}
|
||||
|
||||
func (j *TestCookieJar) Cookies(u *url.URL) []*http.Cookie {
|
||||
return j.cookies[u.Host]
|
||||
}
|
||||
|
||||
type TestLocationsProvider struct {
|
||||
config, data, cache string
|
||||
}
|
||||
|
||||
func NewTestLocationsProvider(dir string) *TestLocationsProvider {
|
||||
config, err := os.MkdirTemp(dir, "config")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
data, err := os.MkdirTemp(dir, "data")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cache, err := os.MkdirTemp(dir, "cache")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &TestLocationsProvider{
|
||||
config: config,
|
||||
data: data,
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *TestLocationsProvider) UserConfig() string {
|
||||
return provider.config
|
||||
}
|
||||
|
||||
func (provider *TestLocationsProvider) UserData() string {
|
||||
return provider.data
|
||||
}
|
||||
|
||||
func (provider *TestLocationsProvider) UserCache() string {
|
||||
return provider.cache
|
||||
}
|
||||
|
||||
type TestUpdater struct {
|
||||
latest updater.VersionInfo
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewTestUpdater(version, minAuto *semver.Version) *TestUpdater {
|
||||
return &TestUpdater{
|
||||
latest: updater.VersionInfo{
|
||||
Version: version,
|
||||
MinAuto: minAuto,
|
||||
|
||||
RolloutProportion: 1.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (testUpdater *TestUpdater) SetLatestVersion(version, minAuto *semver.Version) {
|
||||
testUpdater.lock.Lock()
|
||||
defer testUpdater.lock.Unlock()
|
||||
|
||||
testUpdater.latest = updater.VersionInfo{
|
||||
Version: version,
|
||||
MinAuto: minAuto,
|
||||
|
||||
RolloutProportion: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
func (testUpdater *TestUpdater) GetVersionInfo(ctx context.Context, downloader updater.Downloader, channel updater.Channel) (updater.VersionInfo, error) {
|
||||
testUpdater.lock.RLock()
|
||||
defer testUpdater.lock.RUnlock()
|
||||
|
||||
return testUpdater.latest, nil
|
||||
}
|
||||
|
||||
func (testUpdater *TestUpdater) InstallUpdate(ctx context.Context, downloader updater.Downloader, update updater.VersionInfo) error {
|
||||
return nil
|
||||
}
|
||||
46
internal/bridge/mocks/async_mocks.go
Normal file
46
internal/bridge/mocks/async_mocks.go
Normal file
@ -0,0 +1,46 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/ProtonMail/proton-bridge/v3/internal/async (interfaces: PanicHandler)
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockPanicHandler is a mock of PanicHandler interface.
|
||||
type MockPanicHandler struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockPanicHandlerMockRecorder
|
||||
}
|
||||
|
||||
// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler.
|
||||
type MockPanicHandlerMockRecorder struct {
|
||||
mock *MockPanicHandler
|
||||
}
|
||||
|
||||
// NewMockPanicHandler creates a new mock instance.
|
||||
func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler {
|
||||
mock := &MockPanicHandler{ctrl: ctrl}
|
||||
mock.recorder = &MockPanicHandlerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// HandlePanic mocks base method.
|
||||
func (m *MockPanicHandler) HandlePanic() {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "HandlePanic")
|
||||
}
|
||||
|
||||
// HandlePanic indicates an expected call of HandlePanic.
|
||||
func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic))
|
||||
}
|
||||
90
internal/bridge/mocks/gluon_mocks.go
Normal file
90
internal/bridge/mocks/gluon_mocks.go
Normal file
@ -0,0 +1,90 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/ProtonMail/gluon/reporter (interfaces: Reporter)
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockReporter is a mock of Reporter interface.
|
||||
type MockReporter struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockReporterMockRecorder
|
||||
}
|
||||
|
||||
// MockReporterMockRecorder is the mock recorder for MockReporter.
|
||||
type MockReporterMockRecorder struct {
|
||||
mock *MockReporter
|
||||
}
|
||||
|
||||
// NewMockReporter creates a new mock instance.
|
||||
func NewMockReporter(ctrl *gomock.Controller) *MockReporter {
|
||||
mock := &MockReporter{ctrl: ctrl}
|
||||
mock.recorder = &MockReporterMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockReporter) EXPECT() *MockReporterMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// ReportException mocks base method.
|
||||
func (m *MockReporter) ReportException(arg0 interface{}) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReportException", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ReportException indicates an expected call of ReportException.
|
||||
func (mr *MockReporterMockRecorder) ReportException(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportException", reflect.TypeOf((*MockReporter)(nil).ReportException), arg0)
|
||||
}
|
||||
|
||||
// ReportExceptionWithContext mocks base method.
|
||||
func (m *MockReporter) ReportExceptionWithContext(arg0 interface{}, arg1 map[string]interface{}) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReportExceptionWithContext", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ReportExceptionWithContext indicates an expected call of ReportExceptionWithContext.
|
||||
func (mr *MockReporterMockRecorder) ReportExceptionWithContext(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportExceptionWithContext", reflect.TypeOf((*MockReporter)(nil).ReportExceptionWithContext), arg0, arg1)
|
||||
}
|
||||
|
||||
// ReportMessage mocks base method.
|
||||
func (m *MockReporter) ReportMessage(arg0 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReportMessage", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ReportMessage indicates an expected call of ReportMessage.
|
||||
func (mr *MockReporterMockRecorder) ReportMessage(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportMessage", reflect.TypeOf((*MockReporter)(nil).ReportMessage), arg0)
|
||||
}
|
||||
|
||||
// ReportMessageWithContext mocks base method.
|
||||
func (m *MockReporter) ReportMessageWithContext(arg0 string, arg1 map[string]interface{}) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReportMessageWithContext", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ReportMessageWithContext indicates an expected call of ReportMessageWithContext.
|
||||
func (mr *MockReporterMockRecorder) ReportMessageWithContext(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportMessageWithContext", reflect.TypeOf((*MockReporter)(nil).ReportMessageWithContext), arg0, arg1)
|
||||
}
|
||||
108
internal/bridge/mocks/matcher.go
Normal file
108
internal/bridge/mocks/matcher.go
Normal file
@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
type refreshContextMatcher struct {
|
||||
wantRefresh proton.RefreshFlag
|
||||
}
|
||||
|
||||
func NewRefreshContextMatcher(refreshFlag proton.RefreshFlag) *refreshContextMatcher { //nolint:revive
|
||||
return &refreshContextMatcher{wantRefresh: refreshFlag}
|
||||
}
|
||||
|
||||
func (m *refreshContextMatcher) Matches(x interface{}) bool {
|
||||
context, ok := x.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
i, ok := context["EventLoop"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
el, ok := i.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
vID, ok := el["EventID"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
id, ok := vID.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
vRefresh, ok := el["Refresh"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
refresh, ok := vRefresh.(proton.RefreshFlag)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return refresh == m.wantRefresh
|
||||
}
|
||||
|
||||
func (m *refreshContextMatcher) String() string {
|
||||
return `map[string]interface which contains "Refresh" field with value proton.RefreshAll`
|
||||
}
|
||||
|
||||
type closedConnectionMatcher struct{}
|
||||
|
||||
func NewClosedConnectionMatcher() *closedConnectionMatcher { //nolint:revive
|
||||
return &closedConnectionMatcher{}
|
||||
}
|
||||
|
||||
func (m *closedConnectionMatcher) Matches(x interface{}) bool {
|
||||
context, ok := x.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
vErr, ok := context["error"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
err, ok := vErr.(error)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.Contains(err.Error(), "used of closed network connection")
|
||||
}
|
||||
|
||||
func (m *closedConnectionMatcher) String() string {
|
||||
return "map containing error of closed network connection"
|
||||
}
|
||||
146
internal/bridge/mocks/mocks.go
Normal file
146
internal/bridge/mocks/mocks.go
Normal file
@ -0,0 +1,146 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/ProtonMail/proton-bridge/v3/internal/bridge (interfaces: TLSReporter,ProxyController,Autostarter)
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockTLSReporter is a mock of TLSReporter interface.
|
||||
type MockTLSReporter struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockTLSReporterMockRecorder
|
||||
}
|
||||
|
||||
// MockTLSReporterMockRecorder is the mock recorder for MockTLSReporter.
|
||||
type MockTLSReporterMockRecorder struct {
|
||||
mock *MockTLSReporter
|
||||
}
|
||||
|
||||
// NewMockTLSReporter creates a new mock instance.
|
||||
func NewMockTLSReporter(ctrl *gomock.Controller) *MockTLSReporter {
|
||||
mock := &MockTLSReporter{ctrl: ctrl}
|
||||
mock.recorder = &MockTLSReporterMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockTLSReporter) EXPECT() *MockTLSReporterMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetTLSIssueCh mocks base method.
|
||||
func (m *MockTLSReporter) GetTLSIssueCh() <-chan struct{} {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetTLSIssueCh")
|
||||
ret0, _ := ret[0].(<-chan struct{})
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetTLSIssueCh indicates an expected call of GetTLSIssueCh.
|
||||
func (mr *MockTLSReporterMockRecorder) GetTLSIssueCh() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTLSIssueCh", reflect.TypeOf((*MockTLSReporter)(nil).GetTLSIssueCh))
|
||||
}
|
||||
|
||||
// MockProxyController is a mock of ProxyController interface.
|
||||
type MockProxyController struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockProxyControllerMockRecorder
|
||||
}
|
||||
|
||||
// MockProxyControllerMockRecorder is the mock recorder for MockProxyController.
|
||||
type MockProxyControllerMockRecorder struct {
|
||||
mock *MockProxyController
|
||||
}
|
||||
|
||||
// NewMockProxyController creates a new mock instance.
|
||||
func NewMockProxyController(ctrl *gomock.Controller) *MockProxyController {
|
||||
mock := &MockProxyController{ctrl: ctrl}
|
||||
mock.recorder = &MockProxyControllerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockProxyController) EXPECT() *MockProxyControllerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AllowProxy mocks base method.
|
||||
func (m *MockProxyController) AllowProxy() {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "AllowProxy")
|
||||
}
|
||||
|
||||
// AllowProxy indicates an expected call of AllowProxy.
|
||||
func (mr *MockProxyControllerMockRecorder) AllowProxy() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllowProxy", reflect.TypeOf((*MockProxyController)(nil).AllowProxy))
|
||||
}
|
||||
|
||||
// DisallowProxy mocks base method.
|
||||
func (m *MockProxyController) DisallowProxy() {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "DisallowProxy")
|
||||
}
|
||||
|
||||
// DisallowProxy indicates an expected call of DisallowProxy.
|
||||
func (mr *MockProxyControllerMockRecorder) DisallowProxy() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisallowProxy", reflect.TypeOf((*MockProxyController)(nil).DisallowProxy))
|
||||
}
|
||||
|
||||
// MockAutostarter is a mock of Autostarter interface.
|
||||
type MockAutostarter struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockAutostarterMockRecorder
|
||||
}
|
||||
|
||||
// MockAutostarterMockRecorder is the mock recorder for MockAutostarter.
|
||||
type MockAutostarterMockRecorder struct {
|
||||
mock *MockAutostarter
|
||||
}
|
||||
|
||||
// NewMockAutostarter creates a new mock instance.
|
||||
func NewMockAutostarter(ctrl *gomock.Controller) *MockAutostarter {
|
||||
mock := &MockAutostarter{ctrl: ctrl}
|
||||
mock.recorder = &MockAutostarterMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockAutostarter) EXPECT() *MockAutostarterMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Disable mocks base method.
|
||||
func (m *MockAutostarter) Disable() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Disable")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Disable indicates an expected call of Disable.
|
||||
func (mr *MockAutostarterMockRecorder) Disable() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Disable", reflect.TypeOf((*MockAutostarter)(nil).Disable))
|
||||
}
|
||||
|
||||
// Enable mocks base method.
|
||||
func (m *MockAutostarter) Enable() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Enable")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Enable indicates an expected call of Enable.
|
||||
func (mr *MockAutostarterMockRecorder) Enable() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enable", reflect.TypeOf((*MockAutostarter)(nil).Enable))
|
||||
}
|
||||
113
internal/bridge/refresh_test.go
Normal file
113
internal/bridge/refresh_test.go
Normal file
@ -0,0 +1,113 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/bradenaw/juniper/iterator"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBridge_Refresh(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
userID, _, err := s.CreateUser("imap", "imap@pm.me", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
names := iterator.Collect(iterator.Map(iterator.Counter(10), func(i int) string {
|
||||
return fmt.Sprintf("folder%v", i)
|
||||
}))
|
||||
|
||||
for _, name := range names {
|
||||
must(s.CreateLabel(userID, name, "", proton.LabelTypeFolder))
|
||||
}
|
||||
|
||||
// The initial user should be fully synced.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
})
|
||||
|
||||
// If we then connect an IMAP client, it should see all the labels with UID validity of 1.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login("imap@pm.me", string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
for _, name := range names {
|
||||
status, err := client.Select("Folders/"+name, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(1), status.UidValidity)
|
||||
}
|
||||
})
|
||||
|
||||
// Refresh the user; this will force a resync.
|
||||
require.NoError(t, s.RefreshUser(userID, proton.RefreshAll))
|
||||
|
||||
// If we then connect an IMAP client, it should see all the labels with UID validity of 1.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
})
|
||||
|
||||
// After resync, the IMAP client should see all the labels with UID validity of 2.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login("imap@pm.me", string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
for _, name := range names {
|
||||
status, err := client.Select("Folders/"+name, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(2), status.UidValidity)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
115
internal/bridge/send_test.go
Normal file
115
internal/bridge/send_test.go
Normal file
@ -0,0 +1,115 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBridge_Send(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
_, _, err := s.CreateUser("recipient", "recipient@pm.me", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
senderInfo, err := bridge.GetUserInfo(senderUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
// Dial the server.
|
||||
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||
require.NoError(t, err)
|
||||
defer client.Close() //nolint:errcheck
|
||||
|
||||
// Upgrade to TLS.
|
||||
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||
|
||||
if i%2 == 0 {
|
||||
// Authorize with SASL PLAIN.
|
||||
require.NoError(t, client.Auth(sasl.NewPlainClient(
|
||||
senderInfo.Addresses[0],
|
||||
senderInfo.Addresses[0],
|
||||
string(senderInfo.BridgePass)),
|
||||
))
|
||||
} else {
|
||||
// Authorize with SASL LOGIN.
|
||||
require.NoError(t, client.Auth(sasl.NewLoginClient(
|
||||
senderInfo.Addresses[0],
|
||||
string(senderInfo.BridgePass)),
|
||||
))
|
||||
}
|
||||
|
||||
// Send the message.
|
||||
require.NoError(t, client.SendMail(
|
||||
senderInfo.Addresses[0],
|
||||
[]string{recipientInfo.Addresses[0]},
|
||||
strings.NewReader(fmt.Sprintf("Subject: Test %v\r\n\r\nHello world!", i)),
|
||||
))
|
||||
}
|
||||
|
||||
// Connect the sender IMAP client.
|
||||
senderIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
|
||||
defer senderIMAPClient.Logout() //nolint:errcheck
|
||||
|
||||
// Connect the recipient IMAP client.
|
||||
recipientIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
|
||||
defer recipientIMAPClient.Logout() //nolint:errcheck
|
||||
|
||||
// Sender should have 10 messages in the sent folder.
|
||||
// Recipient should have 0 messages in inbox.
|
||||
require.Eventually(t, func() bool {
|
||||
sent, err := senderIMAPClient.Status(`Sent`, []imap.StatusItem{imap.StatusMessages})
|
||||
require.NoError(t, err)
|
||||
|
||||
inbox, err := recipientIMAPClient.Status(`Inbox`, []imap.StatusItem{imap.StatusMessages})
|
||||
require.NoError(t, err)
|
||||
|
||||
return sent.Messages == 10 && inbox.Messages == 10
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
})
|
||||
}
|
||||
78
internal/bridge/sentry_test.go
Normal file
78
internal/bridge/sentry_test.go
Normal file
@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gluon/liner"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBridge_Report(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
// Log in the user.
|
||||
userID, err := b.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait until the sync has finished.
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
|
||||
// Get the IMAP info.
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
// Dial the IMAP port.
|
||||
conn, err := net.Dial("tcp", fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, conn.Close()) }()
|
||||
|
||||
// Sending garbage to the IMAP port should cause the bridge to report it.
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(
|
||||
gomock.Eq("Failed to parse IMAP command"),
|
||||
gomock.Any(),
|
||||
).Return(nil)
|
||||
|
||||
// Read lines from the IMAP port.
|
||||
lineCh := liner.New(conn).Lines(func() error { return nil })
|
||||
|
||||
// On connection, we should get the greeting.
|
||||
require.Contains(t, string((<-lineCh).Line), "* OK")
|
||||
|
||||
// Send garbage data.
|
||||
must(conn.Write([]byte("tag garbage\r\n")))
|
||||
|
||||
// Bridge will reply with BAD.
|
||||
require.Contains(t, string((<-lineCh).Line), "tag BAD")
|
||||
})
|
||||
})
|
||||
}
|
||||
338
internal/bridge/settings.go
Normal file
338
internal/bridge/settings.go
Normal file
@ -0,0 +1,338 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) GetKeychainApp() (string, error) {
|
||||
vaultDir, err := bridge.locator.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return vault.GetHelper(vaultDir)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetKeychainApp(helper string) error {
|
||||
vaultDir, err := bridge.locator.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return vault.SetHelper(vaultDir, helper)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetIMAPPort() int {
|
||||
return bridge.vault.GetIMAPPort()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetIMAPPort(newPort int) error {
|
||||
if newPort == bridge.vault.GetIMAPPort() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetIMAPPort(newPort); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bridge.restartIMAP()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetIMAPSSL() bool {
|
||||
return bridge.vault.GetIMAPSSL()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetIMAPSSL(newSSL bool) error {
|
||||
if newSSL == bridge.vault.GetIMAPSSL() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetIMAPSSL(newSSL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bridge.restartIMAP()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetSMTPPort() int {
|
||||
return bridge.vault.GetSMTPPort()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetSMTPPort(newPort int) error {
|
||||
if newPort == bridge.vault.GetSMTPPort() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetSMTPPort(newPort); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bridge.restartSMTP()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetSMTPSSL() bool {
|
||||
return bridge.vault.GetSMTPSSL()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
|
||||
if newSSL == bridge.vault.GetSMTPSSL() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetSMTPSSL(newSSL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bridge.restartSMTP()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetGluonDir() string {
|
||||
return bridge.vault.GetGluonDir()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
|
||||
return safe.RLockRet(func() error {
|
||||
currentGluonDir := bridge.GetGluonDir()
|
||||
if newGluonDir == currentGluonDir {
|
||||
return fmt.Errorf("new gluon dir is the same as the old one")
|
||||
}
|
||||
|
||||
currentVolumeName := filepath.VolumeName(currentGluonDir)
|
||||
newVolumeName := filepath.VolumeName(newGluonDir)
|
||||
|
||||
if currentVolumeName != newVolumeName {
|
||||
return fmt.Errorf("it's currently not possible to move the cache between different volumes")
|
||||
}
|
||||
|
||||
if err := bridge.closeIMAP(context.Background()); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP: %w", err)
|
||||
}
|
||||
|
||||
if err := moveDir(bridge.GetGluonDir(), newGluonDir); err != nil {
|
||||
return fmt.Errorf("failed to move gluon dir: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetGluonDir(newGluonDir); err != nil {
|
||||
return fmt.Errorf("failed to set new gluon dir: %w", err)
|
||||
}
|
||||
|
||||
imapServer, err := newIMAPServer(
|
||||
bridge.vault.GetGluonDir(),
|
||||
bridge.curVersion,
|
||||
bridge.tlsConfig,
|
||||
bridge.reporter,
|
||||
bridge.logIMAPClient,
|
||||
bridge.logIMAPServer,
|
||||
bridge.imapEventCh,
|
||||
bridge.tasks,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create new IMAP server: %w", err)
|
||||
}
|
||||
|
||||
bridge.imapServer = imapServer
|
||||
|
||||
for _, user := range bridge.users {
|
||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to add users to new IMAP server: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := bridge.serveIMAP(); err != nil {
|
||||
return fmt.Errorf("failed to serve IMAP: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetProxyAllowed() bool {
|
||||
return bridge.vault.GetProxyAllowed()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetProxyAllowed(allowed bool) error {
|
||||
if allowed {
|
||||
bridge.proxyCtl.AllowProxy()
|
||||
} else {
|
||||
bridge.proxyCtl.DisallowProxy()
|
||||
}
|
||||
|
||||
return bridge.vault.SetProxyAllowed(allowed)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetShowAllMail() bool {
|
||||
return bridge.vault.GetShowAllMail()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetShowAllMail(show bool) error {
|
||||
return safe.RLockRet(func() error {
|
||||
for _, user := range bridge.users {
|
||||
user.SetShowAllMail(show)
|
||||
}
|
||||
|
||||
return bridge.vault.SetShowAllMail(show)
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetAutostart() bool {
|
||||
return bridge.vault.GetAutostart()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetAutostart(autostart bool) error {
|
||||
if err := bridge.vault.SetAutostart(autostart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
if autostart {
|
||||
err = bridge.autostarter.Enable()
|
||||
} else {
|
||||
err = bridge.autostarter.Disable()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetAutoUpdate() bool {
|
||||
return bridge.vault.GetAutoUpdate()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetAutoUpdate(autoUpdate bool) error {
|
||||
if bridge.vault.GetAutoUpdate() == autoUpdate {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetAutoUpdate(autoUpdate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.goUpdate()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetUpdateChannel() updater.Channel {
|
||||
return bridge.vault.GetUpdateChannel()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetUpdateChannel(channel updater.Channel) error {
|
||||
if bridge.vault.GetUpdateChannel() == channel {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetUpdateChannel(channel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.goUpdate()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetCurrentVersion() *semver.Version {
|
||||
return bridge.curVersion
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetLastVersion() *semver.Version {
|
||||
return bridge.vault.GetLastVersion()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetFirstStart() bool {
|
||||
return bridge.vault.GetFirstStart()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetFirstStart(firstStart bool) error {
|
||||
return bridge.vault.SetFirstStart(firstStart)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetFirstStartGUI() bool {
|
||||
return bridge.vault.GetFirstStartGUI()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetFirstStartGUI(firstStart bool) error {
|
||||
return bridge.vault.SetFirstStartGUI(firstStart)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetColorScheme() string {
|
||||
return bridge.vault.GetColorScheme()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetColorScheme(colorScheme string) error {
|
||||
return bridge.vault.SetColorScheme(colorScheme)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
||||
// Delete all the users.
|
||||
safe.Lock(func() {
|
||||
for _, user := range bridge.users {
|
||||
bridge.logoutUser(ctx, user, true, true)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
// Wipe the vault.
|
||||
gluonDir, err := bridge.locator.ProvideGluonPath()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to provide gluon dir")
|
||||
} else if err := bridge.vault.Reset(gluonDir); err != nil {
|
||||
logrus.WithError(err).Error("Failed to reset vault")
|
||||
}
|
||||
|
||||
// Then delete all files.
|
||||
if err := bridge.locator.Clear(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to clear data paths")
|
||||
}
|
||||
|
||||
// Lastly clear the keychain.
|
||||
vaultDir, err := bridge.locator.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get vault dir")
|
||||
} else if helper, err := vault.GetHelper(vaultDir); err != nil {
|
||||
logrus.WithError(err).Error("Failed to get keychain helper")
|
||||
} else if keychain, err := keychain.NewKeychain(helper, constants.KeyChainName); err != nil {
|
||||
logrus.WithError(err).Error("Failed to get keychain")
|
||||
} else if err := keychain.Clear(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to clear keychain")
|
||||
}
|
||||
}
|
||||
|
||||
func getPort(addr net.Addr) int {
|
||||
switch addr := addr.(type) {
|
||||
case *net.TCPAddr:
|
||||
return addr.Port
|
||||
|
||||
case *net.UDPAddr:
|
||||
return addr.Port
|
||||
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
178
internal/bridge/settings_test.go
Normal file
178
internal/bridge/settings_test.go
Normal file
@ -0,0 +1,178 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBridge_Settings_GluonDir(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Create a user.
|
||||
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a new location for the Gluon data.
|
||||
newGluonDir := t.TempDir()
|
||||
|
||||
// Move the gluon dir; it should also move the user's data.
|
||||
require.NoError(t, bridge.SetGluonDir(context.Background(), newGluonDir))
|
||||
|
||||
// Check that the new directory is not empty.
|
||||
entries, err := os.ReadDir(newGluonDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// There should be at least one entry.
|
||||
require.NotEmpty(t, entries)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Settings_IMAPPort(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
curPort := bridge.GetIMAPPort()
|
||||
|
||||
// Set the port to 1144.
|
||||
require.NoError(t, bridge.SetIMAPPort(1144))
|
||||
|
||||
// Get the new setting.
|
||||
require.Equal(t, 1144, bridge.GetIMAPPort())
|
||||
|
||||
// Assert that it has changed.
|
||||
require.NotEqual(t, curPort, bridge.GetIMAPPort())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Settings_IMAPSSL(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// By default, IMAP SSL is disabled.
|
||||
require.False(t, bridge.GetIMAPSSL())
|
||||
|
||||
// Enable IMAP SSL.
|
||||
require.NoError(t, bridge.SetIMAPSSL(true))
|
||||
|
||||
// Get the new setting.
|
||||
require.True(t, bridge.GetIMAPSSL())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Settings_SMTPPort(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
curPort := bridge.GetSMTPPort()
|
||||
|
||||
// Set the port to 1024.
|
||||
require.NoError(t, bridge.SetSMTPPort(1024))
|
||||
|
||||
// Get the new setting.
|
||||
require.Equal(t, 1024, bridge.GetSMTPPort())
|
||||
|
||||
// Assert that it has changed.
|
||||
require.NotEqual(t, curPort, bridge.GetSMTPPort())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Settings_SMTPSSL(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// By default, SMTP SSL is disabled.
|
||||
require.False(t, bridge.GetSMTPSSL())
|
||||
|
||||
// Enable SMTP SSL.
|
||||
require.NoError(t, bridge.SetSMTPSSL(true))
|
||||
|
||||
// Get the new setting.
|
||||
require.True(t, bridge.GetSMTPSSL())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Settings_Proxy(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// By default, proxy is allowed.
|
||||
require.True(t, bridge.GetProxyAllowed())
|
||||
|
||||
// Disallow proxy.
|
||||
mocks.ProxyCtl.EXPECT().DisallowProxy()
|
||||
require.NoError(t, bridge.SetProxyAllowed(false))
|
||||
|
||||
// Get the new setting.
|
||||
require.False(t, bridge.GetProxyAllowed())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Settings_Autostart(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// By default, autostart is disabled.
|
||||
require.False(t, bridge.GetAutostart())
|
||||
|
||||
// Enable autostart.
|
||||
mocks.Autostarter.EXPECT().Enable().Return(nil)
|
||||
require.NoError(t, bridge.SetAutostart(true))
|
||||
|
||||
// Get the new setting.
|
||||
require.True(t, bridge.GetAutostart())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Settings_FirstStart(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// By default, first start is true.
|
||||
require.True(t, bridge.GetFirstStart())
|
||||
|
||||
// Set first start to false.
|
||||
require.NoError(t, bridge.SetFirstStart(false))
|
||||
|
||||
// Get the new setting.
|
||||
require.False(t, bridge.GetFirstStart())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Settings_FirstStartGUI(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// By default, first start is true.
|
||||
require.True(t, bridge.GetFirstStartGUI())
|
||||
|
||||
// Set first start to false.
|
||||
require.NoError(t, bridge.SetFirstStartGUI(false))
|
||||
|
||||
// Get the new setting.
|
||||
require.False(t, bridge.GetFirstStartGUI())
|
||||
})
|
||||
})
|
||||
}
|
||||
116
internal/bridge/smtp.go
Normal file
116
internal/bridge/smtp.go
Normal file
@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) serveSMTP() error {
|
||||
logrus.Info("Starting SMTP server")
|
||||
|
||||
smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create SMTP listener: %w", err)
|
||||
}
|
||||
|
||||
bridge.smtpListener = smtpListener
|
||||
|
||||
bridge.tasks.Once(func(context.Context) {
|
||||
if err := bridge.smtpServer.Serve(smtpListener); err != nil {
|
||||
logrus.WithError(err).Info("SMTP server stopped")
|
||||
}
|
||||
})
|
||||
|
||||
if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil {
|
||||
return fmt.Errorf("failed to set IMAP port: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) restartSMTP() error {
|
||||
logrus.Info("Restarting SMTP server")
|
||||
|
||||
if err := bridge.closeSMTP(); err != nil {
|
||||
return fmt.Errorf("failed to close SMTP: %w", err)
|
||||
}
|
||||
|
||||
bridge.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
|
||||
|
||||
return bridge.serveSMTP()
|
||||
}
|
||||
|
||||
// We close the listener ourselves even though it's also closed by smtpServer.Close().
|
||||
// This is because smtpServer.Serve() is called in a separate goroutine and might be executed
|
||||
// after we've already closed the server. However, go-smtp has a bug; it blocks on the listener
|
||||
// even after the server has been closed. So we close the listener ourselves to unblock it.
|
||||
func (bridge *Bridge) closeSMTP() error {
|
||||
logrus.Info("Closing SMTP server")
|
||||
|
||||
if bridge.smtpListener != nil {
|
||||
if err := bridge.smtpListener.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close SMTP listener: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := bridge.smtpServer.Close(); err != nil {
|
||||
logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newSMTPServer(bridge *Bridge, tlsConfig *tls.Config, logSMTP bool) *smtp.Server {
|
||||
logrus.WithField("logSMTP", logSMTP).Info("Creating SMTP server")
|
||||
|
||||
smtpServer := smtp.NewServer(&smtpBackend{Bridge: bridge})
|
||||
|
||||
smtpServer.TLSConfig = tlsConfig
|
||||
smtpServer.Domain = constants.Host
|
||||
smtpServer.AllowInsecureAuth = true
|
||||
smtpServer.MaxLineLength = 1 << 16
|
||||
smtpServer.ErrorLog = logging.NewSMTPLogger()
|
||||
|
||||
// go-smtp suppors SASL PLAIN but not LOGIN. We need to add LOGIN support ourselves.
|
||||
smtpServer.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server {
|
||||
return sasl.NewLoginServer(func(username, password string) error {
|
||||
return conn.Session().AuthPlain(username, password)
|
||||
})
|
||||
})
|
||||
|
||||
if logSMTP {
|
||||
log := logrus.WithField("protocol", "SMTP")
|
||||
log.Warning("================================================")
|
||||
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
||||
log.Warning("================================================")
|
||||
|
||||
smtpServer.Debug = logging.NewSMTPDebugLogger()
|
||||
}
|
||||
|
||||
return smtpServer
|
||||
}
|
||||
96
internal/bridge/smtp_backend.go
Normal file
96
internal/bridge/smtp_backend.go
Normal file
@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/emersion/go-smtp"
|
||||
)
|
||||
|
||||
type smtpBackend struct {
|
||||
*Bridge
|
||||
}
|
||||
|
||||
type smtpSession struct {
|
||||
*Bridge
|
||||
|
||||
userID string
|
||||
authID string
|
||||
|
||||
from string
|
||||
to []string
|
||||
}
|
||||
|
||||
func (be *smtpBackend) NewSession(*smtp.Conn) (smtp.Session, error) {
|
||||
return &smtpSession{Bridge: be.Bridge}, nil
|
||||
}
|
||||
|
||||
func (s *smtpSession) AuthPlain(username, password string) error {
|
||||
return safe.RLockRet(func() error {
|
||||
for _, user := range s.users {
|
||||
addrID, err := user.CheckAuth(username, []byte(password))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
s.userID = user.ID()
|
||||
s.authID = addrID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("invalid username or password")
|
||||
}, s.usersLock)
|
||||
}
|
||||
|
||||
func (s *smtpSession) Reset() {
|
||||
s.from = ""
|
||||
s.to = nil
|
||||
}
|
||||
|
||||
func (s *smtpSession) Logout() error {
|
||||
s.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *smtpSession) Mail(from string, opts *smtp.MailOptions) error {
|
||||
s.from = from
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *smtpSession) Rcpt(to string) error {
|
||||
if len(to) > 0 {
|
||||
s.to = append(s.to, to)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *smtpSession) Data(r io.Reader) error {
|
||||
return safe.RLockRet(func() error {
|
||||
user, ok := s.users[s.userID]
|
||||
if !ok {
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
return user.SendMail(s.authID, s.from, s.to, r)
|
||||
}, s.usersLock)
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/store"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/store/cache"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/users"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
|
||||
)
|
||||
|
||||
type storeFactory struct {
|
||||
cacheProvider CacheProvider
|
||||
sentryReporter *sentry.Reporter
|
||||
panicHandler users.PanicHandler
|
||||
eventListener listener.Listener
|
||||
events *store.Events
|
||||
cache cache.Cache
|
||||
builder *message.Builder
|
||||
}
|
||||
|
||||
func newStoreFactory(
|
||||
cacheProvider CacheProvider,
|
||||
sentryReporter *sentry.Reporter,
|
||||
panicHandler users.PanicHandler,
|
||||
eventListener listener.Listener,
|
||||
cache cache.Cache,
|
||||
builder *message.Builder,
|
||||
) *storeFactory {
|
||||
return &storeFactory{
|
||||
cacheProvider: cacheProvider,
|
||||
sentryReporter: sentryReporter,
|
||||
panicHandler: panicHandler,
|
||||
eventListener: eventListener,
|
||||
events: store.NewEvents(cacheProvider.GetIMAPCachePath()),
|
||||
cache: cache,
|
||||
builder: builder,
|
||||
}
|
||||
}
|
||||
|
||||
// New creates new store for given user.
|
||||
func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) {
|
||||
return store.New(
|
||||
f.sentryReporter,
|
||||
f.panicHandler,
|
||||
user,
|
||||
f.eventListener,
|
||||
f.cache,
|
||||
f.builder,
|
||||
getUserStorePath(f.cacheProvider.GetDBDir(), user.ID()),
|
||||
f.events,
|
||||
)
|
||||
}
|
||||
|
||||
// Remove removes all store files for given user.
|
||||
func (f *storeFactory) Remove(userID string) error {
|
||||
return store.RemoveStore(
|
||||
f.events,
|
||||
getUserStorePath(f.cacheProvider.GetDBDir(), userID),
|
||||
userID,
|
||||
)
|
||||
}
|
||||
|
||||
// getUserStorePath returns the file path of the store database for the given userID.
|
||||
func getUserStorePath(storeDir string, userID string) (path string) {
|
||||
return filepath.Join(storeDir, fmt.Sprintf("mailbox-%v.db", userID))
|
||||
}
|
||||
205
internal/bridge/sync_test.go
Normal file
205
internal/bridge/sync_test.go
Normal file
@ -0,0 +1,205 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/bradenaw/juniper/iterator"
|
||||
"github.com/bradenaw/juniper/stream"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBridge_Sync(t *testing.T) {
|
||||
numMsg := 1 << 8
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
userID, addrID, err := s.CreateUser("imap", "imap@pm.me", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||
require.NoError(t, err)
|
||||
|
||||
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||
createMessages(ctx, t, c, addrID, labelID, numMsg)
|
||||
})
|
||||
|
||||
var total uint64
|
||||
|
||||
// The initial user should be fully synced.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
// Count how many bytes it takes to fully sync the user.
|
||||
total = countBytesRead(netCtl, func() {
|
||||
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
})
|
||||
})
|
||||
|
||||
// If we then connect an IMAP client, it should see all the messages.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login("imap@pm.me", string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
status, err := client.Select(`Folders/folder`, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(numMsg), status.Messages)
|
||||
})
|
||||
|
||||
// Now let's remove the user and simulate a network error.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.NoError(t, bridge.DeleteUser(ctx, userID))
|
||||
})
|
||||
|
||||
// Pretend we can only sync 2/3 of the original messages.
|
||||
netCtl.SetReadLimit(2 * total / 3)
|
||||
|
||||
// Login the user; its sync should fail.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
{
|
||||
syncCh, done := chToType[events.Event, events.SyncFailed](b.GetEvents(events.SyncFailed{}))
|
||||
defer done()
|
||||
|
||||
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login("imap@pm.me", string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
status, err := client.Select(`Folders/folder`, false)
|
||||
require.NoError(t, err)
|
||||
require.Less(t, status.Messages, uint32(numMsg))
|
||||
}
|
||||
|
||||
// Remove the network limit, allowing the sync to finish.
|
||||
netCtl.SetReadLimit(0)
|
||||
|
||||
{
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login("imap@pm.me", string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
status, err := client.Select(`Folders/folder`, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(numMsg), status.Messages)
|
||||
}
|
||||
})
|
||||
}, server.WithTLS(false))
|
||||
}
|
||||
|
||||
func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) {
|
||||
m := proton.New(
|
||||
proton.WithHostURL(s.GetHostURL()),
|
||||
proton.WithTransport(proton.InsecureTransport()),
|
||||
)
|
||||
|
||||
c, _, err := m.NewClientWithLogin(ctx, username, password)
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
fn(ctx, c)
|
||||
}
|
||||
|
||||
func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, count int) {
|
||||
literal, err := os.ReadFile(filepath.Join("testdata", "text-plain.eml"))
|
||||
require.NoError(t, err)
|
||||
|
||||
user, err := c.GetUser(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
addr, err := c.GetAddresses(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
salt, err := c.GetSalts(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyPass, err := salt.SaltForKey(password, user.Keys.Primary().ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, addrKRs, err := proton.Unlock(user, addr, keyPass)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, getErr(stream.Collect(ctx, c.ImportMessages(
|
||||
ctx,
|
||||
addrKRs[addrID],
|
||||
runtime.NumCPU(),
|
||||
runtime.NumCPU(),
|
||||
iterator.Collect(iterator.Map(iterator.Counter(count), func(i int) proton.ImportReq {
|
||||
return proton.ImportReq{
|
||||
Metadata: proton.ImportMetadata{
|
||||
AddressID: addrID,
|
||||
LabelIDs: []string{labelID},
|
||||
Flags: proton.MessageFlagReceived,
|
||||
},
|
||||
Message: literal,
|
||||
}
|
||||
}))...,
|
||||
))))
|
||||
}
|
||||
|
||||
func countBytesRead(ctl *proton.NetCtl, fn func()) uint64 {
|
||||
var read uint64
|
||||
|
||||
ctl.OnRead(func(b []byte) {
|
||||
atomic.AddUint64(&read, uint64(len(b)))
|
||||
})
|
||||
|
||||
fn()
|
||||
|
||||
return read
|
||||
}
|
||||
6
internal/bridge/testdata/text-plain.eml
vendored
Normal file
6
internal/bridge/testdata/text-plain.eml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
To: recipient@pm.me
|
||||
From: sender@pm.me
|
||||
Subject: Test
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Test
|
||||
@ -13,13 +13,10 @@
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package cache
|
||||
package bridge
|
||||
|
||||
type Options struct {
|
||||
MinFreeAbs uint64
|
||||
MinFreeRat float64
|
||||
ConcurrentRead int
|
||||
ConcurrentWrite int
|
||||
func (bridge *Bridge) GetBridgeTLSCert() ([]byte, []byte) {
|
||||
return bridge.vault.GetBridgeTLSCert(), bridge.vault.GetBridgeTLSKey()
|
||||
}
|
||||
@ -13,42 +13,47 @@
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"context"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
)
|
||||
|
||||
type Locator interface {
|
||||
Clear() error
|
||||
ClearUpdates() error
|
||||
ProvideSettingsPath() (string, error)
|
||||
ProvideLogsPath() (string, error)
|
||||
ProvideGluonPath() (string, error)
|
||||
GetLicenseFilePath() string
|
||||
GetDependencyLicensesLink() string
|
||||
Clear() error
|
||||
}
|
||||
|
||||
type CacheProvider interface {
|
||||
GetIMAPCachePath() string
|
||||
GetDBDir() string
|
||||
GetDefaultMessageCacheDir() string
|
||||
type Identifier interface {
|
||||
GetUserAgent() string
|
||||
HasClient() bool
|
||||
SetClient(name, version string)
|
||||
SetPlatform(platform string)
|
||||
}
|
||||
|
||||
type SettingsProvider interface {
|
||||
Get(key string) string
|
||||
Set(key string, value string)
|
||||
GetBool(key string) bool
|
||||
SetBool(key string, val bool)
|
||||
GetInt(key string) int
|
||||
type ProxyController interface {
|
||||
AllowProxy()
|
||||
DisallowProxy()
|
||||
}
|
||||
|
||||
type TLSReporter interface {
|
||||
GetTLSIssueCh() <-chan struct{}
|
||||
}
|
||||
|
||||
type Autostarter interface {
|
||||
Enable() error
|
||||
Disable() error
|
||||
}
|
||||
|
||||
type Updater interface {
|
||||
Check() (updater.VersionInfo, error)
|
||||
IsDowngrade(updater.VersionInfo) bool
|
||||
InstallUpdate(updater.VersionInfo) error
|
||||
}
|
||||
|
||||
type Versioner interface {
|
||||
RemoveOtherVersions(*semver.Version) error
|
||||
GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error)
|
||||
InstallUpdate(context.Context, updater.Downloader, updater.VersionInfo) error
|
||||
}
|
||||
|
||||
156
internal/bridge/updates.go
Normal file
156
internal/bridge/updates.go
Normal file
@ -0,0 +1,156 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) CheckForUpdates() {
|
||||
bridge.goUpdate()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) InstallUpdate(version updater.VersionInfo) {
|
||||
log := logrus.WithFields(logrus.Fields{
|
||||
"version": version.Version,
|
||||
"current": bridge.curVersion,
|
||||
"channel": bridge.vault.GetUpdateChannel(),
|
||||
})
|
||||
|
||||
select {
|
||||
case bridge.installCh <- installJob{version: version, silent: false}:
|
||||
log.Info("The update will be installed manually")
|
||||
|
||||
default:
|
||||
log.Info("An update is already being installed")
|
||||
}
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
|
||||
log := logrus.WithFields(logrus.Fields{
|
||||
"version": version.Version,
|
||||
"current": bridge.curVersion,
|
||||
"channel": bridge.vault.GetUpdateChannel(),
|
||||
})
|
||||
|
||||
bridge.publish(events.UpdateLatest{
|
||||
Version: version,
|
||||
})
|
||||
|
||||
switch {
|
||||
case !version.Version.GreaterThan(bridge.curVersion):
|
||||
log.Debug("No update available")
|
||||
|
||||
bridge.publish(events.UpdateNotAvailable{})
|
||||
|
||||
case version.RolloutProportion < bridge.vault.GetUpdateRollout():
|
||||
log.Info("An update is available but has not been rolled out yet")
|
||||
|
||||
bridge.publish(events.UpdateNotAvailable{})
|
||||
|
||||
case bridge.curVersion.LessThan(version.MinAuto):
|
||||
log.Info("An update is available but is incompatible with this version")
|
||||
|
||||
bridge.publish(events.UpdateAvailable{
|
||||
Version: version,
|
||||
Compatible: false,
|
||||
Silent: false,
|
||||
})
|
||||
|
||||
case !bridge.vault.GetAutoUpdate():
|
||||
log.Info("An update is available but auto-update is disabled")
|
||||
|
||||
bridge.publish(events.UpdateAvailable{
|
||||
Version: version,
|
||||
Compatible: true,
|
||||
Silent: false,
|
||||
})
|
||||
|
||||
default:
|
||||
safe.RLock(func() {
|
||||
if version.Version.GreaterThan(bridge.newVersion) {
|
||||
log.Info("An update is available")
|
||||
|
||||
select {
|
||||
case bridge.installCh <- installJob{version: version, silent: true}:
|
||||
log.Info("The update will be installed silently")
|
||||
|
||||
default:
|
||||
log.Info("An update is already being installed")
|
||||
}
|
||||
}
|
||||
}, bridge.newVersionLock)
|
||||
}
|
||||
}
|
||||
|
||||
type installJob struct {
|
||||
version updater.VersionInfo
|
||||
silent bool
|
||||
}
|
||||
|
||||
func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
||||
safe.Lock(func() {
|
||||
log := logrus.WithFields(logrus.Fields{
|
||||
"version": job.version.Version,
|
||||
"current": bridge.curVersion,
|
||||
"channel": bridge.vault.GetUpdateChannel(),
|
||||
})
|
||||
|
||||
bridge.publish(events.UpdateAvailable{
|
||||
Version: job.version,
|
||||
Compatible: true,
|
||||
Silent: job.silent,
|
||||
})
|
||||
|
||||
bridge.publish(events.UpdateInstalling{
|
||||
Version: job.version,
|
||||
Silent: job.silent,
|
||||
})
|
||||
|
||||
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.version)
|
||||
|
||||
switch {
|
||||
case errors.Is(err, updater.ErrUpdateAlreadyInstalled):
|
||||
log.Info("The update was already installed")
|
||||
|
||||
case err != nil:
|
||||
log.WithError(err).Error("The update could not be installed")
|
||||
|
||||
bridge.publish(events.UpdateFailed{
|
||||
Version: job.version,
|
||||
Silent: job.silent,
|
||||
Error: err,
|
||||
})
|
||||
default:
|
||||
log.Info("The update was installed successfully")
|
||||
|
||||
bridge.publish(events.UpdateInstalled{
|
||||
Version: job.version,
|
||||
Silent: job.silent,
|
||||
})
|
||||
|
||||
bridge.newVersion = job.version.Version
|
||||
}
|
||||
}, bridge.newVersionLock)
|
||||
}
|
||||
574
internal/bridge/user.go
Normal file
574
internal/bridge/user.go
Normal file
@ -0,0 +1,574 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/try"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type UserState int
|
||||
|
||||
const (
|
||||
SignedOut UserState = iota
|
||||
Locked
|
||||
Connected
|
||||
)
|
||||
|
||||
type UserInfo struct {
|
||||
// UserID is the user's API ID.
|
||||
UserID string
|
||||
|
||||
// Username is the user's API username.
|
||||
Username string
|
||||
|
||||
// Signed Out is true if the user is signed out (no AuthUID, user will need to provide credentials to log in again)
|
||||
State UserState
|
||||
|
||||
// Addresses holds the user's email addresses. The first address is the primary address.
|
||||
Addresses []string
|
||||
|
||||
// AddressMode is the user's address mode.
|
||||
AddressMode vault.AddressMode
|
||||
|
||||
// BridgePass is the user's bridge password.
|
||||
BridgePass []byte
|
||||
|
||||
// UsedSpace is the amount of space used by the user.
|
||||
UsedSpace int
|
||||
|
||||
// MaxSpace is the total amount of space available to the user.
|
||||
MaxSpace int
|
||||
}
|
||||
|
||||
// GetUserIDs returns the IDs of all known users (authorized or not).
|
||||
func (bridge *Bridge) GetUserIDs() []string {
|
||||
return bridge.vault.GetUserIDs()
|
||||
}
|
||||
|
||||
// GetUserInfo returns info about the given user.
|
||||
func (bridge *Bridge) GetUserInfo(userID string) (UserInfo, error) {
|
||||
return safe.RLockRetErr(func() (UserInfo, error) {
|
||||
if user, ok := bridge.users[userID]; ok {
|
||||
return getConnUserInfo(user), nil
|
||||
}
|
||||
|
||||
var info UserInfo
|
||||
|
||||
if err := bridge.vault.GetUser(userID, func(user *vault.User) {
|
||||
state := Locked
|
||||
if len(user.AuthUID()) == 0 {
|
||||
state = SignedOut
|
||||
}
|
||||
info = getUserInfo(user.UserID(), user.Username(), state, user.AddressMode())
|
||||
}); err != nil {
|
||||
return UserInfo{}, fmt.Errorf("failed to get user info: %w", err)
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
// QueryUserInfo queries the user info by username or address.
|
||||
func (bridge *Bridge) QueryUserInfo(query string) (UserInfo, error) {
|
||||
return safe.RLockRetErr(func() (UserInfo, error) {
|
||||
for _, user := range bridge.users {
|
||||
if user.Match(query) {
|
||||
return getConnUserInfo(user), nil
|
||||
}
|
||||
}
|
||||
|
||||
return UserInfo{}, ErrNoSuchUser
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
// LoginAuth begins the login process. It returns an authorized client that might need 2FA.
|
||||
func (bridge *Bridge) LoginAuth(ctx context.Context, username string, password []byte) (*proton.Client, proton.Auth, error) {
|
||||
logrus.WithField("username", logging.Sensitive(username)).Info("Authorizing user for login")
|
||||
|
||||
if username == "crash@bandicoot" {
|
||||
panic("Your wish is my command.. I crash!")
|
||||
}
|
||||
|
||||
client, auth, err := bridge.api.NewClientWithLogin(ctx, username, password)
|
||||
if err != nil {
|
||||
return nil, proton.Auth{}, fmt.Errorf("failed to create new API client: %w", err)
|
||||
}
|
||||
|
||||
if ok := safe.RLockRet(func() bool { return mapHas(bridge.users, auth.UID) }, bridge.usersLock); ok {
|
||||
logrus.WithField("userID", auth.UserID).Warn("User already logged in")
|
||||
|
||||
if err := client.AuthDelete(ctx); err != nil {
|
||||
logrus.WithError(err).Warn("Failed to delete auth")
|
||||
}
|
||||
|
||||
return nil, proton.Auth{}, ErrUserAlreadyLoggedIn
|
||||
}
|
||||
|
||||
return client, auth, nil
|
||||
}
|
||||
|
||||
// LoginUser finishes the user login process using the client and auth received from LoginAuth.
|
||||
func (bridge *Bridge) LoginUser(
|
||||
ctx context.Context,
|
||||
client *proton.Client,
|
||||
auth proton.Auth,
|
||||
keyPass []byte,
|
||||
) (string, error) {
|
||||
logrus.WithField("userID", auth.UserID).Info("Logging in authorized user")
|
||||
|
||||
userID, err := try.CatchVal(
|
||||
func() (string, error) {
|
||||
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
|
||||
},
|
||||
func() error {
|
||||
return client.AuthDelete(ctx)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to login user: %w", err)
|
||||
}
|
||||
|
||||
bridge.publish(events.UserLoggedIn{
|
||||
UserID: userID,
|
||||
})
|
||||
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
// LoginFull authorizes a new bridge user with the given username and password.
|
||||
// If necessary, a TOTP and mailbox password are requested via the callbacks.
|
||||
// This is equivalent to doing LoginAuth and LoginUser separately.
|
||||
func (bridge *Bridge) LoginFull(
|
||||
ctx context.Context,
|
||||
username string,
|
||||
password []byte,
|
||||
getTOTP func() (string, error),
|
||||
getKeyPass func() ([]byte, error),
|
||||
) (string, error) {
|
||||
logrus.WithField("username", logging.Sensitive(username)).Info("Performing full user login")
|
||||
|
||||
client, auth, err := bridge.LoginAuth(ctx, username, password)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to begin login process: %w", err)
|
||||
}
|
||||
|
||||
if auth.TwoFA.Enabled == proton.TOTPEnabled {
|
||||
logrus.WithField("userID", auth.UserID).Info("Requesting TOTP")
|
||||
|
||||
totp, err := getTOTP()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get TOTP: %w", err)
|
||||
}
|
||||
|
||||
if err := client.Auth2FA(ctx, proton.Auth2FAReq{TwoFactorCode: totp}); err != nil {
|
||||
return "", fmt.Errorf("failed to authorize 2FA: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var keyPass []byte
|
||||
|
||||
if auth.PasswordMode == proton.TwoPasswordMode {
|
||||
logrus.WithField("userID", auth.UserID).Info("Requesting mailbox password")
|
||||
|
||||
userKeyPass, err := getKeyPass()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get key password: %w", err)
|
||||
}
|
||||
|
||||
keyPass = userKeyPass
|
||||
} else {
|
||||
keyPass = password
|
||||
}
|
||||
|
||||
return bridge.LoginUser(ctx, client, auth, keyPass)
|
||||
}
|
||||
|
||||
// LogoutUser logs out the given user.
|
||||
func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
|
||||
logrus.WithField("userID", userID).Info("Logging out user")
|
||||
|
||||
return safe.LockRet(func() error {
|
||||
user, ok := bridge.users[userID]
|
||||
if !ok {
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
bridge.logoutUser(ctx, user, true, false)
|
||||
|
||||
bridge.publish(events.UserLoggedOut{
|
||||
UserID: userID,
|
||||
})
|
||||
|
||||
return nil
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
// DeleteUser deletes the given user.
|
||||
func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
||||
logrus.WithField("userID", userID).Info("Deleting user")
|
||||
|
||||
return safe.LockRet(func() error {
|
||||
if !bridge.vault.HasUser(userID) {
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
if user, ok := bridge.users[userID]; ok {
|
||||
bridge.logoutUser(ctx, user, true, true)
|
||||
}
|
||||
|
||||
if err := bridge.vault.DeleteUser(userID); err != nil {
|
||||
logrus.WithError(err).Error("Failed to delete vault user")
|
||||
}
|
||||
|
||||
bridge.publish(events.UserDeleted{
|
||||
UserID: userID,
|
||||
})
|
||||
|
||||
return nil
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
// SetAddressMode sets the address mode for the given user.
|
||||
func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode vault.AddressMode) error {
|
||||
logrus.WithField("userID", userID).WithField("mode", mode).Info("Setting address mode")
|
||||
|
||||
return safe.RLockRet(func() error {
|
||||
user, ok := bridge.users[userID]
|
||||
if !ok {
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
if user.GetAddressMode() == mode {
|
||||
return fmt.Errorf("address mode is already %q", mode)
|
||||
}
|
||||
|
||||
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
|
||||
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
||||
}
|
||||
|
||||
if err := user.SetAddressMode(ctx, mode); err != nil {
|
||||
return fmt.Errorf("failed to set address mode: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||
}
|
||||
|
||||
bridge.publish(events.AddressModeChanged{
|
||||
UserID: userID,
|
||||
AddressMode: mode,
|
||||
})
|
||||
|
||||
return nil
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, authUID, authRef string, keyPass []byte) (string, error) {
|
||||
apiUser, err := client.GetUser(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get API user: %w", err)
|
||||
}
|
||||
|
||||
salts, err := client.GetSalts(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get key salts: %w", err)
|
||||
}
|
||||
|
||||
saltedKeyPass, err := salts.SaltForKey(keyPass, apiUser.Keys.Primary().ID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to salt key password: %w", err)
|
||||
}
|
||||
|
||||
if userKR, err := apiUser.Keys.Unlock(saltedKeyPass, nil); err != nil {
|
||||
return "", fmt.Errorf("failed to unlock user keys: %w", err)
|
||||
} else if userKR.CountDecryptionEntities() == 0 {
|
||||
return "", fmt.Errorf("failed to unlock user keys")
|
||||
}
|
||||
|
||||
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {
|
||||
return "", fmt.Errorf("failed to add bridge user: %w", err)
|
||||
}
|
||||
|
||||
return apiUser.ID, nil
|
||||
}
|
||||
|
||||
// loadUsers tries to load each user in the vault that isn't already loaded.
|
||||
func (bridge *Bridge) loadUsers(ctx context.Context) error {
|
||||
return bridge.vault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
|
||||
if user.AuthUID() == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if safe.RLockRet(func() bool { return mapHas(bridge.users, user.UserID()) }, bridge.usersLock) {
|
||||
return nil
|
||||
}
|
||||
|
||||
logrus.WithField("userID", user.UserID()).Info("Loading connected user")
|
||||
|
||||
bridge.publish(events.UserLoading{
|
||||
UserID: user.UserID(),
|
||||
})
|
||||
|
||||
if err := bridge.loadUser(ctx, user); err != nil {
|
||||
logrus.WithError(err).Error("Failed to load connected user")
|
||||
|
||||
bridge.publish(events.UserLoadFail{
|
||||
UserID: user.UserID(),
|
||||
Error: err,
|
||||
})
|
||||
} else {
|
||||
logrus.WithField("userID", user.UserID()).Info("Successfully loaded user")
|
||||
|
||||
bridge.publish(events.UserLoadSuccess{
|
||||
UserID: user.UserID(),
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// loadUser loads an existing user from the vault.
|
||||
func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
|
||||
client, auth, err := bridge.api.NewClientWithRefresh(ctx, user.AuthUID(), user.AuthRef())
|
||||
if err != nil {
|
||||
if apiErr := new(proton.Error); errors.As(err, &apiErr) && (apiErr.Code == proton.AuthRefreshTokenInvalid) {
|
||||
// The session cannot be refreshed, we sign out the user by clearing his auth secrets.
|
||||
if err := user.Clear(); err != nil {
|
||||
logrus.WithError(err).Warn("Failed to clear user secrets")
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to create API client: %w", err)
|
||||
}
|
||||
|
||||
if err := user.SetAuth(auth.UID, auth.RefreshToken); err != nil {
|
||||
return fmt.Errorf("failed to set auth: %w", err)
|
||||
}
|
||||
|
||||
apiUser, err := client.GetUser(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.addUser(ctx, client, apiUser, auth.UID, auth.RefreshToken, user.KeyPass(), false); err != nil {
|
||||
return fmt.Errorf("failed to add user: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addUser adds a new user with an already salted mailbox password.
|
||||
func (bridge *Bridge) addUser(
|
||||
ctx context.Context,
|
||||
client *proton.Client,
|
||||
apiUser proton.User,
|
||||
authUID, authRef string,
|
||||
saltedKeyPass []byte,
|
||||
isLogin bool,
|
||||
) error {
|
||||
vaultUser, isNew, err := bridge.newVaultUser(apiUser, authUID, authRef, saltedKeyPass)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add vault user: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser); err != nil {
|
||||
if _, ok := err.(*resty.ResponseError); ok || isLogin {
|
||||
logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault")
|
||||
|
||||
if err := vaultUser.Clear(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to clear user secrets")
|
||||
}
|
||||
} else {
|
||||
logrus.WithError(err).Error("Failed to add user")
|
||||
}
|
||||
|
||||
if err := vaultUser.Close(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close vault user")
|
||||
}
|
||||
|
||||
if isNew {
|
||||
logrus.Warn("Deleting newly added vault user")
|
||||
|
||||
if err := bridge.vault.DeleteUser(apiUser.ID); err != nil {
|
||||
logrus.WithError(err).Error("Failed to delete vault user")
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to add user with vault: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addUserWithVault adds a new user to bridge with the given vault.
|
||||
func (bridge *Bridge) addUserWithVault(
|
||||
ctx context.Context,
|
||||
client *proton.Client,
|
||||
apiUser proton.User,
|
||||
vault *vault.User,
|
||||
) error {
|
||||
user, err := user.New(
|
||||
ctx,
|
||||
vault,
|
||||
client,
|
||||
apiUser,
|
||||
bridge.crashHandler,
|
||||
bridge.reporter,
|
||||
bridge.vault.SyncWorkers(),
|
||||
bridge.vault.GetShowAllMail(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
|
||||
// Connect the user's address(es) to gluon.
|
||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||
}
|
||||
|
||||
// Handle events coming from the user before forwarding them to the bridge.
|
||||
// For example, if the user's addresses change, we need to update them in gluon.
|
||||
bridge.tasks.Once(func(ctx context.Context) {
|
||||
async.RangeContext(ctx, user.GetEventCh(), func(event events.Event) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"userID": apiUser.ID,
|
||||
"event": event,
|
||||
}).Debug("Received user event")
|
||||
|
||||
if err := bridge.handleUserEvent(ctx, user, event); err != nil {
|
||||
logrus.WithError(err).Error("Failed to handle user event")
|
||||
} else {
|
||||
bridge.publish(event)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Gluon will set the IMAP ID in the context, if known, before making requests on behalf of this user.
|
||||
// As such, if we find this ID in the context, we should use it to update our user agent.
|
||||
client.AddPreRequestHook(func(_ *resty.Client, r *resty.Request) error {
|
||||
if imapID, ok := imap.GetIMAPIDFromContext(r.Context()); ok {
|
||||
bridge.identifier.SetClient(imapID.Name, imapID.Version)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// Finally, save the user in the bridge.
|
||||
safe.Lock(func() {
|
||||
bridge.users[apiUser.ID] = user
|
||||
}, bridge.usersLock)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// newVaultUser creates a new vault user from the given auth information.
|
||||
// If one already exists in the vault, its data will be updated.
|
||||
func (bridge *Bridge) newVaultUser(
|
||||
apiUser proton.User,
|
||||
authUID, authRef string,
|
||||
saltedKeyPass []byte,
|
||||
) (*vault.User, bool, error) {
|
||||
if !bridge.vault.HasUser(apiUser.ID) {
|
||||
user, err := bridge.vault.AddUser(apiUser.ID, apiUser.Name, authUID, authRef, saltedKeyPass)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to add user to vault: %w", err)
|
||||
}
|
||||
|
||||
return user, true, nil
|
||||
}
|
||||
|
||||
user, err := bridge.vault.NewUser(apiUser.ID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if err := user.SetAuth(authUID, authRef); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if err := user.SetKeyPass(saltedKeyPass); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return user, false, nil
|
||||
}
|
||||
|
||||
// logout logs out the given user, optionally logging them out from the API too.
|
||||
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData bool) {
|
||||
defer delete(bridge.users, user.ID())
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"userID": user.ID(),
|
||||
"withAPI": withAPI,
|
||||
"withData": withData,
|
||||
}).Debug("Logging out user")
|
||||
|
||||
if err := bridge.removeIMAPUser(ctx, user, withData); err != nil {
|
||||
logrus.WithError(err).Error("Failed to remove IMAP user")
|
||||
}
|
||||
|
||||
if err := user.Logout(ctx, withAPI); err != nil {
|
||||
logrus.WithError(err).Error("Failed to logout user")
|
||||
}
|
||||
|
||||
user.Close()
|
||||
}
|
||||
|
||||
// getUserInfo returns information about a disconnected user.
|
||||
func getUserInfo(userID, username string, state UserState, addressMode vault.AddressMode) UserInfo {
|
||||
return UserInfo{
|
||||
State: state,
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
AddressMode: addressMode,
|
||||
}
|
||||
}
|
||||
|
||||
// getConnUserInfo returns information about a connected user.
|
||||
func getConnUserInfo(user *user.User) UserInfo {
|
||||
return UserInfo{
|
||||
State: Connected,
|
||||
UserID: user.ID(),
|
||||
Username: user.Name(),
|
||||
Addresses: user.Emails(),
|
||||
AddressMode: user.GetAddressMode(),
|
||||
BridgePass: user.BridgePass(),
|
||||
UsedSpace: user.UsedSpace(),
|
||||
MaxSpace: user.MaxSpace(),
|
||||
}
|
||||
}
|
||||
|
||||
func mapHas[Key comparable, Val any](m map[Key]Val, key Key) bool {
|
||||
_, ok := m[key]
|
||||
return ok
|
||||
}
|
||||
132
internal/bridge/user_events.go
Normal file
132
internal/bridge/user_events.go
Normal file
@ -0,0 +1,132 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) error {
|
||||
switch event := event.(type) {
|
||||
case events.UserAddressCreated:
|
||||
if err := bridge.handleUserAddressCreated(ctx, user, event); err != nil {
|
||||
return fmt.Errorf("failed to handle user address created event: %w", err)
|
||||
}
|
||||
|
||||
case events.UserAddressUpdated:
|
||||
if err := bridge.handleUserAddressUpdated(ctx, user, event); err != nil {
|
||||
return fmt.Errorf("failed to handle user address updated event: %w", err)
|
||||
}
|
||||
|
||||
case events.UserAddressDeleted:
|
||||
if err := bridge.handleUserAddressDeleted(ctx, user, event); err != nil {
|
||||
return fmt.Errorf("failed to handle user address deleted event: %w", err)
|
||||
}
|
||||
|
||||
case events.UserRefreshed:
|
||||
if err := bridge.handleUserRefreshed(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to handle user refreshed event: %w", err)
|
||||
}
|
||||
|
||||
case events.UserDeauth:
|
||||
bridge.handleUserDeauth(ctx, user)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.User, event events.UserAddressCreated) error {
|
||||
if user.GetAddressMode() == vault.SplitMode {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add user to IMAP server: %w", err)
|
||||
}
|
||||
|
||||
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to set gluon ID: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GODT-1948: Handle addresses that have been disabled!
|
||||
func (bridge *Bridge) handleUserAddressUpdated(_ context.Context, user *user.User, _ events.UserAddressUpdated) error {
|
||||
switch user.GetAddressMode() {
|
||||
case vault.CombinedMode:
|
||||
return fmt.Errorf("not implemented")
|
||||
|
||||
case vault.SplitMode:
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.User, event events.UserAddressDeleted) error {
|
||||
if user.GetAddressMode() == vault.SplitMode {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
gluonID, ok := user.GetGluonID(event.AddressID)
|
||||
if !ok {
|
||||
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
|
||||
}
|
||||
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
||||
}
|
||||
|
||||
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserRefreshed(ctx context.Context, user *user.User) error {
|
||||
return safe.RLockRet(func() error {
|
||||
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
|
||||
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
||||
safe.Lock(func() {
|
||||
bridge.logoutUser(ctx, user, false, false)
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
650
internal/bridge/user_test.go
Normal file
650
internal/bridge/user_test.go
Normal file
@ -0,0 +1,650 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
mocksPkg "github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBridge_WithoutUsers(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Login(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The user is now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginLogoutLogin(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
// The user is now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// Logout the user.
|
||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||
|
||||
// The user is now disconnected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// Login the user again.
|
||||
newUserID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
require.Equal(t, userID, newUserID)
|
||||
|
||||
// The user is connected again.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginDeleteLogin(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
// The user is now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// Delete the user.
|
||||
require.NoError(t, bridge.DeleteUser(ctx, userID))
|
||||
|
||||
// The user is now gone.
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// Login the user again.
|
||||
newUserID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
require.Equal(t, userID, newUserID)
|
||||
|
||||
// The user is connected again.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginDeauthLogin(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
// Get a channel to receive the deauth event.
|
||||
eventCh, done := bridge.GetEvents(events.UserDeauth{})
|
||||
defer done()
|
||||
|
||||
// Deauth the user.
|
||||
require.NoError(t, s.RevokeUser(userID))
|
||||
|
||||
// The user is eventually disconnected.
|
||||
require.Eventually(t, func() bool {
|
||||
return len(getConnectedUserIDs(t, bridge)) == 0
|
||||
}, 10*time.Second, time.Second)
|
||||
|
||||
// We should get a deauth event.
|
||||
require.IsType(t, events.UserDeauth{}, <-eventCh)
|
||||
|
||||
// Login the user after the disconnection.
|
||||
newUserID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
require.Equal(t, userID, newUserID)
|
||||
|
||||
// The user is connected again.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginDeauthRestartLogin(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
// Get a channel to receive the deauth event.
|
||||
eventCh, done := bridge.GetEvents(events.UserDeauth{})
|
||||
defer done()
|
||||
|
||||
// Deauth the user.
|
||||
require.NoError(t, s.RevokeUser(userID))
|
||||
|
||||
// The user is eventually disconnected.
|
||||
require.Eventually(t, func() bool {
|
||||
return len(getConnectedUserIDs(t, bridge)) == 0
|
||||
}, 10*time.Second, time.Second)
|
||||
|
||||
// We should get a deauth event.
|
||||
require.IsType(t, events.UserDeauth{}, <-eventCh)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// The user should be disconnected at startup.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// Login the user after the disconnection.
|
||||
newUserID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
require.Equal(t, userID, newUserID)
|
||||
|
||||
// The user is connected again.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginExpireLogin(t *testing.T) {
|
||||
const authLife = 2 * time.Second
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
s.SetAuthLife(authLife)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user. Its auth will only be valid for a short time.
|
||||
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
// Wait until the auth expires.
|
||||
time.Sleep(authLife)
|
||||
|
||||
// The user will have to refresh but the logout will still succeed.
|
||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_FailToLoad(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
// Login the user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
})
|
||||
|
||||
// Deauth the user while bridge is stopped.
|
||||
require.NoError(t, s.RevokeUser(userID))
|
||||
|
||||
// When bridge starts, the user will not be logged in.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoadWithoutInternet(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
// Login the user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
})
|
||||
|
||||
// Simulate loss of internet connection.
|
||||
netCtl.Disable()
|
||||
|
||||
// Start bridge without internet.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Initially, users are not connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// Simulate internet connection.
|
||||
netCtl.Enable()
|
||||
|
||||
// The user will eventually be connected.
|
||||
require.Eventually(t, func() bool {
|
||||
return len(getConnectedUserIDs(t, bridge)) == 1 && getConnectedUserIDs(t, bridge)[0] == userID
|
||||
}, 10*time.Second, time.Second)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginRestart(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginLogoutRestart(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
// Logout the user.
|
||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// The user is still disconnected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginDeleteRestart(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
// Delete the user.
|
||||
require.NoError(t, bridge.DeleteUser(ctx, userID))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// The user is still gone.
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_FailLoginRecover(t *testing.T) {
|
||||
for i := uint64(1); i < 10; i++ {
|
||||
t.Run(fmt.Sprintf("read %v%% of the data", 100*i/10), func(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
// Log the user in, wait for it to sync, then log it out.
|
||||
// (We don't want to count message sync data in the test.)
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||
})
|
||||
|
||||
var total uint64
|
||||
|
||||
// Now that the user is synced, we can measure exactly how much data is needed during login.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
total = countBytesRead(netCtl, func() {
|
||||
must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
})
|
||||
|
||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||
})
|
||||
|
||||
// Now simulate failing to login.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Simulate a partial read.
|
||||
netCtl.SetReadLimit(i * total / 10)
|
||||
|
||||
// We should fail to log the user in because we can't fully read its data.
|
||||
require.Error(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||
|
||||
// The user should still be there (but disconnected).
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
|
||||
// Simulate the network recovering.
|
||||
netCtl.SetReadLimit(0)
|
||||
|
||||
// We should now be able to log the user in.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||
|
||||
// The user should be there, now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridge_FailLoadRecover(t *testing.T) {
|
||||
for i := uint64(1); i < 10; i++ {
|
||||
t.Run(fmt.Sprintf("read %v%% of the data", 100*i/10), func(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
// Log the user in and wait for it to sync.
|
||||
// (We don't want to count message sync data in the test.)
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
})
|
||||
|
||||
// See how much data it takes to load the user at startup.
|
||||
total := countBytesRead(netCtl, func() {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// ...
|
||||
})
|
||||
})
|
||||
|
||||
// Simulate a partial read.
|
||||
netCtl.SetReadLimit(i * total / 10)
|
||||
|
||||
// We should fail to load the user; it should be listed but disconnected.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
|
||||
// Simulate the network recovering.
|
||||
netCtl.SetReadLimit(0)
|
||||
|
||||
// We should now be able to load the user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridge_BridgePass(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
var pass []byte
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
// Retrieve the bridge pass.
|
||||
pass = must(bridge.GetUserInfo(userID)).BridgePass
|
||||
|
||||
// Log the user out.
|
||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||
|
||||
// Log the user back in.
|
||||
must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
// The bridge pass should be the same.
|
||||
require.Equal(t, pass, pass)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// The bridge should load the user.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// The bridge pass should be the same.
|
||||
require.Equal(t, pass, must(bridge.GetUserInfo(userID)).BridgePass)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_AddressMode(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get the user's info.
|
||||
info, err := bridge.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The user is in combined mode by default.
|
||||
require.Equal(t, vault.CombinedMode, info.AddressMode)
|
||||
|
||||
// Repeatedly switch between address modes.
|
||||
for i := 1; i <= 10; i++ {
|
||||
var target vault.AddressMode
|
||||
|
||||
if i%2 == 0 {
|
||||
target = vault.CombinedMode
|
||||
} else {
|
||||
target = vault.SplitMode
|
||||
}
|
||||
|
||||
// Put the user in the target mode.
|
||||
require.NoError(t, bridge.SetAddressMode(ctx, userID, target))
|
||||
|
||||
// Get the user's info.
|
||||
info, err = bridge.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The user is in the target mode.
|
||||
require.Equal(t, target, info.AddressMode)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginLogoutRepeated(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
for i := 0; i < 10; i++ {
|
||||
// Log the user in.
|
||||
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
// Log the user out.
|
||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LogoutOffline(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
// The user is now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// Go offline.
|
||||
netCtl.Disable()
|
||||
|
||||
// We can still log the user out.
|
||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||
|
||||
// The user is now disconnected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
|
||||
// Go back online.
|
||||
netCtl.Enable()
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// The user is still disconnected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_DeleteDisconnected(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The user is now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// Logout the user.
|
||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||
|
||||
// The user is now disconnected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// Delete the user.
|
||||
require.NoError(t, bridge.DeleteUser(ctx, userID))
|
||||
|
||||
// The user is now deleted.
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_DeleteOffline(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The user is now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// Go offline.
|
||||
netCtl.Disable()
|
||||
|
||||
// We can still log the user out.
|
||||
require.NoError(t, bridge.DeleteUser(ctx, userID))
|
||||
|
||||
// The user is now gone.
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_UserInfo_Alias(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Create a new user.
|
||||
userID, _, err := s.CreateUser("primary", "primary@pm.me", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Give the new user an alias.
|
||||
require.NoError(t, getErr(s.CreateAddress(userID, "alias@pm.me", []byte("password"))))
|
||||
|
||||
// Login the user.
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, "primary", []byte("password"), nil, nil)))
|
||||
|
||||
// Get user info.
|
||||
info, err := bridge.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The user should have two addresses, the primary should be first.
|
||||
require.Equal(t, []string{"primary@pm.me", "alias@pm.me"}, info.Addresses)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_Refresh(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(
|
||||
gomock.Eq("Warning: refresh occurred"),
|
||||
mocksPkg.NewRefreshContextMatcher(proton.RefreshAll),
|
||||
).Return(nil)
|
||||
|
||||
// Get a channel of sync started events.
|
||||
syncStartCh, done := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
||||
defer done()
|
||||
|
||||
// Get a channel of sync finished events.
|
||||
syncFinishCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
// Log the user in.
|
||||
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
// The sync should start and finish.
|
||||
require.Equal(t, userID, (<-syncStartCh).UserID)
|
||||
require.Equal(t, userID, (<-syncFinishCh).UserID)
|
||||
|
||||
// Trigger a refresh.
|
||||
require.NoError(t, s.RefreshUser(userID, proton.RefreshAll))
|
||||
|
||||
// The sync should start and finish again.
|
||||
require.Equal(t, userID, (<-syncStartCh).UserID)
|
||||
require.Equal(t, userID, (<-syncFinishCh).UserID)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// getErr returns the error that was passed to it.
|
||||
func getErr[T any](val T, err error) error {
|
||||
return err
|
||||
}
|
||||
@ -15,9 +15,31 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package tls
|
||||
package certs
|
||||
|
||||
import "golang.org/x/sys/execabs"
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
func installCert(certPEM []byte) error {
|
||||
name, err := writeToTempFile(certPEM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addTrustedCert(name)
|
||||
}
|
||||
|
||||
func uninstallCert(certPEM []byte) error {
|
||||
name, err := writeToTempFile(certPEM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return removeTrustedCert(name)
|
||||
}
|
||||
|
||||
func addTrustedCert(certPath string) error {
|
||||
return execabs.Command( //nolint:gosec
|
||||
@ -44,10 +66,20 @@ func removeTrustedCert(certPath string) error {
|
||||
).Run()
|
||||
}
|
||||
|
||||
func (t *TLS) InstallCerts() error {
|
||||
return addTrustedCert(t.getTLSCertPath())
|
||||
}
|
||||
// writeToTempFile writes the given data to a temporary file and returns the path.
|
||||
func writeToTempFile(data []byte) (string, error) {
|
||||
f, err := os.CreateTemp("", "tls")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
func (t *TLS) UninstallCerts() error {
|
||||
return removeTrustedCert(t.getTLSCertPath())
|
||||
if _, err := f.Write(data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return f.Name(), nil
|
||||
}
|
||||
@ -15,12 +15,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package tls
|
||||
package certs
|
||||
|
||||
func (t *TLS) InstallCerts() error {
|
||||
func installCert([]byte) error {
|
||||
return nil // Linux doesn't have a root cert store.
|
||||
}
|
||||
|
||||
func (t *TLS) UninstallCerts() error {
|
||||
func uninstallCert([]byte) error {
|
||||
return nil // Linux doesn't have a root cert store.
|
||||
}
|
||||
@ -15,12 +15,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package tls
|
||||
package certs
|
||||
|
||||
func (t *TLS) InstallCerts() error {
|
||||
func installCert([]byte) error {
|
||||
return nil // NOTE(GODT-986): Install certs to root cert store?
|
||||
}
|
||||
|
||||
func (t *TLS) UninstallCerts() error {
|
||||
func uninstallCert([]byte) error {
|
||||
return nil // NOTE(GODT-986): Uninstall certs from root cert store?
|
||||
}
|
||||
32
internal/certs/installer.go
Normal file
32
internal/certs/installer.go
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package certs
|
||||
|
||||
type Installer struct{}
|
||||
|
||||
func NewInstaller() *Installer {
|
||||
return &Installer{}
|
||||
}
|
||||
|
||||
func (installer *Installer) InstallCert(certPEM []byte) error {
|
||||
return installCert(certPEM)
|
||||
}
|
||||
|
||||
func (installer *Installer) UninstallCert(certPEM []byte) error {
|
||||
return uninstallCert(certPEM)
|
||||
}
|
||||
@ -15,9 +15,10 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package tls
|
||||
package certs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
@ -27,22 +28,13 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type TLS struct {
|
||||
settingsPath string
|
||||
}
|
||||
|
||||
func New(settingsPath string) *TLS {
|
||||
return &TLS{
|
||||
settingsPath: settingsPath,
|
||||
}
|
||||
}
|
||||
// ErrTLSCertExpiresSoon is returned when the TLS certificate is about to expire.
|
||||
var ErrTLSCertExpiresSoon = fmt.Errorf("TLS certificate will expire soon")
|
||||
|
||||
// NewTLSTemplate creates a new TLS template certificate with a random serial number.
|
||||
func NewTLSTemplate() (*x509.Certificate, error) {
|
||||
@ -69,65 +61,36 @@ func NewTLSTemplate() (*x509.Certificate, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
var ErrTLSCertExpiresSoon = fmt.Errorf("TLS certificate will expire soon")
|
||||
|
||||
// getTLSCertPath returns path to certificate; used for TLS servers (IMAP, SMTP).
|
||||
func (t *TLS) getTLSCertPath() string {
|
||||
return filepath.Join(t.settingsPath, "cert.pem")
|
||||
}
|
||||
|
||||
// getTLSKeyPath returns path to private key; used for TLS servers (IMAP, SMTP).
|
||||
func (t *TLS) getTLSKeyPath() string {
|
||||
return filepath.Join(t.settingsPath, "key.pem")
|
||||
}
|
||||
|
||||
// HasCerts returns whether TLS certs have been generated.
|
||||
func (t *TLS) HasCerts() bool {
|
||||
if _, err := os.Stat(t.getTLSCertPath()); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, err := os.Stat(t.getTLSKeyPath()); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GenerateCerts generates certs from the given template.
|
||||
func (t *TLS) GenerateCerts(template *x509.Certificate) error {
|
||||
// GenerateCert generates a new TLS certificate and returns it as PEM.
|
||||
var GenerateCert = func(template *x509.Certificate) ([]byte, []byte, error) { //nolint:gochecknoglobals
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate private key")
|
||||
return nil, nil, errors.Wrap(err, "failed to generate private key")
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create certificate")
|
||||
return nil, nil, errors.Wrap(err, "failed to create certificate")
|
||||
}
|
||||
|
||||
certOut, err := os.Create(t.getTLSCertPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer certOut.Close() //nolint:errcheck,gosec
|
||||
certPEM := new(bytes.Buffer)
|
||||
|
||||
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
return err
|
||||
if err := pem.Encode(certPEM, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
keyOut, err := os.OpenFile(t.getTLSKeyPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer keyOut.Close() //nolint:errcheck,gosec
|
||||
keyPEM := new(bytes.Buffer)
|
||||
|
||||
return pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||
if err := pem.Encode(keyPEM, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return certPEM.Bytes(), keyPEM.Bytes(), nil
|
||||
}
|
||||
|
||||
// GetConfig tries to load TLS config or generate new one which is then returned.
|
||||
func (t *TLS) GetConfig() (*tls.Config, error) {
|
||||
c, err := tls.LoadX509KeyPair(t.getTLSCertPath(), t.getTLSKeyPath())
|
||||
func GetConfig(certPEM, keyPEM []byte) (*tls.Config, error) {
|
||||
c, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load keypair")
|
||||
}
|
||||
@ -15,10 +15,10 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package tls
|
||||
package certs
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"crypto/tls"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -26,12 +26,6 @@ import (
|
||||
)
|
||||
|
||||
func TestGetOldConfig(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "test-tls")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create new tls object.
|
||||
tls := New(dir)
|
||||
|
||||
// Create new TLS template.
|
||||
tlsTemplate, err := NewTLSTemplate()
|
||||
require.NoError(t, err)
|
||||
@ -41,20 +35,15 @@ func TestGetOldConfig(t *testing.T) {
|
||||
tlsTemplate.NotAfter = time.Now()
|
||||
|
||||
// Generate the certs from the template.
|
||||
require.NoError(t, tls.GenerateCerts(tlsTemplate))
|
||||
certPEM, keyPEM, err := GenerateCert(tlsTemplate)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate the config from the certs -- it's going to expire soon so we don't want to use it.
|
||||
_, err = tls.GetConfig()
|
||||
_, err = GetConfig(certPEM, keyPEM)
|
||||
require.Equal(t, err, ErrTLSCertExpiresSoon)
|
||||
}
|
||||
|
||||
func TestGetValidConfig(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "test-tls")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create new tls object.
|
||||
tls := New(dir)
|
||||
|
||||
// Create new TLS template.
|
||||
tlsTemplate, err := NewTLSTemplate()
|
||||
require.NoError(t, err)
|
||||
@ -64,10 +53,11 @@ func TestGetValidConfig(t *testing.T) {
|
||||
tlsTemplate.NotAfter = time.Now().Add(2 * 365 * 24 * time.Hour)
|
||||
|
||||
// Generate the certs from the template.
|
||||
require.NoError(t, tls.GenerateCerts(tlsTemplate))
|
||||
certPEM, keyPEM, err := GenerateCert(tlsTemplate)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate the config from the certs -- it's not going to expire soon so we want to use it.
|
||||
config, err := tls.GetConfig()
|
||||
config, err := GetConfig(certPEM, keyPEM)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(config.Certificates), 1)
|
||||
|
||||
@ -75,3 +65,15 @@ func TestGetValidConfig(t *testing.T) {
|
||||
now, notValidAfter := time.Now(), config.Certificates[0].Leaf.NotAfter
|
||||
require.False(t, now.After(notValidAfter), "new certificate expected to be valid at %v but have valid until %v", now, notValidAfter)
|
||||
}
|
||||
|
||||
func TestNewConfig(t *testing.T) {
|
||||
tlsTemplate, err := NewTLSTemplate()
|
||||
require.NoError(t, err)
|
||||
|
||||
pemCert, pemKey, err := GenerateCert(tlsTemplate)
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := tls.X509KeyPair(pemCert, pemKey)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cert)
|
||||
}
|
||||
119
internal/clientconfig/applemail.go
Normal file
119
internal/clientconfig/applemail.go
Normal file
@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package clientconfig
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/mobileconfig"
|
||||
"golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
const (
|
||||
bigSurPreferencesPane = "/System/Library/PreferencePanes/Profiles.prefPane"
|
||||
venturaPreferencesPane = "x-apple.systempreferences:com.apple.preferences.configurationprofiles"
|
||||
)
|
||||
|
||||
type AppleMail struct{}
|
||||
|
||||
func (c *AppleMail) Configure(
|
||||
hostname string,
|
||||
imapPort, smtpPort int,
|
||||
imapSSL, smtpSSL bool,
|
||||
username, addresses string,
|
||||
password []byte,
|
||||
) error {
|
||||
mc := prepareMobileConfig(hostname, imapPort, smtpPort, imapSSL, smtpSSL, username, addresses, password)
|
||||
|
||||
confPath, err := saveConfigTemporarily(mc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if useragent.IsBigSurOrNewer() {
|
||||
prefPane := bigSurPreferencesPane
|
||||
|
||||
if useragent.IsVenturaOrNewer() {
|
||||
prefPane = venturaPreferencesPane
|
||||
}
|
||||
|
||||
return execabs.Command("open", prefPane, confPath).Run() //nolint:gosec // G204 open command is safe, mobileconfig is generated by us
|
||||
}
|
||||
|
||||
return execabs.Command("open", confPath).Run() //nolint:gosec // G204 open command is safe, mobileconfig is generated by us
|
||||
}
|
||||
|
||||
func prepareMobileConfig(
|
||||
hostname string,
|
||||
imapPort, smtpPort int,
|
||||
imapSSL, smtpSSL bool,
|
||||
username, addresses string,
|
||||
password []byte,
|
||||
) *mobileconfig.Config {
|
||||
return &mobileconfig.Config{
|
||||
DisplayName: username,
|
||||
EmailAddress: addresses,
|
||||
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
|
||||
IMAP: &mobileconfig.IMAP{
|
||||
Hostname: hostname,
|
||||
Port: imapPort,
|
||||
TLS: imapSSL,
|
||||
Username: username,
|
||||
Password: string(password),
|
||||
},
|
||||
SMTP: &mobileconfig.SMTP{
|
||||
Hostname: hostname,
|
||||
Port: smtpPort,
|
||||
TLS: smtpSSL,
|
||||
Username: username,
|
||||
Password: string(password),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func saveConfigTemporarily(mc *mobileconfig.Config) (fname string, err error) {
|
||||
dir, err := os.MkdirTemp("", "protonmail-autoconfig")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the temporary file is deleted.
|
||||
go func() {
|
||||
<-time.After(10 * time.Minute)
|
||||
_ = os.RemoveAll(dir)
|
||||
}()
|
||||
|
||||
// Make sure the file is only readable for the current user.
|
||||
fname = filepath.Clean(filepath.Join(dir, "protonmail.mobileconfig"))
|
||||
f, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE, 0o600) //nolint:gosec
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = mc.WriteOut(f); err != nil {
|
||||
_ = f.Close()
|
||||
return
|
||||
}
|
||||
_ = f.Close()
|
||||
|
||||
return
|
||||
}
|
||||
70
internal/config/cache/cache.go
vendored
70
internal/config/cache/cache.go
vendored
@ -1,70 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package cache provides access to contents inside a cache directory.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/files"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
dir, version string
|
||||
}
|
||||
|
||||
func New(dir, version string) (*Cache, error) {
|
||||
if err := os.MkdirAll(filepath.Join(dir, version), 0o700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Cache{
|
||||
dir: dir,
|
||||
version: version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetDBDir returns folder for db files.
|
||||
func (c *Cache) GetDBDir() string {
|
||||
return c.getCurrentCacheDir()
|
||||
}
|
||||
|
||||
// GetDefaultMessageCacheDir returns folder for cached messages files.
|
||||
func (c *Cache) GetDefaultMessageCacheDir() string {
|
||||
return filepath.Join(c.getCurrentCacheDir(), "messages")
|
||||
}
|
||||
|
||||
// GetIMAPCachePath returns path to file with IMAP status.
|
||||
func (c *Cache) GetIMAPCachePath() string {
|
||||
return filepath.Join(c.getCurrentCacheDir(), "user_info.json")
|
||||
}
|
||||
|
||||
// GetTransferDir returns folder for import-export rules files.
|
||||
func (c *Cache) GetTransferDir() string {
|
||||
return c.getCurrentCacheDir()
|
||||
}
|
||||
|
||||
// RemoveOldVersions removes any cache dirs that are not the current version.
|
||||
func (c *Cache) RemoveOldVersions() error {
|
||||
return files.Remove(c.dir).Except(c.getCurrentCacheDir()).Do()
|
||||
}
|
||||
|
||||
func (c *Cache) getCurrentCacheDir() string {
|
||||
return filepath.Join(c.dir, c.version)
|
||||
}
|
||||
70
internal/config/cache/cache_test.go
vendored
70
internal/config/cache/cache_test.go
vendored
@ -1,70 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRemoveOldVersions(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "test-cache")
|
||||
require.NoError(t, err)
|
||||
|
||||
cache, err := New(dir, "c4")
|
||||
require.NoError(t, err)
|
||||
|
||||
createFilesInDir(t, dir,
|
||||
"unexpected1.txt",
|
||||
"c1/unexpected1.txt",
|
||||
"c2/unexpected2.txt",
|
||||
"c3/unexpected3.txt",
|
||||
"something.txt",
|
||||
)
|
||||
|
||||
require.DirExists(t, filepath.Join(dir, "c4"))
|
||||
require.FileExists(t, filepath.Join(dir, "unexpected1.txt"))
|
||||
require.FileExists(t, filepath.Join(dir, "c1", "unexpected1.txt"))
|
||||
require.FileExists(t, filepath.Join(dir, "c2", "unexpected2.txt"))
|
||||
require.FileExists(t, filepath.Join(dir, "c3", "unexpected3.txt"))
|
||||
require.FileExists(t, filepath.Join(dir, "something.txt"))
|
||||
|
||||
assert.NoError(t, cache.RemoveOldVersions())
|
||||
|
||||
assert.DirExists(t, filepath.Join(dir, "c4"))
|
||||
assert.NoFileExists(t, filepath.Join(dir, "unexpected1.txt"))
|
||||
assert.NoFileExists(t, filepath.Join(dir, "c1", "unexpected1.txt"))
|
||||
assert.NoFileExists(t, filepath.Join(dir, "c2", "unexpected2.txt"))
|
||||
assert.NoFileExists(t, filepath.Join(dir, "c3", "unexpected3.txt"))
|
||||
assert.NoFileExists(t, filepath.Join(dir, "something.txt"))
|
||||
}
|
||||
|
||||
func createFilesInDir(t *testing.T, dir string, files ...string) {
|
||||
for _, target := range files {
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(dir, target)), 0o700))
|
||||
|
||||
f, err := os.Create(filepath.Join(dir, target))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
}
|
||||
}
|
||||
@ -1,152 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type keyValueStore struct {
|
||||
cache map[string]string
|
||||
path string
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
|
||||
// newKeyValueStore returns loaded preferences.
|
||||
func newKeyValueStore(path string) *keyValueStore {
|
||||
p := &keyValueStore{
|
||||
path: path,
|
||||
lock: &sync.RWMutex{},
|
||||
}
|
||||
if err := p.load(); err != nil {
|
||||
logrus.WithError(err).Warn("Cannot load preferences file, creating new one")
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *keyValueStore) load() error {
|
||||
if p.cache != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
p.cache = map[string]string{}
|
||||
|
||||
f, err := os.Open(p.path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close() //nolint:errcheck,gosec
|
||||
|
||||
return json.NewDecoder(f).Decode(&p.cache)
|
||||
}
|
||||
|
||||
func (p *keyValueStore) save() error {
|
||||
if p.cache == nil {
|
||||
return errors.New("cannot save preferences: cache is nil")
|
||||
}
|
||||
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
b, err := json.MarshalIndent(p.cache, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(p.path, b, 0o600)
|
||||
}
|
||||
|
||||
func (p *keyValueStore) setDefault(key, value string) {
|
||||
if p.Get(key) == "" {
|
||||
p.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *keyValueStore) Get(key string) string {
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
|
||||
return p.cache[key]
|
||||
}
|
||||
|
||||
func (p *keyValueStore) GetBool(key string) bool {
|
||||
return p.Get(key) == "true"
|
||||
}
|
||||
|
||||
func (p *keyValueStore) GetInt(key string) int {
|
||||
if p.Get(key) == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
value, err := strconv.Atoi(p.Get(key))
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Cannot parse int")
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func (p *keyValueStore) GetFloat64(key string) float64 {
|
||||
if p.Get(key) == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
value, err := strconv.ParseFloat(p.Get(key), 64)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Cannot parse float64")
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func (p *keyValueStore) Set(key, value string) {
|
||||
p.lock.Lock()
|
||||
p.cache[key] = value
|
||||
p.lock.Unlock()
|
||||
|
||||
if err := p.save(); err != nil {
|
||||
logrus.WithError(err).Warn("Cannot save preferences")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *keyValueStore) SetBool(key string, value bool) {
|
||||
if value {
|
||||
p.Set(key, "true")
|
||||
} else {
|
||||
p.Set(key, "false")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *keyValueStore) SetInt(key string, value int) {
|
||||
p.Set(key, strconv.Itoa(value))
|
||||
}
|
||||
|
||||
func (p *keyValueStore) SetFloat64(key string, value float64) {
|
||||
p.Set(key, fmt.Sprintf("%v", value))
|
||||
}
|
||||
@ -1,142 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package settings
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLoadNoKeyValueStore(t *testing.T) {
|
||||
r := require.New(t)
|
||||
pref, clean := newTestEmptyKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
r.Equal("", pref.Get("key"))
|
||||
}
|
||||
|
||||
func TestLoadBadKeyValueStore(t *testing.T) {
|
||||
r := require.New(t)
|
||||
path, clean := newTmpFile(r)
|
||||
defer clean()
|
||||
|
||||
r.NoError(ioutil.WriteFile(path, []byte("{\"key\":\"MISSING_QUOTES"), 0o700))
|
||||
pref := newKeyValueStore(path)
|
||||
r.Equal("", pref.Get("key"))
|
||||
}
|
||||
|
||||
func TestKeyValueStor(t *testing.T) {
|
||||
r := require.New(t)
|
||||
pref, clean := newTestKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
r.Equal("value", pref.Get("str"))
|
||||
r.Equal("42", pref.Get("int"))
|
||||
r.Equal("true", pref.Get("bool"))
|
||||
r.Equal("t", pref.Get("falseBool"))
|
||||
}
|
||||
|
||||
func TestKeyValueStoreGetInt(t *testing.T) {
|
||||
r := require.New(t)
|
||||
pref, clean := newTestKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
r.Equal(0, pref.GetInt("str"))
|
||||
r.Equal(42, pref.GetInt("int"))
|
||||
r.Equal(0, pref.GetInt("bool"))
|
||||
r.Equal(0, pref.GetInt("falseBool"))
|
||||
}
|
||||
|
||||
func TestKeyValueStoreGetBool(t *testing.T) {
|
||||
r := require.New(t)
|
||||
pref, clean := newTestKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
r.Equal(false, pref.GetBool("str"))
|
||||
r.Equal(false, pref.GetBool("int"))
|
||||
r.Equal(true, pref.GetBool("bool"))
|
||||
r.Equal(false, pref.GetBool("falseBool"))
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSetDefault(t *testing.T) {
|
||||
r := require.New(t)
|
||||
pref, clean := newTestEmptyKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
pref.setDefault("key", "value")
|
||||
pref.setDefault("key", "othervalue")
|
||||
r.Equal("value", pref.Get("key"))
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSet(t *testing.T) {
|
||||
r := require.New(t)
|
||||
pref, clean := newTestEmptyKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
pref.Set("str", "value")
|
||||
checkSavedKeyValueStore(r, pref.path, "{\n\t\"str\": \"value\"\n}")
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSetInt(t *testing.T) {
|
||||
r := require.New(t)
|
||||
pref, clean := newTestEmptyKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
pref.SetInt("int", 42)
|
||||
checkSavedKeyValueStore(r, pref.path, "{\n\t\"int\": \"42\"\n}")
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSetBool(t *testing.T) {
|
||||
r := require.New(t)
|
||||
pref, clean := newTestEmptyKeyValueStore(r)
|
||||
defer clean()
|
||||
|
||||
pref.SetBool("trueBool", true)
|
||||
pref.SetBool("falseBool", false)
|
||||
checkSavedKeyValueStore(r, pref.path, "{\n\t\"falseBool\": \"false\",\n\t\"trueBool\": \"true\"\n}")
|
||||
}
|
||||
|
||||
func newTmpFile(r *require.Assertions) (path string, clean func()) {
|
||||
tmpfile, err := ioutil.TempFile("", "pref.*.json")
|
||||
r.NoError(err)
|
||||
defer r.NoError(tmpfile.Close())
|
||||
|
||||
return tmpfile.Name(), func() {
|
||||
r.NoError(os.Remove(tmpfile.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
func newTestEmptyKeyValueStore(r *require.Assertions) (*keyValueStore, func()) {
|
||||
path, clean := newTmpFile(r)
|
||||
return newKeyValueStore(path), clean
|
||||
}
|
||||
|
||||
func newTestKeyValueStore(r *require.Assertions) (*keyValueStore, func()) {
|
||||
path, clean := newTmpFile(r)
|
||||
r.NoError(ioutil.WriteFile(path, []byte("{\"str\":\"value\",\"int\":\"42\",\"bool\":\"true\",\"falseBool\":\"t\"}"), 0o700))
|
||||
return newKeyValueStore(path), clean
|
||||
}
|
||||
|
||||
func checkSavedKeyValueStore(r *require.Assertions, path, expected string) {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
r.NoError(err)
|
||||
r.Equal(expected, string(data))
|
||||
}
|
||||
@ -1,113 +0,0 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package settings provides access to persistent user settings.
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Keys of preferences in JSON file.
|
||||
const (
|
||||
FirstStartKey = "first_time_start"
|
||||
FirstStartGUIKey = "first_time_start_gui"
|
||||
LastHeartbeatKey = "last_heartbeat"
|
||||
APIPortKey = "user_port_api"
|
||||
IMAPPortKey = "user_port_imap"
|
||||
SMTPPortKey = "user_port_smtp"
|
||||
SMTPSSLKey = "user_ssl_smtp"
|
||||
AllowProxyKey = "allow_proxy"
|
||||
AutostartKey = "autostart"
|
||||
AutoUpdateKey = "autoupdate"
|
||||
CookiesKey = "cookies"
|
||||
ReportOutgoingNoEncKey = "report_outgoing_email_without_encryption"
|
||||
LastVersionKey = "last_used_version"
|
||||
UpdateChannelKey = "update_channel"
|
||||
RolloutKey = "rollout"
|
||||
PreferredKeychainKey = "preferred_keychain"
|
||||
CacheEnabledKey = "cache_enabled"
|
||||
CacheCompressionKey = "cache_compression"
|
||||
CacheLocationKey = "cache_location"
|
||||
CacheMinFreeAbsKey = "cache_min_free_abs"
|
||||
CacheMinFreeRatKey = "cache_min_free_rat"
|
||||
CacheConcurrencyRead = "cache_concurrent_read"
|
||||
CacheConcurrencyWrite = "cache_concurrent_write"
|
||||
IMAPWorkers = "imap_workers"
|
||||
FetchWorkers = "fetch_workers"
|
||||
AttachmentWorkers = "attachment_workers"
|
||||
ColorScheme = "color_scheme"
|
||||
RebrandingMigrationKey = "rebranding_migrated"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
*keyValueStore
|
||||
|
||||
settingsPath string
|
||||
}
|
||||
|
||||
func New(settingsPath string) *Settings {
|
||||
s := &Settings{
|
||||
keyValueStore: newKeyValueStore(filepath.Join(settingsPath, "prefs.json")),
|
||||
settingsPath: settingsPath,
|
||||
}
|
||||
|
||||
s.setDefaultValues()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultIMAPPort = "1143"
|
||||
DefaultSMTPPort = "1025"
|
||||
DefaultAPIPort = "1042"
|
||||
)
|
||||
|
||||
func (s *Settings) setDefaultValues() {
|
||||
s.setDefault(FirstStartKey, "true")
|
||||
s.setDefault(FirstStartGUIKey, "true")
|
||||
s.setDefault(LastHeartbeatKey, fmt.Sprintf("%v", time.Now().YearDay()))
|
||||
s.setDefault(AllowProxyKey, "true")
|
||||
s.setDefault(AutostartKey, "true")
|
||||
s.setDefault(AutoUpdateKey, "true")
|
||||
s.setDefault(ReportOutgoingNoEncKey, "false")
|
||||
s.setDefault(LastVersionKey, "")
|
||||
s.setDefault(UpdateChannelKey, "")
|
||||
s.setDefault(RolloutKey, fmt.Sprintf("%v", rand.Float64())) //nolint:gosec // G404 It is OK to use weak random number generator here
|
||||
s.setDefault(PreferredKeychainKey, "")
|
||||
s.setDefault(CacheEnabledKey, "true")
|
||||
s.setDefault(CacheCompressionKey, "true")
|
||||
s.setDefault(CacheLocationKey, "")
|
||||
s.setDefault(CacheMinFreeAbsKey, "250000000")
|
||||
s.setDefault(CacheMinFreeRatKey, "")
|
||||
s.setDefault(CacheConcurrencyRead, "16")
|
||||
s.setDefault(CacheConcurrencyWrite, "16")
|
||||
s.setDefault(IMAPWorkers, "16")
|
||||
s.setDefault(FetchWorkers, "16")
|
||||
s.setDefault(AttachmentWorkers, "16")
|
||||
s.setDefault(ColorScheme, "")
|
||||
|
||||
s.setDefault(APIPortKey, DefaultAPIPort)
|
||||
s.setDefault(IMAPPortKey, DefaultIMAPPort)
|
||||
s.setDefault(SMTPPortKey, DefaultSMTPPort)
|
||||
|
||||
// By default, stick to STARTTLS. If the user uses catalina+applemail they'll have to change to SSL.
|
||||
s.setDefault(SMTPSSLKey, "false")
|
||||
}
|
||||
@ -18,14 +18,20 @@
|
||||
// Package constants contains variables that are set via ldflags during build.
|
||||
package constants
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
const VendorName = "protonmail"
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
// FullAppName is the full app name (to show to the user).
|
||||
FullAppName = ""
|
||||
|
||||
// Version of the build.
|
||||
Version = ""
|
||||
Version = "0.0.0"
|
||||
|
||||
// Revision is current hash of the build.
|
||||
Revision = ""
|
||||
@ -33,9 +39,43 @@ var (
|
||||
// BuildTime stamp of the build.
|
||||
BuildTime = ""
|
||||
|
||||
// DSNSentry client keys to be able to report crashes to Sentry.
|
||||
DSNSentry = ""
|
||||
|
||||
// BuildVersion is derived from LongVersion and BuildTime.
|
||||
BuildVersion = fmt.Sprintf("%v (%v) %v", Version, Revision, BuildTime)
|
||||
|
||||
// DSNSentry client keys to be able to report crashes to Sentry.
|
||||
DSNSentry = ""
|
||||
)
|
||||
|
||||
const (
|
||||
// AppName is the name of the product appearing in the request headers.
|
||||
AppName = "bridge"
|
||||
|
||||
// UpdateName is the name of the product appearing in the update URL.
|
||||
UpdateName = "bridge"
|
||||
|
||||
// ConfigName determines the name of the location where bridge stores config/cache files.
|
||||
ConfigName = "bridge-v3"
|
||||
|
||||
// KeyChainName is the name of the entry in the OS keychain.
|
||||
KeyChainName = "bridge-v3"
|
||||
|
||||
// Host is the hostname of the bridge server.
|
||||
Host = "127.0.0.1"
|
||||
)
|
||||
|
||||
// nolint:goconst
|
||||
func getAPIOS() string {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return "macos"
|
||||
|
||||
case "linux":
|
||||
return "linux"
|
||||
|
||||
case "windows":
|
||||
return "windows"
|
||||
|
||||
default:
|
||||
return "linux"
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,8 +16,8 @@
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !build_qa
|
||||
// +build !build_qa
|
||||
|
||||
package smtp
|
||||
package constants
|
||||
|
||||
func dumpMessageData([]byte, string) {}
|
||||
// APIHost is our API address.
|
||||
const APIHost = "https://mail-api.proton.me"
|
||||
@ -15,11 +15,17 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build imaptest
|
||||
// +build imaptest
|
||||
//go:build build_qa
|
||||
|
||||
package credentials
|
||||
package constants
|
||||
|
||||
func generatePassword() string {
|
||||
return "abcdefgh12345678"
|
||||
import "os"
|
||||
|
||||
// APIHost is our API address.
|
||||
var APIHost = "https://mail-api.proton.me"
|
||||
|
||||
func init() {
|
||||
if apiHost := os.Getenv("BRIDGE_HOST_URL"); apiHost != "" {
|
||||
APIHost = apiHost
|
||||
}
|
||||
}
|
||||
@ -22,8 +22,5 @@ package constants
|
||||
|
||||
import "time"
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
// UpdateCheckInterval defines how often we check for new version.
|
||||
UpdateCheckInterval = time.Hour //nolint:gochecknoglobals
|
||||
)
|
||||
// UpdateCheckInterval defines how often we check for new version.
|
||||
const UpdateCheckInterval = time.Hour
|
||||
|
||||
@ -22,8 +22,5 @@ package constants
|
||||
|
||||
import "time"
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
// UpdateCheckInterval defines how often we check for new version
|
||||
UpdateCheckInterval = time.Duration(5 * time.Minute)
|
||||
)
|
||||
// UpdateCheckInterval defines how often we check for new version
|
||||
const UpdateCheckInterval = time.Duration(5 * time.Minute)
|
||||
|
||||
@ -15,17 +15,14 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package fakeapi
|
||||
//go:build !build_qa
|
||||
// +build !build_qa
|
||||
|
||||
package constants
|
||||
|
||||
import "fmt"
|
||||
|
||||
type idGenerator int
|
||||
|
||||
func (g *idGenerator) last(prefix string) string {
|
||||
return fmt.Sprintf("%s%d", prefix, *g)
|
||||
}
|
||||
|
||||
func (g *idGenerator) next(prefix string) string {
|
||||
(*g)++
|
||||
return fmt.Sprintf("%s%d", prefix, *g)
|
||||
// AppVersion returns the full rendered version of the app (to be used in request headers).
|
||||
func AppVersion(version string) string {
|
||||
return fmt.Sprintf("%v-%v@%v", getAPIOS(), AppName, version)
|
||||
}
|
||||
34
internal/constants/version_qa.go
Normal file
34
internal/constants/version_qa.go
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build build_qa
|
||||
// +build build_qa
|
||||
|
||||
package constants
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
)
|
||||
|
||||
// AppVersion returns the full rendered version of the app (to be used in request headers).
|
||||
func AppVersion(version string) string {
|
||||
ver, _ := semver.MustParse(version).SetPrerelease("dev")
|
||||
|
||||
return fmt.Sprintf("%v-%v@%v", getAPIOS(), AppName, ver.String())
|
||||
}
|
||||
@ -15,19 +15,23 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package context
|
||||
package constants
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeVersioner struct{}
|
||||
func TestPrereleaseSemver(t *testing.T) {
|
||||
ver, err := semver.MustParse("2.3.0+qa").SetPrerelease("dev")
|
||||
require.NoError(t, err)
|
||||
|
||||
// newFakeVersioner creates an empty versioner just to fulfill Bridge dependencies.
|
||||
func newFakeVersioner() *fakeVersioner {
|
||||
return &fakeVersioner{}
|
||||
}
|
||||
require.Equal(t, "2.3.0-dev+qa", ver.String())
|
||||
|
||||
func (c *fakeVersioner) RemoveOtherVersions(_ *semver.Version) error {
|
||||
return nil
|
||||
ver, err = semver.MustParse("2.3.0-dev+qa").SetPrerelease("dev")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "2.3.0-dev+qa", ver.String())
|
||||
}
|
||||
@ -22,32 +22,30 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
)
|
||||
|
||||
type cookiesByHost map[string][]*http.Cookie
|
||||
|
||||
type Persister interface {
|
||||
GetCookies() ([]byte, error)
|
||||
SetCookies([]byte) error
|
||||
}
|
||||
|
||||
// Jar implements http.CookieJar by wrapping the standard library's cookiejar.Jar.
|
||||
// The jar uses a pantry to load cookies at startup and save cookies when set.
|
||||
type Jar struct {
|
||||
jar *cookiejar.Jar
|
||||
settings *settings.Settings
|
||||
cookies cookiesByHost
|
||||
locker sync.Locker
|
||||
jar http.CookieJar
|
||||
|
||||
persister Persister
|
||||
cookies cookiesByHost
|
||||
locker sync.RWMutex
|
||||
}
|
||||
|
||||
func NewCookieJar(s *settings.Settings) (*Jar, error) {
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cookiesByHost, err := loadCookies(s)
|
||||
func NewCookieJar(jar http.CookieJar, persister Persister) (*Jar, error) {
|
||||
cookiesByHost, err := loadCookies(persister)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -62,10 +60,10 @@ func NewCookieJar(s *settings.Settings) (*Jar, error) {
|
||||
}
|
||||
|
||||
return &Jar{
|
||||
jar: jar,
|
||||
settings: s,
|
||||
cookies: cookiesByHost,
|
||||
locker: &sync.Mutex{},
|
||||
jar: jar,
|
||||
|
||||
persister: persister,
|
||||
cookies: cookiesByHost,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -85,38 +83,39 @@ func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
|
||||
}
|
||||
|
||||
func (j *Jar) Cookies(u *url.URL) []*http.Cookie {
|
||||
j.locker.Lock()
|
||||
defer j.locker.Unlock()
|
||||
j.locker.RLock()
|
||||
defer j.locker.RUnlock()
|
||||
|
||||
return j.jar.Cookies(u)
|
||||
}
|
||||
|
||||
// PersistCookies persists the cookies to disk.
|
||||
func (j *Jar) PersistCookies() error {
|
||||
j.locker.Lock()
|
||||
defer j.locker.Unlock()
|
||||
j.locker.RLock()
|
||||
defer j.locker.RUnlock()
|
||||
|
||||
rawCookies, err := json.Marshal(j.cookies)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
j.settings.Set(settings.CookiesKey, string(rawCookies))
|
||||
|
||||
return nil
|
||||
return j.persister.SetCookies(rawCookies)
|
||||
}
|
||||
|
||||
// loadCookies loads all non-expired cookies from disk.
|
||||
func loadCookies(s *settings.Settings) (cookiesByHost, error) {
|
||||
rawCookies := s.Get(settings.CookiesKey)
|
||||
func loadCookies(persister Persister) (cookiesByHost, error) {
|
||||
rawCookies, err := persister.GetCookies()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rawCookies == "" {
|
||||
if len(rawCookies) == 0 {
|
||||
return make(cookiesByHost), nil
|
||||
}
|
||||
|
||||
var cookiesByHost cookiesByHost
|
||||
|
||||
if err := json.Unmarshal([]byte(rawCookies), &cookiesByHost); err != nil {
|
||||
if err := json.Unmarshal(rawCookies, &cookiesByHost); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@ -18,13 +18,16 @@
|
||||
package cookies
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -37,7 +40,7 @@ func TestJarGetSet(t *testing.T) {
|
||||
})
|
||||
defer ts.Close()
|
||||
|
||||
client, _ := getClientWithJar(t, newFakeSettings())
|
||||
client, _ := getClientWithJar(t, newTestPersister(t))
|
||||
|
||||
// Hit a server that sets some cookies.
|
||||
setRes, err := client.Get(ts.URL + "/set")
|
||||
@ -63,7 +66,7 @@ func TestJarLoad(t *testing.T) {
|
||||
defer ts.Close()
|
||||
|
||||
// This will be our "persistent storage" from which the cookie jar should load cookies.
|
||||
s := newFakeSettings()
|
||||
s := newTestPersister(t)
|
||||
|
||||
// This client saves cookies to persistent storage.
|
||||
oldClient, jar := getClientWithJar(t, s)
|
||||
@ -98,7 +101,7 @@ func TestJarExpiry(t *testing.T) {
|
||||
defer ts.Close()
|
||||
|
||||
// This will be our "persistent storage" from which the cookie jar should load cookies.
|
||||
s := newFakeSettings()
|
||||
s := newTestPersister(t)
|
||||
|
||||
// This client saves cookies to persistent storage.
|
||||
oldClient, jar1 := getClientWithJar(t, s)
|
||||
@ -122,9 +125,12 @@ func TestJarExpiry(t *testing.T) {
|
||||
// Save the cookies (expired ones were cleared out).
|
||||
require.NoError(t, jar2.PersistCookies())
|
||||
|
||||
assert.Contains(t, s.Get(settings.CookiesKey), "TestName1")
|
||||
assert.NotContains(t, s.Get(settings.CookiesKey), "TestName2")
|
||||
assert.Contains(t, s.Get(settings.CookiesKey), "TestName3")
|
||||
cookies, err := s.GetCookies()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, string(cookies), "TestName1")
|
||||
assert.NotContains(t, string(cookies), "TestName2")
|
||||
assert.Contains(t, string(cookies), "TestName3")
|
||||
}
|
||||
|
||||
type testCookie struct {
|
||||
@ -132,11 +138,14 @@ type testCookie struct {
|
||||
maxAge int
|
||||
}
|
||||
|
||||
func getClientWithJar(t *testing.T, s *settings.Settings) (*http.Client, *Jar) {
|
||||
jar, err := NewCookieJar(s)
|
||||
func getClientWithJar(t *testing.T, persister Persister) (*http.Client, *Jar) {
|
||||
jar, err := cookiejar.New(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &http.Client{Jar: jar}, jar
|
||||
wrapper, err := NewCookieJar(jar, persister)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &http.Client{Jar: wrapper}, wrapper
|
||||
}
|
||||
|
||||
func getTestServer(t *testing.T, wantCookies []testCookie) *httptest.Server {
|
||||
@ -168,12 +177,26 @@ func getTestServer(t *testing.T, wantCookies []testCookie) *httptest.Server {
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
// newFakeSettings creates a temporary folder for files.
|
||||
func newFakeSettings() *settings.Settings {
|
||||
dir, err := ioutil.TempDir("", "test-settings")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
type testPersister struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func newTestPersister(tb testing.TB) *testPersister {
|
||||
path := filepath.Join(tb.TempDir(), "cookies.json")
|
||||
|
||||
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
|
||||
if err := os.WriteFile(path, []byte{}, 0o600); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return settings.New(dir)
|
||||
return &testPersister{path: path}
|
||||
}
|
||||
|
||||
func (p *testPersister) GetCookies() ([]byte, error) {
|
||||
return os.ReadFile(p.path)
|
||||
}
|
||||
|
||||
func (p *testPersister) SetCookies(rawCookies []byte) error {
|
||||
return os.WriteFile(p.path, rawCookies, 0o600)
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
package crash
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -41,14 +41,11 @@ func (h *Handler) AddRecoveryAction(action RecoveryAction) *Handler {
|
||||
func (h *Handler) HandlePanic() {
|
||||
sentry.SkipDuringUnwind()
|
||||
|
||||
r := recover()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, action := range h.actions {
|
||||
if err := action(r); err != nil {
|
||||
logrus.WithError(err).Error("Failed to execute recovery action")
|
||||
if r := recover(); r != nil {
|
||||
for _, action := range h.actions {
|
||||
if err := action(r); err != nil {
|
||||
logrus.WithError(err).Error("Failed to execute recovery action")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,9 +15,10 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package pmapi
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -25,13 +26,13 @@ import (
|
||||
)
|
||||
|
||||
type TLSDialer interface {
|
||||
DialTLS(network, address string) (conn net.Conn, err error)
|
||||
DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error)
|
||||
}
|
||||
|
||||
// CreateTransportWithDialer creates an http.Transport that uses the given dialer to make TLS connections.
|
||||
func CreateTransportWithDialer(dialer TLSDialer) *http.Transport {
|
||||
return &http.Transport{
|
||||
DialTLS: dialer.DialTLS,
|
||||
DialTLSContext: dialer.DialTLSContext,
|
||||
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
MaxIdleConns: 100,
|
||||
@ -53,26 +54,24 @@ func CreateTransportWithDialer(dialer TLSDialer) *http.Transport {
|
||||
|
||||
// BasicTLSDialer implements TLSDialer.
|
||||
type BasicTLSDialer struct {
|
||||
cfg Config
|
||||
hostURL string
|
||||
}
|
||||
|
||||
// NewBasicTLSDialer returns a new BasicTLSDialer.
|
||||
func NewBasicTLSDialer(cfg Config) *BasicTLSDialer {
|
||||
func NewBasicTLSDialer(hostURL string) *BasicTLSDialer {
|
||||
return &BasicTLSDialer{
|
||||
cfg: cfg,
|
||||
hostURL: hostURL,
|
||||
}
|
||||
}
|
||||
|
||||
// DialTLS returns a connection to the given address using the given network.
|
||||
func (d *BasicTLSDialer) DialTLS(network, address string) (conn net.Conn, err error) {
|
||||
dialer := &net.Dialer{Timeout: 30 * time.Second} // Alternative Routes spec says this should be a 30s timeout.
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
|
||||
// If we are not dialing the standard API then we should skip cert verification checks.
|
||||
if address != d.cfg.HostURL {
|
||||
tlsConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec
|
||||
}
|
||||
|
||||
return tls.DialWithDialer(dialer, network, address, tlsConfig)
|
||||
// DialTLSContext returns a connection to the given address using the given network.
|
||||
func (d *BasicTLSDialer) DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
||||
return (&tls.Dialer{
|
||||
NetDialer: &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
Config: &tls.Config{
|
||||
InsecureSkipVerify: address != d.hostURL, //nolint:gosec
|
||||
},
|
||||
}).DialContext(ctx, network, address)
|
||||
}
|
||||
@ -15,13 +15,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package pmapi
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// TrustedAPIPins contains trusted public keys of the protonmail API and proxies.
|
||||
@ -33,10 +32,16 @@ var TrustedAPIPins = []string{ //nolint:gochecknoglobals
|
||||
`pin-sha256="AfMENBVvOS8MnISprtvyPsjKlPooqh8nMB/pvCrpJpw="`, // cold backup
|
||||
|
||||
// protonmail.com
|
||||
// \todo remove when sure no one is using it.
|
||||
`pin-sha256="8joiNBdqaYiQpKskgtkJsqRxF7zN0C0aqfi8DacknnI="`, // current
|
||||
`pin-sha256="JMI8yrbc6jB1FYGyyWRLFTmDNgIszrNEMGlgy972e7w="`, // hot backup
|
||||
`pin-sha256="Iu44zU84EOCZ9vx/vz67/MRVrxF1IO4i4NIa8ETwiIY="`, // cold backup
|
||||
|
||||
// proton.me
|
||||
`pin-sha256="CT56BhOTmj5ZIPgb/xD5mH8rY3BLo/MlhP7oPyJUEDo="`, // current
|
||||
`pin-sha256="35Dx28/uzN3LeltkCBQ8RHK0tlNSa2kCpCRGNp34Gxc="`, // hot backup
|
||||
`pin-sha256="qYIukVc63DEITct8sFT7ebIq5qsWmuscaIKeJx+5J5A="`, // col backup
|
||||
|
||||
// proxies
|
||||
`pin-sha256="EU6TS9MO0L/GsDHvVc9D5fChYLNy5JdGYpJw0ccgetM="`, // main
|
||||
`pin-sha256="iKPIHPnDNqdkvOnTClQ8zQAIKG0XavaPkcEo0LBAABA="`, // backup 1
|
||||
@ -50,36 +55,37 @@ const TLSReportURI = "https://reports.protonmail.ch/reports/tls"
|
||||
// PinningTLSDialer wraps a TLSDialer to check fingerprints after connecting and
|
||||
// to report errors if the fingerprint check fails.
|
||||
type PinningTLSDialer struct {
|
||||
dialer TLSDialer
|
||||
|
||||
// pinChecker is used to check TLS keys of connections.
|
||||
pinChecker *pinChecker
|
||||
|
||||
reporter *tlsReporter
|
||||
|
||||
// tlsIssueNotifier is used to notify something when there is a TLS issue.
|
||||
tlsIssueNotifier func()
|
||||
|
||||
// A logger for logging messages.
|
||||
log logrus.FieldLogger
|
||||
dialer TLSDialer
|
||||
pinChecker PinChecker
|
||||
reporter Reporter
|
||||
tlsIssueCh chan struct{}
|
||||
}
|
||||
|
||||
// NewPinningTLSDialer constructs a new dialer which only returns tcp connections to servers
|
||||
// Reporter is used to report TLS issues.
|
||||
type Reporter interface {
|
||||
ReportCertIssue(reportURI, host, port string, state tls.ConnectionState)
|
||||
}
|
||||
|
||||
// PinChecker is used to check TLS keys of connections.
|
||||
type PinChecker interface {
|
||||
CheckCertificate(conn net.Conn) error
|
||||
}
|
||||
|
||||
// NewPinningTLSDialer constructs a new dialer which only returns TCP connections to servers
|
||||
// which present known certificates.
|
||||
// If enabled, it reports any invalid certificates it finds.
|
||||
func NewPinningTLSDialer(cfg Config, dialer TLSDialer) *PinningTLSDialer {
|
||||
// It checks pins using the given pinChecker and reports issues using the given reporter.
|
||||
func NewPinningTLSDialer(dialer TLSDialer, reporter Reporter, pinChecker PinChecker) *PinningTLSDialer {
|
||||
return &PinningTLSDialer{
|
||||
dialer: dialer,
|
||||
pinChecker: newPinChecker(TrustedAPIPins),
|
||||
reporter: newTLSReporter(cfg, TrustedAPIPins),
|
||||
tlsIssueNotifier: cfg.TLSIssueHandler,
|
||||
log: logrus.WithField("pkg", "pmapi/tls-pinning"),
|
||||
dialer: dialer,
|
||||
pinChecker: pinChecker,
|
||||
reporter: reporter,
|
||||
tlsIssueCh: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// DialTLS dials the given network/address, returning an error if the certificates don't match the trusted pins.
|
||||
func (p *PinningTLSDialer) DialTLS(network, address string) (net.Conn, error) {
|
||||
conn, err := p.dialer.DialTLS(network, address)
|
||||
// DialTLSContext dials the given network/address, returning an error if the certificates don't match the trusted pins.
|
||||
func (p *PinningTLSDialer) DialTLSContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
conn, err := p.dialer.DialTLSContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -89,22 +95,20 @@ func (p *PinningTLSDialer) DialTLS(network, address string) (net.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := p.pinChecker.checkCertificate(conn); err != nil {
|
||||
if p.tlsIssueNotifier != nil {
|
||||
go p.tlsIssueNotifier()
|
||||
if err := p.pinChecker.CheckCertificate(conn); err != nil {
|
||||
if tlsConn, ok := conn.(*tls.Conn); ok && p.reporter != nil {
|
||||
p.reporter.ReportCertIssue(TLSReportURI, host, port, tlsConn.ConnectionState())
|
||||
}
|
||||
|
||||
if tlsConn, ok := conn.(*tls.Conn); ok && p.reporter != nil {
|
||||
p.reporter.reportCertIssue(
|
||||
TLSReportURI,
|
||||
host,
|
||||
port,
|
||||
tlsConn.ConnectionState(),
|
||||
)
|
||||
}
|
||||
p.tlsIssueCh <- struct{}{}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// GetTLSIssueCh returns a channel which notifies when a TLS issue is reported.
|
||||
func (p *PinningTLSDialer) GetTLSIssueCh() <-chan struct{} {
|
||||
return p.tlsIssueCh
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user