/*
 * License
 */
package org.igniterealtime.xiff.core
{
	import flash.events.TimerEvent;
	import flash.utils.ByteArray;
	import flash.utils.Timer;
	import flash.xml.XMLDocument;
	import flash.xml.XMLNode;

	import mx.logging.ILogger;
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import mx.rpc.http.HTTPService;

	import org.igniterealtime.xiff.events.*;
	import org.igniterealtime.xiff.logging.LoggerFactory;
	import org.igniterealtime.xiff.util.Callback;

	/**
	 * Bidirectional-streams Over Synchronous HTTP (BOSH)
	 * @see http://xmpp.org/extensions/xep-0206.html
	 */
	public class XMPPBOSHConnection extends XMPPConnection
	{
		private static const BOSH_VERSION:String = "1.6";

		private static const HTTPS_PORT:int = 7443;

		private static const HTTP_PORT:int = 7070;

		private static const headers:Object = {
			"post": [],
			"get": [ 'Cache-Control',
					 'no-store',
					 'Cache-Control',
					 'no-cache',
					 'Pragma', 'no-cache' ]
		};

		private static const logger:ILogger = LoggerFactory.getLogger( "org.igniterealtime.xiff.core.XMPPBOSHConnection" );

		private var _boshPath:String = "http-bind/";

		private var _hold:uint = 1;

		private var _maxConcurrentRequests:uint = 2;

		private var _port:Number;

		private var _secure:Boolean;

		private var _wait:uint = 20;

		private var boshPollingInterval:uint = 10000;

		private var inactivity:uint;

		private var isDisconnecting:Boolean = false;

		private var lastPollTime:Date = null;

		private var maxPause:uint;

		private var pauseEnabled:Boolean = false;

		private var pauseTimer:Timer;

		private var pollingEnabled:Boolean = false;

		private var requestCount:int = 0;

		private var requestQueue:Array = [];

		private var responseQueue:Array = [];

		private const responseTimer:Timer = new Timer( 0.0, 1 );

		private var rid:Number;

		private var sid:String;

		private var streamRestarted:Boolean;

		/**
		 *
		 * @param	secure
		 */
		public function XMPPBOSHConnection( secure:Boolean = false ):void
		{
			super();
			this.secure = secure;
		}

		override public function connect( streamType:String = null ):Boolean
		{
			logger.debug( "BOSH connect()" );

			var attrs:Object = {
				"xml:lang": "en",
				"xmlns": "http://jabber.org/protocol/httpbind",
				"xmlns:xmpp": "urn:xmpp:xbosh",
				"xmpp:version": "1.0",
				"hold": hold,
				"rid": nextRID,
				"secure": secure,
				"wait": wait,
				"ver": BOSH_VERSION,
				"to": domain
			};

			var result:XMLNode = new XMLNode( 1, "body" );
			result.attributes = attrs;
			sendRequests( result );

			return true;
		}

		override public function disconnect():void
		{
			if ( active )
			{
				var data:XMLNode = createRequest();
				data.attributes.type = "terminate";
				sendRequests( data );
				active = false;
				loggedIn = false;
				dispatchEvent( new DisconnectionEvent());
			}
		}

		/**
		 * @return	true if pause request is sent
		 */
		public function pauseSession( seconds:uint ):Boolean
		{
			logger.debug( "Pausing session for {0} seconds", seconds );

			var pauseDuration:uint = seconds * 1000;
			if ( !pauseEnabled || pauseDuration > maxPause || pauseDuration <= boshPollingInterval )
				return false;

			pollingEnabled = false;

			var data:XMLNode = createRequest();
			data.attributes[ "pause" ] = seconds;
			sendRequests( data );

			pauseTimer = new Timer( pauseDuration - 2000, 1 );
			pauseTimer.addEventListener( TimerEvent.TIMER, handlePauseTimeout );
			pauseTimer.start();

			return true;
		}

		public function processConnectionResponse( responseBody:XMLNode ):void
		{
			dispatchEvent( new ConnectionSuccessEvent());

			var attributes:Object = responseBody.attributes;

			sid = attributes.sid;
			wait = attributes.wait;

			if ( attributes.polling )
			{
				boshPollingInterval = attributes.polling * 1000;
			}
			if ( attributes.inactivity )
			{
				inactivity = attributes.inactivity * 1000;
			}
			if ( attributes.maxpause )
			{
				maxPause = attributes.maxpause * 1000;
				pauseEnabled = true;
			}
			if ( attributes.requests )
			{
				maxConcurrentRequests = attributes.requests;
			}

			logger.debug( "Polling interval: {0}", boshPollingInterval );
			logger.debug( "Inactivity timeout: {0}", inactivity );
			logger.debug( "Max requests: {0}", maxConcurrentRequests );
			logger.debug( "Max pause: {0}", maxPause );

			active = true;

			addEventListener( LoginEvent.LOGIN, handleLogin );
			responseTimer.addEventListener( TimerEvent.TIMER_COMPLETE, processResponse );
		}

		//do nothing, we use polling instead
		override public function sendKeepAlive():void
		{
		}

		override protected function restartStream():void
		{
			var data:XMLNode = createRequest();
			data.attributes[ "xmpp:restart" ] = "true";
			data.attributes[ "xmlns:xmpp" ] = "urn:xmpp:xbosh";
			data.attributes[ "xml:lang" ] = "en";
			data.attributes[ "to" ] = domain;
			sendRequests( data );
			streamRestarted = true;
		}

		override protected function sendXML( body:* ):void
		{
			sendQueuedRequests( body );
		}

		private function createRequest( bodyContent:Array = null ):XMLNode
		{
			var attrs:Object = {
				"xmlns": "http://jabber.org/protocol/httpbind",
				"rid": nextRID,
				"sid": sid
			};
			var req:XMLNode = new XMLNode( 1, "body" );
			if ( bodyContent )
			{
				for each ( var content:XMLNode in bodyContent )
				{
					req.appendChild( content );
				}
			}

			req.attributes = attrs;

			return req;
		}

		private function handleLogin( e:LoginEvent ):void
		{
			pollingEnabled = true;
			pollServer();
		}

		private function handlePauseTimeout( event:TimerEvent ):void
		{
			logger.debug( "handlePauseTimeout" );
			pollingEnabled = true;
			pollServer();
		}

		private function httpError( req:XMLNode, isPollResponse:Boolean, event:FaultEvent ):void
		{
			disconnect();
			dispatchError( "Unknown HTTP Error", event.fault.rootCause.text, "",
						   -1 );
		}

		private function httpResponse( req:XMLNode, isPollResponse:Boolean, event:ResultEvent ):void
		{
			requestCount--;
			var rawXML:String = event.result as String;

			logger.info( "INCOMING {0}", rawXML );

			var xmlData:XMLDocument = new XMLDocument();
			xmlData.ignoreWhite = this.ignoreWhite;
			xmlData.parseXML( rawXML );
			var bodyNode:XMLNode = xmlData.firstChild;

			var byteData:ByteArray = new ByteArray();
			byteData.writeUTFBytes(xmlData.toString());

			var incomingEvent:IncomingDataEvent = new IncomingDataEvent();
			incomingEvent.data = byteData;
			dispatchEvent( incomingEvent );

			if ( streamRestarted && !bodyNode.hasChildNodes())
			{
				streamRestarted = false;
				bindConnection();
			}

			if ( bodyNode.attributes[ "type" ] == "terminate" )
			{
				dispatchError( "BOSH Error", bodyNode.attributes[ "condition" ],
							   "", -1 );
				active = false;
			}

			if ( bodyNode.attributes[ "sid" ] && !loggedIn )
			{
				processConnectionResponse( bodyNode );

				var featuresFound:Boolean = false;
				for each ( var child:XMLNode in bodyNode.childNodes )
				{
					if ( child.nodeName == "stream:features" )
						featuresFound = true;
				}
				if ( !featuresFound )
				{
					pollingEnabled = true;
					pollServer();
				}
			}

			for each ( var childNode:XMLNode in bodyNode.childNodes )
			{
				responseQueue.push( childNode );
			}

			resetResponseProcessor();

			//if we have no outstanding requests, then we're free to send a poll at the next opportunity
			if ( requestCount == 0 && !sendQueuedRequests())
				pollServer();
		}

		private function pollServer():void
		{
			//We shouldn't poll if the connection is dead, if we had requests to send instead, or if there's already one in progress
			if ( !isActive() || !pollingEnabled || sendQueuedRequests() || requestCount >
				0 )
				return;

			//this should be safe since sendRequests checks to be sure it's not over the concurrent requests limit, and we just ensured that the queue is empty by calling sendQueuedRequests()
			sendRequests( null, true );
		}

		private function processResponse( event:TimerEvent = null ):void
		{
			// Read the data and send it to the appropriate parser
			var currentNode:XMLNode = responseQueue.shift();
			var nodeName:String = currentNode.nodeName.toLowerCase();

			switch ( nodeName )
			{
				case "stream:features":
					handleStreamFeatures( currentNode );
					streamRestarted = false; //avoid triggering the old server workaround
					break;

				case "stream:error":
					handleStreamError( currentNode );
					break;

				case "iq":
					handleIQ( currentNode );
					break;

				case "message":
					handleMessage( currentNode );
					break;

				case "presence":
					handlePresence( currentNode );
					break;

				case "success":
					handleAuthentication( currentNode );
					break;

				case "failure":
					handleAuthentication( currentNode );
					break;
				default:
					dispatchError( "undefined-condition", "Unknown Error", "modify",
								   500 );
					break;
			}

			resetResponseProcessor();
		}

		private function resetResponseProcessor():void
		{
			if ( responseQueue.length > 0 )
			{
				responseTimer.reset();
				responseTimer.start();
			}
		}

		private function sendQueuedRequests( body:* = null ):Boolean
		{
			if ( body )
			{
				requestQueue.push( body );
			}
			else if ( requestQueue.length == 0 )
			{
				return false;
			}

			return sendRequests();
		}

		//returns true if any requests were sent
		private function sendRequests( data:XMLNode = null, isPoll:Boolean = false ):Boolean
		{
			if ( requestCount >= maxConcurrentRequests )
			{
				return false;
			}

			requestCount++;

			if ( !data )
			{
				if ( isPoll )
				{
					data = createRequest();
				}
				else
				{
					var temp:Array = [];
					for ( var i:uint = 0; i < 10 && requestQueue.length > 0; ++i )
					{
						temp.push( requestQueue.shift());
					}
					data = createRequest( temp );
				}
			}

			// TODO: Could this be replaced with URLLoader ?
			//build the http request
			var request:HTTPService = new HTTPService();
			request.method = "post";
			request.headers = headers[ request.method ];
			request.url = httpServer;
			request.resultFormat = HTTPService.RESULT_FORMAT_TEXT;
			request.contentType = "text/xml";

			var responseCallback:Callback = new Callback( this, httpResponse, data,
														  isPoll );
			var errorCallback:Callback = new Callback( this, httpError, data, isPoll );

			request.addEventListener( ResultEvent.RESULT, responseCallback.call,
									  false );
			request.addEventListener( FaultEvent.FAULT, errorCallback.call, false );

			request.send( data );

			var byteData:ByteArray = new ByteArray();
			byteData.writeUTFBytes(data.toString());
			var event:OutgoingDataEvent = new OutgoingDataEvent();
			event.data = byteData;
			dispatchEvent( event );

			if ( isPoll )
			{
				lastPollTime = new Date();
				logger.info( "Polling" );
			}

			logger.info( "OUTGOING {0}", data );

			return true;
		}

		public function get boshPath():String
		{
			return _boshPath;
		}
		public function set boshPath( value:String ):void
		{
			_boshPath = value;
		}

		private function get nextRID():Number
		{
			if ( !rid )
				rid = Math.floor( Math.random() * 1000000 );
			return ++rid;
		}

		public function get wait():uint
		{
			return _wait;
		}
		public function set wait( value:uint ):void
		{
			_wait = value;
		}

		public function get secure():Boolean
		{
			return _secure;
		}
		public function set secure( flag:Boolean ):void
		{
			logger.debug( "set secure: {0}", flag );
			_secure = flag;
			port = _secure ? HTTPS_PORT : HTTP_PORT;
		}

		override public function get port():Number
		{
			return _port;
		}
		override public function set port( portnum:Number ):void
		{
			logger.debug( "set port: {0}", portnum );
			_port = portnum;
		}

		public function get hold():uint
		{
			return _hold;
		}
		public function set hold( value:uint ):void
		{
			_hold = value;
		}

		public function get httpServer():String
		{
			return ( secure ? "https" : "http" ) + "://" + server + ":" + port +
				"/" + boshPath;
		}

		public function get maxConcurrentRequests():uint
		{
			return _maxConcurrentRequests;
		}
		public function set maxConcurrentRequests( value:uint ):void
		{
			_maxConcurrentRequests = value;
		}
	}
}

