/src/grpc-swift/Sources/GRPC/ConnectionManagerChannelProvider.swift
Line | Count | Source |
1 | | /* |
2 | | * Copyright 2021, gRPC Authors All rights reserved. |
3 | | * |
4 | | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | | * you may not use this file except in compliance with the License. |
6 | | * You may obtain a copy of the License at |
7 | | * |
8 | | * http://www.apache.org/licenses/LICENSE-2.0 |
9 | | * |
10 | | * Unless required by applicable law or agreed to in writing, software |
11 | | * distributed under the License is distributed on an "AS IS" BASIS, |
12 | | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | | * See the License for the specific language governing permissions and |
14 | | * limitations under the License. |
15 | | */ |
16 | | import Logging |
17 | | import NIOCore |
18 | | import NIOPosix |
19 | | import NIOTransportServices |
20 | | |
21 | | #if canImport(NIOSSL) |
22 | | import NIOSSL |
23 | | #endif |
24 | | |
25 | | #if canImport(Network) |
26 | | import Network |
27 | | #endif |
28 | | |
29 | | @usableFromInline |
30 | | internal protocol ConnectionManagerChannelProvider { |
31 | | /// Make an `EventLoopFuture<Channel>`. |
32 | | /// |
33 | | /// - Parameters: |
34 | | /// - connectionManager: The `ConnectionManager` requesting the `Channel`. |
35 | | /// - eventLoop: The `EventLoop` to use for the`Channel`. |
36 | | /// - connectTimeout: Optional connection timeout when starting the connection. |
37 | | /// - logger: A logger. |
38 | | func makeChannel( |
39 | | managedBy connectionManager: ConnectionManager, |
40 | | onEventLoop eventLoop: EventLoop, |
41 | | connectTimeout: TimeAmount?, |
42 | | logger: Logger |
43 | | ) -> EventLoopFuture<Channel> |
44 | | } |
45 | | |
46 | | @usableFromInline |
47 | | internal struct DefaultChannelProvider: ConnectionManagerChannelProvider { |
48 | | @usableFromInline |
49 | | enum TLSMode { |
50 | | #if canImport(NIOSSL) |
51 | | case configureWithNIOSSL(Result<NIOSSLContext, Error>) |
52 | | #endif // canImport(NIOSSL) |
53 | | case configureWithNetworkFramework |
54 | | case disabled |
55 | | } |
56 | | |
57 | | @usableFromInline |
58 | | internal var connectionTarget: ConnectionTarget |
59 | | @usableFromInline |
60 | | internal var connectionKeepalive: ClientConnectionKeepalive |
61 | | @usableFromInline |
62 | | internal var connectionIdleTimeout: TimeAmount |
63 | | @usableFromInline |
64 | | internal var connectionMaxAge: TimeAmount? |
65 | | |
66 | | @usableFromInline |
67 | | internal var tlsMode: TLSMode |
68 | | @usableFromInline |
69 | | internal var tlsConfiguration: GRPCTLSConfiguration? |
70 | | |
71 | | @usableFromInline |
72 | | internal var httpTargetWindowSize: Int |
73 | | @usableFromInline |
74 | | internal var httpMaxFrameSize: Int |
75 | | @usableFromInline |
76 | | internal var httpMaxResetStreams: Int |
77 | | |
78 | | @usableFromInline |
79 | | internal var errorDelegate: Optional<ClientErrorDelegate> |
80 | | @usableFromInline |
81 | | internal var debugChannelInitializer: Optional<(Channel) -> EventLoopFuture<Void>> |
82 | | |
83 | | #if canImport(Network) |
84 | | @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) |
85 | | @usableFromInline |
86 | | internal var nwParametersConfigurator: (@Sendable (NWParameters) -> Void)? { |
87 | | get { |
88 | | self._nwParametersConfigurator as! (@Sendable (NWParameters) -> Void)? |
89 | | } |
90 | | set { |
91 | | self._nwParametersConfigurator = newValue |
92 | | } |
93 | | } |
94 | | |
95 | | private var _nwParametersConfigurator: (any Sendable)? |
96 | | #endif |
97 | | |
98 | | #if canImport(Network) |
99 | | @inlinable |
100 | | @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) |
101 | | internal init( |
102 | | connectionTarget: ConnectionTarget, |
103 | | connectionKeepalive: ClientConnectionKeepalive, |
104 | | connectionIdleTimeout: TimeAmount, |
105 | | connectionMaxAge: TimeAmount?, |
106 | | tlsMode: TLSMode, |
107 | | tlsConfiguration: GRPCTLSConfiguration?, |
108 | | httpTargetWindowSize: Int, |
109 | | httpMaxFrameSize: Int, |
110 | | httpMaxResetStreams: Int, |
111 | | errorDelegate: ClientErrorDelegate?, |
112 | | debugChannelInitializer: ((Channel) -> EventLoopFuture<Void>)?, |
113 | | nwParametersConfigurator: (@Sendable (NWParameters) -> Void)? |
114 | | ) { |
115 | | self.init( |
116 | | connectionTarget: connectionTarget, |
117 | | connectionKeepalive: connectionKeepalive, |
118 | | connectionIdleTimeout: connectionIdleTimeout, |
119 | | connectionMaxAge: connectionMaxAge, |
120 | | tlsMode: tlsMode, |
121 | | tlsConfiguration: tlsConfiguration, |
122 | | httpTargetWindowSize: httpTargetWindowSize, |
123 | | httpMaxFrameSize: httpMaxFrameSize, |
124 | | httpMaxResetStreams: httpMaxResetStreams, |
125 | | errorDelegate: errorDelegate, |
126 | | debugChannelInitializer: debugChannelInitializer |
127 | | ) |
128 | | |
129 | | self.nwParametersConfigurator = nwParametersConfigurator |
130 | | } |
131 | | #endif |
132 | | |
133 | | @inlinable |
134 | | internal init( |
135 | | connectionTarget: ConnectionTarget, |
136 | | connectionKeepalive: ClientConnectionKeepalive, |
137 | | connectionIdleTimeout: TimeAmount, |
138 | | connectionMaxAge: TimeAmount?, |
139 | | tlsMode: TLSMode, |
140 | | tlsConfiguration: GRPCTLSConfiguration?, |
141 | | httpTargetWindowSize: Int, |
142 | | httpMaxFrameSize: Int, |
143 | | httpMaxResetStreams: Int, |
144 | | errorDelegate: ClientErrorDelegate?, |
145 | | debugChannelInitializer: ((Channel) -> EventLoopFuture<Void>)? |
146 | 0 | ) { |
147 | 0 | self.connectionTarget = connectionTarget |
148 | 0 | self.connectionKeepalive = connectionKeepalive |
149 | 0 | self.connectionIdleTimeout = connectionIdleTimeout |
150 | 0 | self.connectionMaxAge = connectionMaxAge |
151 | 0 |
|
152 | 0 | self.tlsMode = tlsMode |
153 | 0 | self.tlsConfiguration = tlsConfiguration |
154 | 0 |
|
155 | 0 | self.httpTargetWindowSize = httpTargetWindowSize |
156 | 0 | self.httpMaxFrameSize = httpMaxFrameSize |
157 | 0 | self.httpMaxResetStreams = httpMaxResetStreams |
158 | 0 |
|
159 | 0 | self.errorDelegate = errorDelegate |
160 | 0 | self.debugChannelInitializer = debugChannelInitializer |
161 | 0 | } |
162 | | |
163 | 0 | internal init(configuration: ClientConnection.Configuration) { |
164 | 0 | // Making a `NIOSSLContext` is expensive and we should only do it (at most) once per TLS |
165 | 0 | // configuration. We do it now and store it in our `tlsMode` and surface any error during |
166 | 0 | // channel creation (we're limited by our API in when we can throw any error). |
167 | 0 | let tlsMode: TLSMode |
168 | 0 |
|
169 | 0 | if let tlsConfiguration = configuration.tlsConfiguration { |
170 | 0 | if tlsConfiguration.isNetworkFrameworkTLSBackend { |
171 | 0 | tlsMode = .configureWithNetworkFramework |
172 | 0 | } else { |
173 | | #if canImport(NIOSSL) |
174 | 0 | // The '!' is okay here, we have a `tlsConfiguration` (so we must be using TLS) and we know |
175 | 0 | // it's not backed by Network.framework, so it must be backed by NIOSSL. |
176 | 0 | tlsMode = .configureWithNIOSSL(Result { try tlsConfiguration.makeNIOSSLContext()! }) |
177 | | #else |
178 | | // TLS is configured, and we aren't using a Network.framework TLS backend, so we must be |
179 | | // using NIOSSL, so we must be able to import it. |
180 | | fatalError() |
181 | | #endif // canImport(NIOSSL) |
182 | 0 | } |
183 | 0 | } else { |
184 | 0 | tlsMode = .disabled |
185 | 0 | } |
186 | 0 |
|
187 | 0 | self.init( |
188 | 0 | connectionTarget: configuration.target, |
189 | 0 | connectionKeepalive: configuration.connectionKeepalive, |
190 | 0 | connectionIdleTimeout: configuration.connectionIdleTimeout, |
191 | 0 | connectionMaxAge: configuration.connectionMaxAge, |
192 | 0 | tlsMode: tlsMode, |
193 | 0 | tlsConfiguration: configuration.tlsConfiguration, |
194 | 0 | httpTargetWindowSize: configuration.httpTargetWindowSize, |
195 | 0 | httpMaxFrameSize: configuration.httpMaxFrameSize, |
196 | 0 | httpMaxResetStreams: configuration.httpMaxResetStreams, |
197 | 0 | errorDelegate: configuration.errorDelegate, |
198 | 0 | debugChannelInitializer: configuration.debugChannelInitializer |
199 | 0 | ) |
200 | 0 |
|
201 | | #if canImport(Network) |
202 | | if #available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) { |
203 | | self.nwParametersConfigurator = configuration.nwParametersConfigurator |
204 | | } |
205 | | #endif |
206 | 0 | } |
207 | | |
208 | 0 | private var serverHostname: String? { |
209 | 0 | let hostname = self.tlsConfiguration?.hostnameOverride ?? self.connectionTarget.host |
210 | 0 | return hostname.isIPAddress ? nil : hostname |
211 | 0 | } |
212 | | |
213 | 0 | private var hasTLS: Bool { |
214 | 0 | return self.tlsConfiguration != nil |
215 | 0 | } |
216 | | |
217 | 0 | private func requiresZeroLengthWorkaround(eventLoop: EventLoop) -> Bool { |
218 | 0 | return PlatformSupport.requiresZeroLengthWriteWorkaround(group: eventLoop, hasTLS: self.hasTLS) |
219 | 0 | } |
220 | | |
221 | | @usableFromInline |
222 | | internal func makeChannel( |
223 | | managedBy connectionManager: ConnectionManager, |
224 | | onEventLoop eventLoop: EventLoop, |
225 | | connectTimeout: TimeAmount?, |
226 | | logger: Logger |
227 | 0 | ) -> EventLoopFuture<Channel> { |
228 | 0 | let hostname = self.serverHostname |
229 | 0 | let needsZeroLengthWriteWorkaround = self.requiresZeroLengthWorkaround(eventLoop: eventLoop) |
230 | 0 |
|
231 | 0 | var bootstrap = PlatformSupport.makeClientBootstrap( |
232 | 0 | group: eventLoop, |
233 | 0 | tlsConfiguration: self.tlsConfiguration, |
234 | 0 | logger: logger |
235 | 0 | ) |
236 | 0 |
|
237 | 0 | bootstrap = |
238 | 0 | bootstrap |
239 | 0 | .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) |
240 | 0 | .channelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1) |
241 | 0 | .channelInitializer { channel in |
242 | 0 | let sync = channel.pipeline.syncOperations |
243 | 0 |
|
244 | 0 | do { |
245 | 0 | if needsZeroLengthWriteWorkaround { |
246 | 0 | try sync.addHandler(NIOFilterEmptyWritesHandler()) |
247 | 0 | } |
248 | 0 |
|
249 | 0 | // We have a NIOSSL context to apply. If we're using TLS from NIOTS then the bootstrap |
250 | 0 | // will already have the TLS options applied. |
251 | 0 | switch self.tlsMode { |
252 | | #if canImport(NIOSSL) |
253 | 0 | case let .configureWithNIOSSL(sslContext): |
254 | 0 | try sync.configureNIOSSLForGRPCClient( |
255 | 0 | sslContext: sslContext, |
256 | 0 | serverHostname: hostname, |
257 | 0 | customVerificationCallback: self.tlsConfiguration?.nioSSLCustomVerificationCallback, |
258 | 0 | logger: logger |
259 | 0 | ) |
260 | | #endif // canImport(NIOSSL) |
261 | 0 |
|
262 | 0 | // Network.framework TLS configuration is applied when creating the bootstrap so is a |
263 | 0 | // no-op here. |
264 | 0 | case .configureWithNetworkFramework, |
265 | 0 | .disabled: |
266 | 0 | () |
267 | 0 | } |
268 | 0 |
|
269 | 0 | try sync.configureHTTP2AndGRPCHandlersForGRPCClient( |
270 | 0 | channel: channel, |
271 | 0 | connectionManager: connectionManager, |
272 | 0 | connectionKeepalive: self.connectionKeepalive, |
273 | 0 | connectionIdleTimeout: self.connectionIdleTimeout, |
274 | 0 | connectionMaxAge: self.connectionMaxAge, |
275 | 0 | httpTargetWindowSize: self.httpTargetWindowSize, |
276 | 0 | httpMaxFrameSize: self.httpMaxFrameSize, |
277 | 0 | httpMaxResetStreams: self.httpMaxResetStreams, |
278 | 0 | errorDelegate: self.errorDelegate, |
279 | 0 | logger: logger |
280 | 0 | ) |
281 | 0 | } catch { |
282 | 0 | return channel.eventLoop.makeFailedFuture(error) |
283 | 0 | } |
284 | 0 |
|
285 | 0 | // Run the debug initializer, if there is one. |
286 | 0 | if let debugInitializer = self.debugChannelInitializer { |
287 | 0 | return debugInitializer(channel) |
288 | 0 | } else { |
289 | 0 | return channel.eventLoop.makeSucceededVoidFuture() |
290 | 0 | } |
291 | 0 | } |
292 | 0 |
|
293 | 0 | if let connectTimeout = connectTimeout { |
294 | 0 | _ = bootstrap.connectTimeout(connectTimeout) |
295 | 0 | } |
296 | 0 |
|
297 | | #if canImport(Network) |
298 | | if #available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *), |
299 | | let configurator = self.nwParametersConfigurator, |
300 | | let transportServicesBootstrap = bootstrap as? NIOTSConnectionBootstrap |
301 | | { |
302 | | _ = transportServicesBootstrap.configureNWParameters(configurator) |
303 | | } |
304 | | #endif |
305 | 0 |
|
306 | 0 | return bootstrap.connect(to: self.connectionTarget) |
307 | 0 | } |
308 | | } |