Files
proton-bridge/internal/bridge/bridge.go
James Houlahan 6fbf6d90dc Other: Fix IMAP/SMTP/Login leaks/race conditions
Depending on the timing of bridge closure, it was possible for the 
IMAP/SMTP servers to not have started serving yet. By grouping this in
a cancelable goroutine group (*xsync.Group), we mitigate this issue.

Further, depending on internet disconnection timing during user login,
it was possible for a user to be improperly logged in. This change 
fixes this and adds test coverage for it.

Lastly, depending on timing, certain background tasks (updates check,
connectivity ping) could be improperly started or closed. This change
groups them in the *xsync.Group as well to be closed properly.
2022-11-16 13:48:30 +01:00

495 lines
13 KiB
Go

// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Package bridge implements the Bridge, which acts as the backend to the UI.
package bridge
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"sync"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon"
imapEvents "github.com/ProtonMail/gluon/events"
"github.com/ProtonMail/gluon/watcher"
"github.com/ProtonMail/proton-bridge/v2/internal/async"
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
"github.com/ProtonMail/proton-bridge/v2/internal/events"
"github.com/ProtonMail/proton-bridge/v2/internal/focus"
"github.com/ProtonMail/proton-bridge/v2/internal/safe"
"github.com/ProtonMail/proton-bridge/v2/internal/user"
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
"github.com/bradenaw/juniper/xslices"
"github.com/bradenaw/juniper/xsync"
"github.com/emersion/go-smtp"
"github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus"
"gitlab.protontech.ch/go/liteapi"
)
type Bridge struct {
// vault holds bridge-specific data, such as preferences and known users (authorized or not).
vault *vault.Vault
// users holds authorized users.
users *safe.Map[string, *user.User]
goLoad func()
// api manages user API clients.
api *liteapi.Manager
proxyCtl ProxyController
identifier Identifier
// tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers.
tlsConfig *tls.Config
// imapServer is the bridge's IMAP server.
imapServer *gluon.Server
imapListener net.Listener
imapEventCh chan imapEvents.Event
// smtpServer is the bridge's SMTP server.
smtpServer *smtp.Server
smtpListener net.Listener
// updater is the bridge's updater.
updater Updater
goUpdate func()
curVersion *semver.Version
// focusService is used to raise the bridge window when needed.
focusService *focus.Service
// autostarter is the bridge's autostarter.
autostarter Autostarter
// locator is the bridge's locator.
locator Locator
// watchers holds all registered event watchers.
watchers []*watcher.Watcher[events.Event]
watchersLock sync.RWMutex
// errors contains errors encountered during startup.
errors []error
// These control the bridge's IMAP and SMTP logging behaviour.
logIMAPClient bool
logIMAPServer bool
logSMTP bool
// tasks manages the bridge's goroutines.
tasks *xsync.Group
}
// New creates a new bridge.
func New( //nolint:funlen
locator Locator, // the locator to provide paths to store data
vault *vault.Vault, // the bridge's encrypted data store
autostarter Autostarter, // the autostarter to manage autostart settings
updater Updater, // the updater to fetch and install updates
curVersion *semver.Version, // the current version of the bridge
apiURL string, // the URL of the API to use
cookieJar http.CookieJar, // the cookie jar to use
identifier Identifier, // the identifier to keep track of the user agent
tlsReporter TLSReporter, // the TLS reporter to report TLS errors
roundTripper http.RoundTripper, // the round tripper to use for API requests
proxyCtl ProxyController, // the DoH controller
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
logSMTP bool, // whether to log SMTP activity
) (*Bridge, <-chan events.Event, error) {
// api is the user's API manager.
api := liteapi.New(
liteapi.WithHostURL(apiURL),
liteapi.WithAppVersion(constants.AppVersion(curVersion.Original())),
liteapi.WithCookieJar(cookieJar),
liteapi.WithTransport(roundTripper),
)
// tasks holds all the bridge's background tasks.
tasks := xsync.NewGroup(context.Background())
// imapEventCh forwards IMAP events from gluon instances to the bridge for processing.
imapEventCh := make(chan imapEvents.Event)
// users holds all the bridge's users.
users := safe.NewMap[string, *user.User](nil)
// bridge is the bridge.
bridge, err := newBridge(
users,
tasks,
imapEventCh,
locator,
vault,
autostarter,
updater,
curVersion,
api,
identifier,
proxyCtl,
logIMAPClient, logIMAPServer, logSMTP,
)
if err != nil {
return nil, nil, fmt.Errorf("failed to create bridge: %w", err)
}
// Get an event channel for all events (individual events can be subscribed to later).
eventCh, _ := bridge.GetEvents()
// Initialize all of bridge's background tasks and operations.
if err := bridge.init(tlsReporter); err != nil {
return nil, nil, fmt.Errorf("failed to initialize bridge: %w", err)
}
// Start serving IMAP.
if err := bridge.serveIMAP(); err != nil {
bridge.PushError(ErrServeIMAP)
}
// Start serving SMTP.
if err := bridge.serveSMTP(); err != nil {
bridge.PushError(ErrServeSMTP)
}
return bridge, eventCh, nil
}
// nolint:funlen
func newBridge(
users *safe.Map[string, *user.User],
tasks *xsync.Group,
imapEventCh chan imapEvents.Event,
locator Locator,
vault *vault.Vault,
autostarter Autostarter,
updater Updater,
curVersion *semver.Version,
api *liteapi.Manager,
identifier Identifier,
proxyCtl ProxyController,
logIMAPClient, logIMAPServer, logSMTP bool,
) (*Bridge, error) {
tlsConfig, err := loadTLSConfig(vault)
if err != nil {
return nil, fmt.Errorf("failed to load TLS config: %w", err)
}
gluonDir, err := getGluonDir(vault)
if err != nil {
return nil, fmt.Errorf("failed to get Gluon directory: %w", err)
}
imapServer, err := newIMAPServer(
gluonDir,
curVersion,
tlsConfig,
logIMAPClient,
logIMAPServer,
imapEventCh,
tasks,
)
if err != nil {
return nil, fmt.Errorf("failed to create IMAP server: %w", err)
}
focusService, err := focus.NewService(curVersion)
if err != nil {
return nil, fmt.Errorf("failed to create focus service: %w", err)
}
return &Bridge{
vault: vault,
users: users,
api: api,
proxyCtl: proxyCtl,
identifier: identifier,
tlsConfig: tlsConfig,
imapServer: imapServer,
imapEventCh: imapEventCh,
smtpServer: newSMTPServer(users, tlsConfig, logSMTP),
updater: updater,
curVersion: curVersion,
focusService: focusService,
autostarter: autostarter,
locator: locator,
logIMAPClient: logIMAPClient,
logIMAPServer: logIMAPServer,
logSMTP: logSMTP,
tasks: tasks,
}, nil
}
// nolint:funlen
func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Enable or disable the proxy at startup.
if bridge.vault.GetProxyAllowed() {
bridge.proxyCtl.AllowProxy()
} else {
bridge.proxyCtl.DisallowProxy()
}
// Handle connection up/down events.
bridge.api.AddStatusObserver(func(status liteapi.Status) {
switch {
case status == liteapi.StatusUp:
bridge.onStatusUp()
case status == liteapi.StatusDown:
bridge.onStatusDown()
}
})
// If any call returns a bad version code, we need to update.
bridge.api.AddErrorHandler(liteapi.AppVersionBadCode, func() {
bridge.publish(events.UpdateForced{})
})
// Ensure all outgoing headers have the correct user agent.
bridge.api.AddPreRequestHook(func(_ *resty.Client, req *resty.Request) error {
req.SetHeader("User-Agent", bridge.identifier.GetUserAgent())
return nil
})
// Publish a TLS issue event if a TLS issue is encountered.
bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, tlsReporter.GetTLSIssueCh(), func(struct{}) {
bridge.publish(events.TLSIssue{})
})
})
// Publish a raise event if the focus service is called.
bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, bridge.focusService.GetRaiseCh(), func(struct{}) {
bridge.publish(events.Raise{})
})
})
// Handle any IMAP events that are forwarded to the bridge from gluon.
bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, bridge.imapEventCh, func(event imapEvents.Event) {
bridge.handleIMAPEvent(event)
})
})
// Attempt to lazy load users when triggered.
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
if err := bridge.loadUsers(ctx); err != nil {
logrus.WithError(err).Error("Failed to load users")
} else {
bridge.publish(events.AllUsersLoaded{})
}
})
defer bridge.goLoad()
// Check for updates when triggered.
bridge.goUpdate = bridge.tasks.PeriodicOrTrigger(constants.UpdateCheckInterval, 0, func(ctx context.Context) {
version, err := bridge.updater.GetVersionInfo(bridge.api, bridge.vault.GetUpdateChannel())
if err != nil {
logrus.WithError(err).Error("Failed to get version info")
} else if err := bridge.handleUpdate(version); err != nil {
logrus.WithError(err).Error("Failed to handle update")
}
})
defer bridge.goUpdate()
return nil
}
// GetEvents returns a channel of events of the given type.
// If no types are supplied, all events are returned.
func (bridge *Bridge) GetEvents(ofType ...events.Event) (<-chan events.Event, context.CancelFunc) {
watcher := bridge.addWatcher(ofType...)
return watcher.GetChannel(), func() { bridge.remWatcher(watcher) }
}
func (bridge *Bridge) PushError(err error) {
bridge.errors = append(bridge.errors, err)
}
func (bridge *Bridge) GetErrors() []error {
return bridge.errors
}
func (bridge *Bridge) Close(ctx context.Context) error {
// Close the IMAP server.
if err := bridge.closeIMAP(ctx); err != nil {
logrus.WithError(err).Error("Failed to close IMAP server")
}
// Close the SMTP server.
if err := bridge.closeSMTP(); err != nil {
logrus.WithError(err).Error("Failed to close SMTP server")
}
// Close all users.
bridge.users.IterValues(func(user *user.User) {
user.Close()
})
// Stop all ongoing tasks.
bridge.tasks.Wait()
// Close the focus service.
bridge.focusService.Close()
// Close the watchers.
bridge.watchersLock.Lock()
defer bridge.watchersLock.Unlock()
for _, watcher := range bridge.watchers {
watcher.Close()
}
bridge.watchers = nil
// Save the last version of bridge that was run.
if err := bridge.vault.SetLastVersion(bridge.curVersion); err != nil {
logrus.WithError(err).Error("Failed to save last version")
}
return nil
}
func (bridge *Bridge) publish(event events.Event) {
bridge.watchersLock.RLock()
defer bridge.watchersLock.RUnlock()
for _, watcher := range bridge.watchers {
if watcher.IsWatching(event) {
if ok := watcher.Send(event); !ok {
logrus.WithField("event", event).Warn("Failed to send event to watcher")
}
}
}
}
func (bridge *Bridge) addWatcher(ofType ...events.Event) *watcher.Watcher[events.Event] {
bridge.watchersLock.Lock()
defer bridge.watchersLock.Unlock()
watcher := watcher.New(ofType...)
bridge.watchers = append(bridge.watchers, watcher)
return watcher
}
func (bridge *Bridge) remWatcher(watcher *watcher.Watcher[events.Event]) {
bridge.watchersLock.Lock()
defer bridge.watchersLock.Unlock()
idx := xslices.Index(bridge.watchers, watcher)
if idx < 0 {
return
}
bridge.watchers = append(bridge.watchers[:idx], bridge.watchers[idx+1:]...)
watcher.Close()
}
func (bridge *Bridge) onStatusUp() {
bridge.publish(events.ConnStatusUp{})
bridge.goLoad()
bridge.users.IterValues(func(user *user.User) {
go user.OnStatusUp()
})
}
func (bridge *Bridge) onStatusDown() {
bridge.publish(events.ConnStatusDown{})
bridge.users.IterValues(func(user *user.User) {
go user.OnStatusDown()
})
bridge.tasks.Once(func(ctx context.Context) {
backoff := time.Second
for {
select {
case <-ctx.Done():
return
case <-time.After(backoff):
if err := bridge.api.Ping(ctx); err != nil {
logrus.WithError(err).Debug("Failed to ping API, will retry")
} else {
return
}
}
if backoff < 30*time.Second {
backoff *= 2
}
}
})
}
func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert(), vault.GetBridgeTLSKey())
if err != nil {
return nil, err
}
return &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
}, nil
}
func newListener(port int, useTLS bool, tlsConfig *tls.Config) (net.Listener, error) {
if useTLS {
tlsListener, err := tls.Listen("tcp", fmt.Sprintf(":%v", port), tlsConfig)
if err != nil {
return nil, err
}
return tlsListener, nil
}
netListener, err := net.Listen("tcp", fmt.Sprintf(":%v", port))
if err != nil {
return nil, err
}
return netListener, nil
}