
features.rpc.rmr.transport.js Maven / Gradle / Ivy
Go to download
Packages all the features that shindig provides into a single jar file to allow
loading from the classpath
The newest version!
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
gadgets.rpctx = gadgets.rpctx || {};
/*
* For older WebKit-based browsers, the security model does not allow for any
* known "native" hacks for conducting cross browser communication. However,
* a variation of the IFPC (see below) can be used, entitled "RMR". RMR is
* a technique that uses the resize event of the iframe to indicate that a
* message was sent (instead of the much slower/performance heavy polling
* technique used when a defined relay page is not avaliable). Simply put,
* RMR uses the same "pass the message by the URL hash" trick that IFPC
* uses to send a message, but instead of having an active relay page that
* runs a piece of code when it is loaded, RMR merely changes the URL
* of the relay page (which does not even have to exist on the domain)
* and then notifies the other party by resizing the relay iframe. RMR
* exploits the fact that iframes in the dom of page A can be resized
* by page A while the onresize event will be fired in the DOM of page B,
* thus providing a single bit channel indicating "message sent to you".
* This method has the added benefit that the relay need not be active,
* nor even exist: a 404 suffices just as well. Note that the technique
* doesn't actually strictly require WebKit; it just so happens that these
* browsers have no known alternatives (but are very ill-used right now).
* The technique's implementation accounts for timing issues through
* a packet-ack'ing protocol, so should work on just about any browser.
* This may be of value in scenarios where neither wpm nor Flash are
* available for some reason.
*
* rmr: Resizing trick, works particularly well on WebKit.
* - Safari 2+
* - Chrome 1
*/
if (!gadgets.rpctx.rmr) { // make lib resilient to double-inclusion
gadgets.rpctx.rmr = function() {
// Consts for RMR, including time in ms RMR uses to poll for
// its relay frame to be created, and the max # of polls it does.
var RMR_SEARCH_TIMEOUT = 500;
var RMR_MAX_POLLS = 10;
// JavaScript references to the channel objects used by RMR.
// Gadgets will have but a single channel under
// rmr_channels['..'] while containers will have a channel
// per gadget stored under the gadget's ID.
var rmr_channels = {};
var parentParam = gadgets.util.getUrlParameters()['parent'];
var process;
var ready;
/**
* Append an RMR relay frame to the document. This allows the receiver
* to start receiving messages.
*
* @param {Node} channelFrame Relay frame to add to the DOM body.
* @param {string} relayUri Base URI for the frame.
* @param {string} data to pass along to the frame.
* @param {string=} opt_frameId ID of frame for which relay is being appended (optional).
*/
function appendRmrFrame(channelFrame, relayUri, data, opt_frameId) {
var appendFn = function() {
// Append the iframe.
document.body.appendChild(channelFrame);
// 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.
channelFrame.src = 'about:blank';
if (opt_frameId) {
// Process the initial sent payload (typically sent by container to
// child/gadget) only when the relay frame has finished loading. We
// do this to ensure that, in processRmrData(...), the ACK sent due
// to processing can actually be sent. Before this time, the frame's
// contentWindow is null, making it impossible to do so.
channelFrame.onload = function() {
processRmrData(opt_frameId);
};
}
channelFrame.src = relayUri + '#' + data;
};
if (document.body) {
appendFn();
} else {
// Common gadget case: attaching header during in-gadget handshake,
// when we may still be in script in head. Attach onload.
gadgets.util.registerOnLoadHandler(function() { appendFn(); });
}
}
/**
* Sets up the RMR transport frame for the given frameId. For gadgets
* calling containers, the frameId should be '..'.
*
* @param {string} frameId The ID of the frame.
*/
function setupRmr(frameId) {
if (typeof rmr_channels[frameId] === 'object') {
// Sanity check. Already done.
return;
}
var channelFrame = document.createElement('iframe');
var frameStyle = channelFrame.style;
frameStyle.position = 'absolute';
frameStyle.top = '0px';
frameStyle.border = '0';
frameStyle.opacity = '0';
// The width here is important as RMR
// makes use of the resize handler for the frame.
// Do not modify unless you test thoroughly!
frameStyle.width = '10px';
frameStyle.height = '1px';
channelFrame.id = 'rmrtransport-' + frameId;
channelFrame.name = channelFrame.id;
// Use the explicitly set relay, if one exists. Otherwise,
// Construct one using the parent parameter plus robots.txt
// as a synthetic relay. This works since browsers using RMR
// treat 404s as legitimate for the purposes of cross domain
// communication.
var relayUri = gadgets.rpc.getRelayUrl(frameId);
var relayOrigin = gadgets.rpc.getOrigin(parentParam);
if (!relayUri) {
relayUri = relayOrigin + '/robots.txt';
}
rmr_channels[frameId] = {
frame: channelFrame,
receiveWindow: null,
relayUri: relayUri,
relayOrigin: relayOrigin,
searchCounter: 0,
width: 10,
// Waiting means "waiting for acknowledgement to be received."
// Acknowledgement always comes as a special ACK
// message having been received. This message is received
// during handshake in different ways by the container and
// gadget, and by normal RMR message passing once the handshake
// is complete.
waiting: true,
queue: [],
// Number of non-ACK messages that have been sent to the recipient
// and have been acknowledged.
sendId: 0,
// Number of messages received and processed from the sender.
// This is the number that accompanies every ACK to tell the
// sender to clear its queue.
recvId: 0,
// Token sent to target to verify domain.
// TODO: switch to shindig.random()
verifySendToken: String(Math.random()),
// Token received from target during handshake. Stored in
// order to send back to the caller for verification.
verifyRecvToken: null,
originVerified: false
};
if (frameId !== '..') {
// Container always appends a relay to the gadget, before
// the gadget appends its own relay back to container. The
// gadget, in the meantime, refuses to attach the container
// relay until it finds this one. Thus, the container knows
// for certain that gadget to container communication is set
// up by the time it finds its own relay. In addition to
// establishing a reliable handshake protocol, this also
// makes it possible for the gadget to send an initial batch
// of messages to the container ASAP.
appendRmrFrame(channelFrame, relayUri, getRmrData(frameId));
}
// Start searching for our own frame on the other page.
conductRmrSearch(frameId);
}
/**
* Searches for a relay frame, created by the sender referenced by
* frameId, with which this context receives messages. Once
* found with proper permissions, attaches a resize handler which
* signals messages to be sent.
*
* @param {string} frameId Frame ID of the prospective sender.
*/
function conductRmrSearch(frameId) {
var channelWindow = null;
// Increment the search counter.
rmr_channels[frameId].searchCounter++;
try {
var targetWin = gadgets.rpc._getTargetWin(frameId);
if (frameId === '..') {
// We are a gadget.
channelWindow = targetWin.frames['rmrtransport-' + gadgets.rpc.RPC_ID];
} else {
// We are a container.
channelWindow = targetWin.frames['rmrtransport-..'];
}
} catch (e) {
// Just in case; may happen when relay is set to about:blank or unset.
// Catching exceptions here ensures that the timeout to continue the
// search below continues to work.
}
var status = false;
if (channelWindow) {
// We have a valid reference to "our" RMR transport frame.
// Register the proper event handlers.
status = registerRmrChannel(frameId, channelWindow);
}
if (!status) {
// Not found yet. Continue searching, but only if the counter
// has not reached the threshold.
if (rmr_channels[frameId].searchCounter > RMR_MAX_POLLS) {
// If we reach this point, then RMR has failed and we
// fall back to IFPC.
return;
}
window.setTimeout(function() {
conductRmrSearch(frameId);
}, RMR_SEARCH_TIMEOUT);
}
}
/**
* Attempts to conduct an RPC call to the specified
* target with the specified data via the RMR
* method. If this method fails, the system attempts again
* using the known default of IFPC.
*
* @param {string} targetId Module Id of the RPC service provider.
* @param {string} serviceName Name of the service to call.
* @param {string} from Module Id of the calling provider.
* @param {Object} rpc The RPC data for this call.
*/
function callRmr(targetId, serviceName, from, rpc) {
var handler = null;
if (from !== '..') {
// Call from gadget to the container.
handler = rmr_channels['..'];
} else {
// Call from container to the gadget.
handler = rmr_channels[targetId];
}
if (handler) {
// Queue the current message if not ACK.
// ACK is always sent through getRmrData(...).
if (serviceName !== gadgets.rpc.ACK) {
handler.queue.push(rpc);
}
if (handler.waiting ||
(handler.queue.length === 0 &&
!(serviceName === gadgets.rpc.ACK && rpc && rpc['ackAlone'] === true))) {
// If we are awaiting a response from any previously-sent messages,
// or if we don't have anything new to send, just return.
// Note that we don't short-return if we're ACKing just-received
// messages.
return true;
}
if (handler.queue.length > 0) {
handler.waiting = true;
}
var url = handler.relayUri + '#' + getRmrData(targetId);
try {
// Update the URL with the message.
handler.frame.contentWindow.location = url;
// Resize the frame.
var newWidth = handler.width == 10 ? 20 : 10;
handler.frame.style.width = newWidth + 'px';
handler.width = newWidth;
// Done!
} catch (e) {
// Something about location-setting or resizing failed.
// This should never happen, but if it does, fall back to
// the default transport.
return false;
}
}
return true;
}
/**
* Returns as a string the data to be appended to an RMR relay frame,
* constructed from the current request queue plus an ACK message indicating
* the currently latest-processed message ID.
*
* @param {string} toFrameId Frame whose sendable queued data to retrieve.
*/
function getRmrData(toFrameId) {
var channel = rmr_channels[toFrameId];
var rmrData = {id: channel.sendId};
if (channel) {
rmrData['d'] = Array.prototype.slice.call(channel.queue, 0);
var ackPacket = { 's': gadgets.rpc.ACK, 'id': channel.recvId };
if (!channel.originVerified) {
ackPacket['sendToken'] = channel.verifySendToken;
}
if (channel.verifyRecvToken) {
ackPacket['recvToken'] = channel.verifyRecvToken;
}
rmrData['d'].push(ackPacket);
}
return gadgets.json.stringify(rmrData);
}
/**
* Retrieve data from the channel keyed by the given frameId,
* processing it as a batch. All processed data is assumed to have been
* generated by getRmrData(...), pairing that method with this.
*
* @param {string} fromFrameId Frame from which data is being retrieved.
*/
function processRmrData(fromFrameId) {
var channel = rmr_channels[fromFrameId];
var data = channel.receiveWindow.location.hash.substring(1);
// Decode the RPC object array.
var rpcObj = gadgets.json.parse(decodeURIComponent(data)) || {};
var rpcArray = rpcObj['d'] || [];
var nonAckReceived = false;
var noLongerWaiting = false;
var numBypassed = 0;
var numToBypass = (channel.recvId - rpcObj['id']);
for (var i = 0; i < rpcArray.length; ++i) {
var rpc = rpcArray[i];
// If we receive an ACK message, then mark the current
// handler as no longer waiting and send out the next
// queued message.
if (rpc['s'] === gadgets.rpc.ACK) {
// ACK received - whether this came from a handshake or
// an active call, in either case it indicates readiness to
// send messages to the from frame.
ready(fromFrameId, true);
// Store sendToken if challenge was passed.
// This will cause the token to be sent back to the sender
// to prove origin verification.
channel.verifyRecvToken = rpc['sendToken'];
// If a recvToken came back, check to see if it matches the
// sendToken originally sent as a challenge. If so, mark
// origin as having been verified.
if (!channel.originVerified && rpc['recvToken'] &&
String(rpc['recvToken']) == String(channel.verifySendToken)) {
channel.originVerified = true;
}
if (channel.waiting) {
noLongerWaiting = true;
}
channel.waiting = false;
var newlyAcked = Math.max(0, rpc['id'] - channel.sendId);
channel.queue.splice(0, newlyAcked);
channel.sendId = Math.max(channel.sendId, rpc['id'] || 0);
continue;
}
// If we get here, we've received > 0 non-ACK messages to
// process. Indicate this bit for later.
nonAckReceived = true;
// Bypass any messages already received.
if (++numBypassed <= numToBypass) {
continue;
}
++channel.recvId;
// Send along the origin if it's been verified during handshake.
// In either case, dispatch the message.
process(rpc, channel.originVerified ? channel.relayOrigin : undefined);
}
// Send an ACK indicating that we got/processed the message(s).
// Do so if we've received a message to process or if we were waiting
// before but a received ACK has cleared our waiting bit, and we have
// more messages to send. Performing this operation causes additional
// messages to be sent.
if (nonAckReceived ||
(noLongerWaiting && channel.queue.length > 0)) {
var from = (fromFrameId === '..') ? gadgets.rpc.RPC_ID : '..';
callRmr(fromFrameId, gadgets.rpc.ACK, from, {'ackAlone': nonAckReceived});
}
}
/**
* Registers the RMR channel handler for the given frameId and associated
* channel window.
*
* @param {string} frameId The ID of the frame for which this channel is being
* registered.
* @param {Object} channelWindow The window of the receive frame for this
* channel, if any.
*
* @return {boolean} True if the frame was setup successfully, false
* otherwise.
*/
function registerRmrChannel(frameId, channelWindow) {
var channel = rmr_channels[frameId];
// Verify that the channel is ready for receiving.
try {
var canAccess = false;
// Check to see if the document is in the window. For Chrome, this
// will return 'false' if the channelWindow is inaccessible by this
// piece of JavaScript code, meaning that the URL of the channelWindow's
// parent iframe has not yet changed from 'about:blank'. We do this
// check this way because any true *access* on the channelWindow object
// will raise a security exception, which, despite the try-catch, still
// gets reported to the debugger (it does not break execution, the try
// handles that problem, but it is still reported, which is bad form).
// This check always succeeds in Safari 3.1 regardless of the state of
// the window.
canAccess = 'document' in channelWindow;
if (!canAccess) {
return false;
}
// Check to see if the document is an object. For Safari 3.1, this will
// return undefined if the page is still inaccessible. Unfortunately, this
// *will* raise a security issue in the debugger.
// TODO Find a way around this problem.
canAccess = typeof channelWindow['document'] == 'object';
if (!canAccess) {
return false;
}
// Once we get here, we know we can access the document (and anything else)
// on the window object. Therefore, we check to see if the location is
// still about:blank (this takes care of the Safari 3.2 case).
var loc = channelWindow.location.href;
// Check if this is about:blank for Safari.
if (loc === 'about:blank') {
return false;
}
} catch (ex) {
// For some reason, the iframe still points to about:blank. We try
// again in a bit.
return false;
}
// Save a reference to the receive window.
channel.receiveWindow = channelWindow;
// Register the onresize handler.
function onresize() {
processRmrData(frameId);
};
if (typeof channelWindow.attachEvent === 'undefined') {
channelWindow.onresize = onresize;
} else {
channelWindow.attachEvent('onresize', onresize);
}
if (frameId === '..') {
// Gadget to container. Signal to the container that the gadget
// is ready to receive messages by attaching the g -> c relay.
// As a nice optimization, pass along any gadget to container
// queued messages that have backed up since then. ACK is enqueued in
// getRmrData to ensure that the container's waiting flag is set to false
// (this happens in the below code run on the container side).
appendRmrFrame(channel.frame, channel.relayUri, getRmrData(frameId), frameId);
} else {
// Process messages that the gadget sent in its initial relay payload.
// We can do this immediately because the container has already appended
// and loaded a relay frame that can be used to ACK the messages the gadget
// sent. In the preceding if-block, however, the processRmrData(...) call
// must wait. That's because appendRmrFrame may not actually append the
// frame - in the context of a gadget, this code may be running in the
// head element, so it cannot be appended to body. As a result, the
// gadget cannot ACK the container for messages it received.
processRmrData(frameId);
}
return true;
}
return {
getCode: function() {
return 'rmr';
},
isParentVerifiable: function() {
return true;
},
init: function(processFn, readyFn) {
// No global setup.
process = processFn;
ready = readyFn;
return true;
},
setup: function(receiverId, token) {
try {
setupRmr(receiverId);
} catch (e) {
gadgets.warn('Caught exception setting up RMR: ' + e);
return false;
}
return true;
},
call: function(targetId, from, rpc) {
return callRmr(targetId, rpc['s'], from, rpc);
}
};
}();
} // !end of double-inclusion guard
© 2015 - 2025 Weber Informatics LLC | Privacy Policy