You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shindig.apache.org by li...@apache.org on 2010/08/30 08:31:01 UTC

svn commit: r990701 - in /shindig/trunk/features/src/main/javascript/features/rpc: nix.transport.js rpc.js wpm.transport.js

Author: lindner
Date: Mon Aug 30 06:31:00 2010
New Revision: 990701

URL: http://svn.apache.org/viewvc?rev=990701&view=rev
Log:
Patch from Javier Pedemonte | Security updates for gadgets.rpc

Modified:
    shindig/trunk/features/src/main/javascript/features/rpc/nix.transport.js
    shindig/trunk/features/src/main/javascript/features/rpc/rpc.js
    shindig/trunk/features/src/main/javascript/features/rpc/wpm.transport.js

Modified: shindig/trunk/features/src/main/javascript/features/rpc/nix.transport.js
URL: http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/rpc/nix.transport.js?rev=990701&r1=990700&r2=990701&view=diff
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/rpc/nix.transport.js (original)
+++ shindig/trunk/features/src/main/javascript/features/rpc/nix.transport.js Mon Aug 30 06:31:00 2010
@@ -81,6 +81,7 @@ gadgets.rpctx.nix = function() {
   // nix_channels['..'] while containers will have a channel
   // per gadget stored under the gadget's ID.
   var nix_channels = {};
+  var isForceSecure = {};
 
   // Store the ready signal method for use on handshake complete.
   var ready;
@@ -138,12 +139,59 @@ gadgets.rpctx.nix = function() {
                       NIX_SEARCH_PERIOD);
   }
 
