GODT-2153: use file socket for bridge gRPC on linux & macOS.

Other: fix integration tests.
This commit is contained in:
Xavier Michelon
2022-11-25 11:21:24 +01:00
parent b7fff07197
commit d4b8f3e1c2
10 changed files with 156 additions and 33 deletions

View File

@ -237,4 +237,47 @@ SPUser randomUser()
}
//****************************************************************************************************************************************************
/// \return The OS the application is running on.
//****************************************************************************************************************************************************
OS os()
{
QString const osStr = QSysInfo::productType();
if ((osStr == "macos") || (osStr == "osx")) // Qt < 5 returns "osx", Qt6 returns "macos".
return OS::MacOS;
if (osStr == "windows")
return OS::Windows;
return OS::Linux;
}
//****************************************************************************************************************************************************
/// \return true if and only if the application is currently running on Linux.
//****************************************************************************************************************************************************
bool onLinux()
{
return OS::Linux == os();
}
//****************************************************************************************************************************************************
/// \return true if and only if the application is currently running on MacOS.
//****************************************************************************************************************************************************
bool onMacOS()
{
return OS::MacOS == os();
}
//****************************************************************************************************************************************************
/// \return true if and only if the application is currently running on Windows.
//****************************************************************************************************************************************************
bool onWindows()
{
return OS::Windows == os();
}
} // namespace bridgepp

View File

@ -27,6 +27,16 @@ namespace bridgepp
{
//****************************************************************************************************************************************************
/// \brief Enumeration for the operating system.
//****************************************************************************************************************************************************
enum class OS {
Linux = 0, ///< The Linux OS.
MacOS = 1, ///< The macOS OS.
Windows = 2, ///< The Windows OS.
};
QString userConfigDir(); ///< Get the path of the user configuration folder.
QString userCacheDir(); ///< Get the path of the user cache folder.
QString userLogsDir(); ///< Get the path of the user logs folder.
@ -35,6 +45,10 @@ qint64 randN(qint64 n); ///< return a random integer in the half open range [0,
QString randomFirstName(); ///< Get a random first name from a pre-determined list.
QString randomLastName(); ///< Get a random first name from a pre-determined list.
SPUser randomUser(); ///< Get a random user.
OS os(); ///< Return the operating system.
bool onLinux(); ///< Check if the OS is Linux.
bool onMacOS(); ///< Check if the OS is macOS.
bool onWindows(); ///< Check if the OS in Windows.
} // namespace

View File

@ -18,6 +18,7 @@
#include "GRPCClient.h"
#include "GRPCUtils.h"
#include "../BridgeUtils.h"
#include "../Exception/Exception.h"
#include "../ProcessMonitor.h"
@ -39,9 +40,17 @@ qint64 const grpcConnectionWaitTimeoutMs = 60000; ///< Timeout for the connectio
qint64 const grpcConnectionRetryDelayMs = 10000; ///< Retry delay for the gRPC connection in milliseconds.
//****************************************************************************************************************************************************
/// return true if gRPC connection should use file socket instead of TCP socket.
//****************************************************************************************************************************************************
bool useFileSocket() {
return !onWindows();
}
} // anonymous namespace
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
@ -113,11 +122,20 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
try
{
serverToken_ = config.token.toStdString();
QString address;
grpc::ChannelArguments chanArgs;
if (useFileSocket())
{
address = QString("unix://" + config.fileSocketPath);
chanArgs.SetSslTargetNameOverride("127.0.0.1"); // for file socket, we skip name verification to avoid a confusion localhost/127.0.0.1
} else {
address = QString("127.0.0.1:%1").arg(config.port);
}
SslCredentialsOptions opts;
opts.pem_root_certs += config.cert.toStdString();
QString const address = QString("127.0.0.1:%1").arg(config.port);
channel_ = CreateChannel(address.toStdString(), grpc::SslCredentials(opts));
channel_ = CreateCustomChannel(address.toStdString(), grpc::SslCredentials(opts),chanArgs);
if (!channel_)
throw Exception("Channel creation failed.");
@ -158,7 +176,6 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
if (clientToken != returnedClientToken)
throw Exception("gRPC server returned an invalid token");
if (!status.ok())
throw Exception(QString::fromStdString(status.error_message()));

View File

@ -31,6 +31,7 @@ Exception const couldNotSaveException("The service configuration file could not
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.
QString const keyFileSocketPath = "fileSocketPath"; ///< The JSON key for the file socket path.
//****************************************************************************************************************************************************
@ -88,6 +89,7 @@ bool GRPCConfig::load(QString const &path, QString *outError)
port = jsonIntValue(object, keyPort);
cert = jsonStringValue(object, keyCert);
token = jsonStringValue(object, keyToken);
fileSocketPath = jsonStringValue(object, keyFileSocketPath);
return true;
}
@ -113,6 +115,7 @@ bool GRPCConfig::save(QString const &path, QString *outError)
object.insert(keyPort, port);
object.insert(keyCert, cert);
object.insert(keyToken, token);
object.insert(keyFileSocketPath, fileSocketPath);
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))

View File

@ -29,6 +29,7 @@ public: // data members
qint32 port; ///< The port.
QString cert; ///< The server TLS certificate.
QString token; ///< The identification token.
QString fileSocketPath; ///< The path of the file socket.
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

View File

@ -27,6 +27,7 @@ type Config struct {
Port int `json:"port"`
Cert string `json:"cert"`
Token string `json:"token"`
FileSocketPath string `json:"fileSocketPath"`
}
// save saves a gRPC service configuration to file.

View File

