vapor / websocket-kit

WebSocket client library built on SwiftNIO
https://docs.vapor.codes/4.0/advanced/websockets/
MIT License
272 stars 79 forks source link

It just doesn't work! #104

Closed ea7kir closed 2 years ago

ea7kir commented 2 years ago

Probable Cause

Lack of examples and documentation causes bad programming.

To Reproduce

Please try my 2 very small Swift packages, WsClient and WsServer, at GitHub.com/ea7kir

The server runs, but the client bombs out with a Fatal error.

Environment

WebSocket-Kit. STANDALONE (ie without Vapor) macOS 12.1, Xcode 13.2.1, Swift 5.5.2

0xTim commented 2 years ago

@ea7kir what's the full console output and description of the error produced?

ea7kir commented 2 years ago

The Client stalls on wait()

_ = try promise.futureResult.wait()

Thread 1: Fatal error: Error raised at top level: WebSocketKit.WebSocketClient.Error.invalidResponseStatus(HTTPResponseHead { version: HTTP/1.1, status: notFound, headers: [("Date", "Wed, 12 Jan 2022 17:48:52 GMT"), ("Server", "Apache/2.4.51 (Unix)"), ("Content-Length", "196"), ("Content-Type", "text/html; charset=iso-8859-1")] })

Thread 1: Fatal error: Error raised at top level: WebSocketKit.WebSocketClient.Error.invalidResponseStatus(HTTPResponseHead { version: HTTP/1.1, status: notFound, headers: [("Date", "Wed, 12 Jan 2022 17:48:52 GMT"), ("Server", "Apache/2.4.51 (Unix)"), ("Content-Length", "196"), ("Content-Type", "text/html; charset=iso-8859-1")] })

The Console output is...

Swift/ErrorType.swift:200: Fatal error: Error raised at top level: WebSocketKit.WebSocketClient.Error.invalidResponseStatus(HTTPResponseHead { version: HTTP/1.1, status: notFound, headers: [("Date", "Wed, 12 Jan 2022 17:48:52 GMT"), ("Server", "Apache/2.4.51 (Unix)"), ("Content-Length", "196"), ("Content-Type", "text/html; charset=iso-8859-1")] }) 2022-01-12 18:48:52.403021+0100 WsClient[6425:176104] Swift/ErrorType.swift:200: Fatal error: Error raised at top level: WebSocketKit.WebSocketClient.Error.invalidResponseStatus(HTTPResponseHead { version: HTTP/1.1, status: notFound, headers: [("Date", "Wed, 12 Jan 2022 17:48:52 GMT"), ("Server", "Apache/2.4.51 (Unix)"), ("Content-Length", "196"), ("Content-Type", "text/html; charset=iso-8859-1")] }) (lldb)

I hope this helps.

0xTim commented 2 years ago

That's a 404 which means the server you're trying to hit doesn't have a route for the url

ea7kir commented 2 years ago

There are no routes on the server - see my GitHub test code WsClient and WsServer - both running on localhost (same machine).

However, I've just discovered the system will work for WS but not for a WSS connections.

WS now giving giving the console output at the client.

Swift/ErrorType.swift:200: Fatal error: Error raised at top level: NIOSSL.NIOSSLError.handshakeFailed(NIOSSL.BoringSSLError.sslError([Error: 268435703 error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER])) 2022-01-12 19:51:17.819758+0100 WsClient[7057:219945] Swift/ErrorType.swift:200: Fatal error: Error raised at top level: NIOSSL.NIOSSLError.handshakeFailed(NIOSSL.BoringSSLError.sslError([Error: 268435703 error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER])) (lldb)

0xTim commented 2 years ago

Does it work with another websocket client? That error means you're trying to connect to a server with a version that Vapor does not support like SSLv3

ea7kir commented 2 years ago

@0xTim To be clear, both server and client are standalone WebSocket-Kit command-line apps.

But testing with Socket Debugger from the AppStore, I get... WsServer with Socket Debugger client: WS yes, WSS no. WsClient with Socket Debugger server: WS almost, WSS crash.

The code I'm using is adapted from a Medium article by Kristaps Grinbergs. It's the only example I can find. Do the WebSocket-Kit team have an example for standalone usage?

0xTim commented 2 years ago

The tests are the best place to start. WSS should work for the client (assuming the server is configured correctly with a valid cert). For the server you'll need something in front (like Vapor) to do the TLS termination or put your own NIOSSL channel handler in front

ea7kir commented 2 years ago

@0xTim Are saying both WebSocket-Kit and Vapor use an SSL that is incompatible with that provided in Monterey - and most probably most other web servers out there?

It was you who suggested on swift.org that for use cases like mine, it would be better to use WebSocket-Kit in standalone instead of SwiftNIO.

The only examples for any type of networking solution are half-baked by bedroom iOS "experts". I don't pretend to be an expert, I just need a decent example of something that actually works.

