Live Chat Support How-To

I'm a longtime user of our campus XMPP Instant Messaging service (a.k.a. Jabber). It can be useful for initiating a support contact and getting a quick answer, or for starting the process of managing a longer-duration support issue in the ticketing system.  Chat isn't always the best solution, and we have no intention of closing other avenues of support, but it can especially be useful for quick issues.  We even set up a chat room there to support UC3, but the fact is that many campus users are unaware of the IM service, or simply don't routinely use it.  Setting up an XMPP chat client and signing in could be a sufficient impediment to keep people from bothering.

So, what about anonymous web-based chat?  It's an obvious solution, since so many support sites do this already, but with no obvious implementation.  And most web chat widgets are bad.  So I started thinking about XMPP over HTTP approaches.

It turns out that this was surprisingly easy to set up, once I knew what to try.  Prosody is an XMPP server written in Lua, so it runs on our UC3 site with little management overhead.  It doesn't require 70 packages to support Erlang and a wild variety of extensions — the software dependency graph only has 6 or 7 nodes.  It doesn't require a Tomcat server that we don't currently use (and don't really want).  It's simple, which is entirely appropriate for our simple use case.

On top of Prosody, we have a web application called Candy.  Candy is a pretty nice web-based XMPP chat client.  It uses a BOSH endpoint for brokering AJAX HTTP requests to a persistent connection to an XMPP server; the BOSH support is built into Prosody, so this really is all we need.  Candy is configured to prompt for a nickname and then automatically join the user to a chat room set up on our Prosody instance.  Our staff can join that chat room using regular, individual accounts via normal XMPP accounts like Adium.

Example Screenshots

Here's the setup:

[caption id="attachment_126" align="alignleft" width="150"]

Admin chat room - Adium

Joining the support room from Adium[/caption]

[caption id="attachment_127" align="alignright" width="150"]

Signing in on the web site

Signing in on the web site[/caption]

Now a brief conversation:

[caption id="attachment_128" align="alignleft" width="150"]

User asks a question

User asks a question[/caption]

[caption id="attachment_130" align="alignright" width="150"]

User reads support response

User reads support response[/caption]

[caption id="attachment_129" align="aligncenter" width="150"]

Support staff responds in fat XMPP client

Support staff responds in fat XMPP client[/caption]


Implementation

Now we'll talk about how we set it up from a technical perspective.

Prosody: XMPP Server

Conveniently, Prosody is available from the EPEL repository for EL6/CentOS6/SL6.  For a Red Hat-based system, installing it is as easy as yum install prosody.  The configuration is easy, too.  The salient points of our configuration:

  • the hostname of our server, and the domain of our XMPP service, is uc3.uchicago.edu.
  • we don't need SSL since this is primarily for local and anonymous accounts.
  • we need to specifically support anonymous binding.
  • we do need BOSH support since we're using a web/javascript client.
  • we do need conference (MUC) support since we want shared chat rooms for connecting users to multiple support staff (domain experts for multiple fields).

Here's a simple diff of our /etc/prosody/prosody.cfg.lua file vs. the distributed one:

