forked from Silverfish/proton-bridge
Add a dedicated go-routine whose sole responsibility is to manage the life time of the IMAP and SMTP servers and their listeners. The current implementation behaves the same way as the previous state. The new behavior will be implemented in a follow MR.
791 lines
24 KiB
Go
791 lines
24 KiB
Go
// Copyright (c) 2023 Proton AG
|
|
//
|
|
// This file is part of Proton Mail Bridge.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 grpc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
"github.com/ProtonMail/gluon/async"
|
|
"github.com/ProtonMail/go-proton-api"
|
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
|
"github.com/ProtonMail/proton-bridge/v3/internal/frontend/theme"
|
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
|
"github.com/ProtonMail/proton-bridge/v3/internal/service"
|
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
|
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
|
"github.com/ProtonMail/proton-bridge/v3/pkg/ports"
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/exp/maps"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/runtime/protoimpl"
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
"google.golang.org/protobuf/types/known/wrapperspb"
|
|
)
|
|
|
|
// CheckTokens implements the CheckToken gRPC service call.
|
|
func (s *Service) CheckTokens(_ context.Context, clientConfigPath *wrapperspb.StringValue) (*wrapperspb.StringValue, error) {
|
|
s.log.Debug("CheckTokens")
|
|
|
|
path := clientConfigPath.Value
|
|
logEntry := s.log.WithField("path", path)
|
|
|
|
var clientConfig service.Config
|
|
if err := clientConfig.Load(path); err != nil {
|
|
logEntry.WithError(err).Error("Could not read gRPC client config file")
|
|
|
|
return nil, err
|
|
}
|
|
|
|
logEntry.Info("gRPC client config file was successfully loaded")
|
|
|
|
return &wrapperspb.StringValue{Value: clientConfig.Token}, nil
|
|
}
|
|
|
|
func (s *Service) AddLogEntry(_ context.Context, request *AddLogEntryRequest) (*emptypb.Empty, error) {
|
|
entry := s.log
|
|
|
|
if len(request.Package) > 0 {
|
|
entry = entry.WithField("pkg", request.Package)
|
|
}
|
|
|
|
level := logrusLevelFromGrpcLevel(request.Level)
|
|
|
|
// we do a special case for Panic and Fatal as using logrus.Entry.Log will not panic nor exit respectively.
|
|
if level == logrus.PanicLevel {
|
|
entry.Panic(request.Message)
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
if level == logrus.FatalLevel {
|
|
entry.Fatal(request.Message)
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
entry.Log(level, request.Message)
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
// GuiReady implement the GuiReady gRPC service call.
|
|
func (s *Service) GuiReady(_ context.Context, _ *emptypb.Empty) (*GuiReadyResponse, error) {
|
|
s.log.Debug("GuiReady")
|
|
|
|
s.initializationDone.Do(s.initializing.Done)
|
|
|
|
// Splash screen should be displayed only to users who start v3.0.17 or later for the first time after upgrading from v2.
|
|
return &GuiReadyResponse{
|
|
ShowSplashScreen: (!s.bridge.GetFirstStart()) &&
|
|
s.bridge.GetLastVersion().LessThan(semver.MustParse("3.0.0")) &&
|
|
s.bridge.GetCurrentVersion().GreaterThan(semver.MustParse("3.0.16")),
|
|
}, nil
|
|
}
|
|
|
|
// Quit implement the Quit gRPC service call.
|
|
func (s *Service) Quit(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
|
s.log.Debug("Quit")
|
|
return &emptypb.Empty{}, s.quit()
|
|
}
|
|
|
|
func (s *Service) quit() error {
|
|
// Windows is notably slow at Quitting. We do it in a goroutine to speed things up a bit.
|
|
go func() {
|
|
defer async.HandlePanic(s.panicHandler)
|
|
|
|
if s.parentPID >= 0 {
|
|
s.parentPIDDoneCh <- struct{}{}
|
|
}
|
|
|
|
var err error
|
|
if s.isStreamingEvents() {
|
|
if err = s.stopEventStream(); err != nil {
|
|
s.log.WithError(err).Error("Quit failed.")
|
|
}
|
|
}
|
|
|
|
// The following call is launched as a goroutine, as it will wait for current calls to end, including this one.
|
|
s.grpcServer.GracefulStop() // gRPC does clean up and remove the file socket if used.
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Restart implement the Restart gRPC service call.
|
|
func (s *Service) Restart(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
|
|
s.log.Debug("Restart")
|
|
|
|
s.restarter.Set(true, false)
|
|
return s.Quit(ctx, empty)
|
|
}
|
|
|
|
func (s *Service) ShowOnStartup(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
|
s.log.Debug("ShowOnStartup")
|
|
|
|
return wrapperspb.Bool(s.showOnStartup), nil
|
|
}
|
|
|
|
func (s *Service) SetIsAutostartOn(_ context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
|
s.log.WithField("show", isOn.Value).Debug("SetIsAutostartOn")
|
|
|
|
defer func() { _ = s.SendEvent(NewToggleAutostartFinishedEvent()) }()
|
|
|
|
if isOn.Value == s.bridge.GetAutostart() {
|
|
s.initAutostart()
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
s.initAutostart()
|
|
|
|
if err := s.bridge.SetAutostart(isOn.Value); err != nil {
|
|
s.log.WithField("makeItEnabled", isOn.Value).WithError(err).Error("Autostart change failed")
|
|
return nil, status.Errorf(codes.Internal, "failed to set autostart: %v", err)
|
|
}
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) IsAutostartOn(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
|
s.log.Debug("IsAutostartOn")
|
|
|
|
return wrapperspb.Bool(s.bridge.GetAutostart()), nil
|
|
}
|
|
|
|
func (s *Service) SetIsBetaEnabled(_ context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
|
s.log.WithField("isEnabled", isEnabled.Value).Debug("SetIsBetaEnabled")
|
|
|
|
channel := updater.StableChannel
|
|
if isEnabled.Value {
|
|
channel = updater.EarlyChannel
|
|
}
|
|
|
|
if err := s.bridge.SetUpdateChannel(channel); err != nil {
|
|
s.log.WithError(err).Error("Failed to set update channel")
|
|
return nil, status.Errorf(codes.Internal, "failed to set update channel: %v", err)
|
|
}
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) IsBetaEnabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
|
s.log.Debug("IsBetaEnabled")
|
|
|
|
return wrapperspb.Bool(s.bridge.GetUpdateChannel() == updater.EarlyChannel), nil
|
|
}
|
|
|
|
func (s *Service) SetIsAllMailVisible(_ context.Context, isVisible *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
|
s.log.WithField("isVisible", isVisible.Value).Debug("SetIsAllMailVisible")
|
|
|
|
if err := s.bridge.SetShowAllMail(isVisible.Value); err != nil {
|
|
s.log.WithError(err).Error("Failed to set show all mail")
|
|
return nil, status.Errorf(codes.Internal, "failed to set show all mail: %v", err)
|
|
}
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) IsAllMailVisible(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
|
s.log.Debug("IsAllMailVisible")
|
|
|
|
return wrapperspb.Bool(s.bridge.GetShowAllMail()), nil
|
|
}
|
|
|
|
func (s *Service) SetIsTelemetryDisabled(_ context.Context, isDisabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
|
s.log.WithField("isEnabled", isDisabled.Value).Debug("SetIsTelemetryDisabled")
|
|
|
|
if err := s.bridge.SetTelemetryDisabled(isDisabled.Value); err != nil {
|
|
s.log.WithError(err).Error("Failed to set telemetry status")
|
|
return nil, status.Errorf(codes.Internal, "failed to set telemetry status: %v", err)
|
|
}
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) IsTelemetryDisabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
|
s.log.Debug("IsTelemetryDisabled")
|
|
|
|
return wrapperspb.Bool(s.bridge.GetTelemetryDisabled()), nil
|
|
}
|
|
|
|
func (s *Service) GoOs(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
|
s.log.Debug("GoOs") // TO-DO We can probably get rid of this and use QSysInfo::product name
|
|
|
|
return wrapperspb.String(runtime.GOOS), nil
|
|
}
|
|
|
|
func (s *Service) TriggerReset(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
|
s.log.Debug("TriggerReset")
|
|
|
|
go func() {
|
|
defer async.HandlePanic(s.panicHandler)
|
|
|
|
s.triggerReset()
|
|
}()
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) Version(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
|
s.log.Debug("Version")
|
|
|
|
return wrapperspb.String(s.bridge.GetCurrentVersion().Original()), nil
|
|
}
|
|
|
|
func (s *Service) LogsPath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
|
s.log.Debug("LogsPath")
|
|
|
|
path, err := s.bridge.GetLogsPath()
|
|
if err != nil {
|
|
s.log.WithError(err).Error("Cannot determine logs path")
|
|
return nil, err
|
|
}
|
|
return wrapperspb.String(path), nil
|
|
}
|
|
|
|
func (s *Service) LicensePath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
|
s.log.Debug("LicensePath")
|
|
|
|
return wrapperspb.String(s.bridge.GetLicenseFilePath()), nil
|
|
}
|
|
|
|
func (s *Service) DependencyLicensesLink(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
|
return wrapperspb.String(s.bridge.GetDependencyLicensesLink()), nil
|
|
}
|
|
|
|
func (s *Service) ReleaseNotesPageLink(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
|
s.latestLock.RLock()
|
|
defer s.latestLock.RUnlock()
|
|
|
|
return wrapperspb.String(s.latest.ReleaseNotesPage), nil
|
|
}
|
|
|
|
func (s *Service) LandingPageLink(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
|
s.latestLock.RLock()
|
|
defer s.latestLock.RUnlock()
|
|
|
|
return wrapperspb.String(s.latest.LandingPage), nil
|
|
}
|
|
|
|
func (s *Service) SetColorSchemeName(_ context.Context, name *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
|
s.log.WithField("ColorSchemeName", name.Value).Debug("SetColorSchemeName")
|
|
|
|
if !theme.IsAvailable(theme.Theme(name.Value)) {
|
|
s.log.WithField("scheme", name.Value).Warn("Color scheme not available")
|
|
return nil, status.Error(codes.NotFound, "Color scheme not available")
|
|
}
|
|
|
|
if err := s.bridge.SetColorScheme(name.Value); err != nil {
|
|
s.log.WithError(err).Error("Failed to set color scheme")
|
|
return nil, status.Errorf(codes.Internal, "failed to set color scheme: %v", err)
|
|
}
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) ColorSchemeName(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
|
s.log.Debug("ColorSchemeName")
|
|
|
|
current := s.bridge.GetColorScheme()
|
|
if !theme.IsAvailable(theme.Theme(current)) {
|
|
current = string(theme.DefaultTheme())
|
|
if err := s.bridge.SetColorScheme(current); err != nil {
|
|
s.log.WithError(err).Error("Failed to set color scheme")
|
|
return nil, status.Errorf(codes.Internal, "failed to set color scheme: %v", err)
|
|
}
|
|
}
|
|
|
|
return wrapperspb.String(current), nil
|
|
}
|
|
|
|
func (s *Service) CurrentEmailClient(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
|
s.log.Debug("CurrentEmailClient")
|
|
|
|
return wrapperspb.String(s.bridge.GetCurrentUserAgent()), nil
|
|
}
|
|
|
|
func (s *Service) ReportBug(_ context.Context, report *ReportBugRequest) (*emptypb.Empty, error) {
|
|
s.log.WithFields(logrus.Fields{
|
|
"osType": report.OsType,
|
|
"osVersion": report.OsVersion,
|
|
"description": report.Description,
|
|
"address": report.Address,
|
|
"emailClient": report.EmailClient,
|
|
"includeLogs": report.IncludeLogs,
|
|
}).Debug("ReportBug")
|
|
|
|
go func() {
|
|
defer async.HandlePanic(s.panicHandler)
|
|
|
|
defer func() { _ = s.SendEvent(NewReportBugFinishedEvent()) }()
|
|
|
|
if err := s.bridge.ReportBug(
|
|
context.Background(),
|
|
report.OsType,
|
|
report.OsVersion,
|
|
report.Description,
|
|
report.Address,
|
|
report.Address,
|
|
report.EmailClient,
|
|
report.IncludeLogs,
|
|
); err != nil {
|
|
s.log.WithError(err).Error("Failed to report bug")
|
|
_ = s.SendEvent(NewReportBugErrorEvent())
|
|
return
|
|
}
|
|
|
|
_ = s.SendEvent(NewReportBugSuccessEvent())
|
|
}()
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) ExportTLSCertificates(_ context.Context, folderPath *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
|
s.log.WithField("folderPath", folderPath).Info("ExportTLSCertificates")
|
|
|
|
go func() {
|
|
defer async.HandlePanic(s.panicHandler)
|
|
|
|
cert, key := s.bridge.GetBridgeTLSCert()
|
|
|
|
if err := os.WriteFile(filepath.Join(folderPath.Value, "cert.pem"), cert, 0o600); err != nil {
|
|
_ = s.SendEvent(NewGenericErrorEvent(ErrorCode_TLS_CERT_EXPORT_ERROR))
|
|
}
|
|
|
|
if err := os.WriteFile(filepath.Join(folderPath.Value, "key.pem"), key, 0o600); err != nil {
|
|
_ = s.SendEvent(NewGenericErrorEvent(ErrorCode_TLS_KEY_EXPORT_ERROR))
|
|
}
|
|
}()
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) ForceLauncher(_ context.Context, launcher *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
|
s.log.WithField("launcher", launcher.Value).Debug("ForceLauncher")
|
|
|
|
s.restarter.Override(launcher.Value)
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) SetMainExecutable(_ context.Context, exe *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
|
s.log.WithField("executable", exe.Value).Debug("SetMainExecutable")
|
|
|
|
s.restarter.AddFlags("--wait", exe.Value)
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) {
|
|
s.log.WithField("username", login.Username).Debug("Login")
|
|
|
|
go func() {
|
|
defer async.HandlePanic(s.panicHandler)
|
|
|
|
password, err := base64Decode(login.Password)
|
|
if err != nil {
|
|
s.log.WithError(err).Error("Cannot decode password")
|
|
_ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, "Cannot decode password"))
|
|
return
|
|
}
|
|
|
|
client, auth, err := s.bridge.LoginAuth(context.Background(), login.Username, password)
|
|
if err != nil {
|
|
defer s.loginClean()
|
|
|
|
if errors.Is(err, bridge.ErrUserAlreadyLoggedIn) {
|
|
_ = s.SendEvent(NewLoginAlreadyLoggedInEvent(auth.UserID))
|
|
} else if apiErr := new(proton.APIError); errors.As(err, &apiErr) {
|
|
switch apiErr.Code { // nolint:exhaustive
|
|
case proton.PasswordWrong, proton.UsernameInvalid:
|
|
_ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, ""))
|
|
|
|
case proton.PaidPlanRequired:
|
|
_ = s.SendEvent(NewLoginError(LoginErrorType_FREE_USER, ""))
|
|
|
|
default:
|
|
_ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, err.Error()))
|
|
}
|
|
} else {
|
|
_ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, err.Error()))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
s.password = password
|
|
s.authClient = client
|
|
s.auth = auth
|
|
|
|
switch {
|
|
case auth.TwoFA.Enabled&proton.HasTOTP != 0:
|
|
_ = s.SendEvent(NewLoginTfaRequestedEvent(login.Username))
|
|
|
|
case auth.PasswordMode == proton.TwoPasswordMode:
|
|
_ = s.SendEvent(NewLoginTwoPasswordsRequestedEvent())
|
|
|
|
default:
|
|
s.finishLogin()
|
|
}
|
|
}()
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) Login2FA(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) {
|
|
s.log.WithField("username", login.Username).Debug("Login2FA")
|
|
|
|
go func() {
|
|
defer async.HandlePanic(s.panicHandler)
|
|
|
|
if s.auth.UID == "" || s.authClient == nil {
|
|
s.log.Errorf("Login 2FA: authethication incomplete %s %p", s.auth.UID, s.authClient)
|
|
_ = s.SendEvent(NewLoginError(LoginErrorType_TFA_ABORT, "Missing authentication, try again."))
|
|
s.loginClean()
|
|
return
|
|
}
|
|
|
|
twoFA, err := base64Decode(login.Password)
|
|
if err != nil {
|
|
s.log.WithError(err).Error("Cannot decode 2fa code")
|
|
_ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, "Cannot decode 2fa code"))
|
|
s.loginClean()
|
|
return
|
|
}
|
|
|
|
if err := s.authClient.Auth2FA(context.Background(), proton.Auth2FAReq{TwoFactorCode: string(twoFA)}); err != nil {
|
|
if apiErr := new(proton.APIError); errors.As(err, &apiErr) && apiErr.Code == proton.PasswordWrong {
|
|
s.log.Warn("Login 2FA: retry 2fa")
|
|
_ = s.SendEvent(NewLoginError(LoginErrorType_TFA_ERROR, ""))
|
|
} else {
|
|
s.log.WithError(err).Warn("Login 2FA: failed")
|
|
_ = s.SendEvent(NewLoginError(LoginErrorType_TFA_ABORT, err.Error()))
|
|
s.loginClean()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if s.auth.PasswordMode == proton.TwoPasswordMode {
|
|
_ = s.SendEvent(NewLoginTwoPasswordsRequestedEvent())
|
|
return
|
|
}
|
|
|
|
s.finishLogin()
|
|
}()
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) Login2Passwords(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) {
|
|
s.log.WithField("username", login.Username).Debug("Login2Passwords")
|
|
|
|
go func() {
|
|
defer async.HandlePanic(s.panicHandler)
|
|
|
|
password, err := base64Decode(login.Password)
|
|
if err != nil {
|
|
s.log.WithError(err).Error("Cannot decode mbox password")
|
|
_ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, "Cannot decode mbox password"))
|
|
s.loginClean()
|
|
return
|
|
}
|
|
|
|
s.password = password
|
|
|
|
s.finishLogin()
|
|
}()
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) LoginAbort(_ context.Context, loginAbort *LoginAbortRequest) (*emptypb.Empty, error) {
|
|
s.log.WithField("username", loginAbort.Username).Debug("LoginAbort")
|
|
|
|
go func() {
|
|
defer async.HandlePanic(s.panicHandler)
|
|
|
|
s.loginAbort()
|
|
}()
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) CheckUpdate(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
|
s.log.Debug("CheckUpdate")
|
|
|
|
go func() {
|
|
defer async.HandlePanic(s.panicHandler)
|
|
|
|
updateCh, done := s.bridge.GetEvents(
|
|
events.UpdateAvailable{},
|
|
events.UpdateNotAvailable{},
|
|
events.UpdateCheckFailed{},
|
|
)
|
|
defer done()
|
|
|
|
s.bridge.CheckForUpdates()
|
|
|
|
switch (<-updateCh).(type) {
|
|
case events.UpdateNotAvailable:
|
|
_ = s.SendEvent(NewUpdateIsLatestVersionEvent())
|
|
|
|
case events.UpdateAvailable:
|
|
// ... this is handled by the main event loop
|
|
|
|
case events.UpdateCheckFailed:
|
|
// ... maybe show an error? but do nothing for now
|
|
}
|
|
|
|
_ = s.SendEvent(NewUpdateCheckFinishedEvent())
|
|
}()
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) InstallUpdate(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
|
s.log.Debug("InstallUpdate")
|
|
|
|
go func() {
|
|
defer async.HandlePanic(s.panicHandler)
|
|
|
|
safe.RLock(func() {
|
|
s.bridge.InstallUpdate(s.target)
|
|
}, s.targetLock)
|
|
}()
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) SetIsAutomaticUpdateOn(_ context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
|
s.log.WithField("isOn", isOn.Value).Debug("SetIsAutomaticUpdateOn")
|
|
|
|
if currentlyOn := s.bridge.GetAutoUpdate(); currentlyOn == isOn.Value {
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
if err := s.bridge.SetAutoUpdate(isOn.Value); err != nil {
|
|
s.log.WithError(err).Error("Failed to set auto update")
|
|
return nil, status.Errorf(codes.Internal, "failed to set auto update: %v", err)
|
|
}
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) IsAutomaticUpdateOn(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
|
s.log.Debug("IsAutomaticUpdateOn")
|
|
|
|
return wrapperspb.Bool(s.bridge.GetAutoUpdate()), nil
|
|
}
|
|
|
|
func (s *Service) DiskCachePath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
|
s.log.Debug("DiskCachePath")
|
|
|
|
return wrapperspb.String(s.bridge.GetGluonCacheDir()), nil
|
|
}
|
|
|
|
func (s *Service) SetDiskCachePath(_ context.Context, newPath *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
|
s.log.WithField("path", newPath.Value).Debug("setDiskCachePath")
|
|
|
|
go func() {
|
|
defer async.HandlePanic(s.panicHandler)
|
|
|
|
defer func() {
|
|
_ = s.SendEvent(NewDiskCachePathChangeFinishedEvent())
|
|
}()
|
|
|
|
path := newPath.Value
|
|
|
|
//goland:noinspection GoBoolExpressions
|
|
if (runtime.GOOS == "windows") && (path[0] == '/') {
|
|
path = path[1:]
|
|
}
|
|
|
|
if path != s.bridge.GetGluonCacheDir() {
|
|
if err := s.bridge.SetGluonDir(context.Background(), path); err != nil {
|
|
s.log.WithError(err).Error("The local cache location could not be changed.")
|
|
_ = s.SendEvent(NewDiskCacheErrorEvent(DiskCacheErrorType_CANT_MOVE_DISK_CACHE_ERROR))
|
|
return
|
|
}
|
|
|
|
_ = s.SendEvent(NewDiskCachePathChangedEvent(s.bridge.GetGluonCacheDir()))
|
|
}
|
|
}()
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) SetIsDoHEnabled(_ context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
|
s.log.WithField("isEnabled", isEnabled.Value).Debug("SetIsDohEnabled")
|
|
|
|
if err := s.bridge.SetProxyAllowed(isEnabled.Value); err != nil {
|
|
s.log.WithError(err).Error("Failed to set DoH")
|
|
return nil, status.Errorf(codes.Internal, "failed to set DoH: %v", err)
|
|
}
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) IsDoHEnabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
|
s.log.Debug("IsDohEnabled")
|
|
|
|
return wrapperspb.Bool(s.bridge.GetProxyAllowed()), nil
|
|
}
|
|
|
|
func (s *Service) MailServerSettings(_ context.Context, _ *emptypb.Empty) (*ImapSmtpSettings, error) {
|
|
s.log.Debug("ConnectionMode")
|
|
|
|
return &ImapSmtpSettings{
|
|
state: protoimpl.MessageState{},
|
|
sizeCache: 0,
|
|
unknownFields: nil,
|
|
ImapPort: int32(s.bridge.GetIMAPPort()),
|
|
SmtpPort: int32(s.bridge.GetSMTPPort()),
|
|
UseSSLForImap: s.bridge.GetIMAPSSL(),
|
|
UseSSLForSmtp: s.bridge.GetSMTPSSL(),
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) SetMailServerSettings(ctx context.Context, settings *ImapSmtpSettings) (*emptypb.Empty, error) {
|
|
s.log.
|
|
WithField("ImapPort", settings.ImapPort).
|
|
WithField("SmtpPort", settings.SmtpPort).
|
|
WithField("UseSSUseSSLForIMAP", settings.UseSSLForImap).
|
|
WithField("UseSSLForSMTP", settings.UseSSLForSmtp).
|
|
Debug("SetConnectionMode")
|
|
|
|
go func() {
|
|
defer async.HandlePanic(s.panicHandler)
|
|
|
|
defer func() { _ = s.SendEvent(NewChangeMailServerSettingsFinishedEvent()) }()
|
|
|
|
if s.bridge.GetIMAPSSL() != settings.UseSSLForImap {
|
|
if err := s.bridge.SetIMAPSSL(ctx, settings.UseSSLForImap); err != nil {
|
|
s.log.WithError(err).Error("Failed to set IMAP SSL")
|
|
_ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_IMAP_CONNECTION_MODE_CHANGE_ERROR))
|
|
}
|
|
}
|
|
|
|
if s.bridge.GetSMTPSSL() != settings.UseSSLForSmtp {
|
|
if err := s.bridge.SetSMTPSSL(ctx, settings.UseSSLForSmtp); err != nil {
|
|
s.log.WithError(err).Error("Failed to set SMTP SSL")
|
|
_ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_SMTP_CONNECTION_MODE_CHANGE_ERROR))
|
|
}
|
|
}
|
|
|
|
if s.bridge.GetIMAPPort() != int(settings.ImapPort) {
|
|
if err := s.bridge.SetIMAPPort(ctx, int(settings.ImapPort)); err != nil {
|
|
s.log.WithError(err).Error("Failed to set IMAP port")
|
|
_ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_IMAP_PORT_CHANGE_ERROR))
|
|
}
|
|
}
|
|
|
|
if s.bridge.GetSMTPPort() != int(settings.SmtpPort) {
|
|
if err := s.bridge.SetSMTPPort(ctx, int(settings.SmtpPort)); err != nil {
|
|
s.log.WithError(err).Error("Failed to set SMTP port")
|
|
_ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_SMTP_PORT_CHANGE_ERROR))
|
|
}
|
|
}
|
|
|
|
_ = s.SendEvent(NewMailServerSettingsChangedEvent(s.getMailServerSettings()))
|
|
}()
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) Hostname(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
|
s.log.Debug("Hostname")
|
|
|
|
return wrapperspb.String(constants.Host), nil
|
|
}
|
|
|
|
func (s *Service) IsPortFree(_ context.Context, port *wrapperspb.Int32Value) (*wrapperspb.BoolValue, error) {
|
|
s.log.Debug("IsPortFree")
|
|
|
|
return wrapperspb.Bool(ports.IsPortFree(int(port.Value))), nil
|
|
}
|
|
|
|
func (s *Service) AvailableKeychains(_ context.Context, _ *emptypb.Empty) (*AvailableKeychainsResponse, error) {
|
|
s.log.Debug("AvailableKeychains")
|
|
|
|
return &AvailableKeychainsResponse{Keychains: maps.Keys(keychain.Helpers)}, nil
|
|
}
|
|
|
|
func (s *Service) SetCurrentKeychain(ctx context.Context, keychain *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
|
s.log.WithField("keychain", keychain.Value).Debug("SetCurrentKeyChain") // we do not check validity.
|
|
|
|
defer func() { _, _ = s.Restart(ctx, &emptypb.Empty{}) }()
|
|
defer func() { _ = s.SendEvent(NewKeychainChangeKeychainFinishedEvent()) }()
|
|
|
|
helper, err := s.bridge.GetKeychainApp()
|
|
if err != nil {
|
|
s.log.WithError(err).Error("Failed to get current keychain")
|
|
return nil, status.Errorf(codes.Internal, "failed to get current keychain: %v", err)
|
|
}
|
|
|
|
if helper == keychain.Value {
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
if err := s.bridge.SetKeychainApp(keychain.Value); err != nil {
|
|
s.log.WithError(err).Error("Failed to set keychain")
|
|
return nil, status.Errorf(codes.Internal, "failed to set keychain: %v", err)
|
|
}
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *Service) CurrentKeychain(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
|
s.log.Debug("CurrentKeychain")
|
|
|
|
helper, err := s.bridge.GetKeychainApp()
|
|
if err != nil {
|
|
s.log.WithError(err).Error("Failed to get current keychain")
|
|
return nil, status.Errorf(codes.Internal, "failed to get current keychain: %v", err)
|
|
}
|
|
|
|
return wrapperspb.String(helper), nil
|
|
}
|
|
|
|
func base64Decode(in []byte) ([]byte, error) {
|
|
out := make([]byte, base64.StdEncoding.DecodedLen(len(in)))
|
|
|
|
n, err := base64.StdEncoding.Decode(out, in)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return out[:n], nil
|
|
}
|
|
|
|
func (s *Service) getMailServerSettings() *ImapSmtpSettings {
|
|
return &ImapSmtpSettings{
|
|
ImapPort: int32(s.bridge.GetIMAPPort()),
|
|
SmtpPort: int32(s.bridge.GetSMTPPort()),
|
|
UseSSLForImap: s.bridge.GetIMAPSSL(),
|
|
UseSSLForSmtp: s.bridge.GetSMTPSSL(),
|
|
}
|
|
}
|