Setting up converse.js as a web-based chat client.

Apr 09 2017

As not bleeding edge, nifty-keen-like-wow the XMPP protocol is, Jabber (the colloquial name for XMPP I'll be using them interchangably in this article) has been my go-to means of person-to-person chat (as well as communication protocol with other parts of me) for a couple of years now.  There are a bunch of different servers out there on multiple platforms, they all support pretty much the same set of features (some have the experimental features, some don't), and the protocol is federated, which is to say that every server can talk to every other server out there (unless you turn that function off), kind of like e-mail.  You can also build some pretty crazy stuff on top of it and not have to worry about the low-level stuff, which isn't necessarily the case with newer protocols like Matrix.  There are also interface libraries for just about every programming language out there.  For example, in my Halo project I use SleekXMPP because it lets me configure only what I want to out of the box and handles all of the fiddly stuff for me (like responding to the different kinds of keepalive pings that Jabber clients send).  Hack to live, not live to hack, right?  There are also XMPP clients for just about every platform out there, from humble Android devices to Windows 10 monstrosities.  However, sometimes you find yourself in a situation in which your XMPP client can't reach the server for whatever reason (and there are some good reasons, let's be fair).

A few weeks ago I tasked one of my searchbots with hunting down web-based Jabber clients, so that I could still communicate with my bots when the XMPP protocol was blocked from where I happened to be at a given time but I could still (mostly) browse the web.  After glancing through the results and reading through a bunch of documentation I settled on converse.js, which is an XMPP client written in JavaScript that you can drop into a web page and use just like a Jabbe client installed on your desktop.  No local software is required to do this, just a regular web browser.  Setting up the converse.js client was pretty easy, configuring the XMPP server a bit harder, and debugging the converse.js configuration was a matter of reading the docs, trial and error, and lots of force-reloading of a browser tab.  Here's how I did it:

First of all, I needed a web page to put the client into.  I'm not a web designer by any stretch of the imagination so I downloaded a very slightly customized copy of the HTML5 Boilerplate code.  All it gives you is a simple but modern HTML page that should be easy to read in just about every web browser that you can mess with 'till your heart's content.  My blog's current theme is an example of such a thing.  Nice, but I wanted to tweak the template a bit to make it a bit more usable: I wanted Bootstrap because I have a little experience with it and there are lots of plugins that I could use later if I wanted to, and no Google Analytics (though chopping out that bit of code is easy).  I was given a .zip file to download and uncompress; everything I care about is in the index.html file, so I immediately opened it in my favorite text editor and in a browser tab and set to work.

The first thing I did was fiddle around a little bit with the Bootstrap code for columns on the page to shake some of the rust out of my head and then delete them because I didn't need them.  I added a little bit of text to serve as a reminder of what the page was for and threw in a couple of links to some useful resources.  Once I was happy with that I ran over to the converse.js quickstart page and followed the instructions to drop a basic Jabber client into the bottom-right corner of the web page:

<link rel="stylesheet" type="text/css" media="screen"
    href="https://cdn.conversejs.org/css/converse.min.css">
<script src="https://cdn.conversejs.org/dist/converse.min.js"></script>

<script>
    converse.initialize({
        bosh_service_url: 'https://bind.conversejs.org',
        show_controlbox_by_default: true
    });
</script>

I realize that I should not be using a copy of the converse.js client served by somebody else and should build and upload my own copy, but for a proof of concept that minimizes the number of variables I have to juggle when I don't know what I'm doing, it'll suffice.  The bosh_service_url bit needed some tweaking later, so hang onto that.

The second bit involved configuring my Prosody server for BOSH, which basically lets the XMPP protocol run over HTTP(S) like any other request from a web browser.  That required reading through the docs a couple of times.  Here's what I had before I started messing with things for reference:

  • SSL/TLS support to encrypt all traffic, courtesy of Let's Encrypt
  • Multi-user chat support
  • No remote account registration, because I want to control who has accounts on my server

This involved editing the /etc/prosody/prosody.cfg.lua file on my server and uncommenting the "bosh" and "http" lines near the top of the file.  Pretty straightforward, that.  After reading through this part of the documentation, which caught my eye because I've run into similar problems at work with web apps, I set the "cross_domain_bosh" and "consider_bosh_secure" configuration options as well because web browser security has gotten better over the years, but sometimes it can break perfectly legitimate things that you're trying to accomplish in weird ways.  BOSH is supposed to run over two network ports, 5280/tcp (BOSH over HTTP) and 5281/tcp (BOSH over HTTPS (encrypted version)), so I restarted Prosody and checked the list of listening network ports to see what was there: sudo lsof -i tcp | grep lua

...
lua5.1  19216    prosody   13u  IPv6 333630      0t0  TCP *:5280 (LISTEN)
lua5.1  19216    prosody   14u  IPv4 333631      0t0  TCP *:5280 (LISTEN)
...

Where was port 5281?  That's the encrypted BOSH port, and no way in hell am I going live without crypto...

I fiddled around some more until I figured out what was going on, or at least found a fix.  What I had to do was copy my SSL/TLS configuration block out of the part of the config file specific to myxmppserver.example.com into the global configuration block.  I don't know why I had to do this, because logically it should have been okay with the /etc/prosody/certs/localhost.[cert,key] temp certs.  There is a separate SSL/TLS configuration block in the part of the file specific to my Jaber server that should have taken precedence, but the BOSH and HTTP modules didn't work properly until I replaced the default part.  Here's what it looks like now:

ssl = {
    key = "/home/letsencrypt/myxmppserver.example.com/domain.key";
    certificate = "/home/letsencrypt/myxmppserver.example.com/chained.pem";
    dhparam = "/etc/prosody/certs/dhparams-2048.pem";
    options = {
        "no_ticket",
        "no_compression",
        "cipher_server_preference",
        "single_dh_use",
        "single_ecdh_use",
        "no_sslv2",
        "no_sslv3"
        };
    ciphers = "ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA;";
    }

After restarting Prosody and looking at the ports the server had open, I saw what I was expecting to find:

...
lua5.1  19216    prosody   13u  IPv6 333630      0t0  TCP *:5280 (LISTEN)
lua5.1  19216    prosody   14u  IPv4 333631      0t0  TCP *:5280 (LISTEN)
lua5.1  19216    prosody   15u  IPv6 333632      0t0  TCP *:5281 (LISTEN)
lua5.1  19216    prosody   16u  IPv4 333633      0t0  TCP *:5281 (LISTEN)
...

Perfect.  Now to install a web server, set up SSL/TLS certificates (again through Let's Encrypt), upload the web content with converse.js, and load it in my browser.  The page came up as expeted, the converse.js client came up... but I wasn't able to log into the server.  I poked around a little bit with Firefox's web developer console, reloaded the page, tried logging in a few more times, but it took a few tries before I realized that I didn't have the bosh_service_url bit in my index.html page right.  Prosody's documentation is pretty clear on what it should look like.  Oops.  To save you some time, here's what I'm using:

https://myxmppserver.example.com:5281/http-bind

This points to the port that Prosody has open, not my web server.  Prosody happens to speak HTTP(S) on its own, so don't worry about messing around with your web server.

Back to my web browser, force-reloading the page, and trying to log in again.  And again.  And again.  Each time, no soap.  I went so far as to install tcpdump on my server and watch the network traffic coming from my laptop while trying to log in.  I saw no traffic that looked like it was from my new XMPP client.

I'll cut to the chase: I forgot to open holes in the firewall for ports 5280/tcp and 5281/tcp.  That's why I couldn't log into my Jabber server.  Here are the commands you'll need to fix that minor problem:

ufw allow 5280
ufw allow 5281

My next login attempt with the web-based client was a success.  Now to screw around with the configuration variables for converse.js to increase its security as much as was reasonable.  After some trial and error, here's what the converse.initialize(); part of the index.html file on my XMPP server looks like now:

<script>
    converse.initialize({
        bosh_service_url: 'https://myxmppserver.example.com:5281/http-bind',
        show_controlbox_by_default: true,
        allow_list_rooms: true,
        auto_away: 180,
        auto_xa: 600,
        auto_reconnect: true,
        allow_otr: true,
        cache_otr_key: true,
        sticky_controlbox: true,
        use_otr_by_default: true
    });
</script>

It took a little doing, because I'd turn on a config option at a time, force-reload the page, and find that the chat box didn't come up.  Or the userlist didn't come up.  Or some other weird-ass thing.  But at last I now have a nice little web-based chat client on my server that I can use when my regular Jabber client isn't able to log in properly.  If there is sufficient interest, I'll post the entire HTML page as a download for everybody to use if you're interested in setting up your own web-based Jabber client.