Using library(hub)

I’m trying to put together a simple test-case for library(hub), but I can’t seem to have messages be sent back-and-forth as expected.

My code looks as follows:

:- http_handler(root(ws), 
                http_upgrade_to_websocket(handle_websocket, []),
                [spawn([])]).

handle_websocket(WebSocket) :-
    debug(server(websocket), "Connection incoming ~w", [WebSocket]),
    hub_add(hotwire_hub, WebSocket, Id),
    debug(server(websocket), "Added ~w", [Id]).

get_or_create_socket_hub(Hub) :-
    current_hub(hotwire_hub, Hub)
    -> true
    ; hub_create(hotwire_hub, Hub, []).

:- dynamic socket_hub_thread/1.

start_socket_hub :-
    get_or_create_socket_hub(Hub),
    debug(server(websocket), "Creating hub ~w", [Hub]),
    ( socket_hub_thread(_) -> stop_socket_hub ; true ),
    thread_create(socket_hub_listener(Hub), SocketListenerId, []),
    assertz(socket_hub_thread(SocketListenerId)).

stop_socket_hub :-
    socket_hub_thread(ThreadId),
    thread_send_message(ThreadId, done),
    thread_join(ThreadId),
    retractall(socket_hub_thread(ThreadId)).

socket_hub_listener(_Hub) :-
    thread_self(Self),
    thread_get_message(Self, done, [timeout(0)]),
    debug(server(websocket), "Stopping socket hub", []),
    !. % done
socket_hub_listener(Hub) :-
    ( thread_get_message(Hub.queues.event, Msg, [timeout(0.1)])
    ->  handle_hub_event(Msg) ; true ),
    socket_hub_listener(Hub).

handle_hub_event(Msg) :-
    hub{error: Error} :<  Msg,
    debug(server(websocket), "Hub error: ~w", [Error]).
handle_hub_event(Msg) :-
    hub{joined: Id} :<  Msg,
    debug(server(websocket), "Hub new connection: ~w", [Id]),
    hub_send(Id, text("Hello!")),
    debug(server(websocket), "Sent message to new", []).
handle_hub_event(Msg) :-
    debug(server(websocket), "Got Message ~k", [Msg]).

After starting the webserver and the socket listening thread, I open a page that includes the following javascript:

const createSocket = () => {
  const proto = window.location.protocol === "https:" ? "wss" : "ws";
  const socket = new WebSocket(`${proto}://${window.location.host}/ws`);
  window.test_socket = socket;
  socket.addEventListener('open', (event) => {
    console.log("Socket open");
  });
  socket.addEventListener('message', (event) => {
    console.log("Socket Message: ${event.data}");
  });
  socket.addEventListener('error', (event) => {
    console.log("Socket error", event);
  });
  window.addEventListener('beforeunload', () => {
    console.log("page unloading");
    socket.close();
  });
};

createSocket();

On the client side, I see it log the “socket open” message. On the server side, I see the following:

% [Thread 15] 2023-02-19 21:34:24 Connection incoming <stream>(0x6000037ccc00,0x6000037ccd00)
% [Thread 15] 2023-02-19 21:34:24 Added 16fd0bd8-b0c7-11ed-9cc2-7b5f40bf6966
% [Thread 12] 2023-02-19 21:34:24 Hub new connection: 16fd0bd8-b0c7-11ed-9cc2-7b5f40bf6966
% [Thread 12] 2023-02-19 21:34:24 Sent message to new
% [Thread hub_out_q_4] 2023-02-19 21:34:24 To: <stream>(0x6000037ccc00,0x6000037ccd00) messages: text("Hello!")
% [Thread 12] 2023-02-19 21:34:24 Hub error: error(existence_error(stream,<stream>(0x6000037ccc00,0x6000037ccd00)),context(websocket:ws_start_message/3,_2700))

No matter what I’ve tried (hub_broadcast/2, hub_send/2), attempting to send from the server to the client gives this error (existence_error for the client stream) and sending from the client to the server gives nothing.

Is there some step I’m missing in the handler to keep the streams open or some such?

1 Like

Bit hard to tell. Did you find the demo at GitHub - JanWielemaker/swi-chat: Demo SWI-Prolog HTTP based chat server?

