websocketrakucro

Secure websockets with Cro


Briefly: I created a service on an internet server using Cro and websocket. Very simple using Cro examples. No problem when sending and receiving data from an HTML page when the page is served as localhost. When the page is served using https, the websocket cannot be established. How is the wss protocol be used with Cro?

Update: After installing cro and running cro stub :secure, the service.p6 has some more code not explicit in the documentation.

More detail: I have a docker file running on the internet server, Cro is set to listen on 35145, so the docker command is docker --rm -t myApp -p 35145:35145

The service file contains

use Cro::HTTP::Log::File;
use Cro::HTTP::Server;
use Cro::HTTP::Router;
use Cro::HTTP::Router::WebSocket;

my $host = %*ENV<RAKU_WEB_REPL_HOST> // '0.0.0.0';
my $port = %*ENV<RAKU_WEB_REPL_PORT> // 35145;
my Cro::Service $http = Cro::HTTP::Server.new(
    http => <1.1>,
    :$host,
    :$port,
    application => routes(),
    after => [
        Cro::HTTP::Log::File.new(logs => $*OUT, errors => $*ERR)
    ]
    );
$http.start;
react {
    whenever signal(SIGINT) {
        say "Shutting down...";
        $http.stop;
        done;
    }
}
sub routes() {
    route {
        get -> 'raku' {
            web-socket :json, -> $incoming {
                supply whenever $incoming -> $message {
                    my $json = await $message.body;
                    if $json<code> {
                        my $stdout, $stderr;
                        # process code 
                        emit({ :$stdout, :$stderr })
                    }
                }
            }
        }
    }
}

In the HTML I have a textarea container with an id raku-code. The js script has the following (I set websocketHost and websocketPort elsewhere in the script) in a handler that fires after the DOM is ready:

const connect = function() {
    // Return a promise, which will wait for the socket to open
    return new Promise((resolve, reject) => {
        // This calculates the link to the websocket.
        const socketProtocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
        const socketUrl = `${socketProtocol}//${websocketHost}:${websocketPort}/raku`;
        socket = new WebSocket(socketUrl);

        // This will fire once the socket opens
        socket.onopen = (e) => {
            // Send a little test data, which we can use on the server if we want
            socket.send(JSON.stringify({ "loaded" : true }));
            // Resolve the promise - we are connected
            resolve();
        }

        // This will fire when the server sends the user a message
        socket.onmessage = (data) => {
            let parsedData = JSON.parse(data.data);
            const resOut = document.getElementById('raku-ws-stdout');
            const resErr = document.getElementById('raku-ws-stderr');
            resOut.textContent = parsedData.stdout;
            resErr.textContent = parsedData.stderr;
        }

When an HTML file with this JS script is set up, and served locally I can send data to the Cro app running on an internet server, and the Cro App (running in a docker image) processes and returns data which is placed in the right HTML container. Using Firefox and the developer tools, I can see that the ws connection is created.

But when I serve the same file via Apache which forces access via https, Firefox issues an error that the 'wss' connection cannot be created. In addition, if I force a 'ws' connection in the JS script, Firefox prevents the creation of a non-secure connection.

a) How do I change the Cro coding to allow for wss? From the Cro documentation it seems I need to add a Cro::TLS listener, but it isn't clear where to instantiate the listener. b) If this is to be in a docker file, would I need to include the secret encryption keys in the image, which is not something I would like to do? c) Is there a way to put the Cro app behind the Apache server so that the websocket is decrypted/encrypted by Apache?


Solution

  • How do I change the Cro coding to allow for wss? From the Cro documentation it seems I need to add a Cro::TLS listener, but it isn't clear where to instantiate the listener.

    Just pass the needed arguments to Cro::HTTP::Server, it will set up the listener for you.

    If this is to be in a docker file, would I need to include the secret encryption keys in the image, which is not something I would like to do?

    No. You can keep them in a volume, or bind-mount them from the host machine.

    Is there a way to put the Cro app behind the Apache server so that the websocket is decrypted/encrypted by Apache?

    Yes, same as with any other app. Use mod_proxy, mod_proxy_wstunnel and a ProxyPass command. Other frontends such as nginx, haproxy, or envoy will also do the job.