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"