/*
 * License
 */
package org.igniterealtime.xiff.core
{
	import flash.events.*;
	import flash.net.*;
	import flash.utils.ByteArray;
	import flash.utils.Timer;
	import flash.xml.XMLDocument;
	import flash.xml.XMLNode;

	import mx.logging.ILogger;
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import mx.rpc.http.HTTPService;

	import org.igniterealtime.xiff.events.*;
	import org.igniterealtime.xiff.logging.LoggerFactory;
	import org.igniterealtime.xiff.util.Callback;

	/**
	 * Bidirectional-streams Over Synchronous HTTP (BOSH)
	 * @see http://xmpp.org/extensions/xep-0206.html
	 */
	public class XMPPBOSHConnection extends XMPPConnection
	{
		private static const BOSH_VERSION:String = "1.6";

		private static const HTTPS_PORT:int = 7443;

		private static const HTTP_PORT:int = 7070;

		/**
		 * Keys should match URLRequestMethod constants.
		 */
		private static const headers:Object = {
			"post": [],
			"get": [ 'Cache-Control',
					 'no-store',
					 'Cache-Control',
					 'no-cache',
					 'Pragma', 'no-cache' ]
		};

		private static const logger:ILogger = LoggerFactory.getLogger( "org.igniterealtime.xiff.core.XMPPBOSHConnection" );

		private var _boshPath:String = "http-bind/";

		private var _hold:uint = 1;

		private var _maxConcurrentRequests:uint = 2;

		private var _port:Number;

		private var _secure:Boolean;

		private var _wait:uint = 20;

		private var boshPollingInterval:uint = 10000;

		private var inactivity:uint;

		private var isDisconnecting:Boolean = false;

		private var lastPollTime:Date = null;

		private var maxPause:uint;

		private var pauseEnabled:Boolean = false;

		private var pauseTimer:Timer;

		private var pollingEnabled:Boolean = false;

		private var requestCount:int = 0;

		private var requestQueue:Array = [];

		private var responseQueue:Array = [];

		private var responseTimer:Timer;

		private var rid:Number;

		private var sid:String;

		private var streamRestarted:Boolean;

		/**
		 *
		 * @param	secure
		 */
		public function XMPPBOSHConnection( secure:Boolean = false ):void
		{
			super();
			this.secure = secure;
			responseTimer = new Timer( 0.0, 1 );
		}

		override public function connect( streamType:String = null ):Boolean
		{
			logger.debug( "BOSH connect()" );

			var attrs:Object = {
				"xml:lang": "en",
				"xmlns": "http://jabber.org/protocol/httpbind",
				"xmlns:xmpp": "urn:xmpp:xbosh",
				"xmpp:version": "1.0",
				"hold": hold,
				"rid": nextRID,
				"secure": secure,
				"wait": wait,
				"ver": BOSH_VERSION,
				"to": domain
			};

			var result:XMLNode = new XMLNode( 1, "body" );
			result.attributes = attrs;
			sendRequests( result );

			return true;
		}

		override public function disconnect():void
		{
			if ( active )
			{
				var data:XMLNode = createRequest();
				data.attributes.type = "terminate";
				sendRequests( data );
				active = false;
				loggedIn = false;
				dispatchEvent( new DisconnectionEvent());
			}
		}

		/**
		 * @return	true if pause request is sent
		 */
		public function pauseSession( seconds:uint ):Boolean
		{
			logger.debug( "Pausing session for {0} seconds", seconds );

			var pauseDuration:uint = seconds * 1000;
			if ( !pauseEnabled || pauseDuration > maxPause || pauseDuration <= boshPollingInterval )
				return false;

			pollingEnabled = false;

			var data:XMLNode = createRequest();
			data.attributes[ "pause" ] = seconds;
			sendRequests( data );

			pauseTimer = new Timer( pauseDuration - 2000, 1 );
			pauseTimer.addEventListener( TimerEvent.TIMER, handlePauseTimeout );
			pauseTimer.start();

			return true;
		}

		public function processConnectionResponse( responseBody:XMLNode ):void
		{
			dispatchEvent( new ConnectionSuccessEvent());

			var attributes:Object = responseBody.attributes;

			sid = attributes.sid;
			wait = attributes.wait;

			if ( attributes.polling )
			{
				boshPollingInterval = attributes.polling * 1000;
			}
			if ( attributes.inactivity )
			{
				inactivity = attributes.inactivity * 1000;
			}
			if ( attributes.maxpause )
			{
				maxPause = attributes.maxpause * 1000;
				pauseEnabled = true;
			}
			if ( attributes.requests )
			{
				maxConcurrentRequests = attributes.requests;
			}

			logger.debug( "Polling interval: {0}", boshPollingInterval );
			logger.debug( "Inactivity timeout: {0}", inactivity );
			logger.debug( "Max requests: {0}", maxConcurrentRequests );
			logger.debug( "Max pause: {0}", maxPause );

			active = true;

			addEventListener( LoginEvent.LOGIN, handleLogin );
			responseTimer.addEventListener( TimerEvent.TIMER_COMPLETE, processResponse );
		}

		//do nothing, we use polling instead
		override public function sendKeepAlive():void
		{
		}

		override protected function restartStream():void
		{
			var data:XMLNode = createRequest();
			data.attributes[ "xmpp:restart" ] = "true";
			data.attributes[ "xmlns:xmpp" ] = "urn:xmpp:xbosh";
			data.attributes[ "xml:lang" ] = "en";
			data.attributes[ "to" ] = domain;
			sendRequests( data );
			streamRestarted = true;
		}

		override protected function sendXML( body:* ):void
		{
			sendQueuedRequests( body );
		}

		private function createRequest( bodyContent:Array = null ):XMLNode
		{
			var attrs:Object = {
				"xmlns": "http://jabber.org/protocol/httpbind",
				"rid": nextRID,
				"sid": sid
			};
			var req:XMLNode = new XMLNode( 1, "body" );
			if ( bodyContent )
			{
				for each ( var content:XMLNode in bodyContent )
				{
					req.appendChild( content );
				}
			}

			req.attributes = attrs;

			return req;
		}

		private function handleLogin( e:LoginEvent ):void
		{
			pollingEnabled = true;
			pollServer();
		}

		private function handlePauseTimeout( event:TimerEvent ):void
		{
			logger.debug( "handlePauseTimeout" );
			pollingEnabled = true;
			pollServer();
		}

		private function httpError( req:XMLNode, isPollResponse:Boolean, event:FaultEvent ):void
		{
			disconnect();
			dispatchError( "Unknown HTTP Error", event.fault.rootCause.text, "",
						   -1 );
		}

		private function httpResponse( req:XMLNode, isPollResponse:Boolean, event:ResultEvent ):void
		{
			requestCount--;
			var rawXML:String = event.result as String;

			logger.info( "INCOMING {0}", rawXML );

			var xmlData:XMLDocument = new XMLDocument();
			xmlData.ignoreWhite = this.ignoreWhite;
			xmlData.parseXML( rawXML );
			var bodyNode:XMLNode = xmlData.firstChild;

			var byteData:ByteArray = new ByteArray();
			byteData.writeUTFBytes(xmlData.toString());

			var incomingEvent:IncomingDataEvent = new IncomingDataEvent();
			incomingEvent.data = byteData;
			dispatchEvent( incomingEvent );

			if ( streamRestarted && !bodyNode.hasChildNodes())
			{
				streamRestarted = false;
				bindConnection();
			}

			if ( bodyNode.attributes[ "type" ] == "terminate" )
			{
				dispatchError( "BOSH Error", bodyNode.attributes[ "condition" ],
							   "", -1 );
				active = false;
			}

			if ( bodyNode.attributes[ "sid" ] && !loggedIn )
			{
				processConnectionResponse( bodyNode );

				var featuresFound:Boolean = false;
				for each ( var child:XMLNode in bodyNode.childNodes )
				{
					if ( child.nodeName == "stream:features" )
						featuresFound = true;
				}
				if ( !featuresFound )
				{
					pollingEnabled = true;
					pollServer();
				}
			}

			for each ( var childNode:XMLNode in bodyNode.childNodes )
			{
				responseQueue.push( childNode );
			}

			resetResponseProcessor();

			//if we have no outstanding requests, then we're free to send a poll at the next opportunity
			if ( requestCount == 0 && !sendQueuedRequests())
				pollServer();
		}

		private function pollServer():void
		{
			//We shouldn't poll if the connection is dead, if we had requests to send instead, or if there's already one in progress
			if ( !isActive() || !pollingEnabled || sendQueuedRequests() || requestCount >
				0 )
				return;

			//this should be safe since sendRequests checks to be sure it's not over the concurrent requests limit, and we just ensured that the queue is empty by calling sendQueuedRequests()
			sendRequests( null, true );
		}

		private function processResponse( event:TimerEvent = null ):void
		{
			// Read the data and send it to the appropriate parser
			var currentNode:XMLNode = responseQueue.shift();
			var nodeName:String = currentNode.nodeName.toLowerCase();

			switch ( nodeName )
			{
				case "stream:features":
					handleStreamFeatures( currentNode );
					streamRestarted = false; //avoid triggering the old server workaround
					break;

				case "stream:error":
					handleStreamError( currentNode );
					break;

				case "iq":
					handleIQ( currentNode );
					break;

				case "message":
					handleMessage( currentNode );
					break;

				case "presence":
					handlePresence( currentNode );
					break;

				case "success":
					handleAuthentication( currentNode );
					break;

				case "failure":
					handleAuthentication( currentNode );
					break;
				default:
					dispatchError( "undefined-condition", "Unknown Error", "modify",
								   500 );
					break;
			}

			resetResponseProcessor();
		}

		private function resetResponseProcessor():void
		{
			if ( responseQueue.length > 0 )
			{
				responseTimer.reset();
				responseTimer.start();
			}
		}

		private function sendQueuedRequests( body:* = null ):Boolean
		{
			if ( body )
			{
				requestQueue.push( body );
			}
			else if ( requestQueue.length == 0 )
			{
				return false;
			}

			return sendRequests();
		}

		//returns true if any requests were sent
		private function sendRequests( data:XMLNode = null, isPoll:Boolean = false ):Boolean
		{
			if ( requestCount >= maxConcurrentRequests )
			{
				return false;
			}

			requestCount++;

			if ( !data )
			{
				if ( isPoll )
				{
					data = createRequest();
				}
				else
				{
					var temp:Array = [];
					for ( var i:uint = 0; i < 10 && requestQueue.length > 0; ++i )
					{
						temp.push( requestQueue.shift());
					}
					data = createRequest( temp );
				}
			}
			/*
			var req:URLRequest = new URLRequest(httpServer);
			req.method = URLRequestMethod.POST;
			req.contentType = "text/xml";
			req.requestHeaders = headers[ req.method ];
			req.data = data;
			var loader:URLLoader = new URLLoader();
			loader.dataFormat = URLLoaderDataFormat.TEXT;
			loader.addEventListener(Event.COMPLETE, onRequestComplete);
			loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, socketSecurityError);
			loader.addEventListener(IOErrorEvent.IO_ERROR, socketIOError);
			loader.load(req);
			*/

			// TODO: Could this be replaced with URLLoader ?
			//build the http request
			var request:HTTPService = new HTTPService();
			request.method = "post";
			request.headers = headers[ request.method ];
			request.url = httpServer;
			request.resultFormat = HTTPService.RESULT_FORMAT_TEXT;
			request.contentType = "text/xml";

			var responseCallback:Callback = new Callback( this, httpResponse, data,
														  isPoll );
			var errorCallback:Callback = new Callback( this, httpError, data, isPoll );

			request.addEventListener( ResultEvent.RESULT, responseCallback.call,
									  false );
			request.addEventListener( FaultEvent.FAULT, errorCallback.call, false );

			request.send( data );

			var byteData:ByteArray = new ByteArray();
			byteData.writeUTFBytes(data.toString());
			var event:OutgoingDataEvent = new OutgoingDataEvent();
			event.data = byteData;
			dispatchEvent( event );

			if ( isPoll )
			{
				lastPollTime = new Date();
				logger.info( "Polling" );
			}

			logger.info( "OUTGOING {0}", data );

			return true;
		}

		public function get boshPath():String
		{
			return _boshPath;
		}
		public function set boshPath( value:String ):void
		{
			_boshPath = value;
		}

		private function get nextRID():Number
		{
			if ( !rid )
				rid = Math.floor( Math.random() * 1000000 );
			return ++rid;
		}

		public function get wait():uint
		{
			return _wait;
		}
		public function set wait( value:uint ):void
		{
			_wait = value;
		}

		public function get secure():Boolean
		{
			return _secure;
		}
		public function set secure( flag:Boolean ):void
		{
			logger.debug( "set secure: {0}", flag );
			_secure = flag;
			port = _secure ? HTTPS_PORT : HTTP_PORT;
		}

		override public function get port():Number
		{
			return _port;
		}
		override public function set port( portnum:Number ):void
		{
			logger.debug( "set port: {0}", portnum );
			_port = portnum;
		}

		public function get hold():uint
		{
			return _hold;
		}
		public function set hold( value:uint ):void
		{
			_hold = value;
		}

		public function get httpServer():String
		{
			return ( secure ? "https" : "http" ) + "://" + server + ":" + port +
				"/" + boshPath;
		}

		public function get maxConcurrentRequests():uint
		{
			return _maxConcurrentRequests;
		}
		public function set maxConcurrentRequests( value:uint ):void
		{
			_maxConcurrentRequests = value;
		}
	}
}

