iosswiftnetwork-programmingwebsocketlocalhost

Development iOS App unable to connect to a self-signed SSL cert Secure WebSocket Server HTTP(s)


Due to my iPhone being upgraded to iOS 16.6, it seems to no longer support a HTTP WebSocket Server connection. Just want to also say I have no prior experience with iOS app development so any help and guidance is appreciated.

Problem to solve: I have been attempting to change the codes where I create a HTTP(s) server and promote it to the WebSocket Server to ultimately get the iOS app to connect with the new HTTP(s) WebSocket Server.

Background: The software engineer who coded the iOS app said it was written to connect to a SimpleWebsocket Server (HTTP) using iOS's built-in Websocket support through 'URLSessionWebsocketTask' and ever since the iOS version upgrade on the iPhone, it is unable to connect to the HTTP server.

The codes for the iOS app are in .swift and using XCode to compile and download the app into the iPhone. The creation of the WebSocket Server is in .py along with the GUI controller for the app.

Originally to get everything running, I would set-up the WebSocket Server using Ubuntu from the Python script since I am controlling the iOS app through another Python script driver which provides a GUI to control the app on a Windows computer (the computer also provides a mobile hotspot that is connected to in the iPhone to be able to connect to the server).

The changes in order from my attempt to convert HTTP server to HTTP(S):

  1. Created a self-signed SSL certificate with the mobile hotspot IP address as the Common Name.

  2. Downloaded and trusted the self-signed SSL certificate on the iPhone

  3. Changed the WebSocketClient.py that is ran in Ubuntu to accept and use SSL cert (running this shows the Secure WebSocket Server is started on the port inputted)

  4. Updated the Python Driver.py script (GUI to control iOS app) on the Windows computer to connect to the Secure WebSocket Server (this also shows it is connected to the Secure WebSocket Server)

  5. Everything looks good at this point

  6. Now, after I connect to the mobile hotspot from the Windows computer, I move onto the iPhone iOS app where it needs to connect to the server as well so in the iOS app swift code for the server connection, I changed the original ws:// to wss:// and re-downloaded the app to the iPhone but it is unable to connect to the server (in the Ubuntu terminal, it would show if it connects but nothing happens now).

  7. Debugging the connection to the WebSocket Server, this error occurs: Error Domain=NSURLErrorDomain Code=-1009 "The Internet connection appears to be offline."

    1. I know for certain the mobile hotspot has internet connection since I used DropBox to receive the cert.pem for the SSL certification to trust it

Based on these attempts I tried, what is missing in my steps to get the iOS app/iPhone connected to the HTTP(S) server? Would I need to add changes to the iOS app's info.plist (Information Property List) to work with HTTP(s) along with the iOS app's WebSocketConnection.swift? Thanks in advance.

Codes for the iOS app that I believe are relevant to the WebSocketServer:
Info.plist where I added the lines (unsure if correct):

