blob: 8db650525b3e143c6cbc8795b484b760574e1206 [file] [log] [blame]
// Copyright 2015 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
* Provides a HTML5 postMessage channel to the injected JS to talk back
* to Authenticator.
*/
'use strict';
// <include src="channel.js">
const PostMessageChannel = (function() {
/**
* Allowed origins of the hosting page.
* @type {Array<string>}
*/
const ALLOWED_ORIGINS = ['chrome://oobe', 'chrome://chrome-signin'];
/** @const */
const PORT_MESSAGE = 'post-message-port-message';
/** @const */
const CHANNEL_INIT_MESSAGE = 'post-message-channel-init';
/** @const */
const CHANNEL_CONNECT_MESSAGE = 'post-message-channel-connect';
/**
* Whether the script runs in a top level window.
*/
function isTopLevel() {
return window === window.top;
}
/**
* A simple event target.
*/
function EventTarget() {
this.listeners_ = [];
}
EventTarget.prototype = {
/**
* Add an event listener.
*/
addListener: function(listener) {
this.listeners_.push(listener);
},
/**
* Dispatches a given event to all listeners.
*/
dispatch: function(e) {
for (let i = 0; i < this.listeners_.length; ++i) {
this.listeners_[i].call(undefined, e);
}
}
};
/**
* ChannelManager handles window message events by dispatching them to
* PostMessagePorts or forwarding to other windows (up/down the hierarchy).
* @constructor
*/
function ChannelManager() {
/**
* Window and origin to forward message up the hierarchy. For subframes,
* they defaults to window.parent and any origin. For top level window,
* this would be set to the hosting webview on CHANNEL_INIT_MESSAGE.
*/
this.upperWindow = isTopLevel() ? null : window.parent;
this.upperOrigin = isTopLevel() ? '' : '*';
/**
* Channle Id to port map.
* @type {Object<number, PostMessagePort>}
*/
this.channels_ = {};
/**
* Deferred messages to be posted to |upperWindow|.
* @type {Array}
*/
this.deferredUpperWindowMessages_ = [];
/**
* Ports that depend on upperWindow and need to be setup when its available.
*/
this.deferredUpperWindowPorts_ = [];
/**
* Whether the ChannelManager runs in daemon mode and accepts connections.
*/
this.isDaemon = false;
/**
* Fires when ChannelManager is in listening mode and a
* CHANNEL_CONNECT_MESSAGE is received.
*/
this.onConnect = new EventTarget();
window.addEventListener('message', this.onMessage_.bind(this));
}
ChannelManager.prototype = {
/**
* Gets a global unique id to use.
* @return {number}
*/
createChannelId_: function() {
return (new Date()).getTime();
},
/**
* Posts data to upperWindow. Queue it if upperWindow is not available.
*/
postToUpperWindow: function(data) {
if (this.upperWindow == null) {
this.deferredUpperWindowMessages_.push(data);
return;
}
this.upperWindow.postMessage(data, this.upperOrigin);
},
/**
* Creates a port and register it in |channels_|.
* @param {number} channelId
* @param {string} channelName
* @param {DOMWindow=} opt_targetWindow
* @param {string=} opt_targetOrigin
*/
createPort: function(
channelId, channelName, opt_targetWindow, opt_targetOrigin) {
const port = new PostMessagePort(channelId, channelName);
if (opt_targetWindow)
port.setTarget(opt_targetWindow, opt_targetOrigin);
this.channels_[channelId] = port;
return port;
},
/*
* Returns a message forward handler for the given proxy port.
* @private
*/
getProxyPortForwardHandler_: function(proxyPort) {
return function(msg) {
proxyPort.postMessage(msg);
};
},
/**
* Creates a forwarding porxy port.
* @param {number} channelId
* @param {string} channelName
* @param {!DOMWindow} targetWindow
* @param {!string} targetOrigin
*/
createProxyPort: function(
channelId, channelName, targetWindow, targetOrigin) {
const port =
this.createPort(channelId, channelName, targetWindow, targetOrigin);
port.onMessage.addListener(this.getProxyPortForwardHandler_(port));
return port;
},
/**
* Creates a connecting port to the daemon and request connection.
* @param {string} name
* @return {PostMessagePort}
*/
connectToDaemon: function(name) {
if (this.isDaemon) {
console.error(
'Error: Connecting from the daemon page is not supported.');
return;
}
const port = this.createPort(this.createChannelId_(), name);
if (this.upperWindow) {
port.setTarget(this.upperWindow, this.upperOrigin);
} else {
this.deferredUpperWindowPorts_.push(port);
}
this.postToUpperWindow({
type: CHANNEL_CONNECT_MESSAGE,
channelId: port.channelId,
channelName: port.name
});
return port;
},
/**
* Dispatches a 'message' event to port.
* @private
*/
dispatchMessageToPort_: function(e) {
const channelId = e.data.channelId;
const port = this.channels_[channelId];
if (!port) {
console.error('Error: Unable to dispatch message. Unknown channel.');
return;
}
port.handleWindowMessage(e);
},
/**
* Window 'message' handler.
*/
onMessage_: function(e) {
if (typeof e.data != 'object' || !e.data.hasOwnProperty('type')) {
return;
}
if (e.data.type === PORT_MESSAGE) {
// Dispatch port message to ports if this is the daemon page or
// the message is from upperWindow. In case of null upperWindow,
// the message is assumed to be forwarded to upperWindow and queued.
if (this.isDaemon ||
(this.upperWindow && e.source === this.upperWindow)) {
this.dispatchMessageToPort_(e);
} else {
this.postToUpperWindow(e.data);
}
} else if (e.data.type === CHANNEL_CONNECT_MESSAGE) {
const channelId = e.data.channelId;
const channelName = e.data.channelName;
if (this.isDaemon) {
const port =
this.createPort(channelId, channelName, e.source, e.origin);
this.onConnect.dispatch(port);
} else {
this.createProxyPort(channelId, channelName, e.source, e.origin);
this.postToUpperWindow(e.data);
}
} else if (e.data.type === CHANNEL_INIT_MESSAGE) {
if (ALLOWED_ORIGINS.indexOf(e.origin) == -1)
return;
this.upperWindow = e.source;
this.upperOrigin = e.origin;
for (let i = 0; i < this.deferredUpperWindowMessages_.length; ++i) {
this.upperWindow.postMessage(
this.deferredUpperWindowMessages_[i], this.upperOrigin);
}
this.deferredUpperWindowMessages_ = [];
for (let i = 0; i < this.deferredUpperWindowPorts_.length; ++i) {
this.deferredUpperWindowPorts_[i].setTarget(
this.upperWindow, this.upperOrigin);
}
this.deferredUpperWindowPorts_ = [];
}
}
};
/**
* Singleton instance of ChannelManager.
* @type {ChannelManager}
*/
const channelManager = new ChannelManager();
/**
* A HTML5 postMessage based port that provides the same port interface
* as the messaging API port.
* @param {number} channelId
* @param {string} name
*/
function PostMessagePort(channelId, name) {
this.channelId = channelId;
this.name = name;
this.targetWindow = null;
this.targetOrigin = '';
this.deferredMessages_ = [];
this.onMessage = new EventTarget();
}
PostMessagePort.prototype = {
/**
* Sets the target window and origin.
* @param {DOMWindow} targetWindow
* @param {string} targetOrigin
*/
setTarget: function(targetWindow, targetOrigin) {
this.targetWindow = targetWindow;
this.targetOrigin = targetOrigin;
for (let i = 0; i < this.deferredMessages_.length; ++i) {
this.postMessage(this.deferredMessages_[i]);
}
this.deferredMessages_ = [];
},
postMessage: function(msg) {
if (!this.targetWindow) {
this.deferredMessages_.push(msg);
return;
}
this.targetWindow.postMessage(
{type: PORT_MESSAGE, channelId: this.channelId, payload: msg},
this.targetOrigin);
},
handleWindowMessage: function(e) {
this.onMessage.dispatch(e.data.payload);
}
};
/**
* A message channel based on PostMessagePort.
* @extends {Channel}
* @constructor
*/
function PostMessageChannel() {
Channel.apply(this, arguments);
}
PostMessageChannel.prototype = {
__proto__: Channel.prototype,
/** @override */
connect: function(name) {
this.port_ = channelManager.connectToDaemon(name);
this.port_.onMessage.addListener(this.onMessage_.bind(this));
},
};
/**
* Initialize webview content window for postMessage channel.
* @param {DOMWindow} webViewContentWindow Content window of the webview.
*/
PostMessageChannel.init = function(webViewContentWindow) {
webViewContentWindow.postMessage({type: CHANNEL_INIT_MESSAGE}, '*');
};
/**
* Run in daemon mode and listen for incoming connections. Note that the
* current implementation assumes the daemon runs in the hosting page
* at the upper layer of the DOM tree. That is, all connect requests go
* up the DOM tree instead of going into sub frames.
* @param {function(PostMessagePort)} callback Invoked when a connection is
* made.
*/
PostMessageChannel.runAsDaemon = function(callback) {
channelManager.isDaemon = true;
const onConnect = function(port) {
callback(port);
};
channelManager.onConnect.addListener(onConnect);
};
return PostMessageChannel;
})();
/** @override */
Channel.create = function() {
return new PostMessageChannel();
};