mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-22 18:16:43 +00:00
Other: Fix flaky cookies test
This commit is contained in:
@ -2,9 +2,13 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"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/focus"
|
||||
bridgeCLI "github.com/ProtonMail/proton-bridge/v2/internal/frontend/cli"
|
||||
@ -12,8 +16,10 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/restarter"
|
||||
"github.com/pkg/profile"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
@ -93,33 +99,66 @@ func run(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start CPU profile if requested.
|
||||
if c.Bool(flagCPUProfile) {
|
||||
p := profile.Start(profile.CPUProfile, profile.ProfilePath("."))
|
||||
defer p.Stop()
|
||||
}
|
||||
|
||||
// Start memory profile if requested.
|
||||
if c.Bool(flagMemProfile) {
|
||||
p := profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath("."))
|
||||
defer p.Stop()
|
||||
}
|
||||
|
||||
// Create the restarter.
|
||||
restarter := restarter.New()
|
||||
defer restarter.Restart()
|
||||
|
||||
// Create a user agent that will be used for all requests.
|
||||
identifier := useragent.New()
|
||||
|
||||
// Create a crash handler that will send crash reports to sentry.
|
||||
crashHandler := crash.NewHandler(
|
||||
sentry.NewReporter(constants.FullAppName, constants.Version, identifier).ReportException,
|
||||
crash.ShowErrorNotification(constants.FullAppName),
|
||||
func(r interface{}) error { restarter.Set(true, true); return nil },
|
||||
)
|
||||
defer crashHandler.HandlePanic()
|
||||
// Create a new Sentry client that will be used to report crashes etc.
|
||||
reporter := sentry.NewReporter(constants.FullAppName, constants.Version, identifier)
|
||||
|
||||
// Run with profiling if requested.
|
||||
return withProfiler(c, func() error {
|
||||
// Restart the app if requested.
|
||||
return withRestarter(func(restarter *restarter.Restarter) error {
|
||||
// Handle crashes with various actions.
|
||||
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler) error {
|
||||
// Load the locations where we store our files.
|
||||
return withLocations(func(locations *locations.Locations) error {
|
||||
// Initialize the logging.
|
||||
if err := initLogging(c, locations, crashHandler); err != nil {
|
||||
return fmt.Errorf("could not initialize logging: %w", err)
|
||||
}
|
||||
|
||||
// Unlock the encrypted vault.
|
||||
return withVault(locations, func(vault *vault.Vault, insecure, corrupt bool) error {
|
||||
// Load the cookies from the vault.
|
||||
return withCookieJar(vault, func(cookieJar http.CookieJar) error {
|
||||
// Create a new bridge instance.
|
||||
return withBridge(c, locations, identifier, reporter, vault, cookieJar, func(b *bridge.Bridge) 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)
|
||||
}
|
||||
|
||||
switch {
|
||||
case c.Bool(flagCLI):
|
||||
return bridgeCLI.New(b).Loop()
|
||||
|
||||
case c.Bool(flagNonInteractive):
|
||||
select {}
|
||||
|
||||
default:
|
||||
service, err := grpc.NewService(crashHandler, restarter, locations, b, !c.Bool(flagNoWindow))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create service: %w", err)
|
||||
}
|
||||
|
||||
return service.Loop()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func withLocations(fn func(*locations.Locations) error) error {
|
||||
// Create a locations provider to determine where to store our files.
|
||||
provider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, constants.ConfigName))
|
||||
if err != nil {
|
||||
@ -129,32 +168,67 @@ func run(c *cli.Context) error {
|
||||
// Create a new locations object that will be used to provide paths to store files.
|
||||
locations := locations.New(provider, constants.ConfigName)
|
||||
|
||||
// Initialize the logging.
|
||||
if err := initLogging(c, locations, crashHandler); err != nil {
|
||||
return fmt.Errorf("could not initialize logging: %w", err)
|
||||
}
|
||||
// TODO: Add teardown actions (removing the lock file, etc.)
|
||||
|
||||
// Create the bridge.
|
||||
bridge, err := newBridge(c, locations, identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create bridge: %w", err)
|
||||
}
|
||||
defer bridge.Close(c.Context)
|
||||
|
||||
// Start the frontend.
|
||||
switch {
|
||||
case c.Bool(flagCLI):
|
||||
return bridgeCLI.New(bridge).Loop()
|
||||
|
||||
case c.Bool(flagNonInteractive):
|
||||
select {}
|
||||
|
||||
default:
|
||||
service, err := grpc.NewService(crashHandler, restarter, locations, bridge, !c.Bool(flagNoWindow))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create service: %w", err)
|
||||
}
|
||||
|
||||
return service.Loop()
|
||||
}
|
||||
return fn(locations)
|
||||
}
|
||||
|
||||
func withProfiler(c *cli.Context, fn func() error) error {
|
||||
// Start CPU profile if requested.
|
||||
if c.Bool(flagCPUProfile) {
|
||||
defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop()
|
||||
}
|
||||
|
||||
// Start memory profile if requested.
|
||||
if c.Bool(flagMemProfile) {
|
||||
defer profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath(".")).Stop()
|
||||
}
|
||||
|
||||
return fn()
|
||||
}
|
||||
|
||||
func withRestarter(fn func(*restarter.Restarter) error) error {
|
||||
restarter := restarter.New()
|
||||
defer restarter.Restart()
|
||||
|
||||
return fn(restarter)
|
||||
}
|
||||
|
||||
func withCrashHandler(restarter *restarter.Restarter, reporter *sentry.Reporter, fn func(*crash.Handler) error) error {
|
||||
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(r any) error { restarter.Set(true, true); return nil })
|
||||
|
||||
return fn(crashHandler)
|
||||
}
|
||||
|
||||
func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
|
||||
// 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() {
|
||||
if err := persister.PersistCookies(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to persist cookies")
|
||||
}
|
||||
}()
|
||||
|
||||
return fn(persister)
|
||||
}
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/urfave/cli/v2"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
@ -11,22 +10,30 @@ import (
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/certs"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/dialer"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/versioner"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/keychain"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/slices"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const vaultSecretName = "bridge-vault-key"
|
||||
|
||||
func newBridge(c *cli.Context, locations *locations.Locations, identifier *useragent.UserAgent) (*bridge.Bridge, error) {
|
||||
// withBridge creates creates and tears down the bridge.
|
||||
func withBridge(
|
||||
c *cli.Context,
|
||||
locations *locations.Locations,
|
||||
identifier *useragent.UserAgent,
|
||||
reporter *sentry.Reporter,
|
||||
vault *vault.Vault,
|
||||
cookieJar http.CookieJar,
|
||||
fn func(*bridge.Bridge) error,
|
||||
) error {
|
||||
// Create the underlying dialer used by the bridge.
|
||||
// It only connects to trusted servers and reports any untrusted servers it finds.
|
||||
pinningDialer := dialer.NewPinningTLSDialer(
|
||||
@ -41,145 +48,55 @@ func newBridge(c *cli.Context, locations *locations.Locations, identifier *usera
|
||||
// Create the autostarter.
|
||||
autostarter, err := newAutostarter()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create autostarter: %w", err)
|
||||
return fmt.Errorf("could not create autostarter: %w", err)
|
||||
}
|
||||
|
||||
// Create the update installer.
|
||||
updater, err := newUpdater(locations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create updater: %w", err)
|
||||
return fmt.Errorf("could not create updater: %w", err)
|
||||
}
|
||||
|
||||
// Get the current bridge version.
|
||||
version, err := semver.NewVersion(constants.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create version: %w", err)
|
||||
}
|
||||
|
||||
// Create the encVault.
|
||||
encVault, insecure, corrupt, err := newVault(locations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create vault: %w", err)
|
||||
} else if insecure {
|
||||
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
|
||||
} else if corrupt {
|
||||
logrus.Warn("The vault is corrupt and has been wiped")
|
||||
}
|
||||
|
||||
// Install the certificates if needed.
|
||||
if installed := encVault.GetCertsInstalled(); !installed {
|
||||
if err := certs.NewInstaller().InstallCert(encVault.GetBridgeTLSCert()); err != nil {
|
||||
return nil, fmt.Errorf("failed to install certs: %w", err)
|
||||
}
|
||||
|
||||
if err := encVault.SetCertsInstalled(true); err != nil {
|
||||
return nil, fmt.Errorf("failed to set certs installed: %w", err)
|
||||
}
|
||||
|
||||
if err := encVault.SetCertsInstalled(true); err != nil {
|
||||
return nil, fmt.Errorf("could not set certs installed: %w", err)
|
||||
}
|
||||
return fmt.Errorf("could not create version: %w", err)
|
||||
}
|
||||
|
||||
// Create a new bridge.
|
||||
bridge, err := bridge.New(
|
||||
constants.APIHost,
|
||||
// The app stuff.
|
||||
locations,
|
||||
encVault,
|
||||
vault,
|
||||
autostarter,
|
||||
updater,
|
||||
version,
|
||||
|
||||
// The API stuff.
|
||||
constants.APIHost,
|
||||
cookieJar,
|
||||
identifier,
|
||||
pinningDialer,
|
||||
dialer.CreateTransportWithDialer(proxyDialer),
|
||||
proxyDialer,
|
||||
autostarter,
|
||||
updater,
|
||||
version,
|
||||
|
||||
// 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 nil, fmt.Errorf("could not create bridge: %w", err)
|
||||
return fmt.Errorf("could not create bridge: %w", err)
|
||||
}
|
||||
|
||||
// If the vault could not be loaded properly, push errors to the bridge.
|
||||
switch {
|
||||
case insecure:
|
||||
bridge.PushError(vault.ErrInsecure)
|
||||
|
||||
case corrupt:
|
||||
bridge.PushError(vault.ErrCorrupt)
|
||||
}
|
||||
|
||||
return bridge, nil
|
||||
}
|
||||
|
||||
func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error) {
|
||||
var insecure bool
|
||||
|
||||
vaultDir, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not get vault dir: %w", err)
|
||||
}
|
||||
|
||||
var vaultKey []byte
|
||||
|
||||
if key, err := getVaultKey(vaultDir); err != nil {
|
||||
insecure = true
|
||||
} else {
|
||||
vaultKey = key
|
||||
}
|
||||
|
||||
gluonDir, err := locations.ProvideGluonPath()
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not provide gluon path: %w", err)
|
||||
}
|
||||
|
||||
vault, corrupt, err := vault.New(vaultDir, gluonDir, vaultKey)
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
|
||||
}
|
||||
|
||||
return vault, insecure, corrupt, nil
|
||||
}
|
||||
|
||||
func getVaultKey(vaultDir string) ([]byte, error) {
|
||||
helper, err := vault.GetHelper(vaultDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get keychain helper: %w", err)
|
||||
}
|
||||
|
||||
keychain, err := keychain.NewKeychain(helper, constants.KeyChainName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create keychain: %w", err)
|
||||
}
|
||||
|
||||
secrets, err := keychain.List()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not list keychain: %w", err)
|
||||
}
|
||||
|
||||
if !slices.Contains(secrets, vaultSecretName) {
|
||||
tok, err := crypto.RandomToken(32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not generate random token: %w", err)
|
||||
// Close the bridge when we exit.
|
||||
defer func() {
|
||||
if err := bridge.Close(c.Context); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close bridge")
|
||||
}
|
||||
}()
|
||||
|
||||
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
|
||||
return fn(bridge)
|
||||
}
|
||||
|
||||
func newAutostarter() (*autostart.App, error) {
|
||||
|
||||
110
internal/app/vault.go
Normal file
110
internal/app/vault.go
Normal file
@ -0,0 +1,110 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/certs"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/keychain"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func withVault(locations *locations.Locations, fn func(*vault.Vault, bool, bool) error) error {
|
||||
// Create the encVault.
|
||||
encVault, insecure, corrupt, err := newVault(locations)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create vault: %w", err)
|
||||
}
|
||||
|
||||
// Install the certificates if needed.
|
||||
if installed := encVault.GetCertsInstalled(); !installed {
|
||||
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)
|
||||
}
|
||||
|
||||
if err := encVault.SetCertsInstalled(true); err != nil {
|
||||
return fmt.Errorf("could not set certs installed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add teardown actions (e.g. to close the vault).
|
||||
|
||||
return fn(encVault, insecure, corrupt)
|
||||
}
|
||||
|
||||
func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error) {
|
||||
var insecure bool
|
||||
|
||||
vaultDir, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not get vault dir: %w", err)
|
||||
}
|
||||
|
||||
var vaultKey []byte
|
||||
|
||||
if key, err := getVaultKey(vaultDir); err != nil {
|
||||
insecure = true
|
||||
} else {
|
||||
vaultKey = key
|
||||
}
|
||||
|
||||
gluonDir, err := locations.ProvideGluonPath()
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not provide gluon path: %w", err)
|
||||
}
|
||||
|
||||
vault, corrupt, err := vault.New(vaultDir, gluonDir, vaultKey)
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
|
||||
}
|
||||
|
||||
return vault, insecure, corrupt, nil
|
||||
}
|
||||
|
||||
func getVaultKey(vaultDir string) ([]byte, error) {
|
||||
helper, err := vault.GetHelper(vaultDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get keychain helper: %w", err)
|
||||
}
|
||||
|
||||
keychain, err := keychain.NewKeychain(helper, constants.KeyChainName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create keychain: %w", err)
|
||||
}
|
||||
|
||||
secrets, err := keychain.List()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not list keychain: %w", err)
|
||||
}
|
||||
|
||||
if !slices.Contains(secrets, vaultSecretName) {
|
||||
tok, err := crypto.RandomToken(32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not generate random token: %w", err)
|
||||
}
|
||||
|
||||
if err := keychain.Put(vaultSecretName, base64.StdEncoding.EncodeToString(tok)); err != nil {
|
||||
return nil, fmt.Errorf("could not put keychain item: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
_, keyEnc, err := keychain.Get(vaultSecretName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get keychain item: %w", err)
|
||||
}
|
||||
|
||||
keyDec, err := base64.StdEncoding.DecodeString(keyEnc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not decode keychain item: %w", err)
|
||||
}
|
||||
|
||||
return keyDec, nil
|
||||
}
|
||||
Reference in New Issue
Block a user