0xTim commented 2 years ago

What do you mean exactly by "an SSL provided by Monterey"? macOS doesn't provide any TLS/SSL certs, you need to generate them for yourself and then set up your web server to serve them when an HTTPS connection is received.

Vapor and NIO support all modern valid TLS certificates (1.1-1.3) both as a server and a client

ea7kir commented 2 years ago

The thing is; I can stream from an external https://wws.somedomain/fft (without knowing anything about SSl certificates) using a URLSession client, but not with a WebSocket-Kit client. I'm also trying to build my own server to do something similar. I have a non-SSL server for WS working, but not with WSS - so I'm stuck.

As you can guess, I have very little knowledge at this level.

0xTim commented 2 years ago

Let's start with the client then. You can connect to a URL with URLSession client. I'm assuming you're using URLSessionWebSocketTask? What does your code for both look like?

ea7kir commented 2 years ago

This works...

//
//  WsClient.swift
//  WebSocketURLSession
//
//  Created by Michael Naylor on 12/01/2022.
//

// derived from https://kristaps.me/blog/websockets-ios-13-swift/

import Foundation

protocol WsNetworkDelegate: AnyObject {
    func dateArrived(data: Data)
}

final class WsNetwork {

    var webSocketTask: URLSessionWebSocketTask?
    var urlSession: URLSession?
    weak var delegate: WsNetworkDelegate?

    // this works with wss://somedomain/fft - I'm reluctant to make the real domain public
    func open(urlStr: String, delegate: WsNetworkDelegate) {
        self.delegate = delegate
        let url = URL(string: urlStr)!
        urlSession = URLSession(configuration: .ephemeral)
        webSocketTask = urlSession?.webSocketTask(with: url)
        webSocketTask?.resume()
    }

    func sendPing() {
        webSocketTask?.sendPing { (error) in
            if let error = error {
                print("Sending PING failed: \(error)")
            }
            DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
                self.sendPing()
            }
        }
    }

    func close() {
        print("webSocketTask?.cancel")
        // no idea which, if any, of these actually work
        webSocketTask?.cancel(with: .goingAway, reason: nil)
//        urlSession?.invalidateAndCancel()
//        print("urlSession?.invalidateAndCancel")
        print("DONE closing")
    }

    func sendSingle() {
        let message = URLSessionWebSocketTask.Message.string("Hello World")
        webSocketTask?.send(message) { error in
            if let error = error {
                print("WebSocket couldn’t send message because: \(error)")
            }
        }
    }

    func receive() {
        webSocketTask?.receive { result in
            switch result {
                case .failure(let error):
                    print("Error in receiving message: \(error)")
                case .success(let message):
                    switch message {
                        case .string(let text):
                            print("Received string: \(text)")
                        case .data(let data):
                            print("Received data: \(data)")
                        @unknown default:
                            print("unused websocket message: \(message)")
                            fatalError()
                    }
            }
        }
    }

    // so far, I'm only testing this one
    func listen() {
        webSocketTask?.receive { result in
            switch result {
                case .failure(let error):
                    print("Error in receiving message: \(error)")
                case .success(let message):
                    switch message {
                        case .string(let text):
                            print("Received string: \(text)")
                        case .data(let data):
//                            DispatchQueue.main.async {
//                                print("Received data: \(data)")
                                self.delegate!.dateArrived(data: data)
//                            }
                        @unknown default:
                            print("unused websocket message: \(message)")
                            fatalError()
                    }
                    self.listen()
            }
        }
    }

    deinit {
        // i'd like to call close() here, it dinit never gets called
        print("deinit WsClient")
    }

}
0xTim commented 2 years ago

What about the WebsocketKit version?

ea7kir commented 2 years ago
import Foundation
import WebSocketKit
import NIOPosix
import NIOHTTP1
import NIOWebSocket

print("Server is starting...")

// Setting host to domain, IP Address or localhost make no difference.
let host = "localhost"
let port = 9999

// create the event loop groop
var eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 2)

// do the the GET dance
let upgradePipelineHandler: ( Channel, HTTPRequestHead ) ->
EventLoopFuture<Void> = { channel, req in
    WebSocket.server(on: channel) { ws in
        ws.send("You have connected to WebSocket.")

        ws.onText { ws, string in
            print("Received: \(string)")
            ws.send("The server got \(string)")
        }
        ws.onClose.whenSuccess { value in
            print("onClose.")
        }
    }
}

print("Get dance completed.")

// WebSock is upgraded, so create promise in which to receive the events
let promise = eventLoopGroup.next().makePromise(of: String.self)

