From 5261dc383671f21e0a6e563169ed43ce97ba00ff Mon Sep 17 00:00:00 2001 From: krolakj Date: Mon, 13 Jul 2026 16:41:11 +0200 Subject: [PATCH] mTLS support --- .../Core/Network/NetworkSessionDelegate.swift | 19 +++++++++++++++++++ .../Core/Network/NetworkingClient.swift | 4 +++- .../Streaming/AudioPlayer/AudioPlayer.swift | 5 +++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/AudioStreaming/Core/Network/NetworkSessionDelegate.swift b/AudioStreaming/Core/Network/NetworkSessionDelegate.swift index 588ae9d..55fc24f 100644 --- a/AudioStreaming/Core/Network/NetworkSessionDelegate.swift +++ b/AudioStreaming/Core/Network/NetworkSessionDelegate.swift @@ -6,8 +6,14 @@ import Foundation import OSLog +/// Provides a `URLCredential` in response to a client-certificate authentication challenge, +/// or `nil` to fall back to the system's default handling. Used to support mutual TLS (mTLS) +/// against servers that sit behind a client-certificate-authenticated proxy. +public typealias ClientCertificateProvider = (URLAuthenticationChallenge) -> URLCredential? + final class NetworkSessionDelegate: NSObject, URLSessionDataDelegate { weak var taskProvider: StreamTaskProvider? + var clientCertificateProvider: ClientCertificateProvider? func stream(for task: URLSessionTask) -> NetworkDataStream? { guard let taskProvider = taskProvider else { @@ -50,4 +56,17 @@ final class NetworkSessionDelegate: NSObject, URLSessionDataDelegate { stream.didReceive(response: response as? HTTPURLResponse) completionHandler(.allow) } + + func urlSession(_: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { + guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate, + let credential = clientCertificateProvider?(challenge) + else { + completionHandler(.performDefaultHandling, nil) + return + } + completionHandler(.useCredential, credential) + } } diff --git a/AudioStreaming/Core/Network/NetworkingClient.swift b/AudioStreaming/Core/Network/NetworkingClient.swift index 49926e4..9fd1f5b 100644 --- a/AudioStreaming/Core/Network/NetworkingClient.swift +++ b/AudioStreaming/Core/Network/NetworkingClient.swift @@ -54,8 +54,10 @@ final class NetworkingClient { init(configuration: URLSessionConfiguration = .networkingConfiguration, delegate: NetworkSessionDelegate = NetworkSessionDelegate(), - networkQueue: DispatchQueue = DispatchQueue(label: "audio.streaming.session.network.queue")) + networkQueue: DispatchQueue = DispatchQueue(label: "audio.streaming.session.network.queue"), + clientCertificateProvider: ClientCertificateProvider? = nil) { + delegate.clientCertificateProvider = clientCertificateProvider let delegateQueue = operationQueue(underlyingQueue: networkQueue) let session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: delegateQueue) self.session = session diff --git a/AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift b/AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift index 1987109..13fba34 100644 --- a/AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift +++ b/AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift @@ -183,7 +183,8 @@ open class AudioPlayer { /// Tracks the current loop iteration (how many times we've looped so far) private var currentLoopIteration: Int = 0 - public init(configuration: AudioPlayerConfiguration = .default) { + public init(configuration: AudioPlayerConfiguration = .default, + clientCertificateProvider: ClientCertificateProvider? = nil) { self.configuration = configuration.normalizeValues() let engine = AVAudioEngine() audioEngine = engine @@ -195,7 +196,7 @@ open class AudioPlayer { sourceQueue = DispatchQueue(label: "source.queue", qos: .default) entryProvider = AudioEntryProvider( - networkingClient: NetworkingClient(), + networkingClient: NetworkingClient(clientCertificateProvider: clientCertificateProvider), underlyingQueue: sourceQueue, outputAudioFormat: outputAudioFormat )