Rewrite work in progress...

Rewrite work in progress...


//
// Flump - Copyright 2013 Flump Authors

package flump.xfl {

import com.adobe.crypto.MD5;
import com.threerings.util.Log;
import com.threerings.util.Map;
import com.threerings.util.Maps;
import com.threerings.util.Set;
import com.threerings.util.Sets;
import com.threerings.util.XmlUtil;

import flash.filesystem.File;
import flash.utils.ByteArray;
import flash.utils.Dictionary;

import flump.SwfTexture;
import flump.Util;
import flump.executor.Executor;
import flump.executor.Future;
import flump.executor.FutureTask;
import flump.executor.load.LoadedSwf;
import flump.executor.load.SwfLoader;
import flump.export.Atlas;
import flump.export.ExportConf;
import flump.export.Files;
import flump.mold.KeyframeMold;
import flump.mold.LayerMold;
import flump.mold.LibraryMold;
import flump.mold.MovieMold;
import flump.mold.TextureGroupMold;

public class XflLibrary
{
    use namespace xflns;

    /**
     * When an exported movie contains an unexported movie, it gets assigned a generated symbol
     * name with this prefix.
     */
    public static const IMPLICIT_PREFIX :String = "~";

    public var swf :LoadedSwf;

    public var frameRate :Number;
    public var backgroundColor :int;

    // The MD5 of the published library SWF
    public var md5 :String;

    public var location :String;

    public const movies :Vector.<MovieMold> = new <MovieMold>[];
    public const textures :Vector.<XflTexture> = new <XflTexture>[];

    public function XflLibrary (location :String) {
        this.location = location;
    }

    public function getItem (id :String, requiredType :Class=null) :* {
        const result :* = _idToItem[id];
        if (result === undefined) throw new Error("Unknown library item '" + id + "'");
        else if (requiredType != null) return requiredType(result);
        else return result;
    }

    public function isExported (movie :MovieMold) :Boolean {
        return _moldToSymbol.containsKey(movie);
    }

    public function get publishedMovies () :Vector.<MovieMold> {
        const result :Vector.<MovieMold> = new <MovieMold>[];
        for each (var movie :MovieMold in _toPublish.toArray().sortOn("id")) result.push(movie);
        return result;
    }

    public function finishLoading () :void {
        var movie :MovieMold = null;

        // Parse all un-exported movies that are referenced by the exported movies.
        for (var ii :int = 0; ii < movies.length; ++ii) {
            movie = movies[ii];
            for each (var symbolName :String in XflMovie.getSymbolNames(movie).toArray()) {
                var xml :XML = _unexportedMovies.remove(symbolName);
                if (xml != null) parseMovie(xml);
            }
        }

        for each (movie in movies) if (isExported(movie)) prepareForPublishing(movie);
    }

    protected function prepareForPublishing (movie :MovieMold) :void {
        if (!_toPublish.add(movie)) return;

        const numMovieFrames :int = movie.frames;
        for each (var layer :LayerMold in movie.layers) {
            for each (var kf :KeyframeMold in layer.keyframes) {
                var swfTexture :SwfTexture = null;
                if (movie.flipbook) {
                    try {
                        swfTexture = SwfTexture.fromFlipbook(this, movie, kf.index)
                    } catch (e :Error) {
                        addTopLevelError(ParseError.CRIT, "Error creating flipbook texture from '" + movie.id + "'");
                        swfTexture = null;
                    }

                } else {
                    if (kf.ref == null) continue;
                    kf.ref = _libraryNameToId.get(kf.ref);
                    var item :Object = _idToItem[kf.ref];
                    if (item == null) {
                        addTopLevelError(ParseError.CRIT,
                                "unrecognized library item '" + kf.ref + "'");
                    } else if (item is MovieMold) {
                        prepareForPublishing(MovieMold(item));
                    } else if (item is XflTexture) {
                        const tex :XflTexture = XflTexture(item);
                        try {
                            swfTexture = SwfTexture.fromTexture(this.swf, tex);
                        } catch (e :Error) {
                            addTopLevelError(ParseError.CRIT, "Error creating texture '" + tex.symbol + "'");
                            swfTexture = null;
                        }
                    }
                }

                if (swfTexture != null) {
                    // Texture symbols have origins. For texture layer keyframes,
                    // we combine the texture's origin with the keyframe's pivot point.
                    kf.pivotX += swfTexture.origin.x;
                    kf.pivotY += swfTexture.origin.y;
                }
            }
        }
    }

    public function createId (item :Object, libraryName :String, symbol :String) :String {
        if (symbol != null) _moldToSymbol.put(item, symbol);
        const id :String = symbol == null ? IMPLICIT_PREFIX + libraryName : symbol;
        _libraryNameToId.put(libraryName, id);
        _idToItem[id] = item;
        return id;
    }

    public function getErrors (sev :String=null) :Vector.<ParseError> {
        if (sev == null) return _errors;
        const sevOrdinal :int = ParseError.severityToOrdinal(sev);
        return _errors.filter(function (err :ParseError, ..._) :Boolean {
            return err.sevOrdinal >= sevOrdinal;
        });
    }

    public function get valid () :Boolean { return getErrors(ParseError.CRIT).length == 0; }

    public function addTopLevelError (severity :String, message :String, e :Object=null) :void {
        addError(location, severity, message, e);
    }

    public function addError (location :String, severity :String, message :String, e :Object=null) :void {
        _errors.push(new ParseError(location, severity, message, e));
    }

    public function toJSONString (atlases :Vector.<Atlas>, conf :ExportConf, pretty :Boolean=false) :String {
        return JSON.stringify(toMold(atlases, conf), null, pretty ? "  " : null);
    }

    public function toMold (atlases :Vector.<Atlas>, conf :ExportConf) :LibraryMold {
        const mold :LibraryMold = new LibraryMold();
        mold.frameRate = frameRate;
        mold.md5 = md5;
        mold.movies = publishedMovies.map(function (movie :MovieMold, ..._) :MovieMold {
            return movie.scale(conf.scale);
        });
        mold.textureGroups = createTextureGroupMolds(atlases);
        return mold;
    }

    public function loadSWF (path :String, loader :Executor=null) :Future {
        const onComplete :FutureTask = new FutureTask();

        const swfFile :File = new File(path);
        const loadSwfFile :Future = Files.load(swfFile, loader);
        loadSwfFile.succeeded.connect(function (data :ByteArray) :void {
            md5 = MD5.hashBytes(data);

            const loadSwf :Future = new SwfLoader().loadFromBytes(data);
            loadSwf.succeeded.connect(function (loadedSwf :LoadedSwf) :void {
                swf = loadedSwf;
            });
            loadSwf.failed.connect(function (error :Error) :void {
                addTopLevelError(ParseError.CRIT, error.message, error);
            });
            loadSwf.completed.connect(onComplete.succeed);
        });
        loadSwfFile.failed.connect(function (error :Error) :void {
            addTopLevelError(ParseError.CRIT, error.message, error);
            onComplete.fail(error);
        });

        return onComplete;
    }

    /**
     * @returns A list of paths to symbols in this library.
     */
    public function parseDocumentFile (fileData :ByteArray, path :String) :Vector.<String> {
        const xml :XML = Util.bytesToXML(fileData);
        frameRate = XmlUtil.getNumberAttr(xml, "frameRate", 24);

        const hex :String = XmlUtil.getStringAttr(xml, "backgroundColor", "#ffffff");
        backgroundColor = parseInt(hex.substr(1), 16);

        if (xml.media != null) {
            for each (var bitmap :XML in xml.media.DOMBitmapItem) {
                if (XmlUtil.getBooleanAttr(bitmap, "linkageExportForAS", false)) {
                    textures.push(new XflTexture(this, location, bitmap));
                }
            }
        }

        const paths :Vector.<String> = new <String>[];
        if (xml.symbols != null) {
            for each (var symbolXmlPath :XML in xml.symbols.Include) {
                paths.push("LIBRARY/" + XmlUtil.getStringAttr(symbolXmlPath, "href"));
            }
        }

        return paths;
    }

    public function parseLibraryFile (fileData :ByteArray, path :String) :void {
        const xml :XML = Util.bytesToXML(fileData);
        if (xml.name().localName != "DOMSymbolItem") {
            addTopLevelError(ParseError.DEBUG,
                "Skipping file since its root element isn't DOMSymbolItem");
            return;
        } else if (XmlUtil.getStringAttr(xml, "symbolType", "") == "graphic") {
            addTopLevelError(ParseError.DEBUG, "Skipping file because symbolType=graphic");
            return;
        }

        const isSprite :Boolean = XmlUtil.getBooleanAttr(xml, "isSpriteSubclass", false);
        log.debug("Parsing for library", "file", path, "isSprite", isSprite);
        try {
            if (isSprite) {
                // if "export in first frame" is not set, we won't be able to load the texture
                // from the swf.
                // TODO: remove this restriction by loading the entire swf before reading textures?
                if (!XmlUtil.getBooleanAttr(xml, "linkageExportInFirstFrame", true)) {
                    addError(location + ":" + XmlUtil.getStringAttr(xml, "linkageClassName"),
                        ParseError.CRIT, "\"Export in frame 1\" must be set");
                    return;
                }
                var texture :XflTexture = new XflTexture(this, location, xml);
                if (texture.isValid(swf)) textures.push(texture);
                else addError(location + ":" + texture.symbol, ParseError.CRIT, "Sprite is empty");

            } else {
                // It's a movie. If it's exported, we parse it now.
                // Else, we save it for possible parsing later.
                // (Un-exported movies that are not referenced will not be published.)
                if (XflMovie.isExported(xml)) parseMovie(xml);
                else _unexportedMovies.put(XflMovie.getName(xml), xml);
            }
        } catch (e :Error) {
            var type :String = isSprite ? "sprite" : "movie";
            addTopLevelError(ParseError.CRIT, "Unable to parse " + type + " in " + path, e);
            log.error("Unable to parse " + path, e);
        }
    }

    protected function parseMovie (xml :XML) :void {
        movies.push(XflMovie.parse(this, xml));
    }

    /** Creates TextureGroupMolds from a list of Atlases */
    protected static function createTextureGroupMolds (atlases :Vector.<Atlas>) :Vector.<TextureGroupMold> {
        const groups :Vector.<TextureGroupMold> = new <TextureGroupMold>[];
        function getGroup (scaleFactor :int) :TextureGroupMold {
            for each (var group :TextureGroupMold in groups) {
                if (group.scaleFactor == scaleFactor) {
                    return group;
                }
            }
            group = new TextureGroupMold();
            group.scaleFactor = scaleFactor;
            groups.push(group);
            return group;
        }

        for each (var atlas :Atlas in atlases) {
            var group :TextureGroupMold = getGroup(atlas.scaleFactor);
            group.atlases.push(atlas.toMold());
        }

        return groups;
    }

    /** Library name to XML for movies in the XFL that are not marked for export */
    protected const _unexportedMovies :Map = Maps.newMapOf(String);

    /** Object to symbol name for all exported textures and movies in the library */
    protected const _moldToSymbol :Map = Maps.newMapOf(Object);

    /** Library name to symbol or generated symbol for all textures and movies in the library */
    protected const _libraryNameToId :Map = Maps.newMapOf(String);

    /** Exported movies or movies used in exported movies. */
    protected const _toPublish :Set = Sets.newSetOf(MovieMold);

    /** Symbol or generated symbol to texture or movie. */
    protected const _idToItem :Dictionary = new Dictionary();

    protected const _errors :Vector.<ParseError> = new <ParseError>[];

    private static const log :Log = Log.getLog(XflLibrary);
}
}

