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.
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.
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:
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:
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.
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.
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.
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.