forked from Silverfish/proton-bridge
GODT-2153: use file socket for bridge gRPC on linux & macOS.
Other: fix integration tests.
This commit is contained in:
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()));
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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...)
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user