Coverage Report

Created: 2026-07-30 06:36

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}