//
// Flump - Copyright 2013 Flump Authors

package flump.xfl {

import com.adobe.crypto.MD5;
import com.threerings.util.Log;
import com.threerings.util.Map;
import com.threerings.util.Maps;
import com.threerings.util.Set;
import com.threerings.util.Sets;
import com.threerings.util.XmlUtil;

import flash.filesystem.File;
import flash.utils.ByteArray;
import flash.utils.Dictionary;

import flump.SwfTexture;
import flump.Util;
import flump.executor.Executor;
import flump.executor.Future;
import flump.executor.FutureTask;
import flump.executor.load.LoadedSwf;
import flump.executor.load.SwfLoader;
import flump.export.Atlas;
import flump.export.ExportConf;
import flump.export.Files;
import flump.mold.KeyframeMold;
import flump.mold.LayerMold;
import flump.mold.LibraryMold;
import flump.mold.MovieMold;
import flump.mold.TextureGroupMold;

public class XflLibrary
{
    use namespace xflns;

    /**
     * When an exported movie contains an unexported movie, it gets assigned a generated symbol
     * name with this prefix.
     */
    public static const IMPLICIT_PREFIX :String = "~";

    public var swf :LoadedSwf;

    public var frameRate :Number;
    public var backgroundColor :int;

    // The MD5 of the published library SWF
    public var md5 :String;

    public var location :String;

    public const movies :Vector.<MovieMold> = new <MovieMold>[];
    public const textures :Vector.<XflTexture> = new <XflTexture>[];

    public function XflLibrary (location :String) {
        this.location = location;
    }

    public function getItem (id :String, requiredType :Class=null) :* {
        const result :* = _idToItem[id];
        if (result === undefined) throw new Error("Unknown library item '" + id + "'");
        else if (requiredType != null) return requiredType(result);
        else return result;
    }

    public function isExported (movie :MovieMold) :Boolean {
        return _moldToSymbol.containsKey(movie);
    }

    public function get publishedMovies () :Vector.<MovieMold> {
        const result :Vector.<MovieMold> = new <MovieMold>[];
        for each (var movie :MovieMold in _toPublish.toArray().sortOn("id")) result.push(movie);
        return result;
    }

    public function finishLoading () :void {
        var movie :MovieMold = null;

        // Parse all un-exported movies that are referenced by the exported movies.
        for (var ii :int = 0; ii < movies.length; ++ii) {
            movie = movies[ii];
            for each (var symbolName :String in XflMovie.getSymbolNames(movie).toArray()) {
                var xml :XML = _unexportedMovies.remove(symbolName);
                if (xml != null) parseMovie(xml);
            }
        }

        for each (movie in movies) if (isExported(movie)) prepareForPublishing(movie);
    }

    protected function prepareForPublishing (movie :MovieMold) :void {
        if (!_toPublish.add(movie)) return;

        for each (var layer :LayerMold in movie.layers) {
            for each (var kf :KeyframeMold in layer.keyframes) {
                var swfTexture :SwfTexture = null;
                if (movie.flipbook) {
                    try {
                        swfTexture = SwfTexture.fromFlipbook(this, movie, kf.index)
                    } catch (e :Error) {
                        addTopLevelError(ParseError.CRIT, "Error creating flipbook texture from '" + movie.id + "'");
                        swfTexture = null;
                    }

                } else {
                    if (kf.ref == null) continue;
                    kf.ref = _libraryNameToId.get(kf.ref);
                    var item :Object = _idToItem[kf.ref];
                    if (item == null) {
                        addTopLevelError(ParseError.CRIT,
                                "unrecognized library item '" + kf.ref + "'");
                    } else if (item is MovieMold) {
                        prepareForPublishing(MovieMold(item));
                    } else if (item is XflTexture) {
                        const tex :XflTexture = XflTexture(item);
                        try {
                            swfTexture = SwfTexture.fromTexture(this.swf, tex);
                        } catch (e :Error) {
                            addTopLevelError(ParseError.CRIT, "Error creating texture '" + tex.symbol + "'");
                            swfTexture = null;
                        }
                    }
                }

                if (swfTexture != null) {
                    // Texture symbols have origins. For texture layer keyframes,
                    // we combine the texture's origin with the keyframe's pivot point.
                    kf.pivotX += swfTexture.origin.x;
                    kf.pivotY += swfTexture.origin.y;
                }
            }
        }
    }

    public function createId (item :Object, libraryName :String, symbol :String) :String {
        if (symbol != null) _moldToSymbol.put(item, symbol);
        const id :String = symbol == null ? IMPLICIT_PREFIX + libraryName : symbol;
        _libraryNameToId.put(libraryName, id);
        _idToItem[id] = item;
        return id;
    }

    public function getErrors (sev :String=null) :Vector.<ParseError> {
        if (sev == null) return _errors;
        const sevOrdinal :int = ParseError.severityToOrdinal(sev);
        return _errors.filter(function (err :ParseError, ..._) :Boolean {
            return err.sevOrdinal >= sevOrdinal;
        });
    }

    public function get valid () :Boolean { return getErrors(ParseError.CRIT).length == 0; }

    public function addTopLevelError (severity :String, message :String, e :Object=null) :void {
        addError(location, severity, message, e);
    }

    public function addError (location :String, severity :String, message :String, e :Object=null) :void {
        _errors.push(new ParseError(location, severity, message, e));
    }

    public function toJSONString (atlases :Vector.<Atlas>, conf :ExportConf, pretty :Boolean=false) :String {
        return JSON.stringify(toMold(atlases, conf), null, pretty ? "  " : null);
    }

    public function toMold (atlases :Vector.<Atlas>, conf :ExportConf) :LibraryMold {
        const mold :LibraryMold = new LibraryMold();
        mold.frameRate = frameRate;
        mold.md5 = md5;
        mold.movies = publishedMovies.map(function (movie :MovieMold, ..._) :MovieMold {
            return movie.scale(conf.scale);
        });
        mold.textureGroups = createTextureGroupMolds(atlases);
        return mold;
    }

    public function loadSWF (path :String, loader :Executor=null) :Future {
        const onComplete :FutureTask = new FutureTask();

        const swfFile :File = new File(path);
        const loadSwfFile :Future = Files.load(swfFile, loader);
        loadSwfFile.succeeded.connect(function (data :ByteArray) :void {
            md5 = MD5.hashBytes(data);

            const loadSwf :Future = new SwfLoader().loadFromBytes(data);
            loadSwf.succeeded.connect(function (loadedSwf :LoadedSwf) :void {
                swf = loadedSwf;
            });
            loadSwf.failed.connect(function (error :Error) :void {
                addTopLevelError(ParseError.CRIT, error.message, error);
            });
            loadSwf.completed.connect(onComplete.succeed);
        });
        loadSwfFile.failed.connect(function (error :Error) :void {
            addTopLevelError(ParseError.CRIT, error.message, error);
            onComplete.fail(error);
        });

        return onComplete;
    }

    /**
     * @returns A list of paths to symbols in this library.
     */
    public function parseDocumentFile (fileData :ByteArray, path :String) :Vector.<String> {
        const xml :XML = Util.bytesToXML(fileData);
        frameRate = XmlUtil.getNumberAttr(xml, "frameRate", 24);

        const hex :String = XmlUtil.getStringAttr(xml, "backgroundColor", "#ffffff");
        backgroundColor = parseInt(hex.substr(1), 16);

        if (xml.media != null) {
            for each (var bitmap :XML in xml.media.DOMBitmapItem) {
                if (XmlUtil.getBooleanAttr(bitmap, "linkageExportForAS", false)) {
                    textures.push(new XflTexture(this, location, bitmap));
                }
            }
        }

        const paths :Vector.<String> = new <String>[];
        if (xml.symbols != null) {
            for each (var symbolXmlPath :XML in xml.symbols.Include) {
                paths.push("LIBRARY/" + XmlUtil.getStringAttr(symbolXmlPath, "href"));
            }
        }

        return paths;
    }

    public function parseLibraryFile (fileData :ByteArray, path :String) :void {
        const xml :XML = Util.bytesToXML(fileData);
        if (xml.name().localName != "DOMSymbolItem") {
            addTopLevelError(ParseError.DEBUG,
                "Skipping file since its root element isn't DOMSymbolItem");
            return;
        } else if (XmlUtil.getStringAttr(xml, "symbolType", "") == "graphic") {
            addTopLevelError(ParseError.DEBUG, "Skipping file because symbolType=graphic");
            return;
        }

        const isSprite :Boolean = XmlUtil.getBooleanAttr(xml, "isSpriteSubclass", false);
        log.debug("Parsing for library", "file", path, "isSprite", isSprite);
        try {
            if (isSprite) {
                // if "export in first frame" is not set, we won't be able to load the texture
                // from the swf.
                // TODO: remove this restriction by loading the entire swf before reading textures?
                if (!XmlUtil.getBooleanAttr(xml, "linkageExportInFirstFrame", true)) {
                    addError(location + ":" + XmlUtil.getStringAttr(xml, "linkageClassName"),
                        ParseError.CRIT, "\"Export in frame 1\" must be set");
                    return;
                }
                var texture :XflTexture = new XflTexture(this, location, xml);
                if (texture.isValid(swf)) textures.push(texture);
                else addError(location + ":" + texture.symbol, ParseError.CRIT, "Sprite is empty");

            } else {
                // It's a movie. If it's exported, we parse it now.
                // Else, we save it for possible parsing later.
                // (Un-exported movies that are not referenced will not be published.)
                if (XflMovie.isExported(xml)) parseMovie(xml);
                else _unexportedMovies.put(XflMovie.getName(xml), xml);
            }
        } catch (e :Error) {
            var type :String = isSprite ? "sprite" : "movie";
            addTopLevelError(ParseError.CRIT, "Unable to parse " + type + " in " + path, e);
            log.error("Unable to parse " + path, e);
        }
    }

    protected function parseMovie (xml :XML) :void {
        movies.push(XflMovie.parse(this, xml));
    }

    /** Creates TextureGroupMolds from a list of Atlases */
    protected static function createTextureGroupMolds (atlases :Vector.<Atlas>) :Vector.<TextureGroupMold> {
        const groups :Vector.<TextureGroupMold> = new <TextureGroupMold>[];
        function getGroup (scaleFactor :int) :TextureGroupMold {
            for each (var group :TextureGroupMold in groups) {
                if (group.scaleFactor == scaleFactor) {
                    return group;
                }
            }
            group = new TextureGroupMold();
            group.scaleFactor = scaleFactor;
            groups.push(group);
            return group;
        }

        for each (var atlas :Atlas in atlases) {
            var group :TextureGroupMold = getGroup(atlas.scaleFactor);
            group.atlases.push(atlas.toMold());
        }

        return groups;
    }

    /** Library name to XML for movies in the XFL that are not marked for export */
    protected const _unexportedMovies :Map = Maps.newMapOf(String);

    /** Object to symbol name for all exported textures and movies in the library */
    protected const _moldToSymbol :Map = Maps.newMapOf(Object);

    /** Library name to symbol or generated symbol for all textures and movies in the library */
    protected const _libraryNameToId :Map = Maps.newMapOf(String);

    /** Exported movies or movies used in exported movies. */
    protected const _toPublish :Set = Sets.newSetOf(MovieMold);

    /** Symbol or generated symbol to texture or movie. */
    protected const _idToItem :Dictionary = new Dictionary();

    protected const _errors :Vector.<ParseError> = new <ParseError>[];

    private static const log :Log = Log.getLog(XflLibrary);
}
}

