diff --git a/internal/app/app.go b/internal/app/app.go index 1af75cfc..d0e3f12b 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -32,6 +32,9 @@ const ( flagNoWindow = "no-window" flagNonInteractive = "non-interactive" + + flagLogIMAP = "log-imap" + flagLogSMTP = "log-smtp" ) const ( @@ -69,6 +72,14 @@ func New() *cli.App { Usage: "Don't show window after start", Hidden: true, }, + &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!)", + }, } app.Action = run @@ -124,7 +135,7 @@ func run(c *cli.Context) error { } // Create the bridge. - bridge, err := newBridge(locations, identifier) + bridge, err := newBridge(c, locations, identifier) if err != nil { return fmt.Errorf("could not create bridge: %w", err) } diff --git a/internal/app/bridge.go b/internal/app/bridge.go index f9ae7e68..e4c75b18 100644 --- a/internal/app/bridge.go +++ b/internal/app/bridge.go @@ -3,6 +3,7 @@ package app import ( "encoding/base64" "fmt" + "github.com/urfave/cli/v2" "os" "runtime" @@ -25,7 +26,7 @@ import ( const vaultSecretName = "bridge-vault-key" -func newBridge(locations *locations.Locations, identifier *useragent.UserAgent) (*bridge.Bridge, error) { +func newBridge(c *cli.Context, locations *locations.Locations, identifier *useragent.UserAgent) (*bridge.Bridge, 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( @@ -92,6 +93,9 @@ func newBridge(locations *locations.Locations, identifier *useragent.UserAgent) autostarter, updater, version, + 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) diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go index 4a4cda29..a7ada2b0 100644 --- a/internal/bridge/bridge.go +++ b/internal/bridge/bridge.go @@ -72,20 +72,27 @@ type Bridge struct { // stopCh is used to stop ongoing goroutines when the bridge is closed. stopCh chan struct{} + + logIMAPClientCommands bool + logIMAPServerCommands bool + logSMTPCommands bool } // New creates a new bridge. func New( - apiURL string, // the URL of the API to use - locator Locator, // the locator to provide paths to store data - vault *vault.Vault, // the bridge's encrypted data store - identifier Identifier, // the identifier to keep track of the user agent - tlsReporter TLSReporter, // the TLS reporter to report TLS errors + apiURL string, // the URL of the API to use + locator Locator, // the locator to provide paths to store data + vault *vault.Vault, // the bridge's encrypted data store + 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 - 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 + proxyCtl ProxyController, // the DoH controller + 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 + logIMAPClientCommands bool, + logIMAPServerCommands bool, + logSMTPCommands bool, ) (*Bridge, error) { if vault.GetProxyAllowed() { proxyCtl.AllowProxy() @@ -116,7 +123,7 @@ func New( return nil, fmt.Errorf("failed to get Gluon directory: %w", err) } - imapServer, err := newIMAPServer(gluonDir, curVersion, tlsConfig) + imapServer, err := newIMAPServer(gluonDir, curVersion, tlsConfig, logIMAPClientCommands, logIMAPServerCommands) if err != nil { return nil, fmt.Errorf("failed to create IMAP server: %w", err) } @@ -126,7 +133,7 @@ func New( return nil, fmt.Errorf("failed to create SMTP backend: %w", err) } - smtpServer, err := newSMTPServer(smtpBackend, tlsConfig) + smtpServer, err := newSMTPServer(smtpBackend, tlsConfig, logSMTPCommands) if err != nil { return nil, fmt.Errorf("failed to create SMTP server: %w", err) } @@ -159,6 +166,10 @@ func New( locator: locator, stopCh: make(chan struct{}), + + logIMAPClientCommands: logIMAPClientCommands, + logIMAPServerCommands: logIMAPServerCommands, + logSMTPCommands: logSMTPCommands, } api.AddStatusObserver(func(status liteapi.Status) { diff --git a/internal/bridge/bridge_test.go b/internal/bridge/bridge_test.go index b39f8d1c..fc534d72 100644 --- a/internal/bridge/bridge_test.go +++ b/internal/bridge/bridge_test.go @@ -426,6 +426,9 @@ func withBridge( mocks.Autostarter, mocks.Updater, v2_3_0, + false, + false, + false, ) require.NoError(t, err) diff --git a/internal/bridge/imap.go b/internal/bridge/imap.go index acb8ab7d..614ba2d4 100644 --- a/internal/bridge/imap.go +++ b/internal/bridge/imap.go @@ -5,6 +5,8 @@ import ( "crypto/tls" "errors" "fmt" + "github.com/ProtonMail/proton-bridge/v2/internal/logging" + "io" "io/fs" "net" "os" @@ -111,7 +113,29 @@ func getGluonDir(encVault *vault.Vault) (string, error) { return encVault.GetGluonDir(), nil } -func newIMAPServer(gluonDir string, version *semver.Version, tlsConfig *tls.Config) (*gluon.Server, error) { +func newIMAPServer(gluonDir string, version *semver.Version, tlsConfig *tls.Config, logIMAPCommandsClient, logImapCommandsServer bool) (*gluon.Server, error) { + var imapClientLog io.Writer + var imapServerLog io.Writer + + if logIMAPCommandsClient || logImapCommandsServer { + log := logrus.WithField("protocol", "IMAP") + log.Warning("================================================") + log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") + log.Warning("================================================") + } + + if logIMAPCommandsClient { + imapClientLog = logging.NewIMAPLogger() + } else { + imapClientLog = io.Discard + } + + if logImapCommandsServer { + imapServerLog = logging.NewIMAPLogger() + } else { + imapClientLog = io.Discard + } + imapServer, err := gluon.New( gluon.WithTLS(tlsConfig), gluon.WithDataDir(gluonDir), @@ -124,8 +148,8 @@ func newIMAPServer(gluonDir string, version *semver.Version, tlsConfig *tls.Conf "TODO", ), gluon.WithLogger( - logrus.StandardLogger().WriterLevel(logrus.TraceLevel), - logrus.StandardLogger().WriterLevel(logrus.TraceLevel), + imapClientLog, + imapServerLog, ), ) if err != nil { diff --git a/internal/bridge/settings.go b/internal/bridge/settings.go index a83d0ca7..082265a3 100644 --- a/internal/bridge/settings.go +++ b/internal/bridge/settings.go @@ -112,7 +112,7 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error return fmt.Errorf("failed to set new gluon dir: %w", err) } - imapServer, err := newIMAPServer(bridge.vault.GetGluonDir(), bridge.curVersion, bridge.tlsConfig) + imapServer, err := newIMAPServer(bridge.vault.GetGluonDir(), bridge.curVersion, bridge.tlsConfig, bridge.logIMAPClientCommands, bridge.logIMAPServerCommands) if err != nil { return fmt.Errorf("failed to create new IMAP server: %w", err) } diff --git a/internal/bridge/smtp.go b/internal/bridge/smtp.go index 818eb4bd..760e7de7 100644 --- a/internal/bridge/smtp.go +++ b/internal/bridge/smtp.go @@ -3,6 +3,7 @@ package bridge import ( "crypto/tls" "fmt" + "github.com/ProtonMail/proton-bridge/v2/internal/logging" "net" "strconv" @@ -48,7 +49,7 @@ func (bridge *Bridge) restartSMTP() error { return err } - smtpServer, err := newSMTPServer(bridge.smtpBackend, bridge.tlsConfig) + smtpServer, err := newSMTPServer(bridge.smtpBackend, bridge.tlsConfig, bridge.logSMTPCommands) if err != nil { return err } @@ -68,13 +69,22 @@ func (bridge *Bridge) closeSMTP() error { return nil } -func newSMTPServer(smtpBackend *smtpBackend, tlsConfig *tls.Config) (*smtp.Server, error) { +func newSMTPServer(smtpBackend *smtpBackend, tlsConfig *tls.Config, shouldLog bool) (*smtp.Server, error) { smtpServer := smtp.NewServer(smtpBackend) smtpServer.TLSConfig = tlsConfig smtpServer.Domain = constants.Host smtpServer.AllowInsecureAuth = true smtpServer.MaxLineLength = 1 << 16 + smtpServer.ErrorLog = logging.NewSMTPLogger() + + if shouldLog { + log := logrus.WithField("protocol", "SMTP") + log.Warning("================================================") + log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") + log.Warning("================================================") + smtpServer.Debug = logging.NewSMTPDebugLogger() + } smtpServer.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server { return sasl.NewLoginServer(func(address, password string) error { diff --git a/internal/logging/imap_logger.go b/internal/logging/imap_logger.go new file mode 100644 index 00000000..fcb4cfe5 --- /dev/null +++ b/internal/logging/imap_logger.go @@ -0,0 +1,16 @@ +package logging + +import "github.com/sirupsen/logrus" + +// IMAPLogger implements the writer interface for Gluon IMAP logs +type IMAPLogger struct { + l *logrus.Entry +} + +func NewIMAPLogger() *IMAPLogger { + return &IMAPLogger{l: logrus.WithField("pkg", "IMAP")} +} + +func (l *IMAPLogger) Write(p []byte) (n int, err error) { + return l.l.WriterLevel(logrus.TraceLevel).Write(p) +} diff --git a/internal/logging/smtp_logger.go b/internal/logging/smtp_logger.go new file mode 100644 index 00000000..02dd7a5e --- /dev/null +++ b/internal/logging/smtp_logger.go @@ -0,0 +1,35 @@ +package logging + +import ( + "github.com/sirupsen/logrus" +) + +// SMTPErrorLogger implements go-smtp/logger interface. +type SMTPErrorLogger struct { + l *logrus.Entry +} + +func NewSMTPLogger() *SMTPErrorLogger { + return &SMTPErrorLogger{l: logrus.WithField("pkg", "SMTP")} +} + +func (s *SMTPErrorLogger) Printf(format string, args ...interface{}) { + s.l.Errorf(format, args...) +} + +func (s *SMTPErrorLogger) Println(args ...interface{}) { + s.l.Errorln(args...) +} + +// SMTPDebugLogger implements the writer interface for debug SMTP logs +type SMTPDebugLogger struct { + l *logrus.Entry +} + +func NewSMTPDebugLogger() *SMTPDebugLogger { + return &SMTPDebugLogger{l: logrus.WithField("pkg", "SMTP")} +} + +func (l *SMTPDebugLogger) Write(p []byte) (n int, err error) { + return l.l.WriterLevel(logrus.TraceLevel).Write(p) +} diff --git a/tests/ctx_bridge_test.go b/tests/ctx_bridge_test.go index 6f25e7d5..e9489aae 100644 --- a/tests/ctx_bridge_test.go +++ b/tests/ctx_bridge_test.go @@ -48,6 +48,9 @@ func (t *testCtx) startBridge() error { t.mocks.Autostarter, t.mocks.Updater, t.version, + false, + false, + false, ) if err != nil { return err