Replace Stream.getStreamsToHost with URLSessionStreamTask.streamTask

Working on an MQTT client. We have a Network.framework solution, but that only supports macOS 10.14+. We need to support 10.13+. We have some code for an NSStream-based solution, but that seems to support only TLSv1, not to mention that a WWDC18 session 'highly discouraged' its use. So, we are attempting to use URLSessionStreamTask to connect and capture the streams which are then handed back to the NSStream-based code. Yes, we could just rewrite more of the code, add our own async read, etc,. but... Anyway, the modified code to use the URLSessionStreamTask has few changes: the streams are obtained by creating a URLSession & streamTask, then calling the task's captureStreams() method. An extension to the class was added to implement the URLSessionStreamDelegate; existing code was moved into the callback method to handle stream setup.



A URLSession is created & successfully connects (we are testing against a mosquitto server which has an insecure cert, but URLSessionStreamDelegate handles the challenge)



private var inputStream: InputStream? = nil

private var outputStream: OutputStream? = nil

private var sessionQueue: DispatchQueue

private var session: URLSession? = nil

private var streamTask: URLSessionStreamTask? = nil

...

session = URLSession(configuration: URLSessionConfiguration.default, delegate: self as URLSessionStreamDelegate, delegateQueue: nil)

streamTask = session!.streamTask(withHostName: host, port: port)

if secure {

streamTask!.startSecureConnection()

}

streamTask!.captureStreams()

streamTask!.resume()

...


The delegate is called with the captured streams which are 'attached' to the existing code



extension MQTTSessionStream: URLSessionStreamDelegate {



func urlSession(_ session: URLSession,

streamTask: URLSessionStreamTask,

didBecome inputStream: InputStream, outputStream: OutputStream) {


//setup the streams

self.inputStream = inputStream

self.inputStream!.delegate = self as StreamDelegate


self.outputStream = outputStream

self.outputStream!.delegate = self as StreamDelegate



self.sessionQueue.async { [weak self] in


guard let `self` = self else {

return

}


self.currentRunLoop = RunLoop.current

self.inputStream!.schedule(in: self.currentRunLoop!, forMode: RunLoop.Mode.default)

self.outputStream!.schedule(in: self.currentRunLoop!, forMode: RunLoop.Mode.default)


let timeout: TimeInterval = 30 //- temporary - later, make this a member variable and set in init...

if timeout > 0 {

DispatchQueue.global().asyncAfter(deadline: .now() + timeout) {

self.connectTimeout()

}

}


self.inputStream!.open()

self.inputReady = true;



self.outputStream!.open()

self.outputReady = true;

self.currentRunLoop!.run()

}

}


func urlSession(_ session: URLSession,

didReceive challenge: URLAuthenticationChallenge,

completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

...

}

}



Log lines:

2020-01-24 09:56:48.184007-0700 0x149a66 Default 0x0 13146 0 stmqtt: (CFNetwork) Connection 1: connected successfully

2020-01-24 09:56:48.184046-0700 0x149a66 Default 0x0 13146 0 stmqtt: (CFNetwork) Connection 1: TLS handshake complete

2020-01-24 09:56:48.184296-0700 0x149a66 Default 0x0 13146 0 stmqtt: (CFNetwork) Connection 1: ready C(N) E(N)

2020-01-24 09:56:48.184320-0700 0x149a66 Default 0x0 13146 0 stmqtt: (CFNetwork) StreamTask <99FB193B-B74A-4BE1-B6BA-EBBAA6C1C315>.<0> is complete and received server trust, marking as secure

2020-01-24 09:56:48.184390-0700 0x149a66 Default 0x0 13146 0 stmqtt: (CFNetwork) Establish Object : 0x7ffb16d08dc8 .Task : 0x7ffb189088d0

2020-01-24 09:56:48.184411-0700 0x149a66 Default 0x0 13146 0 stmqtt: (CFNetwork) Connection 1: received viability advisory(Y)

2020-01-24 09:56:54.442740-0700 0x149ad6 Activity 0x1103d4 13146 0 stmqtt: (Security) SecKeychainAddCallback

2020-01-24 09:58:13.850514-0700 0x149d3e Default 0x0 13146 0 stmqtt: (libboringssl.dylib) [com.apple.network.boringssl:BoringSSL] boringssl_context_message_handler(2259) [C1.1:2][0x7ffb1890b240] Reading SSL3_RT_ALERT 2 bytes





However, the StreamDelegate method to handle the events is never called and so the stream is soon closed.



extension MQTTSessionStream: StreamDelegate {



@objc internal func stream(_ aStream: Stream, handle eventCode: Stream.Event) {



switch eventCode {



case Stream.Event.openCompleted:

...



case Stream.Event.hasBytesAvailable:

...



case Stream.Event.errorOccurred:

...



case Stream.Event.endEncountered:

...



case Stream.Event.hasSpaceAvailable:

...



default:

break

}

}

}



Log lines:

2020-01-24 09:58:13.850554-0700 0x149d3e Default 0x0 13146 0 stmqtt: (libboringssl.dylib) [com.apple.network.boringssl:BoringSSL] boringssl_context_handle_warning_alert(1894) [C1.1:2][0x7ffb1890b240] read alert, level: warning, description: close notify

2020-01-24 09:58:13.850571-0700 0x149d3e Default 0x0 13146 0 stmqtt: (libboringssl.dylib) [com.apple.network.boringssl:BoringSSL] nw_protocol_boringssl_input_finished(1676) [C1.1:2][0x7ffb1890b240] state: 2

2020-01-24 09:58:13.850607-0700 0x149d3e Default 0x0 13146 0 stmqtt: (CFNetwork) Connection 1: read-side closed

2020-01-24 09:58:13.850621-0700 0x149d3e Error 0x0 13146 0 stmqtt: (CFNetwork) Connection 1: missing error, so heuristics synthesized error(1:53)

2020-01-24 09:58:13.850631-0700 0x149d3e Error 0x0 13146 0 stmqtt: (CFNetwork) Connection 1: encountered error(1:53)



The WWDC18 session in which the URLSessionStreamTask was presented indicated that once the streams were captured, they could be used in code that previously had used NSStreams. Did I miss some other required setup? Does anything seem obviously wrong with

Replying to myself, since I found the problem over the weekend, and I'm not (too) afraid of admintting that I am an *****... setting inputReady = true and outputReady = true must be done in the StreamDelegate callback. Everything is working well now.

We have some code for an

NSStream
-based solution, but that seems to support only TLSv1

CFSocketStream, which is what

NSStream
connects you to, supports TLS 1.2.

not to mention that a WWDC18 session 'highly discouraged' its use.

Yeah, but only in favour of Network framework. If you have existing Network framework code for 10.14 and above, and existing

NSStream
code for older systems, I think you’re done. Putting in the work to move the older systems to
NSURLSessionStream
does not buy you much.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
Replace Stream.getStreamsToHost with URLSessionStreamTask.streamTask
 
 
Q