Jabbr, an XMPP Library for OCaml

Mike Lin
mikelin (at) mit.edu

Jabbr is an OCaml library for the XML Messaging and Presence Protocol, more commonly known as Jabber, based on my Yaxpo reentrant XML parser. Jabbr provides basic XML stream and authentication services based on a clean state machine abstraction which imposes minimal constraints on the underlying I/O mechanisms used by the driver program. It currently does not provide automated support for higher-level XMPP abstractions like presence and roster management.

In addition to Yaxpo, Jabbr requires Xavier Leroy's Cryptokit library, which provides SHA-1 hash support for authentication. I have provided a native Windows port of Cryptokit.


Dependencies


Jabbr Package

The latest tarball for Jabbr can be found here. It is dated 24-Nov-2002. The library should still be considered "alpha" code, and interfaces may still change in the future.

To build Jabbr, simply extract the source tree and run make. The libraries and interfaces will be copied to the built directory. The libraries to link with your bytecode- and native-compiled programs are jabbr.cma and jabbr.cmxa, respectively. In addition, you may wish to use the jabbr_sync.cma/jabber_sync.cmxa module as explained below. These files can be automatically put into your OCaml libraries directory by running make install as superuser. Note that to compile programs you will also need to link (through direct and indirect dependencies): nums, unix, cryptokit, yaxpo.

There are some explanation and examples below, and the OCamldoc can be viewed here. The documentation can also be built from the sources if you have OCamldoc installed; this is done with make doc.

Jabbr provides the following modules:

Module Description
Auth jabber:iq:auth authentication state machine
Jabber Defines classes for the IQ, Presence, and Message Jabber packets.
Jabbersm Defines an abstract interface for a Jabber packet processing state machine.
Jabber_sync Synchronous (blocking) Jabber client sessions, suitable for creating components and bots.
Jid An abstraction for Jabber Identifies (JIDs).
Xmlstream Provides XML streams, the basic XMPP wire protocol.

Explanation and examples follow. You should familiarize yourself with the use of the Yaxpo XML parser, particularly the Yaxpodom interface, before continuing.


Asynchronous XML streams

The use of a fully reentrant XML parser allows Jabbr's XML streams implementation to be considerably more flexible than most other XMPP libraries. Informally, consider an XML streams implementation as two parts: a writer and a reader. The writer is fairly trivial; it is simply responsible for serializing an XML element (represented as a Yaxpodom element) into a string of bytes suitable for transmitting over the network. The reader has more interesting problems to solve; it must examine data as it is received from the network in order to determine where the XMPP packet boundary is located. Because XMPP lacks a byte-counting or sentinel framing mechanism, this means the reader must essentially XML-parse the data as it is received.

A fully asnchronous XML stream reader should satisfy these requirements:

  1. It should accept data from the network in arbitrary chunks.
  2. It should return parsed packets (XML elements) as soon as they are available.
  3. It should always return control to its caller; that is, it should never block even if a packet has been only partially received.

These requirements are typically very difficult to satisfy in the case of XMPP, because most XML parsers are not re-entrant; they typically cannot satisfy requirement (3) and sometimes cannot even satisfy requirement (1). This is not the fault of the XML parsers, which are designed with the concept of parsing XML contained in a file in mind; the fault lies squarely with the XMPP protocol which imposes this on-the-fly parsing requirement on us. Jabbr, nonetheless, is based on a fully reentrant XML parser, so it can satisfy all three requirements.

The basic interface to Jabbr's xml_stream_reader looks as follows (this is heavily abbreviated, and only meant to convey the basic idea):

open Yaxpodom

exception Not_enough

class type xml_stream_reader =
object
  method add_data : (string->int->int->unit)

  method pull_handshake : (unit->(qname*att list))

  method pull_ele : (unit->ele)   
end

val mk_xml_stream_reader : (unit -> xml_stream_reader)

The reader itself does not concern itself with anything about TCP/IP, sockets APIs, or other arcanity; that is the responsibility of the driver program. The driver program should follow these basic steps:

  1. Receive some data from the server, by whatever means necessary (blocking, nonblocking, ...)
  2. Pass the data to the reader using add_data
  3. Call pull_ele to extract the next XML element from the stream.
  4. Process the received element, and transmit any replies through an xml_stream_writer. Go to (3).

There are of course some technicalities: in XMPP each stream has a "handshake" consisting of a document start tag, which must be extracted before any other elements; also, the client has to transmit its handshake first. But the above should represent the basic structure of the driver program.

The flexibility of this model should be clear, especially if you are used to using other XMPP libraries. It does not mandate that you use blocking I/O or an event-driven programming model; you can structure your I/O however you wish, and the XML stream reader is totally abstracted from it.


XMPP packet processing

The XML stream readers and writers speak in terms of Yaxpodom elements, while XMPP is concerned with message, presence, and iq packets. Jabbr provides some nice abstractions for these, which can be converted to or from XML elements. The use of these is pretty obvious and I'll defer to OCamldoc for their description. Jabbr also provides a convienient Jid abstraction for dealing with Jabber IDs.


State machine abstraction

Jabber operations such as authentication typically involve several request-response transactions with the server. This can make it pretty complicated to implement under a generalized I/O model. To ease things a bit for the driver program, I defined a simple state machine abstraction which hopefully provides a uniform interface for making use of these sorts of operations. Currently, the authenticator is the only thing that uses it, but we could imagine presence and roster management components using them in the future.

Essentially, such a state machine is an object that systematically accepts packets from the server, and then has some state which the driver program can query for. For example, the authenticator may have intermediate, accept, and reject states.

Suppose for simplicity we have recv_packet : unit -> Jabbr.packet, which gets the next packet from the server, and send_packet : Jabbr.packet -> unit, which sends a packet to the server. The use of the authentication state machine might proceed as follows:

let authenticator = Auth.mk_auth_machine (Auth.Plaintext ("username","password"))
                                         "resource" send_packet;;
authenticator#start ();;

while authenticator#get_state () = Jabbersm.Intermediate do
  authenticator#add_packet (recv_packet ())
done

When this code finishes, the authenticator will be in either an accept or reject state, according to whether the server accepted or rejected the credentials.


High-level synchronous Jabber sessions

It is often the case for writing simple components and bots that you don't really care about asynchronous socket I/O, and you just want a simple high-level Jabber session abstraction. The Jabbr_sync module provides this as follows:

This makes connecting to Jabber as simple as:

let s = Jabber_sync.mk_session ();;
s#connect "jabber.org" 5222;;
s#authenticate (Auth.Digest ("username","password",s#get_stream_id())) "ocaml";;

This connects us as username@jabber.org/ocaml. We could then continue with something like:

let mymsg = Jabber.mk_message "";;
mymsg#set_to (Some "mlin@xmpp.mit.edu");;
mymsg#set_children [Yaxpodom.Text "hello, world!"];;
s#write_packet (Jabber.Message mymsg);;
while true do
  let p = s#pull_packet () in
   (* process p *)
   ()
done

This dispatches a message, then receives packets forever.


Code examples on this page were generated with caml2html by Sebastien Ailleret.