<key>NSAppTransportSecurity</key>:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>$(DEVELOPMENT_LANGUAGE)</string>
    <key>CFBundleExecutable</key>
    <string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleIdentifier</key>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>$(PRODUCT_NAME)</string>
    <key>CFBundlePackageType</key>
    <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0</string>
    <key>CFBundleVersion</key>
    <string>1</string>
    <key>LSRequiresIPhoneOS</key>
    <true/>
    <key>NSCameraUsageDescription</key>
    <string>ARKit live camera</string>
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>for image anchor</string>
    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
        <key>NSAllowsLocalNetworking</key>
        <true/>
        <key>NSExceptionDomains</key>
        <dict>
            <key>192.168.137.1</key>
            <dict>
                <key>NSExceptionAllowsInsecureHTTPLoads</key>
                <true/>
                <key>NSExceptionMinimumTLSVersion</key>
                <string>TLSv1.2</string>
                <key>NSExceptionRequiresForwardSecrecy</key>
                <false/>
                <key>NSIncludesSubdomains</key>
                <true/>
            </dict>
        </dict>
    </dict>
    <key>UIApplicationSceneManifest</key>
    <dict>
        <key>UIApplicationSupportsMultipleScenes</key>
        <false/>
        <key>UISceneConfigurations</key>
        <dict>
            <key>UIWindowSceneSessionRoleApplication</key>
            <array>
                <dict>
                    <key>UISceneConfigurationName</key>
                    <string>Default Configuration</string>
                    <key>UISceneDelegateClassName</key>
                    <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
                    <key>UISceneStoryboardFile</key>
                    <string>Main</string>
                </dict>
            </array>
        </dict>
    </dict>
    <key>UIApplicationSupportsIndirectInputEvents</key>
    <true/>
    <key>UILaunchStoryboardName</key>
    <string>LaunchScreen</string>
    <key>UIMainStoryboardFile</key>
    <string>Main</string>
    <key>UIRequiredDeviceCapabilities</key>
    <array>
        <string>armv7</string>
    </array>
    <key>UISupportedInterfaceOrientations</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    <key>UISupportedInterfaceOrientations~ipad</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationPortraitUpsideDown</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
</dict>
</plist>
//
//  WebSocketConnection.swift
//  WebSockets
//
//  Created by zen on 6/17/19.
//  Copyright © 2019 AppSpector. All rights reserved.
//

import Foundation
import Combine

protocol WebSocketConnection {
    func send(text: String)
    func send(data: Data)
    func connect()
    func disconnect()
    var delegate: WebSocketConnectionDelegate? {
        get
        set
    }
}

protocol WebSocketConnectionDelegate {
    func onConnected(connection: WebSocketConnection)
    func onDisconnected(connection: WebSocketConnection, error: Error?)
    func onError(connection: WebSocketConnection, error: Error)
    func onMessage(connection: WebSocketConnection, text: String)
    func onMessage(connection: WebSocketConnection, data: Data)
}

class WebSocketTaskConnection: NSObject, WebSocketConnection, URLSessionWebSocketDelegate {
    var delegate: WebSocketConnectionDelegate?
    var webSocketTask: URLSessionWebSocketTask!
    var urlSession: URLSession!
    let delegateQueue = OperationQueue()
    
    // Commented out below - ML 11/18/2024
    // init(url: URL) {
    //     super.init()
    //     urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: delegateQueue)
    //     webSocketTask = urlSession.webSocketTask(with: url)
    // }

    // Updated init() for configuration with SSL - ML 11/18/2024
    init(url: URL) {
        super.init()
        
        // Create URLSession configuration with SSL settings
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForResource = 30 // Adjust timeout if needed
        
        urlSession = URLSession(configuration: config, 
                              delegate: self, 
                              delegateQueue: delegateQueue)
        
        // Create WebSocket task with the URL
        var request = URLRequest(url: url)
        request.setValue("", forHTTPHeaderField: "Origin") // Handle any CORS issues
        webSocketTask = urlSession.webSocketTask(with: request)
    }
    
    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
        self.delegate?.onConnected(connection: self)
    }
    
    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
        self.delegate?.onDisconnected(connection: self, error: nil)
    }

    // Added additional func urlSession() to accept/trust SSL - ML 11/18/2024
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
              let serverTrust = challenge.protectionSpace.serverTrust else {
            completionHandler(.performDefaultHandling, nil)
            return
        }
        
        let credential = URLCredential(trust: serverTrust)
        completionHandler(.useCredential, credential)
    }
    
    func connect() {
        webSocketTask.resume()
        
        listen()
    }
    
    func disconnect() {
        webSocketTask.cancel(with: .goingAway, reason: nil)
    }
    
    func listen()  {
        webSocketTask.receive { result in
            switch result {
            case .failure(let error):
                self.delegate?.onError(connection: self, error: error)
            case .success(let message):
                switch message {
                case .string(let text):
                    self.delegate?.onMessage(connection: self, text: text)
                case .data(let data):
                    self.delegate?.onMessage(connection: self, data: data)
                @unknown default:
                    fatalError()
                }
                
                self.listen()
            }
        }
    }
    
    func send(text: String) {
        webSocketTask.send(URLSessionWebSocketTask.Message.string(text)) { error in
            if let error = error {
                self.delegate?.onError(connection: self, error: error)
            }
        }
    }
    
    func send(data: Data) {
        webSocketTask.send(URLSessionWebSocketTask.Message.data(data)) { error in
            if let error = error {
                self.delegate?.onError(connection: self, error: error)
            }
        }
    }
}

