// Copyright 2008 Google, Inc. // All Rights Reserved. /** * @fileoverview Implementation of low-level request pieces for the Scotty API, * to be run in a Gears worker pool. * * @author Chris Monson * @author Charles Fry */ var wp = google.gears.workerPool; wp.allowCrossOrigin(); // only accept cross-domain traffic from port 80 on these domains var WHITELIST = ['youtube.com', 'google.com']; /* A map of all outstanding requests, indexed by the caller-specified * requestId. This id is assumed to be globally unique. If ever this worker * will be called from multiple senders, the sender id should be combined with * the requestId. */ var xhrRequests = {}; /** * Makes a key/value headers object from a string * * @param {String} headerStr String containing all headers * @returns {Object} */ function headersFromString(headerStr) { var headerList = headerStr.split(/\r\n|\n|\r/); var headerMap = {}; for (var i = 0; i < headerList.length; ++i) { var line = headerList[i]; if (line.length == 0) { continue; } var colon = line.search(/: */); if (colon == -1) { debug( "Invalid response header: '" + line + "'", sender, opt_callbackData); } else { var key = line.slice(0, colon); var val = line.slice(colon + 1, line.length).replace(/^\s*/, ''); headerMap[key] = val; } } return headerMap; }; /** * Registers a request object with the pool and gives the request a unique * identifier. * * @param {Object} request The XHR request object * @param {Number} requestId The new ID of this request object */ function registerRequest(request, requestId) { if (requestId == null) { throw new Error('Null request ID'); } if (xhrRequests[requestId]) { throw new Error('Request id ' + requestId + ' already in use'); } xhrRequests[requestId] = request; }; /** * Destroys and unregisters the given request ID, optionally aborting the * request in the process. * * @param {Number} requestId ID of the request to be deallocated * @param {Boolean} opt_abort Whether to also abort the request * * @returns {Boolean} Success */ function unregisterRequest(requestId, opt_abort) { var request = xhrRequests[requestId] || null; if (request) { delete xhrRequests[requestId]; } else { return false; } if (opt_abort) { request.abort(); } return true; }; /** * Creates a readystatechange callback function with minimal closure baggage * * @param {String} sender Sender of the message that initiated the action * resulting in this callback being called. * @param {Object} request Request object (XHR) * @param {Number} requestId The global identifier of request * @param {Object} opt_callbackData Optional callback data to be sent out as * part of a workerpool message. */ function onReadyStateChangeFactory( sender, request, requestId, opt_callbackData) { // Generate a pass-through function that just sends relevant XHR stuff back // to the caller. return function() { // Propagate state change message to parent var message = { type: 'XhrStateChange', requestId: requestId, state: request.readyState, data: opt_callbackData }; if (request.readyState == 4) { try { message.responseHeaders = headersFromString( request.getAllResponseHeaders()); message.responseText = request.responseText; message.responseStatusText = request.statusText; // use status as the sentinal that all of the request properties are set message.responseStatus = request.status; } catch (e) { debug("Request aborted: " + e, sender, opt_callbackData); } // we're done with this request! unregisterRequest(requestId); } wp.sendMessage(message, sender); }; } /** * Creates a progress callback function with minimal closure baggage * * @param {String} sender Sender of the message that initiated the action * resulting in this callback being called. * @param {Object} request Request object (XHR) * @param {Number} requestId The global identifier of request */ function onProgressFactory( sender, request, requestId, opt_callbackData) { // Generate a pass-through function that just sends relevant XHR stuff back // to the caller. return function(event) { // Send messages indicating what is going on in here. var message = { type: 'XhrProgress', requestId: requestId, state: request.readyState, loaded: event.loaded, total: event.total, lengthComputable: event.lengthComputable, data: opt_callbackData }; wp.sendMessage(message, sender); }; } /** * Sends debug information to be logged on the console. * * @param {String} message Error message * @param {String} sender Message sender ID * @param {Object} opt_callbackData Optional callback data to be sent out */ function debug(message, sender, opt_callbackData) { // TODO(fry): eventually replace this with the Console module wp.sendMessage( { type: 'XhrDebug', message: message, data: opt_callbackData }, sender); }; var messageTargets = {}; /** * Makes an XHR request to the given URL with given headers and data * * @param {Object} message Initiating message, body contains the following: * - method: 'GET' or 'POST', defaults to 'GET' * - url: url to which a request must be sent * - headers: object of header: value members, default is empty * - data: something that can be posted via XHR (blob or string) * - abortAfter: optional number of seconds of inactivity after which this * should just be aborted. Default is DEFAULT_ABORT_SECONDS. * - callbackData: a payload that will be sent right back with all calls * back into the calling worker. */ messageTargets.xhrStart = function(message) { var sender = message.sender; var url = message.body.url; var requestId = message.body.requestId; var method = message.body.method || 'GET'; var headers = message.body.headers || {}; var data = message.body.data || null; var callbackData = message.body.callbackData; var request = google.gears.factory.create('beta.httprequest'); // keep a reference to this request to allow future aborts registerRequest(request, requestId); request.open(method, url); for (var name in headers) { request.setRequestHeader(name, headers[name]); } request.onreadystatechange = onReadyStateChangeFactory( sender, request, requestId, callbackData); request.upload.onprogress = onProgressFactory( sender, request, requestId, callbackData); request.send(data); }; /** * Aborts an XHR request * * @param {Object} message Initiating message, body contains the following: * - requestId: ID of the request to abort */ messageTargets.xhrAbort = function(message) { var sender = message.sender; var requestId = message.body.requestId; var callbackData = message.body.callbackData; if (!unregisterRequest(requestId, true)) { // also abort debug('Request ID ' + requestId + ' not found', sender, callbackData); } }; // from lang.js /** * Fast suffix-checker. * @param {String} suffix String that may appear at end * @return {Boolean} True/false */ String.prototype.endsWith = function (suffix) { var l = this.length - suffix.length; return l >= 0 && this.lastIndexOf(suffix, l) == l; }; /** * Returns true if the specified origin is a domain on the whitelist, using * port 80. */ function originAllowed(origin) { var numDomains = WHITELIST.length; for (var i = 0; i < numDomains; ++i) { if (origin.endsWith('://' + WHITELIST[i]) || origin.endsWith('.' + WHITELIST[i])) { return true; } } return false; } // Note that the first two arguments are deprecated, which is why they are just // a and b. /** * Message dispatcher for this worker */ wp.onmessage = function(a, b, message) { if (!originAllowed(message.origin)) { throw new Error('Origin "' + message.origin + '" is not in the whitelist.'); } var targetName = message.body.target; if (!targetName) { throw new Error('No target specified'); } var target = messageTargets[targetName]; if (!target) { throw new Error('Unknown target: ' + targetName); } // Finally, call the darn target target(message); };