/* * Copyright (C) 2003-2007 * Nick Velloff * Derrick Grigg * Sean Voisen * Sean Treadway * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ package org.jivesoftware.xiff.core { import flash.events.DataEvent; import flash.events.Event; import flash.events.EventDispatcher; import flash.events.IOErrorEvent; import flash.events.SecurityErrorEvent; import flash.events.TimerEvent; import flash.net.XMLSocket; import flash.utils.Timer; import flash.xml.XMLDocument; import flash.xml.XMLNode; import org.jivesoftware.xiff.data.IExtension; import org.jivesoftware.xiff.data.IQ; import org.jivesoftware.xiff.data.Message; import org.jivesoftware.xiff.data.Presence; import org.jivesoftware.xiff.data.XMPPStanza; import org.jivesoftware.xiff.data.auth.AuthExtension; import org.jivesoftware.xiff.data.forms.FormExtension; import org.jivesoftware.xiff.data.register.RegisterExtension; import org.jivesoftware.xiff.events.*; import org.jivesoftware.xiff.exception.SerializationException; /** * Dispatched when a password change is successful. * * @eventType org.jivesoftware.xiff.events.ChangePasswordSuccessEvent.PASSWORD_SUCCESS */ [Event(name="changePasswordSuccess", type="org.jivesoftware.xiff.events.ChangePasswordSuccessEvent")] /** * Dispatched when the connection is successfully made to the server. * * @eventType org.jivesoftware.xiff.events.ConnectionSuccessEvent.CONNECT_SUCCESS */ [Event(name="connection", type="org.jivesoftware.xiff.events.ConnectionSuccessEvent")] /** * Dispatched when there is a disconnection from the server. * * @eventType org.jivesoftware.xiff.events.DisconnectionEvent.DISCONNECT */ [Event(name="disconnection", type="org.jivesoftware.xiff.events.DisconnectionEvent")] /** * Dispatched when there is some type of XMPP error. * * @eventType org.jivesoftware.xiff.events.XIFFErrorEvent.XIFF_ERROR */ [Event(name="error", type="org.jivesoftware.xiff.events.XIFFErrorEvent")] /** * Dispatched whenever there is incoming XML data. * * @eventType org.jivesoftware.xiff.events.IncomingDataEvent.INCOMING_DATA */ [Event(name="incomingData", type="org.jivesoftware.xiff.events.IncomingDataEvent")] /** * Dispatched on successful authentication (login) with the server. * * @eventType org.jivesoftware.xiff.events.LoginEvent.LOGIN */ [Event(name="login", type="org.jivesoftware.xiff.events.LoginEvent")] /** * Dispatched on incoming messages. * * @eventType org.jivesoftware.xiff.events.MessageEvent.MESSAGE */ [Event(name="message", type="org.jivesoftware.xiff.events.MessageEvent")] /** * Dispatched whenever data is sent to the server. * * @eventType org.jivesoftware.xiff.events.OutgoingDataEvent.OUTGOING_DATA */ [Event(name="outgoingData", type="org.jivesoftware.xiff.events.OutgoingDataEvent")] /** * Dispatched on incoming presence data. * * @eventType org.jivesoftware.xiff.events.PresenceEvent.PRESENCE */ [Event(name="presence", type="org.jivesoftware.xiff.events.PresenceEvent")] /** * Dispatched on when new user account registration is successful. * * @eventType org.jivesoftware.xiff.events.RegistrationSuccessEvent.REGISTRATION_SUCCESS */ [Event(name="registrationSuccess", type="org.jivesoftware.xiff.events.RegistrationSuccessEvent")] /** * This class is used to connect to and manage data coming from an XMPP server. Use one instance * of this class per connection. */ public class XMPPConnection extends EventDispatcher { /** * @private */ protected var _useAnonymousLogin:Boolean; /** * @private */ protected var _socket:XMLSocket; /** * @private */ protected var myServer:String; /** * @private */ protected var myUsername:String; /** * @private */ protected var myResource:String; /** * @private */ protected var myPassword:String; /** * @private */ protected var myPort:Number; /** * @private */ protected var active:Boolean; /** * @private */ protected var loggedIn:Boolean; /** * @private */ protected var ignoreWhitespace:Boolean; /** * @private */ protected var openingStreamTag:String; /** * @private */ protected var closingStreamTag:String; /** * @private */ protected var sessionID:String; /** * @private */ protected var pendingIQs:Object; /** * @private */ protected var _expireTagSearch:Boolean; public function XMPPConnection() { // Hash to hold callbacks for IQs pendingIQs = new Object(); _useAnonymousLogin = false; active = false; loggedIn = false; ignoreWhitespace = true; resource = "xiff"; port = 5222; AuthExtension.enable(); RegisterExtension.enable(); FormExtension.enable(); } /** * Connects to the server. * * @param streamType (Optional) The type of initial stream negotiation, either <flash:stream> or <stream:stream>. * Some servers, like Jabber, Inc.'s XCP and Jabberd 1.4 expect <flash:stream> from a Flash client instead of the standard <stream:stream>. * The options for this parameter are: "flash", "terminatedFlash", "standard" and "terminatedStandard". The default is "terminatedStandard". * * @return A boolean indicating whether the server was found. */ public function connect( streamType:String = "terminatedStandard" ):Boolean { // Create the socket _socket = _createXmlSocket(); active = false; loggedIn = false; // Stream type lets user set opening/closing tag - some servers (jadc2s) prefer to the standard // switch( streamType ) { case "flash": openingStreamTag = new String( "" ); closingStreamTag = new String( "" ); break; case "terminatedFlash": openingStreamTag = new String( "" ); closingStreamTag = new String( "" ); break; case "standard": openingStreamTag = new String( "" ); closingStreamTag = new String( "" ); break; case "terminatedStandard": default: openingStreamTag = new String( "" ); closingStreamTag = new String( "" ); break; } _socket.connect( server, port ); return true; } /** * Disconnects from the server if currently connected. After disconnect, * a DisconnectionEvent.DISCONNECT event is broadcast. */ public function disconnect():void { if( isActive() ) { sendXML( closingStreamTag ); _socket.close(); active = false; loggedIn = false; var event:DisconnectionEvent = new DisconnectionEvent(); dispatchEvent(event); } } /** * Sends data to the server. If the data to send cannot be serialized properly, this method throws a SerializeException. * * @param o The data to send. This must be an instance of a class that implements the ISerializable interface. * @see org.jivesoftware.xiff.data.ISerializable * @example The following example sends a basic chat message to the user with the JID "sideshowbob@springfieldpenitentiary.gov".
*
var msg:Message = new Message( "sideshowbob@springfieldpenitentiary.gov", null, "Hi Bob.", "Hi Bob.", Message.CHAT_TYPE );
		 * myXMPPConnection.send( msg );
*/ public function send( o:XMPPStanza ):void { if( isActive() ) { if( o is IQ ) { var iq:IQ = o as IQ; if ((iq.callbackName != null && iq.callbackScope != null) || iq.callback != null) { addIQCallbackToPending( iq.id, iq.callbackName, iq.callbackScope, iq.callback ); } } var root:XMLNode = o.getNode().parentNode; if (root == null) { root = new XMLDocument(); } if (o.serialize(root)) { sendXML( root.firstChild ); } else { throw new SerializationException(); } } } public function sendKeepAlive():void { if( isActive() ) { sendXML(" "); } } /** * Determines whether the connection with the server is currently active. (Not necessarily logged in. * For login status, use the isLoggedIn() method.) * * @return A boolean indicating whether the connection is active. * @see org.jivesoftware.xiff.core.XMPPConnection#isLoggedIn */ public function isActive():Boolean { return active; } /** * Determines whether the user is connected and logged into the server. * * @return A boolean indicating whether the user is logged in. * @see org.jivesoftware.xiff.core.XMPPConnection#isActive */ public function isLoggedIn():Boolean { return loggedIn; } /** * Issues a request for the information that must be submitted for registration with the server. * When the data returns, a RegistrationFieldsEvent.REG_FIELDS event is dispatched * containing the requested data. */ public function getRegistrationFields():void { var regIQ:IQ = new IQ( server, IQ.GET_TYPE, XMPPStanza.generateID("reg_info_"), "getRegistrationFields_result", this, null); regIQ.addExtension(new RegisterExtension(regIQ.getNode())); send( regIQ ); } /** * Registers a new account with the server, sending the registration data as specified in the fieldMap paramter. * * @param fieldMap An object map containing the data to use for registration. The map should be composed of * attribute:value pairs for each registration data item. * @param key (Optional) If a key was passed in the "data" field of the "registrationFields" event, * that key must also be passed here. * required field needed for registration. */ public function sendRegistrationFields( fieldMap:Object, key:String ):void { var regIQ:IQ = new IQ( server, IQ.SET_TYPE, XMPPStanza.generateID("reg_attempt_"), "sendRegistrationFields_result", this, null ); var ext:RegisterExtension = new RegisterExtension(regIQ.getNode()); for( var i:String in fieldMap ) { ext[i] = fieldMap[i]; } if (key != null) { ext.key = key; } regIQ.addExtension(ext); send( regIQ ); } /** * Changes the user's account password on the server. If the password change is successful, * the class will broadcast a ChangePasswordSuccessEvent.PASSWORD_SUCCESS event. * * @param newPassword The new password */ public function changePassword( newPassword:String ):void { var passwdIQ:IQ = new IQ( server, IQ.SET_TYPE, XMPPStanza.generateID("pswd_change_"), "changePassword_result", this, null ); var ext:RegisterExtension = new RegisterExtension(passwdIQ.getNode()); ext.username = getBareJID(); ext.password = newPassword; passwdIQ.addExtension(ext); send( passwdIQ ); } /** * Gets the fully qualified JID (user@server/resource) of the user. A fully-qualified JID includes * the resource. A bare JID does not. To get the bare JID, use the getBareJID() method. * * @return The fully qualified JID * @see #getBareJID */ public function getJID():String { return myUsername + "@" + myServer + "/" + myResource; } /** * Gets the bare JID (non-fully qualified: user@server) of the user. The bare JID does not include the resource. * * @return The bare JID * @see #getJID */ public function getBareJID():String { // NOTE: what happens with anonymous login? return getJID().split( "/" )[0]; } /** * @private */ protected function changePassword_result( resultIQ:IQ ):void { if( resultIQ.type == IQ.RESULT_TYPE ) { var event:ChangePasswordSuccessEvent = new ChangePasswordSuccessEvent(); dispatchEvent(event); } else if ( resultIQ.type == IQ.ERROR_TYPE ){ // Server send error message dispatchError(resultIQ.errorCondition,resultIQ.errorMessage,resultIQ.errorType,resultIQ.errorCode); } else { // We weren't expecting this dispatchError( "unexpected-request", "Unexpected Request", "wait", 400 ); }*/ } /** * @private */ protected function getRegistrationFields_result( resultIQ:IQ ):void { try { var ext:RegisterExtension = resultIQ.getAllExtensionsByNS(RegisterExtension.NS)[0]; var fields:Array = ext.getRequiredFieldNames(); //TODO, phase this out var event:RegistrationFieldsEvent = new RegistrationFieldsEvent(); event.fields = fields; event.data = ext; } catch (e:Error) { trace(e.getStackTrace()); } } /** * @private */ protected function sendRegistrationFields_result( resultIQ:IQ ):void { if( resultIQ.type == IQ.RESULT_TYPE ) { var event:RegistrationSuccessEvent = new RegistrationSuccessEvent(); dispatchEvent( event ); } else if ( resultIQ.type == IQ.ERROR_TYPE ){ // Server send error message dispatchError(resultIQ.errorCondition,resultIQ.errorMessage,resultIQ.errorType,resultIQ.errorCode); } else { // We weren't expecting this dispatchError( "unexpected-request", "Unexpected Request", "wait", 400 ); } */ } // Listener function from the ListenerXMLSocket /** * @private */ protected function socketConnected(ev:Event):void { active = true; sendXML( openingStreamTag ); var event:ConnectionSuccessEvent = new ConnectionSuccessEvent(); dispatchEvent( event ); } /** * @private */ protected function socketReceivedData( ev:DataEvent ):void { // parseXML is more strict in AS3 so we must check for the presence of flash:stream // the unterminated tag should be in the first string of xml data retured from the server if (!_expireTagSearch) { var pattern:RegExp = new RegExp(""); _expireTagSearch = true; } } if(ev.data == "") { socketClosed(null); return; } var xmlData:XMLDocument = new XMLDocument(); xmlData.ignoreWhite = this.ignoreWhite; xmlData.parseXML( ev.data ); var event:IncomingDataEvent = new IncomingDataEvent(); event.data = xmlData; dispatchEvent( event ); // Read the data and send it to the appropriate parser var firstNode:XMLNode = xmlData.firstChild; var nodeName:String = firstNode.nodeName.toLowerCase(); // trace("RECV: " + firstNode); switch( nodeName ) { case "stream:stream": case "flash:stream": _expireTagSearch = false; handleStream( firstNode ); break; case "stream:error": handleStreamError( firstNode ); break; case "iq": handleIQ( firstNode ); break; case "message": handleMessage( firstNode ); break; case "presence": handlePresence( firstNode ); break; case "stream:features": break; default: // silently ignore lack of or unknown stanzas // if the app designer wishes to handle raw data they // can on "incomingData". // Use case: received null byte, XMLSocket parses empty document // sends empty document // I am enabling this for debugging dispatchError( "undefined-condition", "Unknown Error", "modify", 500 ); break; } } /** * @private */ protected function socketClosed(e:Event):void { var event:DisconnectionEvent = new DisconnectionEvent(); dispatchEvent( event ); } /** * @private */ protected function handleStream( node:XMLNode ):void { sessionID = node.attributes.id; server = node.attributes.from; if(_useAnonymousLogin) { // Begin anonymous login sendAnonymousLogin(); } else if( username != null && username.length > 0 ) { // Begin login sequence beginAuthentication(); } else { //get registration fields getRegistrationFields(); } } /** * @private */ protected function handleStreamError( node:XMLNode ):void { dispatchError( "service-unavailable", "Remote Server Error", "cancel", 502 ); // Cancel everything by closing connection try { _socket.close(); } catch (error:Error){ } active = false; loggedIn = false; var event:DisconnectionEvent = new DisconnectionEvent(); dispatchEvent( event ); } /** * @private */ protected function handleIQ( node:XMLNode ):IQ { var iq:IQ = new IQ(); // Populate the IQ with the incoming data if( !iq.deserialize( node ) ) { throw new SerializationException(); } // If it's an error, handle it if( iq.type == IQ.ERROR_TYPE && !pendingIQs[iq.id]) { dispatchError( iq.errorCondition, iq.errorMessage, iq.errorType, iq.errorCode ); } else { // Start the callback for this IQ if one exists if( pendingIQs[iq.id] !== undefined ) { var callbackInfo:* = pendingIQs[iq.id]; if(callbackInfo.methodScope && callbackInfo.methodName) { callbackInfo.methodScope[callbackInfo.methodName].apply( callbackInfo.methodScope, [iq] ); } if (callbackInfo.func != null) { callbackInfo.func( iq ); } pendingIQs[iq.id] = null; delete pendingIQs[iq.id]; } else { var exts:Array = iq.getAllExtensions(); for (var ns:String in exts) { // Static type casting var ext:IExtension = exts[ns] as IExtension; if (ext != null) { var event:IQEvent = new IQEvent(ext.getNS()); event.data = ext; event.iq = iq; dispatchEvent( event ); } } } } return iq; } /** * @private */ protected function handleMessage( node:XMLNode ):Message { var msg:Message = new Message(); //trace(msg); // Populate with data if( !msg.deserialize( node ) ) { throw new SerializationException(); } // ADDED in error handling for messages if( msg.type == Message.ERROR_TYPE ) { dispatchError( msg.errorCondition, msg.errorMessage, msg.errorType, msg.errorCode ); } else { var event:MessageEvent = new MessageEvent(); event.data = msg; dispatchEvent( event ); } return msg; } /** * @private */ private var presenceQueue:Array = []; private var presenceQueueTimer:Timer; protected function handlePresence( node:XMLNode ):Presence { if(!presenceQueueTimer) { presenceQueueTimer = new Timer(1, 1); presenceQueueTimer.addEventListener(TimerEvent.TIMER_COMPLETE, flushPresenceQueue); } var pres:Presence = new Presence(); // Populate if( !pres.deserialize( node ) ) { throw new SerializationException(); } presenceQueue.push(pres); presenceQueueTimer.reset(); presenceQueueTimer.start(); return pres; } protected function flushPresenceQueue(evt:TimerEvent):void { var event:PresenceEvent = new PresenceEvent(); event.data = presenceQueue; dispatchEvent( event ); presenceQueue = []; } /** * @private */ protected function onIOError(event:IOErrorEvent):void{ /* this fires the standard dispatchError method. need to add the appropriate error code */ dispatchError( "service-unavailable", "Service Unavailable", "cancel", 503 ); } /** * @private */ protected function securityError(event:SecurityErrorEvent):void{ trace("there was a security error of type: " + event.type + "\nError: " + event.text); dispatchError( "not-authorized", "Not Authorized", "auth", 401 ); } /** * @private */ protected function dispatchError( condition:String, message:String, type:String, code:Number ):void { var event:XIFFErrorEvent = new XIFFErrorEvent(); event.errorCondition = condition; event.errorMessage = message; event.errorType = type; event.errorCode = code; dispatchEvent( event ); } /** * @private */ protected function sendXML( someData:* ):void { //trace("SEND: " + someData); // Data is untyped because it could be a string or XML _socket.send( someData ); var event:OutgoingDataEvent = new OutgoingDataEvent(); event.data = someData; dispatchEvent( event ); } // anonymous login /** * @private */ protected function sendAnonymousLogin():void { var anonIQ:IQ = new IQ(null, IQ.SET_TYPE, XMPPStanza.generateID("log_anom_"), "sendAnonymousLogin_result", this, null ); var authExt:AuthExtension = new AuthExtension(anonIQ.getNode()); anonIQ.addExtension(authExt); send(anonIQ); } /** * @private */ protected function sendAnonymousLogin_result(resultIQ:IQ):void {resultIQ if( resultIQ.type == IQ.RESULT_TYPE ) { // update resource var jid:JID = new JID(resultIQ.to); resource = jid.resource; username = jid.node; // dispatch login event loggedIn = true; var event:LoginEvent = new LoginEvent(); dispatchEvent( event ); } } /** * @private */ protected function beginAuthentication():void { var authIQ:IQ = new IQ( null, IQ.GET_TYPE, XMPPStanza.generateID("log_user_"), "beginAuthentication_result", this, null ); var authExt:AuthExtension = new AuthExtension(authIQ.getNode()); authExt.username = username authIQ.addExtension(authExt); send( authIQ ); } /** * @private */ protected function beginAuthentication_result( resultIQ:IQ ):void { var connectionType:String = "none"; // Begin authentication procedure if( resultIQ.type == IQ.RESULT_TYPE ) { var authIQ:IQ = new IQ( null, IQ.SET_TYPE, XMPPStanza.generateID("log_user2_"), "sendAuthentication_result", this, null ); try { var resultAuth:* = resultIQ.getAllExtensionsByNS(AuthExtension.NS)[0]; var responseAuth:AuthExtension = new AuthExtension(authIQ.getNode()); responseAuth.password = password; responseAuth.username = username; responseAuth.resource = resource; authIQ.addExtension(responseAuth); send( authIQ ); } catch (e:Error) { trace("Error : null trapped. Resuming."); } } else if ( resultIQ.type == IQ.ERROR_TYPE ){ // Server send error message dispatchError(resultIQ.errorCondition,resultIQ.errorMessage,resultIQ.errorType,resultIQ.errorCode); } else { // We weren't expecting this dispatchError( "unexpected-request", "Unexpected Request", "wait", 400 ); } } /** * @private */ protected function sendAuthentication_result( resultIQ:IQ ):void { if( resultIQ.type == IQ.RESULT_TYPE ) { loggedIn = true; var event:LoginEvent = new LoginEvent(); dispatchEvent( event ); } else if ( resultIQ.type == IQ.ERROR_TYPE ){ // We weren't expecting this dispatchError(resultIQ.errorCondition,resultIQ.errorMessage,resultIQ.errorType,resultIQ.errorCode); } else { // We weren't expecting this dispatchError( "unexpected-request", "Unexpected Request", "wait", 400 ); } } /** * @private */ protected function addIQCallbackToPending( id:String, callbackName:String, callbackScope:Object, callbackFunc:Function ):void { pendingIQs[id] = {methodName:callbackName, methodScope:callbackScope, func:callbackFunc}; } /** * The XMPP server to use for connection. */ public function get server():String { return myServer; } /** * @private */ public function set server( theServer:String ):void { myServer = theServer; } /** * The username to use for connection. If this property is null when connect() is called, * the class will fetch registration field data rather than attempt to login. */ public function get username():String { return myUsername; } /** * @private */ public function set username( theUsername:String ):void { myUsername = theUsername; } /** * The password to use when logging in. */ public function get password():String { return myPassword; } public function set password( thePassword:String ):void { myPassword = thePassword; } /** * The resource to use when logging in. A resource is required (defaults to "XIFF") and * allows a user to login using the same account simultaneously (most likely from multiple machines). * Typical examples of the resource include "Home" or "Office" to indicate the user's current location. */ public function get resource():String { return myResource; } /** * @private */ public function set resource( theResource:String ):void { if( theResource.length > 0 ) { myResource = theResource; } } /** * Whether to use anonymous login or not. */ public function get useAnonymousLogin():Boolean { return _useAnonymousLogin; } /** * @private */ public function set useAnonymousLogin(value:Boolean):void { // set only if not connected if(!isActive()) _useAnonymousLogin = value; } /** * The port to use when connecting. The default XMPP port is 5222. */ public function get port():Number { return myPort; } public function set port( portNum:Number ):void { myPort = portNum; } /** * Determines whether whitespace will be ignored on incoming XML data. * Behaves the same as XML.ignoreWhite */ public function get ignoreWhite():Boolean { return ignoreWhitespace; } public function set ignoreWhite( val:Boolean ):void { ignoreWhitespace = val; } private function _createXmlSocket():XMLSocket { var socket:XMLSocket = new XMLSocket(server, port); socket.addEventListener(Event.CONNECT,socketConnected); socket.addEventListener(IOErrorEvent.IO_ERROR,onIOError); socket.addEventListener(Event.CLOSE,socketClosed); socket.addEventListener(DataEvent.DATA,socketReceivedData); socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR,securityError); return socket; } } }