diff options
author | jamiewalch@chromium.org <jamiewalch@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-01-29 21:24:10 +0000 |
---|---|---|
committer | jamiewalch@chromium.org <jamiewalch@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-01-29 21:24:10 +0000 |
commit | 38643635962d27bf9cb6cc1d7f9701a29ecbe90e (patch) | |
tree | 78b671af90079ab65e1d4a602b550a7240b14618 /remoting/webapp | |
parent | db471019d15ed9e8e086bf36f426326e3ebbe86d (diff) | |
download | chromium_src-38643635962d27bf9cb6cc1d7f9701a29ecbe90e.zip chromium_src-38643635962d27bf9cb6cc1d7f9701a29ecbe90e.tar.gz chromium_src-38643635962d27bf9cb6cc1d7f9701a29ecbe90e.tar.bz2 |
Get Chromoting client working in Apps V2
This involves the following:
1. Removing chrome extension API calls from wcs.js and wcs_loader.js.
2. Moving those files into a separate <iframe> sandbox.
3. Adding a postMessage-based API to communicate with the rest of the code.
4. Working around miscellaneous sandbox restrictions.
BUG=134213
Review URL: https://chromiumcodereview.appspot.com/12021029
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@179413 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'remoting/webapp')
-rw-r--r-- | remoting/webapp/all_js_load.gtestjs | 5 | ||||
-rw-r--r-- | remoting/webapp/appsv2.patch | 17 | ||||
-rw-r--r-- | remoting/webapp/client_screen.js | 78 | ||||
-rw-r--r-- | remoting/webapp/client_session.js | 51 | ||||
-rw-r--r-- | remoting/webapp/jscompiler_hacks.js | 3 | ||||
-rw-r--r-- | remoting/webapp/log_to_server.js | 6 | ||||
-rw-r--r-- | remoting/webapp/main.html | 5 | ||||
-rw-r--r-- | remoting/webapp/remoting.js | 3 | ||||
-rw-r--r-- | remoting/webapp/wcs.js | 31 | ||||
-rw-r--r-- | remoting/webapp/wcs_loader.js | 62 | ||||
-rw-r--r-- | remoting/webapp/wcs_sandbox.html | 18 | ||||
-rw-r--r-- | remoting/webapp/wcs_sandbox_container.js | 244 | ||||
-rw-r--r-- | remoting/webapp/wcs_sandbox_content.js | 222 | ||||
-rw-r--r-- | remoting/webapp/xhr_proxy.js | 93 |
14 files changed, 714 insertions, 124 deletions
diff --git a/remoting/webapp/all_js_load.gtestjs b/remoting/webapp/all_js_load.gtestjs index c752d5d..4e09e5e 100644 --- a/remoting/webapp/all_js_load.gtestjs +++ b/remoting/webapp/all_js_load.gtestjs @@ -46,6 +46,8 @@ AllJsLoadTest.prototype = { // |window.addEventListener| when loaded. 'oauth2.js', 'plugin_settings.js', + //'xhr_proxy.js', // Disabled because it accesses |XMLHttpRequest| + // when loaded, which is not available to tests. 'remoting.js', 'server_log_entry.js', 'stats_accumulator.js', @@ -57,6 +59,9 @@ AllJsLoadTest.prototype = { //'wcs_iq_client_proto.js', // Only used by jscompiler. 'wcs.js', 'wcs_loader.js', + //'wcs_sandbox_content.js', // Disabled because it calls + // |window.addEventListener| when loaded. + 'wcs_sandbox_container.js', 'xhr.js', ], }; diff --git a/remoting/webapp/appsv2.patch b/remoting/webapp/appsv2.patch index 24747c0..758bcb8 100644 --- a/remoting/webapp/appsv2.patch +++ b/remoting/webapp/appsv2.patch @@ -93,7 +93,7 @@ index a52b1da..fdae490 100644 "https://accounts.google.com/*", "https://www.googleapis.com/chromoting/*", "https://*.talkgadget.google.com/talkgadget/*", -@@ -31,12 +25,12 @@ +@@ -31,14 +25,17 @@ "clipboardRead", "clipboardWrite" ], @@ -112,6 +112,11 @@ index a52b1da..fdae490 100644 "requirements": { "plugins": { "npapi": false + } ++ }, ++ "sandbox": { ++ "pages": [ "wcs_sandbox.html" ] + } diff --git a/remoting/webapp/remoting.js b/remoting/webapp/remoting.js index a8ab35b..9c6df35 100644 --- a/remoting.js @@ -138,3 +143,13 @@ index a8ab35b..9c6df35 100644 remoting.hostSetupDialog = new remoting.HostSetupDialog(remoting.hostController); // Display the cached host list, then asynchronously update and re-display it. +diff --git a/remoting/webapp/xhr_proxy.js b/remoting/webapp/xhr_proxy.js +index 4c45780..653b481 100644 +--- a/xhr_proxy.js ++++ b/xhr_proxy.js +@@ -90,4 +90,4 @@ remoting.XMLHttpRequestProxy.prototype.DONE = 4; + + // Since the WCS driver code constructs XHRs directly, the only mechanism for + // proxying them is to replace the XMLHttpRequest constructor. +-//XMLHttpRequest = remoting.XMLHttpRequestProxy; ++XMLHttpRequest = remoting.XMLHttpRequestProxy; diff --git a/remoting/webapp/client_screen.js b/remoting/webapp/client_screen.js index a1a01b9..97bf94e 100644 --- a/remoting/webapp/client_screen.js +++ b/remoting/webapp/client_screen.js @@ -62,8 +62,16 @@ remoting.currentConnectionType = null; */ remoting.connectIt2Me = function() { remoting.currentConnectionType = remoting.ClientSession.Mode.IT2ME; - remoting.WcsLoader.load(connectIt2MeWithAccessToken_, - remoting.showErrorMessage); + /** @param {string} token */ + var startWcsAndConnect = function(token) { + remoting.wcsSandbox.setOnReady( + connectIt2MeWithAccessToken_.bind(null, token)); + remoting.wcsSandbox.setOnError(remoting.showErrorMessage); + remoting.wcsSandbox.setAccessToken(token); + startAccessTokenRefreshTimer_(); + }; + remoting.identity.callWithToken(startWcsAndConnect, + remoting.showErrorMessage); }; /** @@ -160,9 +168,10 @@ remoting.sendPrintScreen = function() { * report an error. * * @param {string} token The OAuth2 access token. + * @param {string} clientJid The full JID of the WCS client. * @return {void} Nothing. */ -function connectIt2MeWithAccessToken_(token) { +function connectIt2MeWithAccessToken_(token, clientJid) { var accessCode = document.getElementById('access-code-entry').value; remoting.accessCode = normalizeAccessCode_(accessCode); // At present, only 12-digit access codes are supported, of which the first @@ -176,7 +185,7 @@ function connectIt2MeWithAccessToken_(token) { } else { var supportId = remoting.accessCode.substring(0, kSupportIdLen); remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); - resolveSupportId(supportId, token); + resolveSupportId(clientJid, supportId, token); } } @@ -311,15 +320,17 @@ function retryConnectOrReportOffline_() { /** * Create the client session object and initiate the connection. * + * @param {string} clientJid The full JID of the WCS client. * @return {void} Nothing. */ -function startSession_() { +function startSession_(clientJid) { console.log('Starting session...'); var accessCode = document.getElementById('access-code-entry'); accessCode.value = ''; // The code has been validated and won't work again. remoting.clientSession = new remoting.ClientSession( - remoting.hostJid, remoting.hostPublicKey, + remoting.hostJid, clientJid, + remoting.hostPublicKey, remoting.accessCode, 'spake2_plain', '', remoting.ClientSession.Mode.IT2ME, onClientStateChange_); @@ -367,10 +378,11 @@ function setConnectionInterruptedButtonsText_() { /** * Parse the response from the server to a request to resolve a support id. * + * @param {string} clientJid The full JID of the WCS client. * @param {XMLHttpRequest} xhr The XMLHttpRequest object. * @return {void} Nothing. */ -function parseServerResponse_(xhr) { +function parseServerResponse_(clientJid, xhr) { remoting.supportHostsXhr_ = null; console.log('parseServerResponse: xhr =', xhr); if (xhr.status == 200) { @@ -381,7 +393,7 @@ function parseServerResponse_(xhr) { remoting.hostPublicKey = host.data.publicKey; var split = remoting.hostJid.split('/'); document.getElementById('connected-to').innerText = split[0]; - startSession_(); + startSession_(clientJid); return; } else { console.error('Invalid "support-hosts" response from server.'); @@ -415,10 +427,11 @@ function normalizeAccessCode_(accessCode) { /** * Initiate a request to the server to resolve a support ID. * + * @param {string} clientJid The full JID of the WCS client. * @param {string} supportId The canonicalized support ID. * @param {string} token The OAuth access token. */ -function resolveSupportId(supportId, token) { +function resolveSupportId(clientJid, supportId, token) { var headers = { 'Authorization': 'OAuth ' + token }; @@ -426,7 +439,7 @@ function resolveSupportId(supportId, token) { remoting.supportHostsXhr_ = remoting.xhr.get( 'https://www.googleapis.com/chromoting/v1/support-hosts/' + encodeURIComponent(supportId), - parseServerResponse_, + parseServerResponse_.bind(null, clientJid), '', headers); } @@ -497,23 +510,32 @@ remoting.connectMe2MeWithPin = function() { document.title = host.hostName + ' - ' + chrome.i18n.getMessage('PRODUCT_NAME'); - remoting.WcsLoader.load(connectMe2MeWithAccessToken_, - remoting.showErrorMessage); + /** @param {string} token */ + var startWcsAndConnect = function(token) { + remoting.wcsSandbox.setOnReady( + connectMe2MeWithAccessToken_.bind(null, token)); + remoting.wcsSandbox.setOnError(remoting.showErrorMessage); + remoting.wcsSandbox.setAccessToken(token); + startAccessTokenRefreshTimer_(); + }; + remoting.identity.callWithToken(startWcsAndConnect, + remoting.showErrorMessage); }; /** * Continue making the connection to a host, once WCS has initialized. * * @param {string} token The OAuth2 access token. + * @param {string} clientJid The full JID of the WCS client. * @return {void} Nothing. */ -function connectMe2MeWithAccessToken_(token) { +function connectMe2MeWithAccessToken_(token, clientJid) { /** @type {string} */ var pin = document.getElementById('pin-entry').value; remoting.clientSession = new remoting.ClientSession( - remoting.hostJid, remoting.hostPublicKey, + remoting.hostJid, clientJid, remoting.hostPublicKey, pin, 'spake2_hmac,spake2_plain', remoting.hostId, remoting.ClientSession.Mode.ME2ME, onClientStateChange_); // Don't log errors for cached JIDs. @@ -521,3 +543,31 @@ function connectMe2MeWithAccessToken_(token) { remoting.clientSession.createPluginAndConnect( document.getElementById('session-mode')); } + +/** @type {number} */ +remoting.wcsAccessTokenRefreshTimer = 0; + +function startAccessTokenRefreshTimer_() { + if (remoting.wcsAccessTokenRefreshTimer != 0) { + return; + } + + /** @param {string} token */ + var updateAccessToken = function(token) { + remoting.wcsSandbox.setAccessToken(token); + }; + /** @param {remoting.Error} error */ + var logError = function(error) { + console.error('updateAccessToken: Authentication failed: ' + error); + }; + var refreshAccessToken = function() { + remoting.identity.callWithToken(updateAccessToken, logError); + }; + /** + * A timer that polls for an updated access token. + * @type {number} + * @private + */ + remoting.wcsAccessTokenRefreshTimer = setInterval(refreshAccessToken, + 60 * 1000); +} diff --git a/remoting/webapp/client_session.js b/remoting/webapp/client_session.js index ee6da89..0c9b14d 100644 --- a/remoting/webapp/client_session.js +++ b/remoting/webapp/client_session.js @@ -24,6 +24,7 @@ var remoting = remoting || {}; /** * @param {string} hostJid The jid of the host to connect to. + * @param {string} clientJid The jid of the WCS client. * @param {string} hostPublicKey The base64 encoded version of the host's * public key. * @param {string} sharedSecret The access code for IT2Me or the PIN @@ -38,18 +39,19 @@ var remoting = remoting || {}; * The callback to invoke when the session changes state. * @constructor */ -remoting.ClientSession = function(hostJid, hostPublicKey, sharedSecret, +remoting.ClientSession = function(hostJid, clientJid, + hostPublicKey, sharedSecret, authenticationMethods, hostId, mode, onStateChange) { this.state = remoting.ClientSession.State.CREATED; this.hostJid = hostJid; + this.clientJid = clientJid; this.hostPublicKey = hostPublicKey; this.sharedSecret = sharedSecret; this.authenticationMethods = authenticationMethods; this.hostId = hostId; this.mode = mode; - this.clientJid = ''; this.sessionId = ''; /** @type {remoting.ClientPlugin} */ this.plugin = null; @@ -385,23 +387,21 @@ remoting.ClientSession.prototype.disconnect = function() { this.logToServer.logClientSessionStateChange( remoting.ClientSession.State.CLOSED, remoting.ClientSession.ConnectionError.NONE, this.mode); - if (remoting.wcs) { - remoting.wcs.setOnIq(function(stanza) {}); - this.sendIq_( - '<cli:iq ' + - 'to="' + this.hostJid + '" ' + - 'type="set" ' + - 'id="session-terminate" ' + - 'xmlns:cli="jabber:client">' + - '<jingle ' + - 'xmlns="urn:xmpp:jingle:1" ' + - 'action="session-terminate" ' + - 'initiator="' + this.clientJid + '" ' + - 'sid="' + this.sessionId + '">' + - '<reason><success/></reason>' + - '</jingle>' + - '</cli:iq>'); - } + remoting.wcsSandbox.setOnIq(null); + this.sendIq_( + '<cli:iq ' + + 'to="' + this.hostJid + '" ' + + 'type="set" ' + + 'id="session-terminate" ' + + 'xmlns:cli="jabber:client">' + + '<jingle ' + + 'xmlns="urn:xmpp:jingle:1" ' + + 'action="session-terminate" ' + + 'initiator="' + this.clientJid + '" ' + + 'sid="' + this.sessionId + '">' + + '<reason><success/></reason>' + + '</jingle>' + + '</cli:iq>'); this.removePlugin(); }; @@ -549,12 +549,7 @@ remoting.ClientSession.prototype.sendIq_ = function(msg) { console.log(remoting.timestamp(), remoting.formatIq.prettifySendIq(msg)); // Send the stanza. - if (remoting.wcs) { - remoting.wcs.sendIq(msg); - } else { - console.error('Tried to send IQ before WCS was ready.'); - this.setState_(remoting.ClientSession.State.FAILED); - } + remoting.wcsSandbox.sendIq(msg); }; /** @@ -564,10 +559,6 @@ remoting.ClientSession.prototype.sendIq_ = function(msg) { * @return {void} Nothing. */ remoting.ClientSession.prototype.connectPluginToWcs_ = function() { - this.clientJid = remoting.wcs.getJid(); - if (this.clientJid == '') { - console.error('Tried to connect without a full JID.'); - } remoting.formatIq.setJids(this.clientJid, this.hostJid); var plugin = this.plugin; var forwardIq = plugin.onIncomingIq.bind(plugin); @@ -591,7 +582,7 @@ remoting.ClientSession.prototype.connectPluginToWcs_ = function() { remoting.formatIq.prettifyReceiveIq(stanza)); forwardIq(stanza); } - remoting.wcs.setOnIq(onIncomingIq); + remoting.wcsSandbox.setOnIq(onIncomingIq); this.plugin.connect(this.hostJid, this.hostPublicKey, this.clientJid, this.sharedSecret, this.authenticationMethods, this.hostId); diff --git a/remoting/webapp/jscompiler_hacks.js b/remoting/webapp/jscompiler_hacks.js index c956e78..c2fc838 100644 --- a/remoting/webapp/jscompiler_hacks.js +++ b/remoting/webapp/jscompiler_hacks.js @@ -44,6 +44,9 @@ HTMLEmbedElement.prototype.height; /** @type {number} */ HTMLEmbedElement.prototype.width; +/** @type {Window} */ +HTMLIFrameElement.prototype.contentWindow; + /** @constructor */ var MutationRecord = function() {}; diff --git a/remoting/webapp/log_to_server.js b/remoting/webapp/log_to_server.js index 0ad8721..ff104d3 100644 --- a/remoting/webapp/log_to_server.js +++ b/remoting/webapp/log_to_server.js @@ -163,10 +163,6 @@ remoting.LogToServer.prototype.log = function(entry) { entry.toDebugLog(1); // Store a stanza for the entry. this.pendingEntries.push(entry.toStanza()); - // Stop if there's no connection to the server. - if (!remoting.wcs) { - return; - } // Send all pending entries to the server. console.log('Sending ' + this.pendingEntries.length + ' log ' + ((this.pendingEntries.length == 1) ? 'entry' : 'entries') + @@ -177,7 +173,7 @@ remoting.LogToServer.prototype.log = function(entry) { stanza += /** @type string */ this.pendingEntries.shift(); } stanza += '</gr:log></cli:iq>'; - remoting.wcs.sendIq(stanza); + remoting.wcsSandbox.sendIq(stanza); }; /** diff --git a/remoting/webapp/main.html b/remoting/webapp/main.html index d1b9a75..4d8aa94 100644 --- a/remoting/webapp/main.html +++ b/remoting/webapp/main.html @@ -45,8 +45,7 @@ found in the LICENSE file. <script src="toolbar.js"></script> <script src="ui_mode.js"></script> <script src="xhr.js"></script> - <script src="wcs.js"></script> - <script src="wcs_loader.js"></script> + <script src="wcs_sandbox_container.js"></script> <title i18n-content="PRODUCT_NAME"></title> </head> @@ -61,6 +60,8 @@ found in the LICENSE file. <div id="daemon-plugin-container"></div> + <iframe id="wcs-sandbox" src="wcs_sandbox.html" hidden></iframe> + <header data-ui-mode="home" hidden> <div> <img src="chromoting48.png"> diff --git a/remoting/webapp/remoting.js b/remoting/webapp/remoting.js index aa4c072..ae2e7b0 100644 --- a/remoting/webapp/remoting.js +++ b/remoting/webapp/remoting.js @@ -83,6 +83,9 @@ remoting.init = function() { } } ); + var sandbox = /** @type {HTMLIFrameElement} */ + document.getElementById('wcs-sandbox'); + remoting.wcsSandbox = new remoting.WcsSandboxContainer(sandbox.contentWindow); remoting.identity.getEmail(remoting.onEmail, remoting.showErrorMessage); diff --git a/remoting/webapp/wcs.js b/remoting/webapp/wcs.js index 6451420..3a6d1e3 100644 --- a/remoting/webapp/wcs.js +++ b/remoting/webapp/wcs.js @@ -20,8 +20,7 @@ remoting.wcs = null; * @constructor * @param {remoting.WcsIqClient} wcsIqClient The WCS client. * @param {string} token An OAuth2 access token. - * @param {function(boolean): void} onReady A function called when the WCS - * client has received a full JID. + * @param {function(string): void} onReady Called with the WCS client's JID. */ remoting.Wcs = function(wcsIqClient, token, onReady) { /** @@ -40,7 +39,7 @@ remoting.Wcs = function(wcsIqClient, token, onReady) { /** * The function called when the WCS client has received a full JID. - * @type {function(boolean): void} + * @type {?function(string): void} * @private */ this.onReady_ = onReady; @@ -52,23 +51,6 @@ remoting.Wcs = function(wcsIqClient, token, onReady) { */ this.clientFullJid_ = ''; - var updateAccessToken = this.updateAccessToken_.bind(this); - /** @param {remoting.Error} error */ - var onError = function(error) { - console.error('updateAccessToken: Authentication failed: ' + error); - }; - - /** - * A timer that polls for an updated access token. - * @type {number} - * @private - */ - this.pollForUpdatedToken_ = setInterval( - function() { - remoting.identity.callWithToken(updateAccessToken, onError); - }, - 60 * 1000); - /** * A function called when an IQ stanza is received. * @param {string} stanza The IQ stanza. @@ -88,9 +70,8 @@ remoting.Wcs = function(wcsIqClient, token, onReady) { * * @param {string} tokenNew A (possibly updated) access token. * @return {void} Nothing. - * @private */ -remoting.Wcs.prototype.updateAccessToken_ = function(tokenNew) { +remoting.Wcs.prototype.updateAccessToken = function(tokenNew) { if (tokenNew != this.token_) { this.token_ = tokenNew; this.wcsIqClient_.updateAccessToken(this.token_); @@ -110,8 +91,10 @@ remoting.Wcs.prototype.onMessage_ = function(msg) { } else if (msg[0] == 'cfj') { this.clientFullJid_ = msg[1]; console.log('Received JID: ' + this.clientFullJid_); - this.onReady_(true); - this.onReady_ = function(success) {}; + if (this.onReady_) { + this.onReady_(this.clientFullJid_); + this.onReady_ = null; + } } }; diff --git a/remoting/webapp/wcs_loader.js b/remoting/webapp/wcs_loader.js index 3a53848..c3ec7e1 100644 --- a/remoting/webapp/wcs_loader.js +++ b/remoting/webapp/wcs_loader.js @@ -30,26 +30,6 @@ remoting.WcsLoader = function() { }; /** - * Load WCS if necessary, then invoke the callback with an access token. - * - * @param {function(string): void} onReady The callback function, called with - * an OAuth2 access token when WCS has been loaded. - * @param {function(remoting.Error):void} onError Function to invoke with an - * error code on failure. - * @return {void} Nothing. - */ -remoting.WcsLoader.load = function(onReady, onError) { - if (!remoting.wcsLoader) { - remoting.wcsLoader = new remoting.WcsLoader(); - } - /** @param {string} token The OAuth2 access token. */ - var start = function(token) { - remoting.wcsLoader.start_(token, onReady, onError); - }; - remoting.identity.callWithToken(start, onError); -}; - -/** * The URL of the GTalk gadget. * @type {string} * @private @@ -65,13 +45,6 @@ remoting.WcsLoader.prototype.TALK_GADGET_URL_ = remoting.WcsLoader.prototype.SCRIPT_NODE_ID_ = 'wcs-script-node'; /** - * The attribute name indicating that the WCS has finished loading. - * @type {string} - * @private - */ -remoting.WcsLoader.prototype.SCRIPT_NODE_LOADED_FLAG_ = 'wcs-script-loaded'; - -/** * Starts loading the WCS IQ client. * * When it's loaded, construct remoting.wcs as a wrapper for it. @@ -80,34 +53,29 @@ remoting.WcsLoader.prototype.SCRIPT_NODE_LOADED_FLAG_ = 'wcs-script-loaded'; * * @param {string} token An OAuth2 access token. * @param {function(string): void} onReady The callback function, called with - * an OAuth2 access token when WCS has been loaded. + * a client JID when WCS has been loaded. * @param {function(remoting.Error):void} onError Function to invoke with an * error code on failure. * @return {void} Nothing. - * @private */ -remoting.WcsLoader.prototype.start_ = function(token, onReady, onError) { +remoting.WcsLoader.prototype.start = function(token, onReady, onError) { var node = document.getElementById(this.SCRIPT_NODE_ID_); - if (!node) { - // The first time, there will be no script node, so create one. - node = document.createElement('script'); - node.id = this.SCRIPT_NODE_ID_; - node.src = this.TALK_GADGET_URL_ + 'iq?access_token=' + token; - node.type = 'text/javascript'; - document.body.insertBefore(node, document.body.firstChild); - } else if (node.hasAttribute(this.SCRIPT_NODE_LOADED_FLAG_)) { - // Subsequently, explicitly invoke onReady if onload has already fired. - // TODO(jamiewalch): It's possible that the WCS client has not finished - // initializing. Add support for multiple callbacks to the remoting.Wcs - // class to address this. - onReady(token); + if (node) { + console.error('Multiple calls to WcsLoader.start are not allowed.'); + onError(remoting.Error.UNEXPECTED); return; } + + // Create a script node to load the WCS driver. + node = document.createElement('script'); + node.id = this.SCRIPT_NODE_ID_; + node.src = this.TALK_GADGET_URL_ + 'iq?access_token=' + token; + node.type = 'text/javascript'; + document.body.insertBefore(node, document.body.firstChild); + /** @type {remoting.WcsLoader} */ var that = this; var onLoad = function() { - var typedNode = /** @type {Element} */ (node); - typedNode.setAttribute(that.SCRIPT_NODE_LOADED_FLAG_, true); that.constructWcs_(token, onReady); }; var onLoadError = function(event) { @@ -143,7 +111,5 @@ remoting.WcsLoader.prototype.start_ = function(token, onReady, onError) { */ remoting.WcsLoader.prototype.constructWcs_ = function(token, onReady) { remoting.wcs = new remoting.Wcs( - remoting.wcsLoader.wcsIqClient, - token, - function() { onReady(token); }); + remoting.wcsLoader.wcsIqClient, token, onReady); }; diff --git a/remoting/webapp/wcs_sandbox.html b/remoting/webapp/wcs_sandbox.html new file mode 100644 index 0000000..bbc9d7f --- /dev/null +++ b/remoting/webapp/wcs_sandbox.html @@ -0,0 +1,18 @@ +<!doctype html> +<!-- +Copyright 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<html> + <head> + <meta charset="utf-8"> + <script src="xhr_proxy.js"></script> + <script src="wcs.js"></script> + <script src="wcs_loader.js"></script> + <script src="wcs_sandbox_content.js"></script> + </head> + <body> + </body> +</html> diff --git a/remoting/webapp/wcs_sandbox_container.js b/remoting/webapp/wcs_sandbox_container.js new file mode 100644 index 0000000..68f2fbd --- /dev/null +++ b/remoting/webapp/wcs_sandbox_container.js @@ -0,0 +1,244 @@ +/* Copyright 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +/** + * @fileoverview + * The application side of the application/sandbox WCS interface, used by the + * application to exchange messages with the sandbox. + */ + +'use strict'; + +/** @suppress {duplicate} */ +var remoting = remoting || {}; + +/** + * @param {Window} sandbox The Javascript Window object representing the + * sandboxed WCS driver. + * @constructor + */ +remoting.WcsSandboxContainer = function(sandbox) { + this.sandbox_ = sandbox; + /** @type {?function(string):void} */ + this.onReady_ = null; + /** @type {?function(remoting.Error):void} */ + this.onError_ = null; + /** @type {?function(string):void} */ + this.onIq_ = null; + /** @type {Object.<number, XMLHttpRequest>} */ + this.pendingXhrs_ = {}; + + window.addEventListener('message', this.onMessage_.bind(this), false); +}; + +/** + * @param {?function(string):void} onReady Callback invoked with the client JID + * when the WCS code has loaded. + * @return {void} Nothing. + */ +remoting.WcsSandboxContainer.prototype.setOnReady = function(onReady) { + this.onReady_ = onReady; +}; + +/** + * @param {?function(remoting.Error):void} onError Callback invoked if the WCS + * code cannot be loaded. + * @return {void} Nothing. + */ +remoting.WcsSandboxContainer.prototype.setOnError = function(onError) { + this.onError_ = onError; +}; + +/** + * @param {?function(string):void} onIq Callback invoked when an IQ stanza is + * received. + * @return {void} Nothing. + */ +remoting.WcsSandboxContainer.prototype.setOnIq = function(onIq) { + this.onIq_ = onIq; +}; + +/** + * @param {string} token The access token. + * @return {void} + */ +remoting.WcsSandboxContainer.prototype.setAccessToken = function(token) { + var message = { + 'command': 'setAccessToken', + 'token': token + }; + this.sandbox_.postMessage(message, '*'); +}; + +/** + * @param {string} stanza The IQ stanza to send. + * @return {void} + */ +remoting.WcsSandboxContainer.prototype.sendIq = function(stanza) { + var message = { + 'command': 'sendIq', + 'stanza': stanza + }; + this.sandbox_.postMessage(message, '*'); +}; + +/** + * Event handler to process messages from the sandbox. + * + * @param {Event} event + */ +remoting.WcsSandboxContainer.prototype.onMessage_ = function(event) { + switch (event.data['command']) { + + case 'onReady': + /** @type {string} */ + var clientJid = event.data['clientJid']; + if (clientJid === undefined) { + console.error('onReady: missing client JID'); + break; + } + if (this.onReady_) { + this.onReady_(clientJid); + } + break; + + case 'onError': + /** @type {remoting.Error} */ + var error = event.data['error']; + if (error === undefined) { + console.error('onError: missing error code'); + break; + } + this.onError_(error); + break; + + case 'onIq': + /** @type {string} */ + var stanza = event.data['stanza']; + if (stanza === undefined) { + console.error('onIq: missing IQ stanza'); + break; + } + if (this.onIq_) { + this.onIq_(stanza); + } + break; + + case 'sendXhr': + /** @type {number} */ + var id = event.data['id']; + if (id === undefined) { + console.error('sendXhr: missing id'); + break; + } + /** @type {Object} */ + var parameters = event.data['parameters']; + if (parameters === undefined) { + console.error('sendXhr: missing parameters'); + break; + } + /** @type {string} */ + var method = parameters['method']; + if (method === undefined) { + console.error('sendXhr: missing method'); + break; + } + /** @type {string} */ + var url = parameters['url']; + if (url === undefined) { + console.error('sendXhr: missing url'); + break; + } + /** @type {string} */ + var data = parameters['data']; + if (data === undefined) { + console.error('sendXhr: missing data'); + break; + } + /** @type {string|undefined}*/ + var user = parameters['user']; + /** @type {string|undefined}*/ + var password = parameters['password']; + var xhr = new XMLHttpRequest; + this.pendingXhrs_[id] = xhr; + xhr.open(method, url, true, user, password); + /** @type {Object} */ + var headers = parameters['headers']; + if (headers) { + for (var header in headers) { + xhr.setRequestHeader(header, headers[header]); + } + } + xhr.onreadystatechange = this.onReadyStateChange_.bind(this, id); + xhr.send(data); + break; + + case 'abortXhr': + var id = event.data['id']; + if (id === undefined) { + console.error('abortXhr: missing id'); + break; + } + var xhr = this.pendingXhrs_[id] + if (!xhr) { + // It's possible for an abort and a reply to cross each other on the + // IPC channel. In that case, we silently ignore the abort. + break; + } + xhr.abort(); + break; + + default: + console.error('Unexpected message:', event.data['command'], event.data); + } +}; + +/** + * Return a "copy" of an XHR object suitable for postMessage. Specifically, + * remove all non-serializable members such as functions. + * + * @param {XMLHttpRequest} xhr The XHR to serialize. + * @return {Object} A serializable version of the input. + */ +function sanitizeXhr_(xhr) { + /** @type {Object} */ + var result = { + readyState: xhr.readyState, + response: xhr.response, + responseText: xhr.responseText, + responseType: xhr.responseType, + responseXML: xhr.responseXML, + status: xhr.status, + statusText: xhr.statusText, + withCredentials: xhr.withCredentials + }; + return result; +} + +/** + * @param {number} id The unique ID of the XHR for which the state has changed. + * @private + */ +remoting.WcsSandboxContainer.prototype.onReadyStateChange_ = function(id) { + var xhr = this.pendingXhrs_[id]; + if (!xhr) { + // XHRs are only removed when they have completed, in which case no + // further callbacks should be received. + console.error('Unexpected callback for xhr', id); + return; + } + var message = { + 'command': 'xhrStateChange', + 'id': id, + 'xhr': sanitizeXhr_(xhr) + }; + this.sandbox_.postMessage(message, '*'); + if (xhr.readyState == 4) { + delete this.pendingXhrs_[id]; + } +} + +/** @type {remoting.WcsSandboxContainer} */ +remoting.wcsSandbox = null;
\ No newline at end of file diff --git a/remoting/webapp/wcs_sandbox_content.js b/remoting/webapp/wcs_sandbox_content.js new file mode 100644 index 0000000..c297900 --- /dev/null +++ b/remoting/webapp/wcs_sandbox_content.js @@ -0,0 +1,222 @@ +/* Copyright 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +/** + * @fileoverview + * The sandbox side of the application/sandbox WCS interface, used by the + * sandbox to exchange messages with the application. + */ + +'use strict'; + +/** @suppress {duplicate} */ +var remoting = remoting || {}; + +/** @constructor */ +remoting.WcsSandboxContent = function() { + /** + * @type {Window} + * @private + */ + this.parentWindow_ = null; + /** + * @type {number} + * @private + */ + this.nextXhrId_ = 0; + /** + * @type {Object.<number, XMLHttpRequest>} + * @private + */ + this.pendingXhrs_ = {}; + + window.addEventListener('message', this.onMessage_.bind(this), false); +}; + +/** + * Event handler to process messages from the application. + * + * @param {Event} event + */ +remoting.WcsSandboxContent.prototype.onMessage_ = function(event) { + this.parentWindow_ = event.source; + + switch (event.data['command']) { + + case 'sendIq': + /** @type {string} */ + var stanza = event.data['stanza']; + if (stanza === undefined) { + console.error('sendIq: missing IQ stanza.'); + break; + } + if (remoting.wcs) { + remoting.wcs.sendIq(stanza); + } else { + console.error('Dropping IQ stanza:', stanza); + } + break; + + case 'setAccessToken': + /** @type {string} */ + var token = event.data['token']; + if (token === undefined) { + console.error('setAccessToken: missing access token.'); + break; + } + // The WCS driver JS requires that remoting.wcsLoader be a global + // variable, so it can't be a member of this class. + // TODO(jamiewalch): remoting.wcs doesn't need to be global and should + // be made a member (http://crbug.com/172348). + if (remoting.wcs) { + remoting.wcs.updateAccessToken(token); + } else if (!remoting.wcsLoader) { + remoting.wcsLoader = new remoting.WcsLoader(); + remoting.wcsLoader.start(token, + this.onReady_.bind(this), + this.onError_.bind(this)); + } + break; + + case 'xhrStateChange': + /** @type {number} */ + var id = event.data['id']; + if (id === undefined) { + console.error('xhrStateChange: missing id.'); + break; + } + var pendingXhr = this.pendingXhrs_[id]; + if (!pendingXhr) { + console.error('xhrStateChange: unrecognized id:', id); + break; + } + /** @type {XMLHttpRequest} */ + var xhr = event.data['xhr']; + if (xhr === undefined) { + console.error('xhrStateChange: missing xhr'); + break; + } + for (var member in xhr) { + pendingXhr[member] = xhr[member]; + } + if (xhr.readyState == 4) { + delete this.pendingXhrs_[id]; + } + if (pendingXhr.onreadystatechange) { + pendingXhr.onreadystatechange(); + } + break; + + default: + console.error('Unexpected message:', event.data['command'], event.data); + } +}; + +/** + * Callback method to indicate that the WCS driver has loaded and provide the + * full JID of the client. + * + * @param {string} clientJid The full JID of the WCS client. + * @private + */ +remoting.WcsSandboxContent.prototype.onReady_ = function(clientJid) { + remoting.wcs.setOnIq(this.onIq_.bind(this)); + var message = { + 'command': 'onReady', + 'clientJid': clientJid + }; + this.parentWindow_.postMessage(message, '*'); +}; + +/** + * Callback method to indicate that something went wrong loading the WCS driver. + * + * @param {remoting.Error} error Details of the error. + * @private + */ +remoting.WcsSandboxContent.prototype.onError_ = function(error) { + var message = { + 'command': 'onError', + 'error': error + }; + this.parentWindow_.postMessage(message, '*'); +}; + +/** + * Forward an XHR to the container process to send. This is analogous to XHR's + * send method. + * + * @param {remoting.XMLHttpRequestProxy} xhr The XHR to send. + * @return {number} The unique ID allocated to the XHR. Used to abort it. + */ +remoting.WcsSandboxContent.prototype.sendXhr = function(xhr) { + var id = this.nextXhrId_++; + this.pendingXhrs_[id] = xhr; + var message = { + 'command': 'sendXhr', + 'id': id, + 'parameters': xhr.sandbox_ipc + }; + this.parentWindow_.postMessage(message, '*'); + delete xhr.sandbox_ipc; + return id; +}; + +/** + * Abort a forwarded XHR. This is analogous to XHR's abort method. + * + * @param {number} id The unique ID of the XHR to abort, as returned by sendXhr. + */ +remoting.WcsSandboxContent.prototype.abortXhr = function(id) { + if (!this.pendingXhrs_[id]) { + // The XHR is removed when it reaches the "ready" state. Calling abort + // subsequently is unusual, but legal, so just silently ignore the request + // in this case. + return; + } + var message = { + 'command': 'abortXhr', + 'id': id + }; + this.parentWindow_.postMessage(message, '*'); +}; + +/** + * Callback to indicate than an IQ stanza has been received from the WCS + * driver, and should be forwarded to the main process. + * + * @param {string} stanza + * @private + */ +remoting.WcsSandboxContent.prototype.onIq_ = function(stanza) { + remoting.wcs.setOnIq(this.onIq_.bind(this)); + var message = { + 'command': 'onIq', + 'stanza': stanza + }; + this.parentWindow_.postMessage(message, '*'); +}; + +/** + * Entry point for the WCS sandbox process. + */ +function onSandboxInit() { + // The WCS code registers for a couple of events that aren't supported in + // Apps V2, so ignore those for now. + var oldAEL = window.addEventListener; + window.addEventListener = function(type, listener, useCapture) { + if (type == 'beforeunload' || type == 'unload') { + return; + } + oldAEL(type, listener, useCapture); + }; + + remoting.sandboxContent = new remoting.WcsSandboxContent(); +} + +window.addEventListener('load', onSandboxInit, false); + +/** @type {remoting.WcsSandboxContent} */ +remoting.sandboxContent = null;
\ No newline at end of file diff --git a/remoting/webapp/xhr_proxy.js b/remoting/webapp/xhr_proxy.js new file mode 100644 index 0000000..4c45780 --- /dev/null +++ b/remoting/webapp/xhr_proxy.js @@ -0,0 +1,93 @@ +/* Copyright 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +/** + * @fileoverview + * The sandbox isn't allowed to make XHRs, so they have to be proxied to the + * main process. The XMLHttpRequestProxy class is API-compatible with the + * XMLHttpRequest class, but forwards the requests to the main process where + * they can be serviced. The forwarding of XHRs and responses is handled by + * the WcsSandboxContent class; this class is just a thin wrapper to hook XHR + * creations by JS running in the sandbox. + * + * Because XMLHttpRequest is implemented natively, and because the intent is + * to replace its functionality entirely, prototype linking is not a suitable + * approach here, so much of the interface definition is duplicated from the + * w3c specification: http://www.w3.org/TR/XMLHttpRequest + */ + +'use strict'; + +/** @suppress {duplicate} */ +var remoting = remoting || {}; + +/** + * @constructor + * @extends {XMLHttpRequest} + */ +remoting.XMLHttpRequestProxy = function() { + /** + * @type {{headers: Object}} + */ + this.sandbox_ipc = { + headers: {} + }; + /** + * @type {number} + * @private + */ + this.xhr_id_ = -1; +}; + +remoting.XMLHttpRequestProxy.prototype.open = function( + method, url, async, user, password) { + if (!async) { + console.warn('Synchronous XHRs are not supported.'); + } + this.sandbox_ipc.method = method; + this.sandbox_ipc.url = url.toString(); + this.sandbox_ipc.user = user; + this.sandbox_ipc.password = password; +}; + +remoting.XMLHttpRequestProxy.prototype.send = function(data) { + if (remoting.sandboxContent) { + this.sandbox_ipc.data = data; + this.xhr_id_ = remoting.sandboxContent.sendXhr(this); + } +}; + +remoting.XMLHttpRequestProxy.prototype.setRequestHeader = function( + header, value) { + this.sandbox_ipc.headers[header] = value; +}; + +remoting.XMLHttpRequestProxy.prototype.abort = function() { + if (this.xhr_id_ != -1) { + remoting.sandboxContent.abortXhr(this.xhr_id_); + } +}; + +remoting.XMLHttpRequestProxy.prototype.getResponseHeader = function(header) { + console.error('Sandbox: unproxied getResponseHeader(' + header + ') called.'); +}; + +remoting.XMLHttpRequestProxy.prototype.getAllResponseHeaders = function() { + console.error('Sandbox: unproxied getAllResponseHeaders called.'); +}; + +remoting.XMLHttpRequestProxy.prototype.overrideMimeType = function() { + console.error('Sandbox: unproxied overrideMimeType called.'); +}; + +remoting.XMLHttpRequestProxy.prototype.UNSENT = 0; +remoting.XMLHttpRequestProxy.prototype.OPENED = 1; +remoting.XMLHttpRequestProxy.prototype.HEADERS_RECEIVED = 2; +remoting.XMLHttpRequestProxy.prototype.LOADING = 3; +remoting.XMLHttpRequestProxy.prototype.DONE = 4; + +// Since the WCS driver code constructs XHRs directly, the only mechanism for +// proxying them is to replace the XMLHttpRequest constructor. +//XMLHttpRequest = remoting.XMLHttpRequestProxy; |