diff --git a/internal/imap/server.go b/internal/imap/server.go index 7dfb3caf..a6cecd3c 100644 --- a/internal/imap/server.go +++ b/internal/imap/server.go @@ -23,13 +23,11 @@ import ( "io" "net" "strings" - "sync/atomic" "time" imapid "github.com/ProtonMail/go-imap-id" "github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/config/useragent" - "github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/imap/id" "github.com/ProtonMail/proton-bridge/internal/imap/idle" "github.com/ProtonMail/proton-bridge/internal/imap/uidplus" @@ -43,43 +41,58 @@ import ( "github.com/emersion/go-imap/backend" imapserver "github.com/emersion/go-imap/server" "github.com/emersion/go-sasl" - "github.com/sirupsen/logrus" ) -type imapServer struct { - panicHandler panicHandler - server *imapserver.Server - userAgent *useragent.UserAgent - eventListener listener.Listener - debugClient bool - debugServer bool - port int - isRunning atomic.Value +// Server takes care of IMAP listening serving. It implements serverutil.Server. +type Server struct { + panicHandler panicHandler + userAgent *useragent.UserAgent + debugClient bool + debugServer bool + port int + + server *imapserver.Server + controller serverutil.Controller } // NewIMAPServer constructs a new IMAP server configured with the given options. -func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, port int, tls *tls.Config, imapBackend backend.Backend, userAgent *useragent.UserAgent, eventListener listener.Listener) *imapServer { // nolint[golint] - s := imapserver.New(imapBackend) - s.Addr = fmt.Sprintf("%v:%v", bridge.Host, port) - s.TLSConfig = tls - s.AllowInsecureAuth = true - s.ErrorLog = newServerErrorLogger("server-imap") - s.AutoLogout = 30 * time.Minute - - if debugServer { - fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") - log.Warning("================================================") - log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") - log.Warning("================================================") +func NewIMAPServer( + panicHandler panicHandler, + debugClient, debugServer bool, + port int, + tls *tls.Config, + imapBackend backend.Backend, + userAgent *useragent.UserAgent, + eventListener listener.Listener, +) *Server { + server := &Server{ + panicHandler: panicHandler, + userAgent: userAgent, + debugClient: debugClient, + debugServer: debugServer, + port: port, } + server.server = newGoIMAPServer(tls, imapBackend, server.Address(), userAgent) + server.controller = serverutil.NewController(server, eventListener) + return server +} + +func newGoIMAPServer(tls *tls.Config, backend backend.Backend, address string, userAgent *useragent.UserAgent) *imapserver.Server { + server := imapserver.New(backend) + server.TLSConfig = tls + server.AllowInsecureAuth = true + server.ErrorLog = serverutil.NewServerErrorLogger(serverutil.IMAP) + server.AutoLogout = 30 * time.Minute + server.Addr = address + serverID := imapid.ID{ imapid.FieldName: "ProtonMail Bridge", imapid.FieldVendor: "Proton Technologies AG", imapid.FieldSupportURL: "https://protonmail.com/support", } - s.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server { + server.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server { return sasl.NewLoginServer(func(address, password string) error { user, err := conn.Server().Backend.Login(nil, address, password) if err != nil { @@ -93,7 +106,7 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por }) }) - s.Enable( + server.Enable( idle.NewExtension(), imapmove.NewExtension(), id.NewExtension(serverID, userAgent), @@ -103,87 +116,35 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por uidplus.NewExtension(), ) - server := &imapServer{ - panicHandler: panicHandler, - server: s, - userAgent: userAgent, - eventListener: eventListener, - debugClient: debugClient, - debugServer: debugServer, - port: port, - } - server.isRunning.Store(false) return server } -func (s *imapServer) HandlePanic() { s.panicHandler.HandlePanic() } -func (s *imapServer) IsRunning() bool { return s.isRunning.Load().(bool) } -func (s *imapServer) Port() int { return s.port } +// ListenAndServe will run server and all monitors. +func (s *Server) ListenAndServe() { s.controller.ListenAndServe() } -// ListenAndServe starts the server and keeps it on based on internet -// availability. -func (s *imapServer) ListenAndServe() { - serverutil.ListenAndServe(s, s.eventListener) -} +// Close turns off server and monitors. +func (s *Server) Close() { s.controller.Close() } -// ListenRetryAndServe will start listener. If port is occupied it will try -// again after coolDown time. Once listener is OK it will serve. -func (s *imapServer) ListenRetryAndServe(retries int, retryAfter time.Duration) { - if s.IsRunning() { - return - } - s.isRunning.Store(true) +// Implements serverutil.Server interface. - l := log.WithField("address", s.server.Addr) - l.Info("IMAP server is starting") - listener, err := net.Listen("tcp", s.server.Addr) - if err != nil { - s.isRunning.Store(false) - if retries > 0 { - l.WithError(err).WithField("retries", retries).Warn("IMAP listener failed") - time.Sleep(retryAfter) - s.ListenRetryAndServe(retries-1, retryAfter) - return - } +func (Server) Protocol() serverutil.Protocol { return serverutil.IMAP } +func (s *Server) UseSSL() bool { return false } +func (s *Server) Address() string { return fmt.Sprintf("%s:%d", bridge.Host, s.port) } +func (s *Server) TLSConfig() *tls.Config { return s.server.TLSConfig } +func (s *Server) HandlePanic() { s.panicHandler.HandlePanic() } - l.WithError(err).Error("IMAP listener failed") - s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error()) - return - } +func (s *Server) DebugServer() bool { return s.debugServer } +func (s *Server) DebugClient() bool { return s.debugClient } - err = s.server.Serve(&connListener{ - Listener: listener, - server: s, - userAgent: s.userAgent, - }) - // Serve returns error every time, even after closing the server. - // User shouldn't be notified about error if server shouldn't be running, - // but it should in case it was not closed by `s.Close()`. - if err != nil && s.IsRunning() { - s.isRunning.Store(false) - l.WithError(err).Error("IMAP server failed") - s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error()) - return - } - defer s.server.Close() //nolint[errcheck] +func (s *Server) SetLoggers(localDebug, remoteDebug io.Writer) { + s.server.Debug = imap.NewDebugWriter(localDebug, remoteDebug) - l.Info("IMAP server stopped") -} - -// Stops the server. -func (s *imapServer) Close() { - if !s.IsRunning() { - return - } - s.isRunning.Store(false) - - log.Info("Closing IMAP server") - if err := s.server.Close(); err != nil { - log.WithError(err).Error("Failed to close the connection") + if !s.userAgent.HasClient() { + s.userAgent.SetClient("UnknownClient", "0.0.1") } } -func (s *imapServer) DisconnectUser(address string) { +func (s *Server) DisconnectUser(address string) { log.Info("Disconnecting all open IMAP connections for ", address) s.server.ForEachConn(func(conn imapserver.Conn) { connUser := conn.Context().User @@ -195,60 +156,5 @@ func (s *imapServer) DisconnectUser(address string) { }) } -// connListener sets debug loggers on server containing fields with local -// and remote addresses right after new connection is accepted. -type connListener struct { - net.Listener - - server *imapServer - userAgent *useragent.UserAgent -} - -func (l *connListener) Accept() (net.Conn, error) { - conn, err := l.Listener.Accept() - - if err == nil && (l.server.debugServer || l.server.debugClient) { - debugLog := log - if addr := conn.LocalAddr(); addr != nil { - debugLog = debugLog.WithField("loc", addr.String()) - } - if addr := conn.RemoteAddr(); addr != nil { - debugLog = debugLog.WithField("rem", addr.String()) - } - - var localDebug, remoteDebug io.Writer - if l.server.debugServer { - localDebug = debugLog.WithField("pkg", "imap/server").WriterLevel(logrus.DebugLevel) - } - if l.server.debugClient { - remoteDebug = debugLog.WithField("pkg", "imap/client").WriterLevel(logrus.DebugLevel) - } - - l.server.server.Debug = imap.NewDebugWriter(localDebug, remoteDebug) - } - - if !l.userAgent.HasClient() { - l.userAgent.SetClient("UnknownClient", "0.0.1") - } - - return conn, err -} - -// serverErrorLogger implements go-imap/logger interface. -type serverErrorLogger struct { - tag string -} - -func newServerErrorLogger(tag string) *serverErrorLogger { - return &serverErrorLogger{tag} -} - -func (s *serverErrorLogger) Printf(format string, args ...interface{}) { - err := fmt.Sprintf(format, args...) - log.WithField("pkg", s.tag).Error(err) -} - -func (s *serverErrorLogger) Println(args ...interface{}) { - err := fmt.Sprintln(args...) - log.WithField("pkg", s.tag).Error(err) -} +func (s *Server) Serve(listener net.Listener) error { return s.server.Serve(listener) } +func (s *Server) StopServe() error { return s.server.Close() } diff --git a/internal/serverutil/controller.go b/internal/serverutil/controller.go new file mode 100644 index 00000000..c2ba1dca --- /dev/null +++ b/internal/serverutil/controller.go @@ -0,0 +1,117 @@ +// Copyright (c) 2021 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 serverutil + +import ( + "crypto/tls" + "fmt" + "net" + + "github.com/ProtonMail/proton-bridge/internal/events" + "github.com/ProtonMail/proton-bridge/pkg/listener" + "github.com/sirupsen/logrus" +) + +// Controller will make sure that server is listening and serving and if needed +// users are disconnected. +type Controller interface { + ListenAndServe() + Close() +} + +// NewController return simple server controller. +func NewController(s Server, l listener.Listener) Controller { + log := logrus.WithField("pkg", "serverutil").WithField("protocol", s.Protocol()) + c := &controller{ + server: s, + signals: l, + log: log, + closeDisconnectUsers: make(chan void), + } + + if s.DebugServer() { + fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") + log.Warning("================================================") + log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") + log.Warning("================================================") + } + + return c +} + +type void struct{} + +type controller struct { + server Server + signals listener.Listener + log *logrus.Entry + + closeDisconnectUsers chan void +} + +func (c *controller) Close() { + c.closeDisconnectUsers <- void{} + if err := c.server.StopServe(); err != nil { + c.log.WithError(err).Error("Issue when closing server") + } +} + +// ListenAndServe starts the server and keeps it on based on internet +// availability. It also monitors and disconnect users if requested. +func (c *controller) ListenAndServe() { + go monitorDisconnectedUsers(c.server, c.signals, c.closeDisconnectUsers) + + defer c.server.HandlePanic() + + l := c.log.WithField("useSSL", c.server.UseSSL()). + WithField("address", c.server.Address()) + + var listener net.Listener + var err error + + if c.server.UseSSL() { + listener, err = tls.Listen("tcp", c.server.Address(), c.server.TLSConfig()) + } else { + listener, err = net.Listen("tcp", c.server.Address()) + } + + if err != nil { + l.WithError(err).Error("Cannot start listner.") + c.signals.Emit(events.ErrorEvent, string(c.server.Protocol())+" failed: "+err.Error()) + return + } + + // When starting the Bridge, we don't want to retry to notify user + // quickly about the issue. Very probably retry will not help anyway. + l.Info("Starting server") + err = c.server.Serve(&connListener{listener, c.server}) + l.WithError(err).Debug("GoSMTP not serving") +} + +func monitorDisconnectedUsers(s Server, l listener.Listener, done <-chan void) { + ch := make(chan string) + l.Add(events.CloseConnectionEvent, ch) + for { + select { + case <-done: + return + case address := <-ch: + s.DisconnectUser(address) + } + } +} diff --git a/internal/serverutil/error_logger.go b/internal/serverutil/error_logger.go new file mode 100644 index 00000000..576f2d84 --- /dev/null +++ b/internal/serverutil/error_logger.go @@ -0,0 +1,39 @@ +// Copyright (c) 2021 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 serverutil + +import ( + "github.com/sirupsen/logrus" +) + +// ServerErrorLogger implements go-imap/logger interface. +type ServerErrorLogger struct { + l *logrus.Entry +} + +func NewServerErrorLogger(protocol Protocol) *ServerErrorLogger { + return &ServerErrorLogger{l: logrus.WithField("protocol", protocol)} +} + +func (s *ServerErrorLogger) Printf(format string, args ...interface{}) { + s.l.Errorf(format, args...) +} + +func (s *ServerErrorLogger) Println(args ...interface{}) { + s.l.Errorln(args...) +} diff --git a/internal/serverutil/listener.go b/internal/serverutil/listener.go new file mode 100644 index 00000000..53450205 --- /dev/null +++ b/internal/serverutil/listener.go @@ -0,0 +1,59 @@ +// Copyright (c) 2021 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 serverutil + +import ( + "io" + "net" + + "github.com/sirupsen/logrus" +) + +// connListener sets debug loggers on server containing fields with local +// and remote addresses right after new connection is accepted. +type connListener struct { + net.Listener + + server Server +} + +func (l *connListener) Accept() (net.Conn, error) { + conn, err := l.Listener.Accept() + + if err == nil && (l.server.DebugServer() || l.server.DebugClient()) { + debugLog := logrus.WithField("pkg", l.server.Protocol()) + if addr := conn.LocalAddr(); addr != nil { + debugLog = debugLog.WithField("loc", addr.String()) + } + if addr := conn.RemoteAddr(); addr != nil { + debugLog = debugLog.WithField("rem", addr.String()) + } + + var localDebug, remoteDebug io.Writer + if l.server.DebugServer() { + localDebug = debugLog.WithField("comm", "server").WriterLevel(logrus.DebugLevel) + } + if l.server.DebugClient() { + remoteDebug = debugLog.WithField("comm", "client").WriterLevel(logrus.DebugLevel) + } + + l.server.SetLoggers(localDebug, remoteDebug) + } + + return conn, err +} diff --git a/internal/serverutil/protocol.go b/internal/serverutil/protocol.go new file mode 100644 index 00000000..c551359f --- /dev/null +++ b/internal/serverutil/protocol.go @@ -0,0 +1,26 @@ +// Copyright (c) 2021 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 serverutil + +type Protocol string + +const ( + HTTP = Protocol("HTTP") + IMAP = Protocol("IMAP") + SMTP = Protocol("SMTP") +) diff --git a/internal/serverutil/server.go b/internal/serverutil/server.go index a9865036..e081b032 100644 --- a/internal/serverutil/server.go +++ b/internal/serverutil/server.go @@ -18,32 +18,24 @@ package serverutil import ( - "time" - - "github.com/ProtonMail/proton-bridge/internal/events" - "github.com/ProtonMail/proton-bridge/pkg/listener" + "crypto/tls" + "io" + "net" ) -// Server which can handle disconnected users and lost internet connection. +// Server can handle disconnected users. type Server interface { + Protocol() Protocol + UseSSL() bool + Address() string + TLSConfig() *tls.Config + + DebugServer() bool + DebugClient() bool + SetLoggers(localDebug, remoteDebug io.Writer) + + HandlePanic() DisconnectUser(string) - ListenRetryAndServe(int, time.Duration) -} - -func monitorDisconnectedUsers(s Server, l listener.Listener) { - ch := make(chan string) - l.Add(events.CloseConnectionEvent, ch) - for address := range ch { - s.DisconnectUser(address) - } -} - -// ListenAndServe starts the server and keeps it on based on internet -// availability. It also monitors and disconnect users if requested. -func ListenAndServe(s Server, l listener.Listener) { - go monitorDisconnectedUsers(s, l) - - // When starting the Bridge, we don't want to retry to notify user - // quickly about the issue. Very probably retry will not help anyway. - s.ListenRetryAndServe(0, 0) + Serve(net.Listener) error + StopServe() error } diff --git a/internal/serverutil/test/controller_test.go b/internal/serverutil/test/controller_test.go new file mode 100644 index 00000000..62cab2ec --- /dev/null +++ b/internal/serverutil/test/controller_test.go @@ -0,0 +1,153 @@ +// Copyright (c) 2021 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 test + +import ( + "net/http" + "testing" + "time" + + "github.com/ProtonMail/proton-bridge/internal/events" + "github.com/ProtonMail/proton-bridge/internal/serverutil" + "github.com/ProtonMail/proton-bridge/pkg/listener" + "github.com/stretchr/testify/require" +) + +func setup(t *testing.T) (*require.Assertions, *testServer, listener.Listener, serverutil.Controller) { + r := require.New(t) + s := newTestServer() + l := listener.New() + c := serverutil.NewController(s, l) + + return r, s, l, c +} + +func TestControllerListernServeClose(t *testing.T) { + r, s, l, c := setup(t) + + errorCh := l.ProvideChannel(events.ErrorEvent) + + r.True(s.portIsFree()) + go c.ListenAndServe() + r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond) + + r.NoError(s.ping()) + + r.Nil(s.localDebug) + r.Nil(s.remoteDebug) + + c.Close() + r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond) + + select { + case msg := <-errorCh: + r.Fail("Expected no error but have %q", msg) + case <-time.Tick(100 * time.Millisecond): + break + } +} + +func TestControllerFailOnBusyPort(t *testing.T) { + r, s, l, c := setup(t) + + ocupator := http.Server{Addr: s.Address()} + defer ocupator.Close() //nolint[errcheck] + + go ocupator.ListenAndServe() //nolint[errcheck] + r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond) + + errorCh := l.ProvideChannel(events.ErrorEvent) + go c.ListenAndServe() + + r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond) + + select { + case <-errorCh: + break + case <-time.Tick(time.Second): + r.Fail("Expected error but have none.") + } +} + +func TestControllerCallDisconnectUser(t *testing.T) { + r, s, l, c := setup(t) + + go c.ListenAndServe() + r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond) + r.NoError(s.ping()) + + l.Emit(events.CloseConnectionEvent, "") + r.Eventually(func() bool { return s.calledDisconnected == 1 }, time.Second, 50*time.Millisecond) + + c.Close() + r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond) + + l.Emit(events.CloseConnectionEvent, "") + r.Equal(1, s.calledDisconnected) +} + +func TestDebugClient(t *testing.T) { + r, s, _, c := setup(t) + + s.debugServer = false + s.debugClient = true + + go c.ListenAndServe() + r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond) + r.NoError(s.ping()) + + r.Nil(s.localDebug) + r.NotNil(s.remoteDebug) + + c.Close() + r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond) +} + +func TestDebugServer(t *testing.T) { + r, s, _, c := setup(t) + + s.debugServer = true + s.debugClient = false + + go c.ListenAndServe() + r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond) + r.NoError(s.ping()) + + r.NotNil(s.localDebug) + r.Nil(s.remoteDebug) + + c.Close() + r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond) +} + +func TestDebugBoth(t *testing.T) { + r, s, _, c := setup(t) + + s.debugServer = true + s.debugClient = true + + go c.ListenAndServe() + r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond) + r.NoError(s.ping()) + + r.NotNil(s.localDebug) + r.NotNil(s.remoteDebug) + + c.Close() + r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond) +} diff --git a/internal/serverutil/test/server.go b/internal/serverutil/test/server.go new file mode 100644 index 00000000..f0615c5d --- /dev/null +++ b/internal/serverutil/test/server.go @@ -0,0 +1,88 @@ +// Copyright (c) 2021 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 test + +import ( + "crypto/tls" + "fmt" + "io" + "net" + "net/http" + + "github.com/ProtonMail/proton-bridge/internal/serverutil" + "github.com/ProtonMail/proton-bridge/pkg/ports" +) + +func newTestServer() *testServer { + return &testServer{port: 11188} +} + +type testServer struct { + http http.Server + + useSSL, + debugServer, + debugClient bool + calledDisconnected int + + port int + tls *tls.Config + + localDebug, remoteDebug io.Writer +} + +func (*testServer) Protocol() serverutil.Protocol { return serverutil.HTTP } +func (s *testServer) UseSSL() bool { return s.useSSL } +func (s *testServer) Address() string { return fmt.Sprintf("127.0.0.1:%d", s.port) } +func (s *testServer) TLSConfig() *tls.Config { return s.tls } +func (s *testServer) HandlePanic() {} + +func (s *testServer) DebugServer() bool { return s.debugServer } +func (s *testServer) DebugClient() bool { return s.debugClient } +func (s *testServer) SetLoggers(localDebug, remoteDebug io.Writer) { + s.localDebug = localDebug + s.remoteDebug = remoteDebug +} + +func (s *testServer) DisconnectUser(string) { + s.calledDisconnected++ +} + +func (s *testServer) Serve(l net.Listener) error { + return s.http.Serve(l) +} + +func (s *testServer) StopServe() error { return s.http.Close() } + +func (s *testServer) portIsFree() bool { + return ports.IsPortFree(s.port) +} + +func (s *testServer) portIsOccupied() bool { + return !ports.IsPortFree(s.port) +} + +func (s *testServer) ping() error { + client := &http.Client{} + resp, err := client.Get("http://" + s.Address() + "/ping") + if err != nil { + return err + } + + return resp.Body.Close() +} diff --git a/internal/smtp/server.go b/internal/smtp/server.go index ecb9f2f6..3db68c69 100644 --- a/internal/smtp/server.go +++ b/internal/smtp/server.go @@ -20,72 +20,60 @@ package smtp import ( "crypto/tls" "fmt" + "io" "net" - "sync/atomic" - "time" "github.com/ProtonMail/proton-bridge/internal/bridge" - "github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/serverutil" "github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/emersion/go-sasl" goSMTP "github.com/emersion/go-smtp" - "github.com/sirupsen/logrus" ) // Server is Bridge SMTP server implementation. type Server struct { - panicHandler panicHandler - backend goSMTP.Backend - server *goSMTP.Server - eventListener listener.Listener - debug bool - useSSL bool - port int - tls *tls.Config - isRunning atomic.Value + panicHandler panicHandler + backend goSMTP.Backend + debug bool + useSSL bool + port int + tls *tls.Config + + server *goSMTP.Server + controller serverutil.Controller } // NewSMTPServer returns an SMTP server configured with the given options. -func NewSMTPServer(panicHandler panicHandler, debug bool, port int, useSSL bool, tls *tls.Config, smtpBackend goSMTP.Backend, eventListener listener.Listener) *Server { - if debug { - fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") - log.Warning("================================================") - log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") - log.Warning("================================================") +func NewSMTPServer( + panicHandler panicHandler, + debug bool, port int, useSSL bool, + tls *tls.Config, + smtpBackend goSMTP.Backend, + eventListener listener.Listener, +) *Server { + server := &Server{ + panicHandler: panicHandler, + backend: smtpBackend, + debug: debug, + useSSL: useSSL, + port: port, + tls: tls, } - server := &Server{ - panicHandler: panicHandler, - backend: smtpBackend, - eventListener: eventListener, - debug: debug, - useSSL: useSSL, - port: port, - tls: tls, - } - server.isRunning.Store(false) + server.server = newGoSMTPServer(server) + server.controller = serverutil.NewController(server, eventListener) return server } -func (s *Server) HandlePanic() { s.panicHandler.HandlePanic() } -func (s *Server) IsRunning() bool { return s.isRunning.Load().(bool) } -func (s *Server) Port() int { return s.port } - -func newGoSMTPServer(debug bool, smtpBackend goSMTP.Backend, port int, tls *tls.Config) *goSMTP.Server { - newSMTP := goSMTP.NewServer(smtpBackend) - newSMTP.Addr = fmt.Sprintf("%v:%v", bridge.Host, port) - newSMTP.TLSConfig = tls +func newGoSMTPServer(s *Server) *goSMTP.Server { + newSMTP := goSMTP.NewServer(s.backend) + newSMTP.Addr = s.Address() + newSMTP.TLSConfig = s.tls newSMTP.Domain = bridge.Host + newSMTP.ErrorLog = serverutil.NewServerErrorLogger(serverutil.SMTP) newSMTP.AllowInsecureAuth = true newSMTP.MaxLineLength = 1 << 16 - if debug { - newSMTP.Debug = logrus. - WithField("pkg", "smtp/server"). - WriterLevel(logrus.DebugLevel) - } - newSMTP.EnableAuth(sasl.Login, func(conn *goSMTP.Conn) sasl.Server { return sasl.NewLoginServer(func(address, password string) error { user, err := conn.Server().Backend.Login(nil, address, password) @@ -100,80 +88,24 @@ func newGoSMTPServer(debug bool, smtpBackend goSMTP.Backend, port int, tls *tls. return newSMTP } -// ListenAndServe starts the server and keeps it on based on internet -// availability. -func (s *Server) ListenAndServe() { - serverutil.ListenAndServe(s, s.eventListener) -} +// ListenAndServe will run server and all monitors. +func (s *Server) ListenAndServe() { s.controller.ListenAndServe() } -func (s *Server) ListenRetryAndServe(retries int, retryAfter time.Duration) { - if s.IsRunning() { - return - } - s.isRunning.Store(true) +// Close turns off server and monitors. +func (s *Server) Close() { s.controller.Close() } - s.server = newGoSMTPServer(s.debug, s.backend, s.port, s.tls) +// Implements servertutil.Server interface. - l := log.WithField("useSSL", s.useSSL).WithField("address", s.server.Addr) - l.Info("SMTP server is starting") +func (Server) Protocol() serverutil.Protocol { return serverutil.SMTP } +func (s *Server) UseSSL() bool { return s.useSSL } +func (s *Server) Address() string { return fmt.Sprintf("%s:%d", bridge.Host, s.port) } +func (s *Server) TLSConfig() *tls.Config { return s.tls } +func (s *Server) HandlePanic() { s.panicHandler.HandlePanic() } - var listener net.Listener - var err error - if s.useSSL { - listener, err = tls.Listen("tcp", s.server.Addr, s.server.TLSConfig) - } else { - listener, err = net.Listen("tcp", s.server.Addr) - } - l.WithError(err).Debug("Listener for SMTP created") - if err != nil { - s.isRunning.Store(false) - if retries > 0 { - l.WithError(err).WithField("retries", retries).Warn("SMTP listener failed") - time.Sleep(retryAfter) - s.ListenRetryAndServe(retries-1, retryAfter) - return - } +func (s *Server) DebugServer() bool { return s.debug } +func (s *Server) DebugClient() bool { return s.debug } - l.WithError(err).Error("SMTP listener failed") - s.eventListener.Emit(events.ErrorEvent, "SMTP failed: "+err.Error()) - return - } - - err = s.server.Serve(listener) - l.WithError(err).Debug("GoSMTP not serving") - // Serve returns error every time, even after closing the server. - // User shouldn't be notified about error if server shouldn't be running, - // but it should in case it was not closed by `s.Close()`. - if err != nil && s.IsRunning() { - s.isRunning.Store(false) - l.WithError(err).Error("SMTP server failed") - s.eventListener.Emit(events.ErrorEvent, "SMTP failed: "+err.Error()) - return - } - defer func() { - // Go SMTP server instance can be closed only once. Otherwise - // it returns an error. The error is not export therefore we - // will check the string value. - err := s.server.Close() - if err == nil || err.Error() != "smtp: server already closed" { - l.WithError(err).Warn("Server was not closed") - } - }() - - l.Info("SMTP server closed") -} - -// Close stops the server. -func (s *Server) Close() { - if !s.IsRunning() { - return - } - s.isRunning.Store(false) - - if err := s.server.Close(); err != nil { - log.WithError(err).Error("Cannot close the server") - } -} +func (s *Server) SetLoggers(localDebug, remoteDebug io.Writer) { s.server.Debug = localDebug } func (s *Server) DisconnectUser(address string) { log.Info("Disconnecting all open SMTP connections for ", address) @@ -186,3 +118,6 @@ func (s *Server) DisconnectUser(address string) { } }) } + +func (s *Server) Serve(l net.Listener) error { return s.server.Serve(l) } +func (s *Server) StopServe() error { return s.server.Close() } diff --git a/test/features/bridge/no_internet.feature b/test/features/bridge/no_internet.feature deleted file mode 100644 index 4025f9a4..00000000 --- a/test/features/bridge/no_internet.feature +++ /dev/null @@ -1,32 +0,0 @@ -Feature: Servers are closed when no internet - - # FIXME: Locally works, has lags on CI. Looks like it breaks other tests as well. - @ignore - Scenario: All connection are closed and then restored multiple times - Given there is connected user "user" - And there is IMAP client "i1" logged in as "user" - And there is SMTP client "s1" logged in as "user" - When there is no internet connection - And 3 seconds pass - Then IMAP client "i1" is logged out - And SMTP client "s1" is logged out - Given the internet connection is restored - And 3 seconds pass - And there is IMAP client "i2" logged in as "user" - And there is SMTP client "s2" logged in as "user" - When IMAP client "i2" gets info of "INBOX" - When SMTP client "s2" sends "HELO example.com" - Then IMAP response to "i2" is "OK" - Then SMTP response to "s2" is "OK" - When there is no internet connection - And 3 seconds pass - Then IMAP client "i2" is logged out - And SMTP client "s2" is logged out - Given the internet connection is restored - And 3 seconds pass - And there is IMAP client "i3" logged in as "user" - And there is SMTP client "s3" logged in as "user" - When IMAP client "i3" gets info of "INBOX" - When SMTP client "s3" sends "HELO example.com" - Then IMAP response to "i3" is "OK" - Then SMTP response to "s3" is "OK"