SWI Websockets can not connect Tornado Websocket Server

I’m using: SWI-Prolog version 8.3.17

I want to connect a Websocket client in SWI Prolog with a Tornado Websocket server over localhost.

The Client Code is:

:- module(logicserver, [client/1]).
:- use_module(library(http/websocket)).

:- initialization(client(3040)).


client(Port) :-
	http_open_websocket('ws://localhost:3040/cmd/logic', Request, []),
	format( "Got ~w~n", [Request]),
	startClient(Request).

startClient(Request) :-
	format(string(Message), "Logic ~w", ['Doedel']),
	format("Sent ~p~n", [Message]),
	ws_send(Request, text(Message)),
	ws_receive(Request, Frames),
	format("Received ~p~n", [Frames]),  
	startClient(Request).

The Tornado websocket server is a simple thing from the tutorial (excerpt):

class queryWebSocket(tornado.websocket.WebSocketHandler):
    def open(self):
        print("Websocket open")

    def check_origin(self, origin)
         return True

    def on_message(self, message):
        print(u"You said: " + message)
        self.write_message(message)
    
    def on_close(self):
        print("Websocket closed")

Tornado runs the open method (printing “Websocket open”) but closes immediatly the connection by itself. No closed_reason and closed_code. Before SWI can perform the first ws_send above the connection is alredy closed. The on_close method of Tornado is executed.

I’ve tried to connect (same uri) from a Javascript client, that works without problems.
As you can see, the origin check is disabled. Could anyone imagine whats going wrong? Can it be that there is a tight difference between the websocket implementation on SWI side vs Tornado implementation? Javascript connection to a SWI Websocket server (accourding the SWI docs) is also no problem.

Running on Win10 with Anaconda / Python 3.7, latest stable Tornado version. I would appreciate any hint to the nature of the problem.

I should mention that this is running in a company enviromment with strict IT rules. Is it possible that company policy only allows localhost connections from JS (Browser) to an app an forbid such connection between two (non-JS) applications. I don’t belive that…

Cheers

Hans

I’d start tracing http_open_websocket/3 to see what it has to say. That may give a hint. If you can share the complete Tornado server code, I’m happy to do a little sniffing on the wire to see what is going on. The Prolog web socket server implementation is heavily used and well tested. I have little clue who is using the client.

1 Like

Yes, tracing sound good. I’ll cut out the relevant tornado code to an executable, demonstrating example and share it here. Thx :slight_smile:

First result of debugging http_open_websocket/3 using the graphical debugger: inside this predicate there is the predicate http_open/3. If I step through this step by step it works. If I step through http_open_websocket but step over http_open it works not. It seems first like a timing problem (which other reason could make a difference between tracing with debugger vs running normally?)
It seems also, that after executing http_open_websocket is all ok (in any case), but if I do ws_send the Tornado server close the connection (exception: when debugging as described above).
It doesn’t make much sense to me. I will provide a running example and share it here.
Oh, BTW, status code resulting of http_open is in all cases the correct 101.

cheers

Hans

Here the small test application which shows the effection described above. You need tornado in your Python environment, tested with Python 3.7

Client:

:- module(wsclient, [client/0]).

:- use_module(library(http/websocket)).

