diff --git a/Sources/GRPCNIOTransportCore/Client/Connection/Connection.swift b/Sources/GRPCNIOTransportCore/Client/Connection/Connection.swift index 3a3132a..a99fc09 100644 --- a/Sources/GRPCNIOTransportCore/Client/Connection/Connection.swift +++ b/Sources/GRPCNIOTransportCore/Client/Connection/Connection.swift @@ -89,6 +89,9 @@ package final class Connection: Sendable { /// being connected to. private let authority: String? + /// The name of the server used for the TLS SNI extension, if applicable. + private let sniServerHostname: String? + /// The default compression algorithm used for requests. private let defaultCompression: CompressionAlgorithm @@ -111,6 +114,20 @@ package final class Connection: Sendable { self.event.stream } + private static func sanitizeAuthorityForSNI(_ authority: String) -> String { + // Strip off a trailing ":{PORT}". Look for the last non-digit byte, if it's + // a colon then keep everything up to that index. + let index = authority.utf8.lastIndex { byte in + return byte < UInt8(ascii: "0") || byte > UInt8(ascii: "9") + } + + if let index = index, authority.utf8[index] == UInt8(ascii: ":") { + return String(authority.utf8[.. HTTP2Connection } diff --git a/Sources/GRPCNIOTransportHTTP2Posix/HTTP2ClientTransport+Posix.swift b/Sources/GRPCNIOTransportHTTP2Posix/HTTP2ClientTransport+Posix.swift index 879c3ec..c68321a 100644 --- a/Sources/GRPCNIOTransportHTTP2Posix/HTTP2ClientTransport+Posix.swift +++ b/Sources/GRPCNIOTransportHTTP2Posix/HTTP2ClientTransport+Posix.swift @@ -165,7 +165,7 @@ extension HTTP2ClientTransport.Posix { func establishConnection( to address: GRPCNIOTransportCore.SocketAddress, - authority: String? + sniServerHostname: String? ) async throws -> HTTP2Connection { let (channel, multiplexer) = try await ClientBootstrap( group: self.eventLoopGroup @@ -175,7 +175,7 @@ extension HTTP2ClientTransport.Posix { try channel.pipeline.syncOperations.addHandler( NIOSSLClientHandler( context: sslContext, - serverHostname: authority + serverHostname: sniServerHostname ) ) } diff --git a/Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ClientTransport+TransportServices.swift b/Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ClientTransport+TransportServices.swift index 4925283..386b3a0 100644 --- a/Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ClientTransport+TransportServices.swift +++ b/Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ClientTransport+TransportServices.swift @@ -149,7 +149,7 @@ extension HTTP2ClientTransport.TransportServices { func establishConnection( to address: GRPCNIOTransportCore.SocketAddress, - authority: String? + sniServerHostname: String? ) async throws -> HTTP2Connection { let bootstrap: NIOTSConnectionBootstrap let isPlainText: Bool @@ -162,7 +162,7 @@ extension HTTP2ClientTransport.TransportServices { case .tls(let tlsConfig): isPlainText = false do { - let options = try NWProtocolTLS.Options(tlsConfig, authority: authority) + let options = try NWProtocolTLS.Options(tlsConfig, authority: sniServerHostname) bootstrap = NIOTSConnectionBootstrap(group: self.eventLoopGroup) .channelOption(NIOTSChannelOptions.waitForActivity, value: false) .tlsOptions(options) diff --git a/Tests/GRPCNIOTransportCoreTests/Client/Connection/ConnectionTests.swift b/Tests/GRPCNIOTransportCoreTests/Client/Connection/ConnectionTests.swift index 4106334..3d08e67 100644 --- a/Tests/GRPCNIOTransportCoreTests/Client/Connection/ConnectionTests.swift +++ b/Tests/GRPCNIOTransportCoreTests/Client/Connection/ConnectionTests.swift @@ -21,6 +21,7 @@ import NIOCore import NIOHPACK import NIOHTTP2 import NIOPosix +import Synchronization import XCTest final class ConnectionTests: XCTestCase { @@ -199,6 +200,44 @@ final class ConnectionTests: XCTestCase { XCTAssertEqual(error.code, .unavailable) } } + + private func testAuthorityIsSanitized(authority: String, expected: String) async throws { + let recorder = SNIRecordingConnector() + let connection = Connection( + address: .ipv4(host: "ignored", port: 0), + authority: authority, + http2Connector: recorder, + defaultCompression: .none, + enabledCompression: .none + ) + + // The connect attempt will fail, but as a side effect the SNI hostname + // will be recorded. + await connection.run() + XCTAssertEqual(recorder.sniHostnames, [expected]) + } + + func testAuthorityIsSanitized() async throws { + try await self.testAuthorityIsSanitized( + authority: "foo.example.com", + expected: "foo.example.com" + ) + + try await self.testAuthorityIsSanitized( + authority: "foo.example.com:31415", + expected: "foo.example.com" + ) + + try await self.testAuthorityIsSanitized( + authority: "foo.example-31415", + expected: "foo.example-31415" + ) + + try await self.testAuthorityIsSanitized( + authority: "foo.example.com:abc123", + expected: "foo.example.com:abc123" + ) + } } extension ClientBootstrap { @@ -252,3 +291,25 @@ extension Metadata { self = metadata } } + +final class SNIRecordingConnector: HTTP2Connector { + private let _sniHostnames: Mutex<[String?]> + + var sniHostnames: [String?] { + self._sniHostnames.withLock { $0 } + } + + init() { + self._sniHostnames = Mutex([]) + } + + func establishConnection( + to address: GRPCNIOTransportCore.SocketAddress, + sniServerHostname: String? + ) async throws -> GRPCNIOTransportCore.HTTP2Connection { + self._sniHostnames.withLock { + $0.append(sniServerHostname) + } + throw RPCError(code: .unavailable, message: "Test is expected to throw.") + } +} diff --git a/Tests/GRPCNIOTransportCoreTests/Client/Connection/Utilities/HTTP2Connectors.swift b/Tests/GRPCNIOTransportCoreTests/Client/Connection/Utilities/HTTP2Connectors.swift index 7cbafa9..c7bde6e 100644 --- a/Tests/GRPCNIOTransportCoreTests/Client/Connection/Utilities/HTTP2Connectors.swift +++ b/Tests/GRPCNIOTransportCoreTests/Client/Connection/Utilities/HTTP2Connectors.swift @@ -62,7 +62,7 @@ struct ThrowingConnector: HTTP2Connector { func establishConnection( to address: GRPCNIOTransportCore.SocketAddress, - authority: String? + sniServerHostname: String? ) async throws -> HTTP2Connection { throw self.error } @@ -71,7 +71,7 @@ struct ThrowingConnector: HTTP2Connector { struct NeverConnector: HTTP2Connector { func establishConnection( to address: GRPCNIOTransportCore.SocketAddress, - authority: String? + sniServerHostname: String? ) async throws -> HTTP2Connection { fatalError("\(#function) called unexpectedly") } @@ -103,7 +103,7 @@ struct NIOPosixConnector: HTTP2Connector { func establishConnection( to address: GRPCNIOTransportCore.SocketAddress, - authority: String? + sniServerHostname: String? ) async throws -> HTTP2Connection { return try await ClientBootstrap(group: self.eventLoopGroup).connect(to: address) { channel in channel.eventLoop.makeCompletedFuture {