Author: johnh
Date: Tue May 3 02:41:03 2011
New Revision: 1098895
URL: http://svn.apache.org/viewvc?rev=1098895&view=rev
Log:
Ensure that Flash works appropriately in all timing scenarios. Add
initialization handshake to ensure that messages sent can be received.
Modified:
shindig/trunk/content/xpc.swf
shindig/trunk/features/src/main/flex/Main.as
shindig/trunk/features/src/main/javascript/features/rpc/flash.transport.js
shindig/trunk/features/src/main/javascript/features/rpc/rpc.js
Modified: shindig/trunk/content/xpc.swf
URL:
http://svn.apache.org/viewvc/shindig/trunk/content/xpc.swf?rev=1098895&r1=1098894&r2=1098895&view=diff
==============================================================================
Files shindig/trunk/content/xpc.swf (original) and
shindig/trunk/content/xpc.swf Tue May 3 02:41:03 2011 differ
Modified: shindig/trunk/features/src/main/flex/Main.as
URL:
http://svn.apache.org/viewvc/shindig/trunk/features/src/main/flex/Main.as?rev=1098895&r1=1098894&r2=1098895&view=diff
==============================================================================
--- shindig/trunk/features/src/main/flex/Main.as (original)
+++ shindig/trunk/features/src/main/flex/Main.as Tue May 3 02:41:03 2011
@@ -24,8 +24,21 @@ import System.security;
* Original design by [email protected] (Eduardo Vela)
*/
class Main {
+ // Ensures that the callbacks installed by the SWF are installed only once
per page context.
private static var SINGLETON:Boolean = false;
+ // Constructor: unused in this case.
+ public function Main() {
+ }
+
+ /**
+ * Simple helper function to replace instances of the given from_str in the
provided
+ * first argument with the given to_str string.
+ * @param str {String} String whose contents to replace.
+ * @param from_str {String} Token to replace in str.
+ * @param to_str {String} String to put in place of from_str in str.
+ * @returns Modified string.
+ */
public static function replace(str:String, from_str:String,
to_str:String):String {
var out_str:String = "";
var search_ix:Number = 0;
@@ -46,6 +59,11 @@ class Main {
return replace(str, "\\", "\\\\");
}
+ /**
+ * Removes the port piece of an assumed well-structured provided host:port
pair.
+ * @param {String} str String representing a URI authority.
+ * @returns The host-only (minus port) piece of the inbound authority String.
+ */
public static function stripPortIfPresent(str:String):String {
var col_ix:Number = str.indexOf(":");
if (col_ix == -1) {
@@ -54,6 +72,32 @@ class Main {
return str.substring(0, col_ix);
}
+ /**
+ * Implementation of handlers facilitating cross-domain communication through
+ * Flash's ExternalInterface and LocalConnection facilities, offering sender
+ * domain verification.
+ *
+ * This method may only be run once such that it has any effect, a fact
enforced
+ * by a static SINGLETON boolean. This prevents confusion if the SWF is
accidentally
+ * loaded more than once in a given Window or page.
+ *
+ * The method whitelists HTTP-to-SWF access only for the domain provided in
the
+ * inbound 'origin' argument. This argument in turn is passed along in each
+ * call made that passes along cross-domain messages on the caller's behalf,
ensuring
+ * that domain verification works as intended.
+ *
+ * It installs a single 'setup' handler, callable from JS, which in turn
sets up
+ * a sendMessage one-way communication method, while starting to listen to
+ * an equivalent channel set up by a party on the opposite side of an IFRAME
+ * boundary. A child context should pass in "INNER" for the role argument to
setup;
+ * the parent passes "OUTER". rpc_key is a unique key provided on the IFRAME
URL's
+ * hash disambiguating it from all other IFRAMEs on-page, window, or
machine, while
+ * channel_id is the child's ID.
+ *
+ * This SWF is written specifically for the gadgets.rpc cross-domain
communication
+ * library, but could be rather easily adapted - with passed-in callback
method
+ * names, for instance - to use by other libraries as well.
+ */
public static function main(swfRoot:MovieClip):Void {
var escFn:Function = esc;
var replaceFn:Function = replace;
@@ -64,20 +108,30 @@ class Main {
var my_origin:String;
if (_level0.origin == undefined){
+ // No origin: accept from all HTTP callers.
+ // Domain verification will not apply.
my_origin = "http://*";
} else {
+ // Get origin from the query string.
my_origin = _level0.origin;
}
+ // Flash doesn't accept/honor ports, so we strip one if present
+ // for canonicalization.
var domain:String = stripPortIfPresent(
my_origin.substr(my_origin.indexOf("//") + 2, my_origin.length));
- if (my_origin.substr(0,5)==="http:") {
+ // Whitelist access to this SWF for the sending HTTP domain.
+ // The my_origin field from which domain derives is passed along
+ // with each sent message. Together, these ensure domain verification
+ // of all sent messages is possible.
+ if (my_origin.substr(0,5) === "http:") {
security.allowInsecureDomain(domain);
} else {
security.allowDomain(domain);
}
+ // Install global communication channel setup method.
ExternalInterface.addCallback("setup", { },
function(rpc_key:String, channel_id:String, role:String) {
var other_role:String;
@@ -91,11 +145,21 @@ class Main {
var receiving_lc:LocalConnection = new LocalConnection();
var sending_lc:LocalConnection = new LocalConnection();
+ var lastSendingDomain:String;
+ // Allow messages to be sent from any other SWF to this channel.
+ // Message verification itself is handled by both the fact that the
+ // channel ID contains the rpc_token as well as the passed message
+ // contents themselves, containing the token too. The SWF is
+ // largely a simple relay.
+ receiving_lc.allowDomain = function(sendingDomain:String) {
+ lastSendingDomain = sendingDomain;
+ return true;
+ };
receiving_lc.receiveMessage =
function(to_origin:String, from_origin:String, in_rpc_key:String,
message:String) {
if ((to_origin === "*" || to_origin === my_origin) && (in_rpc_key ==
rpc_key)) {
ExternalInterface.call("gadgets.rpctx.flash._receiveMessage",
- escFn(message), escFn(from_origin), escFn(to_origin));
+ escFn(message), escFn(from_origin), escFn(to_origin),
escFn(lastSendingDomain));
}
};
@@ -103,16 +167,22 @@ class Main {
{ }, function(message:String, to_origin:String) {
if (!to_origin) to_origin = "*";
var sendId:String =
- replaceFn("channel_" + channel_id + "_" + rpc_key + "_" +
other_role, ":", "");
+ replaceFn("_channel_" + channel_id + "_" + rpc_key + "_" +
other_role, ":", "");
sending_lc.send(sendId,
"receiveMessage", to_origin, my_origin, rpc_key, message);
});
- var recvId:String = replaceFn("channel_" + channel_id + "_" + rpc_key +
"_" + role, ":", "");
+ var recvId:String = replaceFn("_channel_" + channel_id + "_" + rpc_key +
"_" + role, ":", "");
receiving_lc.connect(recvId);
+ if (role == "INNER") {
+ // In child context, trigger notice that the setup method is complete.
+ // This in turn initiates a child-to-parent polling procedure to
complete a bidirectional
+ // communication handshake, since otherwise meaningful messages could
be passed and dropped
+ // before the receiving end was ready.
+ ExternalInterface.call("gadgets.rpctx.flash._setupDone");
+ }
});
+
+ // Signal completion of the setup callback to calling-context JS for
proper ordering.
ExternalInterface.call("gadgets.rpctx.flash._ready");
}
-
- public function Main() {
- }
}
Modified:
shindig/trunk/features/src/main/javascript/features/rpc/flash.transport.js
URL:
http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/rpc/flash.transport.js?rev=1098895&r1=1098894&r2=1098895&view=diff
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/rpc/flash.transport.js
(original)
+++ shindig/trunk/features/src/main/javascript/features/rpc/flash.transport.js
Tue May 3 02:41:03 2011
@@ -41,11 +41,18 @@ if (!gadgets.rpctx.flash) { // make lib
var relayHandle = null;
var LOADER_TIMEOUT_MS = 100;
- var MAX_LOADER_RETRIES = 100;
+ var MAX_LOADER_RETRIES = 50;
var pendingHandshakes = [];
var setupHandle = null;
var setupAttempts = 0;
+ var SWF_CHANNEL_READY = "_scr";
+ var READY_TIMEOUT_MS = 100;
+ var MAX_READY_RETRIES = 50;
+ var readyAttempts = 0;
+ var readyHandle = null;
+ var readyMsgs = {};
+
var myLoc = window.location.protocol + "//" + window.location.host;
function getChannelId(receiverId) {
@@ -65,7 +72,7 @@ if (!gadgets.rpctx.flash) { // make lib
function relayLoader() {
if (relayHandle === null && document.body && swfUrl) {
- var theSwf = swfUrl + '?origin=' + myLoc;
+ var theSwf = swfUrl + '?cb=' + Math.random() + '&origin=' + myLoc;
var containerDiv = document.createElement('div');
containerDiv.style.height = '1px';
@@ -87,6 +94,29 @@ if (!gadgets.rpctx.flash) { // make lib
if (setupHandle !== null &&
(relayHandle !== null || setupAttempts >= MAX_LOADER_RETRIES)) {
window.clearTimeout(setupHandle);
+ } else {
+ // Either document.body doesn't yet exist or config doesn't.
+ // In either case the relay handle isn't set up properly yet, and
+ // so should be retried.
+ setupHandle = window.setTimeout(relayLoader, LOADER_TIMEOUT_MS);
+ }
+ }
+
+ function childReadyPoller() {
+ // Attempt sending a message to parent indicating that child is ready
+ // to receive messages. This only occurs after the SWF indicates that
+ // its setup() method has been successfully called and completed, and
+ // only in child context.
+ if (readyMsgs[".."]) return;
+ sendChannelReady("..");
+ readyAttempts++;
+ if (readyAttempts >= MAX_READY_RETRIES && readyHandle !== null) {
+ window.clearTimeout(readyHandle);
+ readyHandle = null;
+ } else {
+ // Try again later. The handle will be cleared during receipt of
+ // the setup ACK.
+ readyHandle = window.setTimeout(childReadyPoller, READY_TIMEOUT_MS);
}
}
@@ -96,11 +126,26 @@ if (!gadgets.rpctx.flash) { // make lib
var shake = pendingHandshakes.shift();
var targetId = shake.targetId;
relayHandle['setup'](shake.token, getChannelId(targetId),
getRoleId(targetId));
- ready(shake.targetId, true);
}
}
}
+ function call(targetId, from, rpc) {
+ var targetOrigin = gadgets.rpc.getTargetOrigin(targetId);
+ var rpcKey = gadgets.rpc.getAuthToken(targetId);
+ var handleKey = "sendMessage_" + getChannelId(targetId) + "_" + rpcKey +
"_" + getRoleId(targetId);
+ var messageHandler = relayHandle[handleKey];
+ messageHandler.call(relayHandle, gadgets.json.stringify(rpc),
targetOrigin);
+ return true;
+ }
+
+ function sendChannelReady(receiverId) {
+ var myId = gadgets.rpc.RPC_ID;
+ var readyAck = {};
+ readyAck[SWF_CHANNEL_READY] = myId;
+ call(receiverId, myId, readyAck);
+ }
+
return {
// "core" transport methods
getCode: function() {
@@ -135,18 +180,28 @@ if (!gadgets.rpctx.flash) { // make lib
return true;
},
- call: function(targetId, from, rpc) {
- var targetOrigin = gadgets.rpc.getTargetOrigin(targetId);
- var rpcKey = gadgets.rpc.getAuthToken(targetId);
- var handleKey = "sendMessage_" + getChannelId(targetId) + "_" + rpcKey
+ "_" + getRoleId(targetId);
- var messageHandler = relayHandle[handleKey];
- messageHandler.call(relayHandle, gadgets.json.stringify(rpc),
targetOrigin);
- return true;
- },
+ call: call,
// Methods called by relay SWF. Should be considered private.
- _receiveMessage: function(message, fromOrigin, toOrigin) {
- window.setTimeout(function() { process(gadgets.json.parse(message),
fromOrigin); }, 0);
+ _receiveMessage: function(message, fromOrigin, toOrigin,
sendingSwfDomain) {
+ var jsonMsg = gadgets.json.parse(message);
+ var channelReady = jsonMsg[SWF_CHANNEL_READY];
+ if (channelReady) {
+ // Special message indicating that a ready message has been
received, indicating
+ // the sender is now prepared to receive messages. This type of
message is instigated
+ // by child context in polling fashion, and is responded-to by
parent context(s).
+ // If readyHandle is non-null, then it should first be cleared.
+ // This method is OK to call twice, if it occurs in a race.
+ ready(channelReady, true);
+ readyMsgs[channelReady] = true;
+ if (channelReady !== "..") {
+ // Child-to-parent: immediately signal that parent is ready.
+ // Now that we know that child can receive messages, it's enough
to send once.
+ sendChannelReady(channelReady);
+ }
+ return;
+ }
+ window.setTimeout(function() { process(jsonMsg, fromOrigin); }, 0);
},
_ready: function() {
@@ -155,6 +210,15 @@ if (!gadgets.rpctx.flash) { // make lib
window.clearTimeout(setupHandle);
}
setupHandle = null;
+ },
+
+ _setupDone: function() {
+ // Called by SWF only for role_id = "INNER" ie when initializing to
parent.
+ // Instantiates a polling handshake mechanism which ensures that any
enqueued
+ // messages remain so until each side is ready to send.
+ if (!readyMsgs[".."] && readyHandle === null) {
+ readyHandle = window.setTimeout(childReadyPoller, READY_TIMEOUT_MS);
+ }
}
};
}();
Modified: shindig/trunk/features/src/main/javascript/features/rpc/rpc.js
URL:
http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/rpc/rpc.js?rev=1098895&r1=1098894&r2=1098895&view=diff
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/rpc/rpc.js (original)
+++ shindig/trunk/features/src/main/javascript/features/rpc/rpc.js Tue May 3
02:41:03 2011
@@ -185,7 +185,7 @@ if (!window['gadgets']['rpc']) { // make
if (params['flash'] == "1") return gadgets.rpctx.flash;
return typeof window.postMessage === 'function' ? gadgets.rpctx.wpm :
typeof window.postMessage === 'object' ? gadgets.rpctx.wpm :
- window.ActiveXObject ? gadgets.rpctx.nix :
+ window.ActiveXObject ? (gadgets.rpctx.flash ? gadgets.rpctx.flash :
gadgets.rpctx.nix) :
navigator.userAgent.indexOf('WebKit') > 0 ? gadgets.rpctx.rmr :
navigator.product === 'Gecko' ? gadgets.rpctx.frameElement :
gadgets.rpctx.ifpc;
@@ -196,6 +196,7 @@ if (!window['gadgets']['rpc']) { // make
* send and receive messages.
*/
function transportReady(receiverId, readySuccess) {
+ if (receiverTx[receiverId]) return;
var tx = transport;
if (!readySuccess) {
tx = fallbackTransport;
@@ -892,7 +893,12 @@ if (!window['gadgets']['rpc']) { // make
// target is misconfigured, it won't affect the others.
// In the case of a sibling relay, channel is not found
// in the receiverTx map but in the transport itself.
- var channel = receiverTx[targetId] || transport;
+ var channel = receiverTx[targetId];
+ if (!channel && parseSiblingId(targetId) !== null) {
+ // Sibling-to-sibling communication; use default trasport
+ // (in practice, wpm) despite not being ready()-indicated.
+ channel = transport;
+ }
if (!channel) {
// Not set up yet. Enqueue the rpc for such time as it is.
@@ -1044,7 +1050,7 @@ if (!window['gadgets']['rpc']) { // make
ACK: ACK,
- RPC_ID: rpcId || "_top",
+ RPC_ID: rpcId || "..",
SEC_ERROR_LOAD_TIMEOUT: LOAD_TIMEOUT,
SEC_ERROR_FRAME_PHISH: FRAME_PHISH,