client :-
http_open_websocket(‘ws://localhost:3045/cmd/logic’, Request, ),
format(string(Message), “Message ~w”, [‘Hello’]),
format(“Sent ~p~n”, [Message]),
ws_send(Request, text(Message)),
ws_receive(Request, Frames),
format(“Received ~p~n”, [Frames]),
ws_close(Request, 1000, ‘God bye’).

server in python/tornado

import tornado.ioloop
import tornado.web
import tornado.websocket
import os
import json

class MainHandler(tornado.web.RequestHandler):
def get(self):
myPath = os.path.join(os.path.dirname(file), “index.html”)
print(“Inital start, root of app:”, myPath)
self.render(myPath)

class logicWebSocket(tornado.websocket.WebSocketHandler):
def open(self):
print(“Websocket open”)

def check_origin(self, origin):
    print("Origin", origin )
    return True

def on_message(self, message):
    print(u"Client said: " + message)
    self.write_message('okok')

def on_close(self):
    print(" Websocket closed")
    print("Reason: " , self.close_reason)
    print("Code : ", self.close_code)

settings = {
“static_path”: os.path.join(os.path.dirname(file), “index.html”),

}

app = tornado.web.Application([
(r"/cmd/logic", logicWebSocket),
(r"/", MainHandler)
], debug = False, **settings)

if name == “main”:
app.listen(3045)
tornado.ioloop.IOLoop.current().start()

and you may need also a simple intex.html.

Thanks for looking at it, I have no idea at the moment whats going on.

Cheers

Hans

The sample code got a bit messed up with discourse formatting … it’d be easier to debug this if you create an easily downloadable file, for example a git gist.

Also, this requires installing tornado. Can you suggest a way of using venv or whatever is the popular “create environment” command these days, so that i can try installing tornado and then can easily uninstall it? (I presume it uses a “pypi” command or something to install, and I’m not sure what to uninstall)

I could try this on Ubuntu 18.04 - which version of Tornado is the “latest stable”?
But I’d need a “venv” (or equivalent) and “pip” (or equivalent) command for setting up and getting rid of Tornado; I presume the test code can be just run from the command line with python3.7 server.py.

Hi Peter,

wsclient.pl (721 Bytes) wsserver.pl (1,6 KB)

Just rename the wsserver.pl to wsserver.py

I use the Anaconda Environment 64 bit Python 3.7 There you can create an environment with

conda create -n yourenvname python=3.7

and activate it with

conda activate yourenvname

next step is install tornado with

pip install tornado

or

conda install tornado

With plain python this seems the docu for it: venv — Creation of virtual environments — Python 3.9.1 documentation

Yes, you can start with

python wsserver.py

, and then load the wsclient.ps in SWI Prolog (my version 8.3.17) and query

?- client.

After this, Python should print out a “Websocket open” and - in the error case - print “Websocket closed” and close_code none and close_reason none.

The exakt Tornado version in my environment is 6.0.4

Thanks for your help

Cheers

Hans

I’ve installed anaconda and tornado and activated the environment on Ubuntu 18.0.4.

Here’s what I got (which appears to have reproduced the problem, but seems to have additional information that you didn’t have with Windows):

$ python3.7 wsserver.py 
Websocket open
 Websocket closed
Reason:  None
Code :  None
$ swipl wsclient.pl 
Welcome to SWI-Prolog (threaded, 64 bits, version 8.3.18)
SWI-Prolog comes with ABSOLUTELY NO WARRANTY. This is free software.
Please run ?- license. for legal details.

For online help and background, visit https://www.swi-prolog.org
For built-in help, use ?- help(Topic). or ?- apropos(Word).

?- client.
Sent "Message Hello"
Received websocket{data:end_of_file,opcode:close}
ERROR: Socket error: Broken pipe
ERROR: In:
ERROR:   [19] websocket:ws_send(<stream>(0x5587204a6aa0,0x5587204a35f0))
ERROR:   [18] <meta call>
ERROR:   [17] setup_call_catcher_cleanup(websocket:ws_start_message(<stream>(0x5587204a6aa0,0x5587204a35f0),8,0),websocket:write_message_data(<stream>(0x5587204a6aa0,0x5587204a35f0),...),exit,websocket:ws_send(<stream>(0x5587204a6aa0,0x5587204a35f0))) <foreign>
ERROR:   [14] websocket:ws_close_(<stream>(0x5587204a6aa0,0x5587204a35f0),1000,'God bye') at /usr/lib/swi-prolog/library/http/websocket.pl:547
ERROR:   [13] setup_call_catcher_cleanup(websocket:true,websocket:ws_close_(<stream>(0x5587204a6aa0,0x5587204a35f0),1000,'God bye'),_30976,websocket:close(<stream>(0x5587204a6aa0,0x5587204a35f0))) at /usr/lib/swi-prolog/boot/init.pl:619
ERROR:    [9] toplevel_call('<garbage_collected>') at /usr/lib/swi-prolog/boot/toplevel.pl:1113
ERROR: 
ERROR: Note: some frames are missing due to last-call optimization.
ERROR: Re-run your program in debug mode (:- debug.) to get more detail.
^  Exception: (13) setup_call_catcher_cleanup(websocket:true, websocket:ws_close_(<stream>(0x5587204a6aa0,0x5587204a35f0), 1000, 'God bye'), _31218, websocket:close(<stream>(0x5587204a6aa0,0x5587204a35f0))) ? 

I then turned on debug(websocket), debug(websocket(open)), debug(websocket(close)) and ran with debug to give a better traceback:

[debug]  ?- client.
Sent "Message Hello"
% ws_receive(<stream>(0x558720594d20,0x558720594f30)): OpCode=end_of_file, RSV=_12038
% ws_receive(<stream>(0x558720594d20,0x558720594f30)) --> websocket{data:end_of_file,opcode:close}
Received websocket{data:end_of_file,opcode:close}
ERROR: Socket error: Broken pipe
ERROR: In:
ERROR:   [19] websocket:ws_send(<stream>(0x558720594d20,0x558720594f30))
ERROR:   [18] <meta call>
ERROR:   [17] setup_call_catcher_cleanup(websocket:ws_start_message(<stream>(0x558720594d20,0x558720594f30),8,0),websocket:write_message_data(<stream>(0x558720594d20,0x558720594f30),...),exit,websocket:ws_send(<stream>(0x558720594d20,0x558720594f30))) <foreign>
ERROR:   [16] setup_call_cleanup(websocket:ws_start_message(<stream>(0x558720594d20,0x558720594f30),8,0),websocket:write_message_data(<stream>(0x558720594d20,0x558720594f30),...),websocket:ws_send(<stream>(0x558720594d20,0x558720594f30))) at /usr/lib/swi-prolog/boot/init.pl:624
ERROR:   [15] websocket:ws_send(<stream>(0x558720594d20,0x558720594f30),close(1000,'God bye')) at /usr/lib/swi-prolog/library/http/websocket.pl:352
ERROR:   [14] websocket:ws_close_(<stream>(0x558720594d20,0x558720594f30),1000,'God bye') at /usr/lib/swi-prolog/library/http/websocket.pl:547
ERROR:   [13] setup_call_catcher_cleanup(websocket:true,websocket:ws_close_(<stream>(0x558720594d20,0x558720594f30),1000,'God bye'),_13960,websocket:close(<stream>(0x558720594d20,0x558720594f30))) at /usr/lib/swi-prolog/boot/init.pl:619
ERROR:   [12] setup_call_cleanup(websocket:true,websocket:ws_close_(<stream>(0x558720594d20,0x558720594f30),1000,'God bye'),websocket:close(<stream>(0x558720594d20,0x558720594f30))) at /usr/lib/swi-prolog/boot/init.pl:624
ERROR:   [11] websocket:ws_close(<stream>(0x558720594d20,0x558720594f30),1000,'God bye') at /usr/lib/swi-prolog/library/http/websocket.pl:542
ERROR:   [10] wsclient:client at /home/peter/Downloads/wsclient.pl:23
ERROR:    [9] toplevel_call(user:user:client) at /usr/lib/swi-prolog/boot/toplevel.pl:1113

I haven’t run wireshark to figure out what’s happening at the TCP/IP level - not sure it’d show anything, as localhost TCP/IP gets short-circuited - but it appears that your server is just returning a close.

Hope this helps.

Thanks. That reproduces. Then I connected from Firefox which works fine. Compared the request headers and noticed that JavaScript passes an Origin header and the Python server prints this. So, I tried to add an origin to the Prolog client and … Bingo! So, this works:

client :-
    http_open_websocket(
        'ws://localhost:3045/cmd/logic', Request,
        [ request_header('Origin' = 'https://www.swi-prolog.org')
        ]),
    format(string(Message), "Message ~w", ['Hello']),
    format("Sent ~p~n", [Message]),
    ws_send(Request, text(Message)),
    ws_receive(Request, Frames),
    format("Received ~p~n", [Frames]),
    ws_close(Request, 1000, 'God bye').

I guess you can pass anything as Origin as long as it is a valid URL. I tried removing the check_origin from the server, but that doesn’t help. I guess there should be some way to disable the server testing the origin header. There surely is no requirement to send an origin in the WS protocol spec.

2 Likes

Jan, Peter,

thank you very much for you very quick help. That’s really cool. :grinning:
Is this worth an entry in the SWI Prolog docu ?

Cheers

Hans

As far as I can see it is a bug of the server. In RFC 6455 we read (bold by me)

The |Origin| header field [RFC6454] is used to protect against unauthorized cross-origin use of a WebSocket server by scripts using the WebSocket API in a web browser. The server is informed of the script origin generating the WebSocket connection request. If the server does not wish to accept connections from this origin, it can choose to reject the connection by sending an appropriate HTTP error code. This header field is sent by browser clients; for non-browser clients, this header field may be sent if it makes sense in the context of those clients.

As Origin is explicitly mentioned though it may be useful to explicitly mention how it must be specified in the docs. I’ll add a line.

1 Like