@ -29,6 +29,7 @@ const (
dummyCert = "A dummy cert"
dummyToken = "A dummy token"
tempFileName = "test.json"
socketPath = "/a/socket/file/path"
)
func TestConfig(t *testing.T) {
@ -36,6 +37,7 @@ func TestConfig(t *testing.T) {
Port: dummyPort,
Cert: dummyCert,
Token: dummyToken,
FileSocketPath: socketPath,
}
// Read-back test

View File

@ -24,8 +24,11 @@ import (
"crypto/tls"
"errors"
"fmt"
"io/fs"
"net"
"os"
"path/filepath"
"runtime"
"sync"
"time"
@ -107,14 +110,38 @@ func NewService(
logrus.WithError(err).Panic("Could not generate gRPC TLS config")
}
listener, err := net.Listen("tcp", "127.0.0.1:0") // Port should be provided by the OS.
config := Config{
Cert: string(certPEM),
Token: uuid.NewString(),
}
var listener net.Listener
if useFileSocket() {
var err error
if config.FileSocketPath, err = computeFileSocketPath(); err != nil {
logrus.WithError(err).WithError(err).Panic("Could not create gRPC file socket")
}
listener, err = net.Listen("unix", config.FileSocketPath)
if err != nil {
logrus.WithError(err).Panic("Could not create gRPC file socket listener")
}
} else {
var err error
listener, err = net.Listen("tcp", "127.0.0.1:0") // Port should be provided by the OS.
if err != nil {
logrus.WithError(err).Panic("Could not create gRPC listener")
}
token := uuid.NewString()
// retrieve the port assigned by the system, so that we can put it in the config file.
address, ok := listener.Addr().(*net.TCPAddr)
if !ok {
return nil, fmt.Errorf("could not retrieve gRPC service listener address")
}
config.Port = address.Port
}
if path, err := saveGRPCServerConfigFile(locations, listener, token, certPEM); err != nil {
if path, err := saveGRPCServerConfigFile(locations, &config); err != nil {
logrus.WithError(err).WithField("path", path).Panic("Could not write gRPC service config file")
} else {
logrus.WithField("path", path).Info("Successfully saved gRPC service config file")
@ -123,8 +150,8 @@ func NewService(
s := &Service{
grpcServer: grpc.NewServer(
grpc.Creds(credentials.NewTLS(tlsConfig)),
grpc.UnaryInterceptor(newUnaryTokenValidator(token)),
grpc.StreamInterceptor(newStreamTokenValidator(token)),
grpc.UnaryInterceptor(newUnaryTokenValidator(config.Token)),
grpc.StreamInterceptor(newStreamTokenValidator(config.Token)),
),
listener: listener,
@ -195,7 +222,7 @@ func (s *Service) Loop() error {
s.watchEvents()
}()
s.log.Info("Starting gRPC server")
s.log.WithField("useFileSocket", useFileSocket()).Info("Starting gRPC server")
doneCh := make(chan struct{})
defer close(doneCh)
@ -456,18 +483,7 @@ func newTLSConfig() (*tls.Config, []byte, error) {
}, certPEM, nil
}
func saveGRPCServerConfigFile(locations Locator, listener net.Listener, token string, certPEM []byte) (string, error) {
address, ok := listener.Addr().(*net.TCPAddr)
if !ok {
return "", fmt.Errorf("could not retrieve gRPC service listener address")
}
sc := Config{
Port: address.Port,
Cert: string(certPEM),
Token: token,
}
func saveGRPCServerConfigFile(locations Locator, config *Config) (string, error) {
settingsPath, err := locations.ProvideSettingsPath()
if err != nil {
return "", err
@ -475,7 +491,7 @@ func saveGRPCServerConfigFile(locations Locator, listener net.Listener, token st
configPath := filepath.Join(settingsPath, serverConfigFileName)
return configPath, sc.save(configPath)
return configPath, config.save(configPath)
}
// validateServerToken verify that the server token provided by the client is valid.
@ -558,3 +574,22 @@ func (s *Service) monitorParentPID() {
}
}
}
// computeFileSocketPath Return an available path for a socket file in the temp folder.
func computeFileSocketPath() (string, error) {
tempPath := os.TempDir()
for i := 0; i < 1000; i++ {
path := filepath.Join(tempPath, fmt.Sprintf("bridge_%v.sock", uuid.NewString()))
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
return path, nil
}
}
return "", errors.New("unable to find a suitable file socket in user config folder")
}
// useFileSocket return true iff file socket should be used for the gRPC service.
func useFileSocket() bool {
//goland:noinspection GoBoolExpressions
return runtime.GOOS != "windows"
}

View File

@ -117,7 +117,7 @@ func (s *Service) quit() error {
}
// The following call is launched as a goroutine, as it will wait for current calls to end, including this one.
s.grpcServer.GracefulStop()
s.grpcServer.GracefulStop() // gRPC does clean up and remove the file socket if used.
}()
return nil

View File

@ -272,10 +272,17 @@ func (t *testCtx) initFrontendClient() error {
return fmt.Errorf("failed to append certificates to pool")
}
var target string
if len(cfg.FileSocketPath) != 0 {
target = "unix://" + cfg.FileSocketPath
} else {
target = fmt.Sprintf("%v:%d", constants.Host, cfg.Port)
}
conn, err := grpc.DialContext(
context.Background(),
fmt.Sprintf("%v:%d", constants.Host, cfg.Port),
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{RootCAs: cp})),
target,
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{RootCAs: cp, ServerName: "127.0.0.1"})),
grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
return invoker(metadata.AppendToOutgoingContext(ctx, "server-token", cfg.Token), method, req, reply, cc, opts...)
}),