// 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 . package app import ( "fmt" "net/http" "net/http/cookiejar" "os" "path/filepath" "runtime" "github.com/Masterminds/semver/v3" "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/events" "github.com/ProtonMail/proton-bridge/v2/internal/focus" "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/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" ) // 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" ) const ( appUsage = "Proton Mail IMAP and SMTP Bridge" ) func New() *cli.App { 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, }, } 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] } // 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) error { // Load the locations where we store our files. return WithLocations(func(locations *locations.Locations) error { // Initialize logging. return withLogging(c, crashHandler, locations, func() error { // 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 { // 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, 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) }) }) }) }) }) }) }) }) }) } // 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") 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 { // 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("Initializing logging") // 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") // 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 { 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") 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) error) error { logrus.Debug("Creating crash handler") 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 }) return fn(crashHandler) } // 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") // 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) }