remove unused variable

remove unused variable


/*
 * Copyright (C) 2003-2007
 * Nick Velloff <nick.velloff@gmail.com>
 * Derrick Grigg <dgrigg@rogers.com>
 * Sean Voisen <sean@voisen.org>
 * Sean Treadway <seant@oncotype.dk>
 *
 * 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.Event;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.xml.XMLDocument;
	import flash.xml.XMLNode;

	import mx.logging.ILogger;

	import org.jivesoftware.xiff.events.*;
	import org.jivesoftware.xiff.logging.LoggerFactory;
	import org.jivesoftware.xiff.util.SocketConn;
	import org.jivesoftware.xiff.util.SocketDataEvent;

	/**
	 * A child of XMPPConnection, this class makes use of the
	 * Flash AVM2 binary socket instead of the older XMLSocket.
	 * This gets rid of issues related to the XMLSocket's appending
	 * of a null-byte to all outgoing data.
	 *
	 * @see org.jivesoftware.xiff.core.XMPPConnection
	 */
	public class XMPPSocketConnection extends XMPPConnection
	{
		private static const logger:ILogger = LoggerFactory.getLogger("org.jivesoftware.xiff.core.XMPPSocketConnection");

		private var _incompleteRawXML: String = '';

		protected var binarySocket:SocketConn;

		public function XMPPSocketConnection()
		{
			super();
			configureSocket();
		}

		private function configureSocket():void {
			binarySocket = new SocketConn();

	        binarySocket.addEventListener(Event.CLOSE, socketClosed);
	        binarySocket.addEventListener(Event.CONNECT, socketConnected);
	        binarySocket.addEventListener(IOErrorEvent.IO_ERROR, onIOError);
	        binarySocket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityError);
	        binarySocket.addEventListener(SocketDataEvent.SOCKET_DATA_RECEIVED, bSocketReceivedData);
	    }

	    override protected function sendXML( someData:* ):void
		{
			logger.info("OUTGOING: {0}", someData);
			// Data is untyped because it could be a string or XML
	        binarySocket.sendString(someData);

			var event:OutgoingDataEvent = new OutgoingDataEvent();
			event.data = someData;
			dispatchEvent( event );
		}

		override public function disconnect():void
		{
			if( isActive() ) {
				sendXML( closingStreamTag );
				binarySocket.close();
				active = false;
				loggedIn = false;
				var event:DisconnectionEvent = new DisconnectionEvent();
				dispatchEvent(event);
			}
		}

		override public function connect( streamType:String = "terminatedStandard" ):Boolean
		{
			active = false;
			loggedIn = false;

			// Stream type lets user set opening/closing tag - some servers (jadc2s) prefer <stream:flash> to the standard
			// <stream:stream>
			switch( streamType ) {
				case "flash":
					openingStreamTag = "<?xml version=\"1.0\"?><flash:stream to=\"" + domain + "\" xmlns=\"jabber:client\" xmlns:flash=\"http://www.jabber.com/streams/flash\" version=\"1.0\">";
					closingStreamTag = "</flash:stream>";
					break;

				case "terminatedFlash":
					openingStreamTag = "<?xml version=\"1.0\"?><flash:stream to=\"" + domain + "\" xmlns=\"jabber:client\" xmlns:flash=\"http://www.jabber.com/streams/flash\" version=\"1.0\" />";
					closingStreamTag = "</flash:stream>";
					break;

				case "standard":
					openingStreamTag = "<?xml version=\"1.0\"?><stream:stream to=\"" + domain + "\" xmlns=\"jabber:client\" xmlns:stream=\"http://etherx.jabber.org/streams\" version=\"1.0\">";
					closingStreamTag = "</stream:stream>";
					break;

				case "terminatedStandard":
				default:
					openingStreamTag = "<?xml version=\"1.0\"?><stream:stream to=\"" + domain + "\" xmlns=\"jabber:client\" xmlns:stream=\"http://etherx.jabber.org/streams\" version=\"1.0\" />";
					closingStreamTag = "</stream:stream>";
					break;
			}
			binarySocket.connect( server, port );
			return true;
		}

		protected function bSocketReceivedData( ev:SocketDataEvent ):void
		{
			var rawXML:String = _incompleteRawXML + ev.data as String;

			logger.info("INCOMING: {0}", rawXML);
			// 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("<flash:stream");
				var resultObj:Object = pattern.exec(rawXML);
				if (resultObj != null) // stop searching for unterminated node
				{
					rawXML = rawXML.concat("</flash:stream>");
					_expireTagSearch = true;
				}
			}

			if (!_expireTagSearch)
			{
				var pattern2:RegExp = new RegExp("<stream:stream");
				var resultObj2:Object = pattern2.exec(rawXML);
				if (resultObj2 != null) // stop searching for unterminated node
				{
					rawXML = rawXML.concat("</stream:stream>");
					_expireTagSearch = true;
				}
			}

			var xmlData:XMLDocument = new XMLDocument();
			xmlData.ignoreWhite = this.ignoreWhite;

			//error handling to catch incomplete xml strings that have
			//been truncated by the socket
			try{
				var isComplete: Boolean = true;
				xmlData.parseXML( rawXML );
				_incompleteRawXML = '';
			}
			catch(err:Error){
				isComplete = false;
				_incompleteRawXML += ev.data as String;//concatenate the raw xml to the previous xml
			}

			if (isComplete){

				var event:IncomingDataEvent = new IncomingDataEvent();
				event.data = xmlData;
				dispatchEvent( event );

				for (var i:int = 0; i<xmlData.childNodes.length; i++)
				{
					// Read the data and send it to the appropriate parser
					var currentNode:XMLNode = xmlData.childNodes[i];
					var nodeName:String = currentNode.nodeName.toLowerCase();

					switch( nodeName )
					{
						case "stream:stream":
							_expireTagSearch = false;
							handleStream( currentNode );
							break;
						case "flash:stream":
							_expireTagSearch = false;
							handleStream( currentNode );
							break;

						case "stream:error":
							handleStreamError( currentNode );
							break;

						case "iq":
							handleIQ( currentNode );
							break;

						case "message":
							handleMessage( currentNode );
							break;

						case "presence":
							handlePresence( currentNode );
							break;

						default:
							dispatchError( "undefined-condition", "Unknown Error", "modify", 500 );
							break;
					}
				}
			}
		}
	}
}

