joffrey-bion / krossbow

A Kotlin multiplatform coroutine-based STOMP client over websockets, with built-in conversions.
MIT License
199 stars 15 forks source link

Cannot connect to CoilMQ: "Failed to connect at web socket level" #261

Closed EugeneSid closed 2 years ago

EugeneSid commented 2 years ago

I'm using STOMP for interaction between android device and raspberry pi. As a server I have Coilmq STOMP server this installed on RPi4 and successfully tested with stomp.py - a python library of STOMP client.

By using krossbow STOMP client library I receive "GET" command only with URL and headers which doesn't have STOMP initial commands (STOMP or CONNECT) but instead it looks like WEB STOMP version:

b'GET /queue/camera HTTP/1.1\r\nHost: 172.18.5.38:61613\r\nAccept-Charset: UTF-8\r\nAccept: */*\r\nUpgrade: websocket\r\nConnection: upgrade\r\nSec-WebSocket-Key: ZDEzNzIyYzEwOGU5MDdkNw==\r\nSec-WebSocket-Version: 13\r\nUser-Agent: Ktor client\r\n\r\n'

And because of this I'm unable to connect to the server.. Android log info:

Failure(org.hildan.krossbow.stomp.WebSocketConnectionException: Failed to connect at web socket level to ws://172.18.5.38:61613/queue/cam)

The coilmq log shorted info:

Exception occurred during processing of request from ('172.18.4.14', 17474)
Traceback (most recent call last):
...
  File "/usr/local/lib/python3.9/dist-packages/coilmq/util/frames.py", line 51, in parse_headers
    return preamble_lines[0], OrderedDict([l.split(':') for l in preamble_lines[1:]])
ValueError: too many values to unpack (expected 2)

On the other hand, when I run python STOMP client, there is STOMP command and the message looks like:

b'STOMP\naccept-version:1.1\nlogin:admin\npasscode:password\n\n\x00'

And after that I'm successfully connected to the server.

Stomp client have been build on KtorWebSocketClient as in official documentation and using that I attempted to connect via local network.

Any Idea what I'm doing wrong? Thank you!

joffrey-bion commented 2 years ago

Hi, thanks a lot for this report and the details in it.

By using krossbow STOMP client library I receive "GET" command only with URL and headers which doesn't have STOMP initial commands (STOMP or CONNECT) but instead it looks like WEB STOMP version

Could you please clarify what you mean by "WEB STOMP"? Do you mean this?

I think this might be the source of the issue here. STOMP is an application-level protocol, so it needs a transport. The STOMP or CONNECT frame is sent on the wire via this transport. The protocol specification doesn't force the use of a particular network protocol underneath, but usually it's raw TCP or web socket. Web sockets are convenient because they already provide some framing on top of TCP.

Krossbow is a STOMP client using web socket as transport (or an emulated version of websocket via SockJS). In order to send STOMP frames (like STOMP or CONNECT), the Krossbow client first needs to connect at the web socket level. This is done via an HTTP request with an Upgrade header (as defined by the websocket RFC).

Does your STOMP server only support raw TCP connections?

EugeneSid commented 2 years ago

Sorry for delay..

Could you please clarify what you mean by "WEB STOMP"? Do you mean this?

Yes, right.

Does your STOMP server only support raw TCP connections?

I checked CoilMQ STOMP server and I see that it listens for web socket frames. CoilMQ STOMP server received that "GET" command inside a socket frame. But this is the only command it gets. No other frames are coming - so no connection frame (STOMP or CONNECT command).

joffrey-bion commented 2 years ago

CoilMQ STOMP server received that "GET" command inside a socket frame.

Now that is quite unexpected. I'm sorry to ask you for more, but you could you please share the code using Krossbow to connect to the server? I would really like to be able to understand or reproduce the issue myself.

EugeneSid commented 2 years ago

Yes sure.

    private val RPI_EM_LOCALHOST = "172.18.5.38"
    private val SERVER_PORT = "61613"
    private var WEBSOCKET_TOPIC = "/queue/cam"
    private var WEBSOCKET_URI = "ws://$RPI_EM_LOCALHOST:$SERVER_PORT"

    @Inject
    lateinit var stompClient: StompClient

    @Inject
    lateinit var stompSession: StompSession

    private fun connectToEm() { //called from init function
        lifecycleScope.launch {
            try {
                //val webSocket = OkHttpWebSocketClient(okHttpClient)
                stompClient = StompClient(KtorWebSocketClient())
                stompSession = stompClient.connect(
                    "$WEBSOCKET_URI$WEBSOCKET_TOPIC",
                    "admin",
                    "password"
                )
            } catch (ex: Exception) {
                when (ex) {
                    is WebSocketConnectionException, is ClassNotFoundException -> {
                        Log.e("Error:", ex.message.toString())
                    }
                    else -> throw ex
                }
            }
        }
    }

This is separated simplified part of code for RPi connection test but has problem described above.

joffrey-bion commented 2 years ago

Thanks a lot for the quick response and the extra code.

I checked CoilMQ STOMP server and I see that it listens for web socket frames.

My Python level is close to 0, but from what I can see in the CoilMQ code, they really are just streaming a TCP connection without any web socket layer.

This explains why this server doesn't speak HTTP nor websocket, it is a raw TCP server. When receiving an HTTP request to open a web socket connection, the server tries to interpret the raw bytes of the HTTP protocol as a STOMP frame and fails.

Unfortunately Krossbow is a client that relies on web socket at the moment, and it is not a short term goal to support raw TCP sockets. That said, I could consider implementing it if you need it.

EugeneSid commented 2 years ago

Now everything became clear thanks to you. I think I'd looking for some other solution for CoilMQ communication. I really appreciate your help and I'll keep monitoring krossbow updates at least because we are using it in our main project and it works well. Also I really like how quick and good support are. Thanks a lot!

joffrey-bion commented 2 years ago

That's heartwarming, thanks! I try to do my best, and this kind of message really motivates me to do more!

I hope you'll find a nice alternative for this use case. Have a good one!

charlie-niekirk commented 1 year ago

Thanks a lot for the quick response and the extra code.

I checked CoilMQ STOMP server and I see that it listens for web socket frames.

My Python level is close to 0, but from what I can see in the CoilMQ code, they really are just streaming a TCP connection without any web socket layer.

  • they seem to use a raw TCPServer here
  • here they receive raw bytes (it's still unclear to me what request is at that point)
  • here they directly append the raw bytes (without framing) to their custom FrameBuffer
  • then in extract_frame they directly read the raw bytes from the buffer until they get a complete STOMP frame - no websocket frame involved.

This explains why this server doesn't speak HTTP nor websocket, it is a raw TCP server. When receiving an HTTP request to open a web socket connection, the server tries to interpret the raw bytes of the HTTP protocol as a STOMP frame and fails.

Unfortunately Krossbow is a client that relies on web socket at the moment, and it is not a short term goal to support raw TCP sockets. That said, I could consider implementing it if you need it.

It would be really helpful for me, I'm trying to consume a STOMP server using raw TCP sockets.

joffrey-bion commented 1 year ago

Thanks for letting me know. I opened the following issue, which you can upvote: https://github.com/joffrey-bion/krossbow/issues/287