UPDATE:

I am able to get the iOS app/iPhone connected to the custom SSL server and communicates but getting Error -1001 "The request timed out" after 30 seconds.

Architecture:

Current Status:

Error Message in Xcode terminal:

WebSocket: Received text message
2024-11-25 15:49:03.678384-0800 iVEERS[1465:454666] Task <CD21B8AD-86D9-4984-8C48-8665CD069CC6>.<1> finished with error [-1001] Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={_kCFStreamErrorCodeKey=-2103, _NSURLErrorFailingURLSessionTaskErrorKey=LocalWebSocketTask <CD21B8AD-86D9-4984-8C48-8665CD069CC6>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "LocalWebSocketTask <CD21B8AD-86D9-4984-8C48-8665CD069CC6>.<1>"
), NSLocalizedDescription=The request timed out., NSErrorFailingURLStringKey=wss://192.168.137.1:8001/, NSErrorFailingURLKey=wss://192.168.137.1:8001/, _kCFStreamErrorDomainKey=4}

Technical Details:

Questions:

  1. What's causing the timeout after 30 seconds?
  2. How can I maintain a persistent WebSocket connection?
  3. Why aren't map updates propagating to the iOS client?

Any additional guidance/suggestions would be greatly appreciated. Please let me know if additional code snippets would help on what I currently have.


Solution

  • Instead of trying to shove a certificate onto the device, I would probably suggest putting a copy of the expected public key into the app and use key pinning to allow connections to any host as long as the host sends back that public key. (I'm assuming this is not a production app; if it is, then you should create a proper certificate infrastructure and instead check to make sure it is signed by a trusted cert; coming up with a reasonable set of designated requirements is way beyond the scope of this question.)

    To do this, you would add a delegate method on the session delegate that is called whenever trust validation occurs, and if the protection space is "server trust", you check to see if the public key matches the one for your trusted server, and if it does, you call the callback saying that it should accept the certificate. If either condition is not met, you tell it to use default handling (trusting it if iOS trusts it, and not trusting it if iOS doesn't trust it).

    Basically, if memory serves, it should look something like this:

    @property(nonatomic) NSData *expectedPublicKeyData;
    
    ...
    
    self.expectedPublicKeyData = ... // See https://stackoverflow.com/questions/10579985/how-can-i-get-seckeyref-from-der-pem-file for help with this part
    
    - (void)URLSession:(NSURLSession *)session 
                  task:(NSURLSessionTask *)task 
    didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge 
     completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
      if (challenge.protectionSpace.authenticationMethod ==
        NSURLAuthenticationMethodServerTrust) {
        SecTrustRef trust = [challenge.protectionSpace serverTrust];
        SecKeyRef serverPublicKey = SecTrustCopyPublicKey(trust);
        NSData *serverPublicKeyData = (__bridge NSData *)SecKeyCopyExternalRepresentation(serverPublicKey);
        CFRelease(serverPublicKey);
    
        /* The self.expectedPublicKeyData property must be prepopulated with the expected public key as an NSData blob containing its public representation. */
        if ([serverPublicKeyData isEqual:self.expectedPublicKeyData]) {
          completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
        } else {
          completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
        }
      } else {
        completionHandler(NSURLSessionAuthChallengePerformDefaultHandling
    , nil);
      }
    }
    

    Useful docs: