emqx / CocoaMQTT

MQTT 5.0 client library for iOS and macOS written in Swift
https://www.emqx.com/en
Other
1.59k stars 418 forks source link

Failure to connect due to completionHandler in URLSessionWebSocketDelegate never being called #344

Open jeremy-meier opened 4 years ago

jeremy-meier commented 4 years ago

I was having issues with a failure to connect using CocoaMQTTWebSocket. Wasn't getting any sort of failure information in the log. This was on iOS 13, so I'm using CocoaMQTTWebSocket.FoundationConnection and not Starscream. The code sets the connection as the delegate to the URLSession session property.

The implementation of the urlSession(_:task:didReceive:completionHandler:) delegate method in the CocoaMQTTWebSocket.FoundationConnection class is as follows:

public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        queue.async {
            if let trust = challenge.protectionSpace.serverTrust, let delegate = self.delegate {
                delegate.connection(self, didReceive: trust) { shouldTrust in
                    completionHandler(shouldTrust ? .performDefaultHandling : .rejectProtectionSpace, nil)
                }
            } else {
                completionHandler(.performDefaultHandling, nil)
            }
        }
    }

Apple documentation states that the completionHandler MUST be called by the delegate: https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/1411595-urlsession.

However, in my case the completionHandler is never being called. The following code is in the CocoaMQTT class implementation of the CocoaMQTTSocketDelegate protocol:

public func socket(_ socket: CocoaMQTTSocketProtocol,
                         didReceive trust: SecTrust,
                         completionHandler: @escaping (Bool) -> Swift.Void) {

        printDebug("Call the SSL/TLS manually validating function")

        delegate?.mqtt?(self, didReceive: trust, completionHandler: completionHandler)
        didReceiveTrust(self, trust, completionHandler)
    }

The default implementation of the didReceiveTrust(self, trust, completionHandler) callback on the CocoaMQTT class is as follows: public var didReceiveTrust: (CocoaMQTT, SecTrust, @escaping (Bool) -> Swift.Void) -> Void = { _, _, _ in }. This implementation does not call the completionHandler. If the CocoaMQTT object does not have a delegate, OR it does have a delegate but the delegate does not implement the @objc optional func mqtt(_ mqtt: CocoaMQTT, didReceive trust: SecTrust, completionHandler: @escaping (Bool) -> Void) method, then the completionHandler is never called and the connection never completes.

Also, the documentation for that delegate method says 'This method will be called if enable allowUntrustCACertificate'. However the method is always called, regardless of the value of allowUntrustCACertificate.

I will work on a patch that addresses both of the above.

jeremy-meier commented 4 years ago

Another possible issue: What if a user implements both the callback on the class as well as the delegate method? Seems like an opportunity for a race condition.