+  // Returns current window location, without hash values
+  function getLocationNoHash() {
+    var loc = window.location.href;
+    var idx = loc.indexOf('#');
+    if (idx == -1) {
+      return loc;
+    }
+    return loc.substring(0, idx);
+  }
+
+  // When "forcesecure" is set to true, use the relay file and a simple variant of IFPC to first
+  // authenticate the container and gadget with each other.  Once that is done, then initialize
+  // the NIX protocol. 
+  function setupSecureRelayToParent(rpctoken) {
+    // To the parent, transmit the child's URL, the passed in auth
+    // token, and another token generated by the child.
+    var childToken = (0x7FFFFFFF * Math.random()) | 0;    // XXX expose way to have child set this value
+    var data = [
+      getLocationNoHash(),
+      childToken
+    ];
+    gadgets.rpc._createRelayIframe(rpctoken, data);
+    
+    // listen for response from parent
+    var hash = window.location.href.split('#')[1] || '';
+  
+    function relayTimer() {
+      var newHash = window.location.href.split('#')[1] || '';
+      if (newHash !== hash) {
+        clearInterval(relayTimerId);
+        var params = gadgets.util.getUrlParameters(window.location.href);
+        if (params.childtoken == childToken) {
+          // parent has been authenticated; now init NIX
+          conductHandlerSearch();
+          return;
+        }
+        // security error -- token didn't match
+        ready('..', false);
+      }
+    }
+    var relayTimerId = setInterval( relayTimer, 100 );
+  }
+
   return {
     getCode: function() {
       return 'nix';
     },
 
-    isParentVerifiable: function() {
+    isParentVerifiable: function(opt_receiverId) {
+      // NIX is only parent verifiable if a receiver was setup with "forcesecure" set to TRUE.
+      if (opt_receiverId) {
+        return isForceSecure[opt_receiverId];
+      }
       return false;
     },
 
@@ -249,9 +297,14 @@ gadgets.rpctx.nix = function() {
       return true;
     },
 
-    setup: function(receiverId, token) {
+    setup: function(receiverId, token, forcesecure) {
+      isForceSecure[receiverId] = !!forcesecure;
       if (receiverId === '..') {
-        conductHandlerSearch();
+        if (forcesecure) {
+          setupSecureRelayToParent(token);
+        } else {
+          conductHandlerSearch();
+        }
         return true;
       }
       try {
@@ -274,6 +327,14 @@ gadgets.rpctx.nix = function() {
         return false;
       }
       return true;
+    },
+    
+    // data = [child URL, child auth token]
+    relayOnload: function(receiverId, data) {
+      // transmit childtoken back to child to complete authentication
+      var src = data[0] + '#childtoken=' + data[1];
+      var childIframe = document.getElementById(receiverId);
+      childIframe.src = src;
     }
   };
 }();

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=990701&r1=990700&r2=990701&view=diff
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/rpc/rpc.js (original)
+++ shindig/trunk/features/src/main/javascript/features/rpc/rpc.js Mon Aug 30 06:31:00 2010
@@ -112,6 +112,11 @@ gadgets.rpc = function() {
   // shadowing of window.name by a "var name" declaration, or similar.
   var rpcId = window.name;
 
+  var securityCallback = function() {};
+  var LOAD_TIMEOUT = 0;
+  var FRAME_PHISH = 1;
+  var FORGED_MSG = 2;
+
   // Fallback transport is simply a dummy impl that emits no errors
   // and logs info on calls it receives, to avoid undesired side-effects
   // from falling back to IFPC or some other transport.
@@ -143,13 +148,6 @@ gadgets.rpc = function() {
     params = gadgets.util.getUrlParameters();
   }
 
-  // Indicates whether to support early-message queueing, which is designed
-  // to ensure that all messages sent by gadgets.rpc.call, irrespective
-  // when they were made (before/after setupReceiver, before/after transport
-  // setup complete), are sent. Hiding behind a query param to allow opt-in
-  // for a time while this technique is proven.
-  var useEarlyQueueing = (params['rpc_earlyq'] === "1");
-
   /**
    * Return a transport representing the best available cross-domain
    * message-passing mechanism available to the browser.
@@ -189,8 +187,7 @@ gadgets.rpc = function() {
     receiverTx[receiverId] = tx;
 
     // If there are any early-queued messages, send them now directly through
-    // the needed transport. This queue will only have contents if
-    // useEarlyQueueing === true (see call method).
+    // the needed transport.
     var earlyQueue = earlyRpcQueue[receiverId] || [];
     for (var i = 0; i < earlyQueue.length; ++i) {
       var rpc = earlyQueue[i];
@@ -203,6 +200,43 @@ gadgets.rpc = function() {
     earlyRpcQueue[receiverId] = [];
   }
 
+  //  Track when this main page is closed or navigated to a different location
+  // ("unload" event).
+  //  NOTE: The use of the "unload" handler here and for the relay iframe
+  // prevents the use of the in-memory page cache in modern browsers.
+  var mainPageUnloading = false,
+      hookedUnload = false;
+  
+  function hookMainPageUnload() {
+    if ( hookedUnload ) {
+      return;
+    }
+    function onunload() {
+      mainPageUnloading = true;
+    }
+    gadgets.util.attachBrowserEvent(window, 'unload', onunload, false);
+    hookedUnload = true;
+  }
+
+  function relayOnload(targetId, sourceId, token, data, relayWindow) {
+    // Validate auth token.
+    if (!authToken[sourceId] || authToken[sourceId] !== token) {
+      gadgets.error("Invalid auth token. " + authToken[sourceId] + " vs " + token);
+      securityCallback(sourceId, FORGED_MSG);
+    }
+    
+    relayWindow.onunload = function() {
+      if (setup[sourceId] && !mainPageUnloading) {
+        securityCallback(sourceId, FRAME_PHISH);
+        gadgets.rpc.removeReceiver(sourceId);
+      }
+    };
+    hookMainPageUnload();
+    
+    data = gadgets.json.parse(decodeURIComponent(data));
+    transport.relayOnload(sourceId, data);
+  }
+
   /**
    * Helper function to process an RPC request
    * @param {Object} rpc RPC request object
@@ -225,8 +259,8 @@ gadgets.rpc = function() {
         // We don't do type coercion here because all entries in the authToken
         // object are strings, as are all url params. See setupReceiver(...).
         if (authToken[rpc.f] !== rpc.t) {
-          throw new Error("Invalid auth token. " +
-              authToken[rpc.f] + " vs " + rpc.t);
+          gadgets.error("Invalid auth token. " + authToken[rpc.f] + " vs " + rpc.t);
+          securityCallback(rpc.f, FORGED_MSG);
         }
       }
 
@@ -371,7 +405,7 @@ gadgets.rpc = function() {
    * RPC mechanism. Gadgets, in turn, will complete the setup
    * of the channel once they send their first messages.
    */
-  function setupFrame(frameId, token) {
+  function setupFrame(frameId, token, forcesecure) {
     if (setup[frameId] === true) {
       return;
     }
@@ -382,7 +416,7 @@ gadgets.rpc = function() {
 
     var tgtFrame = document.getElementById(frameId);
     if (frameId === '..' || tgtFrame != null) {
-      if (transport.setup(frameId, token) === true) {
+      if (transport.setup(frameId, token, forcesecure) === true) {
         setup[frameId] = true;
         return;
       }
@@ -390,7 +424,7 @@ gadgets.rpc = function() {
 
     if (setup[frameId] !== true && setup[frameId]++ < SETUP_FRAME_MAX_TRIES) {
       // Try again in a bit, assuming that frame will soon exist.
-      window.setTimeout(function() { setupFrame(frameId, token) },
+      window.setTimeout(function() { setupFrame(frameId, token, forcesecure) },
                         SETUP_FRAME_TIMEOUT);
     } else {
       // Fail: fall back for this gadget.
@@ -451,6 +485,17 @@ gadgets.rpc = function() {
    * @deprecated
    */
   function setRelayUrl(targetId, url, opt_useLegacy) {
+    // make URL absolute if necessary
+    if (!/http(s)?:\/\/.+/.test(url)) {
+      if (url.indexOf("//") == 0) {
+        url = window.location.protocol + url;
+      } else if (url.charAt(0) == '/') {
+        url = window.location.protocol + "//" + window.location.host + url;
+      } else if (url.indexOf("://") == -1) {
+        // Assumed to be schemaless. Default to current protocol.
+        url = window.location.protocol + "//" + url;
+      }
+    }
     relayUrl[targetId] = url;
     useLegacyProtocol[targetId] = !!opt_useLegacy;
   }
@@ -474,7 +519,7 @@ gadgets.rpc = function() {
    * @member gadgets.rpc
    * @deprecated
    */
-  function setAuthToken(targetId, token) {
+  function setAuthToken(targetId, token, forcesecure) {
     token = token || "";
 
     // Coerce token to a String, ensuring that all authToken values
@@ -482,10 +527,10 @@ gadgets.rpc = function() {
     // in the process(rpc) method.
     authToken[targetId] = String(token);
 
-    setupFrame(targetId, token);
+    setupFrame(targetId, token, forcesecure);
   }
 
-  function setupContainerGadgetContext(rpctoken) {
+  function setupContainerGadgetContext(rpctoken, opt_forcesecure) {
     /**
      * Initializes gadget to container RPC params from the provided configuration.
      */
@@ -525,7 +570,8 @@ gadgets.rpc = function() {
       }
 
       // Sets the auth token and signals transport to setup connection to container.
-      setAuthToken('..', rpctoken);
+      var forceSecure = opt_forcesecure || params.forcesecure || false;
+      setAuthToken('..', rpctoken, forceSecure);
     }
 
     var requiredConfig = {
@@ -534,19 +580,20 @@ gadgets.rpc = function() {
     gadgets.config.register("rpc", requiredConfig, init);
   }
 
-  function setupContainerGenericIframe(rpctoken, opt_parent) {
+  function setupContainerGenericIframe(rpctoken, opt_parent, opt_forcesecure) {
     // Generic child IFRAME setting up connection w/ its container.
     // Use the opt_parent param if provided, or the "parent" query param
     // if found -- otherwise, do nothing since this call might be initiated
     // automatically at first, then actively later in IFRAME code.
+    var forcesecure = opt_forcesecure || params.forcesecure || false;
     var parent = opt_parent || params.parent;
     if (parent) {
       setRelayUrl('..', parent);
-      setAuthToken('..', rpctoken);
+      setAuthToken('..', rpctoken, forcesecure);
     }
   }
 
-  function setupChildIframe(gadgetId, opt_frameurl, opt_authtoken) {
+  function setupChildIframe(gadgetId, opt_frameurl, opt_authtoken, opt_forcesecure) {
     if (!gadgets.util) {
       return;
     }
@@ -564,7 +611,8 @@ gadgets.rpc = function() {
     // The auth token is parsed from child params (rpctoken) or overridden.
     var childParams = gadgets.util.getUrlParameters(childIframe.src);
     var rpctoken = opt_authtoken || childParams.rpctoken;
-    setAuthToken(gadgetId, rpctoken);
+    var forcesecure = opt_forcesecure || childParams.forcesecure;
+    setAuthToken(gadgetId, rpctoken, forcesecure);
   }
 
   /**
@@ -610,22 +658,28 @@ gadgets.rpc = function() {
    * @param {string=} opt_receiverurl
    * @param {string=} opt_authtoken
    */
-  function setupReceiver(targetId, opt_receiverurl, opt_authtoken) {
+  function setupReceiver(targetId, opt_receiverurl, opt_authtoken, opt_forcesecure) {
     if (targetId === '..') {
       // Gadget/IFRAME to container.
       var rpctoken = opt_authtoken || params.rpctoken || params.ifpctok || "";
       if (window['__isgadget'] === true) {
-        setupContainerGadgetContext(rpctoken);
+        setupContainerGadgetContext(rpctoken, opt_forcesecure);
       } else {
-        setupContainerGenericIframe(rpctoken, opt_receiverurl);
+        setupContainerGenericIframe(rpctoken, opt_receiverurl, opt_forcesecure);
       }
     } else {
       // Container to child.
-      setupChildIframe(targetId, opt_receiverurl, opt_authtoken);
+      setupChildIframe(targetId, opt_receiverurl, opt_authtoken, opt_forcesecure);
     }
   }
 
   return /** @scope gadgets.rpc */ {
+    config: function(config) {
+      if (typeof config.securityCallback === 'function') {
+        securityCallback = config.securityCallback;
+      }
+    },
+    
     /**
      * Registers an RPC service.
      * @param {string} serviceName Service name to register.
@@ -747,7 +801,11 @@ gadgets.rpc = function() {
       // Attempt to make call via a cross-domain transport.
       // Retrieve the transport for the given target - if one
       // target is misconfigured, it won't affect the others.
-      var channel = receiverTx[targetId] ? receiverTx[targetId] : transport;
+// XXX Since 'transport' is always set (on load of rpc.js), channel will never
+//    be null (and earlyRpcQueue will never be used).  Only use
+//    receiverTx[targetId].
+//      var channel = receiverTx[targetId] ? receiverTx[targetId] : transport;
+      var channel = receiverTx[targetId];
 
       if (!channel) {
         // Not set up yet. Enqueue the rpc for such time as it is.
@@ -797,6 +855,16 @@ gadgets.rpc = function() {
     setAuthToken: setAuthToken,
     setupReceiver: setupReceiver,
     getAuthToken: getAuthToken,
+    
+    // Note: Does not delete iframe
+    removeReceiver: function(receiverId) {
+      delete relayUrl[receiverId];
+      delete useLegacyProtocol[receiverId];
+      delete authToken[receiverId];
+      delete setup[receiverId];
+      delete sameDomain[receiverId];
+      delete receiverTx[receiverId];
+    },
 
     /**
      * Gets the RPC relay mechanism.
@@ -820,10 +888,12 @@ gadgets.rpc = function() {
      * @member gadgets.rpc
      * @deprecated
      */
-    receive: function(fragment) {
+    receive: function(fragment, otherWindow) {
       if (fragment.length > 4) {
         process(gadgets.json.parse(
             decodeURIComponent(fragment[fragment.length - 1])));
+      } else {
+        relayOnload.apply(null, fragment.concat(otherWindow));
       }
     },
 
@@ -844,6 +914,21 @@ gadgets.rpc = function() {
     // see docs above
     getOrigin: getOrigin,
 
+    getReceiverOrigin: function(receiverId) {
+      var channel = receiverTx[receiverId];
+      if (!channel) {
+        // not set up yet
+        return null;
+      }
+      if (!channel.isParentVerifiable(receiverId)) {
+        // given transport cannot verify receiver origin
+        return null;
+      }
+      var origRelay = gadgets.rpc.getRelayUrl(receiverId) ||
+                      gadgets.util.getUrlParameters().parent;
+      return gadgets.rpc.getOrigin(origRelay);
+    },
+
     /**
      * Internal-only method used to initialize gadgets.rpc.
      * @member gadgets.rpc
@@ -863,9 +948,52 @@ gadgets.rpc = function() {
     /** Returns the window keyed by the ID. null/".." for parent, else child */
     _getTargetWin: getTargetWin,
 
+    /** Create an iframe for loading the relay URL. Used by child only. */ 
+    _createRelayIframe: function(token, data) {
+      var relay = gadgets.rpc.getRelayUrl('..');
+      if (!relay) {
+        return;
+      }
+      
+      // Format: #targetId & sourceId & authToken & data
+      var src = relay + '#..&' + rpcId + '&' + token + '&' +
+          encodeURIComponent(gadgets.json.stringify(data));
+  
+      var iframe = document.createElement('iframe');
+      iframe.style.border = iframe.style.width = iframe.style.height = '0px';
+      iframe.style.visibility = 'hidden';
+      iframe.style.position = 'absolute';
+
+      function appendFn() {
+        // Append the iframe.
+        document.body.appendChild(iframe);
+  
+        // Set the src of the iframe to 'about:blank' first and then set it
+        // to the relay URI. This prevents the iframe from maintaining a src
+        // to the 'old' relay URI if the page is returned to from another.
+        // In other words, this fixes the bfcache issue that causes the iframe's
+        // src property to not be updated despite us assigning it a new value here.
+        iframe.src = 'javascript:"<html></html>"';
+        iframe.src = src;
+      }
+      
+      if (document.body) {
+        appendFn();
+      } else {
+        gadgets.util.registerOnLoadHandler(function() { appendFn(); });
+      }
+      
+      return iframe;
+    },
+
     ACK: ACK,
 
-    RPC_ID: rpcId
+    RPC_ID: rpcId,
+    
+    LOAD_TIMEOUT: LOAD_TIMEOUT,
+    FRAME_PHISH: FRAME_PHISH,
+    FORGED_MSG : FORGED_MSG
+
   };
 }();
 

Modified: shindig/trunk/features/src/main/javascript/features/rpc/wpm.transport.js
URL: http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/rpc/wpm.transport.js?rev=990701&r1=990700&r2=990701&view=diff
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/rpc/wpm.transport.js (original)
+++ shindig/trunk/features/src/main/javascript/features/rpc/wpm.transport.js Mon Aug 30 06:31:00 2010
@@ -44,7 +44,62 @@ gadgets.rpctx = gadgets.rpctx || {};
 if (!gadgets.rpctx.wpm) {  // make lib resilient to double-inclusion
 
 gadgets.rpctx.wpm = function() {
-  var ready;
+  var process, ready;
+  var postMessage;
+  var pmSync = false;
+  var pmEventDomain = false;
+
+  // Some browsers (IE, Opera) have an implementation of postMessage that is
+  // synchronous, although HTML5 specifies that it should be asynchronous.  In
+  // order to make all browsers behave consistently, we run a small test to detect
+  // if postMessage is asynchronous or not.  If not, we wrap calls to postMessage
+  // in a setTimeout with a timeout of 0.
+  // Also, Opera's "message" event does not have an "origin" property (at least,
+  // it doesn't in version 9.64;  presumably, it will in version 10).  If
+  // event.origin does not exist, use event.domain.  The other difference is that
+  // while event.origin looks like <scheme>://<hostname>:<port>, event.domain
+  // consists only of <hostname>.
+  //
+  function testPostMessage() {
+    var hit = false;
+    
+    function receiveMsg(event) {
+      if (event.data == "postmessage.test") {
+        hit = true;
+        if (typeof event.origin === "undefined") {
+          pmEventDomain = true;
+        }
+      }
+    }
+    
+    gadgets.util.attachBrowserEvent(window, "message", receiveMsg, false);
+    window.postMessage("postmessage.test", "*");
+    
+    // if 'hit' is true here, then postMessage is synchronous
+    if (hit) {
+      pmSync = true;
+    }
+    
+    gadgets.util.removeBrowserEvent(window, "message", receiveMsg, false);
+  }
+
+  function onmessage(packet) {
+    var rpc = gadgets.json.parse(packet.data);
+    if (!rpc || !rpc.f) {
+      return;
+    }
+    
+    // for security, check origin against expected value
+    var origRelay = gadgets.rpc.getRelayUrl(rpc.f) ||
+                    gadgets.util.getUrlParameters()["parent"];
+    var origin = gadgets.rpc.getOrigin(origRelay);
+    if (!pmEventDomain ? packet.origin !== origin :
+                         packet.domain !== /^.+:\/\/([^:]+).*/.exec( origin )[1]) {
+      return;
+    }
+
+    process(rpc);
+  }
 
   return {
     getCode: function() {
@@ -56,11 +111,21 @@ gadgets.rpctx.wpm = function() {
     },
 
     init: function(processFn, readyFn) {
+      process = processFn;
       ready = readyFn;
-      var onmessage = function(packet) {
-        // TODO validate packet.domain for security reasons
-        processFn(gadgets.json.parse(packet.data));
-      };
+
+      testPostMessage();
+      if (!pmSync) {
+        postMessage = function(win, msg, origin) {
+          win.postMessage(msg, origin);
+        };
+      } else {
+        postMessage = function(win, msg, origin) {
+          window.setTimeout( function() {
+            win.postMessage(msg, origin);
+          }, 0);
+        };
+      }
  
       // Set up native postMessage handler.
       gadgets.util.attachBrowserEvent(window, 'message', onmessage, false);
@@ -69,11 +134,15 @@ gadgets.rpctx.wpm = function() {
       return true;
     },
 
-    setup: function(receiverId, token) {
+    setup: function(receiverId, token, forcesecure) {
       // If we're a gadget, send an ACK message to indicate to container
       // that we're ready to receive messages.
       if (receiverId === '..') {
-        gadgets.rpc.call(receiverId, gadgets.rpc.ACK);
+        if (forcesecure) {
+          gadgets.rpc._createRelayIframe(token);
+        } else {
+          gadgets.rpc.call(receiverId, gadgets.rpc.ACK);
+        }
       }
       return true;
     },
@@ -85,12 +154,16 @@ gadgets.rpctx.wpm = function() {
                       gadgets.util.getUrlParameters()["parent"];
       var origin = gadgets.rpc.getOrigin(origRelay);
       if (origin) {
-        targetWin.postMessage(gadgets.json.stringify(rpc), origin);
+        postMessage(targetWin, gadgets.json.stringify(rpc), origin);
       } else {
         gadgets.error("No relay set (used as window.postMessage targetOrigin)" +
             ", cannot send cross-domain message");
       }
       return true;
+    },
+
+    relayOnload: function(receiverId, data) {
+      ready(receiverId, true);
     }
   };
 }();