Swift Network.framework WebSocket handshake nil returned

1.8k Views Asked by At

I'm trying to use the new Network.framework to connect to WebSocket but facing nil handshake response from server.

(Yes, I know Starscream exist but it didnt support Proxy / Mobility of user switching between network interface)

My test code:

func beginTest() {

    let connection = NWConnection(host: "echo.websocket.org", port: 443, using: .tls)

    connection.stateUpdateHandler = { state in
        print("State:", state)
        switch state {
        case .ready:
            self.connectionReady(connection)
        default:
            break
        }
    }
    connection.start(queue: .main)
}

func connectionReady(_ connection: NWConnection) {

    let raw = """
    GET / HTTP/1.1
    Upgrade: websocket
    Connection: Upgrade
    Host: echo.websocket.org
    Origin: https://echo.websocket.org
    Sec-WebSocket-Key: s04nPqA7M6pQ3Lu2jRJLSQ==
    Sec-WebSocket-Version: 13
    """

    let rawData = raw.appending("\n\n\n").replacingOccurrences(of: "\n", with: "\r\n").data(using: .utf8)

    connection.send(content: rawData!, completion: .idempotent)

    connection.receiveMessage(completion: {data, context, bool, error in
        if let data = data {
            print("Received:", String(data: data, encoding: .utf8))
        }

        print("Error:", error)

        let hello = "Hello".data(using: .utf8)
        connection.send(content: hello, completion: .idempotent)
    })
}

It's nil response and connection dropped instead of getting Upgrade handshake response from server, below with console logs:

State: preparing
State: ready
Received: nil
Error: nil
2018-10-08 11:38:57.314885+0800 SwiftNetworkTest[86448:3026660] [] nw_socket_handle_socket_event [C1.1:2] Socket SO_ERROR [54: Connection reset by peer]

Can anyone guide me how to utilize Apple new Network.framework? It will be much appreciated!

Update1

  • My bad, I'm now able to see the handshake response with using .ascii encoding instead of .utf8.

  • But I'm still having connection dropped Connection reset by peer. How do I retain the connection after upgrade to WebSocket?

2

There are 2 best solutions below

0
On

You should follow the Websocket guidelines how to format Websocket sent message.

I think this is a good resource. I used it myself.

https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers

0
On

After reading through the documentation: https://developer.apple.com/documentation/security/certificate_key_and_trust_services/trust/evaluating_a_trust_and_parsing_the_result

I setup my authenticated TLS connection like this:

    init(endpoint: NWEndpoint, interface: NWInterface?, passcode: String, delegate: BitfinexConnectionDelegate)
    {
        self.delegate            = delegate
        self.initiatedConnection = false

        let host = "api.bitfinex.com"
        let port = 443
        
        let options = NWProtocolTCP.Options()
        options.connectionTimeout = 15
        
        let tlsOptions = NWProtocolTLS.Options()
        sec_protocol_options_set_verify_block(
            tlsOptions.securityProtocolOptions,
            {
                (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in
                
                let trust = sec_trust_copy_ref(sec_trust).takeRetainedValue()
                
                let pinner = FoundationSecurity()
                
                pinner.evaluateTrust(trust: trust, domain: host, completion:
                {
                    (state) in
                    
                    switch state
                    {
                    case .success:
                        sec_protocol_verify_complete(true)
                    case .failed(_):
                        sec_protocol_verify_complete(false)
                    }
                })
            }, queue
        )
        
        let parameters = NWParameters(tls: tlsOptions, tcp: options)
        let conn = NWConnection(host: NWEndpoint.Host.name(host, nil),
                                port: NWEndpoint.Port(rawValue: UInt16(port))!,
                                using: parameters
        )
        self.connection = conn
        
        startConnection()
    }

The FoundationSecurity() block is a simple TLS evaluation

if SecTrustEvaluateWithError(trust, &error)
{
    completion(.success)
}
else
{
    completion(.failed(error))
}

Once my connection was ready. I sent through a Data object which I created like this. This depends on the API which you are interfacing with.

    private func prepareWebSocket() throws -> Data
    {
        let apiKey    = "API_KEY"
        let apiSecret = "API_SECRET"
        let authNonce = NonceProvider.sharedInstanse.nonce
        let authPayload = "AUTH\(authNonce)"
        
        
        let authenticationKey  = SymmetricKey(data: apiSecret.data(using: .ascii)!)
        let authenticationCode = HMAC<SHA384>.authenticationCode(for: authPayload.data(using: .ascii)!,
                                                                 using: authenticationKey
        )
        
        let authSig = authenticationCode.compactMap { String(format: "%02hhx", $0) }.joined()
        
        let payload: [String : Any] =
        [
            "event":       "auth",
            "apiKey" :     apiKey,
            "authSig":     authSig,
            "authPayload": authPayload,
            "authNonce":   authNonce
        ]

        return try JSONSerialization.data(withJSONObject: payload, options: .fragmentsAllowed)
    }