--- prosody.cfg.lua.dist        2013-04-27 17:32:20.000000000 -0500
+++ prosody.cfg.lua     2013-06-06 13:13:28.241929347 -0500
@@ -20,7 +20,7 @@
 -- for the server. Note that you must create the accounts separately
 -- (see http://prosody.im/doc/creating_accounts for info)
 -- Example: admins = { "user1@example.com", "user2@example.net" }
-admins = { }
+admins = { "dgc@im.uchicago.edu", "dgc@uc3.uchicago.edu" }

 -- Enable use of libevent for better performance under high load
 -- For more information see: http://prosody.im/doc/libevent
@@ -60,7 +60,7 @@

        -- Other specific functionality
                "posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
-               --"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
+               "bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
                --"httpserver"; -- Serve static files from a directory over HTTP
                --"groups"; -- Shared roster support
                --"announce"; -- Send announcement to all online users
@@ -85,10 +85,10 @@

 -- These are the SSL/TLS-related settings. If you don't want
 -- to use SSL/TLS, you may comment or remove this
-ssl = {
-       key = "/etc/pki/tls/private/prosody.key";
-       certificate = "/etc/pki/tls/certs/prosody.crt";
-}
+--ssl = {
+--     key = "/etc/pki/tls/private/prosody.key";
+--     certificate = "/etc/pki/tls/certs/prosody.crt";
+--}

 -- Only allow encrypted streams? Encryption is already used when
 -- available. These options will cause Prosody to deny connections that
@@ -136,17 +136,21 @@

 VirtualHost "localhost"

-VirtualHost "example.com"
-       enabled = false -- Remove this line to enable this host
+VirtualHost "uc3.uchicago.edu"
+       enabled = true -- Remove this line to enable this host
+
+VirtualHost "guest.uc3.uchicago.edu"
+       enabled = true -- Remove this line to enable this host
+       authentication = 'anonymous'

        -- Assign this host a certificate for TLS, otherwise it would use the one
        -- set in the global section (if any).
        -- Note that old-style SSL on port 5223 only supports one certificate, and will always
        -- use the global one.
-       ssl = {
-               key = "certs/example.com.key";
-               certificate = "certs/example.com.crt";
-       }
+       --ssl = {
+       --      key = "certs/example.com.key";
+       --      certificate = "certs/example.com.crt";
+       --}

 ------ Components ------
 -- You can specify components to add hosts that provide special services,
@@ -154,7 +158,7 @@
 -- For more information on components, see http://prosody.im/doc/components

 ---Set up a MUC (multi-user chat) room server on conference.example.com:
---Component "conference.example.com" "muc"
+Component "conference.uc3.uchicago.edu" "muc"

 -- Set up a SOCKS5 bytestream proxy for server-proxied file transfers:
 --Component "proxy.example.com" "proxy65"
@@ -167,3 +171,12 @@
 --
 --Component "gateway.example.com"
 --     component_secret = "password"
+
+bosh_ports = {
+       {
+               port = 5280;
+               path = 'http-bind';
+               interface = "127.0.0.1";
+       }
+}
+

In order of chunks, I:

  • set the admin JIDs (Jabber IDs)
  • enabled BOSH
  • removed SSL certificate requirements
  • added a virtual host for uc3.uchicago.edu
  • added a virtual host for guest.uc3.uchicago.edu that supports anonymous binding — note that this need not be a valid DNS name; it's only meaningful within Prosody!
  • disabled another SSL certificate
  • added a MUC component for conference.uc3.uchicago.edu — again, this need not be a valid DNS name
  • added a port configuration for BOSH support

Then I ran prosodyctl adduser dgc@uc3.uchicago.edu to create my admin user.  That's it!  At this point you can start the server (service prosody start) and enable it on reboot (chkconfig prosody on).  You should be able to configure an account in your XMPP client of choice and log in, then add accounts for all your support colleagues.

BOSH configuration

BOSH is fully provided by Prosody, but you need to instruct your web server to make the endpoint available.  How to do that depends on your web server and the location in your site that you want the endpoint to reside.  Prosody's documentation has some hints on doing this.

For UC3, I decided to root the BOSH service at /, because I don't want its location to depend on where I'm calling it from.  I want the one BOSH endpoint to work regardless of where I anchor the application.  Here's the configuration I put into our Apache VirtualHost entry:

<Location /http-bind>
    Order allow,deny
    Allow from all
</Location>
RewriteEngine On
RewriteRule ^/http-bind/*$ http://localhost:5280/http-bind/ [P,L]

As you see, a call to /http-bind/ will proxy to Prosody's BOSH endpoint over localhost.  This keeps the BOSH endpoint private: it's only exposed within our server and cannot be contacted from outside.  (N.B. This isn't true.  Prosody seems to have a bug wherein the BOSH point will be opened on INADDR_ANY — that is, all interfaces — instead of only on localhost.  But it should be true!  Meanwhile, I blocked public access using iptables.)

Candy: Web XMPP client

Setting up Candy is even easier, once you know what to do.  First, get the Candy tarball.  Unpack it at a convenient location in your web server's DocumentRoot — for example, I used .../candy.  Ensure that you can reach this directory using a web browser — you may need to do special things in your web server configuration to make that possible, or it might just happen automatically.  It depends on your site.

There's only one file you need to adjust: example/index.html.  This file is indeed only an example, but it's fully functional, and you can actually use it for your site's support chat.

Here is the entirety of my example/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Candy - Chats are not dead yet</title>
    <link rel="shortcut icon" href="../res/img/favicon.png" type="image/gif" />
    <link rel="stylesheet" type="text/css" href="../res/default.css" />

    <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"></script>
    <script type="text/javascript" src="../libs/libs.min.js"></script>
    <script type="text/javascript" src="../candy.min.js"></script>
    <script type="text/javascript">
        $(document).ready(function() {
            Candy.init('/http-bind/', {
                core: {
                    debug: false,
                    autojoin: ['support@conference.uc3.uchicago.edu']
                },
                view: { resources: '../res/' }
            });

            Candy.Core.connect('guest.uc3.uchicago.edu');
        });
    </script>
</head>
<body>
    <div id="candy"></div>
</body>
</html>

The essential things here are the autojoin parameter and the Candy.Core.connect argument.  The former tells Candy to connect users automatically to our support chat room.  The latter tells it to connect them anonymously to our special "guest" virtual host in Prosody.  This requires no authentication from the user, and only prompts them for a nickname to use during the chat.

Stitching it all together

You should now be able to create a live chat using your //candy/example/index.html URL.  But that's not what you want to present!  You have two options, really: most obviously, you can take the HTML and Javascript from the example file, and embed it into your own page(s).  That route is a bit perilous though, since the Candy code makes a few assumptions and actually expands the chat <div> to use your full window.  So the easier, and perhaps more precise, route is just to put an <iframe> into your site's HTML.  That's what we did: we created a new page that contains a little intro text, then has the following HTML:

<iframe src="/chat/example/index.html"></iframe>

... and with that, it's done.  You can check out the result here: https://uc3.uchicago.edu/chat.  We're not committed to using this as a formal support channel yet (we have policy questions to work out) but I thought it was worth documenting procedurally for our CIC and other colleagues.