mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-15 14:56:42 +00:00
GODT-1187: Remove IMAP/SMTP blocking when no internet.
This commit is contained in:
@ -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() }
|
||||
|
||||
117
internal/serverutil/controller.go
Normal file
117
internal/serverutil/controller.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
39
internal/serverutil/error_logger.go
Normal file
39
internal/serverutil/error_logger.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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...)
|
||||
}
|
||||
59
internal/serverutil/listener.go
Normal file
59
internal/serverutil/listener.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
||||
26
internal/serverutil/protocol.go
Normal file
26
internal/serverutil/protocol.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package serverutil
|
||||
|
||||
type Protocol string
|
||||
|
||||
const (
|
||||
HTTP = Protocol("HTTP")
|
||||
IMAP = Protocol("IMAP")
|
||||
SMTP = Protocol("SMTP")
|
||||
)
|
||||
@ -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
|
||||
}
|
||||
|
||||
153
internal/serverutil/test/controller_test.go
Normal file
153
internal/serverutil/test/controller_test.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
||||
88
internal/serverutil/test/server.go
Normal file
88
internal/serverutil/test/server.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
||||
}
|
||||
@ -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() }
|
||||
|
||||
Reference in New Issue
Block a user