let server = try ServerBootstrap(group: eventLoopGroup).childChannelInitializer { channel in
    let webSocket = NIOWebSocketServerUpgrader(
        shouldUpgrade: { channel, reg in
            return channel.eventLoop.makeSucceededFuture([:])
        },
        upgradePipelineHandler: upgradePipelineHandler
    )

    return channel.pipeline.configureHTTPServerPipeline(
        withServerUpgrade: (
            upgraders: [webSocket],
            completionHandler: { ctx in
                // complete
            })
    )
}.bind(host: host, port: port).wait()

print("Server is listening on \(host):\(port)")

_ = try promise.futureResult.wait()
try server.close(mode: .all).wait()
0xTim commented 2 years ago

The client - we're trying to fix the client problem first

ea7kir commented 2 years ago

Sorry

import Foundation
import WebSocketKit
import NIOPosix

// Setting host to domain, IP Address or localhost make no difference.
let host = "ws://localhost"
let port = 9999

let url = String("\(host):\(port)")

// create the event loop groop
var eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 2)

let promise = eventLoopGroup.next().makePromise(of: String.self)
WebSocket.connect(to: url, on: eventLoopGroup) { ws in
    ws.send("Hello.")
    ws.onText { ws, string in
        print(string)
    }
}.cascadeFailure(to: promise)

// This line bombs out with "Thread 1: Fatal error: blah, blah, blah..."
_ = try promise.futureResult.wait()
0xTim commented 2 years ago

What's the full error output. Please provide as much information as possible, it's really hard to remotely debug issues without all the details

0xTim commented 2 years ago

The errors linked above for the client are invalid certificate and 404, which one was it?

ea7kir commented 2 years ago

Tim, thanks for trying to help me, but I'm lost. Both client and server are here for you run run...

https://github.com/ea7kir/WsClient

https://github.com/ea7kir/WsServer

Please can play with them and figure out why they don't work?

ea7kir commented 2 years ago

When the client asks for ws.localhost.local it works.

When the client asks for wss.localhost.local it halts with this console output.

Swift/ErrorType.swift:200: Fatal error: Error raised at top level: NIOSSL.NIOSSLError.handshakeFailed(NIOSSL.BoringSSLError.sslError([Error: 268435703 error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER]))
2022-01-18 15:18:45.930354+0100 WsClient[8067:158760] Swift/ErrorType.swift:200: Fatal error: Error raised at top level: NIOSSL.NIOSSLError.handshakeFailed(NIOSSL.BoringSSLError.sslError([Error: 268435703 error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER]))
(lldb) 
ea7kir commented 2 years ago

PS: Perhaps your Package.swift needs updating. For example, doesn't sift-no now include some of the product names you're asking for?

It also seems odd that I need to import anything more than WeSocketKit.

Whatever, Xcode is resolving...

swift-nio 2.36.0 swift-nio-ssl 2.17.1 websocket-kit 2.3.0

Also, your websocket-kit package is set for swift 5.2 and macOS(.v10_15) and things have moved on since then.

0xTim commented 2 years ago

This works for me

let host = "wss://ws.ifelse.io"
let port = 443

let url = String("\(host):\(port)")

// create the event loop groop
var eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 2)

let promise = eventLoopGroup.next().makePromise(of: String.self)
WebSocket.connect(to: url, on: eventLoopGroup) { ws in
    ws.send("Hello.")
    ws.onText { ws, string in
        print(string)
    }
}.cascadeFailure(to: promise)

// This line bombs out with "Thread 1: Fatal error: blah, blah, blah..."
_ = try promise.futureResult.wait()

The server you're connecting to must have an old or invalid certificate

ea7kir commented 2 years ago

So is kit client incompatible with the kit server?

0xTim commented 2 years ago

WebsocketKit's server does not support WSS (or TLS) by default. You need to add a TLS channel handler like https://github.com/vapor/vapor/blob/main/Sources/Vapor/HTTP/Server/HTTPServer.swift#330 to make it work and configure it with a valid certificate. Though at that point it's probably just easier to use Vapor anyway

ea7kir commented 2 years ago

Finally, we now know WebSocket-Kit can't be used standalone - as advertised. When I started my big project 6 months ago, I figured Vapor would be overkill and would probably be more trouble that it's worth. My project is a real time client/server application, running on a Raspberry Pi with dozens of relays, sensors and two more Pi s over UDP. It is definitely not a typical iOS app. So, it's back to the drawing board. Why networking is so difficult after nearly half a century is beyond me. Anyway, thanks for your help.

0xTim commented 2 years ago

Just to be clear, it does work as described. WebsocketKit is a low level websocket library to enable you to create WebSocket clients and servers. It is not intended to be a simple, easy to use library that you can configure in one line. If you want that you should use something like Vapor. It's the same as building a low level NIO web server - it you want to be able to serve HTTPS traffic, or WSS traffic in this case then you need to set up, configure and provide the TLS channel handler yourself. WebsocketKit deals with the WebSocket protocol, TLS is out of scope. However, if you need to do it, you can take the few lines linked above, integrate it with a few tweaks and it should work