Anne uses library(hub) for Ludum Dare, the code is on GitHub:

A search of the repository for hub_create and then choosing the code

Search results

Ah, thanks for the links @jan and @EricGT, I will take a look.

Ah, of course, I wasn’t passing the guarded(false) option to http_upgrade_to_websocket/2. I’d missed that it also closes on normal termination of the goal. Seems good now!

1 Like

Care to share the working code and instructions on how to use it. From what I see you probably have one of those most basic and easy to understand examples of web sockets being used with SWI-Prolog.

Will do! I’m actually using it to put together a template for a basic interactive web application in Prolog; I’ll share the link here when it’s all put together.

1 Like

(I’ll add some documentation to this & post a proper link in a bit, but here it is if you want to take a look)

Here is a simple TLS/Websocket server example (not using library(hub) ):

Simple TLS/Websocket server:

:- use_module(library(http/websocket)).
:- use_module(library(http/http_server)).
:- use_module(library(http/http_ssl_plugin)). % don't forget this, otherwise it will act as an http server and not print any error


:- http_handler(root(ws),
                http_upgrade_to_websocket(echo, []),
                [spawn([])]).
:- http_handler(root(.),
                home_page,
                [spawn([])]).
:- http_server([port(8002) ,
                ssl([  certificate_file('/tmp/fullchain.pem'), % letsencrypt gives you this file
                       key_file('/tmp/privkey.pem'), % letsencrypt gives you this file
                       cacerts([file('/etc/ssl/certs/ca-certificates.crt'),
											 system(root_certificates)]),
                       cert_verify_hook(cert_accept_any) % later delete this when a real cert is used that matches the hostname (we're using localhost/127.0.0.1 here)
                    ])
                ]).


echo(WebSocket) :-
    ws_receive(WebSocket, Message),
    (   Message.opcode == close
    ->  true
    ;   ws_send(WebSocket, Message),
        echo(WebSocket)
    ).

home_page(_Request) :-
    reply_html_page(
        title('Demo server'),
        [ h1('Hello world!')
        ]).

Client WSS:

?- URL = 'wss://127.0.0.1:8002/ws',
   http_open_websocket(URL, WS, [cert_verify_hook(cert_accept_any)]),
   ws_send(WS, text('Hello World!')),
   ws_receive(WS, Reply),
   ws_close(WS, 1000, "Goodbye").
URL = 'wss://127.0.0.1:8002/ws',
WS = <stream>(0x5608c775b900,0x5608c761d100),
Reply = websocket{data:"Hello World!", format:string, opcode:text}

Client HTTPS:

?- http_get('https://127.0.0.1:8002',D,
[cert_verify_hook(cert_accept_any)]). % delete option later, when using a real cert that matches the hostname
D = '<!DOCTYPE html>\n<html>\n<head>\n<title>Demo server</title>\n\n<meta http-equiv="content-type" content="text/html; charset=UTF-8">\n\n</head>\n<body>\n\n<h1>Hello world!</h1>\n\n</body>\n</html>\n'.

EDIT:

To generate test key and certificate you can do (with newer openssl):

 openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \
  -keyout /tmp/privkey.pem -out /tmp/fullchain.pem -subj "/CN=domain.com"
1 Like

An example with SSL, we don’t see that often.

Will definitely try this.

Is there a how-to for testing an internal webserver with SSL? letsencrypt.org seems to require a webserver that’s on the internet; but I want to test things out on my internal network.

Peter, you can use the openssl command I posted as an EDIT.

This will allow you to test the server internally without a need for letsencrypt. The queries I included in the post (“Client WSS” and “Client HTTPS”) actually test TLS (used to be called ssl) with an internal (127.0.0.1) server both for websockets (wss) and http (https).

So just follow these steps.

  1. Copy the source file from the post (“Simple TLS/Websocket server”) into /tmp/w.pl
  2. run the openssl command in the post to generate test key and certificate (/tmp/privkey.pem and /tmp/fullchain.pem)
  3. swipl /tmp/w.pl
  4. Run the “Client WSS” or “Client HTTPS” queries

There is a lot of stuff in the ssl package, also in use for https tests by the http package. Both use a self created CA and certificates created from that. It doesn’t test handling public authorities.