/*
 * Copyright (C) 2003-2007
 * Nick Velloff <nick.velloff@gmail.com>
 * Derrick Grigg <dgrigg@rogers.com>
 * Sean Voisen <sean@voisen.org>
 * Sean Treadway <seant@oncotype.dk>
 *
 * 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.Event;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.xml.XMLDocument;
	import flash.xml.XMLNode;

	import mx.logging.ILogger;

	import org.jivesoftware.xiff.events.*;
	import org.jivesoftware.xiff.logging.LoggerFactory;
	import org.jivesoftware.xiff.util.SocketConn;
	import org.jivesoftware.xiff.util.SocketDataEvent;

	/**
	 * A child of XMPPConnection, this class makes use of the
	 * Flash AVM2 binary socket instead of the older XMLSocket.
	 * This gets rid of issues related to the XMLSocket's appending
	 * of a null-byte to all outgoing data.
	 *
	 * @see org.jivesoftware.xiff.core.XMPPConnection
	 */
	public class XMPPSocketConnection extends XMPPConnection
	{
		private static const logger:ILogger = LoggerFactory.getLogger("org.jivesoftware.xiff.core.XMPPSocketConnection");

		private var _incompleteRawXML: String = '';

		protected var binarySocket:SocketConn;

		public function XMPPSocketConnection()
		{
			super();
			configureSocket();
		}

		private function configureSocket():void {
			binarySocket = new SocketConn();

	        binarySocket.addEventListener(Event.CLOSE, socketClosed);
	        binarySocket.addEventListener(Event.CONNECT, socketConnected);
	        binarySocket.addEventListener(IOErrorEvent.IO_ERROR, onIOError);
	        binarySocket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityError);
	        binarySocket.addEventListener(SocketDataEvent.SOCKET_DATA_RECEIVED, bSocketReceivedData);
	    }

	    override protected function sendXML( someData:* ):void
		{
			logger.info("OUTGOING: {0}", someData);
			// Data is untyped because it could be a string or XML
	        binarySocket.sendString(someData);

			var event:OutgoingDataEvent = new OutgoingDataEvent();
			event.data = someData;
			dispatchEvent( event );
		}

		override public function disconnect():void
		{
			if( isActive() ) {
				sendXML( closingStreamTag );
				binarySocket.close();
				active = false;
				loggedIn = false;
				var event:DisconnectionEvent = new DisconnectionEvent();
				dispatchEvent(event);
			}
		}

		override public function connect( streamType:String = "terminatedStandard" ):Boolean
		{
			active = false;
			loggedIn = false;

			// Stream type lets user set opening/closing tag - some servers (jadc2s) prefer <stream:flash> to the standard
			// <stream:stream>
			switch( streamType ) {
				case "flash":
					openingStreamTag = "<?xml version=\"1.0\"?><flash:stream to=\"" + domain + "\" xmlns=\"jabber:client\" xmlns:flash=\"http://www.jabber.com/streams/flash\" version=\"1.0\">";
					closingStreamTag = "</flash:stream>";
					break;

				case "terminatedFlash":
					openingStreamTag = "<?xml version=\"1.0\"?><flash:stream to=\"" + domain + "\" xmlns=\"jabber:client\" xmlns:flash=\"http://www.jabber.com/streams/flash\" version=\"1.0\" />";
					closingStreamTag = "</flash:stream>";
					break;

				case "standard":
					openingStreamTag = "<?xml version=\"1.0\"?><stream:stream to=\"" + domain + "\" xmlns=\"jabber:client\" xmlns:stream=\"http://etherx.jabber.org/streams\" version=\"1.0\">";
					closingStreamTag = "</stream:stream>";
					break;

				case "terminatedStandard":
				default:
					openingStreamTag = "<?xml version=\"1.0\"?><stream:stream to=\"" + domain + "\" xmlns=\"jabber:client\" xmlns:stream=\"http://etherx.jabber.org/streams\" version=\"1.0\" />";
					closingStreamTag = "</stream:stream>";
					break;
			}
			binarySocket.connect( server, port );
			return true;
		}

		protected function bSocketReceivedData( ev:SocketDataEvent ):void
		{
			var rawXML:String = _incompleteRawXML + ev.data as String;

			logger.info("INCOMING: {0}", rawXML);
			// 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("<flash:stream");
				var resultObj:Object = pattern.exec(rawXML);
				if (resultObj != null) // stop searching for unterminated node
				{
					rawXML = rawXML.concat("</flash:stream>");
					_expireTagSearch = true;
				}
			}

			if (!_expireTagSearch)
			{
				var pattern2:RegExp = new RegExp("<stream:stream");
				var resultObj2:Object = pattern2.exec(rawXML);
				if (resultObj2 != null) // stop searching for unterminated node
				{
					rawXML = rawXML.concat("</stream:stream>");
					_expireTagSearch = true;
				}
			}

			var xmlData:XMLDocument = new XMLDocument();
			xmlData.ignoreWhite = this.ignoreWhite;

			//error handling to catch incomplete xml strings that have
			//been truncated by the socket
			try{
				var isComplete: Boolean = true;
				xmlData.parseXML( rawXML );
				_incompleteRawXML = '';
			}
			catch(err:Error){
				isComplete = false;
				_incompleteRawXML += ev.data as String;//concatenate the raw xml to the previous xml
			}

			if (isComplete){

				var event:IncomingDataEvent = new IncomingDataEvent();
				event.data = xmlData;
				dispatchEvent( event );

				for (var i:int = 0; i<xmlData.childNodes.length; i++)
				{
					// Read the data and send it to the appropriate parser
					var currentNode:XMLNode = xmlData.childNodes[i];
					var nodeName:String = currentNode.nodeName.toLowerCase();

					switch( nodeName )
					{
						case "stream:stream":
							_expireTagSearch = false;
							handleStream( currentNode );
							break;
						case "flash:stream":
							_expireTagSearch = false;
							handleStream( currentNode );
							break;

						case "stream:error":
							handleStreamError( currentNode );
							break;

						case "iq":
							handleIQ( currentNode );
							break;

						case "message":
							handleMessage( currentNode );
							break;

						case "presence":
							handlePresence( currentNode );
							break;

						case "stream:features":
							handleStreamFeatures( currentNode );
							break;

						case "success":
							handleAuthentication( currentNode );
							break;

						case "failure":
							handleAuthentication( currentNode );
							break;

						default:
							dispatchError( "undefined-condition", "Unknown Error", "modify", 500 );
							break;
					}
				}
			}
		}
	}
}

Allow XMPPSocketConnection to handle the stream:features packet.

git-svn-id: c197267f952b24206666de142881703007ca05d5@10749 b35dd754-fafc-0310-a699-88a17e54d16e


package
{
    import flash.display.Sprite;
    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    import flash.text.TextField;
    import flash.text.TextFormat;
    import flash.net.Socket;
    import flash.events.Event;
    import flash.events.IOErrorEvent;
    import flash.events.ProgressEvent;
    import flash.events.SecurityErrorEvent;
    import flash.utils.ByteArray;
    import flash.utils.setTimeout;

    public class swfcat extends Sprite
    {
        /* David's relay (nickname 3VXRyxz67OeRoqHn) that also serves a
           crossdomain policy. */
        private const DEFAULT_TOR_ADDR:Object = {
            host: "173.255.221.44",
            port: 9001
        };
        private const DEFAULT_FACILITATOR_ADDR:Object = {
            host: "173.255.221.44",
            port: 9002
        };

        private const MAX_NUM_PROXY_PAIRS:uint = 1;

        // Milliseconds.
        private const FACILITATOR_POLL_INTERVAL:int = 10000;

        // Bytes per second.
        public const RATE_LIMIT:Number = 10 * 1024;
        // Seconds.
        private const RATE_LIMIT_HISTORY:Number = 5.0;

        // Socket to facilitator.
        private var s_f:Socket;

        /* TextField for debug output. */
        private var output_text:TextField;

        private var fac_addr:Object;

        /* Number of proxy pairs currently connected (up to
           MAX_NUM_PROXY_PAIRS). */
        private var num_proxy_pairs:int = 0;
        /* Number of proxy pairs ever connected. */
        private var total_proxy_pairs:int = 0;

        public var rate_limit:RateLimit;

        /* Badge with a client counter */
        [Embed(source="badge_con_counter.png")]
        private var BadgeImage:Class;
        private var client_count_tf:TextField;
        private var client_count_fmt:TextFormat;

        public function puts(s:String):void
        {
            output_text.appendText(s + "\n");
            output_text.scrollV = output_text.maxScrollV;
        }

        public function update_client_count():void
        {
            if (String(total_proxy_pairs).length == 1)
                client_count_tf.text = "0" + String(total_proxy_pairs);
            else
                client_count_tf.text = String(total_proxy_pairs);
        }

        public function swfcat()
        {
            // Absolute positioning.
            stage.scaleMode = StageScaleMode.NO_SCALE;
            stage.align = StageAlign.TOP_LEFT;

            output_text = new TextField();
            output_text.width = stage.stageWidth;
            output_text.height = stage.stageHeight;
            output_text.background = true;
            output_text.backgroundColor = 0x001f0f;
            output_text.textColor = 0x44cc44;

            /* Setup client counter for badge. */
            client_count_fmt = new TextFormat();
            client_count_fmt.color = 0xFFFFFF;
            client_count_fmt.align = "center";
            client_count_fmt.font = "courier-new";
            client_count_fmt.bold = true;
            client_count_fmt.size = 10;
            client_count_tf = new TextField();
            client_count_tf.width = 20;
            client_count_tf.height = 17;
            client_count_tf.background = false;
            client_count_tf.defaultTextFormat = client_count_fmt;
            client_count_tf.x=47;
            client_count_tf.y=3;

            /* Update the client counter on badge. */
            update_client_count();

            rate_limit = new RateLimit(RATE_LIMIT * RATE_LIMIT_HISTORY, RATE_LIMIT_HISTORY);

            puts("Starting.");
            // Wait until the query string parameters are loaded.
            this.loaderInfo.addEventListener(Event.COMPLETE, loaderinfo_complete);
        }

        private function loaderinfo_complete(e:Event):void
        {
            var fac_spec:String;

            puts("Parameters loaded.");

            if (this.loaderInfo.parameters["debug"])
                addChild(output_text);
            else {
                addChild(new BadgeImage());
                /* Tried unsuccessfully to add counter to badge. */
                /* For now, need two addChilds :( */
                addChild(client_count_tf);
            }

            fac_spec = this.loaderInfo.parameters["facilitator"];
            if (fac_spec) {
                puts("Facilitator spec: \"" + fac_spec + "\"");
                fac_addr = parse_addr_spec(fac_spec);
                if (!fac_addr) {
                    puts("Error: Facilitator spec must be in the form \"host:port\".");
                    return;
                }
            } else {
                fac_addr = DEFAULT_FACILITATOR_ADDR;
            }

            main();
        }

        /* The main logic begins here, after start-up issues are taken care of. */
        private function main():void
        {
            if (num_proxy_pairs >= MAX_NUM_PROXY_PAIRS) {
                setTimeout(main, FACILITATOR_POLL_INTERVAL);
                return;
            }

            s_f = new Socket();

            s_f.addEventListener(Event.CONNECT, fac_connected);
            s_f.addEventListener(Event.CLOSE, function (e:Event):void {
                puts("Facilitator: closed connection.");
                setTimeout(main, FACILITATOR_POLL_INTERVAL);
            });
            s_f.addEventListener(IOErrorEvent.IO_ERROR, function (e:IOErrorEvent):void {
                puts("Facilitator: I/O error: " + e.text + ".");
            });
            s_f.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function (e:SecurityErrorEvent):void {
                puts("Facilitator: security error: " + e.text + ".");
            });

            puts("Facilitator: connecting to " + fac_addr.host + ":" + fac_addr.port + ".");
            s_f.connect(fac_addr.host, fac_addr.port);
        }

        private function fac_connected(e:Event):void
        {
            puts("Facilitator: connected.");

            s_f.addEventListener(ProgressEvent.SOCKET_DATA, fac_data);

            s_f.writeUTFBytes("GET / HTTP/1.0\r\n\r\n");
        }

        private function fac_data(e:ProgressEvent):void
        {
            var client_spec:String;
            var client_addr:Object;
            var proxy_pair:Object;

            client_spec = s_f.readMultiByte(e.bytesLoaded, "utf-8");
            puts("Facilitator: got \"" + client_spec + "\".");

            client_addr = parse_addr_spec(client_spec);
            if (!client_addr) {
                puts("Error: Client spec must be in the form \"host:port\".");
                return;
            }

            num_proxy_pairs++;
            total_proxy_pairs++;
            proxy_pair = new ProxyPair(this, client_addr, DEFAULT_TOR_ADDR);
            proxy_pair.addEventListener(Event.COMPLETE, function(e:Event):void {
                proxy_pair.log("Complete.");
                num_proxy_pairs--;
            });
            proxy_pair.connect();

            /* Update the client count on the badge. */
            update_client_count();
        }

        /* Parse an address in the form "host:port". Returns an Object with
           keys "host" (String) and "port" (int). Returns null on error. */
        private static function parse_addr_spec(spec:String):Object
        {
            var parts:Array;
            var addr:Object;

            parts = spec.split(":", 2);
            if (parts.length != 2 || !parseInt(parts[1]))
                return null;
            addr = {}
            addr.host = parts[0];
            addr.port = parseInt(parts[1]);

            return addr;
        }
    }
}

