From 20c802a1e588ec2d0e4f753ee8c5543e693f26ed Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Fri, 30 Sep 2022 11:12:56 +0200 Subject: [PATCH] GODT-1917: gRPC service should use random port. WIP: bridge-gui wait and parse gRPC service config fie. --- internal/app/bridge/bridge.go | 1 + .../bridge-gui/bridge-gui/QMLBackend.cpp | 6 +- .../bridge-gui/bridge-gui/QMLBackend.h | 2 +- .../frontend/bridge-gui/bridge-gui/main.cpp | 10 +- .../bridge-gui/bridgepp/CMakeLists.txt | 4 +- .../bridgepp/bridgepp/GRPC/GRPCClient.cpp | 55 +++++++- .../bridgepp/bridgepp/GRPC/GRPCClient.h | 8 +- .../bridgepp/bridgepp/GRPC/GRPCConfig.cpp | 133 ++++++++++++++++++ .../bridgepp/bridgepp/GRPC/GRPCConfig.h | 39 +++++ .../bridgepp/bridgepp/GRPC/GRPCUtils.cpp | 17 +++ .../bridgepp/bridgepp/GRPC/GRPCUtils.h | 2 +- internal/frontend/frontend.go | 3 + internal/frontend/grpc/config.go | 65 +++++++++ internal/frontend/grpc/config_test.go | 55 ++++++++ internal/frontend/grpc/service.go | 38 ++++- 15 files changed, 423 insertions(+), 15 deletions(-) create mode 100644 internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCConfig.cpp create mode 100644 internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCConfig.h create mode 100644 internal/frontend/grpc/config.go create mode 100644 internal/frontend/grpc/config_test.go diff --git a/internal/app/bridge/bridge.go b/internal/app/bridge/bridge.go index dbabecfd..c20f9536 100644 --- a/internal/app/bridge/bridge.go +++ b/internal/app/bridge/bridge.go @@ -160,6 +160,7 @@ func main(b *base.Base, c *cli.Context) error { //nolint:funlen b.Updater, bridge, b, + b.Locations, ) // Watch for updates routine diff --git a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp index dcd2ee1f..cbc2a1b1 100644 --- a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp +++ b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp @@ -37,9 +37,9 @@ QMLBackend::QMLBackend() //**************************************************************************************************************************************************** -// +/// \param[in] serviceConfig //**************************************************************************************************************************************************** -void QMLBackend::init() +void QMLBackend::init(GRPCConfig const &serviceConfig) { users_ = new UserList(this); @@ -47,7 +47,7 @@ void QMLBackend::init() this->connectGrpcEvents(); QString error; - if (app().grpc().connectToServer(error)) + if (app().grpc().connectToServer(serviceConfig, error)) app().log().info("Connected to backend via gRPC service."); else throw Exception(QString("Cannot connectToServer to go backend via gRPC: %1").arg(error)); diff --git a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h index 57523519..69ca1c8f 100644 --- a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h +++ b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h @@ -42,7 +42,7 @@ public: // member functions. ~QMLBackend() override = default; ///< Destructor. QMLBackend &operator=(QMLBackend const &) = delete; ///< Disabled assignment operator. QMLBackend &operator=(QMLBackend &&) = delete; ///< Disabled move assignment operator. - void init(); ///< Initialize the backend. + void init(GRPCConfig const &serviceConfig); ///< Initialize the backend. bool waitForEventStreamReaderToFinish(qint32 timeoutMs); ///< Wait for the event stream reader to finish. // invokable methods can be called from QML. They generally return a value, which slots cannot do. diff --git a/internal/frontend/bridge-gui/bridge-gui/main.cpp b/internal/frontend/bridge-gui/bridge-gui/main.cpp index bef0c101..997a0f11 100644 --- a/internal/frontend/bridge-gui/bridge-gui/main.cpp +++ b/internal/frontend/bridge-gui/bridge-gui/main.cpp @@ -41,6 +41,7 @@ namespace QString const bridgeLock = "bridge-gui.lock"; ///< file name used for the lock file. QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.* + qint64 const grpcServiceConfigWaitDelayMs = 60000; ///< The wait delay for the gRPC config file in milliseconds. } @@ -261,9 +262,16 @@ int main(int argc, char *argv[]) log.setLevel(logLevel); if (!attach) + { + // before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one. + GRPCClient::removeServiceConfigFile(); launchBridge(args); + } - app().backend().init(); + app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(attach ? 0 : grpcServiceConfigWaitDelayMs)); + + if (!attach) + GRPCClient::removeServiceConfigFile(); QQmlApplicationEngine engine; std::unique_ptr rootComponent(createRootQmlComponent(engine)); diff --git a/internal/frontend/bridge-gui/bridgepp/CMakeLists.txt b/internal/frontend/bridge-gui/bridgepp/CMakeLists.txt index 792b4fdc..8edfc3c3 100644 --- a/internal/frontend/bridge-gui/bridgepp/CMakeLists.txt +++ b/internal/frontend/bridge-gui/bridgepp/CMakeLists.txt @@ -107,13 +107,13 @@ add_library(bridgepp bridgepp/Exception/Exception.h bridgepp/Exception/Exception.cpp bridgepp/GRPC/GRPCClient.cpp bridgepp/GRPC/GRPCClient.h bridgepp/GRPC/EventFactory.cpp bridgepp/GRPC/EventFactory.h + bridgepp/GRPC/GRPCConfig.cpp bridgepp/GRPC/GRPCConfig.h bridgepp/GRPC/GRPCUtils.cpp bridgepp/GRPC/GRPCUtils.h ${PROTO_CPP_FILE} ${PROTO_H_FILE} ${GRPC_CPP_FILE} ${GRPC_H_FILE} bridgepp/Log/Log.h bridgepp/Log/Log.cpp bridgepp/ProcessMonitor.cpp bridgepp/ProcessMonitor.h bridgepp/User/User.cpp bridgepp/User/User.h - bridgepp/Worker/Worker.h bridgepp/Worker/Overseer.h bridgepp/Worker/Overseer.cpp - ) + bridgepp/Worker/Worker.h bridgepp/Worker/Overseer.h bridgepp/Worker/Overseer.cpp) target_include_directories(bridgepp PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCClient.cpp b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCClient.cpp index 47e3c3c0..ec592db3 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCClient.cpp +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCClient.cpp @@ -43,6 +43,54 @@ int const maxCertificateWaitMsecs = 60 * 1000; ///< Amount of time we wait for h } +//**************************************************************************************************************************************************** +// +//**************************************************************************************************************************************************** +void GRPCClient::removeServiceConfigFile() +{ + QString const path = serviceConfigPath(); + if (!QFile(path).exists()) + return; + if (!QFile().remove(path)) + throw Exception("Could not remove gRPC service config file."); +} + + +//**************************************************************************************************************************************************** +/// \param[in] timeoutMs The timeout in milliseconds +/// \return The service config. +//**************************************************************************************************************************************************** +GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs) +{ + QString const path = serviceConfigPath(); + QFile file(path); + + QElapsedTimer timer; + timer.start(); + bool found = false; + while (true) + { + if (file.exists()) + { + found = true; + break; + } + if (timer.elapsed() > timeoutMs) + break; + QThread::msleep(100); + } + + if (!found) + throw Exception("Server did not provide gRPC service configuration in time."); + + GRPCConfig sc; + if (!sc.load(path)) + throw Exception("The gRPC service configuration file is invalid."); + + return sc; +} + + //**************************************************************************************************************************************************** /// \brief wait for certificate generation by Bridge /// \return server certificate generated by Bridge @@ -110,14 +158,15 @@ void GRPCClient::setLog(Log *log) /// \param[out] outError If the function returns false, this variable contains a description of the error. /// \return true iff the connection was successful. //**************************************************************************************************************************************************** -bool GRPCClient::connectToServer(QString &outError) +bool GRPCClient::connectToServer(GRPCConfig const &config, QString &outError) { try { SslCredentialsOptions opts; opts.pem_root_certs += this->getServerCertificate(); - channel_ = CreateChannel("127.0.0.1:9292", grpc::SslCredentials(opts)); + QString const address = QString("127.0.0.1:%1").arg(config.port); + channel_ = CreateChannel(address.toStdString(), grpc::SslCredentials(opts)); if (!channel_) throw Exception("Channel creation failed."); @@ -130,7 +179,7 @@ bool GRPCClient::connectToServer(QString &outError) while (true) { if (log_) - log_->debug(QString("Connection to gRPC server. attempt #%1").arg(++i)); + log_->debug(QString("Connection to gRPC server at %1. attempt #%2").arg(address).arg(++i)); if (channel_->WaitForConnected(gpr_time_add(gpr_now(GPR_CLOCK_REALTIME), gpr_time_from_seconds(5, GPR_TIMESPAN)))) break; // connection established. diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCClient.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCClient.h index 20cff84d..ca67e8a1 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCClient.h +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCClient.h @@ -22,6 +22,7 @@ #include "../User/User.h" #include "../Log/Log.h" +#include "GRPCConfig.h" #include "bridge.grpc.pb.h" #include "grpc++/grpc++.h" @@ -46,6 +47,10 @@ typedef grpc::Status (grpc::Bridge::Stub::*StringParamMethod)(grpc::ClientContex class GRPCClient : public QObject { Q_OBJECT +public: // static member functions + static void removeServiceConfigFile(); ///< Delete the service config file. + static GRPCConfig waitAndRetrieveServiceConfig(qint64 timeoutMs); ///< Wait and retrieve the service configuration. + public: // member functions. GRPCClient() = default; ///< Default constructor. GRPCClient(GRPCClient const &) = delete; ///< Disabled copy-constructor. @@ -54,7 +59,7 @@ public: // member functions. GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator. GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator. void setLog(Log *log); ///< Set the log for the client. - bool connectToServer(QString &outError); ///< Establish connection to the gRPC server. + bool connectToServer(GRPCConfig const &config, QString &outError); ///< Establish connection to the gRPC server. grpc::Status addLogEntry(Log::Level level, QString const &package, QString const &message); ///< Performs the "AddLogEntry" gRPC call. grpc::Status guiReady(); ///< performs the "GuiReady" gRPC call. @@ -218,6 +223,7 @@ private: grpc::Status methodWithStringParam(StringParamMethod method, QString const &str); ///< Perform a gRPC call that takes a string as a parameter and returns an Empty. SPUser parseGRPCUser(grpc::User const &grpcUser); ///< Parse a gRPC user struct and return a User. + std::string getServerCertificate(); ///< Wait until server certificates is generated and retrieve it. void processAppEvent(grpc::AppEvent const &event); ///< Process an 'App' event. void processLoginEvent(grpc::LoginEvent const &event); ///< Process a 'Login' event. diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCConfig.cpp b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCConfig.cpp new file mode 100644 index 00000000..b1dc0ff6 --- /dev/null +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCConfig.cpp @@ -0,0 +1,133 @@ +// 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 . + + +#include "GRPCConfig.h" +#include "../Exception/Exception.h" + + +using namespace bridgepp; + + +namespace +{ + +Exception const invalidFileException("The service configuration file is invalid"); // Exception for invalid config. +Exception const couldNotSaveException("The service configuration file could not be saved"); ///< Exception for write errors. +QString const keyPort = "port"; ///< The JSON key for the port. +QString const keyCert = "cert"; ///< The JSON key for the TLS certificate. +QString const keyToken = "token"; ///< The JSON key for the identification token. + + +//**************************************************************************************************************************************************** +/// \brief read a string value from a JSON object. +/// +/// This function throws an Exception in case of error +/// +/// \param[in] object The JSON object containing the value. +/// \param[in] key The key under which the value is stored. +//**************************************************************************************************************************************************** +QString jsonStringValue(QJsonObject const &object, QString const &key) +{ + QJsonValue const v = object[key]; + if (!v.isString()) + throw invalidFileException; + return v.toString(); +} + + +//**************************************************************************************************************************************************** +/// \brief read a string value from a JSON object. +/// +/// This function throws an Exception in case of error. +/// +/// \param[in] object The JSON object containing the value. +/// \param[in] key The key under which the value is stored. +//**************************************************************************************************************************************************** +qint32 jsonIntValue(QJsonObject const &object, QString const &key) +{ + QJsonValue const v = object[key]; + if (!v.isDouble()) + throw invalidFileException; + return v.toInt(); +} + + +} // anonymous namespace + + +//**************************************************************************************************************************************************** +/// \param[in] path The path of the file to load from. +/// \param[out] outError if not null and an error occurs, this variable contains a description of the error. +/// \return true iff the operation was successful. +//**************************************************************************************************************************************************** +bool GRPCConfig::load(QString const &path, QString *outError) +{ + try + { + QFile file(path); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + throw Exception("Could not open gRPC service config file."); + + QJsonDocument const doc = QJsonDocument::fromJson(file.readAll()); + QJsonObject const object = doc.object(); + port = jsonIntValue(object, keyPort); + cert = jsonStringValue(object, keyCert); + token = jsonStringValue(object, keyToken); + + return true; + } + catch (Exception const &e) + { + if (outError) + *outError = e.qwhat(); + return false; + } +} + + +//**************************************************************************************************************************************************** +/// \param[in] path The path of the file to write to. +/// \param[out] outError if not null and an error occurs, this variable contains a description of the error. +/// \return true iff the operation was successful. +//**************************************************************************************************************************************************** +bool GRPCConfig::save(QString const &path, QString *outError) +{ + try + { + QJsonObject const object; + object[keyPort] = port; + object[keyCert] = cert; + object[keyToken] = token; + + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) + throw couldNotSaveException; + + QByteArray const array = QJsonDocument(object).toJson(); + if (array.size() != file.write(array)) + throw couldNotSaveException; + + return true; + } + catch (Exception const &e) + { + if (outError) + *outError = e.qwhat(); + return false; + } +} diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCConfig.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCConfig.h new file mode 100644 index 00000000..37972921 --- /dev/null +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCConfig.h @@ -0,0 +1,39 @@ +// 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 . + + +#ifndef BRIDGE_PP_GRPC_CONFIG_H +#define BRIDGE_PP_GRPC_CONFIG_H + + +//**************************************************************************************************************************************************** +/// Service configuration class. +//**************************************************************************************************************************************************** +struct GRPCConfig +{ +public: // data members + qint32 port; ///< The port. + QString cert; ///< The server TLS certificate. + QString token; ///< The identification token. + + bool load(QString const &path, QString *outError = nullptr); ///< Load the service config from file + bool save(QString const &path, QString *outError = nullptr); ///< Save the service config to file +}; + + + +#endif //BRIDGE_PP_GRPC_CONFIG_H diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCUtils.cpp b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCUtils.cpp index cb5efef8..0d1dabab 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCUtils.cpp +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCUtils.cpp @@ -38,6 +38,14 @@ QString serverCertificateFilename() } +//**************************************************************************************************************************************************** +/// \return the service config file name +//**************************************************************************************************************************************************** +QString serviceConfigFilename() +{ + return "grpcServiceConfig.json"; +} + //**************************************************************************************************************************************************** // //**************************************************************************************************************************************************** @@ -50,6 +58,15 @@ QString serverKeyFilename() } +//**************************************************************************************************************************************************** +/// \return The absolute path of the service config path. +//**************************************************************************************************************************************************** +QString serviceConfigPath() +{ + return QDir(userConfigDir()).absoluteFilePath(serviceConfigFilename()); +} + + //**************************************************************************************************************************************************** /// \return The absolute path of the server certificate. //**************************************************************************************************************************************************** diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCUtils.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCUtils.h index f207a563..b15da733 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCUtils.h +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCUtils.h @@ -31,7 +31,7 @@ namespace bridgepp typedef std::shared_ptr SPStreamEvent; ///< Type definition for shared pointer to grpc::StreamEvent. - +QString serviceConfigPath(); ///< Return the path of the service config file. QString serverCertificatePath(); ///< Return the path of the server certificate. QString serverKeyPath(); ///< Return the path of the server key. grpc::LogLevel logLevelToGRPC(Log::Level level); ///< Convert a Log::Level to gRPC enum value. diff --git a/internal/frontend/frontend.go b/internal/frontend/frontend.go index 8cae6995..fc3e407b 100644 --- a/internal/frontend/frontend.go +++ b/internal/frontend/frontend.go @@ -23,6 +23,7 @@ import ( "github.com/ProtonMail/proton-bridge/v2/internal/frontend/cli" "github.com/ProtonMail/proton-bridge/v2/internal/frontend/grpc" "github.com/ProtonMail/proton-bridge/v2/internal/frontend/types" + "github.com/ProtonMail/proton-bridge/v2/internal/locations" "github.com/ProtonMail/proton-bridge/v2/internal/updater" "github.com/ProtonMail/proton-bridge/v2/pkg/listener" ) @@ -45,6 +46,7 @@ func New( updater types.Updater, bridge *bridge.Bridge, restarter types.Restarter, + locations *locations.Locations, ) Frontend { switch frontendType { case "grpc": @@ -55,6 +57,7 @@ func New( updater, bridge, restarter, + locations, ) case "cli": diff --git a/internal/frontend/grpc/config.go b/internal/frontend/grpc/config.go new file mode 100644 index 00000000..7bd7b0e5 --- /dev/null +++ b/internal/frontend/grpc/config.go @@ -0,0 +1,65 @@ +// Copyright (c) 2022 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 . + +package grpc + +import ( + "encoding/json" + "os" +) + +// config is a structure containing the service configuration data that are exchanged by the gRPC server and client. +type config struct { + Port int `json:"port"` + Cert string `json:"cert"` // coming soon + Token string `json:"token"` // coming soon +} + +// save saves a gRPC service configuration to file. +func (s *config) save(path string) error { + // Another process may be waiting for this file to be available. In order to prevent this process to open + // the file while we are writing in it, we write it with a temp file name, then rename it. + tempPath := path + "_" + if err := s._save(tempPath); err != nil { + return err + } + + return os.Rename(tempPath, path) +} + +func (s *config) _save(path string) error { + f, err := os.Create(path) //nolint:errcheck,gosec + if err != nil { + return err + } + + defer func() { _ = f.Close() }() + + return json.NewEncoder(f).Encode(s) +} + +// load loads a gRPC service configuration from file. +func (s *config) load(path string) error { + f, err := os.Open(path) //nolint:errcheck,gosec + if err != nil { + return err + } + + defer func() { _ = f.Close() }() + + return json.NewDecoder(f).Decode(s) +} diff --git a/internal/frontend/grpc/config_test.go b/internal/frontend/grpc/config_test.go new file mode 100644 index 00000000..f54e6f88 --- /dev/null +++ b/internal/frontend/grpc/config_test.go @@ -0,0 +1,55 @@ +// 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 . + +package grpc + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +const ( + dummyPort = 12 + dummyCert = "A dummy cert" + dummyToken = "A dummy token" + tempFileName = "test.json" +) + +func TestConfig(t *testing.T) { + conf1 := config{ + Port: dummyPort, + Cert: dummyCert, + Token: dummyToken, + } + + // Read-back test + tempDir := t.TempDir() + tempFilePath := filepath.Join(tempDir, tempFileName) + require.NoError(t, conf1.save(tempFilePath)) + + conf2 := config{} + require.NoError(t, conf2.load(tempFilePath)) + require.Equal(t, conf1, conf2) + + // failure to load + require.Error(t, conf2.load(tempFilePath+"_")) + + // failure to save + require.Error(t, conf2.save(filepath.Join(tempDir, "non/existing/folder", tempFileName))) +} diff --git a/internal/frontend/grpc/service.go b/internal/frontend/grpc/service.go index e2548b4b..6438b02c 100644 --- a/internal/frontend/grpc/service.go +++ b/internal/frontend/grpc/service.go @@ -22,7 +22,9 @@ package grpc import ( "context" cryptotls "crypto/tls" + "fmt" "net" + "path/filepath" "strings" "sync" "time" @@ -31,6 +33,7 @@ import ( "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" "github.com/ProtonMail/proton-bridge/v2/internal/events" "github.com/ProtonMail/proton-bridge/v2/internal/frontend/types" + "github.com/ProtonMail/proton-bridge/v2/internal/locations" "github.com/ProtonMail/proton-bridge/v2/internal/updater" "github.com/ProtonMail/proton-bridge/v2/internal/users" "github.com/ProtonMail/proton-bridge/v2/pkg/keychain" @@ -43,6 +46,10 @@ import ( "google.golang.org/protobuf/types/known/emptypb" ) +const ( + serviceConfigFileName = "grpcServiceConfig.json" +) + // Service is the RPC service struct. type Service struct { // nolint:structcheck UnimplementedBridgeServer @@ -68,6 +75,7 @@ type Service struct { // nolint:structcheck initializing sync.WaitGroup initializationDone sync.Once firstTimeAutostart sync.Once + locations *locations.Locations } // NewService returns a new instance of the service. @@ -78,6 +86,7 @@ func NewService( updater types.Updater, bridge types.Bridger, restarter types.Restarter, + locations *locations.Locations, ) *Service { s := Service{ UnimplementedBridgeServer: UnimplementedBridgeServer{}, @@ -92,6 +101,7 @@ func NewService( initializing: sync.WaitGroup{}, initializationDone: sync.Once{}, firstTimeAutostart: sync.Once{}, + locations: locations, } // Initializing.Done is only called sync.Once. Please keep the increment @@ -111,12 +121,18 @@ func NewService( RegisterBridgeServer(s.grpcServer, &s) - s.listener, err = net.Listen("tcp", "127.0.0.1:9292") // Port should be configurable from the command-line. + s.listener, err = net.Listen("tcp", "127.0.0.1:0") // Port 0 means that the port is randomly picked by the system. + if err != nil { - s.log.WithError(err).Error("could not create listener") - panic(err) + s.log.WithError(err).Panic("could not create listener") } + if err := s.saveGRPCServerConfigFile(); err != nil { + s.log.WithError(err).Panic("could not write gRPC service configuration file") + } + + s.log.Info("gRPC server listening at ", s.listener.Addr()) + return &s } @@ -387,3 +403,19 @@ func (s *Service) installUpdate() { _ = s.SendEvent(NewUpdateSilentRestartNeededEvent()) } + +func (s *Service) saveGRPCServerConfigFile() error { + address, ok := s.listener.Addr().(*net.TCPAddr) + if !ok { + return fmt.Errorf("could not retrieve gRPC service listener address") + } + + sc := config{Port: address.Port} + + settingsPath, err := s.locations.ProvideSettingsPath() + if err != nil { + return err + } + + return sc.save(filepath.Join(settingsPath, serviceConfigFileName)) +}