directtransport.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. // Copyright 2013 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Provides an implementation of a transport that can call methods
  16. * directly on a frame. Useful if you want to use XPC for crossdomain messaging
  17. * (using another transport), or same domain messaging (using this transport).
  18. */
  19. goog.provide('goog.net.xpc.DirectTransport');
  20. goog.require('goog.Timer');
  21. goog.require('goog.async.Deferred');
  22. goog.require('goog.events.EventHandler');
  23. goog.require('goog.log');
  24. goog.require('goog.net.xpc');
  25. goog.require('goog.net.xpc.CfgFields');
  26. goog.require('goog.net.xpc.CrossPageChannelRole');
  27. goog.require('goog.net.xpc.Transport');
  28. goog.require('goog.net.xpc.TransportTypes');
  29. goog.require('goog.object');
  30. goog.scope(function() {
  31. var CfgFields = goog.net.xpc.CfgFields;
  32. var CrossPageChannelRole = goog.net.xpc.CrossPageChannelRole;
  33. var Deferred = goog.async.Deferred;
  34. var EventHandler = goog.events.EventHandler;
  35. var Timer = goog.Timer;
  36. var Transport = goog.net.xpc.Transport;
  37. /**
  38. * A direct window to window method transport.
  39. *
  40. * If the windows are in the same security context, this transport calls
  41. * directly into the other window without using any additional mechanism. This
  42. * is mainly used in scenarios where you want to optionally use a cross domain
  43. * transport in cross security context situations, or optionally use a direct
  44. * transport in same security context situations.
  45. *
  46. * Note: Global properties are exported by using this transport. One to
  47. * communicate with the other window by, currently crosswindowmessaging.channel,
  48. * and by using goog.getUid on window, currently closure_uid_[0-9]+.
  49. *
  50. * @param {!goog.net.xpc.CrossPageChannel} channel The channel this
  51. * transport belongs to.
  52. * @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use for
  53. * finding the correct window/document. If omitted, uses the current
  54. * document.
  55. * @constructor
  56. * @extends {Transport}
  57. */
  58. goog.net.xpc.DirectTransport = function(channel, opt_domHelper) {
  59. goog.net.xpc.DirectTransport.base(this, 'constructor', opt_domHelper);
  60. /**
  61. * The channel this transport belongs to.
  62. * @private {!goog.net.xpc.CrossPageChannel}
  63. */
  64. this.channel_ = channel;
  65. /** @private {!EventHandler<!goog.net.xpc.DirectTransport>} */
  66. this.eventHandler_ = new EventHandler(this);
  67. this.registerDisposable(this.eventHandler_);
  68. /**
  69. * Timer for connection reattempts.
  70. * @private {!Timer}
  71. */
  72. this.maybeAttemptToConnectTimer_ = new Timer(
  73. DirectTransport.CONNECTION_ATTEMPT_INTERVAL_MS_, this.getWindow());
  74. this.registerDisposable(this.maybeAttemptToConnectTimer_);
  75. /**
  76. * Fires once we've received our SETUP_ACK message.
  77. * @private {!Deferred}
  78. */
  79. this.setupAckReceived_ = new Deferred();
  80. /**
  81. * Fires once we've sent our SETUP_ACK message.
  82. * @private {!Deferred}
  83. */
  84. this.setupAckSent_ = new Deferred();
  85. /**
  86. * Fires once we're marked connected.
  87. * @private {!Deferred}
  88. */
  89. this.connected_ = new Deferred();
  90. /**
  91. * The unique ID of this side of the connection. Used to determine when a peer
  92. * is reloaded.
  93. * @private {string}
  94. */
  95. this.endpointId_ = goog.net.xpc.getRandomString(10);
  96. /**
  97. * The unique ID of the peer. If we get a message from a peer with an ID we
  98. * don't expect, we reset the connection.
  99. * @private {?string}
  100. */
  101. this.peerEndpointId_ = null;
  102. /**
  103. * The map of sending messages.
  104. * @private {Object}
  105. */
  106. this.asyncSendsMap_ = {};
  107. /**
  108. * The original channel name.
  109. * @private {string}
  110. */
  111. this.originalChannelName_ = this.channel_.name;
  112. // We reconfigure the channel name to include the role so that we can
  113. // communicate in the same window between the different roles on the
  114. // same channel.
  115. this.channel_.updateChannelNameAndCatalog(
  116. DirectTransport.getRoledChannelName_(
  117. this.channel_.name, this.channel_.getRole()));
  118. /**
  119. * Flag indicating if this instance of the transport has been initialized.
  120. * @private {boolean}
  121. */
  122. this.initialized_ = false;
  123. // We don't want to mark ourselves connected until we have sent whatever
  124. // message will cause our counterpart in the other frame to also declare
  125. // itself connected, if there is such a message. Otherwise we risk a user
  126. // message being sent in advance of that message, and it being discarded.
  127. // Two sided handshake:
  128. // SETUP_ACK has to have been received, and sent.
  129. this.connected_.awaitDeferred(this.setupAckReceived_);
  130. this.connected_.awaitDeferred(this.setupAckSent_);
  131. this.connected_.addCallback(this.notifyConnected_, this);
  132. this.connected_.callback(true);
  133. this.eventHandler_.listen(
  134. this.maybeAttemptToConnectTimer_, Timer.TICK,
  135. this.maybeAttemptToConnect_);
  136. goog.log.info(
  137. goog.net.xpc.logger,
  138. 'DirectTransport created. role=' + this.channel_.getRole());
  139. };
  140. goog.inherits(goog.net.xpc.DirectTransport, Transport);
  141. var DirectTransport = goog.net.xpc.DirectTransport;
  142. /**
  143. * @private {number}
  144. * @const
  145. */
  146. DirectTransport.CONNECTION_ATTEMPT_INTERVAL_MS_ = 100;
  147. /**
  148. * The delay to notify the xpc of a successful connection. This is used
  149. * to allow both parties to be connected if one party's connection callback
  150. * invokes an immediate send.
  151. * @private {number}
  152. * @const
  153. */
  154. DirectTransport.CONNECTION_DELAY_INTERVAL_MS_ = 0;
  155. /**
  156. * @param {!Window} peerWindow The peer window to check if DirectTranport is
  157. * supported on.
  158. * @return {boolean} Whether this transport is supported.
  159. */
  160. DirectTransport.isSupported = function(peerWindow) {
  161. try {
  162. return window.document.domain == peerWindow.document.domain;
  163. } catch (e) {
  164. return false;
  165. }
  166. };
  167. /**
  168. * Tracks the number of DirectTransport channels that have been
  169. * initialized but not disposed yet in a map keyed by the UID of the window
  170. * object. This allows for multiple windows to be initiallized and listening
  171. * for messages.
  172. * @private {!Object<number>}
  173. */
  174. DirectTransport.activeCount_ = {};
  175. /**
  176. * Path of global message proxy.
  177. * @private {string}
  178. * @const
  179. */
  180. // TODO(user): Make this configurable using the CfgFields.
  181. DirectTransport.GLOBAL_TRANPORT_PATH_ = 'crosswindowmessaging.channel';
  182. /**
  183. * The delimiter used for transport service messages.
  184. * @private {string}
  185. * @const
  186. */
  187. DirectTransport.MESSAGE_DELIMITER_ = ',';
  188. /**
  189. * Initializes this transport. Registers a method for 'message'-events in the
  190. * global scope.
  191. * @param {!Window} listenWindow The window to listen to events on.
  192. * @private
  193. */
  194. DirectTransport.initialize_ = function(listenWindow) {
  195. var uid = goog.getUid(listenWindow);
  196. var value = DirectTransport.activeCount_[uid] || 0;
  197. if (value == 0) {
  198. // Set up a handler on the window to proxy messages to class.
  199. var globalProxy = goog.getObjectByName(
  200. DirectTransport.GLOBAL_TRANPORT_PATH_, listenWindow);
  201. if (globalProxy == null) {
  202. goog.exportSymbol(
  203. DirectTransport.GLOBAL_TRANPORT_PATH_,
  204. DirectTransport.messageReceivedHandler_, listenWindow);
  205. }
  206. }
  207. DirectTransport.activeCount_[uid]++;
  208. };
  209. /**
  210. * @param {string} channelName The channel name.
  211. * @param {string|number} role The role.
  212. * @return {string} The formatted channel name including role.
  213. * @private
  214. */
  215. DirectTransport.getRoledChannelName_ = function(channelName, role) {
  216. return channelName + '_' + role;
  217. };
  218. /**
  219. * @param {!Object} literal The literal unrenamed message.
  220. * @return {boolean} Whether the message was successfully delivered to a
  221. * channel.
  222. * @private
  223. */
  224. DirectTransport.messageReceivedHandler_ = function(literal) {
  225. var msg = DirectTransport.Message_.fromLiteral(literal);
  226. var channelName = msg.channelName;
  227. var service = msg.service;
  228. var payload = msg.payload;
  229. goog.log.fine(
  230. goog.net.xpc.logger, 'messageReceived: channel=' + channelName +
  231. ', service=' + service + ', payload=' + payload);
  232. // Attempt to deliver message to the channel. Keep in mind that it may not
  233. // exist for several reasons, including but not limited to:
  234. // - a malformed message
  235. // - the channel simply has not been created
  236. // - channel was created in a different namespace
  237. // - message was sent to the wrong window
  238. // - channel has become stale (e.g. caching iframes and back clicks)
  239. var channel = goog.net.xpc.channels[channelName];
  240. if (channel) {
  241. channel.xpcDeliver(service, payload);
  242. return true;
  243. }
  244. var transportMessageType = DirectTransport.parseTransportPayload_(payload)[0];
  245. // Check if there are any stale channel names that can be updated.
  246. for (var staleChannelName in goog.net.xpc.channels) {
  247. var staleChannel = goog.net.xpc.channels[staleChannelName];
  248. if (staleChannel.getRole() == CrossPageChannelRole.INNER &&
  249. !staleChannel.isConnected() &&
  250. service == goog.net.xpc.TRANSPORT_SERVICE_ &&
  251. transportMessageType == goog.net.xpc.SETUP) {
  252. // Inner peer received SETUP message but channel names did not match.
  253. // Start using the channel name sent from outer peer. The channel name
  254. // of the inner peer can easily become out of date, as iframe's and their
  255. // JS state get cached in many browsers upon page reload or history
  256. // navigation (particularly Firefox 1.5+).
  257. staleChannel.updateChannelNameAndCatalog(channelName);
  258. staleChannel.xpcDeliver(service, payload);
  259. return true;
  260. }
  261. }
  262. // Failed to find a channel to deliver this message to, so simply ignore it.
  263. goog.log.info(goog.net.xpc.logger, 'channel name mismatch; message ignored.');
  264. return false;
  265. };
  266. /**
  267. * The transport type.
  268. * @type {number}
  269. * @override
  270. */
  271. DirectTransport.prototype.transportType = goog.net.xpc.TransportTypes.DIRECT;
  272. /**
  273. * Handles transport service messages.
  274. * @param {string} payload The message content.
  275. * @override
  276. */
  277. DirectTransport.prototype.transportServiceHandler = function(payload) {
  278. var transportParts = DirectTransport.parseTransportPayload_(payload);
  279. var transportMessageType = transportParts[0];
  280. var peerEndpointId = transportParts[1];
  281. switch (transportMessageType) {
  282. case goog.net.xpc.SETUP_ACK_:
  283. if (!this.setupAckReceived_.hasFired()) {
  284. this.setupAckReceived_.callback(true);
  285. }
  286. break;
  287. case goog.net.xpc.SETUP:
  288. this.sendSetupAckMessage_();
  289. if ((this.peerEndpointId_ != null) &&
  290. (this.peerEndpointId_ != peerEndpointId)) {
  291. // Send a new SETUP message since the peer has been replaced.
  292. goog.log.info(
  293. goog.net.xpc.logger,
  294. 'Sending SETUP and changing peer ID to: ' + peerEndpointId);
  295. this.sendSetupMessage_();
  296. }
  297. this.peerEndpointId_ = peerEndpointId;
  298. break;
  299. }
  300. };
  301. /**
  302. * Sends a SETUP transport service message.
  303. * @private
  304. */
  305. DirectTransport.prototype.sendSetupMessage_ = function() {
  306. // Although we could send real objects, since some other transports are
  307. // limited to strings we also keep this requirement.
  308. var payload = goog.net.xpc.SETUP;
  309. payload += DirectTransport.MESSAGE_DELIMITER_;
  310. payload += this.endpointId_;
  311. this.send(goog.net.xpc.TRANSPORT_SERVICE_, payload);
  312. };
  313. /**
  314. * Sends a SETUP_ACK transport service message.
  315. * @private
  316. */
  317. DirectTransport.prototype.sendSetupAckMessage_ = function() {
  318. this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP_ACK_);
  319. if (!this.setupAckSent_.hasFired()) {
  320. this.setupAckSent_.callback(true);
  321. }
  322. };
  323. /** @override */
  324. DirectTransport.prototype.connect = function() {
  325. var win = this.getWindow();
  326. if (win) {
  327. DirectTransport.initialize_(win);
  328. this.initialized_ = true;
  329. this.maybeAttemptToConnect_();
  330. } else {
  331. goog.log.fine(goog.net.xpc.logger, 'connect(): no window to initialize.');
  332. }
  333. };
  334. /**
  335. * Connects to other peer. In the case of the outer peer, the setup messages are
  336. * likely sent before the inner peer is ready to receive them. Therefore, this
  337. * function will continue trying to send the SETUP message until the inner peer
  338. * responds. In the case of the inner peer, it will occasionally have its
  339. * channel name fall out of sync with the outer peer, particularly during
  340. * soft-reloads and history navigations.
  341. * @private
  342. */
  343. DirectTransport.prototype.maybeAttemptToConnect_ = function() {
  344. if (this.channel_.isConnected()) {
  345. this.maybeAttemptToConnectTimer_.stop();
  346. return;
  347. }
  348. this.maybeAttemptToConnectTimer_.start();
  349. this.sendSetupMessage_();
  350. };
  351. /**
  352. * Prepares to send a message.
  353. * @param {string} service The name of the service the message is to be
  354. * delivered to.
  355. * @param {string} payload The message content.
  356. * @override
  357. */
  358. DirectTransport.prototype.send = function(service, payload) {
  359. if (!this.channel_.getPeerWindowObject()) {
  360. goog.log.fine(goog.net.xpc.logger, 'send(): window not ready');
  361. return;
  362. }
  363. var channelName = DirectTransport.getRoledChannelName_(
  364. this.originalChannelName_, this.getPeerRole_());
  365. var message = new DirectTransport.Message_(channelName, service, payload);
  366. if (this.channel_.getConfig()[CfgFields.DIRECT_TRANSPORT_SYNC_MODE]) {
  367. this.executeScheduledSend_(message);
  368. } else {
  369. // Note: goog.async.nextTick doesn't support cancelling or disposal so
  370. // leaving as 0ms timer, though this may have performance implications.
  371. this.asyncSendsMap_[goog.getUid(message)] =
  372. Timer.callOnce(goog.bind(this.executeScheduledSend_, this, message), 0);
  373. }
  374. };
  375. /**
  376. * Sends the message.
  377. * @param {!DirectTransport.Message_} message The message to send.
  378. * @private
  379. */
  380. DirectTransport.prototype.executeScheduledSend_ = function(message) {
  381. var messageId = goog.getUid(message);
  382. if (this.asyncSendsMap_[messageId]) {
  383. delete this.asyncSendsMap_[messageId];
  384. }
  385. try {
  386. var peerProxy = goog.getObjectByName(
  387. DirectTransport.GLOBAL_TRANPORT_PATH_,
  388. this.channel_.getPeerWindowObject());
  389. } catch (error) {
  390. goog.log.warning(
  391. goog.net.xpc.logger, 'Can\'t access other window, ignoring.', error);
  392. return;
  393. }
  394. if (goog.isNull(peerProxy)) {
  395. goog.log.warning(
  396. goog.net.xpc.logger, 'Peer window had no global function.');
  397. return;
  398. }
  399. try {
  400. peerProxy(message.toLiteral());
  401. goog.log.info(
  402. goog.net.xpc.logger, 'send(): channelName=' + message.channelName +
  403. ' service=' + message.service + ' payload=' + message.payload);
  404. } catch (error) {
  405. goog.log.warning(
  406. goog.net.xpc.logger, 'Error performing call, ignoring.', error);
  407. }
  408. };
  409. /**
  410. * @return {goog.net.xpc.CrossPageChannelRole} The role of peer channel (either
  411. * inner or outer).
  412. * @private
  413. */
  414. DirectTransport.prototype.getPeerRole_ = function() {
  415. var role = this.channel_.getRole();
  416. return role == goog.net.xpc.CrossPageChannelRole.OUTER ?
  417. goog.net.xpc.CrossPageChannelRole.INNER :
  418. goog.net.xpc.CrossPageChannelRole.OUTER;
  419. };
  420. /**
  421. * Notifies the channel that this transport is connected.
  422. * @private
  423. */
  424. DirectTransport.prototype.notifyConnected_ = function() {
  425. // Add a delay as the connection callback will break if this transport is
  426. // synchronous and the callback invokes send() immediately.
  427. this.channel_.notifyConnected(
  428. this.channel_.getConfig()[CfgFields.DIRECT_TRANSPORT_SYNC_MODE] ?
  429. DirectTransport.CONNECTION_DELAY_INTERVAL_MS_ :
  430. 0);
  431. };
  432. /** @override */
  433. DirectTransport.prototype.disposeInternal = function() {
  434. if (this.initialized_) {
  435. var listenWindow = this.getWindow();
  436. var uid = goog.getUid(listenWindow);
  437. var value = --DirectTransport.activeCount_[uid];
  438. if (value == 1) {
  439. goog.exportSymbol(
  440. DirectTransport.GLOBAL_TRANPORT_PATH_, null, listenWindow);
  441. }
  442. }
  443. if (this.asyncSendsMap_) {
  444. goog.object.forEach(
  445. this.asyncSendsMap_, function(timerId) { Timer.clear(timerId); });
  446. this.asyncSendsMap_ = null;
  447. }
  448. // Deferred's aren't disposables.
  449. if (this.setupAckReceived_) {
  450. this.setupAckReceived_.cancel();
  451. delete this.setupAckReceived_;
  452. }
  453. if (this.setupAckSent_) {
  454. this.setupAckSent_.cancel();
  455. delete this.setupAckSent_;
  456. }
  457. if (this.connected_) {
  458. this.connected_.cancel();
  459. delete this.connected_;
  460. }
  461. DirectTransport.base(this, 'disposeInternal');
  462. };
  463. /**
  464. * Parses a transport service payload message.
  465. * @param {string} payload The payload.
  466. * @return {!Array<?string>} An array with the message type as the first member
  467. * and the endpoint id as the second, if one was sent, or null otherwise.
  468. * @private
  469. */
  470. DirectTransport.parseTransportPayload_ = function(payload) {
  471. var transportParts = /** @type {!Array<?string>} */ (
  472. payload.split(DirectTransport.MESSAGE_DELIMITER_));
  473. transportParts[1] = transportParts[1] || null; // Usually endpointId.
  474. return transportParts;
  475. };
  476. /**
  477. * Message container that gets passed back and forth between windows.
  478. * @param {string} channelName The channel name to tranport messages on.
  479. * @param {string} service The service to send the payload to.
  480. * @param {string} payload The payload to send.
  481. * @constructor
  482. * @struct
  483. * @private
  484. */
  485. DirectTransport.Message_ = function(channelName, service, payload) {
  486. /**
  487. * The name of the channel.
  488. * @type {string}
  489. */
  490. this.channelName = channelName;
  491. /**
  492. * The service on the channel.
  493. * @type {string}
  494. */
  495. this.service = service;
  496. /**
  497. * The payload.
  498. * @type {string}
  499. */
  500. this.payload = payload;
  501. };
  502. /**
  503. * Converts a message to a literal object.
  504. * @return {!Object} The message as a literal object.
  505. */
  506. DirectTransport.Message_.prototype.toLiteral = function() {
  507. return {
  508. 'channelName': this.channelName,
  509. 'service': this.service,
  510. 'payload': this.payload
  511. };
  512. };
  513. /**
  514. * Creates a Message_ from a literal object.
  515. * @param {!Object} literal The literal to convert to Message.
  516. * @return {!DirectTransport.Message_} The Message.
  517. */
  518. DirectTransport.Message_.fromLiteral = function(literal) {
  519. return new DirectTransport.Message_(
  520. literal['channelName'], literal['service'], literal['payload']);
  521. };
  522. }); // goog.scope