import flash.display.Sprite;
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.IOErrorEvent;
import flash.events.ProgressEvent;
import flash.events.SecurityErrorEvent;
import flash.net.Socket;
import flash.utils.ByteArray;
import flash.utils.clearTimeout;
import flash.utils.getTimer;
import flash.utils.setTimeout;

class RateLimit
{
    private var amount:Number;
    private var capacity:Number;
    private var time:Number;
    private var last_update:uint;

    public function RateLimit(capacity:Number, time:Number)
    {
        this.amount = 0.0;
        /* capacity / time is the rate we are aiming for. */
        this.capacity = capacity;
        this.time = time;
        this.last_update = getTimer();
    }

    private function age():void
    {
        var now:uint;
        var delta:Number;

        now = getTimer();
        delta = (now - last_update) / 1000.0;
        last_update = now;

        amount -= delta * capacity / time;
        if (amount < 0.0)
            amount = 0.0;
    }

    public function update(n:Number):Boolean
    {
        age();
        amount += n;

        return amount <= capacity;
    }

    public function when():Number
    {
        age();
        return (amount - capacity) / (capacity / time);
    }

    public function is_limited():Boolean
    {
        age();
        return amount > capacity;
    }
}

/* An instance of a client-relay connection. */
class ProxyPair extends EventDispatcher
{
    // Address ({host, port}) of client.
    private var addr_c:Object;
    // Address ({host, port}) of relay.
    private var addr_r:Object;

    // Socket to client.
    private var s_c:Socket;
    // Socket to relay.
    private var s_r:Socket;

    // Parent swfcat, for UI updates and rate meter.
    private var ui:swfcat;

    // Pending byte read counts for relay and client sockets.
    private var r2c_schedule:Array;
    private var c2r_schedule:Array;
    // Callback id.
    private var flush_id:uint;

    public function log(msg:String):void
    {
        ui.puts(id() + ": " + msg)
    }

    // String describing this pair for output.
    public function id():String
    {
        return "<" + this.addr_c.host + ":" + this.addr_c.port +
            "," + this.addr_r.host + ":" + this.addr_r.port + ">";
    }

    public function ProxyPair(ui:swfcat, addr_c:Object, addr_r:Object)
    {
        this.ui = ui;
        this.addr_c = addr_c;
        this.addr_r = addr_r;

        this.c2r_schedule = [];
        this.r2c_schedule = [];
    }

    public function connect():void
    {
        s_r = new Socket();

        s_r.addEventListener(Event.CONNECT, tor_connected);
        s_r.addEventListener(Event.CLOSE, function (e:Event):void {
            log("Tor: closed.");
            if (s_c.connected)
                s_c.close();
            dispatchEvent(new Event(Event.COMPLETE));
        });
        s_r.addEventListener(IOErrorEvent.IO_ERROR, function (e:IOErrorEvent):void {
            log("Tor: I/O error: " + e.text + ".");
            if (s_c.connected)
                s_c.close();
            dispatchEvent(new Event(Event.COMPLETE));
        });
        s_r.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function (e:SecurityErrorEvent):void {
            log("Tor: security error: " + e.text + ".");
            if (s_c.connected)
                s_c.close();
            dispatchEvent(new Event(Event.COMPLETE));
        });
        s_r.addEventListener(ProgressEvent.SOCKET_DATA, relay_to_client);

        log("Tor: connecting to " + addr_r.host + ":" + addr_r.port + ".");
        s_r.connect(addr_r.host, addr_r.port);
    }

    private function tor_connected(e:Event):void
    {
        log("Tor: connected.");

        s_c = new Socket();

        s_c.addEventListener(Event.CONNECT, client_connected);
        s_c.addEventListener(Event.CLOSE, function (e:Event):void {
            log("Client: closed.");
            if (s_r.connected)
                s_r.close();
            dispatchEvent(new Event(Event.COMPLETE));
        });
        s_c.addEventListener(IOErrorEvent.IO_ERROR, function (e:IOErrorEvent):void {
            log("Client: I/O error: " + e.text + ".");
            if (s_r.connected)
                s_r.close();
            dispatchEvent(new Event(Event.COMPLETE));
        });
        s_c.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function (e:SecurityErrorEvent):void {
            log("Client: security error: " + e.text + ".");
            if (s_r.connected)
                s_r.close();
            dispatchEvent(new Event(Event.COMPLETE));
        });
        s_c.addEventListener(ProgressEvent.SOCKET_DATA, client_to_relay);

        log("Client: connecting to " + addr_c.host + ":" + addr_c.port + ".");
        s_c.connect(addr_c.host, addr_c.port);
    }

    private function relay_to_client(e:ProgressEvent):void
    {
        r2c_schedule.push(e.bytesLoaded);
        flush();
    }

    private function client_to_relay(e:ProgressEvent):void
    {
        c2r_schedule.push(e.bytesLoaded);
        flush();
    }

    private function client_connected(e:Event):void
    {
        log("Client: connected.");
    }

    private function transfer_chunk(s_from:Socket, s_to:Socket, n:uint,
        label:String):void
    {
        var bytes:ByteArray;

        bytes = new ByteArray();
        s_from.readBytes(bytes, 0, n);
        s_to.writeBytes(bytes);
        ui.rate_limit.update(n);
        log(label + ": read " + bytes.length + ".");
    }

    /* Send as much data as the rate limit currently allows. */
    private function flush():void
    {
        if (flush_id)
            clearTimeout(flush_id);
        flush_id = undefined;

        if (!(s_r.connected && s_c.connected))
            /* Can't do anything until both sockets are connected. */
            return;

        while (!ui.rate_limit.is_limited() &&
               (r2c_schedule.length > 0 || c2r_schedule.length > 0)) {
            if (r2c_schedule.length > 0)
                transfer_chunk(s_r, s_c, r2c_schedule.shift(), "Tor");
            if (c2r_schedule.length > 0)
                transfer_chunk(s_c, s_r, c2r_schedule.shift(), "Client");
        }

        /* Call again when safe, if necessary. */
        if (r2c_schedule.length > 0 || c2r_schedule.length > 0)
            flush_id = setTimeout(flush, ui.rate_limit.when() * 1000);
    }
}

