mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
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.
495 lines
13 KiB
Go
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
|
|
}
|