From 8cbd49a1913ece74a66c7503f2024346f60ccbc2 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 26 Nov 2024 16:14:24 +0000 Subject: [PATCH] Propagate the authority pseudoheader Motivation: We should be sending the ":authority" pseudoheader, somehow this was missed when doing the stream state machine. The strategy for propagating the authority will be to: - Use any override value set on the client transport, otherwise - Use the value provided by a name resolver (if any), otherwise - Derive a value from the target address. Modifications: - Add client config allowing the authority to be overridden - Add authority to the name resolver interface - Propagate the authority through to the stream state machine - Add a percent encoder, as the authority should be percent encoded - Remove hostname from the client TLS config and use the authority instead. This is generally a usability win as clients generally won't have to set this value themselves. Result: Authority is set automatically, and can be overridden if necessary. --- .../Client/Connection/Connection.swift | 12 +- .../Client/Connection/ConnectionFactory.swift | 5 +- .../Client/Connection/GRPCChannel.swift | 12 ++ .../LoadBalancers/PickFirstLoadBalancer.swift | 7 + .../RoundRobinLoadBalancer.swift | 7 + .../Connection/LoadBalancers/Subchannel.swift | 21 +- .../Client/GRPCClientStreamHandler.swift | 2 + .../Client/HTTP2ClientTransport.swift | 13 +- .../Client/Resolver/NameResolver+DNS.swift | 26 ++- .../Client/Resolver/NameResolver+UDS.swift | 28 ++- .../Client/Resolver/NameResolver.swift | 10 +- .../Client/Resolver/SocketAddress.swift | 29 +++ .../GRPCStreamStateMachine.swift | 9 + .../Internal/PercentEncoding.swift | 135 +++++++++++++ .../Config+TLS.swift | 17 +- .../HTTP2ClientTransport+Posix.swift | 8 +- .../Config+TLS.swift | 11 -- ...TP2ClientTransport+TransportServices.swift | 12 +- .../Client/Connection/ConnectionTests.swift | 1 + .../LoadBalancers/LoadBalancerTest.swift | 2 + .../RoundRobinLoadBalancerTests.swift | 2 + .../LoadBalancers/SubchannelTests.swift | 1 + .../Connection/Utilities/ConnectionTest.swift | 1 + .../Utilities/HTTP2Connectors.swift | 9 +- .../Client/GRPCClientStreamHandlerTests.swift | 15 ++ .../Client/Resolver/SocketAddressTests.swift | 14 ++ .../GRPCStreamStateMachineTests.swift | 1 + .../Internal/PercentEncodingTests.swift | 41 ++++ ...P2TransportNIOTransportServicesTests.swift | 1 - .../HTTP2TransportTLSEnabledTests.swift | 179 +++++++++--------- .../HTTP2TransportTests.swift | 138 +++++++++++++- 31 files changed, 624 insertions(+), 145 deletions(-) create mode 100644 Sources/GRPCNIOTransportCore/Internal/PercentEncoding.swift create mode 100644 Tests/GRPCNIOTransportCoreTests/Internal/PercentEncodingTests.swift diff --git a/Sources/GRPCNIOTransportCore/Client/Connection/Connection.swift b/Sources/GRPCNIOTransportCore/Client/Connection/Connection.swift index 1a4330a..a2defda 100644 --- a/Sources/GRPCNIOTransportCore/Client/Connection/Connection.swift +++ b/Sources/GRPCNIOTransportCore/Client/Connection/Connection.swift @@ -85,6 +85,10 @@ package final class Connection: Sendable { /// The address to connect to. private let address: SocketAddress + /// The server authority. If `nil`, a value will be computed based on the endpoint being + /// connected to. + private let authority: String? + /// The default compression algorithm used for requests. private let defaultCompression: CompressionAlgorithm @@ -109,11 +113,13 @@ package final class Connection: Sendable { package init( address: SocketAddress, + authority: String?, http2Connector: any HTTP2Connector, defaultCompression: CompressionAlgorithm, enabledCompression: CompressionAlgorithmSet ) { self.address = address + self.authority = authority self.defaultCompression = defaultCompression self.enabledCompression = enabledCompression self.http2Connector = http2Connector @@ -129,7 +135,10 @@ package final class Connection: Sendable { package func run() async { func establishConnectionOrThrow() async throws(RPCError) -> HTTP2Connection { do { - return try await self.http2Connector.establishConnection(to: self.address) + return try await self.http2Connector.establishConnection( + to: self.address, + authority: self.authority ?? self.address.sniHostname + ) } catch let error as RPCError { throw error } catch { @@ -214,6 +223,7 @@ package final class Connection: Sendable { let streamHandler = GRPCClientStreamHandler( methodDescriptor: descriptor, scheme: scheme, + authority: self.authority ?? self.address.authority, outboundEncoding: compression, acceptedEncodings: self.enabledCompression, maxPayloadSize: maxRequestSize diff --git a/Sources/GRPCNIOTransportCore/Client/Connection/ConnectionFactory.swift b/Sources/GRPCNIOTransportCore/Client/Connection/ConnectionFactory.swift index 3063ead..9ce584d 100644 --- a/Sources/GRPCNIOTransportCore/Client/Connection/ConnectionFactory.swift +++ b/Sources/GRPCNIOTransportCore/Client/Connection/ConnectionFactory.swift @@ -19,7 +19,10 @@ package import NIOHTTP2 internal import NIOPosix package protocol HTTP2Connector: Sendable { - func establishConnection(to address: SocketAddress) async throws -> HTTP2Connection + func establishConnection( + to address: SocketAddress, + authority: String? + ) async throws -> HTTP2Connection } package struct HTTP2Connection: Sendable { diff --git a/Sources/GRPCNIOTransportCore/Client/Connection/GRPCChannel.swift b/Sources/GRPCNIOTransportCore/Client/Connection/GRPCChannel.swift index 8bd5e46..53c954d 100644 --- a/Sources/GRPCNIOTransportCore/Client/Connection/GRPCChannel.swift +++ b/Sources/GRPCNIOTransportCore/Client/Connection/GRPCChannel.swift @@ -52,6 +52,10 @@ package final class GRPCChannel: ClientTransport { /// A factory for connections. private let connector: any HTTP2Connector + /// The server authority. If `nil`, a value will be computed based on the endpoint being + /// connected to. + private let authority: String? + /// The connection backoff configuration used by the subchannel when establishing a connection. private let backoff: ConnectionBackoff @@ -82,6 +86,12 @@ package final class GRPCChannel: ClientTransport { self.input = AsyncStream.makeStream() self.connector = connector + if let authority = config.http2.authority ?? resolver.authority { + self.authority = PercentEncoding.encodeAuthority(authority) + } else { + self.authority = nil + } + self.backoff = ConnectionBackoff( initial: config.backoff.initial, max: config.backoff.max, @@ -446,6 +456,7 @@ extension GRPCChannel { state.changeLoadBalancerKind(to: loadBalancerConfig) { let loadBalancer = RoundRobinLoadBalancer( connector: self.connector, + authority: self.authority, backoff: self.backoff, defaultCompression: self.defaultCompression, enabledCompression: self.enabledCompression @@ -463,6 +474,7 @@ extension GRPCChannel { state.changeLoadBalancerKind(to: loadBalancerConfig) { let loadBalancer = PickFirstLoadBalancer( connector: self.connector, + authority: self.authority, backoff: self.backoff, defaultCompression: self.defaultCompression, enabledCompression: self.enabledCompression diff --git a/Sources/GRPCNIOTransportCore/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift b/Sources/GRPCNIOTransportCore/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift index 0cbd63b..39c958e 100644 --- a/Sources/GRPCNIOTransportCore/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift +++ b/Sources/GRPCNIOTransportCore/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift @@ -77,6 +77,10 @@ package final class PickFirstLoadBalancer: Sendable { /// A connector, capable of creating connections. private let connector: any HTTP2Connector + /// The server authority. If `nil`, a value will be computed based on the endpoint being + /// connected to. + private let authority: String? + /// Connection backoff configuration. private let backoff: ConnectionBackoff @@ -94,11 +98,13 @@ package final class PickFirstLoadBalancer: Sendable { package init( connector: any HTTP2Connector, + authority: String?, backoff: ConnectionBackoff, defaultCompression: CompressionAlgorithm, enabledCompression: CompressionAlgorithmSet ) { self.connector = connector + self.authority = authority self.backoff = backoff self.defaultCompression = defaultCompression self.enabledCompression = enabledCompression @@ -174,6 +180,7 @@ extension PickFirstLoadBalancer { endpoint: endpoint, id: id, connector: self.connector, + authority: self.authority, backoff: self.backoff, defaultCompression: self.defaultCompression, enabledCompression: self.enabledCompression diff --git a/Sources/GRPCNIOTransportCore/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift b/Sources/GRPCNIOTransportCore/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift index df917e5..ce3244a 100644 --- a/Sources/GRPCNIOTransportCore/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift +++ b/Sources/GRPCNIOTransportCore/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift @@ -109,6 +109,10 @@ package final class RoundRobinLoadBalancer: Sendable { /// A connector, capable of creating connections. private let connector: any HTTP2Connector + /// The server authority. If `nil`, a value will be computed based on the endpoint being + /// connected to. + private let authority: String? + /// Connection backoff configuration. private let backoff: ConnectionBackoff @@ -123,11 +127,13 @@ package final class RoundRobinLoadBalancer: Sendable { package init( connector: any HTTP2Connector, + authority: String?, backoff: ConnectionBackoff, defaultCompression: CompressionAlgorithm, enabledCompression: CompressionAlgorithmSet ) { self.connector = connector + self.authority = authority self.backoff = backoff self.defaultCompression = defaultCompression self.enabledCompression = enabledCompression @@ -223,6 +229,7 @@ extension RoundRobinLoadBalancer { endpoint: endpoint, id: id, connector: self.connector, + authority: self.authority, backoff: self.backoff, defaultCompression: self.defaultCompression, enabledCompression: self.enabledCompression diff --git a/Sources/GRPCNIOTransportCore/Client/Connection/LoadBalancers/Subchannel.swift b/Sources/GRPCNIOTransportCore/Client/Connection/LoadBalancers/Subchannel.swift index b2be017..207a9a4 100644 --- a/Sources/GRPCNIOTransportCore/Client/Connection/LoadBalancers/Subchannel.swift +++ b/Sources/GRPCNIOTransportCore/Client/Connection/LoadBalancers/Subchannel.swift @@ -83,6 +83,10 @@ package final class Subchannel: Sendable { /// A factory for connections. private let connector: any HTTP2Connector + /// The server authority. If `nil`, a value will be computed based on the endpoint being + /// connected to. + private let authority: String? + /// The connection backoff configuration used by the subchannel when establishing a connection. private let backoff: ConnectionBackoff @@ -96,6 +100,7 @@ package final class Subchannel: Sendable { endpoint: Endpoint, id: SubchannelID, connector: any HTTP2Connector, + authority: String?, backoff: ConnectionBackoff, defaultCompression: CompressionAlgorithm, enabledCompression: CompressionAlgorithmSet @@ -106,6 +111,7 @@ package final class Subchannel: Sendable { self.endpoint = endpoint self.id = id self.connector = connector + self.authority = authority self.backoff = backoff self.defaultCompression = defaultCompression self.enabledCompression = enabledCompression @@ -194,6 +200,7 @@ extension Subchannel { state.makeConnection( to: self.endpoint.addresses, using: self.connector, + authority: self.authority, backoff: self.backoff, defaultCompression: self.defaultCompression, enabledCompression: self.enabledCompression @@ -283,7 +290,10 @@ extension Subchannel { } private func handleConnectFailedEvent(in group: inout DiscardingTaskGroup, error: RPCError) { - let onConnectFailed = self.state.withLock { $0.connectFailed(connector: self.connector) } + let onConnectFailed = self.state.withLock { + $0.connectFailed(connector: self.connector, authority: self.authority) + } + switch onConnectFailed { case .connect(let connection): // Try the next address. @@ -469,6 +479,7 @@ extension Subchannel { mutating func makeConnection( to addresses: [SocketAddress], using connector: any HTTP2Connector, + authority: String?, backoff: ConnectionBackoff, defaultCompression: CompressionAlgorithm, enabledCompression: CompressionAlgorithmSet @@ -480,6 +491,7 @@ extension Subchannel { let connection = Connection( address: address, + authority: authority, http2Connector: connector, defaultCompression: defaultCompression, enabledCompression: enabledCompression @@ -563,7 +575,10 @@ extension Subchannel { case backoff(Duration) } - mutating func connectFailed(connector: any HTTP2Connector) -> OnConnectFailed { + mutating func connectFailed( + connector: any HTTP2Connector, + authority: String? + ) -> OnConnectFailed { let onConnectFailed: OnConnectFailed switch self { @@ -571,6 +586,7 @@ extension Subchannel { if let address = state.addressIterator.next() { state.connection = Connection( address: address, + authority: authority, http2Connector: connector, defaultCompression: .none, enabledCompression: .all @@ -582,6 +598,7 @@ extension Subchannel { let address = state.addressIterator.next()! state.connection = Connection( address: address, + authority: authority, http2Connector: connector, defaultCompression: .none, enabledCompression: .all diff --git a/Sources/GRPCNIOTransportCore/Client/GRPCClientStreamHandler.swift b/Sources/GRPCNIOTransportCore/Client/GRPCClientStreamHandler.swift index f5c72cc..db9c43f 100644 --- a/Sources/GRPCNIOTransportCore/Client/GRPCClientStreamHandler.swift +++ b/Sources/GRPCNIOTransportCore/Client/GRPCClientStreamHandler.swift @@ -33,6 +33,7 @@ final class GRPCClientStreamHandler: ChannelDuplexHandler { init( methodDescriptor: MethodDescriptor, scheme: Scheme, + authority: String?, outboundEncoding: CompressionAlgorithm, acceptedEncodings: CompressionAlgorithmSet, maxPayloadSize: Int, @@ -43,6 +44,7 @@ final class GRPCClientStreamHandler: ChannelDuplexHandler { .init( methodDescriptor: methodDescriptor, scheme: scheme, + authority: authority, outboundEncoding: outboundEncoding, acceptedEncodings: acceptedEncodings ) diff --git a/Sources/GRPCNIOTransportCore/Client/HTTP2ClientTransport.swift b/Sources/GRPCNIOTransportCore/Client/HTTP2ClientTransport.swift index 7f0f938..4bfbf4f 100644 --- a/Sources/GRPCNIOTransportCore/Client/HTTP2ClientTransport.swift +++ b/Sources/GRPCNIOTransportCore/Client/HTTP2ClientTransport.swift @@ -144,15 +144,24 @@ extension HTTP2ClientTransport.Config { /// The value is clamped to `... (1 << 31) - 1`. public var targetWindowSize: Int + /// The authority of the server. + /// + /// Any value set here will unconditionally override any value derived from the target address. + /// + /// The server authority is used in the ":authority" pseudoheader and in the TLS SNI + /// extension, if applicable. + public var authority: String? + /// Creates a new HTTP/2 configuration. - public init(maxFrameSize: Int, targetWindowSize: Int) { + public init(maxFrameSize: Int, targetWindowSize: Int, authority: String?) { self.maxFrameSize = maxFrameSize self.targetWindowSize = targetWindowSize + self.authority = authority } /// Default values, max frame size is 16KiB, and the target window size is 8MiB. public static var defaults: Self { - Self(maxFrameSize: 1 << 14, targetWindowSize: 8 * 1024 * 1024) + Self(maxFrameSize: 1 << 14, targetWindowSize: 8 * 1024 * 1024, authority: nil) } } } diff --git a/Sources/GRPCNIOTransportCore/Client/Resolver/NameResolver+DNS.swift b/Sources/GRPCNIOTransportCore/Client/Resolver/NameResolver+DNS.swift index ca57918..48aa77e 100644 --- a/Sources/GRPCNIOTransportCore/Client/Resolver/NameResolver+DNS.swift +++ b/Sources/GRPCNIOTransportCore/Client/Resolver/NameResolver+DNS.swift @@ -26,13 +26,15 @@ extension ResolvableTargets { public var host: String /// The port to use with resolved addresses. - public var port: Int + /// + /// If no port is specified then 443 is used. + public var port: Int? /// Create a new DNS target. /// - Parameters: /// - host: The host to resolve via DNS. /// - port: The port to use with resolved addresses. - public init(host: String, port: Int) { + public init(host: String, port: Int?) { self.host = host self.port = port } @@ -43,9 +45,9 @@ extension ResolvableTarget where Self == ResolvableTargets.DNS { /// Creates a new resolvable DNS target. /// - Parameters: /// - host: The host address to resolve. - /// - port: The port to use for each resolved address. + /// - port: The port to use for each resolved address. 443 will be used if unspecified. /// - Returns: A ``ResolvableTarget``. - public static func dns(host: String, port: Int = 443) -> Self { + public static func dns(host: String, port: Int? = nil) -> Self { return Self(host: host, port: port) } } @@ -60,7 +62,14 @@ extension NameResolvers { public func resolver(for target: Target) -> NameResolver { let resolver = Self.Resolver(target: target) - return NameResolver(names: RPCAsyncSequence(wrapping: resolver), updateMode: .pull) + // Only append the port if explicitly set. If it's nil the default port of 443 is used + // should be omitted from the authority. + let authority = target.host + (target.port.map { ":\($0)" } ?? "") + return NameResolver( + names: RPCAsyncSequence(wrapping: resolver), + updateMode: .pull, + authority: authority + ) } } } @@ -79,13 +88,16 @@ extension NameResolvers.DNS { let addresses: [SocketAddress] do { - addresses = try await DNSResolver.resolve(host: self.target.host, port: self.target.port) + addresses = try await DNSResolver.resolve( + host: self.target.host, + port: self.target.port ?? 443 // Assume TLS if no port is specified. + ) } catch let error as CancellationError { throw error } catch { throw RPCError( code: .internalError, - message: "Couldn't resolve address for \(self.target.host):\(self.target.port)", + message: "Couldn't resolve address for \(self.target.host):\(self.target.port ?? 443)", cause: error ) } diff --git a/Sources/GRPCNIOTransportCore/Client/Resolver/NameResolver+UDS.swift b/Sources/GRPCNIOTransportCore/Client/Resolver/NameResolver+UDS.swift index 124d17d..5fcfdb1 100644 --- a/Sources/GRPCNIOTransportCore/Client/Resolver/NameResolver+UDS.swift +++ b/Sources/GRPCNIOTransportCore/Client/Resolver/NameResolver+UDS.swift @@ -25,18 +25,32 @@ extension ResolvableTargets { /// The Unix Domain Socket address. public var address: SocketAddress.UnixDomainSocket + /// The authority of the service. + /// + /// If unset then the path of the address will be used. + public var authority: String? + /// Create a new Unix Domain Socket address. - public init(address: SocketAddress.UnixDomainSocket) { + public init(address: SocketAddress.UnixDomainSocket, authority: String?) { self.address = address + self.authority = authority } } } extension ResolvableTarget where Self == ResolvableTargets.UnixDomainSocket { /// Creates a new resolvable Unix Domain Socket target. - /// - Parameter path: The path of the socket. - public static func unixDomainSocket(path: String) -> Self { - return Self(address: SocketAddress.UnixDomainSocket(path: path)) + /// - Parameters + /// - path: The path of the socket. + /// - authority: The service authority. + public static func unixDomainSocket( + path: String, + authority: String? = nil + ) -> Self { + return Self( + address: SocketAddress.UnixDomainSocket(path: path), + authority: authority + ) } } @@ -53,7 +67,11 @@ extension NameResolvers { public func resolver(for target: Target) -> NameResolver { let endpoint = Endpoint(addresses: [.unixDomainSocket(target.address)]) let resolutionResult = NameResolutionResult(endpoints: [endpoint], serviceConfig: nil) - return NameResolver(names: .constant(resolutionResult), updateMode: .pull) + return NameResolver( + names: .constant(resolutionResult), + updateMode: .pull, + authority: target.authority ?? target.address.path + ) } } } diff --git a/Sources/GRPCNIOTransportCore/Client/Resolver/NameResolver.swift b/Sources/GRPCNIOTransportCore/Client/Resolver/NameResolver.swift index cd3cf2e..0eb0339 100644 --- a/Sources/GRPCNIOTransportCore/Client/Resolver/NameResolver.swift +++ b/Sources/GRPCNIOTransportCore/Client/Resolver/NameResolver.swift @@ -31,6 +31,9 @@ public struct NameResolver: Sendable { /// How ``names`` is updated and should be consumed. public let updateMode: UpdateMode + /// The authority of the service. + public let authority: String? + public struct UpdateMode: Hashable, Sendable { enum Value: Hashable, Sendable { case push @@ -51,9 +54,14 @@ public struct NameResolver: Sendable { } /// Create a new name resolver. - public init(names: RPCAsyncSequence, updateMode: UpdateMode) { + public init( + names: RPCAsyncSequence, + updateMode: UpdateMode, + authority: String? = nil + ) { self.names = names self.updateMode = updateMode + self.authority = authority } } diff --git a/Sources/GRPCNIOTransportCore/Client/Resolver/SocketAddress.swift b/Sources/GRPCNIOTransportCore/Client/Resolver/SocketAddress.swift index 8f5d961..b701160 100644 --- a/Sources/GRPCNIOTransportCore/Client/Resolver/SocketAddress.swift +++ b/Sources/GRPCNIOTransportCore/Client/Resolver/SocketAddress.swift @@ -69,6 +69,35 @@ public struct SocketAddress: Hashable, Sendable { } } +extension SocketAddress { + package var authority: String { + let rawValue: String + + switch self.value { + case .ipv4(let address): + rawValue = "\(address.host):\(address.port)" + case .ipv6(let address): + rawValue = "[\(address.host)]:\(address.port)" + case .unix(let address): + rawValue = address.path + case .vsock(let address): + rawValue = "\(address.contextID.rawValue):\(address.port.rawValue)" + } + + return PercentEncoding.encodeAuthority(rawValue) + } + + package var sniHostname: String? { + switch self.value { + case .ipv4, .ipv6: + // Literal IP addresses aren't allowed in the SNI hostname. + return nil + case .vsock, .unix: + return self.authority + } + } +} + extension SocketAddress { /// Creates a socket address by wrapping a ``SocketAddress/IPv4-swift.struct``. public static func ipv4(_ address: IPv4) -> Self { diff --git a/Sources/GRPCNIOTransportCore/GRPCStreamStateMachine.swift b/Sources/GRPCNIOTransportCore/GRPCStreamStateMachine.swift index 6ce1e3d..a42dd64 100644 --- a/Sources/GRPCNIOTransportCore/GRPCStreamStateMachine.swift +++ b/Sources/GRPCNIOTransportCore/GRPCStreamStateMachine.swift @@ -31,17 +31,20 @@ enum GRPCStreamStateMachineConfiguration { struct ClientConfiguration { var methodDescriptor: MethodDescriptor var scheme: Scheme + var authority: String? var outboundEncoding: CompressionAlgorithm var acceptedEncodings: CompressionAlgorithmSet init( methodDescriptor: MethodDescriptor, scheme: Scheme, + authority: String?, outboundEncoding: CompressionAlgorithm, acceptedEncodings: CompressionAlgorithmSet ) { self.methodDescriptor = methodDescriptor self.scheme = scheme + self.authority = authority self.outboundEncoding = outboundEncoding self.acceptedEncodings = acceptedEncodings.union(.none) } @@ -630,6 +633,7 @@ extension GRPCStreamStateMachine { private func makeClientHeaders( methodDescriptor: MethodDescriptor, scheme: Scheme, + authority: String?, outboundEncoding: CompressionAlgorithm?, acceptedEncodings: CompressionAlgorithmSet, customMetadata: Metadata @@ -645,6 +649,9 @@ extension GRPCStreamStateMachine { headers.add("POST", forKey: .method) headers.add(scheme.rawValue, forKey: .scheme) headers.add(methodDescriptor.path, forKey: .path) + if let authority = authority { + headers.add(authority, forKey: .authority) + } // Add required gRPC headers. headers.add(ContentType.grpc.canonicalValue, forKey: .contentType) @@ -690,6 +697,7 @@ extension GRPCStreamStateMachine { return self.makeClientHeaders( methodDescriptor: configuration.methodDescriptor, scheme: configuration.scheme, + authority: configuration.authority, outboundEncoding: configuration.outboundEncoding, acceptedEncodings: configuration.acceptedEncodings, customMetadata: metadata @@ -1764,6 +1772,7 @@ extension MethodDescriptor { } internal enum GRPCHTTP2Keys: String { + case authority = ":authority" case path = ":path" case contentType = "content-type" case encoding = "grpc-encoding" diff --git a/Sources/GRPCNIOTransportCore/Internal/PercentEncoding.swift b/Sources/GRPCNIOTransportCore/Internal/PercentEncoding.swift new file mode 100644 index 0000000..e902274 --- /dev/null +++ b/Sources/GRPCNIOTransportCore/Internal/PercentEncoding.swift @@ -0,0 +1,135 @@ +/* + * Copyright 2024, 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. + */ + +package enum PercentEncoding { + package static func encodeAuthority(_ input: String) -> String { + Self.encode(input) { + Self.isAuthorityChar($0) + } + } + + package static func encode( + _ input: String, + isValidCharacter: (UInt8) -> Bool + ) -> String { + var output: [UInt8] = [] + output.reserveCapacity(input.utf8.count) + + for char in input.utf8 { + if isValidCharacter(char) { + output.append(char) + } else { + output.append(UInt8(ascii: "%")) + output.append(Self.hexByte(char >> 4)) + output.append(Self.hexByte(char & 0xF)) + } + } + + return String(decoding: output, as: UTF8.self) + } + + private static func hexByte(_ nibble: UInt8) -> UInt8 { + assert(nibble & 0xF == nibble) + + switch nibble { + case 0 ... 9: + return nibble &+ UInt8(ascii: "0") + default: + return nibble &+ (UInt8(ascii: "A") &- 10) + } + } + + // Characters from RFC 3986 § 2.2 + private static func isAlphaNumericChar(_ char: UInt8) -> Bool { + switch char { + case UInt8(ascii: "a") ... UInt8(ascii: "z"): + return true + case UInt8(ascii: "A") ... UInt8(ascii: "Z"): + return true + case UInt8(ascii: "0") ... UInt8(ascii: "9"): + return true + default: + return false + } + } + + // Characters from RFC 3986 § 2.2 + private static func isSubDelimChar(_ char: UInt8) -> Bool { + switch char { + case UInt8(ascii: "!"): + return true + case UInt8(ascii: "$"): + return true + case UInt8(ascii: "&"): + return true + case UInt8(ascii: "'"): + return true + case UInt8(ascii: "("): + return true + case UInt8(ascii: ")"): + return true + case UInt8(ascii: "*"): + return true + case UInt8(ascii: "+"): + return true + case UInt8(ascii: ","): + return true + case UInt8(ascii: ";"): + return true + case UInt8(ascii: "="): + return true + default: + return false + } + } + + // Characters from RFC 3986 § 2.3 + private static func isUnreservedChar(_ char: UInt8) -> Bool { + if Self.isAlphaNumericChar(char) { return true } + + switch char { + case UInt8(ascii: "-"): + return true + case UInt8(ascii: "."): + return true + case UInt8(ascii: "_"): + return true + case UInt8(ascii: "~"): + return true + default: + return false + } + } + + // Characters from RFC 3986 § 3.2 + private static func isAuthorityChar(_ char: UInt8) -> Bool { + if Self.isUnreservedChar(char) { return true } + if Self.isSubDelimChar(char) { return true } + + switch char { + case UInt8(ascii: ":"): + return true + case UInt8(ascii: "["): + return true + case UInt8(ascii: "]"): + return true + case UInt8(ascii: "@"): + return true + default: + return false + } + } +} diff --git a/Sources/GRPCNIOTransportHTTP2Posix/Config+TLS.swift b/Sources/GRPCNIOTransportHTTP2Posix/Config+TLS.swift index 5f100d4..aaf1de2 100644 --- a/Sources/GRPCNIOTransportHTTP2Posix/Config+TLS.swift +++ b/Sources/GRPCNIOTransportHTTP2Posix/Config+TLS.swift @@ -159,28 +159,22 @@ extension HTTP2ClientTransport.Posix.Config { /// The trust roots to be used when verifying server certificates. public var trustRoots: TLSConfig.TrustRootsSource - /// An optional server hostname to use when verifying certificates. - public var serverHostname: String? - /// Create a new HTTP2 NIO Posix client transport TLS config. /// - Parameters: /// - certificateChain: The certificates the client will offer during negotiation. /// - privateKey: The private key associated with the leaf certificate. /// - serverCertificateVerification: How to verify the server certificate, if one is presented. /// - trustRoots: The trust roots to be used when verifying server certificates. - /// - serverHostname: An optional server hostname to use when verifying certificates. public init( certificateChain: [TLSConfig.CertificateSource], privateKey: TLSConfig.PrivateKeySource?, serverCertificateVerification: TLSConfig.CertificateVerification, - trustRoots: TLSConfig.TrustRootsSource, - serverHostname: String? + trustRoots: TLSConfig.TrustRootsSource ) { self.certificateChain = certificateChain self.privateKey = privateKey self.serverCertificateVerification = serverCertificateVerification self.trustRoots = trustRoots - self.serverHostname = serverHostname } /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted: @@ -188,7 +182,6 @@ extension HTTP2ClientTransport.Posix.Config { /// - `privateKey` equals `nil` /// - `serverCertificateVerification` equals `fullVerification` /// - `trustRoots` equals `systemDefault` - /// - `serverHostname` equals `nil` /// /// - Parameters: /// - configure: A closure which allows you to modify the defaults before returning them. @@ -200,8 +193,7 @@ extension HTTP2ClientTransport.Posix.Config { certificateChain: [], privateKey: nil, serverCertificateVerification: .fullVerification, - trustRoots: .systemDefault, - serverHostname: nil + trustRoots: .systemDefault ) configure(&config) return config @@ -212,14 +204,12 @@ extension HTTP2ClientTransport.Posix.Config { /// - `privateKey` equals `nil` /// - `serverCertificateVerification` equals `fullVerification` /// - `trustRoots` equals `systemDefault` - /// - `serverHostname` equals `nil` public static var defaults: Self { .defaults() } /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted to match /// the requirements of mTLS: /// - `trustRoots` equals `systemDefault` /// - `serverCertificateVerification` equals `fullVerification` - /// - `serverHostname` equals `nil` /// /// - Parameters: /// - certificateChain: The certificates the client will offer during negotiation. @@ -235,8 +225,7 @@ extension HTTP2ClientTransport.Posix.Config { certificateChain: certificateChain, privateKey: privateKey, serverCertificateVerification: .fullVerification, - trustRoots: .systemDefault, - serverHostname: nil + trustRoots: .systemDefault ) configure(&config) return config diff --git a/Sources/GRPCNIOTransportHTTP2Posix/HTTP2ClientTransport+Posix.swift b/Sources/GRPCNIOTransportHTTP2Posix/HTTP2ClientTransport+Posix.swift index b94faf6..00eed8e 100644 --- a/Sources/GRPCNIOTransportHTTP2Posix/HTTP2ClientTransport+Posix.swift +++ b/Sources/GRPCNIOTransportHTTP2Posix/HTTP2ClientTransport+Posix.swift @@ -127,7 +127,6 @@ extension HTTP2ClientTransport.Posix { private let eventLoopGroup: any EventLoopGroup private let sslContext: NIOSSLContext? - private let serverHostname: String? private let isPlainText: Bool init(eventLoopGroup: any EventLoopGroup, config: HTTP2ClientTransport.Posix.Config) throws { @@ -137,12 +136,10 @@ extension HTTP2ClientTransport.Posix { switch self.config.transportSecurity.wrapped { case .plaintext: self.sslContext = nil - self.serverHostname = nil self.isPlainText = true case .tls(let tlsConfig): do { self.sslContext = try NIOSSLContext(configuration: TLSConfiguration(tlsConfig)) - self.serverHostname = tlsConfig.serverHostname self.isPlainText = false } catch { throw RuntimeError( @@ -155,7 +152,8 @@ extension HTTP2ClientTransport.Posix { } func establishConnection( - to address: GRPCNIOTransportCore.SocketAddress + to address: GRPCNIOTransportCore.SocketAddress, + authority: String? ) async throws -> HTTP2Connection { let (channel, multiplexer) = try await ClientBootstrap( group: self.eventLoopGroup @@ -165,7 +163,7 @@ extension HTTP2ClientTransport.Posix { try channel.pipeline.syncOperations.addHandler( NIOSSLClientHandler( context: sslContext, - serverHostname: self.serverHostname + serverHostname: authority ) ) } diff --git a/Sources/GRPCNIOTransportHTTP2TransportServices/Config+TLS.swift b/Sources/GRPCNIOTransportHTTP2TransportServices/Config+TLS.swift index bf898ff..118b524 100644 --- a/Sources/GRPCNIOTransportHTTP2TransportServices/Config+TLS.swift +++ b/Sources/GRPCNIOTransportHTTP2TransportServices/Config+TLS.swift @@ -148,9 +148,6 @@ extension HTTP2ClientTransport.TransportServices.Config { /// - Important: If specifying custom certificates, they must be DER-encoded X509 certificates. public var trustRoots: TLSConfig.TrustRootsSource - /// An optional server hostname to use when verifying certificates. - public var serverHostname: String? - /// An optional provider for the `SecIdentity` to be used when setting up TLS. public var identityProvider: (@Sendable () throws -> SecIdentity)? @@ -158,16 +155,13 @@ extension HTTP2ClientTransport.TransportServices.Config { /// - Parameters: /// - serverCertificateVerification: How to verify the server certificate, if one is presented. /// - trustRoots: The trust roots to be used when verifying server certificates. - /// - serverHostname: An optional server hostname to use when verifying certificates. /// - identityProvider: A provider for the `SecIdentity` to be used when setting up TLS. public init( serverCertificateVerification: TLSConfig.CertificateVerification, trustRoots: TLSConfig.TrustRootsSource, - serverHostname: String?, identityProvider: (@Sendable () throws -> SecIdentity)? ) { self.serverCertificateVerification = serverCertificateVerification - self.serverHostname = serverHostname self.trustRoots = trustRoots self.identityProvider = identityProvider } @@ -175,7 +169,6 @@ extension HTTP2ClientTransport.TransportServices.Config { /// Create a new HTTP2 NIO Transport Services transport TLS config, with some values defaulted: /// - `serverCertificateVerification` equals `fullVerification` /// - `trustRoots` equals `systemDefault` - /// - `serverHostname` equals `nil` /// - `identityProvider` equals `nil` /// /// - Parameters: @@ -187,7 +180,6 @@ extension HTTP2ClientTransport.TransportServices.Config { var config = Self( serverCertificateVerification: .fullVerification, trustRoots: .systemDefault, - serverHostname: nil, identityProvider: nil ) configure(&config) @@ -197,7 +189,6 @@ extension HTTP2ClientTransport.TransportServices.Config { /// Create a new HTTP2 NIO Transport Services transport TLS config, with some values defaulted: /// - `serverCertificateVerification` equals `fullVerification` /// - `trustRoots` equals `systemDefault` - /// - `serverHostname` equals `nil` /// - `identityProvider` equals `nil` public static var defaults: Self { .defaults() } @@ -205,7 +196,6 @@ extension HTTP2ClientTransport.TransportServices.Config { /// the requirements of mTLS: /// - `serverCertificateVerification` equals `fullVerification` /// - `trustRoots` equals `systemDefault` - /// - `serverHostname` equals `nil` /// /// - Parameters: /// - identityProvider: A provider for the `SecIdentity` to be used when setting up TLS. @@ -218,7 +208,6 @@ extension HTTP2ClientTransport.TransportServices.Config { var config = Self( serverCertificateVerification: .fullVerification, trustRoots: .systemDefault, - serverHostname: nil, identityProvider: identityProvider ) configure(&config) diff --git a/Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ClientTransport+TransportServices.swift b/Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ClientTransport+TransportServices.swift index 92f1590..77c2495 100644 --- a/Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ClientTransport+TransportServices.swift +++ b/Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ClientTransport+TransportServices.swift @@ -137,7 +137,8 @@ extension HTTP2ClientTransport.TransportServices { } func establishConnection( - to address: GRPCNIOTransportCore.SocketAddress + to address: GRPCNIOTransportCore.SocketAddress, + authority: String? ) async throws -> HTTP2Connection { let bootstrap: NIOTSConnectionBootstrap let isPlainText: Bool @@ -150,7 +151,7 @@ extension HTTP2ClientTransport.TransportServices { case .tls(let tlsConfig): isPlainText = false do { - let options = try NWProtocolTLS.Options(tlsConfig) + let options = try NWProtocolTLS.Options(tlsConfig, authority: authority) bootstrap = NIOTSConnectionBootstrap(group: self.eventLoopGroup) .channelOption(NIOTSChannelOptions.waitForActivity, value: false) .tlsOptions(options) @@ -309,7 +310,10 @@ extension ClientTransport where Self == HTTP2ClientTransport.TransportServices { } extension NWProtocolTLS.Options { - convenience init(_ tlsConfig: HTTP2ClientTransport.TransportServices.Config.TLS) throws { + convenience init( + _ tlsConfig: HTTP2ClientTransport.TransportServices.Config.TLS, + authority: String? + ) throws { self.init() if let identityProvider = tlsConfig.identityProvider { @@ -341,7 +345,7 @@ extension NWProtocolTLS.Options { self.securityProtocolOptions, true ) - tlsConfig.serverHostname?.withCString { serverName in + authority?.withCString { serverName in sec_protocol_options_set_tls_server_name( self.securityProtocolOptions, serverName diff --git a/Tests/GRPCNIOTransportCoreTests/Client/Connection/ConnectionTests.swift b/Tests/GRPCNIOTransportCoreTests/Client/Connection/ConnectionTests.swift index 71e0e26..47a1d5f 100644 --- a/Tests/GRPCNIOTransportCoreTests/Client/Connection/ConnectionTests.swift +++ b/Tests/GRPCNIOTransportCoreTests/Client/Connection/ConnectionTests.swift @@ -187,6 +187,7 @@ final class ConnectionTests: XCTestCase { func testMakeStreamOnNotRunningConnection() async throws { let connection = Connection( address: .ipv4(host: "ignored", port: 0), + authority: "ignored", http2Connector: .never, defaultCompression: .none, enabledCompression: .none diff --git a/Tests/GRPCNIOTransportCoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift b/Tests/GRPCNIOTransportCoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift index d5a4ae0..396b2e8 100644 --- a/Tests/GRPCNIOTransportCoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift +++ b/Tests/GRPCNIOTransportCoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift @@ -41,6 +41,7 @@ enum LoadBalancerTest { ) { let pickFirst = PickFirstLoadBalancer( connector: connector, + authority: nil, backoff: backoff, defaultCompression: .none, enabledCompression: .none @@ -67,6 +68,7 @@ enum LoadBalancerTest { ) { let roundRobin = RoundRobinLoadBalancer( connector: connector, + authority: nil, backoff: backoff, defaultCompression: .none, enabledCompression: .none diff --git a/Tests/GRPCNIOTransportCoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift b/Tests/GRPCNIOTransportCoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift index 2915d76..ff825a0 100644 --- a/Tests/GRPCNIOTransportCoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift +++ b/Tests/GRPCNIOTransportCoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift @@ -297,6 +297,7 @@ final class RoundRobinLoadBalancerTests: XCTestCase { func testPickSubchannelWhenNotReady() { let loadBalancer = RoundRobinLoadBalancer( connector: .never, + authority: "ignored", backoff: .defaults, defaultCompression: .none, enabledCompression: .none @@ -308,6 +309,7 @@ final class RoundRobinLoadBalancerTests: XCTestCase { func testPickSubchannelWhenClosed() async { let loadBalancer = RoundRobinLoadBalancer( connector: .never, + authority: "ignored", backoff: .defaults, defaultCompression: .none, enabledCompression: .none diff --git a/Tests/GRPCNIOTransportCoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift b/Tests/GRPCNIOTransportCoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift index 069a03a..545aed6 100644 --- a/Tests/GRPCNIOTransportCoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift +++ b/Tests/GRPCNIOTransportCoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift @@ -568,6 +568,7 @@ final class SubchannelTests: XCTestCase { endpoint: Endpoint(addresses: addresses), id: SubchannelID(), connector: connector, + authority: nil, backoff: backoff ?? .defaults, defaultCompression: .none, enabledCompression: .none diff --git a/Tests/GRPCNIOTransportCoreTests/Client/Connection/Utilities/ConnectionTest.swift b/Tests/GRPCNIOTransportCoreTests/Client/Connection/Utilities/ConnectionTest.swift index 6653f06..c64a2f6 100644 --- a/Tests/GRPCNIOTransportCoreTests/Client/Connection/Utilities/ConnectionTest.swift +++ b/Tests/GRPCNIOTransportCoreTests/Client/Connection/Utilities/ConnectionTest.swift @@ -42,6 +42,7 @@ enum ConnectionTest { try await withThrowingTaskGroup(of: Void.self) { group in let connection = Connection( address: address, + authority: nil, http2Connector: connector, defaultCompression: .none, enabledCompression: .none diff --git a/Tests/GRPCNIOTransportCoreTests/Client/Connection/Utilities/HTTP2Connectors.swift b/Tests/GRPCNIOTransportCoreTests/Client/Connection/Utilities/HTTP2Connectors.swift index a5c6ce7..f23631a 100644 --- a/Tests/GRPCNIOTransportCoreTests/Client/Connection/Utilities/HTTP2Connectors.swift +++ b/Tests/GRPCNIOTransportCoreTests/Client/Connection/Utilities/HTTP2Connectors.swift @@ -61,7 +61,8 @@ struct ThrowingConnector: HTTP2Connector { } func establishConnection( - to address: GRPCNIOTransportCore.SocketAddress + to address: GRPCNIOTransportCore.SocketAddress, + authority: String? ) async throws -> HTTP2Connection { throw self.error } @@ -69,7 +70,8 @@ struct ThrowingConnector: HTTP2Connector { struct NeverConnector: HTTP2Connector { func establishConnection( - to address: GRPCNIOTransportCore.SocketAddress + to address: GRPCNIOTransportCore.SocketAddress, + authority: String? ) async throws -> HTTP2Connection { fatalError("\(#function) called unexpectedly") } @@ -100,7 +102,8 @@ struct NIOPosixConnector: HTTP2Connector { } func establishConnection( - to address: GRPCNIOTransportCore.SocketAddress + to address: GRPCNIOTransportCore.SocketAddress, + authority: String? ) async throws -> HTTP2Connection { return try await ClientBootstrap(group: self.eventLoopGroup).connect(to: address) { channel in channel.eventLoop.makeCompletedFuture { diff --git a/Tests/GRPCNIOTransportCoreTests/Client/GRPCClientStreamHandlerTests.swift b/Tests/GRPCNIOTransportCoreTests/Client/GRPCClientStreamHandlerTests.swift index 5d7dca2..b2c09c7 100644 --- a/Tests/GRPCNIOTransportCoreTests/Client/GRPCClientStreamHandlerTests.swift +++ b/Tests/GRPCNIOTransportCoreTests/Client/GRPCClientStreamHandlerTests.swift @@ -29,6 +29,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { let handler = GRPCClientStreamHandler( methodDescriptor: .testTest, scheme: .http, + authority: nil, outboundEncoding: .none, acceptedEncodings: [], maxPayloadSize: 1 @@ -60,6 +61,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { let handler = GRPCClientStreamHandler( methodDescriptor: .testTest, scheme: .http, + authority: nil, outboundEncoding: .none, acceptedEncodings: [], maxPayloadSize: 1, @@ -96,6 +98,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { let handler = GRPCClientStreamHandler( methodDescriptor: .testTest, scheme: .http, + authority: nil, outboundEncoding: .none, acceptedEncodings: [], maxPayloadSize: 1, @@ -127,6 +130,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { let handler = GRPCClientStreamHandler( methodDescriptor: .testTest, scheme: .http, + authority: nil, outboundEncoding: .none, acceptedEncodings: [], maxPayloadSize: 1, @@ -164,6 +168,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { let handler = GRPCClientStreamHandler( methodDescriptor: .testTest, scheme: .http, + authority: nil, outboundEncoding: .none, acceptedEncodings: [], maxPayloadSize: 1, @@ -200,6 +205,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { let handler = GRPCClientStreamHandler( methodDescriptor: .testTest, scheme: .http, + authority: nil, outboundEncoding: .deflate, acceptedEncodings: [.deflate], maxPayloadSize: 1 @@ -258,6 +264,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { let handler = GRPCClientStreamHandler( methodDescriptor: .testTest, scheme: .http, + authority: nil, outboundEncoding: .none, acceptedEncodings: [], maxPayloadSize: 1, @@ -325,6 +332,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { let handler = GRPCClientStreamHandler( methodDescriptor: .testTest, scheme: .http, + authority: nil, outboundEncoding: .none, acceptedEncodings: [], maxPayloadSize: 100, @@ -393,6 +401,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { let handler = GRPCClientStreamHandler( methodDescriptor: .testTest, scheme: .http, + authority: nil, outboundEncoding: .none, acceptedEncodings: [], maxPayloadSize: 1, @@ -459,6 +468,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { let handler = GRPCClientStreamHandler( methodDescriptor: .testTest, scheme: .http, + authority: nil, outboundEncoding: .none, acceptedEncodings: [], maxPayloadSize: 100, @@ -577,6 +587,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { let handler = GRPCClientStreamHandler( methodDescriptor: .testTest, scheme: .http, + authority: nil, outboundEncoding: .none, acceptedEncodings: [], maxPayloadSize: 100, @@ -682,6 +693,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { let handler = GRPCClientStreamHandler( methodDescriptor: .testTest, scheme: .http, + authority: nil, outboundEncoding: .none, acceptedEncodings: [], maxPayloadSize: 100, @@ -766,6 +778,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { let handler = GRPCClientStreamHandler( methodDescriptor: .testTest, scheme: .http, + authority: nil, outboundEncoding: .none, acceptedEncodings: [], maxPayloadSize: 1, @@ -816,6 +829,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { let handler = GRPCClientStreamHandler( methodDescriptor: .testTest, scheme: .http, + authority: nil, outboundEncoding: .none, acceptedEncodings: [], maxPayloadSize: 1, @@ -862,6 +876,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { let handler = GRPCClientStreamHandler( methodDescriptor: .testTest, scheme: .http, + authority: nil, outboundEncoding: .none, acceptedEncodings: [], maxPayloadSize: 1, diff --git a/Tests/GRPCNIOTransportCoreTests/Client/Resolver/SocketAddressTests.swift b/Tests/GRPCNIOTransportCoreTests/Client/Resolver/SocketAddressTests.swift index 6b731a9..cd210c3 100644 --- a/Tests/GRPCNIOTransportCoreTests/Client/Resolver/SocketAddressTests.swift +++ b/Tests/GRPCNIOTransportCoreTests/Client/Resolver/SocketAddressTests.swift @@ -77,4 +77,18 @@ final class SocketAddressTests: XCTestCase { vsock.port = .any XCTAssertDescription(vsock, "[vsock]-1:-1") } + + func testAuthority() { + var address: SocketAddress = .ipv4(host: "127.0.0.1", port: 42) + XCTAssertEqual(address.authority, "127.0.0.1:42") + + address = .ipv6(host: "::1", port: 42) + XCTAssertEqual(address.authority, "[::1]:42") + + address = .unixDomainSocket(path: "foo") + XCTAssertEqual(address.authority, "foo") + + address = .vsock(contextID: 42, port: 8000) + XCTAssertEqual(address.authority, "42:8000") + } } diff --git a/Tests/GRPCNIOTransportCoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCNIOTransportCoreTests/GRPCStreamStateMachineTests.swift index b310ad4..41f41b0 100644 --- a/Tests/GRPCNIOTransportCoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCNIOTransportCoreTests/GRPCStreamStateMachineTests.swift @@ -145,6 +145,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { .init( methodDescriptor: .testTest, scheme: .http, + authority: nil, outboundEncoding: compressionEnabled ? .deflate : .none, acceptedEncodings: [.deflate] ) diff --git a/Tests/GRPCNIOTransportCoreTests/Internal/PercentEncodingTests.swift b/Tests/GRPCNIOTransportCoreTests/Internal/PercentEncodingTests.swift new file mode 100644 index 0000000..d447c18 --- /dev/null +++ b/Tests/GRPCNIOTransportCoreTests/Internal/PercentEncodingTests.swift @@ -0,0 +1,41 @@ +/* + * Copyright 2024, 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 GRPCNIOTransportCore +import Testing + +@Suite("Percent encoding") +struct PercentEncodingTests { + @Test( + "encode ':authority'", + arguments: [ + ("", ""), + ("foo", "foo"), + ("FOO", "FOO"), + ("f00", "f00"), + ("f0&", "f0&"), + ("f**", "f**"), + ("fo#", "fo%23"), + ("fo/o|bar", "fo%2Fo%7Cbar"), + ("foo?bar", "foo%3Fbar"), + ("foo", "foo%3Cbar%3E"), + ] + ) + func encodeAuthority(_ input: String, expected: String) { + let encoded = PercentEncoding.encodeAuthority(input) + #expect(encoded == expected) + } +} diff --git a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOTransportServicesTests.swift b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOTransportServicesTests.swift index c3866ca..568623b 100644 --- a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOTransportServicesTests.swift +++ b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOTransportServicesTests.swift @@ -229,7 +229,6 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { XCTAssertEqual(grpcConfig.backoff, HTTP2ClientTransport.Config.Backoff.defaults) XCTAssertNil(grpcTLSConfig.identityProvider) - XCTAssertNil(grpcTLSConfig.serverHostname) XCTAssertEqual(grpcTLSConfig.serverCertificateVerification, .fullVerification) XCTAssertEqual(grpcTLSConfig.trustRoots, .systemDefault) } diff --git a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift index 3945751..8cd5f1d 100644 --- a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift +++ b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift @@ -37,18 +37,18 @@ struct HTTP2TransportTLSEnabledTests { serverTransport: TransportKind ) async throws { let certificateKeyPairs = try SelfSignedCertificateKeyPairs() - let clientTransportConfig = self.makeDefaultClientTLSConfig( + let clientConfig = self.makeDefaultTLSClientConfig( for: clientTransport, certificateKeyPairs: certificateKeyPairs ) - let serverTransportConfig = self.makeDefaultServerTLSConfig( + let serverConfig = self.makeDefaultTLSServerConfig( for: serverTransport, certificateKeyPairs: certificateKeyPairs ) try await self.withClientAndServer( - clientTLSConfig: clientTransportConfig, - serverTLSConfig: serverTransportConfig + clientConfig: clientConfig, + serverConfig: serverConfig ) { control in await #expect(throws: Never.self) { try await self.executeUnaryRPC(control: control) @@ -66,20 +66,20 @@ struct HTTP2TransportTLSEnabledTests { serverTransport: TransportKind ) async throws { let certificateKeyPairs = try SelfSignedCertificateKeyPairs() - let clientTransportConfig = self.makeMTLSClientTLSConfig( + let clientConfig = self.makeMTLSClientConfig( for: clientTransport, certificateKeyPairs: certificateKeyPairs, serverHostname: "localhost" ) - let serverTransportConfig = self.makeMTLSServerTLSConfig( + let serverConfig = self.makeMTLSServerConfig( for: serverTransport, certificateKeyPairs: certificateKeyPairs, includeClientCertificateInTrustRoots: true ) try await self.withClientAndServer( - clientTLSConfig: clientTransportConfig, - serverTLSConfig: serverTransportConfig + clientConfig: clientConfig, + serverConfig: serverConfig ) { control in await #expect(throws: Never.self) { try await self.executeUnaryRPC(control: control) @@ -98,20 +98,21 @@ struct HTTP2TransportTLSEnabledTests { serverTransport: TransportKind ) async throws { let certificateKeyPairs = try SelfSignedCertificateKeyPairs() - let clientTransportConfig = self.makeMTLSClientTLSConfig( + let clientConfig = self.makeMTLSClientConfig( for: clientTransport, certificateKeyPairs: certificateKeyPairs, - serverHostname: nil + serverHostname: "the-wrong-hostname" ) - let serverTransportConfig = self.makeMTLSServerTLSConfig( + + let serverConfig = self.makeMTLSServerConfig( for: serverTransport, certificateKeyPairs: certificateKeyPairs, includeClientCertificateInTrustRoots: true ) try await self.withClientAndServer( - clientTLSConfig: clientTransportConfig, - serverTLSConfig: serverTransportConfig + clientConfig: clientConfig, + serverConfig: serverConfig ) { control in await #expect { try await self.executeUnaryRPC(control: control) @@ -152,20 +153,20 @@ struct HTTP2TransportTLSEnabledTests { serverTransport: TransportKind ) async throws { let certificateKeyPairs = try SelfSignedCertificateKeyPairs() - let clientTransportConfig = self.makeMTLSClientTLSConfig( + let clientConfig = self.makeMTLSClientConfig( for: clientTransport, certificateKeyPairs: certificateKeyPairs, serverHostname: "localhost" ) - let serverTransportConfig = self.makeMTLSServerTLSConfig( + let serverConfig = self.makeMTLSServerConfig( for: serverTransport, certificateKeyPairs: certificateKeyPairs, includeClientCertificateInTrustRoots: false ) try await self.withClientAndServer( - clientTLSConfig: clientTransportConfig, - serverTLSConfig: serverTransportConfig + clientConfig: clientConfig, + serverConfig: serverConfig ) { control in await #expect { try await self.executeUnaryRPC(control: control) @@ -201,106 +202,118 @@ struct HTTP2TransportTLSEnabledTests { case posix } - enum TLSConfig { + enum Config { enum Client { - case posix(HTTP2ClientTransport.Posix.Config.TransportSecurity) + case posix(HTTP2ClientTransport.Posix.Config) } enum Server { - case posix(HTTP2ServerTransport.Posix.Config.TransportSecurity) + case posix(HTTP2ServerTransport.Posix.Config) + } + } + + private func makeDefaultPlaintextPosixClientConfig() -> HTTP2ClientTransport.Posix.Config { + .defaults(transportSecurity: .plaintext) { config in + config.backoff.initial = .milliseconds(100) + config.backoff.multiplier = 1 + config.backoff.jitter = 0 } } - func makeDefaultClientTLSConfig( + private func makeDefaultTLSClientConfig( for transportSecurity: TransportKind, certificateKeyPairs: SelfSignedCertificateKeyPairs - ) -> TLSConfig.Client { + ) -> Config.Client { switch transportSecurity { case .posix: - return .posix( - .tls( - .defaults { - $0.trustRoots = .certificates([ - .bytes(certificateKeyPairs.server.certificate, format: .der) - ]) - $0.serverHostname = "localhost" - } - ) + var config = self.makeDefaultPlaintextPosixClientConfig() + config.transportSecurity = .tls( + .defaults { + $0.trustRoots = .certificates([ + .bytes(certificateKeyPairs.server.certificate, format: .der) + ]) + } ) + config.http2.authority = "localhost" + return .posix(config) } } - func makeMTLSClientTLSConfig( + private func makeMTLSClientConfig( for transportKind: TransportKind, certificateKeyPairs: SelfSignedCertificateKeyPairs, serverHostname: String? - ) -> TLSConfig.Client { + ) -> Config.Client { switch transportKind { case .posix: - return .posix( - .tls( - .mTLS( - certificateChain: [.bytes(certificateKeyPairs.client.certificate, format: .der)], - privateKey: .bytes(certificateKeyPairs.client.key, format: .der) - ) { - $0.trustRoots = .certificates([ - .bytes(certificateKeyPairs.server.certificate, format: .der) - ]) - $0.serverHostname = serverHostname - } - ) + var config = self.makeDefaultPlaintextPosixClientConfig() + config.transportSecurity = .tls( + .mTLS( + certificateChain: [.bytes(certificateKeyPairs.client.certificate, format: .der)], + privateKey: .bytes(certificateKeyPairs.client.key, format: .der) + ) { + $0.trustRoots = .certificates([ + .bytes(certificateKeyPairs.server.certificate, format: .der) + ]) + } ) + config.http2.authority = serverHostname + return .posix(config) } } - func makeDefaultServerTLSConfig( + private func makeDefaultPlaintextPosixServerConfig() -> HTTP2ServerTransport.Posix.Config { + .defaults(transportSecurity: .plaintext) + } + + private func makeDefaultTLSServerConfig( for transportKind: TransportKind, certificateKeyPairs: SelfSignedCertificateKeyPairs - ) -> TLSConfig.Server { + ) -> Config.Server { switch transportKind { case .posix: - return .posix( - .tls( - .defaults( - certificateChain: [.bytes(certificateKeyPairs.server.certificate, format: .der)], - privateKey: .bytes(certificateKeyPairs.server.key, format: .der) - ) + var config = self.makeDefaultPlaintextPosixServerConfig() + config.transportSecurity = .tls( + .defaults( + certificateChain: [.bytes(certificateKeyPairs.server.certificate, format: .der)], + privateKey: .bytes(certificateKeyPairs.server.key, format: .der) ) ) + return .posix(config) } } - func makeMTLSServerTLSConfig( + private func makeMTLSServerConfig( for transportKind: TransportKind, certificateKeyPairs: SelfSignedCertificateKeyPairs, includeClientCertificateInTrustRoots: Bool - ) -> TLSConfig.Server { + ) -> Config.Server { switch transportKind { case .posix: - return .posix( - .tls( - .mTLS( - certificateChain: [.bytes(certificateKeyPairs.server.certificate, format: .der)], - privateKey: .bytes(certificateKeyPairs.server.key, format: .der) - ) { - if includeClientCertificateInTrustRoots { - $0.trustRoots = .certificates([ - .bytes(certificateKeyPairs.client.certificate, format: .der) - ]) - } + var config = self.makeDefaultPlaintextPosixServerConfig() + config.transportSecurity = .tls( + .mTLS( + certificateChain: [.bytes(certificateKeyPairs.server.certificate, format: .der)], + privateKey: .bytes(certificateKeyPairs.server.key, format: .der) + ) { + if includeClientCertificateInTrustRoots { + $0.trustRoots = .certificates([ + .bytes(certificateKeyPairs.client.certificate, format: .der) + ]) } - ) + } ) + return .posix(config) } } func withClientAndServer( - clientTLSConfig: TLSConfig.Client, - serverTLSConfig: TLSConfig.Server, + clientConfig: Config.Client, + serverConfig: Config.Server, _ test: (ControlClient) async throws -> Void ) async throws { try await withThrowingDiscardingTaskGroup { group in - let server = self.makeServer(tlsConfig: serverTLSConfig) + let server = self.makeServer(config: serverConfig) group.addTask { try await server.serve() @@ -311,7 +324,7 @@ struct HTTP2TransportTLSEnabledTests { return } let target: any ResolvableTarget = .ipv4(host: address.host, port: address.port) - let client = try self.makeClient(tlsConfig: clientTLSConfig, target: target) + let client = try self.makeClient(config: clientConfig, target: target) group.addTask { try await client.run() @@ -325,15 +338,15 @@ struct HTTP2TransportTLSEnabledTests { } } - private func makeServer(tlsConfig: TLSConfig.Server) -> GRPCServer { + private func makeServer(config: Config.Server) -> GRPCServer { let services = [ControlService()] - switch tlsConfig { - case .posix(let transportSecurity): + switch config { + case .posix(let config): let server = GRPCServer( transport: .http2NIOPosix( address: .ipv4(host: "127.0.0.1", port: 0), - config: .defaults(transportSecurity: transportSecurity) + config: config ), services: services ) @@ -343,22 +356,14 @@ struct HTTP2TransportTLSEnabledTests { } private func makeClient( - tlsConfig: TLSConfig.Client, + config: Config.Client, target: any ResolvableTarget ) throws -> GRPCClient { let transport: any ClientTransport - switch tlsConfig { - case .posix(let transportSecurity): - transport = try HTTP2ClientTransport.Posix( - target: target, - config: .defaults(transportSecurity: transportSecurity) { config in - config.backoff.initial = .milliseconds(100) - config.backoff.multiplier = 1 - config.backoff.jitter = 0 - }, - serviceConfig: ServiceConfig() - ) + switch config { + case .posix(let config): + transport = try HTTP2ClientTransport.Posix(target: target, config: config) } return GRPCClient(transport: transport) diff --git a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTests.swift b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTests.swift index 1adc9e1..8bbaa72 100644 --- a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTests.swift +++ b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTests.swift @@ -1442,7 +1442,6 @@ final class HTTP2TransportTests: XCTestCase { let responses = try await response.messages.reduce(into: []) { $0.append($1) } XCTAssert(responses.isEmpty) } - } } } @@ -1478,6 +1477,143 @@ final class HTTP2TransportTests: XCTestCase { } } } + + private func checkAuthority(client: GRPCClient, expected: String) async throws { + let control = ControlClient(wrapping: client) + let input = ControlInput.with { + $0.echoMetadataInHeaders = true + $0.echoMetadataInTrailers = true + $0.numberOfMessages = 1 + $0.payloadParameters = .with { + $0.content = 0 + $0.size = 1024 + } + } + + try await control.unary(request: ClientRequest(message: input)) { response in + let initial = response.metadata + XCTAssertEqual(Array(initial["echo-authority"]), [.string(expected)]) + } + } + + private func testAuthority( + serverAddress: SocketAddress, + authorityOverride override: String? = nil, + clientTarget: (SocketAddress) -> any ResolvableTarget, + expectedAuthority: (SocketAddress) -> String + ) async throws { + try await withGRPCServer( + transport: .http2NIOPosix( + address: serverAddress, + config: .defaults(transportSecurity: .plaintext) + ), + services: [ControlService()] + ) { server in + guard let listeningAddress = try await server.listeningAddress else { + XCTFail("No listening address") + return + } + + let target = clientTarget(listeningAddress) + try await withGRPCClient( + transport: .http2NIOPosix( + target: target, + config: .defaults(transportSecurity: .plaintext) { + $0.http2.authority = override + } + ) + ) { client in + try await self.checkAuthority(client: client, expected: expectedAuthority(listeningAddress)) + } + } + } + + func testAuthorityDNS() async throws { + try await self.testAuthority(serverAddress: .ipv4(host: "127.0.0.1", port: 0)) { address in + return .dns(host: "localhost", port: address.ipv4!.port) + } expectedAuthority: { address in + return "localhost:\(address.ipv4!.port)" + } + } + + func testOverrideAuthorityDNS() async throws { + try await self.testAuthority( + serverAddress: .ipv4(host: "127.0.0.1", port: 0), + authorityOverride: "respect-my-authority" + ) { address in + return .dns(host: "localhost", port: address.ipv4!.port) + } expectedAuthority: { _ in + return "respect-my-authority" + } + } + + func testAuthorityIPv4() async throws { + try await self.testAuthority(serverAddress: .ipv4(host: "127.0.0.1", port: 0)) { address in + return .ipv4(host: "127.0.0.1", port: address.ipv4!.port) + } expectedAuthority: { address in + return "127.0.0.1:\(address.ipv4!.port)" + } + } + + func testOverrideAuthorityIPv4() async throws { + try await self.testAuthority( + serverAddress: .ipv4(host: "127.0.0.1", port: 0), + authorityOverride: "respect-my-authority" + ) { address in + return .ipv4(host: "127.0.0.1", port: address.ipv4!.port) + } expectedAuthority: { _ in + return "respect-my-authority" + } + } + + func testAuthorityIPv6() async throws { + try await self.testAuthority(serverAddress: .ipv6(host: "::1", port: 0)) { address in + return .ipv6(host: "::1", port: address.ipv6!.port) + } expectedAuthority: { address in + return "[::1]:\(address.ipv6!.port)" + } + } + + func testOverrideAuthorityIPv6() async throws { + try await self.testAuthority( + serverAddress: .ipv6(host: "::1", port: 0), + authorityOverride: "respect-my-authority" + ) { address in + return .ipv6(host: "::1", port: address.ipv6!.port) + } expectedAuthority: { _ in + return "respect-my-authority" + } + } + + func testAuthorityUDS() async throws { + let path = "test-authority-uds" + try await self.testAuthority(serverAddress: .unixDomainSocket(path: path)) { address in + return .unixDomainSocket(path: path) + } expectedAuthority: { _ in + return path + } + } + + func testAuthorityLocalUDSOverride() async throws { + let path = "test-authority-local-uds-override" + try await self.testAuthority(serverAddress: .unixDomainSocket(path: path)) { address in + return .unixDomainSocket(path: path, authority: "respect-my-authority") + } expectedAuthority: { _ in + return "respect-my-authority" + } + } + + func testOverrideAuthorityUDS() async throws { + let path = "test-override-authority-uds" + try await self.testAuthority( + serverAddress: .unixDomainSocket(path: path), + authorityOverride: "respect-my-authority" + ) { _ in + return .unixDomainSocket(path: path, authority: "should-be-ignored") + } expectedAuthority: { _ in + return "respect-my-authority" + } + } } extension [HTTP2TransportTests.Transport] {