// Copyright (c) 2020 Proton Technologies AG // // This file is part of ProtonMail Bridge. // // ProtonMail Bridge is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // ProtonMail Bridge is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . package main /* ___....___ ^^ __..-:'':__:..:__:'':-..__ _.-:__:.-:'': : : :'':-.:__:-._ .':.-: : : : : : : : : :._:'. _ :.': : : : : : : : : : : :'.: _ [ ]: : : : : : : : : : : : : :[ ] [ ]: : : : : : : : : : : : : :[ ] :::::::::[ ]:__:__:__:__:__:__:__:__:__:__:__:__:__:[ ]::::::::::: !!!!!!!!![ ]!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!![ ]!!!!!!!!!!! ^^^^^^^^^[ ]^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^[ ]^^^^^^^^^^^ [ ] [ ] [ ] [ ] jgs [ ] [ ] ~~^_~^~/ \~^-~^~ _~^-~_^~-^~_^~~-^~_~^~-~_~-^~_^/ \~^ ~~_ ^ */ import ( "fmt" "io/ioutil" "os" "os/exec" "path/filepath" "runtime" "runtime/pprof" "strconv" "strings" "github.com/ProtonMail/proton-bridge/internal/api" "github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge/credentials" "github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/frontend" "github.com/ProtonMail/proton-bridge/internal/imap" "github.com/ProtonMail/proton-bridge/internal/pmapifactory" "github.com/ProtonMail/proton-bridge/internal/preferences" "github.com/ProtonMail/proton-bridge/internal/smtp" "github.com/ProtonMail/proton-bridge/pkg/args" "github.com/ProtonMail/proton-bridge/pkg/config" "github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/updates" "github.com/allan-simon/go-singleinstance" "github.com/getsentry/raven-go" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) // cacheVersion is used for cache files such as lock, events, preferences, user_info, db files. // Different number will drop old files and create new ones. const cacheVersion = "c11" // Following variables are set via ldflags during build. var ( // Version of the build. Version = "" //nolint[gochecknoglobals] // Revision is current hash of the build. Revision = "" //nolint[gochecknoglobals] // BuildTime stamp of the build. BuildTime = "" //nolint[gochecknoglobals] // AppShortName to make setup AppShortName = "bridge" //nolint[gochecknoglobals] // DSNSentry client keys to be able to report crashes to Sentry DSNSentry = "" //nolint[gochecknoglobals] ) var ( longVersion = Version + " (" + Revision + ")" //nolint[gochecknoglobals] buildVersion = longVersion + " " + BuildTime //nolint[gochecknoglobals] log = config.GetLogEntry("main") //nolint[gochecknoglobals] // How many crashes in a row. numberOfCrashes = 0 //nolint[gochecknoglobals] // After how many crashes bridge gives up starting. maxAllowedCrashes = 10 //nolint[gochecknoglobals] ) func main() { if err := raven.SetDSN(DSNSentry); err != nil { log.WithError(err).Errorln("Can not setup sentry DSN") } raven.SetRelease(Revision) bridge.UpdateCurrentUserAgent(Version, runtime.GOOS, "", "") args.FilterProcessSerialNumberFromArgs() filterRestartNumberFromArgs() app := cli.NewApp() app.Name = "Protonmail Bridge" app.Version = buildVersion app.Flags = []cli.Flag{ cli.StringFlag{ Name: "log-level, l", Usage: "Set the log level (one of panic, fatal, error, warn, info, debug, debug-client, debug-server)"}, cli.BoolFlag{ Name: "no-window", Usage: "Don't show window after start"}, cli.BoolFlag{ Name: "cli, c", Usage: "Use command line interface"}, cli.BoolFlag{ Name: "noninteractive", Usage: "Start Bridge entirely noninteractively"}, cli.StringFlag{ Name: "version-json, g", Usage: "Generate json version file"}, cli.BoolFlag{ Name: "mem-prof, m", Usage: "Generate memory profile"}, cli.BoolFlag{ Name: "cpu-prof, p", Usage: "Generate CPU profile"}, } app.Usage = "ProtonMail IMAP and SMTP Bridge" app.Action = run // Always log the basic info about current bridge. logrus.SetLevel(logrus.InfoLevel) log.WithField("version", Version). WithField("revision", Revision). WithField("runtime", runtime.GOOS). WithField("build", BuildTime). WithField("args", os.Args). WithField("appLong", app.Name). WithField("appShort", AppShortName). Info("Run app") if err := app.Run(os.Args); err != nil { log.Error("Program exited with error: ", err) } } type panicHandler struct { cfg *config.Config err *error // Pointer to error of cli action. } func (ph *panicHandler) HandlePanic() { r := recover() if r == nil { return } config.HandlePanic(ph.cfg, fmt.Sprintf("Recover: %v", r)) frontend.HandlePanic() *ph.err = cli.NewExitError("Panic and restart", 666) numberOfCrashes++ log.Error("Restarting after panic") restartApp() os.Exit(666) } // run initializes and starts everything in a precise order. // // IMPORTANT: ***Read the comments before CHANGING the order *** func run(context *cli.Context) (contextError error) { // nolint[funlen] // We need to have config instance to setup a logs, panic handler, etc ... cfg := config.New(AppShortName, Version, Revision, cacheVersion) // We want to know about any problem. Our PanicHandler calls sentry which is // not dependent on anything else. If that fails, it tries to create crash // report which will not be possible if no folder can be created. That's the // only problem we will not be notified about in any way. panicHandler := &panicHandler{cfg, &contextError} defer panicHandler.HandlePanic() // First we need config and create necessary folder; it's dependency for everything. if err := cfg.CreateDirs(); err != nil { log.Fatal("Cannot create necessary folders: ", err) } // Setup of logs should be as soon as possible to ensure we record every wanted report in the log. logLevel := context.GlobalString("log-level") debugClient, debugServer := config.SetupLog(cfg, logLevel) // Should be called after logs are configured but before preferences are created. migratePreferencesFromC10(cfg) if err := cfg.ClearOldData(); err != nil { log.Error("Cannot clear old data: ", err) } // Doesn't make sense to continue when Bridge was invoked with wrong arguments. // We should tell that to the user before we do anything else. if context.Args().First() != "" { _ = cli.ShowAppHelp(context) return cli.NewExitError("Unknown argument", 4) } // It's safe to get version JSON file even when other instance is running. // (thus we put it before check of presence of other Bridge instance). updates := updates.New(AppShortName, Version, Revision, BuildTime, bridge.ReleaseNotes, bridge.ReleaseFixedBugs, cfg.GetUpdateDir()) if dir := context.GlobalString("version-json"); dir != "" { generateVersionFiles(updates, dir) return nil } // ClearOldData before starting new bridge to do a proper setup. // // IMPORTANT: If you the change position of this you will need to wait // until force-update to be applied on all currently used bridge // versions if err := cfg.ClearOldData(); err != nil { log.Error("Cannot clear old data: ", err) } // GetTLSConfig is needed for IMAP, SMTL and local bridge API (to check second instance). // // This should be called after ClearOldData, in order to re-create the // certificates if clean data will remove them (accidentally or on purpose). tls, err := config.GetTLSConfig(cfg) if err != nil { log.WithError(err).Fatal("Cannot get TLS certificate") } pref := preferences.New(cfg) // Now we can try to proceed with starting the bridge. First we need to ensure // this is the only instance. If not, we will end and focus the existing one. lock, err := singleinstance.CreateLockFile(cfg.GetLockPath()) if err != nil { log.Warn("Bridge is already running") if err := api.CheckOtherInstanceAndFocus(pref.GetInt(preferences.APIPortKey), tls); err != nil { numberOfCrashes = maxAllowedCrashes log.Error("Second instance: ", err) } return cli.NewExitError("Bridge is already running.", 3) } defer lock.Close() //nolint[errcheck] // In case user wants to do CPU or memory profiles... if doCPUProfile := context.GlobalBool("cpu-prof"); doCPUProfile { f, err := os.Create("cpu.pprof") if err != nil { log.Fatal("Could not create CPU profile: ", err) } if err := pprof.StartCPUProfile(f); err != nil { log.Fatal("Could not start CPU profile: ", err) } defer pprof.StopCPUProfile() } if doMemoryProfile := context.GlobalBool("mem-prof"); doMemoryProfile { defer makeMemoryProfile() } // Now we initialize all Bridge parts. log.Debug("Initializing bridge...") eventListener := listener.New() events.SetupEvents(eventListener) credentialsStore, credentialsError := credentials.NewStore() if credentialsError != nil { log.Error("Could not get credentials store: ", credentialsError) } pmapiClientFactory := pmapifactory.New(cfg, eventListener) bridgeInstance := bridge.New(cfg, pref, panicHandler, eventListener, Version, pmapiClientFactory, credentialsStore) imapBackend := imap.NewIMAPBackend(panicHandler, eventListener, cfg, bridgeInstance) smtpBackend := smtp.NewSMTPBackend(panicHandler, eventListener, pref, bridgeInstance) go func() { defer panicHandler.HandlePanic() apiServer := api.NewAPIServer(pref, tls, cfg.GetTLSCertPath(), cfg.GetTLSKeyPath(), eventListener) apiServer.ListenAndServe() }() go func() { defer panicHandler.HandlePanic() imapPort := pref.GetInt(preferences.IMAPPortKey) imapServer := imap.NewIMAPServer(debugClient, debugServer, imapPort, tls, imapBackend, eventListener) imapServer.ListenAndServe() }() go func() { defer panicHandler.HandlePanic() smtpPort := pref.GetInt(preferences.SMTPPortKey) useSSL := pref.GetBool(preferences.SMTPSSLKey) smtpServer := smtp.NewSMTPServer(debugClient || debugServer, smtpPort, useSSL, tls, smtpBackend, eventListener) smtpServer.ListenAndServe() }() // Decide about frontend mode before initializing rest of bridge. var frontendMode string switch { case context.GlobalBool("cli"): frontendMode = "cli" case context.GlobalBool("noninteractive"): frontendMode = "noninteractive" default: frontendMode = "qt" } log.WithField("mode", frontendMode).Debug("Determined frontend mode to use") // If we are starting bridge in noninteractive mode, simply block instead of starting a frontend. if frontendMode == "noninteractive" { <-(make(chan struct{})) return nil } showWindowOnStart := !context.GlobalBool("no-window") frontend := frontend.New(Version, buildVersion, frontendMode, showWindowOnStart, panicHandler, cfg, pref, eventListener, updates, bridgeInstance, smtpBackend) // Last part is to start everything. log.Debug("Starting frontend...") if err := frontend.Loop(credentialsError); err != nil { log.Error("Frontend failed with error: ", err) return cli.NewExitError("Frontend error", 2) } if frontend.IsAppRestarting() { restartApp() } return nil } // migratePreferencesFromC10 will copy preferences from c10 folder to c11. // It will happen only when c10/prefs.json exists and c11/prefs.json not. // No configuration changed between c10 and c11 versions. func migratePreferencesFromC10(cfg *config.Config) { pref10Path := config.New(AppShortName, Version, Revision, "c10").GetPreferencesPath() if _, err := os.Stat(pref10Path); os.IsNotExist(err) { log.WithField("path", pref10Path).Trace("Old preferences does not exist, migration skipped") return } pref11Path := cfg.GetPreferencesPath() if _, err := os.Stat(pref11Path); err == nil { log.WithField("path", pref11Path).Trace("New preferences already exists, migration skipped") return } data, err := ioutil.ReadFile(pref10Path) //nolint[gosec] if err != nil { log.WithError(err).Error("Problem to load old preferences") return } err = ioutil.WriteFile(pref11Path, data, 0644) if err != nil { log.WithError(err).Error("Problem to migrate preferences") return } log.Info("Preferences migrated") } // generateVersionFiles writes a JSON file with details about current build. // Those files are used for upgrading the app. func generateVersionFiles(updates *updates.Updates, dir string) { log.Info("Generating version files") for _, goos := range []string{"windows", "darwin", "linux"} { log.Debug("Generating JSON for ", goos) if err := updates.CreateJSONAndSign(dir, goos); err != nil { log.Error(err) } } } func makeMemoryProfile() { name := "./mem.pprof" f, err := os.Create(name) if err != nil { log.Error("Could not create memory profile: ", err) } if abs, err := filepath.Abs(name); err == nil { name = abs } log.Info("Writing memory profile to ", name) runtime.GC() // get up-to-date statistics if err := pprof.WriteHeapProfile(f); err != nil { log.Error("Could not write memory profile: ", err) } _ = f.Close() } // filterRestartNumberFromArgs removes flag with a number how many restart we already did. // See restartApp how that number is used. func filterRestartNumberFromArgs() { tmp := os.Args[:0] for i, arg := range os.Args { if !strings.HasPrefix(arg, "--restart_") { tmp = append(tmp, arg) continue } var err error numberOfCrashes, err = strconv.Atoi(os.Args[i][10:]) if err != nil { numberOfCrashes = maxAllowedCrashes } } os.Args = tmp } // restartApp starts a new instance in background. func restartApp() { if numberOfCrashes >= maxAllowedCrashes { log.Error("Too many crashes") return } if exeFile, err := os.Executable(); err == nil { arguments := append(os.Args[1:], fmt.Sprintf("--restart_%d", numberOfCrashes)) cmd := exec.Command(exeFile, arguments...) //nolint[gosec] cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin if err := cmd.Start(); err != nil { log.Error("Restart failed: ", err) } } }