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);
}
};
}();