December 12, 2007

XMPP in your browser: Flex 2 with XIFF

Posted in Software at 23:40 by graham

An entry about using the XIFF API to get your Flex application talking XMPP. If you don’t know what I’m talking about no need to read on really.

Some definitions

  • XMPP is an open protocol for instant messaging, that runs the Jabber network and Google talk.
  • Flex 2 is a programmer-friendly way of developing Flash applications, using ActionScript 3, which looks a lot like Java.
  • XIFF is an ActionScript 3 API for XMPP.

If you put those three items together you get, yes, a paradigm shift in web based application development. But let’s not get fancy just yet (a later blog post might wax lyrical about the future of web apps), for now here are some recipes for doing just about anything with XMPP, ActionScript 3, and XIFF.

Permission to connect

The Flash client in the browser is by default only allowed to access the exact domain it came from, so if your XMPP server is on chat.myserver.com (or even just myserver.com), and your Flash was served from www.myserver.com, you need a crossdomain.xml file.

    < ?xml version='1.0'>
    <!DOCTYPE cross-domain-policy SYSTEM 'http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd'>
    <cross-domain-policy>
    <allow-access-from domain='*' />
    </cross-domain-policy>

[Remove the space between the first < and the ?]

Save this in the root of chat.myserver.com, and make sure an HTTP server is there ready to serve it. If you are not using the standard XMPP port 5222, but instead are using a port below 1024 (possibly to get through a firewall), add to-ports="*" to the allow-access-from line.

Now start your Flex application with:

import flash.system.Security;
Security.loadPolicyFile("http://"+ server +"/crossdomain.xml");

The Openfire server provides a built-in crossdomain.xml for you on an xml socket on port 5229, so on that server you can skip creating the file , you don’t need to run an HTTP server (although the user has to get your Flash from somewhere), and use:

Security.loadPolicyFile("xmlsocket://"+ server +":5229");

Only port 80 is permitted for HTTP serving of policy files – any other port must use an xmlsocket. See Actionscript docs for Security.loadPolicyFile

Create the connection

    import org.jivesoftware.xiff.core.XMPPSocketConnection;
    connection = new XMPPSocketConnection();

Use XMPPSocketConnection instead of just XMPPConnection, as the later doesn’t work correctly with ejabberd.

Get the roster ready

    import org.jivesoftware.xiff.im.Roster;

    roster = new Roster(connection);
    roster.addEventListener(RosterEvent.SUBSCRIPTION_DENIAL, rosterHandler);
    roster.addEventListener(RosterEvent.SUBSCRIPTION_REQUEST, rosterHandler);
    roster.addEventListener(RosterEvent.SUBSCRIPTION_REVOCATION, rosterHandler);
    roster.addEventListener(RosterEvent.USER_AVAILABLE, rosterHandler);
    roster.addEventListener(RosterEvent.USER_UNAVAILABLE, rosterHandler);

    private function rosterHandler(event:RosterEvent):void {

        trace("rosterHandler");

        switch (event.type){
            case RosterEvent.SUBSCRIPTION_REQUEST:
                // Fill this bit in, obviously
                break;
            case RosterEvent.USER_UNAVAILABLE :
                trace (event.jid + " is Unavailable (RosterEvent)");
                break;
            case RosterEvent.USER_AVAILABLE :
                trace (event.jid + " is Available (RosterEvent)");
                break;
            case RosterEvent.SUBSCRIPTION_DENIAL :
                trace (event.jid + " denied your request (RosterEvent)");
                break;
            case RosterEvent.SUBSCRIPTION_REVOCATION :
                // this fires at unexpected times so ignore it.
                // trace (event.jid + " revoked your presence (RosterEvent)");
                break;
            default :
                // do nothing... not recognized
        }
    }

Login

    connection.username = "webuser";
    connection.password = "secret";
    connection.server = "chat.myserver.com";
    connection.resource = "Web";
    connection.connect("standard");

At this stage if any of ‘webuser’s contacts are online the USER_AVAILABLE event will fire once for each, and connectionManager.isLoggedIn() should return true.

Register a new account

It seems you need to fail a login before you can register a new user, and after creating a new user you need to disconnect and reconnect as that user (assuming you want to use the account you just created):

connection.addEventListener(XIFFErrorEvent.XIFF_ERROR, registerNewUser);
connection.addEventListener(
    RegistrationSuccessEvent.REGISTRATION_SUCCESS, reconnect);

