| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 /** |
| 6 * @fileoverview |
| 7 * Provides a HTML5 postMessage channel to the injected JS to talk back |
| 8 * to Authenticator. |
| 9 */ |
| 10 'use strict'; |
| 11 |
| 12 <include src="../gaia_auth/channel.js"> |
| 13 |
| 14 var PostMessageChannel = (function() { |
| 15 /** |
| 16 * Allowed origins of the hosting page. |
| 17 * @type {Array.<string>} |
| 18 */ |
| 19 var ALLOWED_ORIGINS = [ |
| 20 'chrome://oobe', |
| 21 'chrome://chrome-signin' |
| 22 ]; |
| 23 |
| 24 /** @const */ |
| 25 var PORT_MESSAGE = 'post-message-port-message'; |
| 26 |
| 27 /** @const */ |
| 28 var CHANNEL_INIT_MESSAGE = 'post-message-channel-init'; |
| 29 |
| 30 /** @const */ |
| 31 var CHANNEL_CONNECT_MESSAGE = 'post-message-channel-connect'; |
| 32 |
| 33 /** |
| 34 * Whether the script runs in a top level window. |
| 35 */ |
| 36 function isTopLevel() { |
| 37 return window === window.top; |
| 38 } |
| 39 |
| 40 /** |
| 41 * A simple event target. |
| 42 */ |
| 43 function EventTarget() { |
| 44 this.listeners_ = []; |
| 45 } |
| 46 |
| 47 EventTarget.prototype = { |
| 48 /** |
| 49 * Add an event listener. |
| 50 */ |
| 51 addListener: function(listener) { |
| 52 this.listeners_.push(listener); |
| 53 }, |
| 54 |
| 55 /** |
| 56 * Dispatches a given event to all listeners. |
| 57 */ |
| 58 dispatch: function(e) { |
| 59 for (var i = 0; i < this.listeners_.length; ++i) { |
| 60 this.listeners_[i].call(undefined, e); |
| 61 } |
| 62 } |
| 63 }; |
| 64 |
| 65 /** |
| 66 * ChannelManager handles window message events by dispatching them to |
| 67 * PostMessagePorts or forwarding to other windows (up/down the hierarchy). |
| 68 * @constructor |
| 69 */ |
| 70 function ChannelManager() { |
| 71 /** |
| 72 * Window and origin to forward message up the hierarchy. For subframes, |
| 73 * they defaults to window.parent and any origin. For top level window, |
| 74 * this would be set to the hosting webview on CHANNEL_INIT_MESSAGE. |
| 75 */ |
| 76 this.upperWindow = isTopLevel() ? null : window.parent; |
| 77 this.upperOrigin = isTopLevel() ? '' : '*'; |
| 78 |
| 79 /** |
| 80 * Channle Id to port map. |
| 81 * @type {Object.<number, PostMessagePort>} |
| 82 */ |
| 83 this.channels_ = {}; |
| 84 |
| 85 /** |
| 86 * Deferred messages to be posted to |upperWindow|. |
| 87 * @type {Array} |
| 88 */ |
| 89 this.deferredUpperWindowMessages_ = []; |
| 90 |
| 91 /** |
| 92 * Ports that depend on upperWindow and need to be setup when its available. |
| 93 */ |
| 94 this.deferredUpperWindowPorts_ = []; |
| 95 |
| 96 /** |
| 97 * Whether the ChannelManager runs in daemon mode and accepts connections. |
| 98 */ |
| 99 this.isDaemon = false; |
| 100 |
| 101 /** |
| 102 * Fires when ChannelManager is in listening mode and a |
| 103 * CHANNEL_CONNECT_MESSAGE is received. |
| 104 */ |
| 105 this.onConnect = new EventTarget(); |
| 106 |
| 107 window.addEventListener('message', this.onMessage_.bind(this)); |
| 108 } |
| 109 |
| 110 ChannelManager.prototype = { |
| 111 /** |
| 112 * Gets a global unique id to use. |
| 113 * @return {number} |
| 114 */ |
| 115 createChannelId_: function() { |
| 116 return (new Date()).getTime(); |
| 117 }, |
| 118 |
| 119 /** |
| 120 * Posts data to upperWindow. Queue it if upperWindow is not available. |
| 121 */ |
| 122 postToUpperWindow: function(data) { |
| 123 if (this.upperWindow == null) { |
| 124 this.deferredUpperWindowMessages_.push(data); |
| 125 return; |
| 126 } |
| 127 |
| 128 this.upperWindow.postMessage(data, this.upperOrigin); |
| 129 }, |
| 130 |
| 131 /** |
| 132 * Creates a port and register it in |channels_|. |
| 133 * @param {number} channelId |
| 134 * @param {string} channelName |
| 135 * @param {DOMWindow=} opt_targetWindow |
| 136 * @param {string=} opt_targetOrigin |
| 137 */ |
| 138 createPort: function( |
| 139 channelId, channelName, opt_targetWindow, opt_targetOrigin) { |
| 140 var port = new PostMessagePort(channelId, channelName); |
| 141 if (opt_targetWindow) |
| 142 port.setTarget(opt_targetWindow, opt_targetOrigin); |
| 143 this.channels_[channelId] = port; |
| 144 return port; |
| 145 }, |
| 146 |
| 147 /* |
| 148 * Returns a message forward handler for the given proxy port. |
| 149 * @private |
| 150 */ |
| 151 getProxyPortForwardHandler_: function(proxyPort) { |
| 152 return function(msg) { proxyPort.postMessage(msg); }; |
| 153 }, |
| 154 |
| 155 /** |
| 156 * Creates a forwarding porxy port. |
| 157 * @param {number} channelId |
| 158 * @param {string} channelName |
| 159 * @param {!DOMWindow} targetWindow |
| 160 * @param {!string} targetOrigin |
| 161 */ |
| 162 createProxyPort: function( |
| 163 channelId, channelName, targetWindow, targetOrigin) { |
| 164 var port = this.createPort( |
| 165 channelId, channelName, targetWindow, targetOrigin); |
| 166 port.onMessage.addListener(this.getProxyPortForwardHandler_(port)); |
| 167 return port; |
| 168 }, |
| 169 |
| 170 /** |
| 171 * Creates a connecting port to the daemon and request connection. |
| 172 * @param {string} name |
| 173 * @return {PostMessagePort} |
| 174 */ |
| 175 connectToDaemon: function(name) { |
| 176 if (this.isDaemon) { |
| 177 console.error( |
| 178 'Error: Connecting from the daemon page is not supported.'); |
| 179 return; |
| 180 } |
| 181 |
| 182 var port = this.createPort(this.createChannelId_(), name); |
| 183 if (this.upperWindow) { |
| 184 port.setTarget(this.upperWindow, this.upperOrigin); |
| 185 } else { |
| 186 this.deferredUpperWindowPorts_.push(port); |
| 187 } |
| 188 |
| 189 this.postToUpperWindow({ |
| 190 type: CHANNEL_CONNECT_MESSAGE, |
| 191 channelId: port.channelId, |
| 192 channelName: port.name |
| 193 }); |
| 194 return port; |
| 195 }, |
| 196 |
| 197 /** |
| 198 * Dispatches a 'message' event to port. |
| 199 * @private |
| 200 */ |
| 201 dispatchMessageToPort_: function(e) { |
| 202 var channelId = e.data.channelId; |
| 203 var port = this.channels_[channelId]; |
| 204 if (!port) { |
| 205 console.error('Error: Unable to dispatch message. Unknown channel.'); |
| 206 return; |
| 207 } |
| 208 |
| 209 port.handleWindowMessage(e); |
| 210 }, |
| 211 |
| 212 /** |
| 213 * Window 'message' handler. |
| 214 */ |
| 215 onMessage_: function(e) { |
| 216 if (typeof e.data != 'object' || |
| 217 !e.data.hasOwnProperty('type')) { |
| 218 return; |
| 219 } |
| 220 |
| 221 if (e.data.type === PORT_MESSAGE) { |
| 222 // Dispatch port message to ports if this is the daemon page or |
| 223 // the message is from upperWindow. In case of null upperWindow, |
| 224 // the message is assumed to be forwarded to upperWindow and queued. |
| 225 if (this.isDaemon || |
| 226 (this.upperWindow && e.source === this.upperWindow)) { |
| 227 this.dispatchMessageToPort_(e); |
| 228 } else { |
| 229 this.postToUpperWindow(e.data); |
| 230 } |
| 231 } else if (e.data.type === CHANNEL_CONNECT_MESSAGE) { |
| 232 var channelId = e.data.channelId; |
| 233 var channelName = e.data.channelName; |
| 234 |
| 235 if (this.isDaemon) { |
| 236 var port = this.createPort( |
| 237 channelId, channelName, e.source, e.origin); |
| 238 this.onConnect.dispatch(port); |
| 239 } else { |
| 240 this.createProxyPort(channelId, channelName, e.source, e.origin); |
| 241 this.postToUpperWindow(e.data); |
| 242 } |
| 243 } else if (e.data.type === CHANNEL_INIT_MESSAGE) { |
| 244 if (ALLOWED_ORIGINS.indexOf(e.origin) == -1) |
| 245 return; |
| 246 |
| 247 this.upperWindow = e.source; |
| 248 this.upperOrigin = e.origin; |
| 249 |
| 250 for (var i = 0; i < this.deferredUpperWindowMessages_.length; ++i) { |
| 251 this.upperWindow.postMessage(this.deferredUpperWindowMessages_[i], |
| 252 this.upperOrigin); |
| 253 } |
| 254 this.deferredUpperWindowMessages_ = []; |
| 255 |
| 256 for (var i = 0; i < this.deferredUpperWindowPorts_.length; ++i) { |
| 257 this.deferredUpperWindowPorts_[i].setTarget(this.upperWindow, |
| 258 this.upperOrigin); |
| 259 } |
| 260 this.deferredUpperWindowPorts_ = []; |
| 261 } |
| 262 } |
| 263 }; |
| 264 |
| 265 /** |
| 266 * Singleton instance of ChannelManager. |
| 267 * @type {ChannelManager} |
| 268 */ |
| 269 var channelManager = new ChannelManager(); |
| 270 |
| 271 /** |
| 272 * A HTML5 postMessage based port that provides the same port interface |
| 273 * as the messaging API port. |
| 274 * @param {number} channelId |
| 275 * @param {string} name |
| 276 */ |
| 277 function PostMessagePort(channelId, name) { |
| 278 this.channelId = channelId; |
| 279 this.name = name; |
| 280 this.targetWindow = null; |
| 281 this.targetOrigin = ''; |
| 282 this.deferredMessages_ = []; |
| 283 |
| 284 this.onMessage = new EventTarget(); |
| 285 }; |
| 286 |
| 287 PostMessagePort.prototype = { |
| 288 /** |
| 289 * Sets the target window and origin. |
| 290 * @param {DOMWindow} targetWindow |
| 291 * @param {string} targetOrigin |
| 292 */ |
| 293 setTarget: function(targetWindow, targetOrigin) { |
| 294 this.targetWindow = targetWindow; |
| 295 this.targetOrigin = targetOrigin; |
| 296 |
| 297 for (var i = 0; i < this.deferredMessages_.length; ++i) { |
| 298 this.postMessage(this.deferredMessages_[i]); |
| 299 } |
| 300 this.deferredMessages_ = []; |
| 301 }, |
| 302 |
| 303 postMessage: function(msg) { |
| 304 if (!this.targetWindow) { |
| 305 this.deferredMessages_.push(msg); |
| 306 return; |
| 307 } |
| 308 |
| 309 this.targetWindow.postMessage({ |
| 310 type: PORT_MESSAGE, |
| 311 channelId: this.channelId, |
| 312 payload: msg |
| 313 }, this.targetOrigin); |
| 314 }, |
| 315 |
| 316 handleWindowMessage: function(e) { |
| 317 this.onMessage.dispatch(e.data.payload); |
| 318 } |
| 319 }; |
| 320 |
| 321 /** |
| 322 * A message channel based on PostMessagePort. |
| 323 * @extends {Channel} |
| 324 * @constructor |
| 325 */ |
| 326 function PostMessageChannel() { |
| 327 }; |
| 328 |
| 329 PostMessageChannel.prototype = { |
| 330 __proto__: Channel.prototype, |
| 331 |
| 332 /** @override */ |
| 333 connect: function(name) { |
| 334 this.port_ = channelManager.connectToDaemon(name); |
| 335 this.port_.onMessage.addListener(this.onMessage_.bind(this)); |
| 336 }, |
| 337 }; |
| 338 |
| 339 /** |
| 340 * Initialize webview content window for postMessage channel. |
| 341 * @param {DOMWindow} webViewContentWindow Content window of the webview. |
| 342 */ |
| 343 PostMessageChannel.init = function(webViewContentWindow) { |
| 344 webViewContentWindow.postMessage({ |
| 345 type: CHANNEL_INIT_MESSAGE |
| 346 }, '*'); |
| 347 }; |
| 348 |
| 349 /** |
| 350 * Run in daemon mode and listen for incoming connections. Note that the |
| 351 * current implementation assumes the daemon runs in the hosting page |
| 352 * at the upper layer of the DOM tree. That is, all connect requests go |
| 353 * up the DOM tree instead of going into sub frames. |
| 354 * @param {function(PostMessagePort)} callback Invoked when a connection is |
| 355 * made. |
| 356 */ |
| 357 PostMessageChannel.runAsDaemon = function(callback) { |
| 358 channelManager.isDaemon = true; |
| 359 |
| 360 var onConnect = function(port) { |
| 361 callback(port); |
| 362 }; |
| 363 channelManager.onConnect.addListener(onConnect); |
| 364 }; |
| 365 |
| 366 return PostMessageChannel; |
| 367 })(); |
| 368 |
| 369 /** @override */ |
| 370 Channel.create = function() { |
| 371 return new PostMessageChannel(); |
| 372 }; |
| OLD | NEW |