package
{
    import flash.display.Sprite;
    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    import flash.text.TextField;
    import flash.text.TextFormat;
    import flash.net.Socket;
    import flash.events.Event;
    import flash.events.IOErrorEvent;
    import flash.events.ProgressEvent;
    import flash.events.SecurityErrorEvent;
    import flash.utils.ByteArray;
    import flash.utils.setTimeout;

    public class swfcat extends Sprite
    {
        /* David's relay (nickname 3VXRyxz67OeRoqHn) that also serves a
           crossdomain policy. */
        private const DEFAULT_TOR_ADDR:Object = {
            host: "173.255.221.44",
            port: 9001
        };
        private const DEFAULT_FACILITATOR_ADDR:Object = {
            host: "173.255.221.44",
            port: 9002
        };

        private const MAX_NUM_PROXY_PAIRS:uint = 1;

        // Milliseconds.
        private const FACILITATOR_POLL_INTERVAL:int = 10000;

        // Bytes per second. Set to undefined to disable limit.
        public const RATE_LIMIT:Number = 10 * 1024;
        // Seconds.
        private const RATE_LIMIT_HISTORY:Number = 5.0;

        // Socket to facilitator.
        private var s_f:Socket;

        /* TextField for debug output. */
        private var output_text:TextField;

        private var fac_addr:Object;

        /* Number of proxy pairs currently connected (up to
           MAX_NUM_PROXY_PAIRS). */
        private var num_proxy_pairs:int = 0;
        /* Number of proxy pairs ever connected. */
        private var total_proxy_pairs:int = 0;

        public var rate_limit:RateLimit;

        /* Badge with a client counter */
        [Embed(source="badge_con_counter.png")]
        private var BadgeImage:Class;
        private var client_count_tf:TextField;
        private var client_count_fmt:TextFormat;

        public function puts(s:String):void
        {
            output_text.appendText(s + "\n");
            output_text.scrollV = output_text.maxScrollV;
        }

        public function update_client_count():void
        {
            if (String(total_proxy_pairs).length == 1)
                client_count_tf.text = "0" + String(total_proxy_pairs);
            else
                client_count_tf.text = String(total_proxy_pairs);
        }

        public function swfcat()
        {
            // Absolute positioning.
            stage.scaleMode = StageScaleMode.NO_SCALE;
            stage.align = StageAlign.TOP_LEFT;

            output_text = new TextField();
            output_text.width = stage.stageWidth;
            output_text.height = stage.stageHeight;
            output_text.background = true;
            output_text.backgroundColor = 0x001f0f;
            output_text.textColor = 0x44cc44;

            /* Setup client counter for badge. */
            client_count_fmt = new TextFormat();
            client_count_fmt.color = 0xFFFFFF;
            client_count_fmt.align = "center";
            client_count_fmt.font = "courier-new";
            client_count_fmt.bold = true;
            client_count_fmt.size = 10;
            client_count_tf = new TextField();
            client_count_tf.width = 20;
            client_count_tf.height = 17;
            client_count_tf.background = false;
            client_count_tf.defaultTextFormat = client_count_fmt;
            client_count_tf.x=47;
            client_count_tf.y=3;

            /* Update the client counter on badge. */
            update_client_count();

            if (RATE_LIMIT)
                rate_limit = new BucketRateLimit(RATE_LIMIT * RATE_LIMIT_HISTORY, RATE_LIMIT_HISTORY);
            else
                rate_limit = new RateUnlimit();

            puts("Starting.");
            // Wait until the query string parameters are loaded.
            this.loaderInfo.addEventListener(Event.COMPLETE, loaderinfo_complete);
        }

        private function loaderinfo_complete(e:Event):void
        {
            var fac_spec:String;

            puts("Parameters loaded.");

            if (this.loaderInfo.parameters["debug"])
                addChild(output_text);
            else {
                addChild(new BadgeImage());
                /* Tried unsuccessfully to add counter to badge. */
                /* For now, need two addChilds :( */
                addChild(client_count_tf);
            }

            fac_spec = this.loaderInfo.parameters["facilitator"];
            if (fac_spec) {
                puts("Facilitator spec: \"" + fac_spec + "\"");
                fac_addr = parse_addr_spec(fac_spec);
                if (!fac_addr) {
                    puts("Error: Facilitator spec must be in the form \"host:port\".");
                    return;
                }
            } else {
                fac_addr = DEFAULT_FACILITATOR_ADDR;
            }

            main();
        }

        /* The main logic begins here, after start-up issues are taken care of. */
        private function main():void
        {
            if (num_proxy_pairs >= MAX_NUM_PROXY_PAIRS) {
                setTimeout(main, FACILITATOR_POLL_INTERVAL);
                return;
            }

            s_f = new Socket();

            s_f.addEventListener(Event.CONNECT, fac_connected);
            s_f.addEventListener(Event.CLOSE, function (e:Event):void {
                puts("Facilitator: closed connection.");
                setTimeout(main, FACILITATOR_POLL_INTERVAL);
            });
            s_f.addEventListener(IOErrorEvent.IO_ERROR, function (e:IOErrorEvent):void {
                puts("Facilitator: I/O error: " + e.text + ".");
            });
            s_f.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function (e:SecurityErrorEvent):void {
                puts("Facilitator: security error: " + e.text + ".");
            });

            puts("Facilitator: connecting to " + fac_addr.host + ":" + fac_addr.port + ".");
            s_f.connect(fac_addr.host, fac_addr.port);
        }

        private function fac_connected(e:Event):void
        {
            puts("Facilitator: connected.");

            s_f.addEventListener(ProgressEvent.SOCKET_DATA, fac_data);

            s_f.writeUTFBytes("GET / HTTP/1.0\r\n\r\n");
        }

        private function fac_data(e:ProgressEvent):void
        {
            var client_spec:String;
            var client_addr:Object;
            var proxy_pair:Object;

            client_spec = s_f.readMultiByte(e.bytesLoaded, "utf-8");
            puts("Facilitator: got \"" + client_spec + "\".");

            client_addr = parse_addr_spec(client_spec);
            if (!client_addr) {
                puts("Error: Client spec must be in the form \"host:port\".");
                return;
            }

            num_proxy_pairs++;
            total_proxy_pairs++;
            proxy_pair = new ProxyPair(this, client_addr, DEFAULT_TOR_ADDR);
            proxy_pair.addEventListener(Event.COMPLETE, function(e:Event):void {
                proxy_pair.log("Complete.");
                num_proxy_pairs--;
            });
            proxy_pair.connect();

            /* Update the client count on the badge. */
            update_client_count();
        }

        /* Parse an address in the form "host:port". Returns an Object with
           keys "host" (String) and "port" (int). Returns null on error. */
        private static function parse_addr_spec(spec:String):Object
        {
            var parts:Array;
            var addr:Object;

            parts = spec.split(":", 2);
            if (parts.length != 2 || !parseInt(parts[1]))
                return null;
            addr = {}
            addr.host = parts[0];
            addr.port = parseInt(parts[1]);

            return addr;
        }
    }
}

import flash.display.Sprite;
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.IOErrorEvent;
import flash.events.ProgressEvent;
import flash.events.SecurityErrorEvent;
import flash.net.Socket;
import flash.utils.ByteArray;
import flash.utils.clearTimeout;
import flash.utils.getTimer;
import flash.utils.setTimeout;

class RateLimit
{
    public function RateLimit()
    {
    }

    public function update(n:Number):Boolean
    {
        return true;
    }

    public function when():Number
    {
        return 0.0;
    }

    public function is_limited():Boolean
    {
        return false;
    }
}

class RateUnlimit extends RateLimit
{
    public function RateUnlimit()
    {
    }

    public override function update(n:Number):Boolean
    {
        return true;
    }

    public override function when():Number
    {
        return 0.0;
    }

    public override function is_limited():Boolean
    {
        return false;
    }
}

class BucketRateLimit extends RateLimit
{
    private var amount:Number;
    private var capacity:Number;
    private var time:Number;
    private var last_update:uint;

    public function BucketRateLimit(capacity:Number, time:Number)
    {
        this.amount = 0.0;
        /* capacity / time is the rate we are aiming for. */
        this.capacity = capacity;
        this.time = time;
        this.last_update = getTimer();
    }

    private function age():void
    {
        var now:uint;
        var delta:Number;

        now = getTimer();
        delta = (now - last_update) / 1000.0;
        last_update = now;

        amount -= delta * capacity / time;
        if (amount < 0.0)
            amount = 0.0;
    }

    public override function update(n:Number):Boolean
    {
        age();
        amount += n;

        return amount <= capacity;
    }

    public override function when():Number
    {
        age();
        return (amount - capacity) / (capacity / time);
    }

    public override function is_limited():Boolean
    {
        age();
        return amount > capacity;
    }
}

/* An instance of a client-relay connection. */
class ProxyPair extends EventDispatcher
{
    // Address ({host, port}) of client.
    private var addr_c:Object;
    // Address ({host, port}) of relay.
    private var addr_r:Object;

    // Socket to client.
    private var s_c:Socket;
    // Socket to relay.
    private var s_r:Socket;

    // Parent swfcat, for UI updates and rate meter.
    private var ui:swfcat;

    // Pending byte read counts for relay and client sockets.
    private var r2c_schedule:Array;
    private var c2r_schedule:Array;
    // Callback id.
    private var flush_id:uint;

    public function log(msg:String):void
    {
        ui.puts(id() + ": " + msg)
    }

    // String describing this pair for output.
    public function id():String
    {
        return "<" + this.addr_c.host + ":" + this.addr_c.port +
            "," + this.addr_r.host + ":" + this.addr_r.port + ">";
    }

    public function ProxyPair(ui:swfcat, addr_c:Object, addr_r:Object)
    {
        this.ui = ui;
        this.addr_c = addr_c;
        this.addr_r = addr_r;

        this.c2r_schedule = [];
        this.r2c_schedule = [];
    }

    public function connect():void
    {
        s_r = new Socket();

        s_r.addEventListener(Event.CONNECT, tor_connected);
        s_r.addEventListener(Event.CLOSE, function (e:Event):void {
            log("Tor: closed.");
            if (s_c.connected)
                s_c.close();
            dispatchEvent(new Event(Event.COMPLETE));
        });
        s_r.addEventListener(IOErrorEvent.IO_ERROR, function (e:IOErrorEvent):void {
            log("Tor: I/O error: " + e.text + ".");
            if (s_c.connected)
                s_c.close();
            dispatchEvent(new Event(Event.COMPLETE));
        });
        s_r.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function (e:SecurityErrorEvent):void {
            log("Tor: security error: " + e.text + ".");
            if (s_c.connected)
                s_c.close();
            dispatchEvent(new Event(Event.COMPLETE));
        });
        s_r.addEventListener(ProgressEvent.SOCKET_DATA, relay_to_client);

        log("Tor: connecting to " + addr_r.host + ":" + addr_r.port + ".");
        s_r.connect(addr_r.host, addr_r.port);
    }

    private function tor_connected(e:Event):void
    {
        log("Tor: connected.");

        s_c = new Socket();

        s_c.addEventListener(Event.CONNECT, client_connected);
        s_c.addEventListener(Event.CLOSE, function (e:Event):void {
            log("Client: closed.");
            if (s_r.connected)
                s_r.close();
            dispatchEvent(new Event(Event.COMPLETE));
        });
        s_c.addEventListener(IOErrorEvent.IO_ERROR, function (e:IOErrorEvent):void {
            log("Client: I/O error: " + e.text + ".");
            if (s_r.connected)
                s_r.close();
            dispatchEvent(new Event(Event.COMPLETE));
        });
        s_c.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function (e:SecurityErrorEvent):void {
            log("Client: security error: " + e.text + ".");
            if (s_r.connected)
                s_r.close();
            dispatchEvent(new Event(Event.COMPLETE));
        });
        s_c.addEventListener(ProgressEvent.SOCKET_DATA, client_to_relay);

        log("Client: connecting to " + addr_c.host + ":" + addr_c.port + ".");
        s_c.connect(addr_c.host, addr_c.port);
    }

    private function relay_to_client(e:ProgressEvent):void
    {
        r2c_schedule.push(e.bytesLoaded);
        flush();
    }

    private function client_to_relay(e:ProgressEvent):void
    {
        c2r_schedule.push(e.bytesLoaded);
        flush();
    }

    private function client_connected(e:Event):void
    {
        log("Client: connected.");
    }

    private function transfer_chunk(s_from:Socket, s_to:Socket, n:uint,
        label:String):void
    {
        var bytes:ByteArray;

        bytes = new ByteArray();
        s_from.readBytes(bytes, 0, n);
        s_to.writeBytes(bytes);
        ui.rate_limit.update(n);
        log(label + ": read " + bytes.length + ".");
    }

    /* Send as much data as the rate limit currently allows. */
    private function flush():void
    {
        if (flush_id)
            clearTimeout(flush_id);
        flush_id = undefined;

        if (!(s_r.connected && s_c.connected))
            /* Can't do anything until both sockets are connected. */
            return;

        while (!ui.rate_limit.is_limited() &&
               (r2c_schedule.length > 0 || c2r_schedule.length > 0)) {
            if (r2c_schedule.length > 0)
                transfer_chunk(s_r, s_c, r2c_schedule.shift(), "Tor");
            if (c2r_schedule.length > 0)
                transfer_chunk(s_c, s_r, c2r_schedule.shift(), "Client");
        }

        /* Call again when safe, if necessary. */
        if (r2c_schedule.length > 0 || c2r_schedule.length > 0)
            flush_id = setTimeout(flush, ui.rate_limit.when() * 1000);
    }
}

Add a RateUnlimit implementation of RateLimit.