755 lines
22 KiB
ActionScript
755 lines
22 KiB
ActionScript
/*
|
|
* Copyright (c) 2009-2010 Digital Bazaar, Inc. All rights reserved.
|
|
*
|
|
* @author Dave Longley
|
|
*/
|
|
package
|
|
{
|
|
import flash.display.Sprite;
|
|
|
|
/**
|
|
* A SocketPool is a flash object that can be embedded in a web page to
|
|
* allow javascript access to pools of Sockets.
|
|
*
|
|
* Javascript can create a pool and then as many Sockets as it desires. Each
|
|
* Socket will be assigned a unique ID that allows continued javascript
|
|
* access to it. There is no limit on the number of persistent socket
|
|
* connections.
|
|
*/
|
|
public class SocketPool extends Sprite
|
|
{
|
|
import flash.events.Event;
|
|
import flash.events.EventDispatcher;
|
|
import flash.errors.IOError;
|
|
import flash.events.IOErrorEvent;
|
|
import flash.events.ProgressEvent;
|
|
import flash.events.SecurityErrorEvent;
|
|
import flash.events.TextEvent;
|
|
import flash.external.ExternalInterface;
|
|
import flash.net.SharedObject;
|
|
import flash.system.Security;
|
|
import flash.utils.ByteArray;
|
|
import mx.utils.Base64Decoder;
|
|
import mx.utils.Base64Encoder;
|
|
|
|
// a map of ID to Socket
|
|
private var mSocketMap:Object;
|
|
|
|
// a counter for Socket IDs (Note: assumes there will be no overflow)
|
|
private var mNextId:uint;
|
|
|
|
// an event dispatcher for sending events to javascript
|
|
private var mEventDispatcher:EventDispatcher;
|
|
|
|
/**
|
|
* Creates a new, unitialized SocketPool.
|
|
*
|
|
* @throws Error - if no external interface is available to provide
|
|
* javascript access.
|
|
*/
|
|
public function SocketPool()
|
|
{
|
|
if(!ExternalInterface.available)
|
|
{
|
|
trace("ExternalInterface is not available");
|
|
throw new Error(
|
|
"Flash's ExternalInterface is not available. This is a " +
|
|
"requirement of SocketPool and therefore, it will be " +
|
|
"unavailable.");
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
// set up javascript access:
|
|
|
|
// initializes/cleans up the SocketPool
|
|
ExternalInterface.addCallback("init", init);
|
|
ExternalInterface.addCallback("cleanup", cleanup);
|
|
|
|
// creates/destroys a socket
|
|
ExternalInterface.addCallback("create", create);
|
|
ExternalInterface.addCallback("destroy", destroy);
|
|
|
|
// connects/closes a socket
|
|
ExternalInterface.addCallback("connect", connect);
|
|
ExternalInterface.addCallback("close", close);
|
|
|
|
// checks for a connection
|
|
ExternalInterface.addCallback("isConnected", isConnected);
|
|
|
|
// sends/receives data over the socket
|
|
ExternalInterface.addCallback("send", send);
|
|
ExternalInterface.addCallback("receive", receive);
|
|
|
|
// gets the number of bytes available on a socket
|
|
ExternalInterface.addCallback(
|
|
"getBytesAvailable", getBytesAvailable);
|
|
|
|
// add a callback for subscribing to socket events
|
|
ExternalInterface.addCallback("subscribe", subscribe);
|
|
|
|
// add callbacks for deflate/inflate
|
|
ExternalInterface.addCallback("deflate", deflate);
|
|
ExternalInterface.addCallback("inflate", inflate);
|
|
|
|
// add callbacks for local disk storage
|
|
ExternalInterface.addCallback("setItem", setItem);
|
|
ExternalInterface.addCallback("getItem", getItem);
|
|
ExternalInterface.addCallback("removeItem", removeItem);
|
|
ExternalInterface.addCallback("clearItems", clearItems);
|
|
|
|
// socket pool is now ready
|
|
ExternalInterface.call("window.forge.socketPool.ready");
|
|
}
|
|
catch(e:Error)
|
|
{
|
|
log("error=" + e.errorID + "," + e.name + "," + e.message);
|
|
throw e;
|
|
}
|
|
|
|
log("ready");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A debug logging function.
|
|
*
|
|
* @param obj the string or error to log.
|
|
*/
|
|
CONFIG::debugging
|
|
private function log(obj:Object):void
|
|
{
|
|
if(obj is String)
|
|
{
|
|
var str:String = obj as String;
|
|
ExternalInterface.call("console.log", "SocketPool", str);
|
|
}
|
|
else if(obj is Error)
|
|
{
|
|
var e:Error = obj as Error;
|
|
log("error=" + e.errorID + "," + e.name + "," + e.message);
|
|
}
|
|
}
|
|
|
|
CONFIG::release
|
|
private function log(obj:Object):void
|
|
{
|
|
// log nothing in release mode
|
|
}
|
|
|
|
/**
|
|
* Called by javascript to initialize this SocketPool.
|
|
*
|
|
* @param options:
|
|
* marshallExceptions: true to pass exceptions to and from
|
|
* javascript.
|
|
*/
|
|
private function init(... args):void
|
|
{
|
|
log("init()");
|
|
|
|
// get options from first argument
|
|
var options:Object = args.length > 0 ? args[0] : null;
|
|
|
|
// create socket map, set next ID, and create event dispatcher
|
|
mSocketMap = new Object();
|
|
mNextId = 1;
|
|
mEventDispatcher = new EventDispatcher();
|
|
|
|
// enable marshalling exceptions if appropriate
|
|
if(options != null &&
|
|
"marshallExceptions" in options &&
|
|
options.marshallExceptions === true)
|
|
{
|
|
try
|
|
{
|
|
// Note: setting marshallExceptions in IE, even inside of a
|
|
// try-catch block will terminate flash. Don't set this on IE.
|
|
ExternalInterface.marshallExceptions = true;
|
|
}
|
|
catch(e:Error)
|
|
{
|
|
log(e);
|
|
}
|
|
}
|
|
|
|
log("init() done");
|
|
}
|
|
|
|
/**
|
|
* Called by javascript to clean up a SocketPool.
|
|
*/
|
|
private function cleanup():void
|
|
{
|
|
log("cleanup()");
|
|
|
|
mSocketMap = null;
|
|
mNextId = 1;
|
|
mEventDispatcher = null;
|
|
|
|
log("cleanup() done");
|
|
}
|
|
|
|
/**
|
|
* Handles events.
|
|
*
|
|
* @param e the event to handle.
|
|
*/
|
|
private function handleEvent(e:Event):void
|
|
{
|
|
// dispatch socket event
|
|
var message:String = (e is TextEvent) ? (e as TextEvent).text : null;
|
|
mEventDispatcher.dispatchEvent(
|
|
new SocketEvent(e.type, e.target as PooledSocket, message));
|
|
}
|
|
|
|
/**
|
|
* Called by javascript to create a Socket.
|
|
*
|
|
* @return the Socket ID.
|
|
*/
|
|
private function create():String
|
|
{
|
|
log("create()");
|
|
|
|
// create a Socket
|
|
var id:String = "" + mNextId++;
|
|
var s:PooledSocket = new PooledSocket();
|
|
s.id = id;
|
|
s.addEventListener(Event.CONNECT, handleEvent);
|
|
s.addEventListener(Event.CLOSE, handleEvent);
|
|
s.addEventListener(ProgressEvent.SOCKET_DATA, handleEvent);
|
|
s.addEventListener(IOErrorEvent.IO_ERROR, handleEvent);
|
|
s.addEventListener(SecurityErrorEvent.SECURITY_ERROR, handleEvent);
|
|
mSocketMap[id] = s;
|
|
|
|
log("socket " + id + " created");
|
|
log("create() done");
|
|
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* Called by javascript to clean up a Socket.
|
|
*
|
|
* @param id the ID of the Socket to clean up.
|
|
*/
|
|
private function destroy(id:String):void
|
|
{
|
|
log("destroy(" + id + ")");
|
|
|
|
if(id in mSocketMap)
|
|
{
|
|
// remove Socket
|
|
delete mSocketMap[id];
|
|
log("socket " + id + " destroyed");
|
|
}
|
|
|
|
log("destroy(" + id + ") done");
|
|
}
|
|
|
|
/**
|
|
* Connects the Socket with the given ID to the given host and port,
|
|
* using the given socket policy port.
|
|
*
|
|
* @param id the ID of the Socket.
|
|
* @param host the host to connect to.
|
|
* @param port the port to connect to.
|
|
* @param spPort the security policy port to use, 0 to use a url.
|
|
* @param spUrl the http URL to the policy file to use, null for default.
|
|
*/
|
|
private function connect(
|
|
id:String, host:String, port:uint, spPort:uint,
|
|
spUrl:String = null):void
|
|
{
|
|
log("connect(" +
|
|
id + "," + host + "," + port + "," + spPort + "," + spUrl + ")");
|
|
|
|
if(id in mSocketMap)
|
|
{
|
|
// get the Socket
|
|
var s:PooledSocket = mSocketMap[id];
|
|
|
|
// load socket policy file
|
|
// (permits socket access to backend)
|
|
if(spPort !== 0)
|
|
{
|
|
spUrl = "xmlsocket://" + host + ":" + spPort;
|
|
log("using cross-domain url: " + spUrl);
|
|
Security.loadPolicyFile(spUrl);
|
|
}
|
|
else if(spUrl !== null && typeof(spUrl) !== undefined)
|
|
{
|
|
log("using cross-domain url: " + spUrl);
|
|
Security.loadPolicyFile(spUrl);
|
|
}
|
|
else
|
|
{
|
|
log("not loading any cross-domain url");
|
|
}
|
|
|
|
// connect
|
|
s.connect(host, port);
|
|
}
|
|
else
|
|
{
|
|
// no such socket
|
|
log("socket " + id + " does not exist");
|
|
}
|
|
|
|
log("connect(" + id + ") done");
|
|
}
|
|
|
|
/**
|
|
* Closes the Socket with the given ID.
|
|
*
|
|
* @param id the ID of the Socket.
|
|
*/
|
|
private function close(id:String):void
|
|
{
|
|
log("close(" + id + ")");
|
|
|
|
if(id in mSocketMap)
|
|
{
|
|
// close the Socket
|
|
var s:PooledSocket = mSocketMap[id];
|
|
if(s.connected)
|
|
{
|
|
s.close();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// no such socket
|
|
log("socket " + id + " does not exist");
|
|
}
|
|
|
|
log("close(" + id + ") done");
|
|
}
|
|
|
|
/**
|
|
* Determines if the Socket with the given ID is connected or not.
|
|
*
|
|
* @param id the ID of the Socket.
|
|
*
|
|
* @return true if the socket is connected, false if not.
|
|
*/
|
|
private function isConnected(id:String):Boolean
|
|
{
|
|
var rval:Boolean = false;
|
|
log("isConnected(" + id + ")");
|
|
|
|
if(id in mSocketMap)
|
|
{
|
|
// check the Socket
|
|
var s:PooledSocket = mSocketMap[id];
|
|
rval = s.connected;
|
|
}
|
|
else
|
|
{
|
|
// no such socket
|
|
log("socket " + id + " does not exist");
|
|
}
|
|
|
|
log("isConnected(" + id + ") done");
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Writes bytes to a Socket.
|
|
*
|
|
* @param id the ID of the Socket.
|
|
* @param bytes the string of base64-encoded bytes to write.
|
|
*
|
|
* @return true on success, false on failure.
|
|
*/
|
|
private function send(id:String, bytes:String):Boolean
|
|
{
|
|
var rval:Boolean = false;
|
|
log("send(" + id + ")");
|
|
|
|
if(id in mSocketMap)
|
|
{
|
|
// write bytes to socket
|
|
var s:PooledSocket = mSocketMap[id];
|
|
try
|
|
{
|
|
var b64:Base64Decoder = new Base64Decoder();
|
|
b64.decode(bytes);
|
|
var b:ByteArray = b64.toByteArray();
|
|
s.writeBytes(b, 0, b.length);
|
|
s.flush();
|
|
rval = true;
|
|
}
|
|
catch(e:IOError)
|
|
{
|
|
log(e);
|
|
|
|
// dispatch IO error event
|
|
mEventDispatcher.dispatchEvent(new SocketEvent(
|
|
IOErrorEvent.IO_ERROR, s, e.message));
|
|
if(s.connected)
|
|
{
|
|
s.close();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// no such socket
|
|
log("socket " + id + " does not exist");
|
|
}
|
|
|
|
log("send(" + id + ") done");
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Receives bytes from a Socket.
|
|
*
|
|
* @param id the ID of the Socket.
|
|
* @param count the maximum number of bytes to receive.
|
|
*
|
|
* @return an object with 'rval' set to the received bytes,
|
|
* base64-encoded, or set to null on error.
|
|
*/
|
|
private function receive(id:String, count:uint):Object
|
|
{
|
|
var rval:String = null;
|
|
log("receive(" + id + "," + count + ")");
|
|
|
|
if(id in mSocketMap)
|
|
{
|
|
// only read what is available
|
|
var s:PooledSocket = mSocketMap[id];
|
|
if(count > s.bytesAvailable)
|
|
{
|
|
count = s.bytesAvailable;
|
|
}
|
|
|
|
try
|
|
{
|
|
// read bytes from socket
|
|
var b:ByteArray = new ByteArray();
|
|
s.readBytes(b, 0, count);
|
|
b.position = 0;
|
|
var b64:Base64Encoder = new Base64Encoder();
|
|
b64.insertNewLines = false;
|
|
b64.encodeBytes(b, 0, b.length);
|
|
rval = b64.toString();
|
|
}
|
|
catch(e:IOError)
|
|
{
|
|
log(e);
|
|
|
|
// dispatch IO error event
|
|
mEventDispatcher.dispatchEvent(new SocketEvent(
|
|
IOErrorEvent.IO_ERROR, s, e.message));
|
|
if(s.connected)
|
|
{
|
|
s.close();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// no such socket
|
|
log("socket " + id + " does not exist");
|
|
}
|
|
|
|
log("receive(" + id + "," + count + ") done");
|
|
return {rval: rval};
|
|
}
|
|
|
|
/**
|
|
* Gets the number of bytes available from a Socket.
|
|
*
|
|
* @param id the ID of the Socket.
|
|
*
|
|
* @return the number of available bytes.
|
|
*/
|
|
private function getBytesAvailable(id:String):uint
|
|
{
|
|
var rval:uint = 0;
|
|
log("getBytesAvailable(" + id + ")");
|
|
|
|
if(id in mSocketMap)
|
|
{
|
|
var s:PooledSocket = mSocketMap[id];
|
|
rval = s.bytesAvailable;
|
|
}
|
|
else
|
|
{
|
|
// no such socket
|
|
log("socket " + id + " does not exist");
|
|
}
|
|
|
|
log("getBytesAvailable(" + id +") done");
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Registers a javascript function as a callback for an event.
|
|
*
|
|
* @param eventType the type of event (socket event types).
|
|
* @param callback the name of the callback function.
|
|
*/
|
|
private function subscribe(eventType:String, callback:String):void
|
|
{
|
|
log("subscribe(" + eventType + "," + callback + ")");
|
|
|
|
switch(eventType)
|
|
{
|
|
case Event.CONNECT:
|
|
case Event.CLOSE:
|
|
case IOErrorEvent.IO_ERROR:
|
|
case SecurityErrorEvent.SECURITY_ERROR:
|
|
case ProgressEvent.SOCKET_DATA:
|
|
{
|
|
log(eventType + " => " + callback);
|
|
mEventDispatcher.addEventListener(
|
|
eventType, function(event:SocketEvent):void
|
|
{
|
|
log("event dispatched: " + eventType);
|
|
|
|
// build event for javascript
|
|
var e:Object = new Object();
|
|
e.id = event.socket ? event.socket.id : 0;
|
|
e.type = eventType;
|
|
if(event.socket && event.socket.connected)
|
|
{
|
|
e.bytesAvailable = event.socket.bytesAvailable;
|
|
}
|
|
else
|
|
{
|
|
e.bytesAvailable = 0;
|
|
}
|
|
if(event.message)
|
|
{
|
|
e.message = event.message;
|
|
}
|
|
|
|
// send event to javascript
|
|
ExternalInterface.call(callback, e);
|
|
});
|
|
break;
|
|
}
|
|
default:
|
|
throw new ArgumentError(
|
|
"Could not subscribe to event. " +
|
|
"Invalid event type specified: " + eventType);
|
|
}
|
|
|
|
log("subscribe(" + eventType + "," + callback + ") done");
|
|
}
|
|
|
|
/**
|
|
* Deflates the given data.
|
|
*
|
|
* @param data the base64-encoded data to deflate.
|
|
*
|
|
* @return an object with 'rval' set to deflated data, base64-encoded.
|
|
*/
|
|
private function deflate(data:String):Object
|
|
{
|
|
log("deflate");
|
|
|
|
var b64d:Base64Decoder = new Base64Decoder();
|
|
b64d.decode(data);
|
|
var b:ByteArray = b64d.toByteArray();
|
|
b.compress();
|
|
b.position = 0;
|
|
var b64e:Base64Encoder = new Base64Encoder();
|
|
b64e.insertNewLines = false;
|
|
b64e.encodeBytes(b, 0, b.length);
|
|
|
|
log("deflate done");
|
|
return {rval: b64e.toString()};
|
|
}
|
|
|
|
/**
|
|
* Inflates the given data.
|
|
*
|
|
* @param data the base64-encoded data to inflate.
|
|
*
|
|
* @return an object with 'rval' set to the inflated data,
|
|
* base64-encoded, null on error.
|
|
*/
|
|
private function inflate(data:String):Object
|
|
{
|
|
log("inflate");
|
|
var rval:Object = {rval: null};
|
|
|
|
try
|
|
{
|
|
var b64d:Base64Decoder = new Base64Decoder();
|
|
b64d.decode(data);
|
|
var b:ByteArray = b64d.toByteArray();
|
|
b.uncompress();
|
|
b.position = 0;
|
|
var b64e:Base64Encoder = new Base64Encoder();
|
|
b64e.insertNewLines = false;
|
|
b64e.encodeBytes(b, 0, b.length);
|
|
rval.rval = b64e.toString();
|
|
}
|
|
catch(e:Error)
|
|
{
|
|
log(e);
|
|
rval.error = {
|
|
id: e.errorID,
|
|
name: e.name,
|
|
message: e.message
|
|
};
|
|
}
|
|
|
|
log("inflate done");
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Stores an item with a key and arbitrary base64-encoded data on local
|
|
* disk.
|
|
*
|
|
* @param key the key for the item.
|
|
* @param data the base64-encoded item data.
|
|
* @param storeId the storage ID to use, defaults to "forge.storage".
|
|
*
|
|
* @return an object with rval set to true on success, false on failure
|
|
* with error included.
|
|
*/
|
|
private function setItem(
|
|
key:String, data:String, storeId:String = "forge.storage"):Object
|
|
{
|
|
var rval:Object = {rval: false};
|
|
try
|
|
{
|
|
var store:SharedObject = SharedObject.getLocal(storeId);
|
|
if(!('keys' in store.data))
|
|
{
|
|
store.data.keys = {};
|
|
}
|
|
store.data.keys[key] = data;
|
|
store.flush();
|
|
rval.rval = true;
|
|
}
|
|
catch(e:Error)
|
|
{
|
|
log(e);
|
|
rval.error = {
|
|
id: e.errorID,
|
|
name: e.name,
|
|
message: e.message
|
|
};
|
|
}
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Gets an item from the local disk.
|
|
*
|
|
* @param key the key for the item.
|
|
* @param storeId the storage ID to use, defaults to "forge.storage".
|
|
*
|
|
* @return an object with rval set to the item data (which may be null),
|
|
* check for error object if null.
|
|
*/
|
|
private function getItem(
|
|
key:String, storeId:String = "forge.storage"):Object
|
|
{
|
|
var rval:Object = {rval: null};
|
|
try
|
|
{
|
|
var store:SharedObject = SharedObject.getLocal(storeId);
|
|
if('keys' in store.data && key in store.data.keys)
|
|
{
|
|
rval.rval = store.data.keys[key];
|
|
}
|
|
}
|
|
catch(e:Error)
|
|
{
|
|
log(e);
|
|
rval.error = {
|
|
id: e.errorID,
|
|
name: e.name,
|
|
message: e.message
|
|
};
|
|
}
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Removes an item from the local disk.
|
|
*
|
|
* @param key the key for the item.
|
|
* @param storeId the storage ID to use, defaults to "forge.storage".
|
|
*
|
|
* @return an object with rval set to true if removed, false if not.
|
|
*/
|
|
private function removeItem(
|
|
key:String, storeId:String = "forge.storage"):Object
|
|
{
|
|
var rval:Object = {rval: false};
|
|
try
|
|
{
|
|
var store:SharedObject = SharedObject.getLocal(storeId);
|
|
if('keys' in store.data && key in store.data.keys)
|
|
{
|
|
delete store.data.keys[key];
|
|
|
|
// clean up storage entirely if empty
|
|
var empty:Boolean = true;
|
|
for(var prop:String in store.data.keys)
|
|
{
|
|
empty = false;
|
|
break;
|
|
}
|
|
if(empty)
|
|
{
|
|
store.clear();
|
|
}
|
|
rval.rval = true;
|
|
}
|
|
}
|
|
catch(e:Error)
|
|
{
|
|
log(e);
|
|
rval.error = {
|
|
id: e.errorID,
|
|
name: e.name,
|
|
message: e.message
|
|
};
|
|
}
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Clears an entire store of all of its items.
|
|
*
|
|
* @param storeId the storage ID to use, defaults to "forge.storage".
|
|
*
|
|
* @return an object with rval set to true if cleared, false if not.
|
|
*/
|
|
private function clearItems(storeId:String = "forge.storage"):Object
|
|
{
|
|
var rval:Object = {rval: false};
|
|
try
|
|
{
|
|
var store:SharedObject = SharedObject.getLocal(storeId);
|
|
store.clear();
|
|
rval.rval = true;
|
|
}
|
|
catch(e:Error)
|
|
{
|
|
log(e);
|
|
rval.error = {
|
|
id: e.errorID,
|
|
name: e.name,
|
|
message: e.message
|
|
};
|
|
}
|
|
return rval;
|
|
}
|
|
}
|
|
}
|