then try and login with a non existent username and password (see Login section above). The login failure will call your method:

    private function registerNewUser(event:XIFFErrorEvent):void {
        trace("registerNewUser");

        if ( event.errorCode != 401 || event.errorType != "auth" ) {
            // Only act on failed login, ignore other errors
            return;
        }

        username = "newUser";
        password = "newUserPassword";

        var registrationFields:Object = new Object();
        registrationFields.username = username;
        registrationFields.password = password;

        connection.sendRegistrationFields(registrationFields, null);

        connection.removeEventListener(XIFFErrorEvent.XIFF_ERROR, registerNewUser);
    }

Your server needs to allow registrations with only the username and password, and to allow registrations from anyone. This is usually the server default, although they might insist on an e-mail. Successful registration will call the method you registered earlier:

    private function reconnect(event:RegistrationSuccessEvent):void {
        trace("reconnect");

        connection.disconnect();

        initRoster();
        login();
    }

The initRoster and login will do the obvious – see the sections above.

Subscribe to the presence of another user

    var userJID:String = "bob@yourserver.com";
    roster.addContact(agentJID, "Bobby", "People", true);

This will ask ‘bob@yourserver.com’ if they allow your user to subscribe to their presence. When they reply, which may be some time if they are not online, it will fire one of RosterEvent.SUBSCRIPTION_DENIAL if they deny you, RosterEvent.SUBSCRIPTION_REQUEST if they choose to subscribe to your presence in turn, and, if they authorized you and are online, RosterEvent.USER_AVAILABLE.

Multi-user chat

To chat with people you create a room, join that room, then invite them to join that room. room = new Room(); room.setRoomJID( roomJID ); // See explanation after the code room.setConnection( connection );

    room.addEventListener(RoomEvent.USER_JOIN, roomHandler);
    room.addEventListener(RoomEvent.USER_DEPARTURE, roomHandler);
    room.addEventListener(RoomEvent.GROUP_MESSAGE, roomHandler);
    room.addEventListener(RoomEvent.ROOM_JOIN, inviteToRoom);   // Invite the guests when we join the room
    // We need the room in our roster to handle presence messages of people in the room
    if ( roster.getContactInformation(roomJID) == null ) {
        roster.addContact(roomJID, "Something", "People", false);
    }

    room.join();

Multi-User chat is an extension to Jabber, so usually uses a separate server. In the case of jabberd this is a physically separate server called ‘mu-conference’. For Openfire and ejabberd this is built in. Hence the roomJID is usually at the server prefixed with ‘conference’: ‘myroomname@conference.yourserver.com’

When you join the room your method will be called: private function inviteToRoom(event:RoomEvent):void { trace(“ConnectionManager.inviteToRoom”); room.invite( “bob@yourserver.com”, “Come and chat” ); }

The roomHandler method should be straighforward – copy the earlier rosterHandler and change the events.

Auto-reconnect

If your Flash is going to be sitting in someone’s open browser for any length of time, the little pixies that run down the pipes that make up the Internet might accidentally disconnect you – remember that your Flex / Flash app is holding an open socket to the XMPP server the whole time the page is open. Hence you need to auto-reconnect:

    connection.addEventListener(XIFFErrorEvent.XIFF_ERROR, reconnectOnError);

    private function reconnectOnError(event:XIFFErrorEvent):void {
        trace("reconnectOnError");

        if ( event.errorCode == 503 ) { // Service unavailable, means we got disconnected
            this.connection = null;
            connect();  // Creates a new connection, a new roster, and logs in

            if ( room != null ) {  // If in a chat rejoin that room
                rejoinRoom( room.roomName );
            }
        }
    }

Choosing an XMPP Server

You’re obviously going to need a server to do this. If you have lots of memory, prefer:

  • Openfire – Built by the company behind XIFF, this is the easiest to install and most fully featured XMPP server out there right now. It’s in Java so it’s not appropriate if your server memory is limited, such as on a shared host or UML server. It has a great web admin interface (port 9090 by default). I use this for development.

    Otherwise choose between these two:

  • Ejabberd – Written in Erlang, this is the one to choose if your site has an insanely large amount of visitors. Also has a pretty decent web admin interface, on yourserver.com:5280/admin/. To login you need to create an account via your Jabber client, authorize that account as admin in /etc/ejabberd/ejabberd.cfg, and login using your full JID, such as ‘admin@yourserver.com’, and your password.

  • Jabberd 1.4 with mu-conference – Standard Unix C/C++ server, and I think the original Jabber server. Makes your sysadmin happy because they are good o’ regular Unix daemons. This is the one I use live because it takes 9M of memory, with only 2M resident. No fancy admin interfaces here – there’s some shell scripts floating around, or hack the database directly.

