From a4d8670c916a530ba15a196f914fa3330ea26dfa Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Wed, 27 May 2026 11:19:45 +0100 Subject: [PATCH] Avoid initializing NIOSSLContext on every connection Motivation: In the secure upgrade pipeline, we currently initialize a [`NIOSSLContext`](https://github.com/apple/swift-nio-ssl/blob/8e3d34d5b6f1be4c1da71cd3f4b86c85f4da99b2/Sources/NIOSSL/SSLContext.swift#L288) on every connection. However, [constructing a `NIOSSLContext` is a very expensive operation](https://github.com/apple/swift-nio-ssl/blob/8e3d34d5b6f1be4c1da71cd3f4b86c85f4da99b2/Sources/NIOSSL/SSLContext.swift#L283-L285). We can easily avoid this by just initializing a `NIOSSLContext` once and using the same instance for all connections. Modifications: - Replaced the `makeServerConfiguration` helper extension on `NIOSSL.TLSConfiguration` to instead be in terms of `NIOSSLContext` (the new helper method is named `makeServerContext`). - Refactored the secure upgrade channel setup to use the new `makeServerContext` helper to initialize a `NIOSSLContext` just once, and plumb that through `setupSecureUpgradeServerChannels` -> `setupSecureUpgradeConnectionChildChannel` -> `makeSSLServerHandler`. - Updated associated test cases. Result: We now only create a new `NIOSSLContext` instance once upon server initialization rather than on every connection. --- .../TransportSecurity+NIOSSL.swift | 58 +++++++++++-------- .../NIOHTTPServer+SecureUpgrade.swift | 23 +++----- Sources/NIOHTTPServer/NIOHTTPServer.swift | 17 ++---- .../TestingChannelServer+SecureUpgrade.swift | 21 ++----- 4 files changed, 52 insertions(+), 67 deletions(-) diff --git a/Sources/NIOHTTPServer/Configuration/TransportSecurity+NIOSSL.swift b/Sources/NIOHTTPServer/Configuration/TransportSecurity+NIOSSL.swift index 87154de..729c105 100644 --- a/Sources/NIOHTTPServer/Configuration/TransportSecurity+NIOSSL.swift +++ b/Sources/NIOHTTPServer/Configuration/TransportSecurity+NIOSSL.swift @@ -17,41 +17,47 @@ import NIOSSL import X509 @available(anyAppleOS 26.0, *) -extension NIOSSL.TLSConfiguration { - /// Creates a `NIOSSL.TLSConfiguration` from the server's TLS credentials and mTLS trust configuration. - static func makeServerConfiguration( - tlsCredentials: NIOHTTPServerConfiguration.TransportSecurity.TLSCredentials, - mTLSConfiguration: NIOHTTPServerConfiguration.TransportSecurity.MTLSTrustConfiguration? +extension NIOSSLContext { + /// Creates a `NIOSSL.NIOSSLContext` from the server's transport security configuration. + static func makeServerContext( + transportSecurity: NIOHTTPServerConfiguration.TransportSecurity, + alpnIdentifiers: [String] ) throws -> Self { - var config: Self + var configuration: TLSConfiguration - switch tlsCredentials.backing { - case .inMemory(let certificateChain, let privateKey): - config = .makeServerConfiguration( - certificateChain: try certificateChain.map { try NIOSSLCertificateSource($0) }, - privateKey: try NIOSSLPrivateKeySource(privateKey) - ) + switch transportSecurity.backing { + case .plaintext: + throw NIOHTTPServerConfigurationError.incompatibleTransportSecurity - case .reloading(let certificateReloader): - config = try .makeServerConfiguration(certificateReloader: certificateReloader) + case .tls(let tlsCredentials), .mTLS(let tlsCredentials, _): + switch tlsCredentials.backing { + case .inMemory(let certificateChain, let privateKey): + configuration = .makeServerConfiguration( + certificateChain: try certificateChain.map { try NIOSSLCertificateSource($0) }, + privateKey: try NIOSSLPrivateKeySource(privateKey) + ) - case .pemFile(let certificateChainPath, let privateKeyPath): - config = try .makeServerConfiguration( - certificateChain: NIOSSLCertificate.fromPEMFile(certificateChainPath).map { .certificate($0) }, - privateKey: .privateKey(.init(file: privateKeyPath, format: .pem)) - ) + case .reloading(let certificateReloader): + configuration = try .makeServerConfiguration(certificateReloader: certificateReloader) + + case .pemFile(let certificateChainPath, let privateKeyPath): + configuration = try .makeServerConfiguration( + certificateChain: NIOSSLCertificate.fromPEMFile(certificateChainPath).map { .certificate($0) }, + privateKey: .privateKey(.init(file: privateKeyPath, format: .pem)) + ) + } } - if let mTLSConfiguration { + if case .mTLS(_, let mTLSConfiguration) = transportSecurity.backing { switch mTLSConfiguration.backing { case .systemDefaults: - config.trustRoots = .default + configuration.trustRoots = .default case .inMemory(let trustRoots): - config.trustRoots = .certificates(try trustRoots.map { try NIOSSLCertificate($0) }) + configuration.trustRoots = .certificates(try trustRoots.map { try NIOSSLCertificate($0) }) case .pemFile(let path): - config.trustRoots = .file(path) + configuration.trustRoots = .file(path) case .customCertificateVerificationCallback: // There are no trust roots when a custom certificate verification callback is specified: the callback @@ -59,9 +65,11 @@ extension NIOSSL.TLSConfiguration { () } - config.certificateVerification = .init(mTLSConfiguration.certificateVerification) + configuration.certificateVerification = .init(mTLSConfiguration.certificateVerification) } - return config + configuration.applicationProtocols = alpnIdentifiers + + return try Self(configuration: configuration) } } diff --git a/Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift b/Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift index 0fd8451..bf3a801 100644 --- a/Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift +++ b/Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift @@ -156,7 +156,7 @@ extension NIOHTTPServer { func setupSecureUpgradeServerChannels( bindTargets: [NIOHTTPServerConfiguration.BindTarget], supportedHTTPVersions: Set, - tlsConfiguration: TLSConfiguration + sslContext: NIOSSLContext ) async throws -> [NIOAsyncChannel, Never>] { let bootstrap = ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup) .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) @@ -178,7 +178,7 @@ extension NIOHTTPServer { self.setupSecureUpgradeConnectionChildChannel( channel: channel, supportedHTTPVersions: supportedHTTPVersions, - tlsConfiguration: tlsConfiguration + sslContext: sslContext ) } serverChannels.append(serverChannel) @@ -263,17 +263,12 @@ extension NIOHTTPServer { func setupSecureUpgradeConnectionChildChannel( channel: any Channel, supportedHTTPVersions: Set, - tlsConfiguration: TLSConfiguration + sslContext: NIOSSLContext ) -> EventLoopFuture> { channel.eventLoop.makeCompletedFuture { - var tlsConfiguration = tlsConfiguration - // Set the application protocols to the appropriate value depending upon whether we want to serve HTTP/1.1, - // HTTP/2, or both. - tlsConfiguration.applicationProtocols = supportedHTTPVersions.alpnIdentifiers - try channel.pipeline.syncOperations.addHandler( self.makeSSLServerHandler( - tlsConfiguration, + sslContext, self.configuration.transportSecurity.customVerificationCallback ) ) @@ -357,12 +352,12 @@ extension NIOHTTPServer { @available(anyAppleOS 26.0, *) extension NIOHTTPServer { func makeSSLServerHandler( - _ tlsConfiguration: TLSConfiguration, + _ sslContext: NIOSSLContext, _ customVerificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)? - ) throws -> NIOSSLServerHandler { + ) -> NIOSSLServerHandler { if let customVerificationCallback { - return try NIOSSLServerHandler( - context: .init(configuration: tlsConfiguration), + return NIOSSLServerHandler( + context: sslContext, customVerificationCallbackWithMetadata: { certificates, promise in promise.completeWithTask { // Convert input [NIOSSLCertificate] to [X509.Certificate] @@ -393,7 +388,7 @@ extension NIOHTTPServer { } ) } else { - return try NIOSSLServerHandler(context: .init(configuration: tlsConfiguration)) + return NIOSSLServerHandler(context: sslContext) } } } diff --git a/Sources/NIOHTTPServer/NIOHTTPServer.swift b/Sources/NIOHTTPServer/NIOHTTPServer.swift index cac5e8f..ed6de5a 100644 --- a/Sources/NIOHTTPServer/NIOHTTPServer.swift +++ b/Sources/NIOHTTPServer/NIOHTTPServer.swift @@ -178,21 +178,14 @@ public struct NIOHTTPServer: HTTPServer { return try await self.setupHTTP1_1ServerChannels(bindTargets: self.configuration.bindTargets) .map { .plaintextHTTP1_1($0) } - case .tls(let credentials): + case .tls, .mTLS: return try await self.setupSecureUpgradeServerChannels( bindTargets: self.configuration.bindTargets, supportedHTTPVersions: self.configuration.supportedHTTPVersions, - tlsConfiguration: try .makeServerConfiguration(tlsCredentials: credentials, mTLSConfiguration: nil) - ).map { .secureUpgrade($0) } - - case .mTLS(let credentials, let mTLSConfiguration): - return try await self.setupSecureUpgradeServerChannels( - bindTargets: self.configuration.bindTargets, - supportedHTTPVersions: self.configuration.supportedHTTPVersions, - tlsConfiguration: try .makeServerConfiguration( - tlsCredentials: credentials, - mTLSConfiguration: mTLSConfiguration - ) + sslContext: .makeServerContext( + transportSecurity: self.configuration.transportSecurity, + alpnIdentifiers: self.configuration.supportedHTTPVersions.alpnIdentifiers + ), ).map { .secureUpgrade($0) } } } diff --git a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift index df932b1..2f185a2 100644 --- a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift @@ -74,28 +74,17 @@ struct TestingChannelSecureUpgradeServer { // Create a connection channel: we will write this to the server channel to simulate an incoming connection. let serverTestConnectionChannel = try await NIOAsyncTestingChannel.createActiveChannel() - let tlsConfiguration: TLSConfiguration - - switch self.server.configuration.transportSecurity.backing { - case .plaintext: - throw NIOHTTPServerConfigurationError.incompatibleTransportSecurity - - case .tls(let credentials): - tlsConfiguration = try .makeServerConfiguration(tlsCredentials: credentials, mTLSConfiguration: nil) - - case .mTLS(let credentials, let trustConfiguration): - tlsConfiguration = try .makeServerConfiguration( - tlsCredentials: credentials, - mTLSConfiguration: trustConfiguration - ) - } + let sslContext = try NIOSSLContext.makeServerContext( + transportSecurity: self.server.configuration.transportSecurity, + alpnIdentifiers: self.server.configuration.supportedHTTPVersions.alpnIdentifiers + ) // Set up the required channel handlers on `serverTestConnectionChannel` let negotiatedServerConnectionFuture = try await serverTestConnectionChannel.eventLoop.flatSubmit { self.server.setupSecureUpgradeConnectionChildChannel( channel: serverTestConnectionChannel, supportedHTTPVersions: self.server.configuration.supportedHTTPVersions, - tlsConfiguration: tlsConfiguration + sslContext: sslContext ) }.get()