From e1a0025a60486f384f72b7ddd99c6c29d48c8192 Mon Sep 17 00:00:00 2001 From: Vimal <80284964+jvimal-eg@users.noreply.github.com> Date: Mon, 11 Apr 2022 01:44:47 -0700 Subject: [PATCH] allow client/server to be initialised with a connected socket (#1385) Motivation: https://github.com/grpc/grpc-swift/issues/1353 Modifications: - A new entry on ConnectionTarget - New API on the ClientConnection builder - Some validation that we're not trying to use this API with a NIOTSEventLoopGroup or NIOTSEventLoop (as far as I can tell, using a file descriptor directly is not possible with Network.framework) - Tests Result: This allows greater flexibility in spawning the client/server; in particular, it allows unix domain sockets for sandboxed apps on macOS. --- Sources/GRPC/ClientConnection.swift | 10 ++- .../GRPC/GRPCChannel/GRPCChannelBuilder.swift | 12 ++++ Sources/GRPC/PlatformSupport.swift | 26 ++++++- Sources/GRPC/Server.swift | 3 + Sources/GRPC/ServerBuilder.swift | 6 ++ .../GRPCTests/WithConnectedSocketTests.swift | 69 +++++++++++++++++++ 6 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 Tests/GRPCTests/WithConnectedSocketTests.swift diff --git a/Sources/GRPC/ClientConnection.swift b/Sources/GRPC/ClientConnection.swift index 9cd57b951..384be70eb 100644 --- a/Sources/GRPC/ClientConnection.swift +++ b/Sources/GRPC/ClientConnection.swift @@ -266,6 +266,7 @@ public struct ConnectionTarget { case hostAndPort(String, Int) case unixDomainSocket(String) case socketAddress(SocketAddress) + case connectedSocket(NIOBSDSocket.Handle) } internal var wrapped: Wrapped @@ -293,6 +294,11 @@ public struct ConnectionTarget { return ConnectionTarget(.socketAddress(address)) } + /// A connected NIO socket. + public static func connectedSocket(_ socket: NIOBSDSocket.Handle) -> ConnectionTarget { + return ConnectionTarget(.connectedSocket(socket)) + } + @usableFromInline var host: String { switch self.wrapped { @@ -302,7 +308,7 @@ public struct ConnectionTarget { return address.host case let .socketAddress(.v6(address)): return address.host - case .unixDomainSocket, .socketAddress(.unixDomainSocket): + case .unixDomainSocket, .socketAddress(.unixDomainSocket), .connectedSocket: return "localhost" } } @@ -540,6 +546,8 @@ extension ClientBootstrapProtocol { case let .socketAddress(address): return self.connect(to: address) + case let .connectedSocket(socket): + return self.withConnectedSocket(socket) } } } diff --git a/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift b/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift index 0141bb3a7..2aed6d157 100644 --- a/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift +++ b/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift @@ -85,6 +85,18 @@ extension ClientConnection { self.configuration.tlsConfiguration = self.maybeTLS return ClientConnection(configuration: self.configuration) } + + public func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> ClientConnection { + precondition( + !PlatformSupport.isTransportServicesEventLoopGroup(self.configuration.eventLoopGroup), + "'\(#function)' requires 'group' to not be a 'NIOTransportServices.NIOTSEventLoopGroup' or 'NIOTransportServices.QoSEventLoop' (but was '\(type(of: self.configuration.eventLoopGroup))'" + ) + self.configuration.target = .connectedSocket(socket) + self.configuration.connectionBackoff = + self.connectionBackoffIsEnabled ? self.connectionBackoff : nil + self.configuration.tlsConfiguration = self.maybeTLS + return ClientConnection(configuration: self.configuration) + } } } diff --git a/Sources/GRPC/PlatformSupport.swift b/Sources/GRPC/PlatformSupport.swift index ad0c041dc..ed6635c80 100644 --- a/Sources/GRPC/PlatformSupport.swift +++ b/Sources/GRPC/PlatformSupport.swift @@ -117,17 +117,28 @@ public protocol ClientBootstrapProtocol { func connect(to: SocketAddress) -> EventLoopFuture func connect(host: String, port: Int) -> EventLoopFuture func connect(unixDomainSocketPath: String) -> EventLoopFuture + func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> EventLoopFuture func connectTimeout(_ timeout: TimeAmount) -> Self func channelOption(_ option: T, value: T.Value) -> Self where T: ChannelOption func channelInitializer(_ handler: @escaping (Channel) -> EventLoopFuture) -> Self } +extension ClientBootstrapProtocol { + public func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> EventLoopFuture { + preconditionFailure("withConnectedSocket(_:) is not implemented") + } +} + extension ClientBootstrap: ClientBootstrapProtocol {} #if canImport(Network) @available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) -extension NIOTSConnectionBootstrap: ClientBootstrapProtocol {} +extension NIOTSConnectionBootstrap: ClientBootstrapProtocol { + public func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> EventLoopFuture { + preconditionFailure("NIOTSConnectionBootstrap does not support withConnectedSocket(_:)") + } +} #endif /// This protocol is intended as a layer of abstraction over `ServerBootstrap` and @@ -136,6 +147,7 @@ public protocol ServerBootstrapProtocol { func bind(to: SocketAddress) -> EventLoopFuture func bind(host: String, port: Int) -> EventLoopFuture func bind(unixDomainSocketPath: String) -> EventLoopFuture + func withBoundSocket(_ connectedSocket: NIOBSDSocket.Handle) -> EventLoopFuture func serverChannelInitializer(_ initializer: @escaping (Channel) -> EventLoopFuture) -> Self func serverChannelOption(_ option: T, value: T.Value) -> Self where T: ChannelOption @@ -144,11 +156,21 @@ public protocol ServerBootstrapProtocol { func childChannelOption(_ option: T, value: T.Value) -> Self where T: ChannelOption } +extension ServerBootstrapProtocol { + public func withBoundSocket(_ connectedSocket: NIOBSDSocket.Handle) -> EventLoopFuture { + preconditionFailure("withBoundSocket(_:) is not implemented") + } +} + extension ServerBootstrap: ServerBootstrapProtocol {} #if canImport(Network) @available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) -extension NIOTSListenerBootstrap: ServerBootstrapProtocol {} +extension NIOTSListenerBootstrap: ServerBootstrapProtocol { + public func withBoundSocket(_ connectedSocket: NIOBSDSocket.Handle) -> EventLoopFuture { + preconditionFailure("NIOTSListenerBootstrap does not support withConnectedSocket(_:)") + } +} #endif // MARK: - Bootstrap / EventLoopGroup helpers diff --git a/Sources/GRPC/Server.swift b/Sources/GRPC/Server.swift index 792b56c88..3a77509bf 100644 --- a/Sources/GRPC/Server.swift +++ b/Sources/GRPC/Server.swift @@ -462,6 +462,9 @@ extension ServerBootstrapProtocol { case let .socketAddress(address): return self.bind(to: address) + + case let .connectedSocket(socket): + return self.withBoundSocket(socket) } } } diff --git a/Sources/GRPC/ServerBuilder.swift b/Sources/GRPC/ServerBuilder.swift index 672f046ef..3082d48c0 100644 --- a/Sources/GRPC/ServerBuilder.swift +++ b/Sources/GRPC/ServerBuilder.swift @@ -54,6 +54,12 @@ extension Server { self.configuration.tlsConfiguration = self.maybeTLS return Server.start(configuration: self.configuration) } + + public func bind(unixDomainSocketPath path: String) -> EventLoopFuture { + self.configuration.target = .unixDomainSocket(path) + self.configuration.tlsConfiguration = self.maybeTLS + return Server.start(configuration: self.configuration) + } } } diff --git a/Tests/GRPCTests/WithConnectedSocketTests.swift b/Tests/GRPCTests/WithConnectedSocketTests.swift new file mode 100644 index 000000000..eeeed3f48 --- /dev/null +++ b/Tests/GRPCTests/WithConnectedSocketTests.swift @@ -0,0 +1,69 @@ +/* + * Copyright 2022, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import EchoImplementation +import EchoModel +@testable import GRPC +import NIOCore +import NIOPosix +import XCTest + +class WithConnectedSockettests: GRPCTestCase { + func testWithConnectedSocket() throws { + let group = NIOPosix.MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + XCTAssertNoThrow(try group.syncShutdownGracefully()) + } + + let path = "/tmp/grpc-\(getpid()).sock" + // Setup a server. + let server = try Server.insecure(group: group) + .withServiceProviders([EchoProvider()]) + .withLogger(self.serverLogger) + .bind(unixDomainSocketPath: path) + .wait() + defer { + XCTAssertNoThrow(try server.close().wait()) + } + + #if os(Linux) + let sockStream = CInt(SOCK_STREAM.rawValue) + #else + let sockStream = SOCK_STREAM + #endif + let clientSocket = socket(AF_UNIX, sockStream, 0) + + XCTAssert(clientSocket != -1) + let addr = try SocketAddress(unixDomainSocketPath: path) + addr.withSockAddr { addr, size in + let ret = connect(clientSocket, addr, UInt32(size)) + XCTAssert(ret != -1) + } + let flags = fcntl(clientSocket, F_GETFL, 0) + XCTAssert(flags != -1) + XCTAssert(fcntl(clientSocket, F_SETFL, flags | O_NONBLOCK) == 0) + + let connection = ClientConnection.insecure(group: group) + .withBackgroundActivityLogger(self.clientLogger) + .withConnectedSocket(clientSocket) + defer { + XCTAssertNoThrow(try connection.close().wait()) + } + + let client = Echo_EchoClient(channel: connection) + let resp = try client.get(Echo_EchoRequest(text: "Hello")).response.wait() + XCTAssertEqual(resp.text, "Swift echo get: Hello") + } +}