Debugging

Get Wireshark so you can see what your Flex app and the XMPP server are saying to each other.

Create a mm.cfg file in your home directory with these lines: ErrorReportingEnable=1 TraceOutputFileEnable=1 MaxWarnings=1 SecurityDialogReportingEnable=true

this allows you to use the trace method in ActionScript to log to ~/.macromedia/Flash_Player/Logs/flashlog.txt.

Editing tips

In Eclipse associate .mxml with the XML editor, and .as with the Javascript editor, and you’ll get syntax highlighting and in ActionScript the Outline window will work.

References

8 Comments »

  1. le said,

    March 16, 2012 at 04:49

    Thank you very much for a very detailed article!

  2. josé said,

    July 1, 2010 at 10:26

    I connect to server chat.facebook.com but don´t login i try connection = new XMPPSocketConnection(); connection.username = “?????”; connection.password = “?????”; connection.server = “chat.facebook.com”; connection.port = 5222;

                connection.addEventListener(XIFFErrorEvent.XIFF_ERROR, registerNewUser);
    

    connection.addEventListener(ConnectionSuccessEvent.CONNECT_SUCCESS, connect); connection.addEventListener(LoginEvent.LOGIN, onLogin);

    connection.connect(“standard”);

    in the handler connect are value true but don´t complet the login… anyone have ideas…thanks zeze_arcos@hotmail.com

  3. tobe said,

    April 7, 2010 at 19:23

    AWESOME! Realise I’m a bit late in the day picking up this forum post, but still wanted say big thanks for the pointers. Really helped me out

  4. Marco Simão said,

    November 20, 2008 at 18:39

    Hey dude, i came up with this problem and got stucked with no straigth answer on what could preventing my flex app to do this kind of access. So THANK YOU SO MUCH! It helped a LOT. Very well writed article.

    About the reconnect, you could use a keepaliveloop to keep connected an prevent reconnect: /* create and start the keepalive */ keepAlive = new Timer(100000); keepAlive.addEventListener(TimerEvent.TIMER, onKeepAliveLoop); keepAlive.start();

    /* define handle function */ private function onKeepAliveLoop(evt:TimerEvent):void { connection.sendKeepAlive(); //Alert.show(“keepalive ” + getTimer()); }

    This works well for me.

  5. Anthony said,

    February 14, 2008 at 22:43

    Thanks for this information Graham. I was particularly interested in your auto-reconnect code. I added it to my error handler and then went into debug mode.

    Problem was that I never see (and have never seen) an error code 503 in my app, which would trigger this auto reconnect.

    My question is: what triggers a 503 error? Based on the explanation above I would imagine it to be when the internet connection is interrupted to the client. However when I unplug my machine from the network no error gets triggered or anything

    So I am trying to figure out what generates the 503, but more importantly, how to reliably auto-reconnect when things go wrong.

    [graham] It’s dispatched in XMPPConnection.as, when it gets an IOError on the underlying socket. It should fire when you unplug your network cable, unless your XMPP server is local! Maybe it sends a DisconnectionEvent instead.

  6. Graham King » Technologies for better web based applications: XMPP, Flex, and more said,

    January 27, 2008 at 21:39

    […] There is an XMPP library for Flex, called XIFF. I’ve blogged some documentation about using XIFF. […]

  7. Michael greene said,

    January 22, 2008 at 02:19

    Thanks Graham, I haven’t tested to make sure your comments helped, but that is a very well-written article on how to set it up and the specific problem I was having — the security sandbox violation — is covered right away in the “Permission to connect” section.

    And since the other posts on your front page are on Python, Flickr, Gallery2, and unit testing, you have a new subscriber.

    Side note: two + four = six, not 6.

  8. shunjie said,

    January 6, 2008 at 00:34

    Thanks, I think thats a very comprehensive list ;)

Leave a Comment

Note: Your comment will only appear on the site once I approve it manually. This can take a day or two. Thanks for taking the time to comment.